### 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, no external libraries, everything inline, canvas rendering. Let me think about what to build: 1. **Side view**: volcano cone with crater, ground, sky. 2. **Pressure meter**: builds over time; when maxed, eruption happens, pressure resets and builds again. 3. **Eruptions**: glowing lava particles from crater with varied velocities, gravity arc, 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 (bidirectional slider), particle amount. 7. **Live counter**: active particles + FPS. Smooth with ≥1000 particles. I need to write my own physics and rendering. Let me plan the architecture carefully for performance since 1000+ particles need to run smoothly. **Terrain generation**: I'll define a volcano profile as a function of x. A classic cone with a crater dip at the summit. Ground at the bottom. The terrain can be sampled per x-pixel-column for fast collision lookup — actually I'll build a height array (e.g., per 2px step) at init. Collision test: particle is below terrain height → resolve. Terrain profile: something like a mountain shape — maybe two peaks? No, keep one volcano centered-ish, with a crater at top (a dip). Let me define: - Ground level: e.g., y = H - 60 (grass/rock ground). - Volcano: from ground up to a summit around y = H * 0.25. Cone shape: width grows from crater to base. Slope ~ steep enough for particles to slide. Classic stratovolcano: slightly concave slopes. - Crater: a dip at summit, width ~40-60 px, with a lava lake inside maybe (glowing). The crater interior should be a depression — particles landing there pool in it, feeding the lava lake visual. That's nice. Terrain function: define via a few control points and interpolate. Or a parametric shape: ``` function terrainHeight(x): // base ground let h = groundY; // y coordinate where terrain surface sits // volcano const cx = W*0.5; const halfW = W*0.42; // base half-width // cone profile: height as function of |x - cx| const d = Math.abs(x - cx)/halfW; // 0 at center, 1 at base if (d < 1) { // concave-up cone: summitY + (baseY - summitY) * pow(d, k) // with crater dip near center } ``` Cone: use `d^1.7` or something for slightly concave profile (steep near top, flatter near base)? Actually for a stratovolcano, slopes are concave (steep at top, shallower at base). Height above ground: `Hc * (1 - d^p)` where p>1 gives steep near summit... let's check: at d=0 → full height Hc (summit). At d=1 → 0 (base). Slope dy/dx = -Hc * p * d^(p-1) / halfW: at d near 0 slope ≈ 0? Hmm, derivative w.r.t d: d/dd of (1 - d^p) = -p d^(p-1), which → 0 at d=0 for p>1. That means flat at summit — wrong, we want steep at summit. Use p<1: p=0.8 → derivative infinite at 0 (vertical at summit) and shallow at base. So height = Hc * (1 - d^0.85)? At d=1, derivative magnitude = Hc*0.85/halfW — shallow-ish base slope. That gives a nice concave stratovolcano shape. Maybe p = 0.7. Then crater: for |x-cx| < craterHalf (say 45px), carve a dip: height reduces... in terms of "surface y", the dip means surface y increases (goes down) inside crater. Depth ~35px with a rim. Actually real craters have rims: the cone peaks at rim, dips to crater floor, and crater floor has a lava lake. Let me do: ``` cone surface y(x) = groundY - Hc*(1 - d^p) (y grows downward) ``` Then apply crater modification: for |dx| < craterHalf, y_surface = lerp between rim surface and crater floor with smooth curve. Use a function: t = |dx|/craterHalf; dip = depth * (something like (cos(t*π)?) Actually smooth: dipAmount = depth * (0.5 + 0.5*cos(t*π))? At t=0 → depth (full dip at center), at t=1 → 0.5*depth (half dip at rim edges) — gives rim slightly lower than peak? Hmm. Better: at t=1 (rim edge), dip=0 meaning rim edge = cone surface, at center dip = depth. That makes crater walls at the rim edges meet the cone slopes. Slightly odd since the dip is steepest at rim edges... Use smoothstep: dip = depth * smoothstep-like with derivative 0 at both ends: dip = depth * (sin(t*π))^1.5? At t=0 → 0 (center not dipped?) no wait we want max dip at center. sin(π t) is 0 at both ends and max at t=0.5. Let me instead make dip = depth * (1 - (t)^2) with a smooth factor... Simplest good-looking crater: dip = depth * cos(t * π/2)^2, where t∈[0,1], giving 0 dip at rim edge... no wait, cos²(t·π/2): at t=0 → 1 (max), at t=1 → 0. And derivative at t=0 is 0 (flat at crater floor center — good for a lava lake), derivative at t=1 is -π/2 * ... derivative of cos²(u) = -sin(2u)·(π/2)... at u=π/2, sin(π)=0 → derivative 0 at rim edge too. So smooth both ends. dip = depth * cos(t*π/2)^2. That's a nice smooth crater bowl. But wait — at center t=0, dip = depth, so the crater floor is at center lowest point. Lava lake: fill the bowl below a certain level with glowing lava. Actually visually I'll draw the lava lake as an ellipse/blob at crater floor, glowing. Also I might slightly randomize the profile with a small noise so the silhouette isn't perfectly smooth — a few bumps. Add subtle noise: `+ noise(x) * amp` where noise from a seeded pseudo-random with smooth interpolation (value noise with a few octaves). Keep it small (a few px) and zero it near the crater/rim to keep crater shape clean. This gives an authored, organic silhouette. Terrain stored: `heights[i]` array per x pixel (surface y). For collision, given particle x, get surface y; if particle y > surfaceY → collide. Also need slope/normal: compute from neighboring heights: normal = perpendicular of (dx, dy) vector along surface. For sliding downhill: compute surface tangent, apply gravity component along tangent with friction, and add slope-biased slide. **Physics**: Particles: Lava particle: pos, vel, age, life, size, heat (0→1 where 1 hot), maybe color derived from heat. States: airborne (ballistic) vs settled/sliding. Ballistic: v += g*dt; pos += v*dt; wind affects smoke more; lava slightly less. Collision with terrain: when y >= surfaceY(x) - small epsilon. On collision: - Compute surface normal. If velocity into surface significant (v·n < -threshold): bounce with restitution ~0.35 and some tangential friction, or if shallow impact → start sliding/rolling. - Better approach used by many: on landing, if speed normal component > threshold, bounce (few times), else convert to "surface mode". Surface mode (sliding): particle sticks to terrain surface. Its position constrained to surface: x moves, y = surfaceY(x). Velocity along surface: v_x' where gravity component along slope accelerates it downhill: a = g * slope direction * (1 - friction-ish). If slope is steep enough relative to friction → slides; else settles (velocity damped to 0, particle becomes static, cools in place). Static particles can remain for a while then fade — they form glowing streaks/casca of lava on slopes which darken into rock. When life ends → remove. To keep count near "particle amount" target, eruption adds particles; old ones die by life. Slope-based sliding: compute local slope s = dy/dx. Gravity along slope for a particle on the surface: a_x = g * s * k / (1+s²) ... Actually component of gravity along tangent unit vector: tangent T = (1, s)/sqrt(1+s²) (pointing +x direction). g vector = (0, G). g·T = G*s/sqrt(1+s²). If s > 0 (downhill to the right since y downward positive... careful: canvas y grows downward. Terrain surface y = surfaceY; downhill means surfaceY increases? No: downhill visually means going away from summit toward ground — surfaceY increases as we go down the slope toward ground level? Wait: ground is at bottom (large y), summit at top (small y). On the left flank, as x increases toward center, surfaceY decreases (going up). Downhill on left flank means x decreasing → surfaceY increasing. Slope s = d(surfaceY)/dx: on left flank s < 0 (y decreases as x increases). Gravity along tangent: g·T with T = (1, s)/L: = G*s/L. On left flank s<0 → negative → pushes x negative → downhill. On right flank s>0 → pushes x positive → downhill. So gravity naturally pushes downhill with formula a_along = G*s/L. Good, sign works out. Friction: a -= sign(a) * friction * ... Use: accel = G*s/L * slideFactor - damping*v. If net accel below a threshold and v small → settle (static). Static particles still cool and eventually disappear; also they could occasionally be re-activated if neighbors move? Skip that — too complex. But "slide or settle downhill" — the requirement says particles land on slopes and slide or settle downhill. So sliding downhill when slope steep, settle when flat. My formula: slide when |slope| > μ (friction coefficient), else settle. Implement: if |s|/L > frictionThreshold → slide; else settle into static mode. Also crater floor: flat, so particles settling there pool — nice, they pile up in crater as glowing pool. Depth stacking: particles are just circles; overlapping fine. Sub-pixel/stacking: don't do full collision between particles (O(n²) too expensive for 1500). Instead, when a sliding particle settles, it may rest on the surface — offset y a bit above surface based on a simple "settle height" grid? A common trick: maintain a grid of settled particle heights per column (deposu deposit height), so new settling particles stack on top: surfaceEffectiveY(x) = terrainY(x) - deposit[x]*r. And deposit grows as particles settle, giving visible accumulation. Deposit decays slowly (rocks crumble/disappear as particle dies → reduce deposit). This creates embroliment: lava flows accumulate and form glowing banks on slopes and in crater. Nice and cheap: one Float32Array per column. Each settled particle increments deposit at its column by ~size; when particle dies, decrement. Need to keep deposit per particle tracked? Could decrement at death using the column recorded at settle time. But particle may slide further while... no, settled particles don't move. Store settleCol; on death reduce deposit[settleCol]. Edge cases with slides: while sliding, particle isn't deposit; only when settled. Also airborne particles colliding with deposit: they collide with effective surface (terrain + deposit). If deposit thick on slope, fresh lava lands on the bank — good. Also lava melting/absorbing: skip. **Heat / color cooling**: Each particle has heat from 1 (just launched) decaying over life: heat = 1 - age/life (or exponential). Color: heat 1 → bright yellow-white (255, 240, 180)? Real lava: white-yellow → orange → red → dark. Map heat: >0.85 → near white-yellow; 0.5-0.85 orange; 0.25-0.5 red; <0.25 → dark brown/black rock (charcoal ~ (40,30,28)). Also add glow: render hot particles with additive blending (globalCompositeOperation 'lighter' or draw with shadowBlur? shadowBlur is slow). Better: draw each particle twice: core fill + for hot ones a slightly larger translucent circle with 'lighter' composite? Switching composite per-particle is costly; instead batch: draw all particles to main canvas normally (dark ones), then set 'lighter' and draw glow layer? Hmm. Performance plan for 1000-2000 particles at 60fps: avoid per-particle shadowBlur, avoid fillRect with state changes. Use one Path2D? Circles need arcs — arc calls are okay-ish. Alternative: use `ctx.fillRect` with rounded? Squares look cheap (and I want to avoid plain squares). Use circles via arc + fill per particle — 1500 arcs + fills per frame. That's probably fine in modern browsers (~ a few ms). But state changes: setFillStyle per particle is expensive-ish (string building). Group particles into a few color buckets! Since color derives from heat and size, quantize heat into e.g. 6 levels × few size buckets. Then draw: for each bucket, begin path, add all arcs (moveTo + arc), single fill. That's fast: ~6-10 fills per frame. Even better with off-DPR. Glow: draw hot particles (heat > 0.5) with a second pass under 'lighter': bucket them similarly with translucent larger circles. Or: draw radial-gradient sprite? Pre-render sprite canvases for each heat level (say 8 sprites: 64×64 radial gradient from color to transparent), then drawImage scaled per particle — drawImage with scaling per particle ×1500 might be okay but heavy. Alternative: use 'lighter' composite and draw bucketed circles: pass 1 normal composite: all dark+mid particles. pass 2 'lighter': hot particles core + halo. Bucketed arcs keep it cheap. Actually simpler robust approach: two layers: - Sky/terrain base drawn each frame (or pre-rendered static background to offscreen canvas — terrain silhouette, ground, stars — static! Draw once to offscreen, then per frame just drawImage it. - Lava glow: on top of terrain? Lava in front of terrain mostly (particles above surface). Order: background (sky+volcano+ground) → crater glow → 'lighter' glow blobs for hot particles → particles → smoke (smoke on top or below? Smoke rises above crater, drawn with translucent gray — over everything, semi-transparent, maybe 'source-over' with rgba fill, bucketed). Also a subtle ambient crater light that flickers when pressure high. Background offscreen: sky gradient... wait — I want to be careful about my own taste regarding gradients. Sky gradients are legitimately natural for skies (a sky IS a gradient in nature). My own guideline is to not lean on gradient decoration to fake design sense; a subtle atmospheric sky gradient is representational, not decorative — that's fine and standard for canvas scenes. I'll keep it tasteful: deep dusk sky (dark blue-teal to warmer horizon) since night scene makes lava glow pop. Dusk/night sky with stars, a moon? Maybe a dim moon. Silhouetted volcano in dark tones, ground dark. The lava glow illuminating: I can draw a big soft radial glow behind crater with 'lighter' when pressure high — flickering. This is representational light emission, fine. Also ash cloud illumination: smoke particles near crater get tinted warm (lit by the glow), drifting up with wind. Smoke: each puff: pos, vel, age, life, size growing, alpha fading, color mixing between dark gray and warm based on proximity to crater / heat at spawn. Smoke rendering: bucketed circles with rgba fill; or sprites. Sprites (pre-rendered soft blobs) with globalAlpha per particle — 300 puffs × drawImage is okay. Bucketing alpha: set globalAlpha per bucket then drawImages. Let me quantize smoke to, say, 12 buckets by alpha×tint... simpler: 3 tint levels × 4 alpha = 12 combos — drawImage per particle anyway, ~300 calls fine. Actually for lava glow sprites: pre-render sprite per heat-bucket (say 5 buckets) as radial-gradient circle images; drawImage with per-bucket globalAlpha? Alpha varies by heat too... Just bake the color and per-particle alpha into buckets: bucket key = heatLevel (which sets sprite + alpha). Since heat decays continuously, alpha per bucket = average. Quantize 6 buckets for glow. drawImage ~ (hot fraction ~ 40% of 1500 = 600) calls/frame — acceptable? drawImage of small sprites is fast. Combined with core circles via arc for the rest. Let me instead do: ALL lava particles drawn via sprites (sprite chosen by heat bucket; sprite includes bright core). Sprite 48px, drawn scaled to particle size (r ~ 3-7px, scale accordingly). Hmm, but crispness: scaling 48→7 fine. For cold dark rock, sprite would be dark blob — fine (rock chunks). This unifies: one drawImage per particle, bucketed by globalAlpha not needed since alpha baked in sprite? Alpha varies with age near death — use globalAlpha per particle → state change per call, meh. Quantize alpha to buckets: sort particles by bucket once per frame (cheap counting sort into arrays), then for each bucket set globalAlpha once and drawImages. ~8 buckets → 8 state changes. Sprite pre-render: for each of N heat levels, render radial gradient: center bright color → mid → transparent. Colors: level 0 (white-hot #fff2), ... level 5 (dark rock, nearly opaque dark, small). Rock particles aren't glowing — sprite for rock: just dark filled circle with soft edge. Fine — bake different sprite types. But dark rock with transparent edge over dark terrain: okay. Alternatively skip sprites for cold ones and use arc-fill buckets. Honestly sprite approach for everything is simplest to code and fast enough. Let me estimate: 1500 drawImage calls with 48×48 source → each ~0.02ms? Probably ~1-2ms total on modern hardware. Plus smoke 300 calls. Fine. Hmm wait — I need 1000+ particles "active" and smooth. I'll target particle amount slider up to 3000, default 1800. Also eruption spawns per eruption: how do we reach steady 1000? Particle lifetime: lava particles live, say, 8-20 seconds (cooling), eruption every ~10-15s (pressure cycle). Steady-state count ≈ spawnRate × avgLife. If eruption dumps E particles over ~3s burst, and life ~15s average, then steady count ≈ E (all still alive) minus decayed. E = particleAmount slider (e.g., 1500) → count ~ up to 1500 shortly after eruption, decaying until next. Good — counter will fluctuate; smoothness maintained since cap ~3000. Also smoke count separate (not counted as lava? "active particle count" — I'll count lava + smoke + sparks? I'll count total particles including smoke, showing breakdown maybe "lava / smoke". I'll show total active particles (lava + smoke + ember sparks) and FPS. Keep it simple: `PARTICLES 1326 · FPS 60` — or two-line. I'll show lava and smoke separately plus total; plus FPS. Fine. **Pressure system**: pressure 0→100. Builds at rate depending on... maybe slower building with some noise (tremor wobble). When 100 → eruption: pressure dumps to ~8-20 (partial reset: "resets and builds again"). During eruption, pressure discharges quickly downward while crater erupts (eruption duration ~2-4s): rate = -(100)/duration. After reaching ~10, builds again. Eruption intensity = power slider (velocity multiplier) + particle amount slider controls count. Also crater glow scales with pressure (pre-eruption glow, tremor shakes? small screen shake at peak? A tiny camera shake at eruption start is a nice signature touch). Add rumble: slight jitter of the whole scene? Could translate canvas slightly. Cheap and fun. Keep subtle (2-3px, decaying). And sound? No audio requirement; could add WebAudio rumble — optional, but adds "wow". Autoplay restrictions: need user gesture. I could add a mute/unmute button that initializes AudioContext on click. Let me include a small synthesized rumble (noise burst filtered) + boom on eruption, plus hiss? This adds craft. But it's extra complexity; I'll include a compact WebAudio rumble (noise buffer + lowpass + gain envelope), initialized on first user interaction, with a small speaker toggle button (drawn icon via inline SVG — no emoji). Actually to keep scope sane, I'll do it — ~60 lines. Hmm, risk of bugs... I'll implement carefully: AudioContext lazy-init on toggle click or first click anywhere; noise buffer looped through biquad lowpass; gain envelope on eruption (attack 0.05s, decay 3s) + a low "boom" (sine drop). Plus crackle? Skip crackle. Muted by default with toggle. It's a nice touch. **Sliders**: Three: 1. Eruption power (velocity multiplier ~ 0.4-1.6× and also spawn rate burst). 2. Wind: bidirectional slider from -1 (left) to +1 (right), magnitude scaling. Label "Wind" with center 0. Show direction by value; also maybe show little arrow direction via CSS transform on a marker? Keep simple: numeric readout like "← 45" or "E 45 →"? I'll style with a custom range and show "+0.6 →" etc. Or a needle indicator. Keep: value readout "west · strong" nah. Just show signed value and a tiny arrow that flips via transform (pure CSS/JS class). Simple. 3. Particle amount: target steady particles cap / spawn count per eruption (200-3000). Also acts as cap for smoke? Smoke count scaled by amount too. Custom-styled range inputs: styled with CSS (webkit-slider-thumb etc.). Put them in a control panel — top-left or bottom-left glassy panel. Layout: title top-left ("STRATOVOLCANO · live simulation" something), stats top-right, sliders bottom-left panel. Buttons: "Erupt now" (trigger eruption regardless of pressure — nice interactivity: forces eruption if pressure > some min, or instantly dumps pressure to eruption — let user force it: "trigger eruption" button consumes pressure to zero with full blast even at low pressure → weaker but still erupts). Also pause button? Space to pause? Add: click on canvas = trigger eruption at will? Fun: click/tap canvas to force a small eruption — gives interactivity. I'll wire canvas click → force eruption (if not currently erupting). And keyboard: Space triggers. Mention in hint text. Icons: I'll use inline SVG icons (speaker, etc.) — no emoji. Or minimal text labels only — fine, no icon library needed since none allowed external... inline SVG is fine (self-made). Keep chrome minimal: labels + values. **Layout/aesthetic of UI**: Dark HUD panels with thin borders, monospace-ish technical type (I'll use a distinctive font — but no external fonts? CDN allowed? External CDN references are allowed generally but "No external libraries" per user: "No external libraries: everything inline, rendered on a canvas." — so I should avoid external fonts to be safe. Use system font stack? I dislike default fonts as primary identity... but for a canvas sim HUD, a monospace stack ("ui-monospace, SFMono, Consolas") gives technical instrument feel and isn't "generic default". I'll treat typography via letter-spacing + uppercase micro-labels + tabular numerals. That reads as authored instrument UI. Since no external resources allowed, that's the right call.) Color scheme: deep night: sky from #0b1026-ish to horizon #2a2438 warm? Night with stars. Volcano silhouette near-black (#0d0f14 / #161a22) against sky, rim-lit? Add subtle rim light on the side facing crater glow? Could draw terrain fill then a faint gradient rim? Terrain is drawn once offscreen; crater glow changes dynamically. I can draw the volcano silhouette dark; dynamic glow appears over it via 'lighter' — the glow sprite at crater will lighten nearby area naturally. Slope glow from settled lava also lights terrain — the glow blobs at surface will show. Good enough; realistic-feeling. Ground: dark rocky plain with subtle texture (noise dots) and grass? Night: dark with faint moss color. Add a few silhouette details: small rock chunks, dead trees? Keep a couple of tiny cone-shaped pines silhouettes near edges — adds craft. Also faint clouds/milky way? Stars (small dots, static). Moon: pale disc with slight glow top-right? Sky gradient + stars + moon = nice dusk. I'll include a moon low on horizon? Simple circle + soft glow, static offscreen. Choose sky: deep indigo → dark teal → faint warm at horizon? Actually warm horizon behind volcano silhouette adds depth. Keep subtle. Volcano: layered silhouette: base fill #131722; inner slope shading: draw darker gradient strips? Since background offscreen, can spend effort once: draw volcano with vertical gradient from #1a2030 top to #0e1116 base? Gradient as fill for form — representational shading, fine (a vertical light-to-dark to model form is shading, not decorative gradient; legitimate). Plus a few horizontal strata lines (darker strokes) and scattered rock dots. Crater interior: darker + lava lake glowing (dynamic — lava lake brightness tied to pressure; draw dynamically each frame over the static bg: lake ellipse with bright color + slight flicker). Also crater inner walls get dynamic light. Hmm, also lava lake level: settled particles pooling in crater will land in lake → they'd disappear? Let them sink into lake and die quickly (they fuse into lake) — reduces clutter. When particle settles inside crater bowl (x within crater, y > lakeLevel?) → kill after brief glow, and lake gets a tiny brightness boost. Nice touch: each landing in lake adds "lake heat" that decays — brightens lake. **Eruption mechanics**: On eruption trigger: - duration ~2.5-4s (scales with power). - spawn N total particles spread over duration: N = amount slider × maybe 0.9; spawn each frame while erupting: rate = N/duration × dt with fractional accumulator. - spawn velocity: from crater: launch direction mostly upward with spread: angle from vertical ±35° (power increases spread & speed). Speed = base (180-320 px/s) × power × jitter. Also initial ejecta: big slow chunks + fast small ones (varied velocities, sizes). - magma spatter particles: some with very high speed, short life, pure sparks (tiny, bright, high drag?). Keep one type with size variation. - Also spawn "bombs" — larger particles, slower, arcs. - Pressure: during eruption, drop at rate 100/durT... to ~5. Then rebuild: rate = 100/buildTime (buildTime ~ 12-20s + power influences? Higher power → drains more → but rebuild constant). Add noise wobble to pressure needle. Also pressure build slows near top (nonlinear: pressure advances with easing? Just rate increases as pressure rises: dp = base*(0.5+0.8*p/100)*dt — feels like rising tension). - Meter UI: a vertical or horizontal gauge? I'll design a canvas-free DOM gauge: a slim horizontal bar with tick marks, needle? A semi-circular gauge drawn on a small canvas? Simpler: horizontal bar with fill and a subtle "danger zone" marker at end, plus numeric %. Or vertical thermometer next to volcano? I'll do an instrument panel: "MAGMA PRESSURE" horizontal gauge in bottom-left control panel with segmented ticks (CSS: repeating-linear-gradient for ticks — hmm, that's a gradient trick; use border-based ticks via background repeating-linear... it's fine functionally but let me instead render ticks with small divs? Or draw gauge on tiny canvas: crisp control). Actually a small canvas (gauge) inside the panel gives me full control: draw arc gauge with needle, ticks, glow when near max, redline zone. That's an instrument look, memorable. 160×90 canvas, redraw each frame (cheap). I'll do a 210°-style arc gauge (from -220°... classic gauge: from 150° sweep to 30°? Standard: start at 135°, sweep 270°... let me do 180°+ a bit). Needle colored hot when >80%. Status line: STATE: BUILDING PRESSURE / ERUPTION IN PROGRESS / VENTING... **FPS counter**: compute via rAF delta smoothing (ema). Display top-right HUD: "1 482 particles · 60 fps". Also maybe a tiny fps sparkline? Keep number + colored (green/amber/red). Also show sim particle budget: "capacity" etc. Keep minimal: TOTAL / LAVA / SMOKE / FPS in tabular. **Particle physics details**: Constants: world coordinates = canvas pixels (fit window, resize handling: on resize re-derive terrain? Terrain depends on W,H; regenerate on resize and deposit grid resize. Keep simple: full-res canvas matching window, terrain arrays per pixel. Particle coordinates in px. DPR handling: use device pixel ratio for crispness — canvas width = W*dpr, ctx.scale(dpr). Terrain per CSS px. That's standard. dt: fixed-ish dt = min(dt, 33ms) with substeps? For physics stability with high velocities (500px/s * 16ms = 8px/frame vs terrain sampling per px — collision check needs sampling along path: sample terrain at start x and end x? For steep slopes with thin walls, a fast particle could tunnel. Simple approach: check y >= terrainY(x) each frame at final position; for high-speed, also do 2 substeps for particles with speed > threshold. I'll implement substeps: split dt into n = ceil(speed*dt/4) steps (cap 4) for ballistic particles. Cheap enough for airborne fraction. Ballistic update: ``` vy += G*dt (G ~ 500 px/s²? For window ~ 900px tall, want arcs reaching ~ 60% of height: launch v 250 px/s up → apex h = v²/2g = 63px at g=500... too low. Let me set G = 900? apex = 250²/1800 ≈ 35px. Hmm, need bigger launch speeds: v=500 → apex 139px at g=900. Summit is at ~y=H*0.28 above ground ~ H*0.93. Eruption from crater: particles should go above summit by 100-300px and fall onto slopes. If launch speed 600-900 px/s and g=900: apex ≈ 200-450px. OK: G=900, launch speeds ~ 420-900 depending on power. Tune at runtime mentally: I'll parametrize: base speed = H*0.55 * (0.5 + power) maybe... Let me define in terms of H: want apex height above crater ≈ H*(0.25..0.6). apex = v²/(2G) → v = sqrt(2G*apex) = sqrt(2*900*0.4*900) for H=900... circular. Just set G = 2.2*H? If H=900, G=1980?? Then v for apex 300px = sqrt(2*1980*300)=~1090 px/s. At 60fps frame 16ms → 17px/frame. OK manageable. Hmm rather use G = 900 fixed and speeds 500-1000. On small windows speeds stay same → relative arcs bigger. It's fine; tune: G = 1100. Launch vy = -(500..950) * power(0.4..1.4). Plus vx spread ±(0.45*speed) with angle jitter. ``` Drag: slight air drag for sparks: v *= (1 - drag*dt). Wind: affects smoke strongly: vx += wind*windForce*dt (windForce ~ 120 px/s² per unit? wind slider -2..2). Lava affected mildly (0.05×) while airborne. Smoke: constant horizontal drift wind*(20-60) px/s plus turbulence (perlin-ish noise via sin functions per particle id) + buoyancy upward vy -= buoy decreasing with age? Smoke: rises fast initially, slows; expands (size grows 1.4×/s), alpha fades after mid-life; life ~3-6s (ash field persist). Wind direction also tilts the column: the classic behavior — column bends downwind. Give smoke horizontal velocity from wind and lift decreases over age → the column arcs downwind as it rises, looking right. Smoke spawn: while erupting (and for ~2s after pressure venting), spawn puffs at crater with upward velocity 60-160 px/s + slight lateral; also weaker continuous "plume" while pressure > 60 (fumarole wisps) — nice: pre-eruption the crater starts smoking as pressure rises. Rate scaled by amount slider × factor. Cap smoke count ~ amount*0.35 (min 60). Also wind affects spawn: strong wind → column bends. Also ash falling? Some smoke particles get heavy ash flecks? Skip; keep smoke visual. **Collision with terrain (ballistic)**: At substep: if y >= surfY(x): - compute normal from slope; penetration = y - surfY; - separate: y = surfY - 0.5; - normal velocity component: vn = v·n. If vn < -minB (fast impact): reflect: v = v - (1+rest)*vn*n; with rest ~0.35 + tangential friction (multiply tangential by 0.75). Small bounce. Also speed-based chance to just slide: if vn small (> -60?) → enter sliding mode. - else enter sliding mode: project velocity onto tangent, damp. Sliding mode: - Stick to surface: y = surfY(x) - radius*0.4 (embed slightly into deposit? set y = effective surface minus small offset so it looks half-buried — use terrain + deposit: effective = terrainY - deposit? wait deposit reduces surfaceY (raises ground). deposit stored as height in px added on top: effSurfY = terrainY(x) - deposit[x] (y smaller = higher). If particle y >= effSurfY → landed on bank.) - Each frame while sliding: y = effSurfY(x) (follow terrain as x changes), and deposit might change under it — recompute; if deposit drops below particle (bank eroded) → become airborne briefly? Edge case rare; just set y each frame and if particle ends up "floating" (y < effSurfY - 2)?? y < effSurfY means above surface — fine, leave (it'll look floating); actually gravity in slide mode pulls along slope, keep pinned: y = effSurfY always in slide mode. Hmm but if particle slides to crater center where deposit tall, pinned on top of bank — fine, looks like flow front. Good. - Velocity: vx only (store s (tangent speed, signed along +x)). Accel: a = G*slope/L (component along tangent; slope s = dy/dx, L = sqrt(1+s²)). Wait sign: tangent unit T = (1, sy)/L where sy = d(surfaceY)/dx. g·T = G * sy / L. If sy > 0 (surface descends as x increases → downhill to the right): positive accel → slides right downhill. Correct. If sy<0 → slides left. So a_t = G * sy / L. Minus friction: friction force = μ*G/... normal force = G/ L?? For unit tangent T and normal N, gravity components: along T: G*sy/L (as above); along N: magnitude G*|sx?|... normal component of g: g·N where N = (-sy, 1)/L (pointing "up" out of surface? y down positive: surface below is +y; outward normal (pointing away from ground into sky) = (sy... let me define: surface tangent (1, sy)/L; normal pointing up (out of ground) is (sy, -1)/L? Check: for flat ground sy=0 → (0,-1)/1 → points up (negative y = up in canvas). Good. g=(0,G): g·N = (0*sy + G*(-1))/L = -G/L → gravity presses into surface with magnitude G/L. So friction decel = μ * G/L opposing motion. Slide condition: |G*sy/L| > μ G/L ⇔ |sy| > μ. So friction coefficient μ ~ 0.45: slides when slope |dy/dx| > 0.45 (~24°). Steeper stratovolcano slopes: crater rim slope dy/dx ~ 2.5? So slides nicely down most of cone, settles near base flats and crater floor. - Update: v_t += a_t*dt; friction decel: v_t -= sign(v_t)*μ*G/L*dt (if it would reverse, zero it). Plus rolling randomness. - Also cap: min speed; if |v_t| < 4 and |a_t| < friction accel → settle: switch to STATIC: add deposit (deposit[col] += height, clamp maxBank maybe 12 per col? deposit per column in px: add particle "size" * 0.9; clamp deposit ≤ 26). Particle stays rendered until death (heat-cooled rock), y pinned at effSurfY - (own embed). On death: deposit[col] -= contribution; kill. - While sliding, if slope becomes gentle (crater floor), they decelerate by friction and settle — pooling in crater. Static particles: don't move; still cool; check if deposit at their column dropped (erosion? deposit only decreases on particle death at that column — if a neighbor dies, bank shrinks, static particle might float). Occasionally re-check: if y != effSurfY... skip, acceptable artifacts minimal. Also static particles buried later by new deposits: draw order? New settle draws over old — fine. Also: particles sliding on slope that exit volcano sides reach ground plane: ground flat (sy~0 with noise) → settle, spreading glow streaks at base — lava flows reaching ground look great. Ground y ~ H - H*0.06? Let ground baseline ~ H*0.9 with gentle noise (±2px) and slight foreground variation. Also maybe ground has subtle dip where lava pools? Keep flat. Deposit grid: Float32Array size W (per CSS px). On settle: deposit[Math.round(x)] += size*0.85, clamp ≤ 24? Clamp per column to avoid infinite growth: if deposit > 22, instead increase neighbor columns? Overflow handling: if deposit[col] > maxCol, distribute: deposit[col±1] += remainder? Simple: cap deposit[col] at 20; excess converts to widening: if deposit at col max, move to col-1 or col+1 (whichever lower) — implements sliding堆积 side spreading. Implement small loop (few iterations). On death subtract similarly (decrement col where recorded). Keep settleCol in particle. Wait — but deposits as terrain-raising: particle standing ON bank is above original terrain — visually fine. Also "lava lake" in crater: crater floor below lakeLevel: any particle settling in lake region (y > lakeSurfaceY?) Actually lake surface drawn at some y (crater floor - few px). Particles entering lake (y >= lakeY) while ballistic → splash: kill particle after brief sizzle, add lake heat + brightness pulse, spawn a smoke puff + a couple sparks. Lake heat adds to lake glow intensity and slight bubbling particles occasionally (random bubbles: small bright dots rising from lake). Nice signature. Lake drawn each frame: ellipse clipped to crater? Just draw ellipse (dark magma #400700 base) + bright crust blobs (animated blotches via time-based noise: draw several blobs with varying brightness) + emissive glow 'lighter'. Flicker with pressure: intensity = 0.35 + pressure/100*0.65 + lakeHeat. Also when erupting, jet fountain from lake (particles originate there). Crater lake adds a lot. **Rendering order** per frame: 1. drawImage static background (sky+stars+moon+terrain silhouette). 2. Crater glow behind particles? Emissive crater: 'lighter' radial glow at crater, intensity = pressure-based flicker. Also lake glow. Draw before particles. 3. Lava glow sprites ('lighter') for hot particles — under or over terrain? Terrain already drawn (bg) — glow over terrain brightens it (light on slopes) — good. But dark rock particles should be occluded... they're on top of terrain anyway (on slopes). Airborne against sky — dark particles on sky bg visible. Fine: draw all particles after bg. Glow pass then core pass? For correct look: glow behind + core over: do glow pass first ('lighter'), then normal composite cores (sprites with baked alpha). Two drawImage passes per particle... doubles cost. Alternative: single sprite per particle that already includes halo baked (radial gradient bright core → transparent). Draw once with 'lighter' for hot ones? 'lighter' with baked-alpha sprite: halo adds over bg — good for hot. For cold rock: normal composite dark sprite. Two composite groups: group A (heat ≥ 0.25) drawn in 'lighter'; group B cold drawn normal. Sorting particles by heat bucket each frame (bucket sort into arrays — reuse arrays). Then: set 'lighter', for buckets 0..K draw; set 'source-over', for cold buckets draw. 2 composite switches per frame. But rock over terrain drawn with 'source-over' dark sprite will look like dark chunk — good. Rock against sky also dark — visible chunk — good (silhouette bombs!). Slight edge: rock sprite alpha — bake full opacity core. Sprite set: heat levels 0..5? Let's define heat h∈[0,1]: 1 = white-hot (sprite: center #fff, mid #ffb36b? to transparent). Level mapping: 5 buckets: - h>0.75: "white-orange" (255, 236, 190 → orange edge) - 0.55-0.75: bright orange #ff9d40 - 0.35-0.55: red-orange #ff5a2a - 0.15-0.35: deep red #c22 (dim alpha) - <0.15: rock dark (40,36,34) opaque. Sprite images: 64×64 with radial gradient stops: stop0 (0.0): color alpha 1; stop 0.35: color alpha 0.85; 0.75: alpha 0.25 (halo); 1: alpha 0. For glow tiers, cores small relative to sprite: drawn scaled to particle radius*3? If sprite 64px drawn at size s = r*2*3.2 (halo extends beyond core). For rock: gradient mostly solid to 0.5, fade 0.75 → slightly soft chunk. Additionally, hot particle core should be small bright dot: baked in sprite (first 30% radius full color). Good. Bucket sorting: particles array; each frame build buckets[6] arrays (reuse arrays, clear lengths via splice? Use arrays with .length=0 reset then push — allocation churn OK-ish; or push indexes). Simpler: for drawing, iterate buckets: loop particles, if bucket b == current push to bucket list. One pass grouping into preallocated arrays (idx arrays). ~1500 iterations trivial. Alpha: baked per bucket? Within a bucket heat varies (bucket range 0.2 wide) → color variance noticeable? Buckets of 6 over 5 heat stages = wide. Use 8 buckets for finer. Per-bucket globalAlpha for fading near death: alpha = min(1, life remaining fade * ...). Particles fade out at end of life: alpha scales (life - age)/0.6s → to 0. Bucket by (heatBucket*4 + alphaBucket*?) — too many combos. Simpler: per-particle set globalAlpha only when differs from last — ordered push per bucket means within bucket iterate and set alpha per particle (state change per drawImage anyway ~1500 — globalAlpha set is a property assignment, cheap-ish; drawImage cost dominates). Actually globalAlpha assignment before each drawImage: 1500 property sets — negligible vs 1500 drawImage. Fine: set globalAlpha per particle (quantized to 0.05 steps to avoid layout... it's just canvas attr). Keep simple: per particle: ctx.globalAlpha = a (skip if same as previous). OK. Hmm, one more consideration: 'lighter' composite with thousands of overlapping halos in crater fountain → bright saturating cluster — that's the beauty of erupting fountains. Smoke sprites: pre-render soft gray blob (radial gradient white→transparent) and use hue via separate sprites: 3 sprites (dark #1b1d20-ish for shadowed ash, warm-lit #6b4a3a tinted, bright #8a8b90?). Choose per-particle "tint" from heat at spawn (near crater = lit warm; higher/cooler = dark). Composite: normal with per-particle globalAlpha (rgba 0.25-0.5). Smoke drawn over terrain with alpha — looks like ash column. Also under 'lighter'? Lit puffs near glow... simpler: tint color baked warm when young+low, drift to gray. Use 2 sprites (warm, gray). drawImage per puff scaled by size (grows). ~300 puffs → fine. Also ash cloud needs wind drift: horizontal velocity = wind * (some) + turbulence; buoyancy: vy negative (up) decaying, then slight settle & drift... Actually real ash rises then disperses; keep: vy = -80 * (1 - age/life*0.7) → slows rising; plus wind vx; plus turbulence noise (sin-based per particle phase). Life 4-8s. Alpha ramp in fast, fade late. Size grows 30→80. Big ash cloud during eruption: spawn rate high during eruption; smaller wisp when pressure > 70. Cap smoke count: if smoke.length > smokeCap skip spawn. **Struct arrays**: For performance use typed arrays? With ≤3000 particles, plain JS object arrays fine (GC ok). I'll use a pool with struct-per-particle in Float32Array-style manual fields? Simplest robust: arrays of objects with reuse pool (free list). 3000 objects fine. But bucket sort referencing objects fine. I'll implement class-less objects created via factory; free list for reuse. Fields: type ('lava'|'smoke'), state flags, x,y,vx,vy, age, life, size, heat, settleCol, mode (0 ballistic, 1 sliding, 2 static), spin?, seed. Update loop: for each particle: switch type. Let me write physics constants relative to canvas height H for resolution independence: G = 2.6*H? Let me compute desired: H=900 → G=2340?? v_launch for 300px apex: sqrt(2*2340*300) = 1187. Frame step 16ms → 19px/step — ok with substeps. Alternatively scale by H: G = H * 2.4; launch speed = sqrt(2*G*apexTarget). I'll parametrize: apexTarget fraction of H (0.25..0.55 by power slider). Then speeds emerge. Wind accel scale = H*0.14 per unit? Tune: want wind 1.0 to visibly bend smoke: lateral accel ~ 300 px/s² → reaches 200px/s in 0.7s. windAcc = H*0.33*windUnit? windUnit slider -2..2 (value w). windAcc = H*0.22*w for smoke; lava airborne *0.06. Hmm 0.22*900*2 = 396 max. ok. Time: use dt = (now - last)/1000 clamped 0.05; substep physics? Single step with substeps for fast ballistic particles (max 3 substeps). Sliding: fine with single step (speeds moderate, terrain per-px slope lookup; sliding vx up to ~600 px/s? On steep slope a_t = G*sy/L; sy up to ~3 → a ~ 2.7G/... G*3/sqrt(10)=0.95G ≈ 2160 px/s² → 0.5s to reach 1000px/s — fast! cap slide speed? On real slopes lava flows slower but this is fun; cap tangential speed at ~ H*0.5? Also friction μ dynamic with heat: hot lava slides more (μ lower), cooled rock sticks (μ higher) — nice: μ = 0.25 + (1-heat)*0.55. So fresh lava streams down slopes, leaving dark static trails behind as they cool — emergent streaks! Great signature visual. Deposit trail: while sliding hot particles could deposit tiny bits? Skip. Erosion/extra: settled cold rocks could "re-heat" if hot lava slides over? Skip. **Particle count target & "amount" slider**: amount controls (a) lava ejected per eruption (burst size), (b) smoke cap, (c) also we should keep ambient activity: to ensure "at least 1000 active particles" at all times? The requirement: simulation stays smooth with ≥1000 active particles — the test is capacity, not constant presence. To demonstrate, allow slider up to 3000 with default ~1800; after each eruption count climbs high. Also spawn ambient small spatter continuously proportional to pressure so there's always decent activity (lava lake bubbling). I'll show FPS so users see smoothness at peak. Steady-state check: eruption every ~14s dumping 1800 particles with life up to 20s → between eruptions count decays as old die. Might dip to ~300 before next. To keep livelier baseline: particle life maybe longer for settled rock (up to 25s) and continuous lake spatter. It's fine. **Pressure details**: - pressure p ∈ [0,100]. Build phase: dp/dt = buildRate * f(p) * (1 + power*0.15?) BuildRate ~ 100/12s baseline with f(p) = 0.55 + 0.9*(p/100) — accelerating. Also small tremor noise: p display wobble ±1.5. - When p ≥ 100 → erupt(): erupting=true, duration D = 2.2 + power*1.8 (power slider 0..1? define power slider range 0-100% mapping velocity mult 0.55-1.5 and D 2-4.5s). During eruption: dp/dt = -(100 - postp)/D*... simpler: p decreases linearly to pEnd=6 over D. Also add bursts: erupt in pulses (2-3 fountains) — spawn rate modulated by |sin|? Fountain pulses: spawnRate *= 0.5+0.5*sin(t*3)? Real Hawaiian fountains pulse. Use pulse = 0.55+0.45*sin(age*2.6+phase) for organic rhythm. - After eruption: brief "venting" wisp, then build. Meter: gauge canvas with needle + numerals + zone shading; when p>85, gauge trembles + redline glow. Status text under gauge. Forced eruption (button/space/click): if building & p<100: trigger erupt with duration scaled by p? Full-power blast regardless: duration D0 = 1.2 + 2.5*(p/100)... if p low, eruption weaker/shorter (pressure dumps to 0). Simple: erupt(pressureNow) sets erupting with strength = max(p/100, 0.35) multiplying spawn count & speed. Pressure → min(p,?) goes to 3. Good. **Camera shake**: shakeT decays; offset = shakeT * sin(t*40)*amp applied via ctx.translate for scene draws (also HUD unaffected since DOM). Apply to background drawImage + particles: translate whole canvas draw. Implement: at frame start ctx.setTransform(dpr...), translate(shakeX, shakeY). Cheap. **Resize**: canvas fixed to viewport; on resize, rebuild terrain arrays + deposit (deposit re-sample? just reset). Debounce resize. **Sky elements**: stars: ~120 dots random, twinkle? Static offscreen — keep static (twinkle cheap: draw 20 twinkle stars dynamically? skip). Moon: small, pale, with soft glow; position top-right; also crescent? Full small disc with subtle crater dots. Clouds: a couple of dark slow clouds silhouettes? Skip; ash will do. Background details: distant ridge silhouettes behind volcano (2 layered hills darker/lighter), foreground ground texture (dots/rocks, small pines silhouettes at edges). Volcano body: fill with vertical gradient dark slate; add slope texture: random rock speckles; strata lines along slopes? Draw ~ faint horizontal contour lines? Add fissure dots. Also subtle ambient occlusion at base. Let me also render a faint pre-glow rim on crater walls toward lake even in static bg? Dynamic glow handles it. **Signature moment**: the eruption itself with camera shake + fountain + shockwave ring? A brief expanding shock ring at eruption start (thin circle expanding+fading, 'lighter') — subtle, tasteful. And the meter needle slam + status flash. Also big-boulder parallax? Keep: shockwave ring + shake + audio thump. Good. Let me also add: lightning? No. **Code structure**: ``` HUD: - header title top-left: "VULKAN / STRATOVOLCANO-01" small caps + status line - top-right stats: particle counts + fps - bottom-left panel: gauge canvas + status + sliders + buttons - bottom-right hint: "click to force eruption · space" ``` All UI DOM overlay with pointer-events (sliders need interaction; canvas click triggers eruption — attach click on canvas element; panel above with pointer-events auto; body-level). Ensure canvas receives clicks where not covered by panel. Fonts: stack: `ui-monospace, 'Cascadia Mono', 'SF Mono', Menlo, Consolas, monospace` — instrument vibe. Colors: HUD ink #e8e4d8 (bone) on translucent dark panels, hairline borders rgba(232,228,216,.14), accent hot amber #ff9a3c used sparingly for needle/active states. Uppercase labels letter-spacing .14em, 10px; values tabular-nums. Gauge: draw on small canvas (say 240×110 CSS px, dpr aware): arc from 210°... gauge from angle 135°→ 45°?? Let me define gauge sweep: start angle 150° (pointing down-left) to 30°?? Standard automotive: angles measured... I'll draw arc spanning from 180°+? Use radians: start = π*0.85? Hmm I'll do: arc start π (left) minus... Let me do classic: start angle = 0.75π? Eh, let me just do: needle angle = π*1.2? Let me define sweep from 140° to 40° going clockwise over top: i.e., angleDeg = 140 - p/100*100?? In canvas coords (y down), angle 140° means... Canvas angle 0 = +x right, 90° = down. Over-the-top gauge: from 145° (down-left) clockwise?? Let me parametrize by angle θ measured in canvas: startθ = 135° (π*0.75 = down-right? π*0.75 rad = 135° which points down-left (cos135=-0.707, sin135=+0.707 → left & down)). Sweep counterclockwise? Ugh. Easier: define θ(p) = π*(1.5) ... Let me think in terms of "up = -y". I want needle pointing up-right at mid, down-left at 0, down-right at 100. Angle measured standard math with y-down flips sign. Define φ = angle from +x axis in canvas (cos → x, sin → y with y down). 0° → right; 90° → down; 180° → left; 270° → up (i.e., -90°). I want: p=0 → needle down-left: direction (-0.866, +0.5) = 150°? cos150=-.866, sin150=.5 → left-down. p=100 → down-right: 30° (cos=.866, sin=.5). Sweep from 150°→30° passing through 270° (up) — going counterclockwise from 150° to 30°? From 150° decreasing angle: 150→90(down!) no that goes through down. Increase: 150→180→270→360/0→30: passes left, up, right — correct over-the-top sweep! φ(p) = 150° + p/100*240° mod 360. In radians: φ = (0.8333 + p*2.6667*0.01)*π... φ(p) = π*(5/6 + p/100 * 4/3). At p=100: 5/6+4/3 = 1.8333π = 330° = -30° = equivalent 30° direction (cos330=.866, sin330=-.5?? sin(-30)=-0.5 → up-right?? Hmm: I want down-right at p=100: direction (cos, sin) = (0.866, +0.5) → angle 30°. φ=330° gives sin=-0.5 → up-right. That's over-the-... wait which side? p=100 needle should point down-right if gauge spans over the top? Both conventions exist: classic car gauge: zero at left-lower, max at right-lower, sweep over top: so p=0 → 150° (down-left ✓ since sin150 = +0.5 → y +0.5 down-left ✓), p=100 → 30° (down-right ✓). Going from 150° counterclockwise (decreasing) passes 90° = straight down — through the bottom — wrong. Going clockwise (increasing angle) from 150°: 150→180 (left) → 225 (up-left) → 270 (up) → 315 (up-right) → 360=0 (right) → 30 (down-right) ✓. So φ(p) = (150° + 240°*p/100) mod 360 in the direction of increasing. In radians: φ = π*(5/6 + p/100*2.4*... 240° = 4π/3) → φ = π*5/6 + p/100*4π/3, mod 2π. Good. Ticks: minor every 10, major with labels 0,25,50,75,100? Label "0", "2", "4"? Use pressure units "bar"? Show % or "BAR". I'll label 0..100 as kPa-ish: put "0" "50" "100" and redline zone arc 85-100 in accent red + "CRITICAL" mark. Needle: line from center, hot color when >85. Center hub dot. Below: digital readout "062.4 %". Gauge canvas drawing per frame: ~50 calls — fine. **Slider styling**: input[type=range] custom: track thin 2px line with filled portion? Filled portion needs background gradient trick (linear-gradient with var(--p)) — functional, fine. Or keep simple track + thumb (rect thumb with border). Thumb: 14×22 rectangle, bg panel color, border 1px ink; hover accent. Track: rgba line 4px. WebKit + Firefox selectors. Wind slider: min -100 max 100 value 20? Show arrow direction: value readout like "22 →" / "← 22" / "0 calm". Also a small compass strip? Keep readout text with arrow char? Arrows via SVG tiny triangle that flips: I'll render text with direction glyph drawn as inline SVG rotated — simplest: text "W → 22" hmm. I'll do readout: `→ 20` where arrow char from text "→"/"←"/"·" — these are unicode arrows, not emoji (text glyphs acceptable? Arrow chars are punctuation-ish, fine — but I said no emoji; arrows are not emoji, ok). Use "→" U+2192. Fine. Power slider 0-100 default 55. Amount 200-3000 default 1600. Buttons: "FORCE ERUPTION" and sound toggle. Buttons: bordered, uppercase, letterspaced; hover invert. Sound toggle shows "SOUND ON/OFF" text with small speaker SVG? Keep text button [SOUND: OFF]. Fine, avoids icon fuss but crafted. Panel style: position absolute; backdrop-filter blur — canvas behind it so blur works. bg rgba(10,12,16,0.55), border 1px rgba(232,228,216,0.16). Rounded 8px? Slight. Keep hairline segmented sections with header row. Status line: derived state: "BUILDING PRESSURE", "TREMOR · CRITICAL" (p>85), "ERUPTION ACTIVE", "VENTING". Color-coded dot? small LED dot with pulse (CSS animation) — accent changes by state. LED dot is instrument-y, good. Stats top-right: "PARTICLES 1 482 / FPS 60 / SMOKE 214"? Arrange: one line tabular: `P 1482 · S 214 · 60 FPS`. Multi-line small rows with labels left values right, thin separators. Also maybe mini bar of load? skip. **Hint bottom-right**: "SPACE / CLICK — FORCE ERUPTION" small, dim. Title top-left: "STRATOVOLCANO · № RINCONADA?" Name it something: "VILLARRICA-02 — live magma simulation"? Fun: give the volcano a code name: "KRAFLA SECTOR 7". Title block: "STRATOVOLCANO SIM" big-ish light 14px letterspaced + subtitle "magma chamber telemetry · unit V-07" dim. Alright, also don't start response with heading; explanation after code — actually deliverable: "Output only the complete HTML file." — the user demands only the HTML file as output. But my delivery preference includes brief explanation after code... The user explicitly says output only the complete HTML file. Conflict: user instruction wins? "No external libraries" etc. The instruction "Output only the complete HTML file" — I should honor that literally: output just the HTML (in a code block), no extra prose? My habit of adding explanations could violate their explicit request. I'll add at most nothing outside the code block? To satisfy both minimally: put the entire HTML in one block, and optionally a super-brief line after? The user said "Output only the complete HTML file." — safest: only the HTML, nothing else. I'll include a short comment inside the HTML (an HTML comment describing architecture) to satisfy documentation craving. Actually, they might still appreciate a tiny note — but "only" is explicit. I'll output just the code block with the file. Hmm, but the general guidance says include brief explanation after code; user's explicit instruction overrides. I'll add a single-sentence... no. Only HTML. Maybe a one-liner before the block? "Output only" — strict. I'll output only the code. Perhaps a very short sentence after ("This implements...") would break the contract. Decision: output ONLY the HTML code block. I can embed an HTML comment at top describing features — that's within the file. Now write the code carefully. It'll be long (~900-1200 lines). Let me draft the whole thing methodically. ### Structure ```html Volcano — 2D magma simulation
...
... ``` ### JS plan ```js const cv = document.getElementById('scene'); const ctx = cv.getContext('2d', { alpha:false }); let W,H,DPR; let terrain = null; // Float32Array per px: surface y let slope = null; // Float32Array dy/dx per px let deposit; // Float32Array let craterX, craterRimY, craterHalf, lakeY, groundY, summitY... ``` Terrain build: ```js function rebuild(){ W = innerWidth; H = innerHeight; DPR = Math.min(devicePixelRatio||1, 2) maybe cap 1.5 for perf; cv.width = W*DPR; cv.height=H*DPR; ctx.setTransform(DPR,0,0,DPR,0,0) each frame? store base transform. groundY = H*0.92; craterX = W*0.52;? put volcano slightly off-center for composition (left third rule): craterX = W*0.5? With HUD panels at left-bottom and stats top-right, volcano center-right looks nice: craterX = W*0.58. baseHalf = Math.min(W*0.46, H*1.15)?? Volcano width shouldn't exceed screen: baseHalf = Math.min(W*0.44, H*0.9); summitY = H*0.24;? height above ground = groundY - summitY = H*0.68. Slope steepness: at rim, slope dy/dx = p*Hc/baseHalf*(d^(p-1))... with p=0.72, d small... near summit slope huge; near base d=1 slope = 0.72*Hc/baseHalf*1 = 0.72*0.68H/(0.9*... let's plug numbers H=900: Hc=612, baseHalf=min(0.44W≈ 700(if W 1600→ 704), 0.9*900=810) → 704. base slope = 0.72*612/704 = 0.63 (32°) hmm base still steep-ish; concave will make mid slopes ~ d^... slope(d)=0.72*612/704 * d^-0.28: at d=0.5: 0.63*1.21=0.76. Hmm that's convex? Wait p=0.72: derivative magnitude = Hc*p*d^(p-1)/baseHalf = 612*0.72*d^(-0.28)/704 = 0.626*d^-0.28. d^-0.28: at d=0.05 → 0.05^-0.28 = e^(0.28*3.0)= e^0.84 = 2.3 → slope 1.44 near summit; at d=1 → 0.63. So slopes from 55° near summit to 32° at base — stratovolcano ✓. All slopes > μ≈0.45 → slides down most of cone ✓. Slope must be < tan(75°)? Also crater rim: crater cut modifies. Terrain array: for x in 0..W-1: y = groundY - Hc*(1 - pow(d,0.72)) where d = clamp((x-craterX)/baseHalf,0,1)? Only if d<1 else groundY. Wait for x beyond baseHalf → ground. d = (|x-craterX|)/baseHalf, if d>=1 → y=groundY. noise: n = fbm(x)*amp: amp ~ H*0.012 (8px). Multiply near crater? Reduce noise within rim region to keep crater bowl clean: amp *= smooth factor outside craterHalf*1.6. crater: ch = |x-craterX|; if ch < craterHalf: t = ch/craterHalf; dip = craterDepth * Math.pow(Math.cos(t*Math.PI/2), 2); y += dip?? careful sign: dipping = surface lower → y larger: y += dip. craterDepth = H*0.11 (99px)? rim to floor. craterHalf = H*0.075 (67px)? For H=900: craterHalf 67px → crater width 134px, depth ~ 99px — good bowl. But wait rim: at t=1 dip=0 → rim edge = cone surface; actual peak of rim occurs at ch slightly > craterHalf where cone surface... the cone surface at d = craterHalf/baseHalf ~ 67/704=0.095 → height = Hc*(1-0.095^0.72)=Hc*(1-0.156)= 0.844Hc → rim y = groundY-0.844*612= groundY-516. Summit of cone (d=0) would be groundY-612; but crater dips center by 99 → floor y = groundY-516+99?? wait rim edges at groundY - 516 = say 828-516=312; center: surface at d~0 → groundY-612=216, plus dip 99 → 315. So floor ≈ rim edges level — crater nearly flat bottom vs rim same height?? That makes crater depth ~0 at center?! No: dip measured from cone surface at center: center surface = groundY-612; +dip 99 → floor at groundY-513 = 315. Rim edges (t=1) = cone surface at ch=67 → x = craterX±67 → d = 67/704 = 0.095 → surface = groundY - 612*(1-0.156) = groundY-516 → y=312. So floor y=315 vs rim 312: nearly equal — the "crater" is level with rim?! Because cone near summit is nearly vertical (height from d=0.095 to 0 only 96px over 67px horizontal — slope 1.44). So the dip of 99px matches the cone's rise. Result: crater bowl whose rim ≈ floor height — too shallow. Fix: make dip relative to rim, not center cone surface: subtract? Redefine: build cone, then crater dip = depth * cos²(t·π/2) where depth relative to the rim edge surface? Let me compute cone surface along the crater zone and force rim height: The classic approach: define crater rim height rimY = surface at ch = craterHalf (edge). Then inside: y = lerp(rimY, floorY, bowl(t)) where bowl(t) = cos²(t*π/2)... at t=1 → 0 → y=rimY ✓ at t=0 → 1 → floorY. floorY = rimY + craterDepth. And outside crater (ch>craterHalf) blend from rimY to cone surface over small transition (smooth within ch lakeY → region width: cos²(tπ/2)*depth > depth - lakeDepth → cos² > (1 - lakeDepth/depth)=0.65 → t < acos(sqrt(0.65))*2/π = 0.36 → halfwidth = 0.36*craterHalf ≈ 24px?? For craterHalf 67: lake width 48px, small lake. Fine, or make lake depth 0.45*depth → width bigger: cos² > 0.55 → t<0.43 → 29px half → 58px wide. OK. lakeY = rimYedge + depth*0.55. Hmm wait, floorY = rimYedge + craterDepth; lake depth from surface to floor = 0.45*craterDepth. Particles settling inside bowl: if x within lake width and y > lakeY → treat as lake splash (kill→fuse). For ballistic entering lake region: check terrain collision: terrain at that x = bowl surface (could be above lake surface on walls). For central area, terrain = floorY but lake surface above → detect water: if y >= lakeY && within lakeHalf → splash. Also sliding particles on floor moving toward center: when their y >= lakeY and within lakeHalf → splash kill. Deposit inside lake? Suppress deposit within lake region (it fuses). Settled count fine. Also I want eruption fountain from the lake — spawn position inside lake ± small; also spawn some from crater walls (vents on rim? side vents: minor jets from lower flank fissures? Extra flavor: 2 small fissure vents on flanks emitting small spatter + smoke when erupting? That adds wow. Keep: during eruption, spawn ~8% of lava from two flank fissure points with lower speed arcs — secondary jets. Nice visual richness. Their terrain: spawn slightly above surface at fissure x positions (craterX ± baseHalf*0.45), y = surfaceY - 4. Small arcs. OK.) **Terrain arrays**: terrainY per px (Float32Array W). slope s per px = (terrainY[x+1]-terrainY[x-1])/2 — compute after. deposit Float32Array W. Normal for bounce: n = (-s, 1)/L normalized with s from slope at x... For surface: tangent T=(1,s)/L; outward normal N = (s,-1)/L?? Let me recheck: for s>0 (downhill right), surface direction right-down. Outward normal should point up-left-ish away from ground: N = (s, -1)/L: at s=0 → (0,-1) up ✓; s=1 → (.707,-.707) up-right ✓ pointing away from slope. Yes N=(s,-1)/L. Collision resolution: after move, if y > terrainEff(x) (where terrainEff = terrainY[x] - deposit[x], and also lake handling): pen = y - eff. Push back: y = eff - eps. vn = vx*s? compute vn = (vx,vy)·N = (vx*s - vy)/L. If vn < -60 (moving into surface): bounce: v' = v - (1+e)*vn*N; tangential damp: v'·T *= 0.6; e=0.3. Else sliding mode: project: vt = (v·T) = (vx + vy*s)/L; set mode=1, vx=vt (store vt in vx field, vy unused in slide? For slide store vx = tangential speed; y pinned). Wait need also handle deposit-l landings: same. Bounce may re-boost upward: check apex... fine. Also lateral bounce for hitting steep inner crater walls while falling into crater from above? Particle falling into crater hits inner wall: terrain handles all — normal from slope ✓. **Slide update**: ``` x += vx*dt (vx tangential) pin y = eff(x) - 1? y = terrainEff(x) - 0.5 → looks resting slightly embedded? Actually particle radius ~3: draw circle centered at y - r*0.35 so it sits on surface. Simpler: keep y = eff(x) - 2. recompute local slope s at x: a = G*s/L - friction... friction dynamic μ = 0.24 + (1-heat)*0.5 (heat 1 → μ 0.24 slides even moderate slopes; cold μ .74 → static quickly) fric decel = μ*G/L if |s| > μ → accel else decel to stop. vx += (a_t)*dt; then clamp friction: vx -= sign*fric*dt; if crossing zero → 0. if |vx| < 6 && |a_t| < fric*1.2 → settle → deposit add, mode=2. speed cap: |vx| = min(|vx|, vmax=H*0.55?) with soft. Also random jiggle: vx += (rand-0.5)*jig. Also slide particles can leave crater sides going down flanks ✓. Also if slide reaches lake region (y > lakeY && inside lakeHalf) → splash fuse. Also slide particles that hit steep uphill?? They only go downhill by gravity; on crater floor moving sideways up bowl wall? slope would decelerate them → settle. ok. ``` Edge: sliding downhill past base into ground flats → slope small → settle → streaks at base ✓. Also deposit clamp: when deposit[col] > cap (say 16), overflow to neighbors: ``` addDeposit(col, amt): let c = col; while(amt>0.01 && c within bounds){ let room = 20 - deposit[c]; if(room>=amt){deposit[c]+=amt; return} else { deposit[c]+=room; amt-=room; c = pickNeighbor(c); ... } } ``` neighbor pick: choose side with lower deposit (deposit[c-1] vs deposit[c+1]); if both full, break. Also on settle, particle records final column? For death decrement: track per particle: settleCols recorded each time it adds (it only adds once at settle — since static doesn't move). But when static particle y pinning uses eff(x) which later changes if deposit grows (new settling on top raises bank — old static particle should ride up? If deposit under a static particle increases, eff decreases (higher surface) → static particle's y should update: pinned: each frame static recomputes y = eff(x) - ownOffset. Since deposit increased at column, the particle floats below new surface? Wait eff = terrain - deposit: larger deposit → smaller eff value → surface higher. A static particle at y should be lifted: y = eff(x) - 0.5 re-pins it on top of bank — but then multiple particles all pin at the same top point → they visually stack at same spot (overlap) — draw overlaps fine (looks like pile). But conceptually "deposit" represents pile height: pin each at top means piles render as overlapping dots at surface — acceptable visually (glowing bank). OK just pin. But careful: particle death decrements deposit[col]; if bank has multiple, ok. While SLIDING on raised banks: pin uses eff(x) → rides over banks ✓. **Airborne wind**: vx += windAcc*0.06*windUnit*dt. **Smoke**: ``` smoke: spawn at crater vent during erupting & pressure plume. fields: x,y,vx,vy,age,life,size0,tint(0..1 warm),phase. update: vy += (-buoy*(1 - t*0.8))*dt where buoy ~ 90? Set: vy starts -120..-260 (scale H*0.3?) let: vy = -(H*0.16..H*0.30) * pulse, then vy relaxes toward drift: vy += (targetVy - vy)*dt*1.5? Simple: vy stays initial *decay? Let: vy = vy0 * exp decay? Use vy += (-lift + buoy decay): code: vy -= lift*dt*(1 - age/life*0.9); hmm produce rise then hover. Let me define: vy = vy0 * (1 - 1.6*age/life)? plus turbulence: vy += sin(age*1.7+phase)*20*dt... simpler: vy += (-H*0.45 + age*H*0.09)*dt? At age=0: up accel -405; at age 1s: -405+81*... after 5s: -405+405=0 → hover. That's smooth: vy += (-H*0.5 + H*0.1*age)*dt. Cap. vx: vx += windAcc * windUnit * dt*? plus turbulence sin horizontal + slight spread: vx += windA*windUnit*dt + sin(age*2.2+phase)*H*0.02*dt; plus drag toward wind velocity: vx += (windSpeedTarget - vx)*dt*0.8 where windSpeedTarget = windUnit*H*0.16? Combined approach: vx += ((windUnit*H*0.18) - vx)*dt*(0.6+..) + turbulence. Good: column follows wind. size = size0*(1+age*1.6); alpha = smoothstep in/out: a = min(age/0.4,1) * (1 - age/life)^? * baseA (0.5?) Actually with sprite alpha baked ~0.55 max; globalAlpha per puff = a*(0.5..0.75 by tint?). tint: young & near crater → warm (lit): tint = max(0, 1 - age*0.6) * eruptive heat? During big eruption more warm. Baked sprite: two sprites (warm brownish #b06a45 gray, cool #737a84...). Choose sprite per puff by tint ≥0.5. Plus per-puff shade variation via alpha. Draw: source-over, per-puff globalAlpha a*0.45; drawImage sprite scaled size. Cap smoke: maxSmoke = clamp(amount*0.4, 80, 1000). ``` Ash spawn rate: erupting: rate = (amount*0.28)/2.5s over eruption duration + tail 2s at 30%; pre-erupt plume when p>65: rate = 8*(p-65)/35 /s. Post: decaying. **Lava spawn during eruption**: ``` rate = amount / D * pulse (pulse 0.55+0.45*sin) — integrate fractional. per spawn: type: 78% fountain lava, 12% bombs (bigger slower?), 10% sparks tiny fast. Simplify: sizes: r = 1.5 + rand^2*3.5; speed multiplier inversely ~ (1 + ...)? Varied velocities ✓ requirement. angle: θ = π/2 + (rand-.5)*spread where spread = 0.5+0.6*power? Power slider also scales speed. Direction mostly up, slight bias outward from center: vx bias = (x - craterX)/craterHalf*0.3. v = speedBase*(0.55+rand*0.9)*powerMult; speedBase = sqrt(2*G*apexT) with apexT = H*(0.18+0.5*power)*... define power slider p∈[0,1]: apexT = H*lerp(0.14, 0.6, p^1.2) + rand jitter*0.3. speed = sqrt(2*G*apexT)*(0.7+0.6*rand). With G=2.4H: H=900 → G=2160; apexT 0.14H=126 → v=sqrt(2*2160*126)=738; *0.7..1.3 → 517-960. apex range 126-520px. Plausible. spawn pos: lake region center ± 0.5 lakeHalf jitter, y = lakeY+4. heat=1, life = 7 + rand*13 (settle lifetime; ballistic flight ~2-4s). Also faster cooling for small? heat decays: heat = 1 - age/life*0.85? Better: heat declines exponential then floor: heat = max(0, 1 - age/life)?? Cooling should be steady: heat = clamp(1 - age/life*0.9...) Let me: heatLevel = 1 - (age/life)^1.1... At death (age→life) heat→0... but particles settle then stay until death: heat at 0.15 becomes "rock" and remain dark until life ends — that's ok: rock chunks resting then fade (disappear). Lifetime fade also alpha. Also faster cooling for particles high up (air)? skip. Also drag: sparks tiny high drag? Apply uniform mild drag to all lava: v *= 1 - 0.12*dt? mild. ``` **Lake splash**: if particle (airborne or sliding) enters lake: kill + lakeHeat += 0.12 (cap 1) + spawn 1-2 sparks (tiny up v) + small smoke puff occasionally + tiny "plop"? Visual: brief bright flash on lake surface (lake flash array? keep lakeHeat covers). **Lake rendering**: ``` each frame: lakeGlow = 0.3 + 0.55*p/100^1.5 + lakeHeat*0.8 + erupting? extra while erupting 0.9 flicker. Draw lake polygon: clip to bowl? Simpler: draw ellipse centered (craterX, lakeY+lakeDepth/2) radiusX lakeHalf, radiusY lakeDepth/2+2. base color dark #2a0d08; then hot crust: draw N blobs (precomputed offsets) with color mix by heat flickering (sin per blob with time). Then 'lighter' glow ellipse alpha ~ lakeGlow*0.35. Also crater inner wall glow: gradient arc? Draw a radial glow sprite at crater vent scaled by pressure each frame under 'lighter' — covers walls+sky behind? Glow sprite at vent (craterX, lakeY) with radius ~ craterHalf*2.2 alpha pressure-based flicker*(0.25). ok. Also eruption adds huge glow flicker. ``` Lake bubbles: occasionally spawn tiny lava particles at lake surface with v up small (2-5/s when lakeHeat high or erupting) — they pop. **Shockwave**: on erupt(): push {t:0} wave: expanding circle stroke from vent, radius = t*H*0.9, alpha (1-t)*0.35 'lighter'; also second slower? one ring. 0.6s. **Shake**: shake = 1 → decays *= exp(-3dt) offset amp = shake*H*0.008* sin? offsetX = shake*amp*sin(t*47), offsetY = shake*amp*sin(t*39+1)*0.6? Apply translate before drawing everything each frame (including bg image). Reset after. **Audio**: ``` let audio = {ctx:null, master, noise, src..., on:false} initAudio(): ctx = new AudioContext; master gain 0→; noise buffer 2s pink-ish (filtered white: just white * lowpass? generate buffer with random, then a biquad lowpass node freq 90, plus another peaking? Keep: white noise buffer → lowpass(120) → gain(env) → master. Also sub-bass osc? A "rumble" gain envelope: on eruption: gain: cancel, setTarget: to 0.7 in 0.1s, then to 0.001 over 4s (exp). Also a "boom": sine osc 60→30Hz freq ramp 0.4s with gain env 0.5→0 1.5s, triggered on erupt. Also continuous "chamber growl": noise gain tied to pressure*0.05? Might get annoying — keep eruption-only + faint wind hiss? Keep eruption rumble only. Toggle button: label SOUND: ON/OFF. First enable creates context (user gesture ok). Also on eruption if enabled → boom. If disabled nothing. ``` Careful: AudioContext requires user gesture — button click qualifies. **FPS**: ema: fps = 0.9*fps + 0.1*(1/dtRaw)? display round. Update DOM every ~250ms (avoid layout thrash). Also particle counts every 250ms. Use textContent updates. Also handle document.hidden / rAF automatic pause; dt clamp prevents explosion. **Perf details**: - Bucket arrays: pre-create 9 arrays for lava glow tiers + cold group + smoke group? Implement drawing: ``` // group particles by tier (0..7 heat) for glow pass; cold tier separate ``` Simplify: each lava particle compute tier = heat mapped: tier sprite index 0..5 where 5 hottest? Then for draw: for t in 0..5: if bucket non-empty: set globalAlpha? per-particle alpha via property each drawImage — fine. Cold (<0.12) drawn source-over first (so glow overlays? hot over cold — fine any order). Then hot tiers with 'lighter'. Wait drawing order interplay with smoke: smoke above lava? Smoke drawn last (ash floats above scene). ok. **Sprites pre-render**: ``` function makeGlowSprite(color core rgb, halo rgb, coreStop, ...): canvas 64x64 radial gradient: stops: 0: core a1; 0.28: core a0.95; 0.55: mid a0.35; 1: a0. Tiers: t5 (white-hot): core #fff7d6? Actually hottest: near white (255,240,205); halo (255,160,60) t4: core (255,196,110); halo(255,120,40) t3: core (255,120,50); halo(255,90,30) t2: core (232,80,32); halo(160,45,25) dim t1: core (150,45,28) dim alpha lower; halo(90,30,20) t0 rock: sprite: solid dark: stops 0:(54,44,38,a1) 0.55:(48,40,36, a1) 0.8:(40,34,32, a0.6) 1: a0 — soft dark chunk. ``` Wait 'lighter' composite with sprite whose alpha < 1: canvas drawImage respects alpha → additive contribution scaled — good. The rock sprite drawn source-over opaque-ish — good. Smoke sprites: 2: warm: radial (185,120,86 → 120,80,70 → transparent) alpha low; cool: (120,124,132 → 70,74,82 → 0). Bake max alpha ~0.9; we apply per-puff globalAlpha ~0.28. Hmm actually for smoke vs 'lighter' glow behind: smoke drawn after lava covers glow with translucent gray — mixture ok. **Main loop**: ``` function frame(tms){ requestAnimationFrame(frame); dtRaw = (tms - lastT)/1000; lastT = tms; dt = clamp(dtRaw, 0, 0.05); if dt==0 → still render? fine skip guard. time += dt; step(dt); // physics+spawn render(); hud updates throttled. } ``` **step(dt)**: ``` pressure dynamics; erupting timer; spawn accumulators (lavaAcc, smokeAcc) integrated. lakeHeat decay: lakeHeat *= exp(-1.2dt)? plus clamp. windActive = windUnit (slider value -2..2)... define slider -100..100 → windUnit = v/50 → [-2,2]. for particles: lava modes... smoke update... remove dead: swap-remove pattern with tail index (particles array maintained as list with swap-pop). ``` particles stored in array `parts` (mixed). Use for i from 0..n, if dead → swap with last, pop, adjust i--. Death: if settled → deposit[col] -= contribution (track contribution on particle: p.dep = amt added at settle col p.col). Clamp not below 0: deposit[c] = max(0, deposit[c]-amt). Also cap total deposit fine. **Ballistic → slide transition** and bounce sound? skip. **Rendering**: ``` ctx.setTransform(DPR,0,0,DPR,0,0); apply shake translate (time-based). ctx.drawImage(bgCanvas, 0,0, W,H) (bg offscreen at DPR? draw bg offscreen with same DPR scaling: bg.width = W*DPR... draw via drawImage(bg, 0,0,W,H) after setting transform scaled — drawImage(bg,0,0,W,H) maps full-size source to WxH dest ✓ (source canvas pixel dims W*DPR, drawn into W CSS px → downscale, crisp). ok.) vent glow ('lighter') behind lava? draw before lava so lava on top: 'lighter' commutative anyway. shockwaves. lava particles: cold pass source-over; hot pass 'lighter'. lake crust? Lake drawn before particles (particles splash over lake). Lake drawing: normal composite base + hot blobs (could use 'lighter' pass) → do base with normal, then a 'lighter' group draw for lake glow along with vent glow. Let me order: 1 bg 2 source-over: lake base fill + crust blobs 3 lighter: lake glow ellipse, vent glow, shockwave rings 4 lava cold pass (source-over) 5 lava hot pass (lighter) 6 smoke (source-over, alpha) restore transform (shake) before? HUD is DOM so unaffected. ``` Wait smoke drawn after hot lava: ash over glow dims the fountain behind — realistic. ok. Also darken vignette? subtle vignette drawn on bg (radial dark edges baked). Fine baked in bg. **bg render (offscreen)**: ``` sky: vertical gradient: top #0a0d1a?? Let me define palette: - zenith: #070a14 deep - mid: #131a2b - horizon: #2b2436? warm dusk: #38253a?? Let me craft: horizon behind volcano glow-ish faint warm: rgba(94,60,50, .35) band. Compose: linear gradient stops: 0:#05070f, .45:#0d1424, .75:#1b2138?? too blue-purple? I said avoid blue/purple clichés for UI, but night sky is naturally dark blue — representational ok; keep muted slate not vivid purple: top #060811, mid #10151f?? hmm with warm horizon #241d20 → subtle. Fine: stops: 0 '#05060d', 0.5 '#0e1220', 0.82 '#1e1c26', 1 '#332728'? Slight warm at horizon. ok tasteful. stars: 140 dots varying alpha size (precompute). Moon: at (W*0.82, H*0.16) r=26: disc #e8e3d4 alpha .9, soft glow sprite around (draw radial), couple of darker blotches. distant ridges: 2 layered silhouettes (dark #10131c, #151824) — generated via noise polylines across width, behind volcano (lower third? hills at horizon y ~ groundY-... ridges at y between H*0.55..0.75 behind volcano base). volcano body: path along terrain (terrain array!) filled vertical gradient: top #262c38 → bottom #0c0f14? Wait summit lighter (rim-lit) or darker? At night with lake glow below, crater walls lit — dynamic. Static body: base #10131a→#181d27 mid? Let me: gradient from summit-ish darker? Choose: body fill gradient vertical: y summit → '#2a2f3a'?? Hmm silhouette night mountains are darker than sky usually. Sky at horizon ~#332728 warm dark. Volcano darker: gradient #0b0d12 (top) → #131822 (mid) → #0a0c10 (base)? Simpler single #101218 with subtle vertical variation + texture speckles + strata. Actually nicer: darker silhouette with faint warm rim on the crater rim (baked faint 2px stroke #5a3a2e alpha .35 around rim curve? Baked using terrain near crater). Eh, dynamic glow will light it anyway. Skip baked rim. ground: fill from groundY to bottom with gradient #0d0e12→#131620; texture: speckles (random dots alpha), few rocks (small polygons), 3-4 pine silhouettes near edges (triangles stacked, dark #0b0d11) — draw at left/right edges away from volcano. speckles also on volcano flanks (tiny dots #1b202c alpha .5, denser lower). strata: few subtle darker arcs across flanks? Skip or minimal. vignette: radial gradient transparent→rgba(0,0,0,.5) edges baked over. ``` Volcano body silhouette uses terrain array path (moveTo (0,groundY)... along terrainY per 3px) then close down to ground. Also ground path includes volcano? Ground fill drawn after volcano body? The terrain already includes volcano shape rising from ground — draw single terrain fill (the whole landmass incl. volcano) with gradient? Gradient vertical across full height: top (summit area) vs base. Single landmass gradient: y from summitY to H: stops: at summitY '#14161c'? hmm crater interior is below rim — inside bowl would get gradient of deep part (darker toward bottom? Bowl walls lit dynamically; base fill: make interior slightly warmer dark '#1a1520'?? Simplify: landmass fill with vertical gradient: 0.2 (summit) #232833 → 0.55 #161a22 → 1 #0c0e13. Bowl interior part of same shape (it's concave — the shape's interior includes bowl? The terrain path follows surface: enters the bowl down and back up — so the fill region includes everything below surface — including crater interior void? Actually the crater interior above floor is AIR (a depression). The fill path along surface dips into bowl — fill covers below-surface region: the bowl interior above floor is above surface?? Let me think: surface path: comes up left flank → at rim edge → dips down into bowl (y increases) → across floor → up other side → along summit? wait crater rim edge is local max? Terrain: flanks rise to rim edges, dip to floor at center. The polygon (surface → bottom) fills everything under the surface — including the bowl depression area? The region "under surface" includes bowl air? No: surface at rim edge high (y=312), floor y=315; between rim edges the surface dips: region between surface line and bottom filled → includes the thin band... The bowl air is ABOVE floor but BELOW rim height, and it's ABOVE the surface line (since surface at those x = floor which is below rim). Air above floor and below rim = above the surface → not filled → appears as sky-colored?? But it's a crater hole — should be darker interior, not sky. Hmm: since bowl walls are steep, at any x within crater, the surface is the bowl profile (rim edges only at edges). The area above floor up to rim height at center x is... surface(x center) = floor 315; sky visible above 315 at center — through the gap between rim edges: YES you'd see sky through the crater mouth from side view — actually in side view, the far inner wall is visible (the crater interior shows the back wall, not sky, unless the far rim is lower). In 2D side view we can cheat: draw crater interior as dark region: after landmass fill, draw a "crater interior" polygon: between rim edges, fill from rim-edge surface line (straight line across at rim height) downward to bowl surface with dark interior color (near-black warm #14100f) — representing shadowed inner wall. Then lake on top. That reads correctly. So bg: landmass fill; then craterInteriorFill: polygon: leftRimPoint(craterX-craterHalf, rimEdgeYleft) → across top at y = min rim heights (straight or slight arc) → rightRim → down right side along bowl surface → floor → up left side along bowl surface → close. Simpler polygon: points: (L, rimY_L) top-left, (R, rimY_R) top-right, then follow bowl surface from R back to L (per-px terrain in bowl). Since rim edges equal height (symmetric), top line horizontal-ish (add slight noise skip). Fill '#120e0d' alpha 1 (over landmass gradient) — crater void dark. Then speckles? then lake dynamic (per frame). And crater interior needs to also receive dynamic glow — glow drawn over bg ✓. Wait but the terrain fill region below bowl surface: from bowl surface down to bottom is filled with landmass gradient — includes the interior region below floor (rock) ✓ correct (deep rock). And above-floor void painted dark inner-wall color ✓. Hmm but at rim edges the transition: bowl surface meets flank surface — inside dark region boundary at edges: top edge line from (L,rimY) to (R,rimY): at x slightly outside L, surface = flank higher than rimY? flank rises steeply outside craterHalf, so surface > rimY (y smaller) → landmass fill covers above rimY there → dark region polygon's top line at rimY would overlap landmass? Dark fill drawn after landmass: dark polygon spans x∈[L,R] at rimY down to bowl surface. Outside [L,R] landmass surface higher (covers dark's edge?) At x +). Heights ~ 14-30px. Draw dark #080a0e. Also scattered boulders on flanks (baked): small dark blobs along terrain, alpha subtle. Fine. Also faint clouds? skip. **Now UI DOM**: ```html
STRATOVOLCANO
magma chamber · unit V-07 — live particle simulation
PARTICLES0
LAVA / SMOKE