### PARTIAL OUTPUT (time limit hit; the model was still generating) ### === thinking so far === Let me plan this volcano simulation carefully. It's a physics/rendering piece, and I want it to feel like a living scene, not a generic canvas demo. **Core architecture:** - Single canvas, full screen, devicePixelRatio-aware. - Fixed-timestep physics (accumulator) decoupled from render for stability, or simpler: dt-clamped variable timestep. With 1000+ particles, I'll use typed-object pools (arrays of objects, reused) to avoid GC churn. - Particle systems: 1. Lava particles — ballistic arc with gravity, collide with terrain (heightmap), then slide downhill along slope with friction, cooling over lifetime. Rendered with additive-ish glow: draw hot particles with radial gradient sprites (pre-rendered offscreen canvases at a few colors for speed — actually per-particle gradients are slow; I'll pre-render ~5 color-stage sprites onto small offscreen canvases and use `drawImage` with globalAlpha + `globalCompositeOperation = 'lighter'` for hot ones). drawImage of small sprites is very fast, easily 1000+ particles. 2. Ash/smoke particles — rise from crater, buoyancy, drift with wind, expand and fade. Rendered as soft dark/gray sprites with normal compositing. 3. Embers/sparks maybe as part of lava (same pool, smaller size). **Terrain:** generate a heightmap for the volcano cone via a profile function: flat ground, a slope rising to a crater with a dip in the middle (classic truncated cone with funnel). Add some noise for natural look. Store as array `terrain[y at x]`. Collision: particle checks `py >= terrain[ipx]` → landing. Sliding downhill: when landed, move along the heightmap gradient: `slope = (terrain[x+1]-terrain[x-1])/2`, particle x-velocity = sign * slideSpeed scaled by slope steepness, friction decays it, and stick when slope flattens. Also particles can roll slightly. Settled lava particles darken and eventually vanish — so terrain doesn't accumulate (requirement says "settle downhill... then disappearing" via cooling — good, keeps count bounded). **Pressure system:** pressure 0→1 over time (rate influenced by nothing or slight randomness with "tremor" wobble for drama). When pressure >= 1 → eruption: continuous emission for a duration scaled by power slider; pressure drains during eruption; then resets to 0 and rebuilds. Add rumbling: screen shake, crater glow intensifies as pressure rises. I'll make the pressure meter a nice vertical gauge on the side with tick marks, styled like an instrument — not a gradient banner. A canvas-drawn or DOM meter? DOM with a fill bar is fine; I'll do a custom-drawn needle gauge... Actually a vertical thermometer-style bar with ticks drawn on the canvas or a separate small canvas. I'll do DOM: a slim vertical track with a fill and tick marks, plus an SVG-ish needle — keep it simple: vertical bar + ticks via repeating-linear-gradient... hmm, I avoid decorative gradients but a functional meter fill is fine as solid color with ticks. Solid crimson fill on dark track, hairline ticks. Good. **Wind:** slider from -N to +N; affects ash strongly, lava slightly, and flag/ambient (maybe drifting ember sparks, subtle sky dust). Visualize wind with faint streaks in the sky — drifting thin dashes whose speed mirrors the wind value. That's a nice alive touch and communicates wind direction at a glance. **Sliders:** three custom-styled range inputs: Eruption Power, Wind, Particle Amount (max concurrent spawn multiplier / pool cap). Live-adjustable. **HUD:** top-left panel: particle count + FPS, updated every ~500ms. Monospace-ish distinctive font — I'll pull a characterful font from Google Fonts CDN (allowed: external CDN references are fine). Something like "Space Grotesk" or a mono like "IBM Plex Mono" for readouts. I'll use IBM Plex Mono for data and maybe "Bricolage Grotesque" or "Instrument Serif" for the title. Keep it restrained: dark UI panels with hairline borders, amber/crimson accent matching lava. **Visual direction:** I want a painterly dusk scene rather than flat cartoon: deep twilight sky (this is a case where a sky gradient is legitimate — a natural atmospheric gradient, not decoration), the volcano as a dark silhouette with a glowing crater, stars faint, layered mountains behind for depth. Lava glow illuminating the cone when erupting (radial glow behind crater). Colors: near-black indigo sky up top → burnt orange dusk at horizon. Lava palette: #fff3b0 → #ffb347 → #ff5e2b → #c93007 → #4a1d14 → fade. Wait — I should be careful about my own stance on gradients: a sky gradient here is a natural rendering of atmosphere in a canvas scene, which is genuinely representational, not a decorative UI gradient. That's fine and I'll own it. UI chrome stays flat, precise, hairline borders. **Performance plan:** - Pre-render sprite atlas: small canvases per color stage (e.g., 6 stages for lava glow, each a radial gradient blob), plus smoke puffs (2–3 gray soft blobs). drawImage with scaling = fast. - Hot lava drawn with `lighter` composite; draw all hot ones in one batch (set composite once, loop), then reset for smoke. - Pool cap = slider-controlled (500–3000), reuse dead particles. - Physics: simple Euler, per-particle O(1). Terrain lookup via index. Avoid Math.random in hot loop where possible — it's fine actually. - FPS: rolling average via frame delta. **Physics details:** Terrain generation: ``` groundY baseline = H*0.82 volcano: centerX = W*0.5, baseHalfWidth = W*0.34, peak height = H*0.42 above ground → peakY = groundY - H*0.40 crater half width = W*0.055, crater depth dip = peakY + 26 heightAt(x): if |x-cx| > baseHalf: groundY + small noise hills else: t = |x-cx|/baseHalf; y = lerp(peakY, groundY, smoothstep-ish t^1.4) crater: if |x-cx| < craterW: dip down via parabola add ridged noise: y += noise for texture (small, e.g. ±6px with a few sine octaves) — but keep crater area usable. ``` Also render the terrain fill each frame? Terrain is static — render once to an offscreen canvas (with texture: dark rock silhouette, some streaks, glow near crater redrawn dynamically? Crater glow pulses — draw glow dynamically on main canvas each frame, terrain silhouette cached offscreen. But lava glow lighting the slope changes... I can draw an additive radial glow at crater each frame over the cached terrain — cheap and looks great.) Background: stars + distant ridge lines — also cached into the background offscreen (sky gradient + stars + far mountains). Redraw only when resized. Eruption sequence: - pressure builds: `pressure += buildRate * dt` with slight nonlinearity + tremor jitter near max. - When pressure >= 1: state = ERUPTING, eruptionTimer = duration = 2.5 + power*4 s. During eruption, pressure drains to 0 across the duration. - Emission rate: `power * 400` particles/sec (scaled by particle amount slider as pool cap) from crater mouth with spread: vx = (rand-0.5)*power*140 + wind*0.3, vy = -(220 + rand*power*380). Also continuous smoke emission scaled by activity, plus after-eruption lingering smoke. - Also small always-on fumarole: occasional wisps even between eruptions for aliveness (few smoke particles trickling). Lava particle update: ``` if airborne: vy += G*dt; vx += wind*windFactor*dt (light drag); x+=vx*dt; y+=vy*dt if y >= terrain[floor(x)] : land → mode=slide, compute slope slide: slope = (terrain[x+1]-terrain[x-1])/2; slideV += slope*slideAccel*dt (slope sign: positive slope means going down to right? terrain y increases downward, so terrain[x+1]>terrain[x-1] means downhill to the right → slideV += that * k) slideV *= friction (e.g., pow per dt) x += slideV*dt; y = terrain[x] - small embed also cool faster on ground; if slideV tiny and slope tiny → settle (mode=still), just cool. still: y = terrain[x]; cool. cooling: heat -= dt * coolRate (air cool slower? actually flight cools less; contact cools faster? I'll do: flight cools slowly, on ground cools faster). heat 1→0. Color index = heat stages. When heat<=0 → despawn (release to pool). Bounce: if landing vy > threshold, small bounce chance? Keep simple: slight bounce if speed high (vy = -vy*0.25) adds life. Do a bounce when impact vy > 180: reflect with damping, spawn 1-2 spark fragments if power high? Fragment spawning risks pool blowout — cap: only if pool has room and count small. I'll include modest splatter: on high-impact landing, spawn 0–2 small sparks with low heat cost. Guard with pool capacity. ``` Smoke particle: ``` vy rises with buoyancy: vy approaches -riseSpeed; vx approaches wind*windScale + turbulence sin(time*f + seed) size grows, alpha fades; life ~4–8s; color dark gray → light gray. Rendered normal composite under? Over lava but below glow? Order: bg → terrain → settled+all lava (lighter for hot) → smoke on top with alpha → foreground? Smoke above lava looks right for plume. ``` Rendering lava: heat 1.0→0 mapped to sprite stages: - 1.0–0.85: white-hot core sprite - 0.85–0.65: yellow - 0.65–0.45: orange - 0.45–0.28: red - 0.28–0.12: dark red (still additive but low alpha) - <0.12: dark rock — draw with normal composite small dark dot, or just skip drawing and let a "rock" stage draw as dark crumb with normal alpha fading. Simplest: stages 0..5 array of sprites; last stage drawn normal-composite. To avoid composite switching per particle, batch: iterate once for hot (lighter), and collect rock-stage indices in a second pass. Or just set composite per particle — bad. Two loops over the array with an if on heat: loop1 lighter-draws heat>0.12, loop2 normal-draws the rest (few). Fine. Sprite pre-render: ``` makeSprite(color stops, size=32): radial gradient center white-ish → color → transparent. Stages: s0: #fff8e5 core → #ffd24d → rgba(255,140,20,0) s1: #ffe27a → #ff9a1f → transparent s2: #ff9a3c → #ff5416 → transparent s3: #ff5e3a → rgba(180,30,10,0) s4: rgba(255,77,20,..) dim smoke: gray radial soft. ``` Particles also flicker: scale = base * (0.85+0.3*rand) cached per particle, plus time flicker cheap: use particle's seed with sin(time*freq+phase) — a sin per particle is okay-ish; cheaper: precomputed flicker array. I'll just use `(Math.sin(t*10+p.seed) *0.15+0.95)` — 1000 sins/frame is fine. Wind visualization: array of ~40 wind streak particles in sky: x drifts with wind*40 + slight per-particle speed, wrap around, draw as short line with low alpha, alpha scales with |wind|. Nice. Screen shake during eruption: shakeAmp tied to eruption intensity, applied as translate before drawing world (not UI since UI is DOM). Crater glow: pressure-driven: glowIntensity = max(pressure, eruptionBurst). Draw radial glow (additive) at crater, plus rim light on cone edges? Rim light is complex; the additive glow over terrain cache gives the impression. Also inner crater lava pool visible: draw bright line/ellipse at crater mouth whose brightness pulses with pressure. Pressure meter (DOM): vertical gauge on right side; fill bar height = pressure; ticks; label "PRESSURE" vertical text; percentage readout; warning color when >0.75 (crimson pulse). Build with plain divs + CSS. Add a "tremor" class that jitters slightly at high pressure. Eruption status line: "DORMANT → building" / "ERUPTING" indicator with small blinking dot. Controls panel bottom-left: three sliders, custom styled (accent-color or full custom webkit styling — I'll style thumb and track with CSS for the aesthetic). Values shown live. Particle amount slider changes pool cap live: cap 300–3000 (default 1600). When cap lowered, excess particles just die out naturally (don't force-kill, or kill oldest when spawning — spawn guard uses active count). Counter top-left: PARTICLES n / FPS n, updated 4×/s. Also maybe show wind direction arrow. Keep it. Signature moment: the eruption itself — pressure needle trembling, ground shake, then a fountain of glowing lava arcing with additive bloom, sparks raining onto slopes and creeping downhill while the ash plume leans with the wind. Also: lava landing creates brief glow spots on slope (the sliding particles themselves do this). Add sound? No audio requested; skip (autoplay issues anyway). Maybe a subtle "boom" via WebAudio on user interaction... skip, keep focused. Title overlay: small, elegant: "VULCAN / stratovolcano simulator" — restrained type, top center-left. Use "Instrument Serif" italic for the wordmark + IBM Plex Mono for data. Nice authored feel. Let me also handle resize: rebuild caches, recompute terrain; particles keep going (terrain array replaced — positions may mismatch; acceptable; or scale x proportionally — just rebuild). DPR: cap at 1.5 for perf on hidpi? Cap 2 but with heavy particles maybe 1.5. I'll cap DPR at 1.75. Now the terrain noise: I'll write a small value-noise (sum of sines with irrational freqs) for ground undulation far from volcano, and slight ruggedness on cone flanks (but ensure crater zone smooth). Terrain render (cached): fill silhouette dark (#17120f-ish), then add: - slope texture: random short strokes slightly lighter/darker along the surface. - a few faint ridge lines from crater downward (darker strokes) for structure. - ground foreground slightly different dark with sparse dead-tree silhouettes? Might add tiny distant trees for scale — small triangle-ish pine silhouettes on the flanks far from crater, very dark. Adds authored charm. A handful of pines near base on both sides. Also a thin mist band at ground horizon in background cache. Background cache: sky vertical gradient (deep #0b0e1d top → #1a1626 → dusk band #3d1f1c near horizon? Let me pick: top #070a18, mid #141328, low #35201e, horizon #54291a — subtle). Stars (random dots, alpha varies, few twinkle? static fine, or a handful drawn per-frame twinkle — keep static + a couple of brighter ones). Distant mountain silhouettes (two layered ridges, bluish dark #12142a / #0d0f1f) behind the volcano. Moon? A small pale disc high on the side with slight halo — tasteful, adds composition. Yes, small moon upper-left (or opposite the prevailing light). I'll place moon at ~ (0.18W, 0.2H). Foreground: maybe a dark foreground ground strip in front (slightly below camera) — the terrain ground already handles it. Ash render: smoke sprites drawn with `globalAlpha`, normal composite, colored dark → they'll darken sky plume. During night scene, ash plume should be dark charcoal with lit underside? Simpler: soft gray-brown puffs alpha ~0.18 each, hundreds overlap → convincing plume. Also at night a plume lit from below by crater glow: draw two sprite types — dark ash sprite and an ember-lit variant near crater mouth (first ~0.5s of life use warm-lit sprite tinted orange, additive). Nice detail: young smoke particles get a warm-lit version that fades. I'll do: smoke has `age`; if age < 0.8, additionally draw small warm sprite with alpha (1-age/0.8)*0.3 using lighter — only for young ones, cheap. Emission scheduling: use fractional accumulators: ``` eruptEmitAcc += eruptRate*dt; while(acc>1){spawnLava(); acc--} smokeAcc += smokeRate*dt; ... ``` Pool: arrays of objects with `active` flag + free list (stack of indices). activeCount tracked. Spawning pops from free stack; killing pushes back. Simple. Also lava that flies off-screen sides: kill when x<−50 or x>W+50 or y>H+50 (below ground can't happen since terrain covers full width? Terrain covers full width — ground at bottom everywhere, so landing catches everything). Wind can push lava past edges — kill if outside. Sliding physics tuning: slope from heightmap in px/px; typical slope ~ (groundY-peakY)/(halfWidth) ≈ 0.4H/0.34W... e.g., H=800: rise 320 over run 400 → slope ~0.8 near mid. slideAccel = slope*900*dt, friction: v *= exp(-1.6*dt) roughly `v *= 1/(1+1.8*dt)`. Terminal-ish speed fine. Settle when |v|<8 and |slope|<0.12 → still. While sliding heat -= 0.25*dt (≈4s visible creep). Still: heat -= 0.35*dt. Air: heat -= 0.06*dt. Plus lifetime cap 20s hard. Bounce: on landing if vy>140 && heat>0.5: vy=-vy*0.22, vx*=0.6, stay airborne; else land. Maybe spawn spark on hard impact: if vy>260 and pool room: spawn 1–2 with random small v. Keep. G (gravity) ~ 340 px/s² scaled with H? Scale physics by canvas height: G = H*0.45? For H≈900 → 400. I'll compute from H so it feels consistent: G = Math.max(280, H*0.5). Velocities similarly scale by H/900 factor. Simpler: define `S = H/900` scale factor used in spawn velocities and gravity. Do that. FPS measurement: ema of 1/dt; update DOM every 300ms. Particle count: count active each frame (we track activeCount already via pool ops) — display it. Pressure build rate: full in ~9–14s: `pressure += dt*(0.085 + 0.02*sin(t*0.7))`; add tremor near >0.7 (screen micro-shake). Eruption duration D = 2.2 + power*3.3; pressure decreases dt/D... but pressure should reset and rebuild — during eruption drain to 0. Also eruption intensity envelope: `env = sin(pi * progress)`? Nicer: intensity = smooth pulse: start strong, decay: `env = Math.pow(1-progress, 0.7)` with initial ramp 0.15s. Use env for emission rate and shake. Power slider (0.2–2, default 1): scales launch speed, emission rate, duration. Wind slider (-10..10, default 2): windAbs/10 → windX = v*10 px/s for ash... tune: ash vx target = wind*18*S. Lava air drag toward wind: vx += (wind*8*S - vx)*0.12*dt... simpler vx += wind*6*S*dt. Wind streaks speed = wind*30. Smoke emission: base trickle rate 3/s; during eruption 30*power*env /s; post-eruption decay over few seconds. Track `activity` variable (0..1) that jumps to 1 at eruption start and decays: activity = max(env, activity - dt*0.25). smokeRate = 4 + 60*activity*power... plus crater pool glow follows activity too. Also lava fountain should spread: vx = (rand-.5)*(140+power*160)*S with slight bias outward both sides; vy = -(300 + rand*260*power)*S... need arcs to reach slopes not just straight up — crater dip means particles rise and fall back into crater? Funnel: particles spawn slightly above crater mouth at varying x within crater width; those near edges may hit inner crater walls and slide into the crater — actually that's realistic (lava lake). Collision handles it: they land on crater floor and slide down-slope... crater interior slopes down toward center — sliding logic follows terrain gradient so they'd pool at crater bottom. That could trap all lava! Crater floor is below rim; particles landing inside will settle in the crater and die there with a glow — actually a glowing lava lake in the crater is gorgeous and realistic. But too many trapped makes few land on slopes. Launch velocities mostly exceed rim height, and spawn positions + outward vx bias will carry most over the rim. I'll bias vx slightly by sign of x offset from center. Also make crater shallow-ish. Ash rises from crater: spawn at crater mouth, initial vy = -(40+..)*S, buoyant rise slows... ash should rise above rim and billow upward — since crater mouth is at dip, ash must climb out of funnel: give strong initial rise vy=-(120+rand*80) and buoyancy: vy += (-50*S - vy)*0.8*dt (approach terminal rise). Turbulence: vx += (wind*windAsh + sin(t*1.3+seed*7)*14 - vx)*0.9*dt. Size grows: r += growth*dt. Alpha: fade in first 0.3s, fade out last 30% of life. Life 5–9s. Smoke sprite: radial gradient rgba(70,66,72,0.5)→transparent, plus lighter variant for young lit ones rgba(255,140,60,...). Composite batching plan per frame: 1. drawImage(bgCache) 2. wind streaks (light strokes) 3. drawImage(terrainCache) 4. crater glow: additive radial (pre-rendered glow sprite scaled) with alpha = f(activity, pressure), flicker. 5. moon halo already in bg. 6. Lava hot: `lighter`, loop draw. 7. Lava rock-stage: normal, loop. 8. Smoke: normal, loop (smoke above lava — plume occludes fountain partially, good). 9. Foreground vignette? subtle bottom vignette in bg cache top layer? A slight vignette adds mood — draw in bg cache corners. Keep subtle. Shake: ctx.translate(shakeX, shakeY) around steps 1–8; shake = tremor + eruption * power * random. DOM structure: - canvas#scene - header .brand: title + subtitle - .readout (top-left below brand or top-right): FPS, particles, status - .gauge (right side): pressure bar + % - .controls (bottom-left): sliders with labels + value spans - small hint text bottom-right maybe "drag sliders live". Fonts: Google Fonts link (IBM Plex Mono 400/600 + Instrument Serif italic). Accent color: lava amber #ffb15e / crimson #e8452c. Panel: rgba(10,10,14,0.55) blur backdrop, 1px border rgba(255,255,255,0.08). Slider styling: custom track (2px line) + round thumb with amber fill using accent-color? Custom webkit: I'll write -webkit-slider-thumb styles, plus Firefox ::-moz-range-thumb. Keep concise. Gauge: container height ~ 200px, 10px wide track, fill div height %. Ticks: 5 hairlines via absolutely positioned divs or background repeating-linear-gradient (functional). Percent label under, plus "PRESSURE" rotated. Pulse class when >0.8. Status text: DORMANT / TREMOR / ERUPTING with dot color. Let me now write the code carefully. I'll aim for clean, well-commented, ~600 lines. Key constants scaled on resize: ``` S = H/900 G = 430*S ``` Terrain function detail: ``` const cx = W*0.5; const halfW = Math.min(W*0.36, 520); const groundY = H*0.84; const peakH = Math.min(H*0.48, 430); const peakY = groundY - peakH; const craterHalf = Math.max(26, halfW*0.14); function heightAt(x){ let y; const d = Math.abs(x-cx); if(d >= halfW){ y = groundY; } else { const t = d/halfW; // concave cone flank: y = peakY + (groundY-peakY)*Math.pow(t,1.5)? pow(t,1.5) at t small rises steeply from peak... At t=0 (center) y=peakY. Slope near peak infinite for pow<1... pow(t,1.5): derivative 1.5 t^0.5 → 0 at peak (rounded peak) and steep near base? d/dt at t=1 =1.5 → steeper at base. Real cones are steeper near top. Use y = peakY + span * (t^0.8)? derivative of t^0.8 at 0 → infinite... For a volcano: straightish flanks slightly concave. Use t^1.15 maybe: at peak slope 0 (rounded), base slope >1. Hmm real stratovolcano: flanks ~30°, slightly concave (steeper up top). t^0.85 gives steeper near peak (since for small t, t^0.85 > t, meaning y further from peak... wait y = peakY + span*t^p; at small t with p<1, y rises quickly → steep near peak. Yes p<1 → concave, steep near peak. p=0.7. y = peakY + (groundY-peakY)*Math.pow(t,0.72); } // crater funnel if(d < craterHalf){ const ct = 1 - d/craterHalf; // 1 at center y += ct*ct * Math.min(34, peakH*0.12) * (something) // dip // funnel shape: smooth } // rim smoothness: blend is fine since crater dip is 0 at d=craterHalf. // noise: const n = noiseVal(x); // reduce noise near crater for clean mouth: const mask = Math.min(1, Math.max(0,(d - craterHalf) / (halfW*0.25))); y += n * mask; return y; } noiseVal(x) = sin(x*0.011)*4 + sin(x*0.023+1.7)*3 + sin(x*0.005)*6 — plus ground rolling far from volcano: if d>halfW add rolling hills sin(x*0.004)*10*sin(...)? Keep: n applies everywhere scaled. ``` Ground should be roughly flat for particles to rest; small ±8px noise ok. Also gentle randomness per load: phase offsets from Math.random at init so each load differs slightly. But heightAt must be deterministic during a session — store phases. Build terrain array: `terr = new Float32Array(W+2)` for x from -1..W. Index Math.round(x) clamped. Terrain cache rendering: path from (0,terr[0]) along points → down to bottom corners → fill. Then texture strokes: iterate x step 3: draw short vertical-ish streaks below surface with rgba lighten/darken random. Plus grass? No — dark volcanic sand: sparse lighter speckles. Plus pines: place ~14 pines at random x where slope gentle and d>halfW*0.75 or on far ground; draw simple triangles stacked, very dark #0a0806. Also draw faint warm reflection strip at crater interior: a soft glow at crater mouth in terrain cache? Glow is dynamic; skip in cache. Crater lava pool: dynamic — at crater floor, draw ellipse of bright lava with pulsing alpha tied to activity/pressure: fillStyle gradient? Just draw glow sprite at crater mouth center (cx, craterFloorY) with 'lighter', scale pulsing. This covers pool visual. Ember sparks from fountain: handled via same lava pool (they're just small heat=1 particles). Fine. Numbers: default cap 1600; eruption rate ~ power*420 /s while env high → bursts of ~800 live. Smoke cap separate ~ 900 (its own pool, count separately? Counter shows total). Particle amount slider scales BOTH caps: lavaCap = amount, smokeCap = amount*0.6. Default amount 1600 → smoke 960. OK. Pool implementation: ``` function makePool(cap, factory){ arr = []; free = stack indices 0..cap-1; active=0 } spawn: if(free.length) idx=free.pop() else steal? When full, optionally reuse oldest: for lava, if full, skip (or steal a settled particle). Stealing settled rocks keeps fountains alive — I'll steal: maintain a ring? Simpler: if free empty, don't spawn (sliders cap it; user sees count plateau). Acceptable and honest. kill(i): arr[i].active=false; free.push(i); activeCount-- ``` Iterating all cap entries each frame with active check: cap up to 3000+1800, two loops — fine. FPS with 4800 iterations + draws: fine on canvas. Draw lava: size varies: r = 3+heat*4 * S ... air particles slightly larger with glow. drawImage(sprite, x-r, y-r, 2r, 2r). alpha = clamp(heat*1.6, 0.15, 1) for hot; rock stage alpha = heat/0.12 * 0.8. Sprite stage selection: heat thresholds: >0.8 s0, >0.62 s1, >0.45 s2, >0.3 s3, >0.14 s4, else rock (normal composite dark dot). Rock drawing: fillStyle set once outside loop, per particle alpha — use globalAlpha per particle + fillRect small (cheaper than sprite): fillRect(x-1.5,y-1.5,3,3) with rgba(30,20,16, a). Batch: set fillStyle once; loop set globalAlpha & fillRect. Wait: dark rocks against dark terrain invisible — fine, they fade out anyway; visible mainly against sky. Good. Ash drawing: globalAlpha per particle, drawImage smoke sprite scaled to r. Dark plume at night should read against sky — use mid-gray rgba(120,110,115) tint sprite with low alpha, layered → plume reads. Also warm-lit young ones. Wind streaks: 50 particles {x,y,len,spd,depth}; speed = wind*36*(0.5+depth); x+=speed*dt; wrap; alpha = min(0.25, |wind|*0.028)*depth; draw as 1px lines with slight downward slope? Keep horizontal thin lines rgba(255,255,255,α). Only when |wind|>0.4. Now UI details & copy (English, since request is English): Brand: "STRATOVOLCANO" small caps mono + big italic serif "Mt. Kessler"? Make up a name: "Vulcania" ... I'll title it "OPEN-VENT VOLCANO — live simulation" hmm. Signature: name the volcano "Monte Aria". Title: italic serif "Monte Aria" + mono sub "STRATOVOLCANO · LIVE SIMULATION". Nice. Readout panel (top-right): - FPS: 60 - PARTICLES: 1247 - STATUS: ● DORMANT / TREMOR / ERUPTING - WIND: → 3.2 (direction arrow via character? no emoji — use a small inline SVG arrow or text like "E 3.2 m/s"). I'll show "WIND: ←/→ value" using CSS triangle? Simplest: text "E" / "W" based on sign, mono. Fine: `WIND E 4.0`. Gauge panel (right, below readout or left side?): Place gauge bottom-right vertical, controls bottom-left. Readout top-right. Brand top-left. Mobile: panels small enough; add media query stacking. Keep basic responsiveness. Controls: sliders with label row (name + value). - POWER: 0.2–2.0 step .05, default 1.0, shown "1.00×" - WIND: −10–10 step .5 default 2.5, shown "→ 2.5" - PARTICLES: 300–3000 step 50 default 1600 shown "1600" Implement slider input events updating state; wind change affects immediately. Pointer events on canvas? Not required. Maybe click crater to force... not required; skip to keep focus. Actually a small delight: clicking the canvas adds a "pressure surge"? Might confuse. Skip. Let me write the eruption state machine: ``` state: 'build' | 'erupt' pressure 0..1 eruptT (elapsed), eruptDur update: if build: pressure += dt*rate; if pressure>=1: startErupt() startErupt: eruptDur = 2.2+power*3.6; eruptT=0; state='erupt'; activity=1; shakeBurst if erupt: eruptT+=dt; prog=eruptT/eruptDur; env = prog<0.08? prog/0.08 : Math.pow(1-(prog-0.08)/0.92, 0.85); pressure = 1-prog (clamp); emission uses env; if prog>=1: state='build'; pressure=0; activity lingers activity = max(env, activity - dt*0.3) ``` Tremor: tremor = state==='erupt'? env : Math.max(0, pressure-0.72)/0.28 * 0.5; shakeAmp = tremor*4*S (+ initial burst). Emission rates: ``` lavaRate = (60 + 380*power) * env * S? rate in particles/s: up to ~440 at power 1. With eruptDur ~5.8s at power1 → 2500 spawned over eruption but pool 1600 with lifetimes... lifetimes: flight ~2.5s, slide ~4-6s → many still alive near end; pool full → skips. Fine, cap works. spawnLava: x = cx + (rand-.5)*craterHalf*1.2; y = craterMouthY - 4; ang: base up, spread: vx = (rand()-.5)*(90+240*power)*S + sign(x-cx)*30*S; vy = -(280 + rand()*(240+320*power))*S... at power 2: vy up to -(280+1800*?) let's compute: 240+320*2=880; vy up to -(280+880)*S... too much? S~1: vy ∈ -(280..1160)? At power 1: -(280..840)*S. With G=430S: apex = vy²/2G ≈ 840²/860 ≈ 820px — off screen top. Hmm H=900, crater at ~H*0.36≈324 from top... apex 820 above → y=-496 offscreen. Too strong. Reintroduce: max vy should send particles near top of screen: available rise ≈ 300px (crater y 324 → apex ~40). vy_max = sqrt(2*G*300) ≈ sqrt(2*430*300) ≈ 508. So vy = -(180 + rand()*(180+150*power))*S → power1: 180..510; power2: 180..660 (some exit top briefly — dramatic, fine, they come back down; particles above screen: don't kill them, keep updating, they fall back. Only kill x out of range. Good, leave y uncapped.) vx spread: (rand-.5)*(120+180*power)*S + outward bias 25S. Wind adds during flight. r (size): 2+rand*2.6 (*S for glow radius factor) heat=1; mode air; seed rand*6.28; slideV=0. ``` Smoke spawn: x = cx+(rand-.5)*craterHalf; y = mouthY-6; life = 4+rand*4; r0=(8+rand*14)*S; growth (6+rand*10)*S; vy=-(50+rand*70)*S; buoy target -(60+20*rand)*S... during high activity stronger: vy0 = -(60+rand*90)*S*(0.5+activity). vx initial = wind*10S. Smoke update: ``` age+=dt; if age>life kill. turb = sin(t*1.1+seed)* 18*S + sin(t*2.3+seed*3)*9*S vx += (wind*14*S*windExp? + turb - vx)*0.7*dt vy += (targetRise - vy)*0.8*dt where targetRise = -(30+40)*S * (1 - 0.5*age/life)?? buoyancy decays with age as cloud cools: targetRise = -(20+50*(1-age/life))*S r = r0 + growth*age... plus slight ease. alpha envelope: a = sin-ish: fadeIn = min(1, age/0.4); fadeOut = 1 - max(0,(age-life*0.65)/(life*0.35)); alpha = 0.16*fadeIn*fadeOut*? maybe up to 0.2. young glow: if age<0.9: warm overlay alpha (1-age/0.9)*0.35*activity-ish (just age-based). ``` Crater glow draw: ``` glow = 0.15 + pressure*0.5 + activity*0.9 (+flicker sin(t*13)*0.08) draw glowSprite (warm radial) at (cx, mouthY): width = craterHalf*6*..., alpha=glow*0.5, 'lighter' Also a vertical flare during eruption: second glow stretched tall (drawImage with big height, small width) alpha=activity*0.5. ``` Mouth Y: craterFloorY = heightAt(cx). Compute from terrain array: terr[round(cx)]. Lava landing inside crater: slide toward center then settle & die glowing → pool glow region gets extra brightness implicitly from particles. Kill conditions lava: heat<=0 or x<−60||x>W+60. Also y> H+80 safety. Slide code: ``` const xi = clamp(Math.round(p.x),1,W-1); const s = (terr[xi+1]-terr[xi-1])*0.5; // positive → down to right (screen y down) p.slideV += s*1400*S*dt; p.slideV *= Math.max(0, 1-2.2*dt); p.slideV = clamp(p.slideV, -260*S, 260*S); p.x += p.slideV*dt; p.y = terr[clamp(round(p.x))]-1; if(Math.abs(p.slideV)<9*S && Math.abs(s)<0.14){ p.mode=2 (still) } heat -= (0.16 + Math.abs(p.slideV)*0.0009)*dt*? Let's: sliding cools at 0.14/s + movement-dependent small; still cools 0.3/s → still lasts ~3.3s after settling. Also hard lifetime 16s. ``` Hmm heat from 1: air cools 0.05/s (flight ~2.5s → 0.87 remaining — lands bright orange ✓). Slide: land heat ~0.85; slide cooling 0.14/s → ~4s sliding bright→red ✓. Still 0.3/s → fades to rock. Total life ~10s. With rate 440/s and ~9s avg life → 3900 needed but cap 1600 → skips spawn late in eruption; visually eruption tapers because pool fills — acceptable, actually natural (fountain weakens as plateau fills). Slightly reduce rates: lavaRate = 40+320*power → power1: 360/s. OK. Actually, sliding particles going downhill off the cone onto flat ground spread out — nice: lava flows reach ground level. slope near base ~ derivative of pow(0.72) at t→1: span/halfW*0.72 → with span 380, halfW 420: 0.65 → decent. On flat ground s≈noise small → they settle. Bounce logic on landing: ``` if(p.vy > 150*S && p.heat>0.4 && rand()<0.5){ p.vy*=-0.2; p.vx*=0.5; p.y = terr-2; stays air } else { mode=1(slide); p.slideV = p.vx*0.4; if(|vy|>240S && poolHasRoom) spawn 1-2 sparks } ``` Sparks: spawn with vx=±(20..120)S, vy=-(40..160)S, heat=0.9, small r. Guard: only if activeCount < cap*0.9 (avoid cascade). Also while airborne: mild wind coupling: p.vx += (wind*6*S - 0)*dt*?? simple: p.vx += wind*7*S*dt. And slight drag: vx*=1-0.05*dt? skip drag. Frame loop: ``` function frame(ts){ dt = clamp((ts-last)/1000, 0, 0.033); last=ts; t+=dt; update(dt); render(); raf } ``` Use requestAnimationFrame; physics per-frame with dt (variable but clamped — fine for this). FPS: fps = fps*0.92 + (1/dtRaw)*0.08 using raw dt before clamp. Update DOM readouts every 0.25s: fps rounded, activeCount (lava.active + smoke.active), wind text, status text+class, gauge fill height + percent, gauge pulse class when pressure>0.8. Wind text: `wind>=0? '→ E':'← W'` plus value. Using arrows "→" is a text glyph, fine (not emoji). Gauge markup: ```
STRATOVOLCANO · LIVE SIMULATION
STRATOVOLCANO — LIVE SIMULATION
`. Readout top-right: ```css .readout{top:20px;right:20px;min-width:190px} .row{display:flex;justify-content:space-between;gap:18px;font-size:11px;line-height:1.9} .k{color:var(--mut);letter-spacing:.14em;font-size:9.5px;padding-top:1px} .v{font-weight:600} .dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;background:#6b7564;vertical-align:0} .dot.tremor{background:var(--amber);animation:blink .5s infinite alternate? use 0.6s} .dot.erupt{background:var(--ember);animation:blink .3s infinite} @keyframes blink{from{opacity:1}to{opacity:.25}} ``` Gauge bottom-right: ```css .gaugeWrap{right:20px;bottom:20px;display:flex;gap:10px;align-items:flex-end;padding:14px} .gauge{position:relative;width:13px;height:168px;border:1px solid var(--line);border-radius:6px;background:rgba(255,255,255,.05);overflow:hidden} .gauge::after{content:'';position:absolute;inset:0;background:linear-gradient(rgba(255,255,255,.16) 1px,transparent 1px);background-size:100% 25%;pointer-events:none} #gFill{position:absolute;left:0;right:0;bottom:0;height:0%;background:var(--amber);transition:height .18s linear, background .3s} #gFill.hot{background:var(--ember);animation:blink .5s infinite} .gaugeSide{display:flex;flex-direction:column;justify-content:space-between;height:168px;align-items:center} .gLabel{writing-mode:vertical-rl;transform:rotate(180deg);font-size:9px;letter-spacing:.3em;color:var(--mut)} #gVal{font-size:11px;font-weight:600;color:var(--amber)} ``` Hmm gaugeSide: label rotated + value. order: value at bottom near bar bottom. fine. Controls bottom-left: ```css .controls{left:20px;bottom:20px;width:250px;display:flex;flex-direction:column;gap:13px} .ctl-head{display:flex;justify-content:space-between;font-size:9.5px;letter-spacing:.18em;color:var(--mut);margin-bottom:7px} .ctl-head b{color:var(--ink);font-weight:600;letter-spacing:0} .sl{position:relative;height:14px;border-radius:7px;background:rgba(255,255,255,.13)} .sl input{position:absolute;inset:0;width:100%;height:100%;-webkit-appearance:none;appearance:none;background:transparent;cursor:ew-resize;outline:none} .sl input::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;border-radius:50%;background:#f6e8d4;border:2px solid #1a120c;box-shadow:0 0 0 1px rgba(255,177,94,.5)} .sl input::-moz-range-thumb{width:11px;height:11px;border-radius:50%;background:#f6e8d4;border:2px solid #1a120c} .hint{font-size:9px;color:var(--mut);letter-spacing:.08em;margin-top:2px;line-height:1.5} ``` Media query: max-width 720px → scale panels: brand h1 26px; controls width 200px; gauge height 120; readout smaller. Basic. Sliders config: - power: min 0.2 max 2 step 0.05 value 1 → display (v).toFixed(2)+'×' - wind: min -10 max 10 step 0.5 value 2.5 → display (v>=0?'→ ':'← ')+abs.toFixed(1) - amount: min 300 max 3000 step 50 value 1600 → display v JS wiring: on input → update state, fillSlider, label. fillSlider sets el.parentNode.style.background = gradient. For wind, fill from center? Nice touch: for wind show fill from middle to thumb: compute p; if v>=0: gradient left 50% → transparent to 50%, amber 50%→p%. Else p%→50%. Implement: ```js function paintTrack(el){ const min=+el.min, max=+el.max, v=+el.value; const p = (v-min)/(max-min)*100; const a='rgba(255,177,94,.8)', b='rgba(255,255,255,.12)'; if(el.dataset.mid){ // bidirectional const mid=50; const g = v>=mid? `linear-gradient(90deg, ${b} 0 ${mid}%, ${a} ${mid}% ${p}%, ${b} ${p}% 100%)` : `linear-gradient(90deg, ${b} 0 ${p}%, ${a} ${p}% ${mid}%, ${b} ${mid}% 100%)`; el.parentNode.style.background=g; } else { el.parentNode.style.background=`linear-gradient(90deg, ${a} 0 ${p}%, ${b} ${p}% 100%)`; } } ``` Now write full JS in order: 1. helpers (clamp, rnd) 2. canvas & resize 3. terrain gen 4. sprites 5. pools 6. bg/tr caches 7. emission & updates 8. render 9. loop 10. UI wiring 11. init Potential bug areas: drawImage of sprite canvases before defined — order matters. Also ensure ctx.imageSmoothingEnabled default true (good for sprites). Let me now also double-check eruption pacing: build rate 0.085–0.115/s → 8.7–11.8s from 0. From 0.3 head start: ~6-8s. Good. EruptDur power1: 6.1s; power2: 9.9s; power 0.2: 3.06s. lavaRate power1 env1: 380/s * ~5s avg env → ~1400 spawned; pool 1600 with overlap — good, most spawn. Now smoke rate: 3 + 70*act → up to 73/s during eruption; life ~6s → ~440 smoke. Cap smoke = 880 at default. ok. Counter could show "1 247" formatting with thin space? Just toLocaleString? Use plain number with space thousands: n.toLocaleString('en'). fine. Also I want the wind slider to have visible immediate effect on plume lean — 15*S factor: wind 10 → 150 px/s target — strong lean ✓. Title flair: In brand panel, also show current vent temperature? gimmick — skip. Let me now write out everything concretely, watching for correctness. Also remember: no emoji anywhere; arrows ←/→ are punctuation, fine. For FPS stability: rawDt for fps calc: dtRaw. Write code: ```js const cv=document.getElementById('scene'), ctx=cv.getContext('2d'); const ui=document.getElementById('ui'); const $=id=>document.getElementById(id); const clamp=(v,a,b)=>vb?b:v; const rnd=Math.random; let W=innerWidth,H=innerHeight,DPR=1,S=1,G=420; let terr=null, cx=0, halfW=0, groundY=0, peakY=0, craterHalf=0, mouthY=0, mouthDepth=40; let bg=null, tr=null; const nph=[rnd()*6.28,rnd()*6.28,rnd()*6.28]; function terrNoise(x){ return Math.sin(x*0.0103+nph[0])*5.2 + Math.sin(x*0.0271+nph[1])*2.4 + Math.sin(x*0.0047+nph[2])*7.5; } function heightAt(x){ const d=Math.abs(x-cx); let y; if(d>=halfW) y=groundY; else y=peakY+(groundY-peakY)*Math.pow(d/halfW,0.72); if(d