Building Particle Simulation Pens
Produce a single self-contained HTML document with inline <style> and <script>, using <canvas> for rendering and a floating .controls panel with <input type="range"> sliders bound to simulation parameters via oninput or addEventListener.
HTML<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Particle Resonance Simulation</title> <style> body,html{margin:0;padding:0;overflow:hidden;background:#000;} canvas{display:block;} .controls{position:fixed;top:10px;left:10px;color:#fff;font-family:monospace; background:rgba(0,0,0,0.5);padding:10px;border-radius:5px;} .controls label{display:block;margin-bottom:5px;} .controls input{width:200px;} </style> </head> <body> <canvas id="c"></canvas> <div class="controls"> <label>Particles: <span id="countVal">200</span> <input type="range" id="count" min="10" max="2000" value="200"></label> <label>Speed: <span id="speedVal">1</span> <input type="range" id="speed" min="0" max="5" step="0.1" value="1"></label> </div> <script> const canvas = document.getElementById('c'); const ctx = canvas.getContext('2d'); let W, H; function resize(){ W = canvas.width = innerWidth; H = canvas.height = innerHeight; } addEventListener('resize', resize); resize(); const params = { count: 200, speed: 1 }; function bindSlider(id, key, decimals=0){ const el = document.getElementById(id); const out = document.getElementById(id+'Val'); el.addEventListener('input', () => { params[key] = parseFloat(el.value); out.textContent = params[key].toFixed(decimals); }); } bindSlider('count','count'); bindSlider('speed','speed',1); let particles = []; function initParticles(){ particles = Array.from({length: params.count}, () => ({ x: Math.random()*W, y: Math.random()*H, vx: (Math.random()-0.5)*params.speed, vy: (Math.random()-0.5)*params.speed, hue: Math.random()*360 })); } initParticles(); function step(){ ctx.fillStyle = 'rgba(0,0,0,0.15)'; ctx.fillRect(0,0,W,H); for(const p of particles){ p.x += p.vx; p.y += p.vy; if(p.x<0||p.x>W) p.vx*=-1; if(p.y<0||p.y>H) p.vy*=-1; ctx.beginPath(); ctx.arc(p.x,p.y,2,0,Math.PI*2); ctx.fillStyle = `hsl(${p.hue},80%,60%)`; ctx.fill(); } requestAnimationFrame(step); } step(); </script> </body> </html>
Progress:
- Step 1: Define the simulation concept (particle field, force type: attraction/repulsion/resonance/cascade, entropy/noise source)
- Step 2: Set up canvas, full-viewport sizing, resize handling, dark background
- Step 3: Build particle data model (position, velocity, mass, phase, color) as array of objects
- Step 4: Implement physics/update step (forces, collisions, decay, boundary behavior)
- Step 5: Implement render step (trail fade via low-alpha fillRect, glow via shadowBlur or additive blending)
- Step 6: Add
.controlspanel with range inputs for key parameters (count, speed, force strength, entropy, decay) - Step 7: Bind inputs to live-update params object; display current value next to each slider
- Step 8: Wire
requestAnimationFrameloop; ensure it references liveparamsobject, not stale copies - Step 9: Test at extremes (min/max slider values, 0 particles, resize mid-animation)
- Step 10: Polish visuals (color palette, blend modes, trail persistence) and performance (cap particle count reasonably, use
forloops overforEachfor large N)
Example 1:
Input: "Build an entropic cascade simulation where particles lose energy over time and trigger cascading color shifts on collision."
Output: Canvas sim with particle objects holding energy (decays each frame via energy *= 0.995), collision detection via distance check, on collision spawn 2-3 child particles with inherited hue+random offset (cascade effect), controls for initial energy, decay rate, and cascade branching factor.
Example 2:
Input: "Particle resonance simulation with adjustable frequency."
Output: Particles oscillate position via sin(time * frequency + phase), frequency slider modulates a shared or per-particle value, visual resonance shown via synchronized pulsing size/opacity when phases align.
- Keep everything in one file unless explicitly asked to split HTML/CSS/JS.
- Use
requestAnimationFrame, neversetInterval, for animation loops. - Fade trails with
ctx.fillStyle = 'rgba(0,0,0,alpha)'+fillRectinstead ofclearRectfor motion-blur aesthetics. - Store all tunable values in one
paramsobject; sliders mutate it, render/update loop reads it — avoids stale closures. - Display live numeric value next to each slider (
<span>updated oninputevent). - Default canvas to full viewport,
overflow:hiddenon body/html, dark background for contrast with glowing particles. - Use
hsl()color for easy hue-cycling effects. - Cap default particle counts (~200-500) for smooth performance; let slider allow higher for stress-testing.
- Handle window resize without resetting particle state abruptly (rescale positions if needed).
- Don't use
clearRectevery frame if trail effects are desired — it erases history instantly. - Don't bind sliders with one-time
valuereads instead of live event listeners — controls won't do anything. - Don't forget
canvassizing on load AND resize — a 300x150 default canvas silently breaks full-screen sims. - Don't let particle arrays grow unbounded in cascade/spawn effects — cap max count or cull dead/low-energy particles.
- Don't mix
requestAnimationFrametiming with fixed-step physics assumptions — use delta time if frame-rate independence matters. - Don't hardcode colors/sizes when a slider-driven parameter would make the demo more compelling and inspectable.