Last active
October 8, 2022 01:46
-
-
Save brandonhill/b0aefe09a618164fd4137a67feae5393 to your computer and use it in GitHub Desktop.
Fireworks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Firework { | |
done = false | |
exploded = false | |
particles = [] | |
constructor(ctx, gravity, x, y, vel) { | |
this.col = { h: Util.rand(360), a: Util.rand(0.5, 0.9) } | |
this.ctx = ctx | |
this.grav = gravity | |
this.vel = vel | |
this.firework = new Particle( | |
ctx, | |
gravity, | |
Util.rand(4, 5), | |
x, | |
y, | |
vel, | |
undefined, | |
this.col) | |
} | |
explode() { | |
this.exploded = true | |
const n = Util.rand(100, 200) | |
for (let i = 0; i < n; i += 1) { | |
const col = { | |
a: Util.rand(0.6, 0.8), | |
h: (this.col.h + 360 + Util.rand(-15, 15)) % 360 } | |
const vel = Vector.random() | |
vel.mult(Util.rand(1, 10)) | |
vel.x += this.vel.x * 2 | |
this.particles.push(new Particle( | |
this.ctx, | |
this.grav, | |
Util.rand(2, 5), | |
this.firework.pos.x, | |
this.firework.pos.y, | |
vel, | |
Util.rand(500, 2000), | |
col | |
)) | |
} | |
} | |
update() { | |
if (!this.exploded) { | |
this.firework.update() | |
if (this.firework.vel.y >= 0) { | |
this.explode() | |
} | |
return | |
} | |
for (let i = this.particles.length - 1; i >= 0; i -= 1) { | |
const p = this.particles[i] | |
p.vel.mult(Util.rand(0.9, 0.98)) | |
p.update() | |
// remove when done | |
if (p.done) this.particles.splice(i, 1) | |
} | |
if (!this.particles.length) { | |
this.done = true | |
} | |
} | |
} | |
class Particle { | |
done = false | |
shrink = 0.99 | |
constructor(ctx, gravity, size, x, y, vel, lifespanMs, col) { | |
this.acc = new Vector(0, 0) | |
this.col = col | |
this.ctx = ctx | |
this.grav = gravity | |
this.pos = new Vector(x, y) | |
this.size = size | |
this.vel = vel | |
if (lifespanMs) { | |
this.startMs = performance.now() | |
this.endMs = this.startMs + lifespanMs | |
} | |
} | |
update() { | |
// physics | |
if (this.endMs && performance.now() >= this.endMs) this.done = true | |
this.acc.add(this.grav) | |
this.size *= this.shrink | |
this.vel.add(this.acc) | |
this.pos.add(this.vel) | |
this.acc.mult(0) | |
// draw | |
const age = (performance.now() - this.startMs) / (this.endMs - this.startMs) | |
// twinkle after half age | |
if (this.endMs && age > 0.5 && Util.rand(1) < 0.2) return | |
this.ctx.fillStyle = Util.rgba({ ...this.col, a: this.endMs ? 1 - age + 0.1 : 1 }).toString() | |
this.ctx.beginPath() | |
this.ctx.ellipse(this.pos.x, this.pos.y, this.size / 2, this.size / 2, 0, 0, 2 * Math.PI) | |
this.ctx.fill() | |
} | |
} | |
class Util { | |
static rand(...args) { | |
if (args.length === 1) { | |
if (Array.isArray(args[0])) { | |
return args[0][Math.floor(Math.random() * (args[0].length - 1))] | |
} | |
return Math.random() * args[0] | |
} | |
const [min, max] = args | |
return Math.random() * (max - min) + min | |
} | |
static rgba({ h, s = 1, b = 1, a }) { | |
const f = n => { | |
const k = (n + h / 60) % 6 | |
return b * (1 - s * Math.max(0, Math.min(k, 4 - k, 1))) | |
} | |
return { | |
r: Math.round(255 * f(5)), | |
g: Math.round(255 * f(3)), | |
b: Math.round(255 * f(1)), | |
a, | |
toString() { | |
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})` | |
} | |
} | |
} | |
} | |
class Vector { | |
static random() { | |
const a = Math.random() * Math.PI * 2 | |
return new Vector(Math.cos(a) * 1 - 0, Math.sin(a) * 1 - 0) | |
} | |
constructor(x = 0, y = 0) { | |
this.x = x | |
this.y = y | |
} | |
add(v) { | |
this.x += v.x | |
this.y += v.y | |
} | |
mult(f) { | |
this.x *= f | |
this.y *= f | |
} | |
} | |
((d, p) => { | |
const el = d.createElement('canvas') | |
const ctx = el.getContext('2d') | |
const endMs = p.now() + 5000 | |
const fireworks = [] | |
const gravity = new Vector(0, 0.098) | |
let rate = 0.01 | |
const updateSize = () => { | |
el.height = innerHeight | |
el.width = innerWidth | |
} | |
// style | |
Object.entries({ | |
pointerEvents: 'none', | |
position: 'fixed', | |
top: 0, | |
left: 0, | |
zIndex: 10000000 | |
}).forEach(([k, v]) => el.style[k] = v) | |
d.body.appendChild(el) | |
addEventListener('resize', updateSize) | |
const loop = () => { | |
// fade out previous | |
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)' | |
const prev = ctx.globalCompositeOperation | |
ctx.globalCompositeOperation = 'destination-out' | |
ctx.fillRect(0, 0, el.width, el.height) | |
ctx.globalCompositeOperation = prev | |
// launch new | |
if (p.now() < endMs && Util.rand(1) < rate) { | |
const f = -0.1 * Math.pow(el.height, 0.7) * Util.rand(0.9, 1.1) | |
fireworks.push( | |
new Firework( | |
ctx, | |
gravity, | |
Util.rand(el.width * 0.25, el.width * 0.75), | |
el.height, | |
new Vector(Util.rand(-3, 3), f))) | |
} | |
rate *= 1.01 | |
// update | |
for (let i = fireworks.length - 1; i >= 0; i -= 1) { | |
const firework = fireworks[i] | |
firework.update() | |
// remove when done | |
if (firework.done) fireworks.splice(i, 1) | |
} | |
// clean up | |
if (p.now() > endMs && !fireworks.length) { | |
removeEventListener('resize', updateSize) | |
d.body.removeChild(el) | |
return | |
} | |
requestAnimationFrame(loop) | |
} | |
updateSize() | |
requestAnimationFrame(loop) | |
})(document, performance) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment