// ── Sprinkle background ──────────────────────────── (function () { const canvas = document.getElementById('bgCanvas'); const ctx = canvas.getContext('2d'); const COLORS = ['#ffd84d', '#8cbcff', '#ff9fca', '#a9e76c', '#b99cff', '#ff9d57']; const INK = '#171717'; const COUNT = 1000; // number of sprinkles const MIN_LEN = 12; // px const MAX_LEN = 18; // px const MIN_W = 4; const MAX_W = 6; let W, H, sprinkles; function rand(a, b) { return a + Math.random() * (b - a); } function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; if (!sprinkles) init(); // Redistribute any sprinkles that landed outside new bounds sprinkles.forEach(s => { if (s.x > W) s.x = rand(0, W); if (s.y > H) s.y = rand(0, H); }); // Resizing clears the canvas; with no animation loop running, repaint it if (reduceMotion.matches) { cancelAnimationFrame(rafId); rafId = requestAnimationFrame(draw); } } function init() { sprinkles = []; for (let i = 0; i < COUNT; i++) { sprinkles.push(makeSprinkle()); } } function makeSprinkle() { return { x: rand(0, W), y: rand(0, H), len: rand(MIN_LEN, MAX_LEN), width: rand(MIN_W, MAX_W), color: COLORS[Math.floor(Math.random() * COLORS.length)], angle: rand(0, Math.PI * 2), // very slow drift vx: rand(-0.1, 0.1), vy: rand(-0.1, 0.1), va: rand(-0.005, 0.005), // angular velocity }; } function draw() { ctx.clearRect(0, 0, W, H); ctx.lineCap = 'round'; sprinkles.forEach(s => { // Endpoints computed directly; no canvas transform per sprinkle const ex = Math.sin(s.angle) * s.len / 2; const ey = -Math.cos(s.angle) * s.len / 2; ctx.beginPath(); ctx.moveTo(s.x + ex, s.y + ey); ctx.lineTo(s.x - ex, s.y - ey); // Ink outline under the colored fill (sticker look): same path, two strokes ctx.strokeStyle = INK; ctx.lineWidth = s.width + 3; ctx.stroke(); ctx.strokeStyle = s.color; ctx.lineWidth = s.width; ctx.stroke(); // Reduced motion: sprinkles stay where they are if (reduceMotion.matches) return; // Drift s.x += s.vx; s.y += s.vy; if (s.vx > 0.1 || s.vx < -0.1) { s.vx *= 0.98; } if (s.vy > 0.1 || s.vy < -0.1) { s.vy *= 0.98; } s.angle += s.va; // Repel from cursor const dx = s.x - mouseX; const dy = s.y - mouseY; const dist = Math.sqrt(dx * dx + dy * dy); const repelRadius = 80; if (dist < repelRadius && dist > 0) { const force = (repelRadius - dist) / repelRadius; s.vx += (dx / dist) * force * 1.0; s.vy += (dy / dist) * force * 1.0; } // Wrap: when a sprinkle drifts off the screen, reappear on the other side if (s.y < -MAX_LEN) s.y = H + MAX_LEN; if (s.y > H + MAX_LEN) s.y = -MAX_LEN if (s.x < -MAX_LEN) s.x = W + MAX_LEN; if (s.x > W + MAX_LEN) s.x = -MAX_LEN; }); if (!reduceMotion.matches) rafId = requestAnimationFrame(draw); } // With reduced motion the sprinkles render once as a static backdrop const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); let rafId; reduceMotion.addEventListener('change', () => { cancelAnimationFrame(rafId); rafId = requestAnimationFrame(draw); }); window.addEventListener('resize', resize, { passive: true }); resize(); let mouseX = -9999, mouseY = -9999; window.addEventListener('mousemove', (e) => { mouseX = e.clientX; mouseY = e.clientY; }, { passive: true }); rafId = requestAnimationFrame(draw); })();