### PARTIAL OUTPUT (time limit hit; the model was still generating) ### === thinking so far === Let me design this volcano simulation carefully. Single HTML file, no external libraries, canvas rendering, everything inline. **Core architecture:** - Fixed timestep physics (or semi-fixed) with particle pooling for performance. - Typed arrays for particle data (SoA layout) so 1000+ (aim much higher) particles stay smooth. - Two particle systems: lava (glowing, physics-driven with collision against terrain heightmap) and ash/smoke (buoyant, wind-drifted, rises then fades). **Terrain:** Generate a volcano cone heightmap procedurally — a truncated cone with a crater dip at the top, plus noise for natural irregularity. Store as an array of ground heights per pixel column so particle–terrain collision is O(1) lookup. Lava particles collide with the heightmap, bounce/slide downhill along the slope (sample gradient), and when they settle, they can slightly "build up" the surface? That's a nice touch but risks runaway growth — instead, cooled lava can briefly leave a fading "rock stain" drawn to an offscreen terrain canvas (decals that fade very slowly). Actually simpler: keep a persistent offscreen "lava crust" layer that slowly fades — draw landing splats there, fade by drawing translucent dark terrain color over it periodically. That gives the volcano dark streaks down its slopes from prior eruptions — a beautiful signature detail. But fading requires care to not erase terrain texture. I'll draw the terrain with texture into the base, then have a separate "crust" offscreen canvas where lava splats accumulate, faded each frame with low-alpha destination-out... Actually fading via `globalCompositeOperation='destination-out'` with low alpha fill removes crust over time. Good. Terrain drawing: I'll pre-render the volcano onto an offscreen canvas with layered shading — sky gradient handled on main canvas, volcano cone with ridged texture: draw base fill, then darker striations following the slope lines (radial streaks from crater), rim highlight, crater inner shadow, some rock noise speckles, trees/ground at bottom? Keep it stylized: dark volcanic basalt tones with subtle warm rim light near crater. **Volcano shape:** width of canvas ~ responsive. Cone: apex at crater, two slopes going down to a ground plain at the bottom. Crater = a dip between two rim peaks (stratovolcano profile). Heightmap array `groundY[x]`. **Physics for lava particles:** - Arrays: x, y, vx, vy, life (or temperature t in [0,1]), size, state (flying/settled), settledTimer. - Gravity ~ 900 px/s². Launch: position at crater mouth, velocity angle spread near vertical ± from power slider, speed proportional to power slider plus randomness. Occasionally bigger "bombs" (larger particles, stronger glow). - Collision: when y >= groundY[round(x)] → land. On landing, compute slope from heightmap: g = groundY[x+1] - groundY[x-1]. If particle speed below threshold or terrain slope shallow, mark settled: then slide downhill — move x toward downhill direction at speed proportional to slope steepness (and remaining heat — hotter slides more, viscous). As it slides, it decelerates as it cools; when temperature hits 0, freeze & start fading. While sliding downhill, also add small bounce-stop randomness. Also hot lava landing on slope should melt/flow: emit occasional tiny ember spawn? Keep perf simple. - Temperature lifecycle: T starts 1, decays over ~4–8 s (faster when settled/exposed). Color map: T=1 → white-yellow core, 0.8 orange, 0.5 deep orange-red, 0.25 dark red, 0 → dark rock gray-brown then fade out over last second. Also emit glow: additive blending (`globalCompositeOperation='lighter'`) for hot particles — with many particles, draw with small rects or circles. For 1000+ particles, use `fillRect` for tiny ones, arcs only for large bombs. Actually radial gradients per particle per frame is too slow for thousands. Use `lighter` composite with plain fillRect squares... wait — plain squares as particles are exactly what I find cheap. I need nicer shapes but perf-capped. Compromise: lava particles drawn as circles via pre-rendered sprite canvases! Pre-render a small set (e.g., 12 temperature bands × 2 sizes) of radial-gradient sprite canvases once, then `drawImage` per particle — very fast (drawImage of small canvas is quick), and looks like glowing soft blobs with hot core. That solves both perf and beauty. For settled particles on terrain, they're static — I can stamp them into the crust offscreen canvas when they settle and stop drawing them actively... but they still cool over lifetime (fade to rock). If settled, stamp periodically? Better: settled particles continue as active drawn sprites but cheap; when they freeze, stamp final rock decal to crust canvas and free the slot immediately. That keeps active count low. But requirement says counter shows active particle count — settled-but-cooling particles still count as active until freed. Fine, but to keep thousands, stamp early and free. Plan: settled lava slides while hot; each frame with probability, stamp a small trail splat into "glow crust" layer? Hmm, two crust layers get complex. Simpler: while sliding hot, particle draws sprite (moved). When it stops/freezes, stamp its current look (rock-colored splat with slight residual glow streak already faded) onto crust canvas at its position, then free. Crust canvas gets periodic slight fade so old flows slowly dim — or actually keep crust semi-permanent (dark rock streaks remain, that's realistic — lava flows leave rock). I'll let crust persist but very slowly fade over ~60s so it doesn't saturate, capped by destination-out fade of tiny alpha. Also stamp hot splats with orange that will fade as crust darkens? The crust stamps can be dark rock color directly with slight ember tone; ember tone fades. I'll do crust as two-stage: stamp rock-colored splat; additionally an additive glow drawn only by live particles. **Ash cloud:** separate smaller pool (e.g., up to 400–700). Spawned at crater during eruption and while pressure high/venting. Physics: upward initial velocity, buoyancy, wind force from slider (wind slider −100..+100), turbulence (per-particle noise via sin(time+seed)), growth over life, alpha fade, color from dark gray-brown to light gray. Drawn as pre-rendered soft sprites with `source-over` (behind lava glow? ash in front of sky but behind lava? Ash above crater; lava arcs can overlap). Draw ash first (normal blending), then lava with lighter blending on top. Also ash tinted slightly orange near crater when hot (emit some "ember" ash that starts glowing). Simplify: ash particle temperature decays quickly at start so those near vent glow. **Pressure meter:** - Builds over time; rate maybe varies (adds slight randomness so eruption timing feels organic). Displayed as a vertical gauge beside the volcano — a classic branded design opportunity: a "VULCANO-VU X-1" style instrument panel? Keep restrained: a vertical brass/steel gauge with needle? A needle gauge is a nice signature. I'll do a vertical tube gauge on the left with a glowing mercury-style fill plus tick marks, and when near max, the tube trembles and a warning glow pulses. When pressure ≥ 100 → ERUPTION: pressure dumps rapidly during eruption (drains to 0 over a few seconds while erupting), spawning bursts. Sequence: pre-eruption rumble (screen shake grows, crater glow intensifies, smoke wisps increase), then main burst for ~2–4s (spawn rate high), then pressure drains, waning puffs, back to building. Between eruptions the crater may gently smoke (small ash trickle). Actually spec: "pressure builds over time; when maxes out, erupts, then pressure resets and builds again." So: pressure 0→100 over, say, 12–20 s (randomized slightly). On max: eruption event lasting ~3–5 s during which lava spawns; pressure drops quickly to 0 during/after eruption. Also allow manual trigger? Not requested — keep auto only, but maybe clicking the volcano triggers? Not requested; sliders only. I'll add a "trigger eruption" on click of crater anyway? Spec says three sliders; extra interactivity could be fine but I'll keep it minimal — maybe click to trigger is harmless fun and doesn't conflict. Hmm, "No non-requested styles/controls" isn't a rule for interactions, but I'll add it subtly: clicking the volcano cone triggers an early eruption (justifiable — feels alive). I'll include it; it adds responsiveness without UI clutter. I'll mention it in a small caption. **Sliders:** 1. Eruption power (launch speed + spread + amount of bombs): range e.g. 20–100. 2. Wind strength & direction: −100..+100 (affects ash drift strongly, lava arcs slightly, and crust trail direction, plus drifting embers). 3. Particle amount: scales spawn rate per eruption / concurrent cap, e.g. 200–2000 target active lava. Map slider to spawn-per-second during eruption and ash count. Custom slider styling: I'll build my own slider UI rather than default form controls? Native range inputs styled with `::-webkit-slider-thumb` etc. can look decent and are accessible + touch-friendly. I'll style them carefully (dark track, engraved thumb). Native range styled well is fine and robust. Keep DOM UI minimal: a bottom-left instrument strip with three sliders, a pressure gauge column, and the stats readout (particles + FPS) top-left as a monospace telemetry readout. Typography: use a characterful font via Google Fonts CDN? External CDN allowed. I'd like a distinctive font — maybe "Chakra Petch" or "IBM Plex Mono" for telemetry + a display font like "Bungee"? For a volcanic observatory vibe: heading could be a condensed display font. Let me choose: UI/display "Chakra Petch" (techy, fits instrument panel) and mono telemetry "IBM Plex Mono" or just Chakra Petch everywhere with tabular numbers. I'll load Google Fonts (allowed as CDN). Keep it restrained: off-white/paper text on near-black, one accent color (ember orange) for live values, hazard red only in warning state. **Layout/scene composition:** Full-viewport canvas. Scene: night-ish dusk sky (deep indigo to warm horizon? Avoid decorative gradient clichés — but a sky gradient is a natural phenomenon, not decoration; a dusk sky gradient is genuinely appropriate). Stars (small twinkling points). Moon? A low moon with glow could be nice but maybe skip to keep focus; a few stars suffice. Ground plain at bottom with subtle silhouette details (grass tufts? dark scrub silhouettes). Volcano centered slightly right or center. Pressure gauge overlaid left side as part of the "field instrument" HUD. HUD text top-left: PARTICLES / FPS with small bar meters. Bottom: slider panel styled as weathered instrument plate. **Signature moment:** the eruption itself — build-up: crater glow pulses, tremor shake, warning light on gauge; then a shockwave ring + muzzle flash, lava fountain with individual arcs, bombs trailing embers, ash column billowing with internal lightning flashes (occasionally draw a jagged lightning bolt inside the ash cloud — volcanic lightning! Great signature detail: small probability per frame during heavy ash, render a brief jagged polyline from within the ash plume, illuminating ash particles nearby via a temporary light multiplier). That's memorable and cheap to render. Also screen shake on eruption + camera micro-shake continuous during eruption. Lightning implementation: during eruption while ashCount > threshold, with prob ~0.02/frame spawn a bolt: pick a point in the ash cloud region, generate jagged segments downward/toward plume edge, life ~0.15s, drawn with lighter blending, white-violet. Also flash the whole scene slightly (a translucent lighter overlay). **Performance plan for ≥1000 particles:** - SoA Float32Arrays, capacity ~ 4000 lava + 900 ash. - Free-list/stack allocation: keep `alive` count and swap-with-last removal to avoid compaction. With SoA, swap copying many fields is a bit verbose but fine; or use compacting loop each frame (iterate alive count, write alive ones to compacted arrays — copying ~8 arrays of length N each frame is fine, N up to few thousand). Simplest: "swap to end" removal: when killing particle i (i < n-1), copy fields from n-1 to i, n--. With ~10 Float32Arrays that's fine. - Spawn during eruption: rate = particles slider mapped, e.g. up to 1200/s bursts. With eruption ~4s, could hit 2000+ concurrent if lifetime ~8s; cap by capacity & slider: I'll compute spawn rate = sliderValue * factor and also drop oldest if exceeding capacity (recycle oldest settled first — reuse oldest index when full). - Rendering: pre-rendered sprites via drawImage. Lava: draw all hot particles into the scene with `lighter`. Batch: set composite once, loop drawImage. Sorting not needed. - Terrain collision: per particle check ground: lookup groundY[idx] with x clamped. Use particle radius ~2-5 → simple point vs heightmap ok. - FPS: EMA of frame dt; display integer. Also show "render budget"? Just particles + fps. - devicePixelRatio: cap at 1.5 for perf; canvas resize handler. - Avoid per-frame allocations: reuse arrays, pre-render sprites. Also stars twinkle: draw once to offscreen? Stars twinkle needs per-frame alpha — keep ~120 stars, draw as tiny fillRect with alpha via fillStyle change — 120 fillStyle changes fine, or precompute twinkle phases and draw with globalAlpha per star. OK. Ground/scrub silhouettes: pre-render into terrain canvas. **Terrain generation detail:** Canvas size W×H (device px, but store heights in CSS px space for logic; I'll just use canvas pixel space with dpr scale via ctx.setTransform, logic in CSS pixels). Simplify: set canvas width = clientWidth, height = clientHeight, dpr = min(devicePixelRatio, 1.5), canvas.width = w*dpr, ctx.scale(dpr,dpr), all logic in CSS px (w,h variables). groundY array length = w (int per column). Volcano: center cx = w*0.5? Put slightly off-center: cx = w*0.55. baseY = h*0.86 (ground plain). coneHeight = h*0.62. Cone profile: rim height at ±R0 from crater center; crater dip between rims; slopes: outer slope from rim down to base with slight concave (steeper at top? stratovolcano: steep upper, concave lower — slope angle decreases near base). Build function: for x in 0..w: - dx = (x - cx) / scale (scale ~ w*0.16 radius at rim). - Base cone: y = baseY - coneHeight * profile(|dx|) where profile uses a smooth concave curve: e.g. height fraction = clamp(1 - pow(|dx|/Rbase,1.6)...). Let me define: rim half-width Rr = w*0.10 (top rim radius). Base radius Rb = w*0.30. For |dx| >= Rb → ground plain. Between: t = (|dx| - Rr)/(Rb - Rr) clamped 0..1; h = lerp(coneH, 0, ease) with ease = smooth pow curve like pow(t, 1.35)? That yields slope steep near top, flattening at bottom. Plus small noise (sum of sines) for rockiness: multiply noise small. - Crater: for |dx| < Rr: dip down: crater floor at coneH*0.86 with parabolic dip: inside crater, height = coneH*0.86 + something... define craterCenter cx; crater inner profile: for |dx|Rr), then walk down: from point (x, groundY[x]) extend line downward along local slope for random length with 1px width, alpha 0.15 dark. Also lighter ridge highlights on the other side. - Noise speckles: random small dots. - Ground plain: fill from baseY to bottom with dark soil gradient + horizon line; scatter grass/rock silhouettes: small tufts (2–3 px arcs) and rocks near bottom edges; maybe a couple of foreground conifer silhouettes at edges for scale (drawn dark). Keep subtle. Crust canvas: same size, transparent; stamps of settled rock splats; fade slowly. **Pressure gauge design:** vertical instrument at left: dark plate, brass ticks, a glass tube with fill that glows ember; numeric "PRESSURE" label vertical; small pulsing warning lamp when >85% ("EVACUATE" blink? cute: small lamp + text "HAZARD"). Needle-style might be nicer: a semicircular dial gauge "V" with needle sweeping 0–100 — draws attention. I'll do a round dial gauge drawn on canvas (part of HUD layer, drawn in canvas each frame): circle dial with ticks, needle with slight overshoot & jitter near max, red arc zone, glass glint. That's charming and authored. Position: top-left area or bottom-right? Put dial top-right? Stats readout top-left. Panel with sliders bottom-left. Dial: top-right corner area over sky. Good. Needle physics: needleAngle eases toward pressure with spring, jitter amplitude grows with pressure (vibration). When eruption triggers: needle snaps to drain. Also during buildup, small crater smoke wisps trickle (ash spawn rate = f(pressure) small) — foreshadowing. Eruption sequence timing: - phase BUILD: pressure += rate*dt; rate varies with a slow sine + noise; also occasional "creaks": tiny shake. - At 100 → phase ERUPT (duration D = 3.5–5s random): during erupt, pressure drains linearly to 0 over D (so "resets and builds again" after). Spawn lava at high rate; spawn ash heavy; shake amplitude high; initial shockwave at t=0; muzzle flash first 0.4s. - After D → phase BUILD again (pressure already ~0). - Also mid-erupt bursts: spawn rate modulated by sin bursts (fountaining pulses ~1.5Hz) — looks organic. Manual click trigger: if phase BUILD and click within cone region → set pressure to 100 quickly? Or start eruption: ramp pressure fast over 0.8s then erupt. I'll do: clicking anywhere on volcano triggers immediate "forced eruption": drain pressure to 0 quickly while erupting with reduced duration if pressure low? Simpler: clicking crater region triggers eruption regardless of pressure (pressure drains to 0). I'll implement: on click in cone → if not erupting, begin eruption (duration scaled by remaining pressure, min 1.5s). Mention in caption "click the volcano to trigger". **Lava spawn:** - From crater mouth (cx, mouthY). vx = wind*windLavaFactor + rand spread; vy = -(speed) where speed = base*(0.6+power) etc. Angles: mostly within ±35° of vertical, weighted near center; some "bombs" (5–8%) bigger radius (4–6px vs 2–3.5), higher speed, longer life. - Power slider: 0..100 → launch speed 250–900 px/s, spread narrows slightly with power (focused jet at high power). Particle amount slider: spawnRate = lerp(150, 1400, amt) during eruption peak (with fountain modulation), plus cap. - Cap by capacity 4000; if full, recycle oldest flying (reuse index 0 slot style — with swap-remove, oldest is index 0 since spawns appended at end; fine, reuse index 0). **Lava update:** - vy += G*dt; x += vx*dt; y += vy*dt; vx += wind*dt*windLavaCoef (small, maybe wind*3 px/s² for light bombs? keep small: wind*8). - Temp: T -= coolRate*dt; coolRate higher when small & when settled & faster when temp high? Simple: T -= dt/(lifeSeconds), lifeSeconds = 5–9 (random per particle; bombs longer). - Ground collision when y >= groundY[ix] (use floor(x)). On hit: - Compute slope s = (groundY[ix+2]-groundY[ix-2])/4 (pixels per px). - Bounce: vy = -vy*0.25 - |vx|*|s|*... simple: vy *= -0.28; vx *= 0.6; also deflect along downhill: vx += -sign(s)*|vy|*? Keep simple: on bounce, add vx -= s*80 (push downhill since s>0 means ground rises with x → downhill is -x). Actually slope sign: groundY larger = lower on screen. groundY increases downward. If ground rises to the right, groundY decreases with x → s = d(groundY)/dx < 0 means uphill to right → downhill is left → slide direction = -sign(s)? Let me define slope = (groundY[ix+1]-groundY[ix-1])/2. If slope > 0, ground lower to the right → downhill is +x. Downhill direction = sign(slope). So on bounce: vx += sign(slope)*impact*0.3. - If |vy| after bounce < 40 and |vx| < 20 → state = SETTLED (slides). Else remain flying with fewer collisions (also collide when moving up? point below ground check handles). - Actually simpler: on any ground contact: set state settled, but if impact speed high and particle hot, chance to bounce (bombs bounce once). I'll do: if vy > 250 and T > 0.55 and rand < 0.5 → bounce (vy*=-0.3, vx*=0.5+downhill push) else settle. - Settled update: - slideSpeed = |slope| * slideCoef * T (viscous: hot flows). Cap. - x += sign(slope) * slideSpeed * dt * ... plus small downhill creep always if slope steep: vel = sign(slope)*maxSlide*|slope|*T... define slideSpeed = T * 60 * (|slope| clamp 0..1.2). vx smoothing. - When sliding, y snaps to groundY (stick to surface): y = groundY[ix] (minus small offset so sprite sits on surface). - If slope ~0 (flat ground) or T < 0.35 (viscosity freezes) → state FROZEN: stamp splat onto crust canvas (size by radius, color dark rock with residual tint), free particle (recycle) — this keeps counts down. But "active particle count" then drops — fine, counter reflects active (moving/glowing) particles; frozen rock is scenery. Hmm, the requirement says "active particle count" — frozen removal is legit. But maybe nicer to keep them counted briefly while fading? No — stamp & free is better for perf. Counter shows active (in-flight + sliding). - While sliding and hot, occasionally stamp trail dots onto crust? Trail stamping every few frames per sliding particle could be heavy with many sliding. Do: when sliding, with prob per frame ~ (dt*8), stamp tiny dim splat. Cap crust operations — fine, it's just a few drawImage/fillRect on offscreen. Actually stamps: fillRect with rgba dark red-brown, cheap. - Temp affects sprite band: band = floor(T*11). Sprites pre-rendered for bands 0..11 with radius 16 (draw scaled by particle size). Two sprite sets: "glow" (lighter blending) maybe same sprite works for both since lighter uses alpha; draw same sprite with lighter for hot, but cooled rock particles should NOT use lighter (they'd be invisible on dark? dark rock with lighter blending ~ nothing visible). For cooling particles (T < 0.5) use source-over with dark-red→rock sprite. So two sprite sets: hotSet (bands T>0.45) drawn lighter, coldSet (T<=0.45... boundary) drawn normal. Simpler: single sprite per band containing both bright core and dark rim? With lighter blending, dark rim adds ~nothing, and bright core glows — actually that's elegant: one sprite set, drawn always with `lighter` while T>0.25 (glow fades as sprite darkens), then switch to source-over for the last rock phase. Hmm but on bright sky? Sky is dark dusk — lighter works. Let me just do: draw with lighter for T > 0.3, source-over sprite (same image) for T ≤ 0.3 — the sprite at low bands is dark rock colored, drawn normal blends over terrain fine. OK single sprite array of 12 bands. Sprite creation: for band b (0..11), t = b/11. Color: - t≥0.75: core near-white #fff7d0 → mid #ffd23e... let's define palette stops: 1.0: rgb(255, 244, 200) core, glow rgba(255,210,80) 0.85: #ffd76a / #ff9d1c 0.6: #ff7a1c 0.45: #e8541c 0.3: #b33318 0.15: #7a3b30 (dark crust, slight ember) 0.0: #4a4038 rock gray-brown Sprite: radial gradient: center color, mid, outer transparent-to-dark. For lighter blending, sprite should have bright center with soft falloff: stops: 0 → coreColor alpha 1, 0.4 → midColor alpha 0.85, 0.75 → outerColor alpha 0.25, 1 → alpha 0. For dark bands (t<0.3), radial: 0 → rockColor alpha 0.9, 0.6 → rgba(rock,0.5), 1 → 0 (soft blob). Radius 24 px sprite (so scaled draw covers particle radius 2–7 → drawImage with dw = r*4? sprite radius 24, particle visual radius = r*2.2 maybe). I'll draw sprite centered with size = particleSize * 6 (sprite diameter). Tune later; keep sprite 32px, scale factor s = r*3. Muzzle flash: big radial sprite at crater for first 0.3s of eruption. Shockwave: expanding ring (stroke arc, alpha fade, slight distortion) + dust kicked at base? Just ring + camera shake punch. **Ash particles:** - Fields: x,y,vx,vy,age,life,size,seed,heat. - Spawn at crater mouth with upward velocities (vy −(30..120) scaled by power), vx small + wind. During eruption spawn rate ~ amountSlider * 0.5. - Update: vy += (buoyGrav - buoy)dt: buoyancy: vy += (−20 − heat*60)*dt (rise when hot) + gravity 60*dt → net: early rise, later sink slowly. Turbulence: vx += wind*windAsh*dt*2 + sin(time*1.7+seed)*20*dt. Drag: vx *= pow(dragK,dt)... use vx *= exp(-dt*0.8) style via vx *= (1 - 0.8*dt) approx. Clamp. - Size grows: size += growth*dt (age-based: r = r0 + k*age). Life 4–8s. Alpha = fadeIn*fadeOut * (1 - sizeFactor dilution) — as it grows, alpha lowers (smoke dissipates). - Color: from heat: hot → dark with warm ember tint at edges; cool → gray #9a9aa2 tinted by ambient. Drawn with pre-rendered soft smoke sprite (white soft blob) tinted via globalAlpha & maybe two sprite variants (dark & light) cross-faded by age: simpler: single light-gray sprite, and set globalAlpha; also draw a "dark" underlay sprite for young ones? Just tint: create 2 sprites: darkSmoke (#2e2b33), lightSmoke (#8f8b96). Blend factor f = clamp(age/life*1.2). Draw dark with alpha (1-f)*a then light with f*a — 2 drawImage per ash particle; with ≤700 ash that's 1400 draws, ok. - Heat: young ash near vent: additive ember glow: if heat>0 draw hot sprite lighter with alpha heat*0.5. heat decays 1.2s. - Wind slider affects ash strongly: vx += wind * 30 * dt plus constant drift; also plume leans. Also lava arcs bend slightly with wind. And crust splat drift: settled particles slide — wind also biases slide? Minor: add wind*0.5 to settled vx on flat ground? Skip. Also ash should render behind lava glow but in front of volcano: order: sky+stars → moon? → terrain → crust → ash → lightning → lava (lighter) → HUD (dial, stats are DOM; shockwave, flash, vent glow). Vent glow during buildup: radial glow sprite at crater mouth with alpha = pressure-based pulse (sin flutter), plus slight illumination of crater inner walls: draw warm gradient clipped? Simplify: draw glow sprite (lighter) of size ~ 120px scaled with pressure. Nice foreshadowing. Camera shake: translate ctx by shake offset (decay, eruption adds). Apply to world drawing, not HUD. HUD drawn in DOM (stats) and dial — dial I'll draw in canvas but after restoring transform (no shake). **Stats DOM:** small monospace-ish block top-left: "PARTICLES 1 284" and "FPS 60" plus tiny bars? Keep text + small activity bar for particle count vs cap. Updated every ~200ms (throttle DOM writes). Dial: drawn on canvas top-right (unshaken). Draw: dark disc, tick ring 0–100 (major ticks every 20), red zone arc 85–100, needle from center bottom pivot? Classic gauge: pivot at bottom center of dial, needle sweeps ±80°. Center label "PRESSURE ×10² kPa"? Keep it playful: "CHAMBER PRESSURE" small text under dial, numeric readout inside dial (pressure % with 1 decimal). Needle: white/ivory with orange tip? Ivory needle, red zone. Glass glint: subtle white arc highlight. Add slight needle wobble when pressure > 85 (vibration) — and during erupt, needle slams down. Sliders panel (DOM, bottom-left): dark metal plate: title row "ERUPTION CONTROL" small caps letterspaced; three slider rows each: label left (POWER / WIND / OUTPUT?) — names: "Jet Power", "Wind Force", "Particle Output"? Spec: eruption power, wind strength & direction, particle amount → labels: POWER, WIND, DENSITY. Wind slider centered zero with a center notch marker and live arrow indication (small "←/→" arrow glyphs? no emoji — draw with characters "◀/▶"? those are geometric shapes, borderline; better: text "W" value showing e.g. "← 35" using a small CSS triangle or just signed number "-35 →"). I'll render value as signed number with a direction word: e.g. "−35 E" / "+35 W"? Overcomplicating. Show numeric value: "−35" and the slider track has a center notch. Wind also shown live in scene by ash drift + a subtle wind streak particles in sky? Nice touch: faint wind streaks (thin lines moving horizontally at wind speed) — communicates wind without UI. Small effort: 20 streak particles wrapping around, alpha 0.08. Good detail. Slider styling: custom range styling: track = dark groove with inset shadow; thumb = square knurled knob (CSS: linear background? avoid gradient decoration... a subtle vertical brushed metal via two-tone is functional skeuomorphism; I'll use flat color with border + inset highlight — fine without gradients). Use accent color ember for thumb when active? Keep thumb ivory/steel, accent underline for value text. Value readouts per slider: numeric (tabular). Also allow keyboard (native range does). Panel styling: background rgba(12,10,12,0.72) with 1px border rgba(255,255,255,0.08), backdrop-filter blur small, padding, letter-spacing. Font: "Chakra Petch" via Google Fonts CDN (link). Fallback sans. Telemetry numbers: font-variant tabular / 'Chakra Petch' is techy enough; or use "IBM Plex Mono" for numbers. Two fonts is fine: Chakra Petch (UI/labels) + IBM Plex Mono (numbers). Colors: page bg near-black; sky dusk: top #0d0f1e deep indigo → mid #1a1430? Let me craft palette: - sky top: #070810; upper: #12101f; horizon warm band: subtle #2b1b22 → ember haze near volcano? Slight warm horizon behind volcano: #33202a at horizon line, fading up. That's natural dusk. - volcano body: #241b20 base, lit ridges #3a2b2c highlights, shadow side darker. - ground: #141018 soil, tufts #0d0b10. - Accent ember: #ff9b2f / core #ffd76a; hazard red: #ff4d3a; ivory text: #e8e2d4; muted: #8a8494. Stars: ivory dots alpha twinkle. Maybe a thin crescent moon top-left? Adds charm; draw simple crescent via two circles (mask) — okay small effort, do it (off-white, subtle). Or skip if crowded — top-right has dial; put moon top-left near stats? Stats DOM top-left too. Put moon at upper-left-center x=w*0.16,y=h*0.14. Fine. **Wind streaks:** y positions random in sky, length ~ 60*|wind|, speed = wind*4 px/s, alpha 0.05–0.1 ivory lines. When wind 0, hidden. **Lightning:** as described. Draw jagged polyline: generate points: start inside plume (cx ± plumeR*0.4, mouthY − plumeH*rand), 6–9 segments downward-outward with jitter, life 0.12–0.2s, alpha flicker. Draw with lighter: strokeStyle rgba(200,190,255, a), lineWidth 2 with second pass wider faint. Also a soft radial flash sprite at bolt origin. Trigger: during ERUPT & ashCount > 150, prob = dt*3. **Screen shake:** shakeAmp; on erupt start: amp = 14 decaying + during erupt sustain 3–6 + per-burst kicks. Apply translate before world draw: ox = (rand-0.5)*amp etc. Also slight crater rumble during pressure > 90: amp = (p-90)/10 * 2. **Fountain modulation:** spawnRate multiplier m = 0.55 + 0.45*sin(eruptT*2π*0.9) clamped ≥0.15 + initial spike (first 0.5s ×2.5). Plus random gusts: m *= 1 + 0.3*sin(t*3.1). **Erupt duration D:** 3.0 + rand*2.5. Pressure drains to 0 across D (linear-ish with ease-out). After D, spawn stops (waning: spawn rate ramps down in last 20% of D). Then BUILD resumes; ensure pressure starts from ~0. Pressure build rate: full in ~14s ±4: rate = 100/(12+rand*6) per s modulated ±15%. Edge cases: resize → regenerate terrain, rebuild sprites? Sprites independent of size. Terrain regen on resize (debounced), clear crust (acceptable). Keep particle positions scaled? Just clamp/remove particles outside new bounds: on resize, kill particles with x>newW. Fine. Also handle `visibilitychange`? dt clamp (max 0.05) prevents jumps. Use fixed-ish variable dt with clamp; physics with dt is fine. **Counter requirement:** "live on-screen counter showing active particle count" — I'll show lava + ash separately? Show "PARTICLES: 1234" (lava+ash) and maybe breakdown small: "LAVA 1103 · ASH 131". Plus FPS. Guarantee ≥1000 active smooth: density slider max should sustain >1000 during eruption: capacity lava 3500, spawn up to ~1500/s during D~4s → up to ~3000 active at peak; even mid-range should exceed 1000 at eruption with slider ≥ ~40%. During quiet buildup counts are low (few hundred ash trickle) — that's inherent (eruptions are periodic). The requirement presumably means during eruption. Ensure at default settings eruption peaks ~1200+. Default density 70%. Perf sanity: 3000 lava drawImage lighter + physics — should hold 60fps on decent machines; dpr capped 1.5 helps. Also cull particles off-screen (x<−20 or x>w+20 → kill; y>h+30 kill). Let me also add subtle audio? No — keep silent (autoplay restrictions anyway). Could add WebAudio rumble toggle... skip, spec doesn't ask. Actually a tiny optional sound could be nice but adds complexity; skip. Now write the code. Structure: ```html Montserrat Vale — Eruption Simulator
...
... 3 sliders ...
...
``` Title idea: give it an authored name: "MONTE CENERE — Volcanic Eruption Simulator" or "MT. CINDER" — I'll call it "MT. VULCA — live eruption model". Header small caps top-left above stats: "MT. VULCA · CRATER 7" with "live simulation" tag. Keep copy minimal & in English (user request English). Panel details HTML: ```html
ERUPTION CONTROL
POWER 62
WIND +12
DENSITY 70
``` Wind slider with center notch: CSS background positioning of a notch via ::before on a wrapper? Simpler: add `background-image: linear-gradient(...)`? avoid gradients... a 2px vertical line via linear-gradient is a technique not decoration; but I'd rather avoid. Use box-shadow trick? Easiest: wrap each slider in a div with position:relative and add a `` absolutely positioned at 50% (only for wind). Do that. Slider CSS (webkit + moz): ```css input[type=range]{ -webkit-appearance:none; appearance:none; width:150px; height:22px; background:transparent; } input[type=range]::-webkit-slider-runnable-track{ height:4px; background:#2a2530; border-radius:2px; box-shadow: inset 0 1px 2px rgba(0,0,0,.6), 0 1px 0 rgba(255,255,255,.06); } input[type=range]::-webkit-slider-thumb{ -webkit-appearance:none; width:14px; height:20px; margin-top:-8px; background:#cfc8ba; border:1px solid #565045; border-radius:3px; box-shadow: inset 0 1px 0 rgba(255,255,255,.5), inset 0 -2px 0 rgba(0,0,0,.25), 0 1px 3px rgba(0,0,0,.5); cursor:grab; } ``` Also ::-moz equivalents. Also a thin ember-colored progress? Not necessary. Stats DOM: ```html
MT. VULCACRATER 7 · LIVE MODEL
PARTICLES0
LAVA / ASH0 / 0
FPS
``` Hint bottom-right: "drag the sliders · click the volcano to force an eruption" small muted. Now JS. Let me draft carefully. ```js 'use strict'; const cv = document.getElementById('scene'); const ctx = cv.getContext('2d'); let W=0,H=0,DPR=1; // ---------- helpers const rand=(a,b)=>a+Math.random()*(b-a); const clamp=(v,a,b)=>vb?b:v; const TAU=Math.PI*2; // ---------- sprites function makeSprite(d, stops){ const c=document.createElement('canvas'); c.width=c.height=d; const g=c.getContext('2d'); const r=d/2; const gr=g.createRadialGradient(r,r,0,r,r,r); for(const [p,col] of stops) gr.addColorStop(p,col); g.fillStyle=gr; g.fillRect(0,0,d,d); return c; } ``` Lava sprites: 12 bands. Band color function: ```js function lavaColor(t, a){ // t 0..1 heat // palette keyed stops const stops=[[1.00, 255,244,196],[0.9,255,214,96],[0.75,255,158,44],[0.55,244,96,26],[0.38,196,52,20],[0.24,122,44,32],[0.12,86,52,44],[0,58,50,44]]; ...interp rgb... } ``` Simpler: write function lerpColor over sorted stops. For band b, t = b/11. Hot sprite (for lighter blending) stops: 0:`rgba(core,1)`, 0.35:`rgba(mid,0.9)`, 0.7:`rgba(outer,0.35)`, 1: alpha 0 — where mid = color at t*0.7? Let's define sprite stops using the particle color c at heat t: - if t>0.45 (glowing): stops: [0, rgba(c*1.15 white-ish boost,1)], [0.4, rgba(c,0.95)], [0.72, rgba(c,0.30)], [1, rgba(c,0)] — lighter blend will make additive glow. To boost core brightness, lerp color toward white by (t-0.45)*... define coreCol = mix(c, [255,250,230], max(0,(t-0.7))*1.6 clamp .85). - if t<=0.45 (rock/ember fade): source-over: stops [0, rgba(c, 0.95)], [0.5, rgba(c,0.55)], [1, rgba(c,0)]. Dark colors blend normally over terrain. Sprite size 48 (so soft edges scale down nicely). Draw with dw= r*RY where r is particle radius: visual radius ≈ r*2.4 → dw = r*4.8? For r≈3 → dw≈14px sprite drawn. Hot glow halo larger: draw dw = r*7 when hot? The sprite gradient covers halo. Let me define draw size: base = r*5 for cold, r*7 for hot(t>0.5)? Interpolate size with heat: dw = r*(5 + t*4). Cap. Fine. Smoke sprites: dark & light, size 64: stops radial [0, rgba(col,0.85)], [0.5, rgba(col,0.5)], [1, rgba(col,0)]. Use puff with slightly irregular? Could add 3 overlapping blobs offset for irregular shape — yes, stamp 3 sub-gradients at offsets within sprite for character. Do it. Glow sprite (vent glow / muzzle flash / bolt flash): warm radial: [0, rgba(255,190,90,1)], [0.4, rgba(255,120,40,0.6)], [1, transparent], size 128. Now terrain generation: ```js let groundY, craterX, mouthY, baseY, coneH, terrainCv, tctx, crustCv, cctx, crustInit=false; function buildTerrain(){ craterX = W*0.54; baseY = H*0.87; coneH = Math.min(H*0.60, W*0.72); // responsive-ish const Rr = Math.max(46, W*0.075); // rim half width const RbL = Math.max(120, W*0.26), RbR = Math.max(150, W*0.33); groundY = new Float32Array(W? ...) // per CSS px column (int index) ``` Note W should be integer width. groundY length W (rounded). For each x (0..W-1): ``` let dx = x - craterX; let h; const adx = Math.abs(dx); const Rb = dx<0? RbL : RbR; if(adx >= Rb) h = 0; else if(adx >= Rr){ const t = (adx-Rr)/(Rb-Rr); // 0 at rim → 1 at base const e = Math.pow(t, 1.42); // concave: steep near top h = coneH*(1-e); // ridge noise h += coneH*0.016*Math.sin(adx*0.11+ (dx<0?2.7:0.4)) + coneH*0.008*Math.sin(adx*0.031*3 + dx*0.02); } else { // crater bowl const u = adx/Rr; // 0 center → 1 rim const floor = coneH*0.70; h = floor + (coneH - floor)*Math.pow(u,1.55); // slight rim peak bumps h += coneH*0.05*Math.exp(-Math.pow((u-0.92)*6,2)); // near rim lip? Actually rim defined by outer branch at adx=Rr giving h=coneH; discontinuity! } ``` Wait continuity: outer branch at adx=Rr: h=coneH*(1-0)=coneH. Inner branch at u=1: floor + (coneH-floor)*1 = coneH. Continuous ✓. Rim bumps: add small peak: h += coneH*0.06 * exp(-((adx-Rr)/ (Rr*0.18))²) applied for both sides slightly (a lip). Apply after branches for adx in [Rr*0.8, Rr*1.15]. ok add: `h += coneH*0.055*Math.exp(-((adx-Rr)/(Rr*0.16))**2)` for all x (harmless far away). Also small global noise: h += coneH*0.006*Math.sin(x*0.09+5)*... keep small. groundY[x] = baseY - h - 2? plus tiny per-pixel noise: groundY[x]= baseY - h + (noise). Also plain ground away from cone: groundY = baseY + small undulation (±3) so ground isn't a dead-flat line: baseY + 2*sin(x*0.01)... make plain y = baseY when adx>=Rb: groundY[x] = baseY + Math.sin(x*0.013)*2. mouthY = groundY[round(craterX)] i.e. crater floor: baseY - coneH*0.70 → spawn slightly above floor: mouthY = baseY - coneH*0.72. Store also `slopeArr`? compute on the fly from groundY. Terrain painting (tctx on offscreen sized W×H, DPR-scaled? For crispness draw terrain at DPR resolution: create canvases at W*DPR × H*DPR with ctx.scale(DPR,DPR) — then drawImage(terrainCv, 0,0,W,H) on main ctx (main ctx already scaled). Simplest: keep offscreen in CSS-pixel coordinate with its own DPR scale, then drawImage(terrainCv,0,0,W,H) with source full canvas → need drawImage(terrainCv, 0,0, terrainCv.width, terrainCv.height, 0,0, W, H). That resamples DPR→CSS (fine, it's 1:1 device pixels effectively). Actually if both are DPR-scaled, drawImage(terrainCv, 0,0,W,H) draws the whole source scaled to W,H CSS units which under main ctx scale(DPR) lands exactly device-pixel-perfect. Good. Painting steps: ``` function paintTerrain(){ tctx.setTransform(DPR,0,0,DPR,0,0); tctx.clearRect(0,0,W,H); // silhouette path from groundY tctx.beginPath(); tctx.moveTo(0, groundY[0]); for(x=1..W-1) lineTo(x, groundY[x]); lineTo(W,H); lineTo(0,H); closePath(); // base fill: flat dark tctx.fillStyle = '#23181d'; fill(); // vertical shading: overlay translucent darker near base & lighter up-slope — use big linear gradient? It's naturalistic shading (light from sky), acceptable: linear gradient from y=baseY (dark #150e12, alpha .55) to y=baseY-coneH (transparent) — composite 'source-atop' after clip? Use the same path as clip: tctx.save(); clip path; const sh = tctx.createLinearGradient(0, baseY, 0, baseY-coneH); sh.addColorStop(0,'rgba(8,5,8,0.72)'); sh.addColorStop(0.35,'rgba(10,6,10,0.25)'); sh.addColorStop(1,'rgba(60,38,34,0.16)'); // slight warm at top fill rect with sh (clipped). // slope striations (ravines): clip still active for(i=0;i<70;i++){ pick side: left/right random; adx0 = Rr + rand(6, Rb-Rr-8)... need actual distance: t0=rand(0.05,0.8); adx = Rr + t0*(Rb-Rr); x0 = craterX ± adx; y0=groundY[x0|0]; len = rand(40, 160) * (1 - t0*0.5); walk: dir = sign(dx) outward-downhill; steps: draw polyline sampling groundY along x, offset perpendicular slightly? Simpler: draw a line from (x0,y0) going outward & downward following slope: x from x0 step dir*1 while adx=lmax[i]) {kill} const x=lx[i], y=ly[i]; // out of bounds if(x<-30||x>W+30||y>H+40){kill} if(state fly){ lvy[i]+=G*dt; lvx[i]+=windF*dt*windLavaCoef; // windLavaCoef = wind*0.9? windF = wind slider -100..100 → windPx = wind*2.2 px/s target drift... define windAcc = wind*6 (px/s²) for lava (light pumice feels wind), bombs less (mass): coef *= (1 - r*0.08)? small effect, skip mass dependence... keep uniform 5. lvx[i]+=windA*5*dt; // windA = wind value (−100..100) → accel −500..500 px/s²?? too strong. wind*2 → ±200 px/s²: over 2s flight = ±400 px/s drift — strong but plausible for ash; for lava maybe wind*1.2. Let me set lava wind accel = wind*1.5. integrate const ix = x|0 clamped; gy = groundY[ix]; if(y>=gy){ slope = groundY[min(ix+2,W-1)] - groundY[max(ix-2,0)] → /4 if(!lbo[i] && lvy[i]>230 && heat>0.5 && Math.random()<0.55){ bounce: ly[i]=gy-1; lvy[i]*=-0.32; lvx[i]=lvx[i]*0.55 + Math.sign(slope||...)*80; lbo[i]=1; spawn tiny ember? no } else { settle: state=1; ly[i]=gy - 1; lvx[i]=Math.sign(slope)*Math.abs(lvx[i])*0.3; lvy[i]=0; } } } else { // sliding const ix=clamp(x|0,1,W-2); const gy=groundY[ix]; ly[i]=gy-1 - lr[i]*0.3; const slope=(groundY[ix+1]-groundY[ix-1])*0.5; // px per px let sp = Math.abs(slope)*90*heat; // slide speed px/s // viscosity limit const dir = slope>0?1:-1; let nvx = dir*sp + windA*0.8*heat*... skip wind on ground except flat creep: on flat ground (|slope|<0.02) wind pushes cooled crumbs? skip. lvx[i] = dir*sp; lx[i]+=lvx[i]*dt; // stamp trail if(heat>0.15 && Math.random()=W-3){ freeze & stamp splat; kill (recycle) } } heat = 1-lage/lmax — computed when needed. } ``` Wait heat: I want cooling faster for small particles (thin crust cools quickly) and slower for bombs. lmax handles it: lmax = base(4.5) + r*1.2 + bomb bonus. heat = 1 - lage/lmax. Also heat influences slide & glow. Also once heat < 0.14 & sliding → freeze; what about flying particles whose heat expires? lage>=lmax → kill (they vanish mid-air; maybe they should turn to rock & fall: at kill by age, if still flying just remove (fade already invisible). Fine. Also flying particles at low heat could just fall fast & freeze on impact → handled by settle logic (if heat<0.14 on settle → freeze immediately). Kill: swap-remove via copy. Spawn lava: ```js function spawnLava(){ // find slot: if lN>=L_CAP reuse index (lN-1? oldest = index0) → overwrite 0 then swap? Simplest: if full: overwrite slot lN-1? Let me reuse slot 0 by copying? Just write into slot 0 and leave n same (index0 oldest). Actually recycling oldest is right: write fields to index 0 (it's oldest since we append at end and swap-remove moves last→i, so relative order approximates age). Hmm swap-remove breaks strict ordering but roughly fine. If lN>=CAP: write to slot 0 else lx[lN++]=… const p = power/100 (0..1) const bomb = Math.random()<0.06+power*0.0004; const ang = Math.PI/2 + rand(-1,1)*rand(0,1)*(0.55 - p*0.25)*... ``` Angles: deviation from vertical: dev = (rand()^1.6 * 0.9) * side ± ; scale by (0.75 − p*0.35) → power focuses jet: devMax = 0.9 - p*0.45 (rad). Also slight outward bias? no. ``` const sp = (260 + p*640) * (bomb? rand(1.05,1.25): rand(0.62,1.0)) * fountainMod; vx = Math.sin(dev)*sp*rand(0.8,1.1)*side + windA*0.6; vy = -Math.cos(dev)*sp; r = bomb? rand(3.4,5.2) : rand(1.6,3.0); x = craterX + rand(-Rr*0.5, Rr*0.5)*? mouth width: spawn across crater mouth: craterX + rand(-1,1)*Rr*0.55, y = mouthY + rand(-6,4); lmax = (bomb? 7:5) + r*0.9 + rand(0,1.5); lage=0; state=0; lbo=0; } ``` Fountain mod computed per frame during eruption. Spawn scheduling: during eruption, accumulator: lavaAcc += rate*dt; while(lavaAcc>=1){spawnLava(); lavaAcc--}. rate = lerp(120, 1500, density^1.2?) — density slider 5..100 → rate = 60 + (d/100)^1.15*1400. Also multiply by fountain pulse m and pressure factor (during eruption pressure drops; spawn tied to erupt phase & remaining pressure*? Rate *= 0.3+0.7*(pressRemaining)). Good: eruption tapers as pressure empties — matches "pressure resets". Ash spawn: during erupt: rate = 40 + d*3.5 (up to ~390/s) * pulse; ash life ~4–7s, size grows → concurrent ~1500? cap A_CAP=900: recycle oldest when full. During buildup: rate = 2 + press*0.12 (trickle ~12/s at high pressure) small puffs. Ash particle fields: ax,ay,avx,avy,aage,amax,ar0, seed, heat? Let me have: ar0 (base radius), plus computed r = ar0 + age*k; heat = max(0,1-aage/1.4). Fields: 8 arrays: x,y,vx,vy,age,max,r0,seed. 9 with heat? heat derivable from age. ok 8. Ash update: ``` aage+=dt; kill if >= amax or y>H+80; heat = 1 - aage/1.5 (clamp 0..1); // buoyancy & gravity avy += (46 - heat*95)*dt; // net: hot rises (−49), cold sinks (+46) avy *= 1 - 0.55*dt... apply drag: avy += -avy*1.0*dt (drag proportional) avx += (windA*2.6)*dt + Math.sin(time*1.6+aseed)*26*dt + (rand-0.5)*30*dt; avx *= 1-0.7*dt; x+=vx*dt; y+=vy*dt; ``` Hmm tune: want plume rising fast initially: spawn vy = -(60+power*1.6)*rand(.7,1.2) ≈ −60..−230; then heat buoyancy keeps rising a bit; then stall & drift. Terminal-ish behavior via drag. Looks fine; will tune constants mentally: heat*95 up to 95 upward accel early + initial −150 vy: rises several hundred px. Good. Spawn ash: ``` x = craterX + rand(-1,1)*Rr*0.5; y = mouthY + rand(-8,2); vx = rand(-30,30) + windA*1.5; vy = -(70 + p*180)*rand(0.7,1.15); r0 = rand(6,14) small puffs / during eruption bigger rand(10,22); max = rand(3.5,7); seed = rand(0,100); ``` Draw ash: size = r0 + aage*(10 + r0*0.9)?? growth: r = r0*(1 + aage*0.55)? Let's do r = r0 + aage*r0*0.45 → grows to ~2.5×. alpha: a = fade: fadeIn = min(1, aage*3), fadeOut = 1 - (aage/amax)^1.6; alpha = 0.5*fadeIn*fadeOut* (dilution: r0/r since spread) → alpha *= (r0/r)^0.6? Simplify alpha = 0.55*fadeIn*fadeOut * r0/r... r0/r could go 0.4 → alpha 0.22 final ok. Draw: f = aage/amax*1.3 clamp 0..1 (dark→light). draw darkSmoke alpha*(1-f), lightSmoke alpha*f. Plus heat glow: if heat>0.05: lighter hotSprite (glow sprite small) alpha heat*0.35*fadeIn. Ash drawn before lava. Order overall per frame: 1. ctx clear / sky 2. stars, moon (sky elements) 3. wind streaks 4. world transform with shake? Apply shake to everything in world incl terrain. Order: save, translate(shake), draw terrain image, crust, ash, lava, lightning, shock ring, vent glow, restore. 5. Dial + (DOM handles stats/panel). Sky: fill rect with vertical gradient (dusk). Stars: precomputed list {x,y,tw}. Draw: alpha = 0.35+0.5*sin(time*tw)... draw as 1px rect with fillStyle changes — 120 changes/frame ok. Use ctx.globalAlpha per star with fixed fillStyle. Moon: crescent: draw circle ivory-ish dim (#d9d3c4 alpha .9), then overlay circle sky-colored offset to cut crescent: use composite 'destination-out'? On main canvas that would erase stars beneath... draw moon into offscreen sky layer? Simpler: draw moon circle, then draw offset circle filled with the sky color at that point (approximating gradient) — pick color sampled: at moon y, sky gradient color — I can compute gradient color function manually: I define sky as custom function lerp between color stops, then moon cut circle uses that. Implement skyColorAt(y) function reused for gradient creation too. Good. Sky gradient stops: - 0: #05060e - 0.45: #131228 - 0.75: #2a2032 - 1: #45303a? horizon warm plum #4a3040. Slightly muted. Let me choose: top #06070f, mid #161227, lower #2b1f33, horizon #55394a? That reads dusk magenta. Slightly desaturate: horizon #4d3341. ok. Wait: avoid "gradient decoration" — but sky gradient is scenery, legitimately physical. Fine. Ground fog / horizon haze: subtle light band at horizon behind volcano: radial? skip. Stars only in upper 70% (avoid horizon clutter). Also stars shouldn't overlap dial area (canvas-drawn dial at top-right) — stars behind dial? Dial drawn after, covers them. Fine. DOM stats top-left overlays stars — fine (stats panel has bg). Wind streaks: array 26 {x,y,spd,len,a}; wrap x. draw when |wind|>3: line (x,y)-(x+len*windSign,y) alpha a*min(1,|wind|/40). Speed = wind*3.2 px/s → windSign = sign(wind). len = 14+|wind|*0.5. update x += wind*3.2*dt; wrap. Lightning bolt gen: ```js function spawnBolt(){ const ax = craterX + rand(-1,1)*Rr*1.1; const ay = mouthY - rand(30, plumeTop() ... estimate plume height: ash avg y? Use mouthY - rand(40, 260). points: p=[ax,ay]; n=6+rand int 5; dir down + outward: for k: x += rand(-14,14)+ lean; y += rand(8, 20); bolts.push({pts, life:0.16+rand*0.1, t:0, seed}); flash: flashes.push({x:ax,y:ay,r:rand(50,120),t:0,life:0.25}); } ``` Keep small arrays (bolts max few). Draw: alpha = (1 - t/life) * flicker(rand per frame? use sin(time*90+seed) > -0.2). strokeStyle 'rgba(205,200,255,α)'; lw 1.6; second pass lw 4 alpha*0.25. Lighter blend. Bolt origin: from within ash cloud — pick random ash particle location! `const j = (Math.random()*aN)|0; ax=ax_[j], ay=ay_[j]` then path goes from that point downward/outward toward cloud edge. Nice grounding. If aN small skip. Flashes: draw glowSprite lighter scaled. Shockwave: one object {r, t}: on erupt start: r=10; update r += 400*dt (ease: r = 40+ t*380, alpha=1-t/0.9). Draw ring: stroke ellipse? circle at crater mouth center: strokeStyle rgba(255,220,180,α*0.5), lw 2*(1-t). Also second dust ring at ground? skip. Vent glow (pre-erupt & erupt): glowSprite drawn at mouth: size = 90 + press*160 (during build) with alpha = (press/100)*0.5*(0.8+0.2*sin(time*7)) + eruptMuzzle. Muzzle flash: first 0.35s of erupt: big glow sprite size 300*ease alpha .9→0. Also draw vent glow with 'lighter'. Crater glow illuminating inner walls: the glow sprite lighter partially does it. Camera shake: shake amp variable; each frame: if erupting: amp = 5 + 4*sin burst? Compose: shakeAmp decays *= exp(-3dt); on events add. Continuous during eruption: shakeAmp = max(shakeAmp, 4 + pulse*3). During press>85: shakeAmp = max(shakeAmp, (press-85)*0.15). offset = rand(-1,1)*amp each axis. Pressure logic: ```js let phase='build', press=0, pressRate, eruptT=0, eruptDur=0; pressRate = 100/(11+rand(0,7)); // recompute each build cycle? compute once + slow noise build: press += pressRate*(1+0.25*Math.sin(time*0.7))*dt; press=min(100) also rumble at >88. if(press>=100) startErupt(); startErupt(): phase='erupt'; eruptT=0; eruptDur=3.2+rand(0,2.6); shock; shake+=12; muzzle; erupt: eruptT+=dt; press = 100*(1 - easeout(eruptT/eruptDur)) → press = 100*Math.pow(1-min(1,eruptT/eruptDur), 0.8); spawning as above (rate scaled by press/100 portion). if(eruptT>=eruptDur){ phase='build'; press=0; pressRate=100/(11+rand(0,7)); } ``` Hmm press reset to exactly 0 at end ✓ "pressure resets and builds again". Manual trigger on click: if phase==='build': startErupt() with eruptDur maybe shorter if press low: eruptDur = clamp(1.6+press*0.03, 1.6, 6). And press drains from current value: need pressStart = press → drain: press = pressStart*(1-e). Store pressAtErupt. Also if press tiny (<15) still allow small burp (fun). Also set shock & shake. Also prevent re-trigger while erupting (ignore clicks during erupt). Also clicking: get click coords (canvas client), map to world; check inside cone region: |x-craterX| < Rb(max) && y > groundY[x] - 8 && y< H? Just: compute groundY at click x; if clickY >= groundY[ix]-10 → inside volcano/ground → trigger. Also require clickY > some min? Any click on the ground/volcano area triggers; sky clicks ignored. Add small "hint" text mentioning it. Dial rendering: ```js function drawDial(x,y, R){ // plate disc: fill #17141c alpha .9? Draw: outer bezel ring stroke #3a3540, face #100d14, subtle inner shadow. ticks: for v=0..100 step 5: ang = PI*(1 - v/100)*? gauge sweep from 135° (left-down) to 45°(right-down)? Standard: angles from -225°? Let me define: sweep from 150° to 30°?? Use: a0 = PI*0.75 + ... I'll use start angle 135° (measured standard CCW from +x axis: pointing down-left) to 45°... Simplest param: ang(v) = PI*1.25 - v/100 * PI*1.5 → v=0 at 225° (down-left), v=100 at 225-270=... 225°−270°*? sweep 270 too wide. Use 240° sweep: from 210° to -30°? Let me just do: start = (135+90)=? I'll define in radians: a = Math.PI*0.75 → (PI*0.25)? Angles: left-down (135°=2.356 rad) sweeping up over top to right-down (45°=0.785): ang = 2.356 - (v/100)*(2.356-0.785)= 2.356 - v*0.01571 → sweep 157° top arc. Hmm classic gauges sweep ~240° with needle pivot bottom. For compact dial in canvas corner, a ~160–200° arc centered at top with pivot at bottom center looks right. ``` Simplify: dial radius R=44. Pivot at center (cx0, cy0). Ticks along arc from a=−215°?? I'll do sweep from 200° to −20° (i.e., 220° arc going over top): ang(v) in radians = (200 − 220*v/100) * π/180 → v=0: 200° (left, slightly below horizontal), v=100: −20° (right below horizontal). Red zone 85–100: arc stroke rgba(255,72,54,0.85) lw 5 at radius R−6? Ticks every 10: line from R−2 to R−8; minor every 5 shorter. Labels 0/50/100 tiny? Draw numbers 0 and 100 at ends, 50 mid? Keep ticks only + big numeric readout center-lower: press.toFixed(0) + '%' — actually show as "kPa ×" whatever: show `press.toFixed(1)` big. Needle: from pivot toward ang(press) length R−10, ivory #e8e2d4, with tail 6px opposite; jitter: ang += (press>80? (rand-0.5)*(press-80)*0.004:0). Needle spring smoothing: displayPress += (press - displayPress)*min(1,dt*7) + tiny wobble sin(time*13)*(press/100)*0.6°... good enough: needle angle uses smoothed value + vibration. Also small "CHAMBER PRESSURE" caption under dial (canvas text, Chakra Petch 9px letterspaced — canvas letterSpacing property exists in modern browsers: ctx.letterSpacing = '2px' (Chrome 125+?); fallback fine without). Use font: '11px "Chakra Petch"'. Also small warning dot: circle blinking red when press>88 (alpha sin) labeled "VENT DANGER"? Keep: tiny red dot near dial blinks; label "HAZARD" 8px. Nice. Position dial: top-right margin 20px: x=W-20-R... define W-64? dialR=46, x=W-70, y=64. Ensure dial drawn unshaken after world restore. Also dial under DOM? DOM stats top-left, panel bottom-left, hint bottom-right — dial canvas top-right free. ✓. FPS: ema: fps = fps*0.9 + (1/dtRaw)*0.1; display floor. dtRaw = raw delta clamped 0..0.1 for fps calc (use raw). Particle count display: lN+aN (thousands separator via toLocaleString? use manual format or just number). Update DOM every 0.25s: counter.textContent etc. pBar width = (lN+aCapUsed)/cap? Show bar = min(1,(lN+aN)/1500)? Show bar relative to capacity L_CAP+A_CAP. Main loop: ```js let last=performance.now(); function frame(now){ const dtRaw=(now-last)/1000; last=now; const dt=Math.min(0.05, dtRaw||0.016); time+=dt; update(dt); render(); requestAnimationFrame(frame); } ``` Update: pressure/phase; spawn scheduling; update lava (iterate backwards with swap-remove — forward iteration with swap-remove: when killing i via copy last→i, the element moved from end was already processed? If iterating forward i current i haven't been processed (forward loop), so copied one is unprocessed ✓ processed exactly once ✓. Good — while-loop pattern with conditional increment. Same for ash. Rendering lava: ```js ctx.globalCompositeOperation='lighter'; for(i0.28 → draw hot: dw = lr[i]*(5+heat*4); drawImage(sprite[band], x-dw/2, y-dw/2, dw, dw); ``` Wait band = clamp(round(heat*11),0,11). Cold ones (heat<=0.28) drawn after with source-over: loop again or single loop switching composite per particle — switching composite per particle is state-change heavy? ctx.globalCompositeOperation assignment is cheap-ish but 3000 toggles... Better: two passes: pass1 lighter for heat>0.28, pass2 source-over for heat<=0.28. Two loops over lN with branch — fine. Actually mid heat 0.28–0.5: sprite is dark red ember — lighter blending of dark red ≈ subtle glow, fine either way. Threshold 0.3. Also flying bombs leave smoke trail? Could spawn tiny ash occasionally behind bombs — adds richness: bombs (r>3.4) with prob dt*3 spawn small ash at position with slight upward drift. Cap by aN. Nice detail, cheap. Also glowing lava should illuminate crust/terrain nearby? Skip (cost); the glow sprites overlapping terrain via lighter already brighten terrain beneath them ✓ automatically since lighter adds over terrain. Settled particles stuck at ground: draw with sprite centered slightly above ground: position already y=gy-1. ✓. Particle radius for draw: lr = stored r. Sprite draw size dw = r*6 + heat*r*3 → hot glow halo bigger. Also faint core for very hot: fine. Render ash pass (before lava): loop aN: compute f, r, alpha; two drawImage per particle. Also maybe draw ash with slight vertical stretch? skip. Lightning after lava? Before lava (ash-lit) — draw bolts & flashes right after ash with lighter. Then lava. Then vent glow & muzzle & shock. Then restore & dial. Numbers formatting: `String(Math.floor(n))`. Also stats: show LAVA n · ASH m separately small. Resize: on resize (debounce 150ms): set W,H, rebuild terrain, clear crust (crustCv.width=... resets), also rebuild star positions (count ~ W/12 capped 140), wind streaks, reposition dial auto (uses W at render). Kill particles out of range automatically next frames. Also mouthY etc. recompute. Note baseY etc. globals set in buildTerrain. Mobile: touch works via slider native; canvas click → touchstart? click fires on tap. ok. Also font sizes small. Fine; keep desktop focus. Now writing all constants tuned. Let me be careful with a few values: - gravity 950. - launch speed: p=power/100 (10..100 → 0.1..1). sp = 240 + p*700 → 310 (min p .1 → 240+70=310) to 940. With dev up to ~0.7 rad. Flight time up 2*vy/g ≈ 2*900/950 ≈ 1.9s, range vx*2t: vx up to sin(0.7)*940≈600 → range ~1200px. Cone base ~ 0.3W ≈ 500px from crater → bombs can clear the crater to slopes and beyond — good variety. Most particles (rand 0.62..1 × sp, small dev) arc nicely onto slopes. Hmm wait dev affects vx = sin(dev)*sp, vy=cos(dev)*sp: vy max = sp. Range on flat = 2*vx*vy/g — with vx=300,vy=800: 2*240000/950≈500px. Good. - Sliding: slope near cone flank: groundY diff per px: cone height coneH over (Rb−Rr)≈0.24W≈300–400px, but concave: near rim slope steep: d(h)/dx ≈ coneH*1.42*t^0.42/(Rb−Rr)... at t small, slope ~ coneH*1.42*t^0.42/(Rb-Rr) → at t=0.05: 0.42*?? pow(0.05,0.42)≈0.28 → 0.28*1.42*coneH/380 ≈ coneH*0.001 → coneH=520 → 0.53 px/px? That's ~28° steep ✓. So |slope| up to ~0.8 near rim, smaller near base. slide speed = |slope|*90*heat → up to 70 px/s when hot near top — hot fast flow ✓. Flat plain slope ~ 2*sin' — |slope| tiny → freezes fast on plain ✓. Wait — the crater floor is nearly flat and BELOW rim: particles landing inside crater slide toward center & freeze in the vent? Slope inside crater: bowl parabola from floor (center) to rim: slope small at center, larger near walls. Particles landing in crater accumulate/freeze near vent — realistic! But could they pile and block? We don't model accumulation (crust stamps only visual). Fine — cooled lava freezing inside crater = realistic "plugged vent" look; stamps inside crater visible ✓ nice. But stamps inside crater at freeze: freeze condition |slope|<0.035 → crater floor center flat → freeze there. But also particles landing in crater when hot slide to center & freeze. The vent keeps erupting through them (fine visually, glow above). - Freeze stamp: draw lavaSprite[band(heat)] into crust at (x, gy - r) size r*5 alpha 0.85. Also stamp a darker "cool rock" underneath: single stamp ok. Since crust fades over ~1-2 min, volcano keeps fresh dark streaks from recent eruptions ✓ signature. - Also sliding trail stamps: alpha 0.5, size r*3.5, every ~dt*10 prob → a sliding particle stamps ~10/s — with 300 sliding → 3000 stamps/s?? Too many. Reduce: prob dt*2 → 2/s each → 600/s stamps of small drawImage on crust: 600 small drawImages/s on top of main rendering — acceptable? drawImage to offscreen 30×30 each = fine perf-wise (GPU), but do only while heat>0.15 and cap total stamps per frame: stampsThisFrame<8. Implement global counter reset per frame. - Crust fade tick: every 0.4s: cctx.fillStyle='rgba(0,0,0,0.022)' destination-out full rect — fade e-fold 1/(0.022*2.5 per s)= ~18s scale → after 60s ~ e^-3.3 ≈ 4% → good. But careful: crust canvas also holds DPR scale; cctx transform set at init to scale(DPR); fillRect(0,0,W,H) covers ✓. - Bomb smoke trail: prob per frame dt*4 for bombs while flying: spawnAshSmall(x,y): r0 rand(3,6), vy rand(-15,10), max 1.5–2.5s. Uses same ash arrays with small sizes. ✓ Now slider defaults: power 62 → p=0.62: sp≈675; dev max ≈0.9−0.62*0.45=0.62 rad. Good arcs over cone. Wind default +12 gentle. Wind accel on ash: windA*2.6 = ±260 px/s² max — at wind 100 ash hugs one direction hard — fun. Lava wind accel wind*1.2 → ±120 px/s² over 2s flight = ±240 px/s → noticeable arc bend ✓. Settled slide unaffected. Density default 70 → rate = 60 + (0.7^1.15≈0.66)*1400 ≈ 990/s peak (× pulse up to ~1.2 → ~1200). Erupt 4s with avg rate maybe 700 → ~2800 spawned; lifetimes ~5–7s → concurrent ~2500+; capacity 3600 ok. Counter will show >1000 easily ✓ even at density 50. Ash rate = 30 + d*3.2 → d70: 254/s × pulse; ash max life 7 → concurrent ~1200 → A_CAP 800 → recycle oldest: since ash looks cumulative plume, recycling oldest is okay (they'd fade anyway). Bump A_CAP to 900. Hmm draw cost: 900×2 drawImages + lava 2500 → ~4300 drawImages + terrain blit: should be OK at 60fps on modern; on weak machines FPS dips — acceptable, and dial FPS readout shows it. Could reduce ash alpha cost by single-sprite blend when f extreme... keep as is. Cull: lava off x bounds kill ✓; ash x beyond W+120 kill (wind may carry plume off) ✓; ash y> H+60 kill. Particles landing below? plain ground fine. One more detail: lava flying upward from vent: immediately y= mouthY − ... spawn y between mouthY−4 and mouthY+3 (inside crater). They may immediately be below groundY at spawn x? groundY at crater center = baseY − coneH*0.70 = mouth floor. mouthY = baseY − coneH*0.72 (slightly above floor). Spawn y in [mouthY−6, mouthY+6]; at edges of mouth (±Rr*0.5), ground rises toward rim? Inside bowl: at |dx|=0.55Rr: h = floor + (coneH−floor)*(0.55)^1.55 ≈ floor + 0.30*(coneH−floor) → ground is HIGHER (closer to sky) at the sides than at center → spawn y at center-ish + rand small may be BELOW ground at side positions (y > groundY) → instant collision on first update → settles on inner wall — actually fine/nice (wall splatter), but many spawning embedded → they'd instantly "settle" onto crater walls with zero velocity and freeze → wall accretion look... Some is nice; too much kills fountain. Fix: spawn y = groundY[ix] − 2 (per-spawn surface-relative): y = groundY[clamp(round(sx))] − rand(2,8) → always just above surface ✓ and vx outward will carry them. Also give initial vy upward anyway. Also ensure spawn x across vent: |dx| ≤ Rr*0.55 might place them on inner walls — visually fountain emerges across crater mouth — good. Also for realism the initial few frames they may collide immediately since y just above surface → they'd settle instantly if vy small... vy is large negative (up to −900) → they leave immediately ✓. Now, stamps visible on slopes darken over eruptions — start crust empty; terrain painted with subtle striations. Extra polish items: - Panel & stats styling with careful typography. - Add slight vignette on main canvas? A gentle vignette adds mood: after world draw, draw radial darkening (source-over, rgba(0,0,0,0.25) edges). It's atmosphere, not decor — include a subtle one via pre-rendered vignette canvas (radial gradient transparent center → dark edges, alpha 0.35 max at corners) drawn each frame. Pre-render at resize. - The "hint" line bottom-right. - Title in stats panel: "MT. VULCA — CRATER 7" with a live status line that changes with phase: "STATUS: BUILDING" → "STATUS: ERUPTION" blinking ember. Nice narrative touch: statusEl text updated. Fonts: ``` ``` Canvas text fonts need fonts loaded before draw — dial draws every frame so once loaded it applies (canvas font uses loaded font if available; before load falls back). Fine. Now write final code fully & carefully. Watch for correctness (no typos). Let me write it out mentally in full. Also: slider "notch" for wind center: wrap: ```html
WIND +12
``` .sl {position:relative; display:inline-block;} .notch{position:absolute;left:50%;top:2px;width:2px;height:18px;background:rgba(232,226,212,0.28);transform:translateX(-1px);pointer-events:none;} Wind value display: show signed with direction arrow? Text: "W +12" confusing. Use: value formatted: wind>=0 ? `E ${wind}` : `W ${-wind}`? Actually wind blowing toward east/west: + pushes particles +x (rightward) → label "→ 12" using "→"? Unicode arrow is a character, not emoji — acceptable typographically (it's a text glyph). I'd rather keep clean: show `+12` and rely on scene streaks. OK: show signed number with explicit sign. Also the power & density show 0–100. Colors CSS: - --ink: #e8e2d4; --mut:#918a96; --ember:#ffb340; --hot:#ffd76a; --bg: rgba(13,10,14,0.78); border: rgba(232,226,212,0.14). Panel width ~ 320px, bottom-left 18px. Stats top-left 18px. Hint bottom-right. Also make canvas cursor: crosshair? Default fine. Panel title small with rule line. Let me now also double check swap-remove copy functions list all arrays. Lava arrays: LX, LY, LVX, LVY, LR, LAGE, LMAX, LST, LBO → 9. Ash arrays: AX, AY, AVX, AVY, AAGE, AMAX, AR0, ASEED → 8. Functions: ```js function lKill(i){ lN--; if(i!==lN){ LX[i]=LX[lN]; LY[i]=LY[lN]; LVX[i]=LVX[lN]; LVY[i]=LVY[lN]; LR[i]=LR[lN]; LAGE[i]=LAGE[lN]; LMAX[i]=LMAX[lN]; LST[i]=LST[lN]; LBO[i]=LBO[lN]; } } ``` Careful: if i===lN after decrement, nothing to copy ✓. Ash similarly. Spawn overwrite when full: ```js function lSpawn(...){ let i; if(lN>=L_CAP){ i=0; } else { i=lN; lN++; } LX[i]=...; } ``` Recycling oldest index 0 — but index0 may be actively drawn... it's fine (it's oldest, near death). Ash recycle oldest similarly. Update loops with while pattern: ```js let i=0; while(i=LMAX[i]){ lKill(i); continue; } const st=LST[i]; if(st===0){ LVY[i]+=950*dt; LVX[i]+=windA*1.2*dt; LX[i]+=LVX[i]*dt; LY[i]+=LVY[i]*dt; const xi=clamp(LX[i]|0,0,W-1); // also bombs trail if(LR[i]>3.3 && Math.random()=groundY[xi]){ const gi=clamp(xi,2,W-3); const sl=(groundY[gi+1]-groundY[gi-1])*0.5; if(!LBO[i] && LVY[i]>240 && (1-LAGE[i]/LMAX[i])>0.5 && Math.random()<0.6){ LBO[i]=1; LY[i]=groundY[xi]-1.5; LVY[i]*=-0.34; LVX[i]=LVX[i]*0.5+Math.sign(sl||0.001)*60; } else { LST[i]=1; LY[i]=groundY[xi]-1; LVY[i]=0; LVX[i]=0; } } } else { const xi=clamp(LX[i]|0,1,W-2); const sl=(groundY[xi+1]-groundY[xi-1])*0.5; const heat=1-LAGE[i]/LMAX[i]; LY[i]=groundY[xi]-1-LR[i]*0.35; if(Math.abs(sl)>0.04 && heat>0.12){ const dir=sl>0?1:-1; const spd=Math.min(1.3,Math.abs(sl))*95*heat; LVX[i]=dir*spd; LX[i]+=LVX[i]*dt; if(stampBudget>0 && heat>0.18 && Math.random()W+40||LY[i]>H+60){ lKill(i); continue; } i++; } ``` Wait bug: for st===0 case after settling we set LST=1 then fall through to bounds check & i++ ✓ fine. Note: `Math.sign(sl||0.001)` — if sl===0 sign(0)=0 → no push; use (sl>=0?1:-1). Fine. stampCrust(x, gy, r, heat, alpha): ```js function stampCrust(x,gy,r,heat,alpha){ const band=clamp(Math.round(heat*11),0,11); const dw=Math.max(4, r*5); cctx.save(); cctx.beginPath(); cctx.rect(x-dw, gy-3, dw*2, dw*3+40); cctx.clip(); cctx.globalAlpha=alpha; cctx.drawImage(lavaSprites[band], x-dw/2, gy-r, dw, dw); cctx.restore(); cctx.globalAlpha=1; } ``` Wait the blob center should be near surface: y center = gy - r*0.6; I set gy - r. OK. Clip rect top at gy-3 → only 3px above line visible ✓. Hmm but rect starts at x-dw... width dw*2 ensures coverage. Fine. Crust fade timer: crustT += dt; if>0.4: crustT=0; cctx.globalCompositeOperation='destination-out'; cctx.fillStyle='rgba(0,0,0,0.03)'; cctx.fillRect(0,0,W,H); restore to 'source-over'. With save/restore of transform? cctx transform set once; fillRect covers W,H ✓ (transform scale DPR maps to full). Note save/restore not needed for composite op; reset manually. Ash update loop: ```js let i=0; while(i=AMAX[i]||AX[i]<-160||AX[i]>W+160||AY[i]>H+80){ aKill(i); continue; } const heat=Math.max(0,1-AAGE[i]/1.4); AVY[i]+=(46-heat*105)*dt; AVY[i]+= -AVY[i]*0.5*dt; AVX[i]+= (windA*2.7 + Math.sin(time*1.7+ASEED[i])*24 + (Math.random()-0.5)*36)*dt; AVX[i]+= -AVX[i]*0.55*dt; AX[i]+=AVX[i]*dt; AY[i]+=AVY[i]*dt; i++; } ``` Hmm drag via -v*k*dt linear approx: fine. Spawn during phases: ```js // eruption spawn if(phase==='erupt'){ const u=Math.min(1,eruptT/eruptDur); const pressF=1-u*0.85; // tapers const pulse=0.5+0.5*Math.sin(eruptT*5.4)+0.25*Math.sin(eruptT*9.7); // clamp const pm=clamp(pulse,0.12,1.6); const spike = eruptT<0.45? 2.2:1; lavaAcc += lavaRate*pressF*pm*spike*dt; ashAcc += ashRate*(0.35+0.65*pressF)*clamp(pulse,0.3,1.3)*dt; } else { const trick=(1+press*0.10)*2.2; // buildup trickle: press 100 → ~24/s ashAcc += trick*dt; } lavaAcc=min(lavaAcc, 60) cap; while(lavaAcc>=1){lavaAcc--; spawnLava();} similarly ash. ``` lavaRate = 60 + Math.pow(density/100,1.15)*1450. ashRate = 26 + density*3.0. Muzzle & shock handled in startErupt. Vent glow render: ```js let glowA=0, glowR=0; if(phase==='erupt'){ glowA=0.75; glowR= 150+power*1.4 + muzzle... } else { glowA=(press/100)*0.55*(0.75+0.25*Math.sin(time*6.3)); glowR=40+press*1.1; } if(muzzleT>0){ draw glow sprite big: s=320*(1-muzzleT/0.4)^?...} ``` Manage muzzleT: set 0.4 on erupt start; decay. Shock ring: shockT set 0 on erupt; if shockT<0.9: r = 30+ shockT*520; alpha=(1-shockT/0.9)*0.6; stroke circle at (craterX, mouthY-20). Also draw crater interior glow: lighter glow sprite at === answer so far ===