### 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