To celebrate an important event, I hastily cobbled together an HTML canvas-based animation.
A few concerns I have include:
- Performance: Does it run reasonably smoothly on most modern machines? How can I make it more efficient?
- Portability / Compatibility: Does it work correctly on all modern browsers (excluding old versions of Internet Explorer)?
- Modelling: Is this a good way to simulate fireworks? Is there anything I could do to enhance the realism?
While we normally don't say "thanks" on Stack Exchange questions, I'd like to break that rule right now and say a big Thank you! to all members of the Code Review community.
function animate(selector) {
var $canvas = $(selector);
var width = $canvas.innerWidth();
var height = $canvas.innerHeight();
/* Based on https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL */
/* hue ∈ [0, 2π), saturation ∈ [0, 1], lightness ∈ [0, 1] */
var fromHSL = function fromHSL(hue, saturation, lightness) {
var c = (1 - Math.abs(2 * lightness - 1)) * saturation;
var h = 3 * hue / Math.PI;
var x = c * (1 - (h % 2 - 1));
var r1 = (h < 1 || 5 <= h) ? c
: (h < 2 || 4 <= h) ? x
: 0;
var g1 = (1 <= h && h < 3) ? c
: (h < 4) ? x
: 0;
var b1 = (3 <= h && h < 5) ? c
: (2 <= h) ? x
: 0;
var m = lightness - c / 2;
var r = Math.floor(256 * (r1 + m));
var g = Math.floor(256 * (g1 + m));
var b = Math.floor(256 * (b1 + m));
/*
console.log('hsl(' + hue + ', ' + saturation + ', ' + lightness +
') = rgb(' + r + ', ' + g + ', ' + b + ')');
*/
return 'rgb(' + r + ', ' + g + ', ' + b + ')';
};
var fireworksFactory = function fireworksFactory() {
var centerX = (0.2 + 0.6 * Math.random()) * width;
var centerY = (0.1 + 0.4 * Math.random()) * height;
var color = fromHSL(2 * Math.PI * Math.random(), Math.random(), 0.9);
return new Firework(centerX, centerY, color);
};
var fireworks = [fireworksFactory()];
var animation = new Animation($canvas, fireworks, fireworksFactory);
animation.start();
return animation;
}
function fillBanner(selector) {
$(selector).text(atob('SGFwcHkgZ3JhZHVhdGlvbiwgQ29kZSBSZXZpZXchIENvbmdyYXR1bGF0aW9ucyE='));
}
//////////////////////////////////////////////////////////////////////
function Animation($canvas, objects, factory) {
this.canvas = $canvas.get(0);
this.canvasContext = this.canvas.getContext('2d');
this.objects = objects;
this.factory = factory;
}
Animation.prototype.start = function start() {
var canvas = this.canvas;
var context = this.canvasContext;
var objects = this.objects;
var factory = this.factory;
var redraw = function redraw() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var f = objects.length - 1; f >= 0; f--) {
var particles = objects[f].particles;
for (var p = particles.length - 1; p >= 0; p--) {
var particle = particles[p];
context.beginPath();
context.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI, false);
context.fillStyle = particle.color;
context.fill();
}
objects[f].update();
}
};
var launch = function launch() {
objects.push(factory());
while (objects.length > 4) {
objects.shift();
}
};
this.redrawInterval = setInterval(redraw, 25 /* ms */);
this.factoryInterval = setInterval(launch, 1500 /* ms */);
}
Animation.prototype.stop = function stop() {
clearInterval(this.redrawInterval);
clearInterval(this.factoryInterval);
}
//////////////////////////////////////////////////////////////////////
function Firework(centerX, centerY, color) {
this.centerX = centerX;
this.centerY = centerY;
this.color = color;
this.particles = new Array(500);
this.Δr = 20;
this.age = 0;
var τ = 2 * Math.PI;
for (var i = 0; i < this.particles.length; i++) {
this.particles[i] = new Particle(
this.centerX, this.centerY,
/* r= */ 0, /* θ= */ τ * Math.random(), /* φ= */ τ * Math.random(),
/* size= */ 2, color
);
}
}
Firework.prototype.update = function update() {
for (var i = 0; i < this.particles.length; i++) {
this.particles[i].r += this.Δr;
this.particles[i].recalcCartesianProjection();
this.Δr -= 0.00005 * this.Δr * this.Δr; // Air resist
this.particles[i].y += 0.00000008 * this.age * this.age; // Gravity
this.particles[i].size *= 0.98; // Fade
this.age++;
}
};
//////////////////////////////////////////////////////////////////////
function Particle(x, y, r, θ, φ, size, color) {
this.origX = x;
this.origY = y;
this.r = r;
this.sinθ = Math.sin(θ);
// this.cosθ = Math.cos(θ); // Not needed
this.sinφ = Math.sin(φ);
this.cosφ = Math.cos(φ);
this.size = size;
this.color = color;
this.recalcCartesianProjection();
}
Particle.prototype.recalcCartesianProjection = function() {
this.x = this.origX + this.r * this.sinθ * this.cosφ;
this.y = this.origY + this.r * this.sinθ * this.sinφ;
};
canvas {
background: black;
background: linear-gradient(to bottom, black, rgba(0,0,99,0) 400%);
}
div.marquee {
white-space: nowrap;
position: absolute;
top: 60px;
-webkit-animation: flyby 15s linear infinite;
animation: flyby 15s linear infinite;
}
@-webkit-keyframes flyby {
from {
left: 640px;
}
to {
left: -640px;
}
}
@keyframes flyby {
from {
left: 640px;
}
to {
left: -640px;
}
}
div.marquee img {
display: inline-block;
}
div.marquee div {
display: inline-block;
position: relative;
top: -0.8em;
font: small-caps bold 18px Optima, Futura, sans-serif;
background: orange;
padding: 2px 10px;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Animation</title>
<link rel="stylesheet" type="text/css" href="celebrate.css">
</head>
<body>
<div id="viewport" style="width: 640px; height: 480px;">
<canvas id="sky" width="640" height="480"></canvas>
<!-- Based on public domain image
https://pixabay.com/en/aeroplane-aircraft-airplane-flight-161999/ -->
<div class="marquee">
<img src="https://i.stack.imgur.com/bGZ1m.png" width="80" height="43">
<div id="banner">Using an incompatible browser? No celebration for you.</div>
</div>
</div>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="celebrate.js"></script>
<script type="text/javascript">
$(function() {
fillBanner('#banner');
var anim = animate('#sky');
setTimeout(function() { anim.stop(); }, 60000);
});
</script>
</body>
</html>