Skip to content

Instantly share code, notes, and snippets.

@seangeleno
Created April 29, 2024 01:06
Show Gist options
  • Save seangeleno/dc75b55befabb8dfd5a9bfd16e80657b to your computer and use it in GitHub Desktop.
Save seangeleno/dc75b55befabb8dfd5a9bfd16e80657b to your computer and use it in GitHub Desktop.
Explosion. Сhain Reaction.
<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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment