Create a single HTML file with a canvas, a volcano, and a physics simulation. Main features: - Terrain: a cone with a crater. Precompute the ground height function for collision detection. - Pressure meter: increases over time, and erupts when full. - Lava particles: emitted from the crater during eruption, gravity, collision with terrain, sliding downhill (simple slope following: after landing, move in the downhill direction, friction), cool from yellow to dark red, then fade. - Ash particles: rise, wind drift, grow larger, fade. - Sliders: eruption power, wind (direction + strength — a single slider from -100 to 100), particle count (affects emission rate / max count). - HUD: active particle count, FPS. - Maintain smoothness with 1000+ particles: use a simple array, avoid per-frame object creation; precompute terrain heights into a column array for fast lookup; simple circle vs. ground line-segment collision (treat ground as a heightmap — sufficient for a cone). Design: width 960, height 540 (or responsive). Volcano centered. Ground baseline y = H-60. Volcano: base half-width ~260, crater at the top. Height function: for |x-cx| < baseW, y = base - height * (1 - (d/baseW)^0.85) or thereabouts; crater: a flat dip near the top. Easier: define the cone as a linear slope from the base to the rim, with a slightly concave crater at the top. terrainHeight(x): return the y coordinate of the top surface. Outside the volcano: flat ground. Cone: y = groundY - coneH * (1 - d/baseW). Crater: in the central region |d| < craterHalf, dip it down: y = rimY + craterDepth*(d/craterHalf)^2. Lava landing: if py >= groundY(x), place the particle at groundY, give it velocity along the slope: compute slope from finite differences, and move in the downhill direction with friction. Cooling: lifetime timer; color interpolation: yellow (255,220,50) → orange → red (200,30,10) → dark (60,20,15), then alpha fades. Draw lava as small circles; additive glow using 'lighter' compositing for hot particles. For performance with 1000+ particles, drawing an arc per particle is fine, but glow shadows should be avoided. Use two passes: dark settled rock particles in normal compositing, hot lava in 'lighter'. Actually even simpler: a single pass with fillStyle per particle; grouping by color bucket could work — bucket by cooling stage (e.g., 24 buckets) to reduce state changes. That's good: quantize color and group by bucket index. Eruption behavior: pressure goes 0→1 over ~6 seconds (rate). When it hits 1, eruption: for a duration depending on power, emit at a high rate. The particle-count slider sets the max count cap, or emission multiplier / target. The particle-count slider scales the number of particles emitted per eruption, and the wind slider -100..100 affects the horizontal drift of ash and the in-flight horizontal wind force on particles. Emission: while erupting, spawnRate = power * amount * e.g. 8 per frame? Cap total particle count at maxParticles (from the slider, minimum 1000? The requirement is that the simulation should stay smooth with at least 1000 active — so the slider range is around 500–4000, default 1500). Ash: spawn at a lower rate during eruption; rise (negative vy), affected by wind, expand, fade, dark gray. FPS: moving average. Performance: fixed array pool? Use an array with a swap-remove technique. Particles as plain objects in an array; swap-remove on death. 2000 particles is fine. Draw order: sky gradient, ash (back), volcano silhouette (dark), lava particles, ground highlight, ash in front? Easier: sky, then ash, then terrain, then lava. The terrain drawn after ash hides ash behind the volcano — acceptable. In reality, the ash should appear above the crater. Draw the terrain first, then ash, then lava. Lava behind the mountain? Particles are mainly above ground level; particles on slopes should be drawn on top of the terrain. So: sky, terrain, ash, lava. Good. Glow: at eruption start, draw the crater glow as a radial gradient (flashing). Also a background ember glow. Sliding: when a particle lands, state = rolling: each frame, find the downhill direction (finite difference of the terrain), add acceleration in the downhill direction, vx += slopeForce, friction, small random jitter; stop when speed is low and slope is flat. While rolling, continue cooling, but cool faster while on the ground (multiplier). When lifetime expires, fade alpha over about 1 second, then remove. Collision details: the particle is in the air (state 0): apply gravity, wind slightly, integrate; if y >= terrainHeight(x), it lands → y = terrain, state 1 (settled/rolling), compute normal? Simply: keep horizontal velocity scaled, start rolling downhill. Also, particles might fly off the edge of the canvas — remove when x < -50 or > W+50. Pressure meter: vertical bar on the left, label "PRESSURE", gradient fill. Eruption state machine: - building: pressure += rate*dt (e.g., 0.18/sec → about 5.5 seconds). - erupting: duration ~ 2.5–4 seconds * power factor; emission; ash emission; after it ends, pressure=0 → building. Power slider 0.2–3, default 1: affects initial launch speed, duration, and particle count per emission. Particle count slider 200–4000, default 1500: max active particle count (lava + ash); emission is capped. Wind slider -100..100: windX = value/100 * 120 px/s^2 as a force on ash; also a small force on airborne lava. Emission velocity: from the crater center, angle within a cone of ±35° upward, speed = 250 + rand*250, scaled by power. Color buckets: cooling progress t is 0..1 over lifetime (e.g., 6 sec airborne, 8 sec ground → use age with a multiplier). Define stops: t0: (255,240,150) t0.2: (255,170,40) t0.45: (255,90,20) t0.7: (150,40,15) t1: (60,25,20) Quantize into 16 buckets; group drawing: iterate buckets, beginPath, arc for each particle, fill. Per-bucket per-particle arc is fine. Alpha fade: the last 15% of lifetime, alpha = 1 - (t-0.85)/0.15. Per-bucket alpha: since the fade is continuous, either set globalAlpha per particle or include a fade bucket. Easier: set globalAlpha per particle (fast enough), group only by color bucket, fill each particle individually. 2000 fill calls is fine. Actually even simpler: just draw each particle with its own fillStyle string computed from a precomputed table (strings for 32 buckets), set globalAlpha when fading. 2000 state changes — modern browsers are fine. Ash: draw as gray circles with low alpha, size grows with age. Draw with 'source-over'. Terrain: fill the path from the terrain heightmap with dark brown/gray. Add subtle crater glow during eruption. Sky: gradient from dark blue to a slightly reddish near the horizon during eruption. HUD text at top-left: particles, fps, pressure %. Let me write the code. const W=960,H=540, fixed canvas size, CSS width 100% to scale the canvas? Keep it fixed but style the canvas with max-width:100%. Terrain function: cx = W/2, groundY = H-70, baseW=300, coneH=300, craterHalf=45, craterDepth=28. For |d| maxParticles. Emission during eruption: perFrame = base * power * amountFactor. amountFactor = amount/1500. base ~ 40/sec... to reach 1500 active with lifetime ~7 sec, we need a spawn rate of ~200/sec at full. Hmm. Lifetime: airborne ~2-3 sec, then ground 6 sec. Set ground life to 4 sec → total ~7 sec. Rate 150–250/sec → 1000–1700 active. Set spawnRate = 90 * power * amount/1500 per second? Default: 90/sec * 7 sec = 630 active. Not enough. Bump it up: spawnRate = 220 * power * (amount/1500). Default → 220/sec. With 6-sec average lifetime ≈ 1300 active. Good. The amount slider goes up to 4000: 4000/1500*220 = 586/sec, capped by the maxParticles check. Use an accumulator: emitAcc += rate*dt; n = floor(emitAcc); emitAcc -= n. Eruption duration: 2.0 + power*1.2 seconds? Plus a continuous emission. FPS: count frames every 0.5 sec. Wind force on airborne lava: vx += windX*0.3*dt. Rolling: downhill direction: slope = tH[x+2]-tH[x-2]; if slope>0, downhill is +x (y is downward, so increasing terrain y = downhill). accel = 250 * slopeSign * |slope|/4 clamped... simpler: g component along the slope. dir = sign(slope); vx += dir*300*dt; vx *= friction (0.985 per frame → use pow(0.4, dt) form: vx *= Math.exp(-1.5*dt)); when |slope| is small, also base friction: vx *= exp(-2*dt). y = tH[x] - 1. Stop condition: speed < 8 and |slope| < 0.05 → settled: vx=0. Rolling life: age advances faster: while rolling, age += dt*1.6 (cools faster while moving), plus a ground-cool multiplier. Color table: 20 buckets with RGB interpolation between stops. Precompute a string array, plus a separate alpha handling. Add glow: for t<0.3, draw a second larger fainter circle in 'lighter' mode. To save performance, do a glow pass only for hot particles in a single 'lighter' pass using rgba fill. Also, crater glow during eruption: orange radial gradient. Ash wind slider: range -100..100, default 15. windX = val/100 * 90 (px/s^2). Display the value. Pressure rate: 0.16/sec → 6.25 sec to full. Let me write the HTML. Control panel: sliders with labels and value display, styled dark. Code: ```html Volcano
sliders
``` Main loop is requestAnimationFrame with dt clamped to 1/30. Particle objects: use an array of objects with fields: x,y,vx,vy,age,life,state (0 airborne, 1 rolling, 2 settled), r (radius). Ash is separate: x,y,vx,vy,age,life,size. Remove via swap-pop: iterate in reverse. Drawing lava: loop, compute t = age/life, bucket = min(19, t*20|0), set fillStyle to colorTab[bucket], globalAlpha is fade if t>0.85, else 1. arc. Glow pass: 'lighter', hot ones with t<0.25: fill rgba(255,140,40,0.25) circle r*3. Draw the volcano: build the path from the tH array, fill with a gradient. Also a crater rim highlight. Sky: vertical linear gradient: top #0b1026 → horizon #2b1c3a; add an eruption-tinted overlay: a reddish overlay with alpha proportional to eruptGlow. Eruption glow variable: rises quickly during eruption, decays. Meter: at x=18, y=60, width 22, height 300. Fill from the bottom, gradient green→yellow→red. Text. HUD: "Particles: N | FPS: M". Write it all out. Keep it concise but complete. Edge case: particle lands off-canvas? x clamped to [1, W-2]. Rolling off the volcano base: once the terrain becomes flat, slope=0, friction stops it. Good — the particles come to rest on the slope. Ambient ember glow at the crater during building? Skip. Also, slight initial horizontal spread on launch. Let me write the final code. const tH = new Float32Array(W); Compute for x: dx = x - cx; ad=abs; if ad < craterHalf: t = ad/craterHalf; y = rimY + craterDepth*t*t; else if ad < baseW: y = rimY + (groundY-rimY)*((ad-craterHalf)/(baseW-craterHalf)); // slope from rim to base else y = groundY. Add a little roughness noise? Skip, or a small deterministic jitter: + Math.sin(x*12.9898)*... skip for clean slopes. coneH: rimY = groundY - coneH, coneH = 290, baseW = 310, craterHalf = 42, craterDepth = 30. groundY = 470 (H=540). rimY = 180. Emitter is at (cx, rimY + craterDepth - 4). Launch speed: s = (240 + Math.random()*260) * (0.7 + 0.3*power). power is 0.2..3. Angle: -90° ± 30°: a = -PI/2 + (rand-0.5)*1.2. Gravity 420 px/s^2. Ash: life 5+rand*4; vy = -(40+rand*60); vx=(rand-0.5)*30; buoyancy: vy += (-25 - vy*0.15)*dt? Simpler: vy += (targetRise - vy)*0.8*dt, targetRise = -35. vx += windX*dt + small turbulence: vx += (Math.random()-0.5)*20*dt... use sin noise. size = 4 + age*4. Wind force on airborne lava: vx += windX*0.25*dt. Max particle count from slider: maxP. OK, let me write it.