(function () { 'use strict' var COUNT = 20 var MAX_RADIUS = 3 var PALETTE = [ [255, 255, 230], [255, 245, 200], [255, 230, 180], [220, 255, 220], [200, 230, 255], ] var canvas, ctx, w, h, flies, id, running function rand(a, b) { return a + Math.random() * (b - a) } function init() { canvas = document.createElement('canvas') canvas.id = 'fireflies-canvas' canvas.setAttribute('aria-hidden', 'true') document.body.prepend(canvas) ctx = canvas.getContext('2d') resize() spawn() running = true tick() addEventListener('resize', onResize) document.addEventListener('visibilitychange', onVisibility) } function resize() { w = canvas.width = innerWidth h = canvas.height = innerHeight } function onResize() { resize() if (!flies) return for (var i = 0; i < flies.length; i++) { var f = flies[i] if (f.x > w) f.x = w * 0.9 if (f.y > h) f.y = h * 0.9 } } function spawn() { flies = [] for (var i = 0; i < COUNT; i++) flies.push(makeFly()) } function makeFly() { var c = PALETTE[Math.floor(Math.random() * PALETTE.length)] return { x: rand(0, w), y: rand(0, h), vx: rand(-0.08, 0.08), vy: rand(-0.08, 0.08), r: rand(1.5, MAX_RADIUS), cr: c[0], cg: c[1], cb: c[2], a: rand(0.15, 0.35), phase: rand(0, 6.2832), flashSpeed: rand(0.002, 0.006), wanderPhase: rand(0, 6.2832), wanderSpeed: rand(0.002, 0.008), driftX: rand(-0.002, 0.002), driftY: rand(-0.002, 0.002), } } function tick() { if (!running) return update() draw() id = requestAnimationFrame(tick) } function update() { for (var i = 0; i < flies.length; i++) { var f = flies[i] f.wanderPhase += f.wanderSpeed var angle = Math.sin(f.wanderPhase) * 1.5 f.vx += Math.cos(angle) * 0.004 + f.driftX f.vy += Math.sin(angle) * 0.004 + f.driftY f.vx *= 0.98 f.vy *= 0.98 var sp = Math.sqrt(f.vx * f.vx + f.vy * f.vy) if (sp > 0.3) { f.vx = (f.vx / sp) * 0.3 f.vy = (f.vy / sp) * 0.3 } f.x += f.vx f.y += f.vy if (f.x < -40) f.x = w + 40 if (f.x > w + 40) f.x = -40 if (f.y < -40) f.y = h + 40 if (f.y > h + 40) f.y = -40 } } function draw() { ctx.clearRect(0, 0, w, h) if (matchMedia('(prefers-reduced-motion: reduce)').matches) return for (var i = 0; i < flies.length; i++) { var f = flies[i] f.phase += f.flashSpeed var pulse = 0.5 + Math.sin(f.phase) * 0.5 var alpha = f.a * pulse if (alpha < 0.01) continue ctx.save() var glowR = f.r * 4 var g = ctx.createRadialGradient(f.x, f.y, 0, f.x, f.y, glowR) g.addColorStop(0, 'rgba(' + f.cr + ',' + f.cg + ',' + f.cb + ',' + alpha + ')') g.addColorStop(0.15, 'rgba(' + f.cr + ',' + f.cg + ',' + f.cb + ',' + (alpha * 0.25) + ')') g.addColorStop(1, 'rgba(' + f.cr + ',' + f.cg + ',' + f.cb + ',0)') ctx.beginPath() ctx.arc(f.x, f.y, glowR, 0, 6.2832) ctx.fillStyle = g ctx.fill() var coreR = f.r * 1.2 var g2 = ctx.createRadialGradient(f.x, f.y, 0, f.x, f.y, coreR) g2.addColorStop(0, 'rgba(' + f.cr + ',' + f.cg + ',' + f.cb + ',' + (alpha * 0.85) + ')') g2.addColorStop(1, 'rgba(' + f.cr + ',' + f.cg + ',' + f.cb + ',0)') ctx.beginPath() ctx.arc(f.x, f.y, coreR, 0, 6.2832) ctx.fillStyle = g2 ctx.fill() ctx.restore() } } function onVisibility() { if (document.hidden) { running = false if (id) { cancelAnimationFrame(id); id = null } } else { running = true if (!id) tick() } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init) } else { init() } })()