eleven / messenger Public

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>
Paul Campbell a6d52399ae5823338267fd78d142a6bd5e72277a parent 35ce983
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;