Create a single HTML file with canvas, volcano, pressure meter, eruption with particles, gravity, terrain collision (side view of a cone shape), particles that slide down and come to rest, cooling color, drifting ash cloud, three sliders (power, wind, particle amount), counters for particle count and FPS.
Volcano terrain: define a height function y = groundY - cone(x). Use this for collision detection: when a particle's y goes below the terrain height, it lands, slides with friction, and comes to rest.
For performance: use a particle pool, simple canvas circles, avoid gradients per particle. Use additive blending (globalCompositeOperation 'lighter') for lava glow. Keep the count modest. Target 1000+ particles.
Let's write the code.
Canvas 960x540 or responsive. Keep it fixed.
Volcano: cone centered at x=W/2, base from cx-280 to cx+280, peak near top y=180, crater: flat top between cx-40 and cx+40 with a dip.
terrainY(x): for |x-cx| <= 280, height = (1 - |x-cx|/280) * coneHeight, crater dip: if |x-cx|<45 then subtract the dip. Otherwise ground at y = H-40.
Particles:
- Lava: spawn at crater top, vx random * power, vy upward * power. Gravity. When y >= terrainY(x), set to rest: y = terrainY(x), compute slope, apply downhill slide: velocity along the slope, friction. After a few seconds cooling stops them (state=settled). Cooling: color goes from yellow (255,220,80) to orange to dark. Life ~6-10 seconds. Disappear.
- Ash: spawn at crater, rise (small buoyancy) and drift with wind, expand, fade alpha.
Emission during eruption: while eruptionT>0, spawn lava at a rate proportional to power and the particle amount slider. Ash spawns continuously during eruption, and a little ambient ash.
Pressure: increases over time (rate might depend on sliders? keep it simple, fixed). When it reaches 1, erupt for eruption duration ~2.5s, then reset pressure to 0.
Sliders: power (1-10), wind (-5..5), particles (200-3000, spawn multiplier).
FPS: computed with smoothing.
Rendering: draw sky gradient (precomputed), volcano shape as filled path (precomputed offscreen? or draw every frame is fine). Draw volcano once to offscreen canvas and blit it.
Lava glow: draw with 'lighter' compositing, circle with shadowBlur? shadowBlur is slow. Instead draw 2 circles: outer semi-transparent orange, inner bright color. Use hsl via precomputed color table by life fraction: index into a 32-entry array of [r,g,b].
Slide: on terrain, slope = (terrainY(x+2)-terrainY(x-2))/4; acceleration along slope; vx += slope-ish... Simpler: treat as ball rolling down: vx += (sign of slope)*g*0.3*... actually downhill direction: if terrainY increases with x (y down is positive), the slope direction going down is toward increasing y, i.e. sign of (dy/dx). So vx += sign(dydx)*g*0.25*dt; friction vx*=0.95; update x, snap y to terrain. Once speed is low and life is short, stop it.
Also cap max particles.
Let's write the file.
Color ramp: t = 1 - age/life (1 new → 0 old).
t>0.6: bright yellow-orange: r=255, g= 120+135*t... let me just do this:
- t in [0.7,1]: color (255, 230, 100)→(255,160,40)
- Simplify with array lookup:
ramp = for i 0..31: t = i/31;
if t > 0.75: mix yellow(255,240,140) and orange(255,140,20) by (1-t)/0.25
else if t > 0.4: mix orange(255,140,20) and red(180,50,20)
else mix red and dark rock (50,35,35), and alpha fades in the last 20%.
Compute a 32-entry table.
Ash: gray particles, size grows, alpha = life-based * 0.4.
Wind slider -5..5 affects ash vx and lava slightly.
Draw eruption flash at crater during eruption.
Write code carefully. Particle cap: pool arrays (typed arrays for performance?) Use object arrays with fixed capacity and a free list — fine for a few thousand.
Actually use a plain object array with swap-remove. Emission rate per frame: during eruption, spawnRate = power * particlesSlider factor. particle amount slider scales both spawn rate and max count.
Let me write:
```js
const W=960,H=540;
const cx=W/2, baseW=300, peakY=150, groundY=H-50, craterHalf=42;
function terrainY(x){
if(xcx+baseW) return groundY;
const d=Math.abs(x-cx)/baseW;
let y=groundY-(groundY-peakY)*(1-d);
const dc=Math.abs(x-cx);
if(dcp.life){remove;continue;}
if(p.state===0){ // flying
p.vy+=G*dt; p.vx+=wind*0.05*dt;
p.x+=p.vx*dt; p.y+=p.vy*dt;
if(p.y>=terrainY(p.x)){ p.y=terrainY(p.x); p.state=1; p.vx*=0.5; }
} else { // on ground
const s=terrainSlope(p.x);
p.vx += s*G*0.35*dt * Math.sign?
```
slope = terrainY(x+1)-terrainY(x-1) / 2 (positive means downhill to the right). Acceleration vx += s * G * 0.3 * dt... units: s is dimensionless (dy/dx in px), G is px/s². If s>0 the downhill direction is +x, so a_x = G * s / sqrt(1+s²) ≈ G*s. Use p.vx += G*s*0.4*dt; friction p.vx *= Math.pow(0.5, dt*2)? Use p.vx -= p.vx*1.5*dt. Then p.x+=p.vx*dt; p.y=terrainY(p.x). If |p.vx|<4 and p.age> p.life*0.7, mark as settled (no movement) to save CPU.
Simplify state 2 = settled.
Ash update: vy decreases toward riseSpeed (buoyancy) — give initial vy upward, apply light gravity or negative buoyancy: p.vy += (p.targetVy - p.vy)*dt; targetVy = -20 (rises). vx += (wind*30 - p.vx)*dt*0.5. Size grows. Alpha fades with life.
Eruption logic:
```
pressure += dt * rate; if pressure>=1 { erupting=true; eruptT=eruptDur; pressure=0; }
if(erupting){ eruptT-=dt; spawn lava... if eruptT<=0 erupting=false; }
```
Eruption duration might scale with power: 1.5 + power*0.3.
Lava spawn during eruption: count per frame = (base 6 + power*2) * amountFactor. Random angle: mostly upward, spread. speed = (300+Math.random()*250)*power/5.
G = 600 px/s².
Also add slight ambient lava glow in crater while pressure>0.5? Add glow intensity with pressure.
UI: div with sliders and values, overlay. Counters: "Particles: N | FPS: 60".
Ash also spawns during eruption, a small amount of ambient ash always.
Particle cap: maxCount = particlesSlider (e.g. 1000-4000).
Performance: avoid shadowBlur; use two-arc technique for glow, or single arc with 'lighter'. ~2000 arcs per frame with two circles each = 4000 arcs — canvas can handle at 60fps on most machines. Use small glow radius.
Alternative: draw lava to a low-res offscreen and blur... Keep it simple.
Let's write the full HTML.
Color ramp table:
```js
function mix(a,b,t){return [a[0]+(b[0]-a[0])*t, ...];}
const C1=[255,244,180],C2=[255,150,40],C3=[190,55,25],C4=[45,32,32];
ramp=new Array(32);
for(let i=0;i<32;i++){const t=i/31; let c;
if(t>0.7)c=mix(C2,C1,(t-0.7)/0.3);
else if(t>0.35)c=mix(C3,C2,(t-0.35)/0.35);
else c=mix(C4,C3,t/0.35);
ramp[i]=`rgb(${c[0]|0},${c[1]|0},${c[2]|0})`;}
```
Also alpha: fade in last 25%: if t<0.25 then alpha = t/0.25, else 1. But with 'lighter' compositing, per-particle alpha requires globalAlpha — fine.
Actually the dark rock in a 'lighter' context just adds a bit — good, fades.
Draw: for lava, pass 1 with 'lighter': glow circle radius*2, low-alpha color, then main circle.
For each particle maybe: ctx.globalAlpha = a*0.25; arc r*2.2; then globalAlpha=a; arc r.
FPS: ema.
Counter text update every ~250ms.
Sliders:
- Power: 1–10, default 5
- Wind: -5–5, default 1
- Amount: 300–3000, default 1400
Particle amount affects spawn rate: amountFactor = amount/1400; max particles = amount.
Let me write it.
Also make canvas scale to window? Fixed 960x540, style max-width 100%. Fine.
Volcano offscreen texture: gradient fill of dark brown/gray rock, plus some random specks, dark crater interior, ground with a gradient.
Eruption flash: while erupting, draw radial gradient at crater with intensity varying sin/noise.
Ambient: pressure glow at crater proportional to pressure.
Now let me compose the HTML.