prototype: confetti goes full-screen and full-spectrum (#359)
The polite puff off the card becomes a room-wide celebration: two corner cannons launch pieces up and inward so the arcs cross mid-screen, while a sky-wide rain falls across the whole viewport, staggered over ~1.8s so it keeps coming (~245 pieces total). Colours are random fully-saturated hues (the whole wheel) instead of the four desk colours. Pieces flutter sideways and "tumble" (width breathes with the flutter so they catch the light), with a terminal velocity so they drift down like paper rather than accelerating like hail.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+55
−24
prototypes/space-lever.html+55 −24
diff --git a/prototypes/space-lever.html b/prototypes/space-lever.html| index 506eaa6..b517029 100644 |
| --- a/prototypes/space-lever.html |
| +++ b/prototypes/space-lever.html |
| @@ -271,43 +271,74 @@ function stepDust(level) { |
| } |
| // ---- confetti (the "space is ready" payoff) ------------------------------------- |
| -// Bursts from the card's top edge the moment building finishes — the rattle cuts |
| -// out and the room throws paper. Same canvas as the dust. |
| +// The rattle cuts out and the whole room celebrates: two cannons from the bottom |
| +// corners arc pieces right across the viewport, while a sky-wide rain falls for |
| +// a couple of seconds. Full-spectrum colours (random saturated hues), paper |
| +// flutter, on the same canvas as the dust. |
| +const CONFETTI_GRAVITY = 0.18; // floatier than the dust — it's paper, not grit |
| let confetti = []; |
| +function confettiPiece(over) { |
| + return { |
| + delay: 0, t: 0, life: 3800, |
| + vx: 0, vy: 0, |
| + w: 5 + Math.random() * 6, |
| + h: 3 + Math.random() * 5, |
| + rot: Math.random() * Math.PI, |
| + vr: (Math.random() - 0.5) * 0.35, |
| + phase: Math.random() * Math.PI * 2, // per-piece flutter offset |
| + c: `hsl(${(Math.random() * 360) | 0} 92% ${52 + Math.random() * 14}%)`, |
| + ...over, |
| + }; |
| +} |
| function spawnConfetti() { |
| if (reduced || !dust) return; |
| - const r = card.getBoundingClientRect(); |
| - const css = getComputedStyle(document.documentElement); |
| - const colors = ["--accent", "--led-g", "--led-a", "--led-r"].map((v) => css.getPropertyValue(v).trim()); |
| - colors.push("#f2b705"); // the star gold |
| - confetti = Array.from({ length: 140 }, () => ({ |
| - x: r.left + Math.random() * r.width, |
| - y: r.top + 6, |
| - vx: (Math.random() - 0.5) * 9, |
| - vy: -(5 + Math.random() * 9), |
| - w: 4 + Math.random() * 5, |
| - h: 3 + Math.random() * 4, |
| - rot: Math.random() * Math.PI, |
| - vr: (Math.random() - 0.5) * 0.3, |
| - c: colors[(Math.random() * colors.length) | 0], |
| - life: 1400 + Math.random() * 900, // ms |
| - t: 0, |
| - })); |
| + const { w, h } = dust; |
| + confetti = []; |
| + // Corner cannons: launch up and inward so the arcs cross mid-screen. |
| + for (let i = 0; i < 75; i++) { |
| + const left = i % 2 === 0; |
| + confetti.push(confettiPiece({ |
| + x: left ? -6 : w + 6, |
| + y: h * (0.75 + Math.random() * 0.25), |
| + vx: (left ? 1 : -1) * (5 + Math.random() * 8), |
| + vy: -(11 + Math.random() * 7), |
| + delay: Math.random() * 150, |
| + })); |
| + } |
| + // Sky rain: the whole width, staggered over ~1.8s so it keeps coming. |
| + for (let i = 0; i < 170; i++) { |
| + confetti.push(confettiPiece({ |
| + x: Math.random() * w, |
| + y: -10 - Math.random() * 40, |
| + vx: (Math.random() - 0.5) * 2, |
| + vy: 1.5 + Math.random() * 2.5, |
| + delay: Math.random() * 1800, |
| + life: 4600, |
| + })); |
| + } |
| } |
| function stepConfetti(dt) { |
| if (!dust || !confetti.length) return; |
| const { g, h } = dust; |
| - confetti = confetti.filter((p) => (p.t += dt) < p.life && p.y < h + 20); |
| + confetti = confetti.filter((p) => { |
| + if (p.delay > 0) { p.delay -= dt; return true; } |
| + return (p.t += dt) < p.life && p.y < h + 20; |
| + }); |
| for (const p of confetti) { |
| - p.vy += 0.22; // floatier than the dust — it's paper, not grit |
| + if (p.delay > 0) continue; |
| + p.vy = Math.min(p.vy + CONFETTI_GRAVITY, 6.5); // terminal velocity: paper drifts |
| p.vx *= 0.99; |
| - p.x += p.vx; p.y += p.vy; p.rot += p.vr; |
| + p.x += p.vx + Math.sin(p.t / 130 + p.phase) * 0.9; // flutter |
| + p.y += p.vy; |
| + p.rot += p.vr; |
| g.save(); |
| g.translate(p.x, p.y); |
| g.rotate(p.rot); |
| - g.globalAlpha = Math.min(1, 4 * (1 - p.t / p.life)); // hold, then fade out |
| + // A cheap tumble: the width breathes with the flutter so pieces catch light. |
| + const tumble = 0.55 + 0.45 * Math.sin(p.t / 90 + p.phase); |
| + g.globalAlpha = Math.min(1, 5 * (1 - p.t / p.life)); // hold, then fade out |
| g.fillStyle = p.c; |
| - g.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); |
| + g.fillRect((-p.w * tumble) / 2, -p.h / 2, p.w * tumble, p.h); |
| g.restore(); |
| } |
| g.globalAlpha = 1; |