Skip to content

Instantly share code, notes, and snippets.

@thucpn
Created July 28, 2023 17:15
Show Gist options
  • Save thucpn/0dea25dc44917ec8869d5b26b15ba624 to your computer and use it in GitHub Desktop.
Save thucpn/0dea25dc44917ec8869d5b26b15ba624 to your computer and use it in GitHub Desktop.
Canvas Fireworks

Canvas Fireworks

Click anywhere to launch fireworks. Click and drag to fire multiple fireworks. Settings can be adjusted in the top right. Be sure to try the presets. If things are running slow, try reducing reduce the partCount setting.

I recommend a full window size to work with the controls easier.

A Pen by Jack Rugile on CodePen.

License.

<div id="gui"></div>
<div id="canvas-container">
<div id="mountains2"></div>
<div id="mountains1"></div>
<div id="skyline"></div>
</div>
var Fireworks = function(){
/*=============================================================================*/
/* Utility
/*=============================================================================*/
var self = this;
var rand = function(rMi, rMa){return ~~((Math.random()*(rMa-rMi+1))+rMi);}
window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)}}();
/*=============================================================================*/
/* Initialize
/*=============================================================================*/
self.init = function(){
self.dt = 0;
self.oldTime = Date.now();
self.canvas = document.createElement('canvas');
self.canvasContainer = $('#canvas-container');
var canvasContainerDisabled = document.getElementById('canvas-container');
self.canvas.onselectstart = function() {
return false;
};
self.canvas.width = self.cw = 600;
self.canvas.height = self.ch = 400;
self.particles = [];
self.partCount = 30;
self.fireworks = [];
self.mx = self.cw/2;
self.my = self.ch/2;
self.currentHue = 170;
self.partSpeed = 5;
self.partSpeedVariance = 10;
self.partWind = 50;
self.partFriction = 5;
self.partGravity = 1;
self.hueMin = 150;
self.hueMax = 200;
self.fworkSpeed = 2;
self.fworkAccel = 4;
self.hueVariance = 30;
self.flickerDensity = 20;
self.showShockwave = false;
self.showTarget = true;
self.clearAlpha = 25;
self.canvasContainer.append(self.canvas);
self.ctx = self.canvas.getContext('2d');
self.ctx.lineCap = 'round';
self.ctx.lineJoin = 'round';
self.lineWidth = 1;
self.bindEvents();
self.canvasLoop();
self.canvas.onselectstart = function() {
return false;
};
};
/*=============================================================================*/
/* Particle Constructor
/*=============================================================================*/
var Particle = function(x, y, hue){
this.x = x;
this.y = y;
this.coordLast = [
{x: x, y: y},
{x: x, y: y},
{x: x, y: y}
];
this.angle = rand(0, 360);
this.speed = rand(((self.partSpeed - self.partSpeedVariance) <= 0) ? 1 : self.partSpeed - self.partSpeedVariance, (self.partSpeed + self.partSpeedVariance));
this.friction = 1 - self.partFriction/100;
this.gravity = self.partGravity/2;
this.hue = rand(hue-self.hueVariance, hue+self.hueVariance);
this.brightness = rand(50, 80);
this.alpha = rand(40,100)/100;
this.decay = rand(10, 50)/1000;
this.wind = (rand(0, self.partWind) - (self.partWind/2))/25;
this.lineWidth = self.lineWidth;
};
Particle.prototype.update = function(index){
var radians = this.angle * Math.PI / 180;
var vx = Math.cos(radians) * this.speed;
var vy = Math.sin(radians) * this.speed + this.gravity;
this.speed *= this.friction;
this.coordLast[2].x = this.coordLast[1].x;
this.coordLast[2].y = this.coordLast[1].y;
this.coordLast[1].x = this.coordLast[0].x;
this.coordLast[1].y = this.coordLast[0].y;
this.coordLast[0].x = this.x;
this.coordLast[0].y = this.y;
this.x += vx * self.dt;
this.y += vy * self.dt;
this.angle += this.wind;
this.alpha -= this.decay;
if(this.alpha < .05){
self.particles.splice(index, 1);
}
};
Particle.prototype.draw = function(){
var coordRand = (rand(1,3)-1);
self.ctx.beginPath();
self.ctx.moveTo(Math.round(this.coordLast[coordRand].x), Math.round(this.coordLast[coordRand].y));
self.ctx.lineTo(Math.round(this.x), Math.round(this.y));
self.ctx.closePath();
self.ctx.strokeStyle = 'hsla('+this.hue+', 100%, '+this.brightness+'%, '+this.alpha+')';
self.ctx.stroke();
if(self.flickerDensity > 0){
var inverseDensity = 50 - self.flickerDensity;
if(rand(0, inverseDensity) === inverseDensity){
self.ctx.beginPath();
self.ctx.arc(Math.round(this.x), Math.round(this.y), rand(this.lineWidth,this.lineWidth+3)/2, 0, Math.PI*2, false)
self.ctx.closePath();
var randAlpha = rand(50,100)/100;
self.ctx.fillStyle = 'hsla('+this.hue+', 100%, '+this.brightness+'%, '+randAlpha+')';
self.ctx.fill();
}
}
};
/*=============================================================================*/
/* Create Particles
/*=============================================================================*/
self.createParticles = function(x,y, hue){
var countdown = self.partCount;
while(countdown--){
self.particles.push(new Particle(x, y, hue));
}
};
/*=============================================================================*/
/* Update Particles
/*=============================================================================*/
self.updateParticles = function(){
var i = self.particles.length;
while(i--){
var p = self.particles[i];
p.update(i);
};
};
/*=============================================================================*/
/* Draw Particles
/*=============================================================================*/
self.drawParticles = function(){
var i = self.particles.length;
while(i--){
var p = self.particles[i];
p.draw();
};
};
/*=============================================================================*/
/* Firework Constructor
/*=============================================================================*/
var Firework = function(startX, startY, targetX, targetY){
this.x = startX;
this.y = startY;
this.startX = startX;
this.startY = startY;
this.hitX = false;
this.hitY = false;
this.coordLast = [
{x: startX, y: startY},
{x: startX, y: startY},
{x: startX, y: startY}
];
this.targetX = targetX;
this.targetY = targetY;
this.speed = self.fworkSpeed;
this.angle = Math.atan2(targetY - startY, targetX - startX);
this.shockwaveAngle = Math.atan2(targetY - startY, targetX - startX)+(90*(Math.PI/180));
this.acceleration = self.fworkAccel/100;
this.hue = self.currentHue;
this.brightness = rand(50, 80);
this.alpha = rand(50,100)/100;
this.lineWidth = self.lineWidth;
this.targetRadius = 1;
};
Firework.prototype.update = function(index){
self.ctx.lineWidth = this.lineWidth;
vx = Math.cos(this.angle) * this.speed,
vy = Math.sin(this.angle) * this.speed;
this.speed *= 1 + this.acceleration;
this.coordLast[2].x = this.coordLast[1].x;
this.coordLast[2].y = this.coordLast[1].y;
this.coordLast[1].x = this.coordLast[0].x;
this.coordLast[1].y = this.coordLast[0].y;
this.coordLast[0].x = this.x;
this.coordLast[0].y = this.y;
if(self.showTarget){
if(this.targetRadius < 8){
this.targetRadius += .25 * self.dt;
} else {
this.targetRadius = 1 * self.dt;
}
}
if(this.startX >= this.targetX){
if(this.x + vx <= this.targetX){
this.x = this.targetX;
this.hitX = true;
} else {
this.x += vx * self.dt;
}
} else {
if(this.x + vx >= this.targetX){
this.x = this.targetX;
this.hitX = true;
} else {
this.x += vx * self.dt;
}
}
if(this.startY >= this.targetY){
if(this.y + vy <= this.targetY){
this.y = this.targetY;
this.hitY = true;
} else {
this.y += vy * self.dt;
}
} else {
if(this.y + vy >= this.targetY){
this.y = this.targetY;
this.hitY = true;
} else {
this.y += vy * self.dt;
}
}
if(this.hitX && this.hitY){
var randExplosion = rand(0, 9);
self.createParticles(this.targetX, this.targetY, this.hue);
self.fireworks.splice(index, 1);
}
};
Firework.prototype.draw = function(){
self.ctx.lineWidth = this.lineWidth;
var coordRand = (rand(1,3)-1);
self.ctx.beginPath();
self.ctx.moveTo(Math.round(this.coordLast[coordRand].x), Math.round(this.coordLast[coordRand].y));
self.ctx.lineTo(Math.round(this.x), Math.round(this.y));
self.ctx.closePath();
self.ctx.strokeStyle = 'hsla('+this.hue+', 100%, '+this.brightness+'%, '+this.alpha+')';
self.ctx.stroke();
if(self.showTarget){
self.ctx.save();
self.ctx.beginPath();
self.ctx.arc(Math.round(this.targetX), Math.round(this.targetY), this.targetRadius, 0, Math.PI*2, false)
self.ctx.closePath();
self.ctx.lineWidth = 1;
self.ctx.stroke();
self.ctx.restore();
}
if(self.showShockwave){
self.ctx.save();
self.ctx.translate(Math.round(this.x), Math.round(this.y));
self.ctx.rotate(this.shockwaveAngle);
self.ctx.beginPath();
self.ctx.arc(0, 0, 1*(this.speed/5), 0, Math.PI, true);
self.ctx.strokeStyle = 'hsla('+this.hue+', 100%, '+this.brightness+'%, '+rand(25, 60)/100+')';
self.ctx.lineWidth = this.lineWidth;
self.ctx.stroke();
self.ctx.restore();
}
};
/*=============================================================================*/
/* Create Fireworks
/*=============================================================================*/
self.createFireworks = function(startX, startY, targetX, targetY){
self.fireworks.push(new Firework(startX, startY, targetX, targetY));
};
/*=============================================================================*/
/* Update Fireworks
/*=============================================================================*/
self.updateFireworks = function(){
var i = self.fireworks.length;
while(i--){
var f = self.fireworks[i];
f.update(i);
};
};
/*=============================================================================*/
/* Draw Fireworks
/*=============================================================================*/
self.drawFireworks = function(){
var i = self.fireworks.length;
while(i--){
var f = self.fireworks[i];
f.draw();
};
};
/*=============================================================================*/
/* Events
/*=============================================================================*/
self.bindEvents = function(){
$(window).on('resize', function(){
clearTimeout(self.timeout);
self.timeout = setTimeout(function() {
self.ctx.lineCap = 'round';
self.ctx.lineJoin = 'round';
}, 100);
});
$(self.canvas).on('mousedown', function(e){
var randLaunch = rand(0, 5);
self.mx = e.pageX - self.canvasContainer.offset().left;
self.my = e.pageY - self.canvasContainer.offset().top;
self.currentHue = rand(self.hueMin, self.hueMax);
self.createFireworks(self.cw/2, self.ch, self.mx, self.my);
$(self.canvas).on('mousemove.fireworks', function(e){
var randLaunch = rand(0, 5);
self.mx = e.pageX - self.canvasContainer.offset().left;
self.my = e.pageY - self.canvasContainer.offset().top;
self.currentHue = rand(self.hueMin, self.hueMax);
self.createFireworks(self.cw/2, self.ch, self.mx, self.my);
});
});
$(self.canvas).on('mouseup', function(e){
$(self.canvas).off('mousemove.fireworks');
});
}
/*=============================================================================*/
/* Clear Canvas
/*=============================================================================*/
self.clear = function(){
self.particles = [];
self.fireworks = [];
self.ctx.clearRect(0, 0, self.cw, self.ch);
};
/*=============================================================================*/
/* Delta
/*=============================================================================*/
self.updateDelta = function(){
var newTime = Date.now();
self.dt = (newTime - self.oldTime)/16;
self.dt = (self.dt > 5) ? 5 : self.dt;
self.oldTime = newTime;
}
/*=============================================================================*/
/* Main Loop
/*=============================================================================*/
self.canvasLoop = function(){
requestAnimFrame(self.canvasLoop, self.canvas);
self.updateDelta();
self.ctx.globalCompositeOperation = 'destination-out';
self.ctx.fillStyle = 'rgba(0,0,0,'+self.clearAlpha/100+')';
self.ctx.fillRect(0,0,self.cw,self.ch);
self.ctx.globalCompositeOperation = 'lighter';
self.updateFireworks();
self.updateParticles();
self.drawFireworks();
self.drawParticles();
};
self.init();
var initialLaunchCount = 10;
while(initialLaunchCount--){
setTimeout(function(){
self.fireworks.push(new Firework(self.cw/2, self.ch, rand(50, self.cw-50), rand(50, self.ch/2)-50));
}, initialLaunchCount*200);
}
}
/*=============================================================================*/
/* GUI
/*=============================================================================*/
var guiPresets = {
"preset": "Default",
"remembered": {
"Default": {
"0": {
"fworkSpeed": 2,
"fworkAccel": 4,
"showShockwave": false,
"showTarget": true,
"partCount": 30,
"partSpeed": 5,
"partSpeedVariance": 10,
"partWind": 50,
"partFriction": 5,
"partGravity": 1,
"flickerDensity": 20,
"hueMin": 150,
"hueMax": 200,
"hueVariance": 30,
"lineWidth": 1,
"clearAlpha": 25
}
},
"Anti Gravity": {
"0": {
"fworkSpeed": 4,
"fworkAccel": 10,
"showShockwave": true,
"showTarget": false,
"partCount": 150,
"partSpeed": 5,
"partSpeedVariance": 10,
"partWind": 10,
"partFriction": 10,
"partGravity": -10,
"flickerDensity": 30,
"hueMin": 0,
"hueMax": 360,
"hueVariance": 30,
"lineWidth": 1,
"clearAlpha": 50
}
},
"Battle Field": {
"0": {
"fworkSpeed": 10,
"fworkAccel": 20,
"showShockwave": true,
"showTarget": true,
"partCount": 200,
"partSpeed": 30,
"partSpeedVariance": 5,
"partWind": 0,
"partFriction": 5,
"partGravity": 0,
"flickerDensity": 0,
"hueMin": 20,
"hueMax": 30,
"hueVariance": 10,
"lineWidth": 1,
"clearAlpha": 40
}
},
"Mega Blast": {
"0": {
"fworkSpeed": 3,
"fworkAccel": 3,
"showShockwave": true,
"showTarget": true,
"partCount": 500,
"partSpeed": 50,
"partSpeedVariance": 5,
"partWind": 0,
"partFriction": 0,
"partGravity": 0,
"flickerDensity": 0,
"hueMin": 0,
"hueMax": 360,
"hueVariance": 30,
"lineWidth": 20,
"clearAlpha": 20
}
},
"Nimble": {
"0": {
"fworkSpeed": 10,
"fworkAccel": 50,
"showShockwave": false,
"showTarget": false,
"partCount": 120,
"partSpeed": 10,
"partSpeedVariance": 10,
"partWind": 100,
"partFriction": 50,
"partGravity": 0,
"flickerDensity": 20,
"hueMin": 0,
"hueMax": 360,
"hueVariance": 30,
"lineWidth": 1,
"clearAlpha": 80
}
},
"Slow Launch": {
"0": {
"fworkSpeed": 2,
"fworkAccel": 2,
"showShockwave": false,
"showTarget": false,
"partCount": 200,
"partSpeed": 10,
"partSpeedVariance": 0,
"partWind": 100,
"partFriction": 0,
"partGravity": 2,
"flickerDensity": 50,
"hueMin": 0,
"hueMax": 360,
"hueVariance": 20,
"lineWidth": 4,
"clearAlpha": 10
}
},
"Perma Trail": {
"0": {
"fworkSpeed": 4,
"fworkAccel": 10,
"showShockwave": false,
"showTarget": false,
"partCount": 150,
"partSpeed": 10,
"partSpeedVariance": 10,
"partWind": 100,
"partFriction": 3,
"partGravity": 0,
"flickerDensity": 0,
"hueMin": 0,
"hueMax": 360,
"hueVariance": 20,
"lineWidth": 1,
"clearAlpha": 0
}
}
},
"closed": true,
"folders": {
"Fireworks": {
"preset": "Default",
"closed": false,
"folders": {}
},
"Particles": {
"preset": "Default",
"closed": true,
"folders": {}
},
"Color": {
"preset": "Default",
"closed": true,
"folders": {}
},
"Other": {
"preset": "Default",
"closed": true,
"folders": {}
}
}
};
var fworks = new Fireworks();
var gui = new dat.GUI({
autoPlace: false,
load: guiPresets,
preset: 'Default'
});
var customContainer = document.getElementById('gui');
customContainer.appendChild(gui.domElement);
var guiFireworks = gui.addFolder('Fireworks');
guiFireworks.add(fworks, 'fworkSpeed').min(1).max(10).step(1);
guiFireworks.add(fworks, 'fworkAccel').min(0).max(50).step(1);
guiFireworks.add(fworks, 'showShockwave');
guiFireworks.add(fworks, 'showTarget');
var guiParticles = gui.addFolder('Particles');
guiParticles.add(fworks, 'partCount').min(0).max(500).step(1);
guiParticles.add(fworks, 'partSpeed').min(1).max(100).step(1);
guiParticles.add(fworks, 'partSpeedVariance').min(0).max(50).step(1);
guiParticles.add(fworks, 'partWind').min(0).max(100).step(1);
guiParticles.add(fworks, 'partFriction').min(0).max(50).step(1);
guiParticles.add(fworks, 'partGravity').min(-20).max(20).step(1);
guiParticles.add(fworks, 'flickerDensity').min(0).max(50).step(1);
var guiColor = gui.addFolder('Color');
guiColor.add(fworks, 'hueMin').min(0).max(360).step(1);
guiColor.add(fworks, 'hueMax').min(0).max(360).step(1);
guiColor.add(fworks, 'hueVariance').min(0).max(180).step(1);
var guiOther = gui.addFolder('Other');
guiOther.add(fworks, 'lineWidth').min(1).max(20).step(1);
guiOther.add(fworks, 'clearAlpha').min(0).max(100).step(1);
guiOther.add(fworks, 'clear').name('Clear');
gui.remember(fworks);
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://jackrugile.com/lab/fireworks-v2/js/dat.gui.min.js"></script>
html, body {
margin: 0;
padding: 0;
}
body {
background: #171717;
color: #999;
font: 100%/18px helvetica, arial, sans-serif;
}
a {
color: #2fa1d6;
font-weight: bold;
text-decoration: none;
}
a:hover {
color: #fff;
}
#canvas-container {
background: #000 url(https://jackrugile.com/lab/fireworks-v2/images/bg.jpg);
height: 400px;
left: 50%;
margin: -200px 0 0 -300px;
position: absolute;
top: 50%;
width: 600px;
z-index: 2;
}
canvas {
cursor: crosshair;
display: block;
position: relative;
z-index: 3;
}
canvas:active {
cursor: crosshair;
}
#skyline {
background: url(https://jackrugile.com/lab/fireworks-v2/images/skyline.png) repeat-x 50% 0;
bottom: 0;
height: 135px;
left: 0;
position: absolute;
width: 100%;
z-index: 1;
}
#mountains1 {
background: url(https://jackrugile.com/lab/fireworks-v2/images/mountains1.png) repeat-x 40% 0;
bottom: 0;
height: 200px;
left: 0;
position: absolute;
width: 100%;
z-index: 1;
}
#mountains2 {
background: url(https://jackrugile.com/lab/fireworks-v2/images/mountains2.png) repeat-x 30% 0;
bottom: 0;
height: 250px;
left: 0;
position: absolute;
width: 100%;
z-index: 1;
}
#gui {
right: 0;
position: fixed;
top: 0;
z-index: 3;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment