Click anywhere to make an explosion.
Forked from towc's Pen Square exploder (mini-game).
A Pen by Artem Zubkov on CodePen.
Click anywhere to make an explosion.
Forked from towc's Pen Square exploder (mini-game).
A Pen by Artem Zubkov on CodePen.
<canvas id=c></canvas> | |
<div id=stats><span id=score>0</span>/<span id=outOf>0</span></div> | |
<div id=rules> | |
Click anywhere to make an explosion.<br /> | |
If any of the particles will bump into your explosion they'll explode as well, making a chain reaction.<br /> | |
Click again to start a new game!<br /> | |
Let's rock in this hole! Uhhh-ha-ha-ha! Let's do it!!!<br /> | |
<br /> | |
</div> |
"use strict"; | |
var w | |
, h | |
, amount | |
, clicked = false | |
, ctx = c.getContext("2d") | |
, inGame = false | |
, cells = [] | |
, cellsExplored = [] | |
, settings = Settings() | |
, nextInit = false | |
, killTimer | |
, expColor | |
, expImg | |
; | |
function restart() { | |
nextInit = true; | |
if (!inGame) { | |
init(); | |
} | |
} | |
function Settings() { | |
return { | |
maxSize: 6, | |
minSize: 2, | |
maxSpeed: 2, | |
speedExplosionX: 0, | |
speedExplosionY: 0, | |
firstExplosionDegree: 10, | |
stepExplosion: 3, | |
maxSizeExp: 20, | |
maxColor: 255, | |
minColor: 100, | |
composite: "lighter", | |
limitParticles: 1999, | |
channelsR: false, | |
channelsG: true, | |
channelsB: true, | |
restart: restart | |
}; | |
} | |
function getRandomColor(min) { | |
return { | |
r: settings.channelsR ? ((Math.random() * (255 - min)) | 0) + min : 0, | |
g: settings.channelsG ? ((Math.random() * (255 - min)) | 0) + min : 0, | |
b: settings.channelsB ? ((Math.random() * (255 - min)) | 0) + min : 0 | |
}; | |
} | |
/** | |
* @param {Cell} a | |
* @param {Cell} b | |
*/ | |
function checkExplosion(a, b) { | |
if (a === b || b.exploded) { | |
return; | |
} | |
var distX = a.x - b.x; | |
var distY = a.y - b.y; | |
var dist = Math.sqrt(distX * distX + distY * distY) - b.size / 2; | |
if (dist <= a.explosionSize) { | |
b.explode(); | |
} | |
} | |
function applyMask(value, mask) { | |
value = Math.abs(((value * mask) / 255) | 0); | |
return value < 256 ? value : 255; | |
} | |
/** | |
* Generate a neon ball | |
* @param {Number} w width | |
* @param {Number} h height | |
* @param {Number} r | |
* @param {Number} g | |
* @param {Number} b | |
* @param {Number} a alpha | |
* @returns {HTMLCanvasElement} | |
*/ | |
function generateFireBall(w, h, a, r, g, b) { | |
r = parseInt(r); | |
r = isNaN(r) || r > 255 ? 255 : r; | |
g = parseInt(g); | |
g = isNaN(g) || g > 255 ? 255 : g; | |
b = parseInt(b); | |
b = isNaN(b) || b > 255 ? 255 : b; | |
var tempCanvas = document.createElement("canvas"); | |
tempCanvas.width = w; | |
tempCanvas.height = h; | |
var imgCtx = tempCanvas.getContext("2d"); | |
var gradient = imgCtx.createRadialGradient( | |
w / 2, | |
h / 2, | |
0, | |
w / 2, | |
h / 2, | |
w / 2 | |
); | |
gradient.addColorStop(0, "rgba(" + [applyMask(r, 255), applyMask(g, 255), applyMask(b, 255), a] + ")"); | |
gradient.addColorStop(0.3, "rgba(" + [applyMask(r, 254), applyMask(g, 239), applyMask(b, 29), a] + ")"); | |
gradient.addColorStop(0.4, "rgba(" + [applyMask(r, 254), applyMask(g, 88), applyMask(b, 29), a] + ")"); | |
gradient.addColorStop(0.6, "rgba(" + [applyMask(r, 239), applyMask(g, 27), applyMask(b, 51), a * 0.05] + ")"); | |
gradient.addColorStop(0.88, "rgba(" + [applyMask(r, 153), applyMask(g, 10), applyMask(b, 27), a * 0.05] + ")"); | |
gradient.addColorStop(0.92, "rgba(" + [applyMask(r, 254), applyMask(g, 39), applyMask(b, 17), a * 0.1] + ")"); | |
gradient.addColorStop(0.98, "rgba(" + [applyMask(r, 254), applyMask(g, 254), applyMask(b, 183), a * 0.2] + ")"); | |
gradient.addColorStop(1, "rgba(" + [applyMask(r, 254), applyMask(g, 39), applyMask(b, 17), 0] + ")"); | |
imgCtx.fillStyle = gradient; | |
imgCtx.fillRect(0, 0, w, h); | |
return tempCanvas; | |
} | |
function Cell(size, x, y) { | |
var c = (this.color = getRandomColor(settings.minColor)); | |
this.img = canvas_utils.colorize( | |
canvas_utils.resources.images.particle, | |
c.r, //(c.r = 0) | |
c.g, | |
c.b, | |
1 | |
); | |
this.expImg = generateFireBall(64, 64, 1, 255, c.g, c.b); | |
var m = settings.maxSize > settings.minSize ? settings.maxSize : settings.minSize; | |
var mm = m === settings.maxSize ? settings.minSize : settings.maxSize; | |
this.size = size || Math.random() * (m - mm) + mm; | |
this.initSize = this.size; | |
this.x = x || Math.random() * w; | |
this.y = y || Math.random() * h; | |
this.vx = Math.random() * settings.maxSpeed * 2 - settings.maxSpeed; | |
this.vy = Math.random() * settings.maxSpeed * 2 - settings.maxSpeed; | |
this.exploded = false; | |
this.explosionSize = settings.maxSizeExp / 5; | |
this.expV = settings.stepExplosion; | |
} | |
function rand(max, min) { | |
min = min || 0; | |
return Math.random() * (max - min) + min; | |
} | |
Cell.prototype.update = function () { | |
var s = this.size * 2; | |
var s2 = s / 2; | |
if (!this.exploded) { | |
ctx.moveTo(this.x, this.y); | |
} | |
this.x += this.vx * rand(rand(5)); | |
this.y += this.vy * rand(rand(5)); | |
if (this.x < 0 || this.x > w) { | |
this.vx *= -1; | |
this.x = this.x > 0 ? w : 0; | |
} | |
if (this.y < 0 || this.y > h) { | |
this.vy *= -1; | |
this.y = this.y > 0 ? h : 0; | |
} | |
if (!this.exploded) { | |
ctx.lineTo(this.x, this.y); | |
ctx.drawImage(this.img, this.x - s2, this.y - s2, s, s); | |
return; | |
} | |
this.explosionSize += (this.expV / this.explosionSize) * 10; | |
if (this.size > 0) { | |
this.size -= 0.05; | |
} | |
if (this.explosionSize < 0) { | |
cellsExplored.splice(cellsExplored.indexOf(this), 1); | |
return; | |
} | |
if (this.explosionSize > settings.maxSizeExp * 2) { | |
this.expV *= -1; | |
this.vx *= 0; | |
this.vy *= 0; | |
} | |
if (this.now == null) { | |
cells.splice(cells.indexOf(this), 1); | |
cellsExplored.push(this); | |
} | |
s = | |
this.now && this.now-- | |
? 2 | |
: settings.firstExplosionDegree - (this.now || 0) || 1; | |
s *= this.explosionSize; | |
s2 = s / 2; | |
this.now = this.now || settings.firstExplosionDegree; | |
ctx.drawImage(this.expImg, this.x - s2, this.y - s2, s, s); | |
var l = cells.length; | |
while (l--) { | |
checkExplosion(this, cells[l]); | |
} | |
}; | |
Cell.prototype.explode = function () { | |
this.exploded = true; | |
this.vx *= settings.speedExplosionX; | |
this.vy *= settings.speedExplosionY; | |
score.textContent = parseInt(score.textContent) + 1; | |
}; | |
function click(e) { | |
if (!inGame) { | |
init(); | |
return; | |
} | |
if (clicked) { | |
nextInit = true; | |
return; | |
} | |
e = e.touches && e.touches.length ? e.touches[0] : e; | |
var cell = new Cell(settings.maxSize, e.pageX, e.pageY); | |
cells.push(cell); | |
cell.explode(); | |
clicked = true; | |
} | |
c.addEventListener("click", click, false); | |
c.addEventListener("touchstart", click, false); | |
window.addEventListener("resize", settings.restart); | |
function anim() { | |
if (nextInit) { | |
nextInit = false; | |
return init(); | |
} | |
if (inGame) { | |
window.requestAnimationFrame(anim); | |
} | |
ctx.save(); | |
ctx.globalCompositeOperation = "destination-out"; | |
ctx.fillStyle = "rgba(0, 0, 0, .2)"; | |
ctx.fillRect(0, 0, w, h); | |
ctx.globalCompositeOperation = settings.composite; | |
ctx.fillStyle = "none"; | |
ctx.strokeStyle = "#fff"; | |
ctx.beginPath(); | |
var l = cells.length; | |
while (l--) { | |
cells[l].update(); | |
} | |
ctx.stroke(); | |
l = cellsExplored.length; | |
while (l--) { | |
cellsExplored[l].update(); | |
} | |
ctx.restore(); | |
if (!cells.length && !cellsExplored.length) { | |
gameOver(); | |
} | |
} | |
function gameOver() { | |
inGame = killTimer-- > 0; | |
console.log(killTimer); | |
if (!inGame) { | |
rules.textContent = "Yo-ho-ho!!! Yyyyeah!!!"; | |
rules.classList.remove("close"); | |
} | |
} | |
function init() { | |
w = +window.innerWidth; | |
h = +window.innerHeight; | |
amount = ((w * h) / 500) | 0; | |
amount = amount > settings.limitParticles ? settings.limitParticles : amount; | |
outOf.textContent = amount + 1; | |
score.textContent = "0"; | |
c.width = w; | |
c.height = h; | |
ctx.fillStyle = "black"; | |
ctx.fillRect(0, 0, w, h); | |
cells.splice(0); | |
cellsExplored.splice(0); | |
var n = amount; | |
while (n--) { | |
cells.push(new Cell()); | |
} | |
clicked = false; | |
inGame = true; | |
if (killTimer != undefined) { | |
rules.className = "close"; | |
} | |
killTimer = 30; | |
anim(); | |
} | |
init(); | |
setTimeout(function () { | |
rules.className = "close"; | |
}, 3000); | |
function initConsole() { | |
var gui = new dat.GUI({ | |
load: JSON, | |
preset: "Default" | |
}); | |
var f = gui.addFolder("Particles"); | |
f.add(settings, "maxSize", 0, 100).listen(); | |
f.add(settings, "minSize", 0, 100).listen(); | |
f.add(settings, "maxSpeed", 0, 100).listen(); | |
f.add(settings, "limitParticles", 1, 10000).step(10).listen(); | |
f = gui.addFolder("Explosion"); | |
f.add(settings, "maxSizeExp", 0, 100).listen(); | |
f.add(settings, "stepExplosion", 1, 100).listen(); | |
f.add(settings, "speedExplosionX", 0, 100).listen(); | |
f.add(settings, "speedExplosionY", 0, 100).listen(); | |
f.add(settings, "firstExplosionDegree", 2, 100).listen(); | |
f = gui.addFolder("Colors"); | |
f.add(settings, "minColor", 0, 255).listen(); | |
f.add(settings, "channelsR").listen(); | |
f.add(settings, "channelsG").listen(); | |
f.add(settings, "channelsB").listen(); | |
gui | |
.add(settings, "composite", [ | |
"source-over", | |
"source-in", | |
"source-out", | |
"source-atop", | |
"destination-over", | |
"destination-in", | |
"destination-out", | |
"destination-atop", | |
"lighter", | |
"darker", | |
"copy", | |
"xor" | |
]) | |
.listen(); | |
gui.add(settings, "restart"); | |
gui.remember(settings); | |
} | |
initConsole(); |
<script src="https://cdn.rawgit.com/artzub/3efd4efcbd8758e4b7e5/raw/0a89298b31b293ed4825b2fc5c54453b5c3101ab/canvas_utils.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js"></script> |
body, | |
html { | |
width: 100vw; | |
height: 100vh; | |
overflow: hidden; | |
background: #000; | |
} | |
canvas { | |
position: absolute; | |
top: 0; | |
left: 0; | |
background: #000; | |
cursor: crosshair; | |
} | |
#stats { | |
color: white; | |
background-color: rgba(255, 255, 255, 0.3); | |
font-size: 20px; | |
padding: 10px; | |
position: absolute; | |
top: 0; | |
left: 0; | |
} | |
#rules { | |
opacity: 1; | |
color: white; | |
background-color: rgba(0, 0, 0, 0.7); | |
font-size: 20px; | |
position: absolute; | |
text-align: center; | |
top: 40vh; | |
width: 96vw; | |
left: 0; | |
padding: 2vw; | |
overflow: hidden; | |
max-height: 100vh; | |
max-width: 100vw; | |
} | |
#rules.close { | |
max-height: 0; | |
max-width: 0; | |
opacity: 0; | |
transition: max-height 2s, opacity 2s, max-width 0.2s 2s; | |
} |