← back to Fireworks Sim
initial scaffold: particle fireworks simulator (rebuilt from Gemma3 12B arena attempt)
b5e9607dcb5e0b9468579fa9bf40f2f0e4d50131 · 2026-07-23 06:51:50 -0700 · Steve Abrams
Files touched
Diff
commit b5e9607dcb5e0b9468579fa9bf40f2f0e4d50131
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 23 06:51:50 2026 -0700
initial scaffold: particle fireworks simulator (rebuilt from Gemma3 12B arena attempt)
---
.gitignore | 8 ++++
index.html | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 163 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..1e02fa4
--- /dev/null
+++ b/index.html
@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Particle Fireworks</title>
+<style>
+ html, body { margin: 0; height: 100%; overflow: hidden; background: #05060f; }
+ canvas { display: block; cursor: crosshair; }
+ #hud { position: fixed; top: 12px; left: 12px; color: #9aa4c7;
+ font: 13px/1.6 system-ui, sans-serif; user-select: none; }
+ #hud button { background: #1b2140; color: #cdd6ff; border: 1px solid #39406b;
+ border-radius: 6px; padding: 4px 12px; font: inherit; cursor: pointer; }
+ #hud button:hover { background: #262e58; }
+</style>
+</head>
+<body>
+<canvas id="sky"></canvas>
+<div id="hud">
+ <button id="autoBtn">auto-show: on</button>
+ <span> click anywhere to launch · press A to toggle</span>
+</div>
+<script>
+'use strict';
+const canvas = document.getElementById('sky');
+const ctx = canvas.getContext('2d');
+let W, H;
+function resize() { W = canvas.width = innerWidth; H = canvas.height = innerHeight; }
+addEventListener('resize', resize);
+resize();
+
+const GRAVITY = 0.05, FRICTION = 0.985, TRAIL = 7;
+const rand = (a, b) => a + Math.random() * (b - a);
+const rockets = [], particles = [];
+
+class Rocket {
+ constructor(tx, ty) {
+ this.x = tx + rand(-25, 25);
+ this.y = H;
+ this.ty = Math.min(ty, H * 0.85);
+ // launch speed chosen so the rocket's apex lands at the target height
+ this.vy = -Math.sqrt(2 * GRAVITY * (H - this.ty)) - rand(0.2, 0.8);
+ this.vx = (tx - this.x) / (Math.abs(this.vy) / GRAVITY);
+ this.hue = rand(0, 360);
+ this.trail = [];
+ }
+ update() {
+ this.trail.push({ x: this.x, y: this.y });
+ if (this.trail.length > TRAIL) this.trail.shift();
+ this.x += this.vx;
+ this.y += this.vy;
+ this.vy += GRAVITY;
+ if (this.vy >= 0 || this.y <= this.ty) { explode(this.x, this.y, this.hue); return true; }
+ return false;
+ }
+ draw() {
+ const tail = this.trail[0] || this;
+ ctx.strokeStyle = `hsl(${this.hue} 100% 75%)`;
+ ctx.lineWidth = 2.5;
+ ctx.beginPath();
+ ctx.moveTo(tail.x, tail.y);
+ ctx.lineTo(this.x, this.y);
+ ctx.stroke();
+ }
+}
+
+class Particle {
+ constructor(x, y, hue) {
+ const angle = rand(0, Math.PI * 2);
+ // sqrt gives an even fill of the burst disc instead of a hollow ring
+ const speed = Math.sqrt(Math.random()) * rand(5, 9);
+ this.x = x; this.y = y;
+ this.vx = Math.cos(angle) * speed;
+ this.vy = Math.sin(angle) * speed;
+ this.hue = hue + rand(-18, 18);
+ this.life = 1;
+ this.decay = rand(0.008, 0.022);
+ this.trail = [];
+ }
+ update() {
+ this.trail.push({ x: this.x, y: this.y });
+ if (this.trail.length > TRAIL) this.trail.shift();
+ this.vx *= FRICTION;
+ this.vy = this.vy * FRICTION + GRAVITY;
+ this.x += this.vx;
+ this.y += this.vy;
+ this.life -= this.decay;
+ return this.life <= 0;
+ }
+ draw() {
+ if (this.life < 0.35 && Math.random() < 0.4) return; // twinkle as it dies
+ const tail = this.trail[0] || this;
+ ctx.strokeStyle = `hsla(${this.hue} 100% ${45 + this.life * 30}% / ${this.life})`;
+ ctx.lineWidth = 1 + this.life * 1.5;
+ ctx.beginPath();
+ ctx.moveTo(tail.x, tail.y);
+ ctx.lineTo(this.x, this.y);
+ ctx.stroke();
+ }
+}
+
+function explode(x, y, hue) {
+ const count = Math.floor(rand(70, 120));
+ const twoTone = Math.random() < 0.3 ? hue + 180 : hue;
+ for (let i = 0; i < count; i++) particles.push(new Particle(x, y, i % 2 ? hue : twoTone));
+ // brief flash at the burst point
+ const flash = ctx.createRadialGradient(x, y, 0, x, y, 60);
+ flash.addColorStop(0, `hsla(${hue} 100% 90% / 0.9)`);
+ flash.addColorStop(1, 'transparent');
+ ctx.fillStyle = flash;
+ ctx.fillRect(x - 60, y - 60, 120, 120);
+}
+
+function launch(tx, ty) { rockets.push(new Rocket(tx, ty)); }
+
+let autoOn = true, autoTimer = 0;
+function tick(now) {
+ // translucent fill (not clearRect) so everything leaves a fading trail
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.fillStyle = 'rgba(5, 6, 15, 0.22)';
+ ctx.fillRect(0, 0, W, H);
+ ctx.globalCompositeOperation = 'lighter'; // additive blend = glow where sparks overlap
+
+ for (let i = rockets.length - 1; i >= 0; i--) {
+ if (rockets[i].update()) rockets.splice(i, 1);
+ else rockets[i].draw();
+ }
+ for (let i = particles.length - 1; i >= 0; i--) {
+ if (particles[i].update()) particles.splice(i, 1);
+ else particles[i].draw();
+ }
+
+ if (autoOn && now > autoTimer) {
+ launch(rand(W * 0.1, W * 0.9), rand(H * 0.1, H * 0.45));
+ autoTimer = now + rand(500, 1600);
+ }
+ requestAnimationFrame(tick);
+}
+requestAnimationFrame(tick);
+
+canvas.addEventListener('pointerdown', e => {
+ launch(e.clientX, e.clientY);
+ autoTimer = performance.now() + 4000; // give the user the sky for a few seconds
+});
+
+const autoBtn = document.getElementById('autoBtn');
+function setAuto(on) {
+ autoOn = on;
+ autoBtn.textContent = `auto-show: ${on ? 'on' : 'off'}`;
+}
+autoBtn.addEventListener('click', e => { e.stopPropagation(); setAuto(!autoOn); });
+addEventListener('keydown', e => { if (e.key.toLowerCase() === 'a') setAuto(!autoOn); });
+</script>
+</body>
+</html>
(oldest)
·
back to Fireworks Sim
·
verified via headless Playwright: no console errors, click-l ce4e672 →