Skip to content

Instantly share code, notes, and snippets.

@brandonhill
Last active October 8, 2022 01:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brandonhill/b0aefe09a618164fd4137a67feae5393 to your computer and use it in GitHub Desktop.
Save brandonhill/b0aefe09a618164fd4137a67feae5393 to your computer and use it in GitHub Desktop.
Fireworks
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