Need to create a volcano eruption simulation using canvas. Single file, no external libraries. Features: - Side-view volcano cone with crater, ground, and sky - Pressure meter that builds up and triggers an eruption when it reaches max, then resets - Lava particles fired from the crater at various velocities, gravity physics, landing on slopes, sliding/settling - Lava cooling: yellow → orange → red → dark rock color, then disappears - Ash cloud rising from crater, drifting with wind - Three sliders: eruption power, wind strength & direction, particle count - FPS + particle count counter, smoothly handles 1000+ particles Design: canvas full window or fixed size (e.g., 960x600). Volcano profile defined by a ground height function — piecewise: slope from left base to crater rim, crater depression, right slope. Height function y(x) for terrain: a cone centered at cx with summit, crater between cx-w and cx+w. Terrain height function: ``` function terrainY(x) { // cone: peak near summit at cx, crater is flat notch const cx=W/2, base=H*0.85, peak=H*0.35; const halfW = W*0.38; const dx = Math.abs(x-cx); let y; if (dx < craterHalf) return craterY; // flat crater floor else if (dx < halfBase) { // slope linear from crater rim to base t = (dx - craterHalf)/(halfBase - craterHalf); y = craterY + t*(baseY-craterY) } else baseY; } ``` Add slight noise for realism, but keep height function deterministic and simple for physics. Lava particles: state 'flight' and 'grounded'. During flight: vx += wind*small factor? Maybe lava is barely affected by wind; wind only affects ash. Grounded lava: slide down slope based on local terrain gradient, viscous, eventually solidifies and fades after lifetime. Cooling: color based on temperature value, temp decays over lifetime. Color interpolation: bright yellow (#fff8a0) → orange → red → dark gray rock. When fully cold and settled, fade out and remove. Ash particles: rise, expand, alpha fade, wind push. Eruption: pressure builds up over time, when >=1, eruption burst: spawn N lava particles and ash puffs over about 1.5 seconds. After eruption, pressure resets to 0 and builds up again. Sliders: power (eruption force), wind (-10 to 10), particle count (lava particle count per eruption, e.g., 200–3000). Need 1000+ simultaneously — set slider max high enough. Performance: use single canvas, avoid per-particle createShadowBlur (slow). Use radial gradient for lava glow? Too slow for 1000+. Instead draw glow with additive compositing: draw circles with globalCompositeOperation 'lighter'. Cheap: fillRect or arc. Use 'lighter' on canvas for pre-rendered glow sprite drawn at different sizes (drawImage). Pre-render glow sprite offscreen canvas (radial gradient), drawImage per particle — fast enough for ~2000. FPS: measure with requestAnimationFrame timestamps, compute smoothed dt, display. Cap dt to avoid spiral. Lava physics: - Spawn at crater center with random angle near vertical ± spread, speed depends on power slider and randomness. - Gravity ~ 500 px/s². - Wind slightly affects airborne lava: vx += wind*5*dt (small). - Each frame check terrain collision: if y >= terrainY(x), land. Set grounded, place on surface, compute slope: gradient between terrainY(x+4) and terrainY(x-4). Slide: vx += gradient*g*slideFactor*dt, friction. Actually simpler: along slope vx proportional to slope. Grounded lava: cool faster, slide down slope until velocity slows enough (flatter ground) to settle, then stays, darkens, lifetime ticks then disappears? Requirement: "fade to dark rock, then disappear". So after some seconds of settling, fade out and delete. OK. Ash: spawn continuously during eruption from crater, vy upward ~ -30 to -80, vx = wind*factor + random, size grows, alpha decays. Gray color, use normal compositing, drawImage with soft sprites. Draw ash first (behind), then glow of lava. Sky: gradient background, maybe sun, dark ground. Draw each frame — pre-render static scene (sky + volcano) on an offscreen canvas and drawImage on each frame. Terrain static → pre-render once (rebuild not needed; volcano shape fixed). Pre-render background on an offscreen canvas. UI: HTML control panel with sliders (power, wind, particle count) and readouts. Pressure meter: draw on canvas or HTML div? Draw on canvas side (right side) or use HTML element with CSS width. Easier as HTML div. Draw a vertical pressure bar on canvas, labeled. Pressure building: pressure += dt / period, period ~ 6-10 seconds maybe. When >=1, trigger eruption that lasts a fixed duration during which emission occurs. During eruption pressure drains rapidly. Emission: at eruption start, emit particleCount of lava over eruptionDuration (e.g., 2 seconds) — spawn rate = count/duration. Plus continuous ash. Also during pause, minor continuous ash from crater. Particle storage: arrays with object pool or simple arrays; splice removal or swap with last. Use arrays for lava and ash with swap-removal for performance. Counter: display activeParticles = lava.length + ash.length, FPS. Let's write the code. Canvas size: fixed 960x600, CSS centered, page background dark. Or make responsive. Fixed is fine. Terrain function: ``` const W=960,H=600, groundY=H*0.82 (=492), peakY=H*0.30(=180) cx=W/2=480, craterHalf=42, baseHalf=400 terrainY(x): d=|x-cx| if d=1 → state 'erupting', eruptTimer=0. - during 'erupting': emit lava at rate, ash at rate, pressure = 1 - timer/duration*... decrease pressure to 0 across eruption; after timer done → 'building', pressure=0. Sliders: - power: 0.4–2.0 (speed factor) - wind: -200 to 200 px/s equivalent; display value and arrow direction. - amount: lava particles per eruption: 300–3000. Ash drawn with pre-rendered radial soft gray sprites; lava drawn with additive-composited glow sprite plus a core dot. Lava color from temperature t (0..1): interpolate colors: white-yellow (255,245,180) → orange (255,150,40) → red (180,40,10) → dark (40,30,30). Compute rgb and fillStyle string. Setting fillStyle per particle for 1000+ is OK-ish but costly due to string creation. Alternative: quantize temperature to buckets (e.g., 8 levels), precompute color strings, and draw rects (fillRect 3x3) per particle. 2000 fillRect is fine. Plus glow sprite drawn with 'lighter' only on hotter ones (temp>0.4) — drawImage with scaled sprite. drawImage 1000 times fine. Ash sprite: 64px soft gray circle, drawn with scaling and globalAlpha. globalAlpha is cheap. Let's write the HTML. Structure: ```
sliders + readout
``` UI overlaid top-left, pressure bar drawn on canvas (right side) or HTML? Pressure meter in DOM as vertical bar? "Pressure meter" — HTML works. Draw pressure bar on canvas (right) with label and glow near top at eruption. Actually easier with HTML: fixed-height container with div that fills from bottom, colored orange, width set in JS. Write JS: ``` const cvs=..., ctx=cvs.getContext('2d'); const W=960,H=600; // terrain function terrainY(x){...} // background pre-render const bg=document.createElement('canvas'); ... // sprites function makeGlow(color stops) -> 64px radial canvas glowSprite = makeGlow('255,200,90'); ashSprite = makeGlow with gray. // state let pressure=0.2, state='building', eruptT=0, pressureTime=7; let lava=[], ash=[]; let lastTime=performance.now(), fps=60 smoothed. Sliders: powEl, windEl, amtEl. spawnLava(power): pos at crater x=cx+rand(-30,30), y=craterFloorY-2; angle up spread: angle = -PI/2 + (rand-0.5)*1.1; speed = (200+rand*260)*power; maybe some speed scale with vertical bias. vx=cos*speed + wind*0.1? ; vy=sin*speed. lava item: {x,y,vx,vy,temp:1,grounded:false,vxg, life, settled} cooling: temp -= dt*(grounded?0.06+... :0.12). Lifetime: total ~ 4 + rand*6 seconds; after grounded and temp<0.2 start fade alpha. Update lava per dt: if !grounded: vy+=g*dt (g=560) vx+= wind*0.15*dt? Wind force small x+=vx*dt; y+=vy*dt if y>=terrainY(x): grounded=true; y=terrainY(x); temp*=0.9; kill bounce: vx = slope stuff; maybe bounce if fast and temp hot: vy=0; compute slope, grounded velocity vx = slope* something. remove if off-canvas else: slope=(terrainY(x+3)-terrainY(x-3))/6? gradient dy/dx; slide: vx += slope*g*0.55*dt (if slope positive downhill right → moves right) vx *= pow(0.4,dt) friction? vx *= Math.exp(-1.6*dt)... wait, viscosity: if slope near zero, velocity dies fast. x+=vx*dt; y=terrainY(x) temp -= dt*(0.10 + 0.02) life -=dt; if life<=0 → remove with alpha fade; store alpha via fadeLife. Simpler: track temp; after grounded, temp decays; when temp<0.15 start shrinking/removing "cooled rock" by temp -=dt*0.06, remove when <0.0? But temp used for color including rock color; remove when temp<-0.25 with alpha=... use alpha = clamp(temp/(-0.25)...) hmm: if temp<0.18 alpha=temp/0.18. remove if temp<0. Ash update: vy -= buoy*dt (accel upward, decreasing) vx += (windTarget - vx)*dt*0.8 wind push x+=vx*dt; y+=vy*dt; r+=grow*dt; alpha-=dt/life remove when alpha<=0 or y< -50 Ash spawn during eruption also from crater with initial upward velocity; also small ambient. Wind slider value in px/s (-250 to 250). Ash vx relaxes toward wind*0.9. Render: ctx.drawImage(bg) // ambient glow over crater when erupting: draw glow sprite large with 'lighter', scaled by intensity ash: ctx.globalCompositeOperation='source-over'? draw ash with normal alpha. Draw ash sprites: ctx.globalAlpha=a; drawImage(ashSprite, x-r,y-r,2r,2r) lava: ctx.globalCompositeOperation='lighter'; for hot ones draw glow sprite size = 10+temp*14, alpha=temp*0.7; then reset to 'source-over' for core rect: fillStyle=bucketColors[b]; fillRect(x-1.5,y-1.5,3,3); maybe core 2x2 with size for grounded lava 3-4px. draw pressure bar in DOM. Bucket colors: temp 0..1 clamp: index=floor(temp*7)... map colors: t>=0.75: (255,250,190) ... define lerp between keyframe colors: keys: [[255,255,210],[255,210,90],[255,120,20],[160,30,10],[45,35,40],[30,28,32]] precompute 32 strings for temp buckets including darker for t<0 (cooled) (alpha handles fade). FPS: smoothed = smoothed*0.9 + (1/dt)*0.1; show Math.round, update every 0.25s. Particle count shown: lava.length+ash.length. Emission rate: lavaRate = amount/eruptDuration (2.2s). accumulate emitAcc += rate*dt; while >=1 spawn. Ash rate = amount*0.12 per second during eruption... e.g., 90/s. Plus during building ~5/s. Cap: if lava.length > 4000 skip spawning? Slider max amount 3500. Grounded lava cools and disappears ~ life total 6-12s after launch. Peak count ~ slider amount. Fine. Erupting visual flash: screen flash at start? Optional small glow. Also draw pressure text "ERUPTION IMMINENT!" when >0.8. Crater rim: floor at peakY+14, but rim slightly higher: for terrain, add slight rim bump for d in [craterHalf-10, craterHalf]? Skip. terrainY with noise: ``` function terrainY(x){ const cx=W/2; const d=Math.abs(x-cx); const peak=178, floor=peak+16, base=492, ch=46, bh=430; let y; if(dbh) noise also applies, good — slight undulation of ground. But crater floor noise: fine. Draw background: sky gradient dark blue with lighter horizon; some stars? Sunset sky: gradient from #2b2340 top to #7a4b5a horizon? Eruptive ambience: dusk sky with orange glow at horizon. Distant ridges silhouette dark purple. Volcano body: path polygon along terrainY from x=0..W to bottom filled, fill with vertical gradient dark brown (#3a2a2a) → (#1d1418). Add inner glow at crater: draw radial orange in crater (part of bg) to hint molten pool. Ground: extend terrain beyond, fill to bottom H, dark color. Also draw lava channel glow inside crater: orange blob at crater floor in bg. Pre-render background on offscreen 'bgCanvas'. Pressure meter DOM: ```
PRESSURE
``` Position absolute right: pfill height = pressure*100%. Control panel: label with span that displays value. Wind slider: -250..250 step 10, label shows e.g. "→ 120" / "← 120". Amount slider: 300..3500 step 100. Power slider: 40..200 (%) speed = pow/100. OK let's write. Also handle tab visibility clamping dt (dt=min(dt,0.05)). Swap removal: write helper that iterates backwards and splice(i,1)? splice is O(n) per removal — could be many removals per frame. Iterate backwards with arr.splice(i,1) for removal — with high removal count, worst O(n^2). Better: filter-style compaction: iterate backward and swap with last: ``` for(let i=arr.length-1;i>=0;i--){ if(dead){ arr[i]=arr[arr.length-1]; arr.pop(); } } ``` Do update in loop that may swap-remove if i becomes last — process in backward loop; swap with last (already updated) then pop. If we go i--, the swapped-in element from the end is already updated? Going backward, indices greater than i are updated; arr[last] is updated — good. Draw ash behind lava. Also draw ash above volcano (yes, above). Glow sprite make: ``` function radial(sprite color inner) { size=64; c=canvas 64; g=ctx.createRadialGradient(32,32,0,32,32,32); g.addColorStop(0,'rgba(255,220,120,1)'); ... } ``` Ash sprite: 'rgba(120,115,120,0.6)' center to transparent. Perf for drawImage of glow sprite for each hot lava particle: fine. Also eruption lightning/glow overlay: when erupting, subtle orange overlay: ctx.fillStyle=`rgba(255,120,30,${0.08*eruptIntensity})`; fillRect whole. Erupt intensity = decaying pulse. Pressure building speed: base period ~8 seconds, maybe faster? pressure += dt/8. Ash spawn also during building (small puffs at crater with low alpha). Edge cases: lava spawn: some low-speed lava fountains: speed range (140+Math.random()*300)*speedMult, speedMult=power/100. Angle: -90° ± 35°. Wind on airborne lava: vx += wind*dt*0.12 (small). Grounded slide: slope = (terrainY(x+4)-terrainY(x-4))/8; vx += slope*900*dt; vx*=Math.exp(-2.2*dt); cap |vx|. On steep slopes slide down, stasis near crater floor. Cool faster: temp -= dt*(0.055 + 0.05*(1-temp)). Also slide near-flat ground off canvas side → remove when x<-50||x>W+50. Grounded lifetime: total temp starts at 1; hot decays during flight 0.055/s, grounded 0.09/s → ~11s to 0, then alpha=temp<0.2? remove at temp<0.02, alpha=min(1,temp/0.18). Wait temp decays during flight too: 0.045/s. Total life from spawn ~13-18s. Peak count ~ amount (fine, they disappear before next eruption in ~8+2 sec? eruption every ~10s; grounded rock lasts ~11s after launch... may overlap somewhat with next eruption, boosting count above 1000 — good for requirement.) Color keys (temp t 0..1): t=1: 255,255,190 0.8: 255,230,120 0.6: 255,150,40 0.4: 235,80,20 0.25: 120,30,15 0.1: 55,40,40 0: 30,26,30 Below: same dark. Precompute 24 buckets for t in [0,1] and index = clamp(floor(t*23)). Bucket for core size: size = grounded? 4:3 px. Use fillRect(x-2,y-2,4,4) for grounded and 3x3 for airborne. Batch: iterate particles, set fillStyle string per particle (string reuse from bucket array, cheap). fillStyle change is somewhat costly but 2000 fillRects OK. Sort by bucket to reduce state changes? Overkill. Ash drawing: alpha per particle, globalAlpha per draw. 600-1500 ash sprites fine. OK, background also includes a faint moon. Keep moderate. Also add "ERUPTING!" indicator text drawn on canvas when erupting. Write HTML file. Ensure sliders styled to look decent. Also add pressure label at eruption: fill turns red pulsing? Simple: pfill background gradient; when state is erupting show fill=orange, else green→red? Simpler: fill color based on pressure: hue = 120 - 120*pressure. Set style.background = `hsl(...)`. Let's write it out. Final check on requirement: "slide down slope or settle" — done. "live on-screen counter of active particles and fps" — DOM counter updated. "stay smooth above 1000 particles" — default amount slider 1500, plus pooling for perf. One more: ash drifts with wind — yes. Code: ```html ... ``` Write it in full.