### PARTIAL OUTPUT (time limit hit; the model was still generating) ###
=== thinking so far ===
The user wants a 2D volcano physics simulation in a single self-contained HTML file. Let me plan this carefully because there's a lot to get right:
**Core requirements:**
1. Side view: volcano cone with crater, ground, sky
2. Pressure meter that builds over time, maxes out → eruption, resets, builds again
3. Lava particles launched with varied velocities, gravity arcs, land on slopes, slide/settle downhill
4. Lava cooling: bright yellow-orange → dark rock → disappear
5. Ash cloud smoke particles rising from crater, drifting with wind
6. Three sliders: eruption power, wind strength/direction, particle amount
7. Live counter: active particles + FPS, smooth at 1000+ particles
**Technical design decisions:**
*Terrain/slope collision:* The volcano needs a heightmap function `terrainY(x)` that defines the cone profile. For performance with thousands of particles, I'll pre-sample the terrain into a Float32Array (e.g., one sample per pixel or every 2px) and do fast lookups. Particles land when y >= terrainY(x), then they slide downhill based on slope.
*Cone profile:* A classic stratovolcano shape — concave slopes steepening toward summit, with a crater notch at the top. I can build it from a function: base height plus a cone shape `h = coneHeight * (1 - |x - cx|/baseRadius)^k` with power k ~ 1.6 for concave flanks, then carve a crater dip near the summit. Also add some gentle noise for natural look, but keep it smooth so sliding works. Actually keep terrain smooth-ish (small noise) — jitter in heightmap can make particles jitter. I'll add slight noise but smooth it.
*Particle system performance:* For 1000+ active particles (the "particle amount" slider should go up to maybe 2500), I need:
- Object pools (no GC churn). Pre-allocate arrays.
- Use typed-array-style plain arrays or classes in a pool. Class instances are fine; the key is reusing them.
- Render: draw each particle as a small rect/circle. `fillRect` is fast. Circles via arc are slower. For glowing hot lava, I could draw with fillRect and rely on slight alpha. For a nice look, hot particles get a radial glow — but radial gradients per particle per frame are expensive. Better approach: pre-render glow sprites (offscreen canvases) at several heat levels, then `drawImage` them. drawImage of small sprites is fast. I'll pre-render maybe 8 heat-level sprites for lava and a few for smoke, then drawImage scaled. With 2500 particles that's 2500 drawImages/frame — that's generally OK on modern hardware at 60fps, especially small sprites. Alternatively, use direct pixel manipulation via ImageData for lava particles — very fast but no glow blending. Hmm.
Actually a solid approach: composite rendering in two passes:
- Terrain, sky: draw each frame (or cache sky + static terrain as offscreen canvas, redraw only when needed — terrain is static! I can cache terrain+sky to an offscreen canvas and just drawImage it once per frame. Good optimization.)
- Lava particles: drawImage sprites with per-particle size/heat.
- Smoke: drawImage sprites with alpha.
To make lava look like it accumulates on slopes: when a lava particle settles, instead of deleting it immediately, I could "paint" it into a persistent lava field canvas (a static accumulation layer) with its current cooled color. That gives a beautiful visual record of eruptions — dark rock builds up on the slopes over time. Then particles get recycled. This is a great "signature moment": the mountain visibly darkens with each eruption's deposits. I'll paint the settled particle into an offscreen "deposit" canvas (same size as terrain area) as a small dark blob. Over many eruptions it accumulates. Maybe occasionally let it fade slightly? No — rock persists; that's realistic and memorable. But it could fully cover the mountain... I could paint it with some transparency and dark color, and maybe slowly fade the deposit layer very gradually over minutes (like weathering). A slow fade of like 1/(many seconds) toward transparent would keep it from saturating. Or just let it build — the user can reload. I'll apply an extremely slow fade (destination-out with tiny alpha every few seconds) so it reaches an equilibrium. Actually simpler: when painting deposits, use low alpha (~0.25), and every N seconds do a very low-alpha clear of the deposit layer (destination-out at 0.01). That gives dynamic equilibrium — old rock weathers away as new lava arrives. Nice.
Hmm, but deposit layer interacts with sliding: settled lava particles paint then vanish, so terrain doesn't change. Fine — real lava films are thin anyway.
*Sliding behavior:* When a particle lands (y >= terrainY), it enters "sliding" state: it moves along the surface downhill. Downhill direction = sign of slope at its x. Velocity along surface: give it initial residual velocity, damp with friction, and only keep sliding if the slope is steep enough relative to friction (angle of repose). Simple approach: while sliding, apply gravity's tangential component `g * sin(theta)` projected onto the surface, with friction opposing motion; particle sticks when speed < threshold and slope is shallow. Steeper slopes near summit → lava flows further down. I'll compute slope from heightmap samples. Also give cooling: while sliding it cools faster (spreading out), eventually freezes (stops) — then paint deposit and recycle. Also viscosity: as it cools, friction increases, so flows start fast and stop. That's physically evocative.
Also particles can bounce off the surface when impact velocity is high (molten spatter): on landing, if vertical speed high, reflect with restitution and lose energy; else start sliding. This makes the ballistic phase look right — some rocks bounce down the slope.
*Cooling lifetime:* Each lava particle has `heat` from 1 → 0. Heat decays over time, faster when sliding (spreading) or based on random per-particle rate. Color maps heat:
- heat ~1: white-yellow core (255, 240, 150)
- ~0.7: orange (255, 150, 40)
- ~0.4: deep red (200, 60, 20)
- ~0.15: dark red crust (90, 30, 20)
- 0: near-black rock (40, 28, 24)
When heat hits 0 (or particle age ends), if it's in flight, just fade out; if settled on surface, paint deposit then recycle. Also flying particles that cool in air become dark "bombs" — they can still fall and land, painting a dark blob. Nice touch: volcanic bombs stay dark. So flying lava that cooled should still collide with terrain and paint. Yes — keep collision for all lava particles regardless of heat; cooling only changes color and lifetime. A cooled airborne particle hitting ground → paint dark deposit, recycle.
Heat sprite lookup: precompute sprite index from heat (quantize into, say, 20 levels) — or compute color directly and draw colored circles? For glow, sprite approach with pre-rendered radial-gradient blobs at each of N heat levels is best. I'll pre-render ~24 lava heat sprites (radial gradient, white-hot core → transparent) and ~12 smoke sprites (soft gray puffs). drawImage with per-particle scale. To avoid `Math.floor` costs... negligible.
*Ash/smoke:* Smoke particles spawn at crater during and after eruption, rise (buoyancy: negative gravity partial), expand over time, fade out, drift with wind. Wind slider (-10..+10) pushes smoke and also slightly affects lava arcs (drag). Smoke should also billow: size grows, alpha decreases. Spawn rate tied to eruption intensity and pressure. Also a persistent weak plume when pressure is high (pre-eruption fumaroles) — nice storytelling detail: as pressure builds, smoke wisps escape the crater.
*Pressure meter:* Builds at a rate (maybe slightly randomized, with small tremor jitter near high pressure). When it hits 1 → eruption event: over an "eruption duration" (~2–4s), spawn lava particles in bursts from crater with velocities scaled by power slider (slider 0.5–2×). Velocity cone: mostly upward with spread; higher power → higher speeds and more violent spread. Pressure resets to 0 quickly during eruption then rebuilds. Also the meter UI: a vertical/horizontal gauge with needle, and during eruption it spikes to max then drops. I'll draw the meter on canvas (part of the HUD) — or in DOM? Canvas HUD keeps it unified and I control typography. I'll draw HUD in canvas: pressure bar with tick marks, FPS, particle count. Actually mixing DOM for sliders + canvas HUD is fine. Let me draw pressure meter on canvas near the volcano base (like an instrument), and stats top-left in canvas. Sliders in DOM overlay panel.
*Eruption sequence:* Eruption is a timed event with intensity envelope (fast attack, decay): intensity = f(t since eruption start). Lava spawn rate = base * intensity * particleAmountMultiplier. Crater "flash" at start (bright glow at vent), screen shake maybe subtle (translate canvas slightly — a small, brief shake adds impact; keep it short and modest). Also initial shockwave? Could do a translucent expanding ring... maybe a bright vent flash + smoke plume surge is enough. Add rumbling: particles on slopes get slight jitter while eruption active (tremor). Nice touch.
*Wind:* Slider from -10 to +10. Affects smoke strongly, lava slightly (small drag acceleration proportional to wind on airborne particles). Also wind should be visualized: maybe drifting streaks in the sky? Could add faint wind streak particles... Keep scope sane; smoke drifting shows wind. Maybe a small flag/indicator in HUD showing wind arrow. I'll draw a small wind gauge arrow in the HUD that responds live.
*Particle amount slider:* Controls lava spawn multiplier AND smoke count; but the requirement says simulation must stay smooth with at least 1000 active particles. I'll make slider range e.g. 200–3000 particles (as a budget/cap for lava). Default maybe 1400. Pool size = max (3000 lava + 800 smoke). With pooling and sprite rendering it'll hold 60fps.
Let me also ensure "particle amount" affects the eruption immediately — spawn rate scales with the setting, and effective cap.
*FPS counter:* exponential moving average of 1/dt, update display every ~0.25s.
**Visual design — making it look authored:**
I want this to feel like a crafted natural scene, not a flat diagram. Ideas:
- Time-of-day: a dusk sky? A deep twilight sky with warm horizon gradient... wait, I avoid decorative gradients, but a sky gradient is a *representational* gradient (an actual sky), which is legitimate — it depicts atmosphere, not decoration. A dusk sky (deep blue-gray at top → warm amber near horizon) with the glowing lava contrast is gorgeous and justified. Yes: dusk/dawn scene gives dark backdrop making the glowing lava pop dramatically. The dark mountain silhouette with incandescent fissures is the classic Arenal/Mt. Pelée imagery.
- Stars? Faint stars in the upper sky — tiny dots, subtle. Adds atmosphere cheaply. A few dozen faint stars drawn into the cached sky layer.
- Moon? Maybe a thin crescent — could be nice but risk of kitsch. A small pale moon disc low near horizon... I'll include a small moon with slight glow, subtle. Hmm, keep it minimal — stars only, plus maybe a very subtle moon. I'll do stars; skip moon to keep the composition focused on the volcano.
- Ground: dark plains with subtle texture, maybe faint distant ridges silhouette layers for depth (two parallax-ish silhouette hills behind the volcano — they're static, drawn into the cached background).
- The volcano: dark rocky silhouette with subtle stratification bands (darker strata lines) and a slightly lighter rim on the windward... Actually since the scene is dark, the cone should be a dark charcoal mass with faint ambient detail: strata lines, some rocky texture noise. Plus a warm rim light on crater edge? A faint ember glow inside the vent (visible magma in the crater throat that brightens as pressure rises!) — that's a great storytelling detail: the crater throat glows progressively as pressure builds, telegraphing the eruption. YES — this is the "pressure made visible" idea: the vent glow intensity = pressure. Plus faint smoke wisps at high pressure. Signature moment.
- Also maybe faint ember particles drifting from vent at high pressure (pre-eruption sparks).
- Ash cloud: smoke sprites from grayish-dark to light gray as they expand (ash clouds lighten as they widen? Actually volcanic ash plumes are dark brown-gray at base, lighter at top). I'll give smoke a color ramp from dark (young, dense) to pale gray (expanded), plus slight warm tint near vent.
- Eruption lightning! Volcanic lightning is a real, spectacular phenomenon. Occasional lightning bolts inside the ash plume during peak eruption — jagged lines drawn with quick fade. That's a memorable touch and cheap to implement (random polyline from plume region, drawn 2-3 frames). I'll add it — it's a genuine volcanic feature (pyrochromatic lightning). Subtle, brief, inside the plume only.
**UI layout:**
- Canvas full-window.
- DOM overlay: a control panel (bottom-left or right) with three sliders. Style: dark glass panel, custom-styled range inputs (thin track, custom thumb), labels with values. Typography: I'll pull a distinctive font from Google Fonts CDN (allowed — external CDN references are fine). Something with character for the title, e.g. "Fraunces" or a display serif for the title "VULCAN" ... The user said "No external libraries: everything inline" — that refers to JS libraries; a font link is a stylesheet not a library... risky interpretation. "everything inline, rendered on a canvas" — I think they mean no JS libraries; the canvas does the rendering. A Google Fonts is an external resource though. To be safe and honor "no external" strictly, I'll use no external anything: pure system stack but crafted — but I dislike Arial/system-ui as identity font... The constraint conflict: user explicitly says no external libraries, everything inline. Fonts aren't libraries, but spirit of the request is self-contained. I'll respect the strictest reading: no external requests at all. For typography, I can use font stacks with character: `Georgia, 'Times New Roman', serif` for the title gives a classic naturalist/journal feel, and letter-spaced small caps for labels via `font-variant: small-caps` or uppercase + tracking. A serif title ("Vulcan / a pressure study") in warm ivory on the dark scene looks authored. For the HUD numerals on canvas, use a monospace stack (`'Courier New'`... hmm) — canvas HUD text: I'll use `font: '13px ui-monospace, Menlo, Consolas, monospace'` — fine for readouts (instrument feel). Title in DOM panel: serif with wide tracking. That's a deliberate typographic voice without external fonts.
Actually — since everything's rendered on canvas per the user, maybe they'd like HUD on canvas too. But sliders must be DOM inputs (input[type=range]) unless I build custom slider interactions on canvas. Building custom canvas sliders is more work and less accessible; DOM sliders overlaid are standard and the user explicitly asked for "three sliders the user can move while it runs" — DOM range inputs are the sane choice. I'll do DOM panel for sliders + title, canvas for scene + readouts (pressure gauge, stats).
Panel position: top-right or bottom-left. The volcano is centered; HUD stats top-left; panel bottom-right maybe, or right side. Bottom-right panel with sliders. Pressure gauge drawn on canvas — where? Left side vertical gauge looks instrument-like, or bottom-center horizontal. I'll draw a vertical pressure gauge on the left edge with tick marks and a needle — like an old brass manometer. Label "PRESSURE" vertically. And during eruption it maxes, flashes red-ish... use warm amber. Colors of the UI: warm ivory text, ember amber accents, dark translucent panels — coherent with the scene.
Wind control: slider -10..+10, center 0. Show current value and direction arrow in panel (or canvas gauge). In the panel, show "wind −4" with a small arrow character (→/←) — text arrows are fine (not emoji). Or draw the wind needle on canvas near the gauge. I'll add a small canvas wind vane next to pressure gauge: a horizontal line with a moving indicator. Actually simpler: show numeric value in the DOM label that updates, and let the smoke show it. Plus I'll draw wind streaks in the sky (thin faint horizontal streaks moving with wind) — this makes wind perceptible even without eruptions and animates the sky. Subtle: 20 streak particles, alpha ~0.05-0.1, length proportional to |wind|. When wind is 0 they vanish. Nice.
**Physics details:**
Gravity g = ~900 px/s² (tune). Lava launch from crater vent at (cx, ventY):
- speed = base * power * (0.7 + 0.6*rand)
- angle: mostly vertical with spread: θ = -90° ± ~35°, biased random (gaussian-ish). Higher power → narrower, faster column plus some wide spatter. I'll mix: 70% "jet" (narrow cone ±15°), 30% "burst" (±55°). Also lateral randomness from wind.
- Actually realistic fountaining: velocities 250–650 px/s vertical. Tune so with high power lava reaches near top of screen and lands on slopes at varying distances.
Airborne integration: semi-implicit Euler with dt clamped. Add wind drag: ax += wind * k (k small, ~2–8 px/s² per wind unit... make wind slider -10..10 and drag = wind * 6 px/s² for lava; smoke uses more). Also maybe slight drag proportional to velocity for lava (negligible; skip).
Collision: sample terrainY(x). If y >= terrain, check impact speed. If vy > threshold (e.g. 140) and heat > 0.3 (still molten — molten splats, rock bounces? actually rock bounces, molten splats!). Hmm: molten lava hitting slope at speed → splats and sticks (starts sliding with damped velocity); cooled bomb → bounce with restitution 0.3, some friction on vx. But visually, bouncing dark bombs look great. Molten hot splat: kill vertical velocity, convert to sliding with tangential velocity = projection of velocity onto slope surface * 0.6 (viscous damping). Let me do: if hot (heat>0.35): splat — enter sliding with strong damping. If cool: bounce with restitution 0.35, tangential friction 0.7; when bounce energy too low, enter rolling/sliding briefly then stop & paint.
Sliding state: position on surface (x, terrainY(x)). Compute slope s = (terrainY(x+2)-terrainY(x-2))/4 — note canvas y grows downward, so terrain height decreases... let me define h(x) = ground elevation above sea level in px, and terrainSurfaceY(x) = H - h(x) for drawing. For physics, work with elevation. Downhill = direction of decreasing elevation. Tangential gravity accel = g * sin(θ) where sinθ ≈ -s/√(1+s²) in the downhill direction. Simpler: vx += g_elevation_effect... Let me just do: slopeAngle-based: downhillAccel = g * (dh/dx in downhill direction)/... Cleanest: let e(x) = elevation. Particle at x moving with velocity u (dx/dt). Acceleration along surface: a = -g * sinθ * sign? The tangential component of gravity is -g * ∂e/∂x / √(1+(∂e/∂x)²) (pointing toward decreasing e). So u += (-g * e'(x) / √(1+e'²)) * dt. On flat ground e'=0 → no accel, friction stops it. Good — this naturally handles both slopes and flats.
Friction: u -= u * μ_eff * dt (viscous). μ_eff grows as heat drops (cooler = stickier): μ = 1.5 + (1-heat)*6. Also static condition: if |u| < 4 and |g*tanθ|-ish below friction threshold → freeze: paint deposit, recycle. Threshold: slope must be shallow enough; approximate: if |u|<4 and |e'(x)| < 0.35 (about 19°, angle of repose-ish) → freeze. On steep cone flanks e' can be ~2-3 (60°+) near summit — lava slides far. But the base apron is shallow → freezes there.
Also while sliding, keep y glued to surface: y = terrainSurfaceY(x) - particleRadius. Paint deposit when frozen (or when heat reaches 0 while sliding → freeze immediately).
Heat decay: heat -= dt * coolRate; coolRate random per particle in [0.15, 0.45]; sliding multiplies by ~2.2 (spreads & crusts). Lifetime also capped (e.g. 6–14s) so particles eventually vanish even if flying forever (with wind they can drift off-screen anyway — recycle when off-screen x or below ground... below ground can't happen with collision; off screen sides or above top → just let them fly, recycle when out of bounds by margin).
Wait — must handle smoke similarly: rises, expands, fades; recycle when alpha < 0.01 or off-screen.
Deposit painting: draw a small circle (r ~ particle size * 1.1) on deposit canvas at (x, y) with color = cooled rock color with alpha 0.28 + slight variation. Deposit canvas starts transparent; composited between background and particles? Order: sky/stars (cached bg incl. terrain silhouette? terrain drawn first, then deposits ON TOP of terrain, then hot lava particles above, smoke above all but behind... Let me define layering:
1. Sky cached layer (sky gradient, stars, distant ridges).
2. Foreground terrain (cached separate canvas: the cone + ground with strata) — includes vent glow hole? The vent glow changes with pressure, so draw terrain first, then vent glow dynamically over the crater area (radial gradient with alpha = pressure) with 'lighter' composite, clipped to crater region.
3. Deposit layer canvas (accumulated cooled rock) — drawn over terrain.
4. Lava particles (sprites, composite 'lighter' for hot ones so they glow additively over dark scene; cool ones normal composite). Hmm mixing composite modes per particle: set globalCompositeOperation 'lighter' only when heat > 0.55, else 'source-over'. Batch: sort not needed; just switch when needed. Simpler: always draw hot with lighter, cooled with source-over — just set the op per particle (state changes are okay-ish; could sort by op but 2500 state changes per frame is fine on modern browsers honestly, though it does cost. Alternative: two passes over particles? Iterating the pool twice doubles loop cost but loops are cheap. I'll do: pass A draws hot (lighter), pass B draws rest (source-over). One branch-free loop each. Actually just one loop with an op check is fine; browsers handle composite op changes OK. Hmm, to be safe for 60fps at 3000 particles, I'll do two loops — memory-local pool iteration is trivial. Fine, two passes.
5. Smoke: source-over with alpha, drawn... smoke plume should render behind hot lava bombs? Ash plume rises above crater; bombs punch through it. Draw smoke BEFORE lava so bombs appear in front. But smoke should be over terrain obviously. Also smoke over deposits. Order: deposits → smoke → lava. Good.
6. Vent glow, eruption flash ('lighter').
7. Lightning.
8. Wind streaks (very faint, 'lighter' or source-over low alpha).
9. HUD (pressure gauge, stats) — canvas-drawn.
10. Screen shake transform applied to world layers (1–9 world content), HUD unshaken. Implement via ctx.translate for world drawing.
Sprite pre-render:
- Lava heat ramp sprite set: for i in 0..23, heat = i/23, create canvas 32×32 (scaled at draw time), radial gradient: core color (white-yellow at high heat → dark red → near black), radius with soft falloff. Actually I want particles to look like glowing blobs with hot core: gradient stops: 0: bright core, 0.35: mid color, 1: transparent-ish darker. For cooled particles, they become dull rock — sprite with soft dark edges.
Let me define color ramp function heatColor(t):
t=1: rgb(255, 245, 190)
t=0.85: rgb(255, 200, 80)
t=0.6: rgb(255, 120, 30)
t=0.4: rgb(215, 65, 18)
t=0.2: rgb(120, 32, 14)
t=0: rgb(52, 36, 30)
Interpolate through stops. Sprite gradient: center = lighten(color, +30%), edge = darken(color), alpha edge 0 for hot (glow fade) — for hot sprites use radial gradient from color → color(darker) → transparent. For deposit color use heat at freeze time.
Smoke sprites: 10 variants of soft noise puff: radial gradient with slightly irregular shape (draw several overlapping blurred circles) in gray tones. Tint per particle from dark (young) to pale (old). I'll pre-render one or two neutral puff sprites and use globalAlpha + maybe per-particle tint via two pre-built ramps (dark puff sprite set and light puff sprite set, cross-fade by age — or just 12 ramp sprites like lava). Do ramp: smoke age t 0→1: color from rgb(70,60,58) → rgb(180,175,170). 12 sprites with soft alpha.
Smoke physics: vy: buoyancy -g*0.25 rising plus initial upward velocity from eruption; expand size 6→60px; alpha in/out (fadeIn 0.15, hold, fadeOut as size grows: alpha ~ (1-age)^1.2 * 0.5). Wind: vx += wind * 14 * dt (stronger response than lava) plus slight turbulence: vx += sin(noise) jitter — use cheap pseudo-noise: sin(age*3 + seed)*k.
Crater geometry: vent opening width ~46px at summit. Carve crater: elevation dips in [cx-w, cx+w] with smoothstep to a floor below rim. Particles spawn at vent floor center. Vent glow: radial gradient centered slightly below rim, radius ~55, alpha 0.15+0.75*pressure² in 'lighter', warm color.
**Pressure model:**
pressure ∈ [0,1]. Build rate: base 0.055/s * (1 + 0.35*sin(t*0.13) jitter) → full build ~18s. Slight acceleration as it grows? Volcanoes: pressure accelerates near failure — rate increases nonlinearly: dp = rate * (0.5 + pressure). When pressure ≥ 1 → erupt(). During eruption: eruptionT += dt; envelope env = exp(-eruptionT/1.6) * smooth attack (min(1, eruptionT/0.25)); pressure drains: pressure = max(0, 1 - eruptionT/1.4) roughly — drains over ~1.4s then rebuild from 0 (plus maybe slight residual). Eruption lasts until env < 0.03 → eruptionActive=false. Lava spawn during eruption: spawnAccumulator += dt * spawnRate * env * powerMult; spawnRate base ~ e.g. 420 particles/s scaled by particleAmount slider (slider 0.2–2.5 multiplier; default 1). Cap active lava at budget: if activeCount >= budget, skip spawn (pool full). Budget = round(200 + slider 0..1 * 2800)? Slider "particle amount" 0–100 → budget 250–3000. Default ~60 → 1900. Plus smoke budget separate ~600, spawn smoke: rate ~ env * 60/s * slider.
Also tremor: while eruptionActive, camera shake amplitude ~ env * 5px; also slope particles jitter.
Pre-eruption signs: pressure > 0.55 → weak fumarole smoke wisps (rate ~ (pressure-0.55)*3/s), vent glow rising, occasional tiny ember sparks (small hot particles with low velocity that pop out and die quickly).
**Lightning:** during eruption when env high, small probability per frame (e.g. if rand < env * 0.04 per frame at 60fps → frequent-ish; tune to ~ every 0.5-1.5s at peak). Bolt: generate jagged polyline from a point in the plume (above vent, within smoke region) downward/branching, 6–9 segments, random offsets; draw with 'lighter', lineWidth 2 core white-violet (volcanic lightning is bluish!) — pale blue-white (200, 210, 255) fits and looks distinct from lava's warm palette; that contrast is authentic. Bolt life ~0.12s with flicker alpha. Also maybe a secondary branch. Keep subtle.
**Terrain construction:**
Canvas W×H = window size (handle resize: rebuild terrain caches on resize; particles positions may exceed — acceptable, terrain heightmap resampled; particles in flight adjust fine).
Elevation function e(x) (px above bottom):
- ground baseline: groundH = 0.09*H + gentle noise ±4px (rolling ground).
- Cone: cx = 0.5W (maybe 0.52W slightly off-center for composition — off-center is more authored; let's put summit at 0.46W so there's breathing room on the right where wind-blown ash drifts. Hmm but panel bottom-right... fine).
- coneH = 0.42H. baseR = 0.40W.
- shape: cone(x) = coneH * (1 - (|x-cx|/baseR))^1.7 for |x-cx| < baseR, else 0. This gives concave-up flanks? (1-u)^1.7: at u→0 derivative 1.7 — steep near summit (slope infinite at summit in this param), gentle at base. Good stratovolcano profile.
- crater: subtract a smooth notch: craterDepth = 0.055H; width cw = 0.045W... Let me carve: for |dx| < craterW: e -= craterDepth * (cos(π dx/craterW)*0.5+0.5)? That makes a dip deepest at center. Actually crater should be a bowl: rim high at |dx|=craterW edges, low center. So subtract bumpAtCenter: e -= craterDepth * (0.5+0.5*cos(π * dx/craterW)) — at dx=0 subtract full depth, at edges 0. But then summit peak height: cone at |dx| small ≈ coneH(1-|dx|/baseR)^1.7 minus bowl → rim height slightly below coneH. Fine — the crater rim will have two little peaks. Good look.
- noise: add smooth noise: sum of sines: n(x) = a1*sin(x*0.011+φ1)+a2*sin(x*0.023+φ2)+a3*sin(x*0.05+φ3), amplitudes ~ (7, 4, 2.5) px, but scale noise down on the cone (×0.6) to keep slopes clean? Slight roughness on slopes is good for lava catching. Keep small everywhere: total ±8px on ground, ±4 on cone. Multiply noise by mask 1 everywhere; small values fine.
- Sample into heightmap array with step 2px for lookup speed: `elev[i]`, plus for slope compute from neighbors at draw/physics time (finite difference of the array — cheap).
terrainSurfaceY(x) = H - elev(x) (y coordinate). For physics I use elevation directly.
Ground to the left/right continues to screen edges; the cone base blends. Also distant ridges behind: two silhouette layers at lower height, drawn in sky cache with bluish dark tones (atmospheric perspective).
Drawing the terrain into cache: fill path along surface down to bottom; color: very dark warm gray-brown (#17120e-ish) with subtle vertical strata: draw strata as slightly lighter/darker horizontal-ish curved bands clipped to cone... simpler: after filling cone silhouette, apply a few semi-transparent darker strokes following elevation contours (for several elevation levels, draw a line along surface offset downward by fixed px with low alpha). And faint noise speckle (random dots) for rock texture at low alpha. Edge highlight: 1px lighter stroke along surface top (rim lighting from sky) with warm faint color, stronger on crater rim. This gives crafted look.
Ground texture: sparse dark speckles.
Sky: gradient from #0b1026 (deep blue) top → #1a1430 mid → #3d2a2a...? Let me craft dusk palette: top #070b1a, mid #141830, horizon #4a2f24 → thin warm line #8a5a33 near horizon behind ridges. Subtle. Stars: ~90 dots random in upper 60%, alpha varying 0.2–0.8, size 1–1.6, slight twinkle? Static in cache (twinkle would need dynamic draw — could add 6 twinkling stars dynamic; skip for perf simplicity, static fine).
**HUD design (canvas):**
- Top-left: "PARTICLES 1 842" and "FPS 60" — monospace, small caps labels, values in amber. Also maybe eruption status line: "STATUS: BUILDING / ERUPTING".
- Left edge: vertical pressure gauge: a slim column ~18px wide, 140px tall with border, ticks every 10%, fill from bottom with warm color intensifying to red at top... gradient inside the meter — a meter fill going amber→red as it fills is representational (pressure danger), acceptable and standard for gauges. I'll fill solid amber with the top portion (last 25%) turning redder — implement as two-segment fill or gradient — fine, it's a gauge. Needle line at current level. Label "PRESSURE" rotated or below. Add small marker at eruption threshold.
- Also mini wind indicator next to gauge? The panel shows wind value; plus sky streaks show it. Enough.
DOM panel (bottom-right): dark translucent (rgba(10,9,12,0.72)), 1px border rgba(255,255,255,0.08), border-radius small (10px), backdrop blur slight. Title block: small caps "VULCAN · 2D" ... Title: "VESUVIUS" or invented "MONTE EMBER"? I'll title it "VULCANO — pressure study" hmm. Keep tasteful: serif italic title "Volcano" with letterpress feel: font-family Georgia 'Playfair'-less. Title: "Vulcan's Forge"? Simpler: "ERUPTION" small-caps serif with thin rule under, subtitle "a live pressure study". Then three sliders:
- ERUPTION POWER: 0.5–2.0 (default 1.0) → affects launch speed & burst rate.
- WIND: −10..10 (default 0? slight 2 gives life; default 2) — actually default 0 cleaner; but a gentle default breeze makes ash drift immediately → choose default +3.
- PARTICLES: 250–3000, default 1400. Display value.
Each slider row: label row (name left, value right in mono amber), input range full width. Custom styling: -webkit-slider-thumb: 14px circle ivory; track 3px rgba white 0.15 with filled portion? Filled portion needs JS to set background gradient on track (linear-gradient two-stop hard cut — that's a functional fill indicator, fine and standard). I'll do it: track background: linear-gradient(90deg, amber p%, rgba(255,255,255,.14) p%) updated on input. For wind (bidirectional) keep simple neutral fill from center? Simpler: just uniform track, value shown numerically. OK do the filled track for all, from left; for wind it still reads fine.
Also a small hint line: "eruptions trigger automatically when pressure maxes" tiny muted text. Maybe also a "TRIGGER NOW" button? Not requested... "when it maxes out, the volcano erupts" — automatic. Adding a manual trigger button is extra but useful and harmless — small text button "Trigger now". It's beyond spec but genuinely useful for interactivity. I'll add it subtly (also maybe a "reset slope" to clear deposits? skip). Keep one small button.
**Performance considerations recap:**
- Terrain cached: 2 offscreen canvases (sky+ridges, terrain).
- Deposit canvas: offscreen, additive painting; slow weathering: every 4s apply 'destination-out' fill alpha 0.02? That fades deposits uniformly. Equilibrium between paint rate and fade. Painting during heavy eruption: thousands of small arcs — use fillRect with rotation? Small circles via arc per frozen particle — freezing happens maybe hundreds per eruption, spread over time; fine.
- Particle pool: preallocate arrays: use plain objects in array with `active` flag, freelist stack for recycling. Iterate full pool each frame (3000 lava + 900 smoke = 3900 objects) — trivial.
- Sprite drawImage: lava sprite base 28px, draw scaled to particle size (varies 3–9px * 2 for glow ~ 2.2× radius). Hmm glow radius bigger: drawImage size = size*3.2 for hot glow. Fine.
- Avoid per-particle save/restore; use setTransform? drawImage(img, x-w/2, y-h/2, w, h) simplest. Alpha via globalAlpha per particle (cheap).
- Text HUD: draw every frame; text rendering is fine (few strings). Update FPS text every 0.25s to avoid layout thrash (canvas has no layout; just avoid re-reading; cheap anyway).
- devicePixelRatio: cap at 1.5 for perf; scale canvas. I'll support dpr up to 2 but scale sprite drawing accordingly... Simpler: use dpr = min(devicePixelRatio, 2) and set canvas width = W*dpr, ctx.scale(dpr,dpr) once (reset transform each frame with setTransform(dpr,0,0,dpr,0,0) + shake translate). drawImage positions in CSS pixels. Good.
Resize: rebuild caches; keep particle pool size fixed (based on max budget 3000+smoke). On resize, remap particle x positions? Just clamp out-of-range; minor artifact acceptable. Debounce resize.
**Numbers tuning (important for feel):**
- H ≈ 800 typical. Gravity 1000 px/s².
- Vent position: cx = 0.47W, vent elevation = elevation at crater center. Launch y = surfaceY(cx) - 4.
- Power slider p ∈ [0.5, 2]. Launch speed: v = (260 + 340*p) * (0.75 + 0.5*r) → at p=1: 600*(0.75..1.25) = 450–750 px/s vertical-ish → apex h = v²/2g ≈ 100–280px above vent. Hmm I want dramatic fountains at high power reaching maybe 400+ px. At p=2: v up to (260+680)*1.25=1175 → apex ~690px. Good range. Angle: jet: θ = 90° ± 12° (from vertical), burst: ±50°. Let me define: u = random(); if u<0.72 → narrow. Also slight bias: multiply horizontal by 1.
- Lava particle radius: 2.2 + 3.2*rand (visual size; a few big "bombs" up to 7px: 8% chance size 5–8).
- Smoke spawn: at vent, initial vy -40..-140, vx ±30 + wind*8; size start 8–16 growing to 30–90; life 3–7s; alpha peak 0.42.
- Wind effect on lava: ax += wind * 9. Smoke: wind * 26 plus turbulence.
Sliding tuning: friction viscous: u *= exp(-μ*dt)? Use u -= u*μ*dt with μ = 2 + (1-heat)*7. Sliding heat decay ×2. Freeze condition: |u| < 6 && |slope e'| < 0.30 → freeze (paint, recycle). Also freeze if heat <= 0 regardless (crusted in place — but on steep slope it should still creep? if heat 0 and moving on steep slope, real rock would... it's molten outside? Simplify: heat 0 → freeze instantly). Also cap slide duration.
Edge case: particle lands on steep flank near summit, slides down gaining speed — with viscous friction terminal velocity ≈ g*tan-ish/μ; near summit slope huge (e' maybe 3+) → sinθ ~ 0.95 → terminal ~ 1000*0.95/ (2+…) → fast; then flattens near base → friction kills it → freezes on apron.
Also sliding particles can re-heat? No.
Tremor jitter for settled... settled ones are recycled (painted), so no jitter needed except airborne unaffected. During eruption, sliding particles get small random lateral kick — adds life. OK.
**Screen composition:**
- Volcano summit around 0.42–0.5 vertical from top? coneH 0.42H means summit at H - (groundH + coneH + noise) ≈ H - (0.09H+0.42H) = 0.49H. So summit roughly mid-screen — leaves sky above for fountain. Good. Distant ridges: heights 0.16H and 0.10H at edges.
- Horizon line: ground fills bottom ~9% + cone.
**Eruption pressure reset:** "pressure resets and builds again on its own" — after eruption, pressure drains to 0 during first ~1.2s of eruption then rebuilds. Implement: during eruption phase, pressure = max(0, pressure - dt*0.8); after eruption ends, rebuild resumes. Also small chance of double eruption? Keep simple.
**Status text:** BUILDING PRESSURE → ERUPTION (with live env) — displayed in HUD.
Let me also add heat shimmer? Skip — perf.
**Wind streaks:** 26 streak particles: each has x,y in sky region, len = 30+|wind|*10, speed = wind*22 + 40*sign... when wind≈0 hide. Draw thin 1px lines alpha 0.05–0.09 ivory. They wrap around screen. Also streaks only above ridge tops (y < some line) or anywhere — anywhere above ground is fine, subtle. Also near summit they'd cross the cone — over the cone silhouette they'd be in front (weird). Restrict to y < summitY-ish region and y > 40? Just draw them before terrain? Then they're hidden behind cone and ridges — but also behind... sky cache includes ridges; streaks drawn after sky but before terrain → occluded by cone (correct, wind in front of far ridges but behind the mountain — physically streaks are atmosphere nearer than mountain? whatever, occluded looks fine). Actually simpler: draw streaks right after sky layer, before cone. They'll show in open sky.
**Ash color & rendering:** smoke sprite ramp 14 levels. Particle: age, life, size grows: s = s0 + (s1-s0)*age^0.7. alpha = 0.5 * sin(π*ageCurve)? Use alpha = 0.55 * (age<0.12 ? age/0.12 : 1 - (age-0.12)/0.88 * 0.95). Composited source-over (dark smoke over dark sky needs to be visible: dark gray on dark blue is low contrast — make plume lean lighter gray-brown: ramp from rgb(58,50,48) young → rgb(165,150,140) old? Old ash clouds catch moonlight/sky light → lighter. Also add slight warm underlit tint near vent for young particles: could tint via second sprite... simpler: young smoke sprite uses warm-dark (rgb(85,60,45)), mid neutral dark, old pale. So ramp: warm dark brown → neutral gray → pale gray. Draw with alpha; on dark sky pale smoke reads well. Also 'source-over'. Maybe draw plume base glow from vent during eruption (lighter radial) to underlight the plume — the vent flash does that: big soft radial 'lighter' gradient behind smoke? Draw vent glow AFTER smoke so it underlights? 'lighter' over smoke brightens them — yes draw vent glow after smoke, giving backlit plume. Order fix: deposits → smoke → ventGlow(lighter) → lava. Hmm vent glow over smoke brightens smoke near vent: nice. And lava drawn after with lighter — glowing bombs pop. OK.
**Flash at eruption onset:** at eruption start, spawn a bright expanding radial flash: radius grows 40→180 over 0.4s, alpha decays. Draw 'lighter'.
**Lightning generation:** function spawnBolt(): origin = vent + (rand offset ±60, up 60–160 into plume); points: n=7+; each step: x += (rand-0.5)*38, y += 12+rand*18, slight downward bias; maybe one branch from mid point (short 4 segments). Store bolts array with life 0.12, draw with lineWidth 2.2, strokeStyle rgba(190,200,255, alpha), plus glow: draw again lineWidth 6 alpha*0.25. Fine.
**Counting:** active lava count tracked via counter variable updated on spawn/recycle. Display active = lavaCount (and maybe smoke count separately: "PARTICLES 1842 + 213 ash"). Show total. Requirement: counter of active particle count and FPS. I'll show both: "LAVA 1842 · ASH 213 · FPS 60". Also cap indicator.
**Pool implementation:**
```
const lava = new Array(MAXLAVA); for i: lava[i] = {active:false, ...};
freelist = [indices...]; spawn: idx = freelist.pop(); if undefined → skip.
recycle: active=false; freelist.push(idx).
```
freelist as plain array with pop/push — LIFO fine.
Smoke same with its own pool/freelist.
**Main loop:** requestAnimationFrame with dt = clamp(now-last, 0, 0.033). Fixed-ish step fine.
**Slope sampling for particle:** x → i = x>>1? Use heightmap with step STEPS=2: idx = clamp(round(x/2)). e(x) interpolate linearly between samples for smoothness. slope = (e[i+1]-e[i-1])/(2*2). Provide functions elevAt(x), slopeAt(x).
Elevation array covers x from 0..W. Build once per resize with the analytic function (including noise with fixed random phases chosen at load — regenerate on resize keeping same phases so shape is stable).
Also crater: I need the vent to be a visible notch: bowl depth ~26px, width ~70px. Plus a subtle inner glow.
**Drawing terrain silhouette details:**
- fill color: create slight vertical shading? Solid very dark with strata lines & speckles & rim light.
- Also draw a subtle warm reflection light on cone flanks during eruption? When env high, faint amber rim on crater rim area — the vent glow covers that. OK.
Deposit painting color: from heat at freeze: use heatColor(h) darkened: deposit uses rock color: mix heatColor(h) toward dark. For simplicity: depositColor = heat<0.25 ? very dark : slightly-reddish dark crust. Actually the deposit records how cooled the lava was when it froze — flowing lava freezes at heat ~0 → dark. Early splats (heat high) would be bright — but splats slide and cool before freezing mostly. Paint color = heatColor(heat*0.35) — always darkish. Use alpha 0.3, radius size*1.15 + 1.
Weathering: every frame with small probability? Use accumulator: every 3.5s: depositCtx.globalCompositeOperation='destination-out'; fillStyle='rgba(0,0,0,0.045)'; fillRect over deposit canvas → fades ~ equally. At that rate deposits halve every ~50s. During eruption painting may outpace. OK.
**HUD pressure gauge details:** position left: x=26, y = H*0.5 - 90? Place vertical gauge left-center: x 24–40, y from H*0.5-80 to H*0.5+80 (160 tall). Frame: 1px stroke rgba(255,255,255,0.18), bg rgba(0,0,0,0.35). Fill height = pressure * innerH, from bottom. Fill color: amber #ffb347 up to 0.7, then shift to #ff5a3c above 0.7 (danger). Implement: fill full with amber then overfill top part with red using clip — or gradient per frame (fine, one gradient per frame OK). Ticks: 11 ticks, labels every 0/50/100? tiny. Needle: horizontal line at level extending right 10px, ivory. Threshold mark at top. Label below: "PRESSURE" small caps letter-spaced. Also small pulse when erupting (gauge flashes).
Also small text under gauge: state word.
Stats top-left:
```
LAVA 1240
ASH 231
FPS 60
```
mono 12px, labels dim ivory, values amber/ivory. Maybe also "ERUPTION 0.8×" when active.
Wind readout also top-left? Panel has it. Fine.
**DOM structure:**
```