### PARTIAL OUTPUT (time limit hit; the model was still generating) ### === thinking so far === Let me design this carefully. Single HTML file, canvas-based volcano eruption sim with: - Side view: volcano cone with crater, ground, sky - Pressure meter building over time; maxes → eruption; resets and rebuilds - Lava particles launched from crater with varied velocities, gravity arcs, land on slopes, slide/settle downhill - Lava cools: bright yellow-orange → dark rock → disappears - Ash/smoke cloud rising from crater, drifting with wind - Three sliders: eruption power, wind strength/direction, particle amount - Live counter: active particles + FPS, smooth at 1000+ particles Performance considerations: 1000+ particles at 60fps. Use canvas 2D with simple fills. For lava particles, drawing circles with fillRect/circle is fine. Use additive composite for glow on hot particles? Switching composite states per particle is expensive — batch: draw all hot particles under 'lighter' composite, then cool ones normal. Or draw everything normal with gradient-free simple circles. Precompute color via a cooling lookup table (LUT) indexed by "heat fraction" to avoid per-particle color math. Use ImageData? Might be simplest: use small "sprite" circles via fillRect with pre-scaled? Actually drawing 1000 arcs per frame is fine on modern browsers (~1000 arc calls). But with glow, use globalCompositeOperation 'lighter' for hot ones — group them. Better perf approach: render lava particles as points using a pre-rendered sprite canvas (small radial glow sprite) drawn with drawImage — very fast. Have a few sprites for heat levels (heat LUT sprites: e.g., 8 sprites from white-yellow → orange → red → dark → gray-black). drawImage 1000+ times per frame is fast. Ash particles: soft circles, semi-transparent gray. Draw with 'source-over' with low alpha; maybe also sprites for softness. Use a soft blob sprite (radial gradient) pre-rendered — that's fine since it's an asset, not decorating. Terrain: generate a profile (array of heights) for a volcano cone with crater at top (concave dip at summit). Ground baseline. Collision: particle lands when y >= terrain(x). Then slides downhill: velocity along slope + friction, or simple "settle" — when speed drops, become "settled" particle that stays until lifetime ends. Sliding downhill: compute slope gradient at particle x, if slope steep enough, apply gravity component along slope. Also particles landing on inner crater walls could slide back into crater? Nice realism: particles on inner crater slope slide back down into the crater and get re-melted / fall into vent. That's a nice touch. Keep simpler: terrain function gives height at x; slope = (h(x+d)-h(x-d))/2d. For a particle on ground: apply downhill accel = g * slope factor (component), friction reduces horizontal. If particle reaches crater bottom, it can "die" (absorbed into vent) — could respawn pressure bonus? Maybe small pressure bump when lava falls back in. Keep it: absorbed lava adds a bit of pressure — fun feedback loop. Actually maybe too much; keep absorbed lava just disappears with a small flash. Eruption mechanics: pressure builds over time (rate maybe influenced by power slider? Pressure build could be constant-ish; power affects eruption strength & duration). When pressure reaches 100, eruption starts: over a duration, launch bursts of particles each frame from crater vent with velocity influenced by power slider, plus noise. During eruption pressure drains to 0, then rebuilds. Also allow "trigger now" — no, user didn't ask; skip. Maybe click canvas to add pressure? Not requested; keep it clean but add small extra: clicking canvas pokes a "premature vent" small burst? Could be a nice interaction. The requirement says sliders while it runs. I'll add a subtle click-to-stir bonus: clicking the canvas injects pressure surge — fun and discoverable. Mention in explanation. Sliders: 1. Eruption power (affects particle speed, eruption intensity, maybe pressure build rate slightly) 2. Wind (slider from -X to +X: strength & direction; visual wind vane? Show arrow indicator maybe. The HUD shows value.) 3. Particle amount — controls particles per eruption burst / emission rate (scaled), so user can push counts up. Counter HUD: "PARTICLES: n | FPS: xx". Show FPS computed with smoothing. Also show active count target note. Put HUD top-left or top-right in a corner, monospace-ish styling but not Inter/Roboto/Arial — use a distinctive font from Google Fonts via CDN (allowed). Use something like "Barlow Condensed" or "Space Grotesk"? For a geology/monitor feel maybe "IBM Plex Mono" for numbers + "Big 下面想想架构上面的东西继续。 Actually stop tracing the source; I'm composing my own reply. Let me plan the design: Visual concept: dusk sky (deep teal/indigo? No default blue-purple... hmm. Palette: I want a volcanic dusk — deep charcoal sky with warm horizon glow? Careful: gradients — decorative gradients are out, but sky gradient is legitimate atmosphere rendering, a natural part of a scene, not decoration. A canvas sky needs a gradient to look right; that's diegetic rendering, not UI ornament. I'll keep UI itself flat and precise, gradients only inside the canvas scene where they represent actual sky/light/smoke.) Sky: night-to-dusk feel. Let's do a dusk: deep slate-green/black at top → burnt amber at horizon. Stars? Maybe a few faint stars. Sun? No. Silhouetted terrain: volcano dark rock against glowing horizon. Lava glow lights the smoke. Terrain rendering: fill terrain silhouette as dark rock with subtle lighting from eruption (glow overlay near crater when hot). Ground foreground dark. Volcano cone with crater dip. Camera: fixed full view; canvas sized to window, terrain generated in world coordinates ~ width. Use device pixel ratio for crispness. World coordinates: I'll define world width = canvas width in px, terrain profile computed from function of x. Simpler: terrain as height array per pixel column. Generate with noise: cone centered at cx, height hBase(x): cone rises to summit at cx with crater dip. Add gentle noise for slopes. Precompute array groundY[x] for each integer x (and slope via neighbors). Particles outside array bounds → clamp/kill. Terrain shape: base ground at H*0.86. Cone: rises from radius R= maybe 0.42*W from center, peak height ~ H*0.42 above ground. Crater: dip at summit width ~ 70px, depth ~ 40px. Slopes: use smooth superposition: coneY(x) = ground - peakH * profile where profile uses a smooth function like max(0, (cos of angle)). Let me think: distance d = |x-cx|; base shape: t = clamp(1 - d/R_outer, ...). Use smoothstep-ish for concave? Real volcanoes are slightly concave. Use profile = pow(max(0,1-d/R),1.5)?? That gives concave slopes (steep near summit). Then crater: subtract a dip: dip = craterDepth * smooth bump at center: dipH * max(0, 1 - (d/cw))^2 with smoothing. Ensure crater walls slope inward so particles slide into vent. Add small noise: h += noise(x)*amp*(d/R) scaled, using value-noise via seeded pseudo-random with smoothing (precomputed array so just generate once). Particle physics: Lava particle: pos x,y; vel vx,vy; heat (1 → 0), life. Launch: at crater vent position (cx, craterBottom or summitY). Actually launch from inside vent: y = crater floor. Give vy upward strong (e.g., -(3.5 + rnd*power)), vx = (rnd-0.5)*spread + wind*windFactor. Bursts each frame during eruption: emitCount = base + particleAmount slider scaling. Varied velocities: cone-shaped distribution: mostly upward with angular spread — v = speed * (dir with random angle around -90° ± spread), speed = power*(base ± rand). Use speed in px/frame scaled by dt. Use fixed timestep physics with dt = 1 (per frame at 60fps) but scale by elapsed to be framerate independent: dt = min(elapsed ms, 33) / 16.67 factor. Velocities in px per 16.6ms. Gravity: vy += G*dt (G ~ 0.35 px/frame²? Tune: for height 300px arc with vy0 ~ 9 px/frame, apex ~ vy²/(2g) = 81/0.7 ≈ 115px... need bigger. Let G=0.5, vy0 up to 12 → apex 144px. Volcano peak ~ 300px above ground; particles should arc above summit and fall on slopes. Speeds: power slider maps speed 4–16. Let's tune: G = 0.42. vy0 = 6–14. vx spread ±5 plus wind push. Looks fine; will tune mentally.) Wind affects airborne particles: vx += wind*dt*0.15 (wind slider -10..10 maps to accel -0.4..0.4). Ash gets stronger drift. Collision with terrain: if y > ground[x]: land. Compute slope s = (ground[x+2]-ground[x-2])/4 per px (dx per px). Downhill direction: downhill accel along slope: for terrain, "downhill" means moving toward lower ground: a_x = -s * k * g? Actually on a slope, gravity component along surface: a along = g * sin(theta) where tan(theta)=s... approx a_x = -g * s / (1+s²) ~ -g*s. Sign: if ground increases with x (rising to the right, s>0), downhill is to the left (negative x) → a_x negative proportional to -s... wait: if s>0 terrain rises rightward; downhill direction is -x. Accel a = -g*s (approx) → negative → moves left. Yes: a_x = -g * s * slideFactor. Friction: multiply vx by (1 - fr) each frame; also "bounce": small restitution: vy landing → set vy = -vy*0.25 * if speed big, else slide. Plus a vertical snap to terrain. Settling: when |vx| < threshold and landed and slope small: particle becomes settled → velocity 0, stays, heat continues to cool, fades/darkens then disappears. Also cap settled particles count? With 1000+ target, many settle; that's fine — they count as active while alive. Settled particles still need rendering (dark rocks scattered on slopes — cool!). Lifetime after settle: they cool to dark and disappear (sink). To avoid unbounded accumulation, settled lifetime limited ~ few seconds after heat is gone? Requirement: "fading to dark rock, then disappearing." So heat 1→0 over life; color LUT from heat; when heat hits ~0 they're dark rock; keep dark rock visible for a while (cooling ember slowly) then fade out (shrink / alpha). Total life maybe 6–10 seconds + settle-dependent. Also to keep count bounded under heavy eruption + high particle slider: cap emission if active > maxCap (slider sets cap 500–3000). Requirement at least 1000 smooth; default target maybe 1200 during eruption. Let slider "particle amount" set emission rate; cap hard at e.g. 3500. Also absorbed: if particle slides into crater vent region (x within vent width and y > craterFloorY) → absorbed: remove, tiny flash, small pressure +? I'll add pressure nudge +0.5 per absorbed chunk ("recycled") — subtle, and gives a self-sustaining feel. Hmm requirement says pressure builds on its own; recycled heat adds spice. Keep small so it doesn't runaway: add 0.4 pressure per absorbed lava with heat>0.3. Cap effect. Ash/smoke particles: spawn at crater during eruption (and lightly during pressure build — pre-eruption fumarole wisps: nice detail). Smoke: rises (vy negative small), buoyancy, drifts by wind, expands (radius grows), fades. Color: dark gray with alpha; when eruption hot, smoke near vent gets glow tint (draw a hot glow under). Smoke count ~ 150–400. Render smoke as big soft sprites — expensive if huge draws; keep max radius ~ 60px, use sprite drawImage scaled. 250 smokes with drawImage scaled 60px = fine. Smoke lighting: when eruption active, smoke near crater tinted warm (orange-ish) — do via drawing glow sprites under 'lighter' for a subset: draw hot smoke sprite (radial warm) then gray on top? Simpler: smoke sprite colored per-heat: LUT sprites from warm-lit gray → dark gray. Pre-render e.g., 5 gray levels + 3 lit levels. Crucible/vent glow: during pressure high, crater emits glow (red pulsing). Draw a glow sprite at vent with pulsing alpha proportional to pressure. Ground illumination: eruption light — draw radial glow centered at vent with 'lighter' when pressure high or eruption occurring. Lava rendering: sprite LUT. Heat levels: - 1.0: near-white yellow core (hot) - 0.8: bright orange - 0.5: deep orange-red - 0.25: dark red ember - 0.1: near-black rock w/ faint red - 0: dark gray rock, fading alpha Sprites: 64px canvas with radial gradient (center white-hot → transparent) per level. Particle radius varies (2–5 px draw scaled). Hot particles also get trails? Trails via drawing line from prev pos — could add motion streaks: draw a short line with strokeStyle per heat (batched by heat bucket with beginPath per bucket). That's nice: streaks for lava bombs. Batching: group by heat bucket (6 buckets): per bucket set strokeStyle, draw all streaks in one path. Plus sprite circles on top. Or just draw sprite with slight elongation? Streak lines are easy and fast (1000 line segments). Do: for hot particles (heat>0.05) draw streak from (x - vx*2, y - vy*2) to (x,y), lineWidth = radius. Then small sprite glow on hot ones only (heat>0.35) with 'lighter' batched (set composite once, draw all, restore). Actually to keep it simple & fast: - Pass A ('lighter'): for particles heat>0.3: drawImage(glowSpriteLUT[heatIdx]) at pos, scaled. - Pass B (normal): streaks for heat>0.05 via lines with dark-warm colors, and dark rocks via sprite drawImage normal composite (gray sprite). Or draw rocks as fillRect with alpha for cheapness: rect fill with rgba dark. Use sprite anyway. Settled rocks: they form piles — draw them under? Fine as-is. Order of drawing per frame: 1. Sky (gradient + stars? maybe faint stars static drawn into an offscreen "backdrop" canvas pre-rendered once: sky gradient + stars + distant clouds? Pre-render background once to offscreen, then each frame drawImage it — fast). 2. Terrain silhouette: also pre-render into backdrop? Terrain must be lit by eruption glow... glow drawn as overlay over terrain, so terrain can be pre-rendered too. Pre-render sky+terrain+ground into one backdrop canvas at init (and on resize). Each frame: drawImage(backdrop). Then smoke BEHIND lava? Smoke rises from crater behind or in front? Smoke should be drawn behind volcano? Actually smoke comes from crater — in front of crater but behind lava. Draw smoke first (over backdrop), then terrain? No: terrain already in backdrop. Smoke plume rises above crater into sky → drawn over backdrop fine. But smoke at vent should appear in front of crater walls: yes drawn after backdrop = in front. Good. Lava particles drawn after smoke (front). Plus eruption glow at vent beneath smoke? Draw glow before smoke so smoke is lit silhouette... Simpler: draw vent glow (lighter) → smoke → lava. Fine. 3. Terrain foreground details: some rock texture? Keep silhouette minimal with subtle ridge highlight. Maybe add scattered dark boulders pre-rendered. Ash behavior: spawn at vent with upward velocity -1 to -3, vx wind*0.05 + small random; vy increases upward (buoyant: vy -= buoy*dt until terminal ~ -1.5?) Actually make smoke rise: vy starts -2 to -4 (fast initial jet), then buoyancy decays toward gentle rise: vy += (targetRise - vy)*0.02... Simple: vy = vy*0.96 - 0.08*dt (rising). vx += wind*0.2*dt + turbulence noise (per-particle random walk via sin(time+phase)*0.1). Radius grows: r += 0.25*dt up to cap. Alpha: starts ~0.5, fades as age → alpha = a0 * (1 - age/life). Life ~ 3–7 s. Big smoke cloud drifting. Also during eruption, spawn lots; after eruption ends, spawn decays. Also spawn smoke when pressure > 70 (pre-eruption fumol) small rate. Turbulence: wind slider changes; existing smoke drifts with current wind each frame — good ("drifts with the wind" live). Pressure mechanics: pressure 0→100. Build rate: base 100/12s → rate ~ 0.5/s *? Let's make build time ~10–16s with slight random wobble and slight dependence on power (higher power → faster build? Independent is fine). Also add small sin pulsing for meter life. When reaches 100 → state ERUPTING: duration ~ 3.5–5s scaled with particle slider? During erupting: pressure drains linearly to 0. Emit bursts each frame: n = emitRate*dt where emitRate = slider-scaled (e.g., 60–260 per second). Also muzzle flash. When pressure hits 0 → state BUILDING. Add slight post-eruption lull (pressure stays 0 for 1.5s). Meter UI: I'll draw a custom meter as canvas-drawn? Better: DOM element — a vertical or horizontal gauge styled carefully (flat, precise, industrial: thin bar with tick marks, needle?). Design: top-left panel: "VENT PRESSURE" with a horizontal segmented bar (like a VU meter with warning zone at the end colored). Since it's a canvas scene, DOM overlay on top with CSS. Style: dark translucent panel, hairline borders, uppercase labels, tabular numbers. Font: use CDN Google Fonts: "Archivo" + "IBM Plex Mono"? I prefer a technical feel: "Space Grotesk" is overused-ish? It's fine but let me pick: headings "Barlow Condensed" (industrial, uppercase tracking) and numbers "IBM Plex Mono". Palette: near-black UI panels (#0d0b09 / rgba), warm amber accent (#f5a623-ish / lava orange #ff8a3c), text warm off-white #f2ede4. Accent red for warning zone. Sliders: custom-styled range inputs (accent color, thin track). Three: POWER (0–100), WIND (-10..+10 or -100..100), PARTICLES (emission rate / cap). Live value readouts next to each. Wind slider should have center-notch? Just show signed value like "+3.2 / −1.0". HUD counters: bottom-left or top-right: "PARTICLES 1247 · FPS 60" in mono, small. Maybe also show state: "BUILDING / ERUPTING". Nice: status word. Signature moment: the eruption itself — a shockwave? Add a subtle screen-space "boom": brief flash + slight camera shake on eruption start, plus a launch "flash" — memorable. Camera shake: translate canvas draw by decaying random offset for ~0.5s. Also an initial mushroom column: lava fountains + smoke billowing. Also a subtle detail: lava landing creates tiny spark sparks? Keep perf. Maybe skip sparks. Add "impact puffs": when lava particle lands with speed, spawn 1 tiny smoke puff (small, cheap) — only for fast ones and limited (probability). Cap smoke count. Another alive detail: crater lake? no. Pre-eruption rumble: ground tremor before eruption (shake amplitude grows as pressure > 85) — nice suspense. Also meter needle jitter at high pressure. Sound? Not requested; skip (autoplay audio requires interaction anyway). Keep click-to-stir: click adds pressure surge (+ small lava puff). Also keyboard? no. Structure the code: - const canvas, ctx, DPR handling; W,H logical size. - Terrain: ground[] Float32Array length W (in CSS px? use logical pixels W = innerWidth, DPR scaling via ctx.scale). Compute at resize. - Backdrop offscreen canvas rendered at DPR. - Sprites: makeGlowSprite(size, colorStops) → offscreen canvas. LUT arrays for lava heat (N=8), smoke (gray levels N=5 + lit), glow (vent glow, impact flash). Lava heat sprite color mapping: I'll craft stops: heat 1: core #fff7d6 → mid #ffb347...? Sprite radial gradient: stops: 0: rgba(255,246,214,1) 0.25: rgba(255,196,64,0.9) 0.6: rgba(255,90,20,0.35) 1: transparent. For LUT simpler: define per level arrays of [inner color, outer color] and generate sprite. Levels (heat→ index 0..7 where 0 hottest): 0: white-hot: #fffbe8 → #ffd96a 1: #fff3b0 → #ff9a2a 2: #ffc354 → #ff5f16 3: #ff7a2e → #e33418 (deep orange-red) 4: #d53f1a → #6e1608 (ember) 5: #7a2410 → #30130a (dark ember) 6: #3c2a22 → #241a15 (rock) 7: #33231c → rgba fade (dying) Rock appearance after cooling: dark basalt. Good. Physics constants (px per frame-step where step=16.7ms): G=0.35? Let's compute with terrain ~ height: ground y ≈ H*0.78 (H ~ 700 logical?). Let's assume H≈700. Ground at 550. Summit at 550-380=170? Peak height 380 → summit y=170. Crater floor y=210ish. Particle launched at y=200 with vy0=-14, G=0.35: apex Δ = 14²/(2*0.35)=280px → apex y=-80 (above screen, fine, dramatic). Horizontal: vx 0–8, flight time up ~40 frames, total ~80 frames, horizontal distance up to ~ 8*80=640px. Slopes at R=380: fine. G=0.35, dt scaled per frame. Emission: during eruption each frame: emit = emitRate * dtFrac (emitRate from slider 40–240/s). Each particle: angle from vertical: θ = -90° + (rand-.5)*2*spread where spread ~ 35°+power*... Actually fountain-like: mostly up with spread. speed = (6 + power*10) * (0.75 + rand*0.5). vx = cos, vy = -sin? Let angle a measured from up: vx = sin(a)*speed, vy = -cos(a)*speed. Plus vent jitter vx += wind*0.3. Also lava "blobs" vs spray: vary radius: mostly small (2–4px), few large (5–8px) — large = bombs that go far. Radius affects nothing physically (same G) fine. Landing: when y > ground[x] - r? snap: y = ground[x]; if vy > 2: bounce vy = -vy*0.3, vx *= 0.6, maybe spawn puff; else grounded=true. When grounded: each frame: compute slope s from ground array (sample x±3). vx += -s * slideAccel * dt; slideAccel ~ 0.5? If slope magnitude |s|>0.25 and |vx| < 0.3, give kick (unstick if steep). friction: vx *= pow(0.86, dt)? Use vx *= 1 - 0.12*dt. If |vx| < 0.05 && |s| < 0.08 → settle (grounded, stuck). Settled particles still cool. While grounded & moving, y = ground[x] (stick to surface). Also downhill into crater: vent absorb if x in [cx-ventW/2, cx+ventW/2] and y > craterFloorY - 4 → absorb (remove; pressure += 0.35 if heat>0.4, spawn tiny spark puff? maybe tiny smoke). Also particles rolling off screen edges (x<0 or >W): remove. Also particles may land on slopes and slide downhill along the cone → pool in crater or on flanks; that fulfills "slide or settle downhill". Heat cooling: heat -= coolRate*dt where coolRate ~ 1/(life 5–9s) with per-particle variation (rate = 0.05–0.09 per frame-step? 1/60s units: heat from 1 to 0 in ~ (0.06/frame * 60fps)=1.66s? Too fast. Want ~6s: rate ≈ 1/(6*60) = 0.0028/frame-step. With randomness 0.002–0.005. When heat <= 0.05 and settled, extend: after heat 0, keep rock visible with fade alpha over ~3–5s (alpha = fade life). Implement life2: when heat<=0 → start fadeTimer (2.5–4s), alpha decreasing; remove when done. If still airborne when heat 0 (rare), continues ballistic dark rock. Wind: slider -10..10 → windAccel = wind * 0.035 per step for lava airborne (gentle) and smoke *0.25. Also wind shifts smoke plume sideways strongly. Also slight lava vx drift even grounded? no. Particle amount slider: controls emission rate AND smoke rate AND cap. Show as "SPRAY RATE" or "PARTICLE OUTPUT". Range 50–300%? Implement emitRate = 40 + slider*? Let's set slider 0–100 mapping emitRate = 30 + v*2.4 (30–270/s). Cap = 800 + v*12 (800–2000)? For "at least 1000 active": with slider at mid (50 → 150/s eruption ~4s → ~600 + smoke... hmm need 1000 active). Eruption duration 4s at 150/s = 600 lava particles; plus lifetime ~7s means at steady state with period ~ 14s build + 4 erupt: active count at eruption end ~ 600, then decaying. To show 1000+, default slider higher: default 70 → emitRate ~ 200/s * 4s = 800 lava + smoke 250 ≈ 1050. But requirement "stay smooth with at least 1000 active particles" — my default should reach 1000+. Let me tune: default slider 65%, emitRate = 30+2.9*v → 30+188=218/s? duration 4.2s → 915 lava. Plus lingering... During eruption, count includes both lava+smoke; show combined count. Make emission math: emitRate = 30 + v*2.8 → at v=65: 212/s. Eruption duration: 3.8s → 800 lava spawned in burst, life up to 8s → during eruption ~800 lava + ~200 smoke = 1000+. Good. Also make cap generous 3000; skip emission when count > cap. FPS: measure via rAF delta EMA; display rounded, update text every 250ms (avoid layout thrash). Counter: count = lava.length + smoke.length (+settled included in lava array). Update DOM text 4x/s. Meter: DOM: container with bar; I'll build the meter as a horizontal track with fill div width % via transform scaleX (cheap). Add tick marks via CSS gradient? Avoid decorative gradients... ticks can be repeating-linear-gradient — that's a pattern, functional (measurement ticks), acceptable? I'd rather avoid; use small divs? Use CSS repeating-linear-gradient for hairline ticks is a functional gauge pattern... I'll draw ticks with box-shadow? Simplest: a few absolutely positioned thin divs at 25/50/75/100%. And warning zone 85–100% tinted (rgba red). Needle marker line. Fill color shifts from amber to red as pressure>80 (swap class). Jitter: at high pressure, add tiny random translate to fill via inline style? Keep it subtle: skip jitter on meter; ground shake suffices. Status label: BUILDING / ERUPTING / UNSTABLE(>85)? Status text under meter. Layout: - Top-left panel (fixed): title small "STRATOVOLKAN · 9" + "VENT PRESSURE" meter + status. - Top-right: stats: PARTICLES n / FPS xx / plus maybe SIM time? Keep two. - Bottom-left or bottom-center: controls panel with 3 sliders (labels + values). Bottom-left. - Hint text: "click the terrain to agitate the vent" small, bottom-right. Panels: rgba(10,8,7,0.55) with backdrop-filter blur? blur is fine, but backdrop-filter over canvas can be perf-heavy — it's only small panels, ok. Use subtle 1px border rgba(255,240,220,0.14), no rounded-blob; small radius 2px, hairline. Uppercase micro-labels letter-spacing 0.14em, 10px. Values in mono. Fonts: link Barlow Condensed (600) + IBM Plex Mono (400/500) via Google Fonts CDN. If CDN fails, fallback stacks. Canvas scene visuals (backdrop): - Sky gradient: top #0b0e14? Hmm palette: dusk volcano: deep ink at top → smoky teal? Let's design: top #07070c → mid #1a1418 → horizon #4a2b20 → hot line #8a5a33? Warm dusk. Stars: ~90 tiny dots alpha varying, only in upper 60%. - Also a moon? A dim moon disc could be nice but keep scene focused; maybe a low pale moon at right with haze — adds composition anchor. Small, subtle. Sure: moon at (W*0.82, H*0.22), r 26, color #d9cfc0 alpha .9 with slight halo. It's diegetic. Nice. - Terrain: volcano silhouette dark: fill color near-black with warm tint #14100d; ground plain #0f0c09. Rim highlight: 1px lighter edge along slope facing horizon glow (draw stroke along terrain path with warm rim #6b4a33 alpha .5 — subtle). Also crater inner wall slightly lit by vent glow (dynamic glow handles). - Foreground ground with a few grass-less rocky speckles: pre-render noise dots. - Optional distant secondary hills on horizon sides: low rolling ridges left/right behind volcano: adds depth. Yes: two gentle hills at horizon (silhouette slightly lighter #1b1512). Vent glow dynamic: sprite radial warm drawn at vent with alpha = pressure^2 * pulse, 'lighter'. Also eruption flash on start: big radial flash alpha decaying 0.3s + shake. Camera shake: shakeT decays; offset = shakeT * random * 14 px applied via ctx.translate before drawing scene (drawImage backdrop with offset — fine). Let me also make sure resize regenerates terrain & backdrop (particles: recompute ground reference? ground array indexed by x integer; on resize recompute; particles' y may mismatch → they'll resnap on landing; acceptable; also reposition crater-based stuff). Terrain function detail: - cx = W*0.5 (volcano center maybe W*0.55 to leave left sky open? center 0.52 fine.) - baseY = H*0.80. - peakH = H*0.36? summit y = baseY - peakH. - R (base radius) = min(W*0.34, H*0.62)? Ensure cone fits with slope ~ 45°? With W=1280,H=720: R=435, peakH=259 → slope avg 0.6 (~31°), concave steeper at top. OK. - profile(x): d=|x-cx|/R; if d>=1: 0 else base = (1-d)^1.6? That's peak 1 at center. Cone shape: use pow(cos(d*π/2 * something))... simpler: base = (1 - d)^1.5 gives straight-ish concave? f(d) = (1-d)^1.5: f'(d) = -1.5(1-d)^0.5 — slope magnitude decreases as d→1?? Wait f'(0) = -1.5 steep at top?? At d=0 slope -1.5*1 = -1.5 steepest near summit, flattening outward — that's the concave cone (steep peak). Yes concave volcanoes are steepest at summit. Good: h = peakH * (1-d)^1.6. - Crater: cw = 55 (half-width vent), dip: dipH = 46; dip(d) = dipH * (1-(d/cw)^2) smoothed for |d| craterFloorY. Hmm wait: crater dip width 55 half → crater opening 110px wide; particles arcs from vent with vx ±5 will exit opening mostly. Sliding particles on inner walls go to floor... then what? On floor, slope ~flat → settle → then they're in crater pile. They'd accumulate forever. Add: lava in crater melts/absorbs after a moment (heat returns?): absorbed → removed with pressure bonus. Rule: particle inside crater (|dx| 0.8s → absorbed (sinks). Gives recycling. Implement via groundedTime counter. Or simpler: if inside crater zone and grounded → absorb immediately on landing+settle? Immediately absorb on becoming grounded inside crater. OK. Smoke spawn: at vent, plume: pos (cx + rnd*16, ventY - 10 - rnd*30) rising column; vy = -(1.5 + rnd*2.5) * ? in px/step; buoy reduces: vy += 0.02*dt (slowing upward speed: since vy negative upward, adding positive reduces) → until vy ~ -0.2 terminal? Add slight rise maintain: vy = max(vy + 0.03*dt, -0.4)? Let's: each step vy = vy*0.985 + (-0.05)*dt? That accelerates upward forever — no. Want: strong exit velocity decays to gentle rise: vy_target = -0.6 + wind vertical? Keep: vy *= exp(-0.02*dt) then vy -= 0.02*dt capped at -1.2: vy = max(vy*0.985 - 0.02*dt, -1.1). Fine. Smoke radius: r0 6–10 → r += 0.35*dt capped 55. Alpha 0.35 → fade by age. Life: age 240–420 steps. Kill when alpha<=0.02 or y < -100. Smoke color: base gray with warm rim? Sprites: level by "lit" factor = eruption heat near vent (distance-based) — I'll compute lit = clamp(( (eruption?1:pressure/130) ) * (1 - age/60)) simpler: smoke born during eruption is warm-tinted, older cool. Tint factor t = clamp( (age/life) reversed * eruptionActive ...). Simplify: smoke sprite LUT: levels 0..4 from warm-lit (#b98a63 based) to dark (#57504e). Choose level from (1 - age/life) * warmth where warmth = eruption intensity at spawn stored per particle (born hot → gradually cools). Store warm0 per particle decaying: warm -= 0.004*dt... Let particle.w (0..1): lit level = w * (1 - age/life*0.5)? Keep simple: level = round(w * 4) where w decays: w *= (1-0.01*dt)? Then plume gradually darkens as drifts — matches "cools" theme. Also alpha low so overlapping builds cloud. Smoke render: composite normal, alpha = particle.a * globalSmokeAlpha. drawImage sprite scaled 2r. ~250 draws fine. Also beneath everything lava glows light the smoke? skip (they're drawn after smoke anyway — glow sprites with 'lighter' will brighten smoke pixels beneath — nice, automatic!) Order: backdrop → ventGlow(vent, 'lighter') → smoke → lava streaks/rocks → lava glow pass ('lighter') → impact flashes → foreground vignette? Subtle vignette: darken corners (pre-render into backdrop? but shake offset shows edges... make backdrop slightly larger? draw backdrop larger than canvas by 40px margin? Simpler: vignette drawn as overlay last with normal composite radial dark alpha 0.25 corners — one drawImage of pre-made vignette. But shake moves nothing since overlay fixed. ok.) Terrain in backdrop includes ground+volcano+hills+sky+stars+moon+vignette? Vignette as separate last layer after particles (dark translucent radial) so particles also dim at edges? Lava in corners dimmed slightly — fine, atmospheric. Draw vignette overlay last. But vent glow 'lighter' over terrain: crater inner walls behind glow? Since glow drawn after backdrop, it brightens crater wall pixels — good, crater looks lit. Perf details: everything drawImage from offscreen canvases; particle loops O(n). GC: avoid per-frame object alloc (reuse arrays, plain objects; use splice-swap removal). Use arrays with swap-remove. Particles count display includes settled — fine. Let me now also think "eruption" emission pattern: rather than constant rate, use pulses: rate multiplier = base * (0.6 + 0.9 * pulseNoise(t)) with pulsing bursts (perlin-ish via sum of sines) — gives fountain surges. And start of eruption: initial violent burst (2x for 0.4s). End: tapering. Pressure build: rate = 100 / (11 + 4*rand per cycle) per sec → but also slight sinus wobble. Show %, maybe with one decimal? Show integer. During eruption, pressure drains: p -= 100/duration * dtFrac *dt. Duration ~ 4s base + particle slider influence? If emission huge, maybe extend duration slightly: duration = 3.2 + emitRate/140 → v=65: 3.2+ (186/140=1.33) ≈ 4.5s. ok. Also "premature vent": click → pressure += 25 (clamped) and small burst of 40 particles + flash small. Also could add: click anywhere adds pressure scaled. Keep. FPS smoothing: fps = 0.9*fps + 0.1*(1000/dtms). Display Math.round. dtFrac: dtms clamp 0–50; dt = dtms/16.667. Physics uses dt multiply; with variable dt and high velocities + terrain sampling could tunnel at low fps: at 30fps dt=2 → vy 14*2=28px per frame — could skip terrain? ground sampling per particle at new position catches since we check y>ground[x] after move with new x — tunneling only if jump past ground into below-ground and then... we clamp to ground at current x, fine even if 28px jump. Ok. But wind & G fine. Also cap dt to avoid spiral: clamp dtms ≤ 42 (dt ≤ 2.5). Collision robust: after integration, sample x index i = clamp(round(x)); if y > ground[i] → handle. Also particles outside [0,W): remove (or clamp bounce? remove). Structure code: ``` const TAU=Math.PI*2; function makeCanvas(w,h){...} sprites: function glowSprite(size, stops) → canvas with radial gradient stops. ``` Sprite LUT definitions: LAVA sprites (8): each defined by inner rgba & mid & outer: ``` const LAVA_STOPS = [ ['255,247,224','255,224,138','255,170,64'], ['255,236,180','255,190,90','255,120,40'], ['255,204,120','255,140,55','245,80,25'], ['255,150,70','240,90,35','200,45,20'], ['225,80,30','140,35,16','80,25,14'], ['120,45,25','60,25,16','35,18,12'], ['70,50,40','45,32,26','30,22,18'], ['55,40,34','40,30,26','28,22,20'], ]; ``` Sprite: radial: 0: inner a=1; 0.35: mid a=0.85; 0.75: outer a=0.3; 1: a=0. Hmm dark rocks shouldn't glow — for levels 6–7 draw flat-ish (still radial but dark, low alpha outer). Rock sprite should be opaque-ish dark disc with soft edge: use stops with alpha 1 at 0..0.7. I'll write generic: each level config {stops:[{o,c,a}...]}. Easier: function makeSprite(stops, size=48). Actually simplest: define per-level an array of gradient stops as strings 'rgba(...)'. I'll hand-write: ``` function radialSprite(s, stops){ // stops: [ [offset, r,g,b,a], ... ] } ``` Build lavaSprites[8], each size 64? 64*8 canvases fine. Draw scaled to r*2 (r 2–7 → scaled 4–14px; sprite 64 scaled down fine). For hot glow pass I'll reuse same sprite drawn with 'lighter' — dark levels under 'lighter' add nothing (near zero) so safe to just draw all in one 'lighter' pass?? 'lighter' adds color — dark rock adds ~nothing → invisible?? Under 'lighter', a dark sprite drawn over terrain adds its RGB — a dark rock would appear as slight dark? No: 'lighter' only adds; dark gray (30,25,22) adds slight warm tint — rock would look like faint warm smudge, not dark rock. So rocks must be drawn with normal composite. So: two passes: normal pass for levels ≥ 4 (cool ones) with alpha 1; lighter pass for levels ≤ 3 (hot). Plus streaks. Streaks: for particles with speed > 1.5 and heat > 0.05: line from (x-vx*2.5, y-vy*2.5) to (x,y), stroke color by heat bucket (dark-red to bright). Batch: buckets by heatIdx 0..5: for each bucket beginPath, add segments, stroke with color[i], lineWidth per? lineWidth varies per particle (radius) — varies; could set lineWidth per segment? Expensive. Use single lineWidth 2.5 for streaks and rely on sprite for thickness. One pass: all hot streaks bright colors differ per bucket: 5 strokes loops. n=1000 → 5 batches. ok. Alternatively skip streaks for settled/grounded (no). streak only when airborne & speed>2. Rocks (normal pass): drawImage(lavaSprites[idx], x-r, y-r, 2r,2r) with globalAlpha = fade for dying. Hot pass ('lighter'): drawImage scaled with alpha maybe 0.9 + flicker: flicker = 0.8+0.2*rand? Random per frame flicker cheap: alpha = base*(0.85+0.15*Math.random()). ok. Also larger hot particles get halo sprite (glowSprite big, alpha 0.35)? That's per-particle big draw for maybe 100 hot particles — ok. Actually heat glow can just be the sprite itself since radial gradient spreads. Skip halo. Vent glow sprite: big radial (200px) orange, alpha = pressureFactor. Also crater floor molten pool: when pressure high, draw a molten pool sprite inside crater + few standing lava sparks? Pool: draw ellipse of hot color at crater floor with alpha = pressure*... use 'lighter' ellipse glow flattened. Simple: drawImage(glowSprite, scaled squashed) at crater floor. Good visual: crater glows increasingly → anticipation. Smoke sprites: 5 levels: - lit warm: rgba stops: (212,150,98) core a .9 → transparent? For smoke want soft blob: radial a 1→0 with low overall alpha applied at draw time (globalAlpha). Colors: level0 (eruption-lit): 200,130,80; 1: 160,105,70; 2: 110,88,74; 3: 90,80,74; 4: 70,64,62. Radial: 0: a0.85, 0.55: a .5, 1: 0. Hmm gradient alpha creates soft edge — yes. Smoke global draw alpha: a = 0.28 * fade. Ah also: ash darkening sky behind heavy plume — overlapping alphas accumulate, fine. Now numbers/perf check: lava up to ~1500, smoke ~ 260. Per frame ops: physics per particle simple math; render: lava streaks batched + sprite draws 1500 + smoke 260 drawImage. drawImage ~ 1800/frame — modern browsers handle (Canvas2D drawImage of small sprites ~ millions/sec? yes typically fine, tens of micro-cpu). Should hit 60fps on typical desktop. Add caps: if fps < 45, reduce smoke spawn? Keep simple; cap particle spawn by slider anyway. Memory: lava array objects {x,y,vx,vy,heat,cool,r,settled,fade,tGround, w?}. Fine. Removal: swap-pop. Terrain draw into backdrop: build path from ground array (moveTo(0,H)... lineTo each x step 4px, down to bottom). Fill dark. Rim stroke: stroke same path top only? Stroke whole path would include bottom edge — draw separate polyline of surface only, stroke with subtle warm rgba(120,80,50,0.35), lineWidth 1.5 — hmm rim light on crest facing horizon: acceptable simple. Also crater inner shading: darker inside crater (arc region between rim points and floor) — the silhouette fill handles crater since ground dips; the fill covers dip. Inner wall highlight from vent glow dynamic anyway. Volcano rock texture: sprinkle darker/lighter speckles pre-rendered: for i<600: random point where y within terrain: draw 1px alpha 0.05 white or black — subtle. Also a few larger angular boulder shapes? Keep speckles + slope gradient shading: add vertical shading: after fill, overlay gradient? gradient dark at bottom lighter near horizon? Terrain: near-ground slightly lighter warm; use linear gradient fill from y=baseY (dark) to H (darker)? Subtle. Also cone body slightly lighter than sky? silhouette darker than sky = good contrast. Horizon hills: left hill and right hill silhouette slightly lighter (#221a13) behind volcano: draw before volcano. Ground foreground band: below baseY darker fill #0d0a08 with speckles. Sky: gradient stops: 0: #06070b; 0.45: #14121a? Avoid purple-ish: use warm-neutral: 0: #05060a, 0.5: #1c1720 → hmm that's purple-ish. Use #171419? Slight wine... I'll go warm dusk: 0: #070709, 0.55: #1b1516, 0.8: #3a251c, 1: #6b3a22 at horizon (behind hills bottom ~ baseY). Horizon glow strongest around volcano? centered gradient? Use radial glow at horizon centered cx? Might look like sunset behind volcano: draw radial gradient ellipse centered (cx, baseY) radius W*0.5 warm (#7a3f1f alpha .5) → atmosphere. That's scene lighting — legit. Add: after sky, radial horizon glow. Stars only above y< H*0.45 with alpha .5*(rand). Moon: pale disc + faint halo, upper right. Vignette overlay: radial transparent center → rgba(0,0,0,0.35) edges; drawn each frame last (pre-made sprite sized W,H). Also subtle heat-haze at crater? skip. DOM & CSS: ``` body{margin:0;overflow:hidden;background:#070709;font-family:'Barlow Condensed'...} canvas#scene{position:fixed;inset:0} .hud panels position fixed, pointer-events none except sliders need events → pointer-events auto on controls. ``` Panel style: ``` .panel{position:fixed;padding:14px 16px;background:rgba(12,9,8,.58);border:1px solid rgba(255,220,190,.13);backdrop-filter:blur(6px) brightness(1.05)? ; border-radius:3px; color:#f0e6da} label micro: font 10px letter-spacing .18em uppercase color rgba(240,230,218,.55) ``` Meter: width 220px; track height 12px? Actually design: a horizontal gauge with ticks above and fill bar; plus numeric % readout right-aligned. Fill: background var accent #ff9d3f; when >82% switch to #ff4c26 with slight pulse animation (CSS keyframes on background? use class + animation opacity flicker). Ticks: I'll add 4 divs at 25/50/75% as 1px rgba(255,255,255,.15) lines over bar. Warning zone: separate absolutely positioned div right 0 width 18% background rgba(255,60,30,.12). Sliders: input[type=range] styled: -webkit-appearance none; height 22px; track via background: linear? track: use background on input: thin 2px line via background-image? Simplest: input background transparent; ::-webkit-slider-rail height 2px background rgba(255,255,255,.16); thumb: 14px x 14px? thumb: width 4px height 14px background #ff9d3f radius 1px — a thin tick-thumb (industrial). Also -moz counterparts. Value labels right side mono. Range accent per slider: power → #ff9d3f; wind → #b9c8c9? wind maybe cool gray-green #9fb3ae; particles → #ff9d3f too? differentiate: particles #e2c9a6? Keep two accents: lava orange for power & particles, wind neutral pale #cfd6cf. Values shown: POWER 62, WIND +2.4, OUTPUT 65% → show derived emitRate like "212/s". Fine: value text updates. Panel positions: top-left pressure panel; bottom-left controls; top-right stats. On small screens, fine. Stats panel: two rows big mono numbers with micro labels: PARTICLES 1284, FPS 60. Maybe also "VENT TEMP"? gimmicky, skip. Add state word inside pressure panel: under meter: "STATUS: BUILDING" small mono. Also a thin title top-center? Keep quiet; small label in pressure panel: "STRATOVOLKAN — PRESSURE VESSEL MONITOR"? Title: "VULKANØIDE / MONITOR"? Let's name: "STROMBIKI"? Cute: "STROMBOLI-9 VENT MONITOR". Or fictional: "MT. KALVERIN — VENT PRESSURE". Title in panel header: "MT. KALVERUIN · VENT MONITOR". Keep subtle. Hint: bottom-right tiny: "CLICK THE MOUNTAIN TO AGITATE THE VENT". Click handler on canvas: adds pressure +12 and a mini burst (30 particles weak) + tiny shake 2px. Only if not erupting? allow always (during erupt adds little). Also agitates meter needle? skip. Edge cases: slider particle count affects ongoing spawn; cap: if lava.length > cap → skip spawn (both lava & smoke separately capSmoke 320). Now write physics numbers concretely (per step = 16.7ms): G = 0.36. Launch: speed base: sp = (5.2 + power01*9.5) * (0.72+rand*0.55); power01 = slider/100. At power 50: 5.2+4.75=9.95 avg ~ *0.99 ≈ 9.9. vy = -cos(a)*sp, a ∈ (-spread..+spread) spread = 0.55 + power01*0.35 rad (~ up to 0.9 rad = 51°). vx = sin(a)*sp*0.9 + (rand-.5)*1.5. Add vent gas: vx += windCur*0.06. Wind: windCur = windSlider * 0.001? slider -10..10 step .1. Airborne lava: vx += windCur*0.05*dt? For wind 5: 0.25/step → over 80 steps adds vx 20?? too much: multiply dt: vx += windCur*0.05*dt where dt~1: +0.25 per step, 80 steps → +20 vx. Way too much. Want drift ~ ±40px over flight: need Δvx ~ 0.5 total → per step 0.006. So vx += windCur*0.0012*dt? Let's aim wind 10 (max) shifts visibly: per step +0.012*10=0.12? over 80 steps Δvx=9.6 → Δx ~ 380px. That's strong max wind — good for max. So factor: vx += windCur*0.012*dt (windCur ∈ [-10,10]). Smoke: vx += windCur*0.045*dt → at wind 5: 0.225/step*~300 steps... too much; smoke drift velocity instead: vx = windCur*0.35 + turbulence; directly set with easing: vx += (windCur*0.35 - vx)*0.02*dt? Simpler: vx = windCur*0.28 + turb, turb = sin(age*0.02+phase)*0.25 + wander random walk: turbW += (rand-.5)*0.05, decay. Implement: p.tx += (Math.random()-.5)*0.08*dt; p.tx*=0.98; vx = windCur*0.28 + p.tx*2? Let's just: p.vx += ((windCur*0.3 + Math.sin(p.ph+p.age*0.015)*0.3) - p.vx) * 0.02*dt. Fine. Lava grounded sliding: slope s = slopeArr[i] (dy/dx). downhill ax = -s * 0.55? Let's check: slope at flank: ground rises 0.6 per px → s=0.6: ax = -0.33 → strong slide. Cap |vx| 6. Friction: vx *= 1 - 0.05*dt (when grounded moving). Unstick: if grounded && |vx|<0.12 && |s|>0.22 → give nudge vx += (−s*0.6)*0.5? Keep sliding loop natural: gravity keeps pulling; friction balances → terminal velocity vx_t = ax/(0.05 per step)... vx approaches ax*20 = 6.6 — too fast for "slide". Add stronger friction 0.12: terminal = 0.33/0.12 = 2.75 px/step ≈ nice slide downhill. If |s|<0.1: ax small → settles. settle condition: |vx|<0.1 && |s|<0.06 → settled=true (stop integrating motion; still cool). If settled but ground under changes? no. Also grounded particles stick: y = ground[i] - 0.5? minus r? draw sprite centered at y-r? Position y = surface - r*0.5 so bottom sits. Simplify: y = ground[i] - r*0.4. Bounce on landing: if vy > 2.2: vy = -vy*0.28; vx *= 0.72; spawn puff smoke small (if smoke.length < cap*0.8 && Math.random()<0.25): small r 4 growing to 14, life short. else grounded. Absorption: if !absorbed && grounded && Math.abs(x-cx) craterFloorY-2 → absorbed: pressure = min(100, p + (heat>0.35? 0.5 : 0.15)); remove; maybe spark flash tiny: draw? skip visual, add pressure. Actually visual: push tiny ember spark upward? spawn 1 lava particle small with vy -2? cute "geyser" but may loop infinitely: falling in adds pressure → eruption sooner → fine, bounded by pressure cap at 100 → eruption starts anyway when full. Wait, potential infinite: particles sliding into crater → absorbed → pressure rises → eruption at 100 drains. It's bounded & fine. Vent position: launch x = cx + (Math.random()-0.5)*10, y = craterFloorY - 4. craterFloorY = ground[clamp(cx)]? ground at cx is crater floor (deepest dip) — but noise... compute floorY = baseY - peakH + dipH (formula), use that. Crater dip must make ground[cx-55..cx+55] < summit so walls slope inward. With dip(d)=dipH*(smooth)(1-(d/cw)^2): at d=cw dip=0 (rim). Rim bump: add +8px bump at |d|=cw*1.05: ground += bump*exp(-((|d|-cw)/ (cw*0.28))²)? careful sign: height y measured as surface Y (lower y = higher). Let me define groundY(x) (canvas y). cone(x) = baseY - peakH*(1-dn)^1.6 where dn=d/R. crater: cone(x) + dip? In canvas coords, dip down into cone = larger y: craterY(x) = cone(x) + dipH*f(d) where f = smooth bump 0 at d≥cw*0.95... Use f(d)= (1-(d/cw)^2) for d 75: smoke rate = (p-70)/30 * 8/s → fumarole wisps. Smoke during eruption: rate = 55 + emitRate*0.35 per sec? smoke spawn per step: sAcc += smokeRate*dt where smokeRate = erupting? 70+v*0.5 : (p>75? (p-75)*0.5 : 0). At v=65: 70+32=102/s → over 4.4s eruption ≈ 450 smoke born, life ~4s → concurrent ~ 350? cap 340 fine. Smoke cap 340; lava cap = 600 + v*12 (v=65 → 1380). Hard max 2600. HUD update: every 200ms set textContent. Draw details ordering with shake: ``` ctx.save(); ctx.translate(shx, shy); ctx.drawImage(backdrop,0,0,W,H) (backdrop includes vignette? no vignette last) drawVentGlow(); drawMoltenPool(); smoke draw (normal comp, alpha per particle) lava: streaks (normal), rocks normal, hot 'lighter' flash overlay if flash>0 ('lighter' radial at vent + slight white screen alpha) ctx.restore(); vignette drawImage ``` Wait: vignette inside or outside shake — outside (fixed). ok. Flash: full-screen additive rgba(255,180,90,flash*0.18) fillRect + vent big glow alpha flash. flash decays *0.9 per step. Shake: amp = shake*10 + tremor (tremor = p>85? (p-85)*0.35:0; plus during eruption early extra). Offset each frame: shx = (rand-.5)*amp*2? Random per frame is jittery-good for earthquake. Decay shake *= pow(0.92, dt). Crater molten pool: draw squashed glow: ctx.save scale? drawImage with width 2rx, height 2ry (ellipse-ish since sprite round squashed). alpha = (p/100)^1.5 * (erupting? 1:0.7) * flicker. Plus during eruption brighter. Also draw vent glow above crater: alpha = (p/100)^2*0.5+flicker. Sky stars twinkle? static ok. Mouse click: add pressure, burst 35 particles: sp smaller (0.5x), shake += 0.3. Wind slider affects smoke immediately (vx target uses windCur), airborne lava. Also maybe flag: draw drifting background clouds? skip. Wind value display signed. Also indicate wind direction in scene? The smoke shows it. Fine. Let me also consider the "signature moment": eruption sequence with flash + shockwave ring? A quick shock ring: on eruption start, spawn a expanding circle stroke (radius grows 0→300 in 0.5s, alpha fades) — cheap, dramatic. Draw as arc stroke with lineWidth 3 alpha. Yes add shock ring (one per eruption). Also small rings on clicks? no. Also I want the meter "needle jitter" — skip. Sound: skip. Now the FPS guarantee: default should comfortably hit. Also add adaptive: if fps < 40 for sustained, reduce emission multiplier 0.7? Keep code simple but I'll include a mild adaptive damp for smoke alpha? Skip; counts capped. Counting: lava.length + smoke.length displayed. One more nuance: particle heat index mapping: idx = min(7, floor(heat*8))? heat 1→0 idx0..? floor(0.999*8)=7?? Wrong direction: hottest should be idx 0. idx = heat>=0.95? 0 : min(7, floor((1-heat)*8))? Let's define colorIdx = clamp(Math.floor((1-heat)*(8-1)),0,7)? heat 1→0 (hottest idx0), heat 0.875→ (0.125*7=0.875)→0? Let's simply idx = Math.min(7, ((1 - heat) * 7.3) | 0)? At heat 0.86 → 1; heat 0.71→2; 0.57→3; 0.43→4; 0.29→5; 0.14→6; ≤0.02→7. Good. Fade: when heat <= 0.02: p.fade = 1 then fade -= (0.006+rand*0.004)*dt → gone after ~ 3s? 1/0.007*16.7ms ≈ 143 steps ≈ 2.4s ok. draw alpha = fade for idx>=6; for hot idx fade not needed (alpha 1). Settled rocks disappearing via fade — "then disappearing" satisfied. Cooling rate: heat -= (0.0032 + p.c0*0.004)*dt where p.c0 rand 0..1 → rates 0.0032–0.0072 → life 139–312 steps ≈ 2.3–5.2s. Plus fade 2.4s → total up to ~7.6s. During 4.4s eruption spawning 900 → after eruption ends ~ still ~800 active (mixed ages decaying). Plus smoke. Peak concurrent ~ 900 lava + 300 smoke ≈ 1200. But wait: lifetime of particles launched early in eruption die ~7s later, while eruption 4.4s: at eruption end, first-spawned (age 4.4s) still alive (~ half gone). Steady between eruptions ~ decays. Fine, count stays >1000 during eruption & shortly after. Requirement "at least 1000 active" satisfied at default. Maybe bump defaults: v default 70 → emitRate 226/s → 4.6s*226 ≈ 1040. Also small slow continuous puff during building? Only fumarole smoke; lava only on eruption. OK. Hmm also many particles settle & stay for fade — they count while visible. Good. Ash "rises from crater and drifts with wind" — done. Sliders live: read input.value on each frame into vars (cheap). Fonts loading: use plus IBM Plex Mono. If offline, fallbacks: 'Barlow Condensed', 'Arial Narrow'?? fallback generic: sans-serif with letter-spacing still ok. Icon library — no icons needed; panels text-only (no emoji anywhere). Good. Now write terrain silhouette function carefully; also ensure crater floor is above vent launch and particles falling back inside crater land on floor. Launch y = floorY - 6 with vy up; when they fall back inside |dx|<26 they hit floorY → absorbed or bounce; those outside hit outer slope. Absorb condition check before bounce/ground handling: when particle y >= floorY-1 && |x-cx| <= 24 && vy >= 0? Actually falling into vent: y > floorY - 2 && |dx| < 22 → absorbed regardless of vy (even falling). But particles launched upward pass through that zone moving up — condition must only trigger when vy > 0 (descending) and y >= floorY-2. If vy>0 && |dx|<22 && y > floorY-2 → absorbed. Settled sliding into crater along floor: grounded && |dx|<30 && slope small... sliding particles arriving at floor from walls: y ≈ floor region: grounded && |dx| < 26 && y > floorY - 4 → absorbed. Combined: if (p.grounded || p.vy>0) && Math.abs(p.x-cx)<24 && p.y > floorY - 3 → absorb. But wait: launched particles start at floorY-6 with |dx|<10 — they immediately satisfy? They start vy<0 (up) and not grounded → condition vy>0 fails, grounded false → safe. After apex descending near vent with |dx|<24 → absorbed — but many launched near-vertical descend right back into vent — absorbed → recycled. Realistic! (close-vent fall-back). But absorb makes pressure +0.5 each; eruption spawns them... loop: eruption ends p=0, particles fall back over next seconds adding pressure to 100?? e.g., 400 fall back *0.5 = 200 pressure → instant re-eruption chain! Cap: absorbed adds pressure only min(p+..) but p from 0 could jump. Limit: absorbed adds only if p < 60? or rate-limited: pressureFromRecycle limited to +0.1 each and only first 200 per cycle? Simpler: absorbed particles add pressure only while p < 45 (they "recharge" early but not after). Hmm arbitrary but works. Or reduce to +0.08: 400*0.08=32 → moderate boost. Let's do: if p<50: p += 0.09. Else nothing. Fine, subtle. Wait also: falling-back particles absorbed → removed quickly → count drops. Many launched with sideways vx will land on slopes. Vertical ones absorbed. Ratio depends on spread. Fine. Also particles that exit upward beyond screen top (y< -50) will return; keep them (no removal at top; only x bounds & bottom absorption/landing). If y > H + 60 remove (below ground? ground at 0.8H so never unless off sides) — remove x<−40 || x>W+40. Draw lava even above screen? drawImage handles offscreen cheaply. Landing puff smoke: p = {x,y-2, vx: vx*0.3, vy:-0.5, r:5, ...small}. cap check. Now code structure & sizes. Let me write it. Constants: - let W,H,DPR; ground = Float32Array; slope = Float32Array. - cx, baseY, peakH, R, cw(=clamp(W*0.05,40,70)? For W=1280: 55. Use cw = Math.max(38, W*0.045)), floorY, summitY. Terrain gen: ``` function terrain(){ ground = new Float32Array(W+8); // value noise const nseed=... noise: build array nz[i]=hash random; smooth: for each x, sample avg of rand[(x/step)] with interpolation. Simpler: precompute coarse random at every 16px, interpolate (linear + slight smooth), amplitude scaled. ``` Simpler noise: nz(x) = sin-based pseudo: n = (Math.sin(x*0.011+2.7)+Math.sin(x*0.037+1.1))*0.5 + ... combination gives organic-ish? Sin sums look wavey. Value noise better: ``` const rnd = new Float32Array(W+16); for i: rnd[i]=Math.random()*2-1 (fixed seed via Math.random fine, regenerated per resize ok) function vnoise(x){ // interp rnd at step 14 const s=14; const i=Math.floor(x/s), f=(x/s)-i; ... but need rnd index >= i+1 } ``` Use rnd[i%len]. Linear interp + smooth by using two octaves (s=14 & s=48). ok. groundY(x): ``` d = Math.abs(x-cx); dn = Math.min(1, d/R); cone = peakH * Math.pow(1-dn, 1.6); // height above baseY y = baseY - cone; // crater dip u = Math.max(0, 1 - d/cw); f = u*u*(3-2*u); // f=1 at center, 0 at edge... wait at u=0.5 f=0.5; derivative at u=1: f'(1)=? u*u*(3-2u) at u=1 → 1*1=1?? At u=1, f=1*(3-2)=1! Wrong: I want f(1)=0. Use f = 1 - u*u*(3-2*u)?? Then at u=1 f=0, at u=0 f=1? 1-0=1 yes. But shape: dips with smooth edge. Actually I want dip amount maximal at center: dip = dipH * (1 - u*u*(3-2*u))? At u=0 (center): 1 → full dip. Edge u=1: 0. But at u=0.5: 1-0.5=0.5 → dip half at half-width: linear-ish, walls slope: dy/dd = dipH * d/du * ... near edge derivative: 1 - (3u²-2u³): derivative -6u+6u² = -6u(u-1) → at u=1 slope 0 (smooth edge), at u=0 slope 0 (flat floor center). Max slope at u=0.79... dipH*~0.9 per unit u; u per px = 1/cw=1/55 → slope ≈ 0.9*46/55 ≈ 0.75 → steep inner walls (39°), good for sliding in. So y += dipH * (1 - u*u*(3-2*u)) for u>0. ``` Wait sign: y += dip (downwards). Yes larger y. Rim: y -= rimH * exp(-((d - cw*1.05)/(cw*0.30))^2) with rimH 9. (subtract = upward bump). Place slightly outside crater edge so outer slope particles can lodge at rim. Might block sliding outward? It creates a small ridge; particles from above may settle behind rim — fine natural levees. Noise: y += nz(x) * amp where amp = 5 + 12*dn (0 at summit? dn near 0 at summit → amp 5 small inside crater, ok). Use two-octave value noise in [-0.5..0.5]*2*amp. Ground plain outside cone: for dn≥1: y = baseY + slight undulation: y = baseY + 6*nz? plus gentle: y = baseY + (x offset?) keep flat-ish with tiny undulation ±4. Also side hills behind? Those are backdrop silhouettes drawn separately (not in ground collision — particles can't reach? particles arc far left/right over hills? Hills at horizon y ~ baseY-60? If particle lands on hill region it should collide... hills are part of terrain visually; make ground[] include hills? Simpler: make hills purely decorative far left/right beyond x where ground exists? They'd overlap baseY. I'll integrate hills into ground array: ground[x] = min(ground[x], hillY(x)) where hillY = baseY - hillH * bump(d from cx ± offset). Hill left at x=W*0.12 height 70 width 260; right hill at W*0.86 height 50 width 300. min = higher ground. Particles landing on hills collide — consistent. Then draw backdrop from ground[] — silhouette includes hills. Volcano drawn same fill. Then baseY region between hills and cone: plain. Backdrop draw: path along ground[] (step 3px), down to H, fill #131009; stroke surface polyline rgba(255,190,140,0.10)? subtle warm rim. Then speckles: for 900 random x: y=ground[x]+rand*30 (within 40 below surface): draw 1-2px rects alpha 0.06 lighter or darker. Also darker patches near base. Sky first: gradient. Horizon glow radial. Stars. Moon. Then terrain. Then vignette separate sprite. Vignette sprite: radial gradient centered, inner transparent → edge rgba(3,2,2,0.4). Also slight top darkening? fine. Sky gradient colors (avoid purple/blue-dominant): 0: '#050608', 0.42: '#161117', 0.66: '#2b1b16', 0.85: '#542c1a', 1 (at horizonY=baseY): '#8a4624'? Sky gradient to baseY then below horizon terrain covers. Also horizon glow around volcano: radial at (cx, baseY) radius W*0.42: stops rgba(255,120,50,0.28) → transparent. Subtle. Stars: 120 dots above y Vent Monitor — Kalveruin Stratovolcano panels... ``` CSS: ```css :root{--ink:#f2e9dc;--dim:rgba(242,233,220,.55);--line:rgba(242,233,220,.14);--hot:#ff9d3f;--warn:#ff4b23;} *{box-sizing:border-box} html,body{margin:0;height:100%;background:#050608;overflow:hidden} #scene{position:fixed;inset:0;width:100%;height:100%;display:block;cursor:crosshair} .panel{position:fixed;z-index:2;background:rgba(13,10,9,.62);border:1px solid var(--line);border-radius:3px;padding:12px 16px 14px;color:var(--ink);backdrop-filter:blur(7px)} .micro{font-family:'Barlow Condensed',sans-serif;font-size:10px;letter-spacing:.22em;color:var(--dim);text-transform:uppercase} #pPanel{top:18px;left:18px;min-width:250px} #pPanel h1{...} maybe skip h1; use .title{font-family Barlow Condensed 600, size 15, letter-spacing .08em} #stats{top:18px;right:18px;text-align:right} .statsrow{display:flex;gap: ...} ``` Stats panel: two stat blocks side by side (flex): big number mono 22px, micro label. Display "PARTICLES" & "FPS". plus third? maybe "PERIOD"? keep two. Meter markup: ```html
vent pressure0%
``` Meter CSS: position relative; height 14px; border:1px solid var(--line); background rgba(255,255,255,.05); overflow hidden; .fill{position:absolute;inset:0;transform-origin:left center;transform:scaleX(0);background:var(--hot);transition:none} .fill.hot{background:var(--warn);animation:pulse .5s infinite? } .zone{position:absolute;top:0;bottom:0;right:0;width:17%;background:rgba(255,75,35,.16);border-left:1px solid rgba(255,75,35,.4)} .ticks i{position:absolute;top:0;bottom:0;width:1px;background:rgba(255,255,255,.18)} plus tick labels? skip. Status line: `
statusBUILDING
` styled row justify-between. Controls panel bottom-left: ```html
mission controls
slider rows:
eruption power55
wind: min=-10 max=10 step=0.5 value=0 → value text fmt signed particles/output: min=0 max=100 value=70 → text "1040 cap"? show "OUTPUT 70" plus note? Show emit rate: computed. Simpler show % and let meter? show value like "70 · 226/s"? Value text: `${v}` and small hint? I'll show value + unit: power → "55 %", wind → "+0.0", output → "70 %". Keep mono small.
``` Slider CSS: ```css input[type=range]{-webkit-appearance:none;appearance:none;width:210px;height:18px;background:transparent;margin:6px 0 2px;cursor:pointer} input[type=range]::-webkit-slider-rail{height:2px;background:rgba(255,255,255,.16)} input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:5px;height:16px;border-radius:2px;background:var(--hot);margin-top:-7px?} ``` For webkit thumb positioning need rail height & thumb margin-top offset: with rail 2px and thumb 16px, margin-top = -(16-2)/2 = -7px. Yes. Firefox: ::-moz-range-track{height:2px;background:...} ::-moz-range-thumb{width:5px;height:16px;background:var(--hot);border:none;border-radius:2px}. Wind thumb color override: give each input a CSS var --th and thumb background var(--th). Use per-input style="--th:#9fb3ae"? thumb background: var(--th, var(--hot)). ok. srow head: flex justify-between width 210. Hint bottom-right: fixed div micro text: "CLICK THE MOUNTAIN TO AGITATE THE VENT". Stats: ```html
0particles
--fps
``` #stats{display:flex;gap:26px} .stat{display:flex;flex-direction:column;align-items:flex-end? } big font 22px IBM Plex Mono 500 color var(--ink). Particle count color hot when >900? subtle. Mono class: font-family:'IBM Plex Mono',monospace;font-size:11px. Now JS. ```js const cvs=document.getElementById('scene'), ctx=cvs.getContext('2d'); let W=0,H=0,DPR=1; let ground,slope,cx,baseY,peakH,RR,cw,floorY,summitY; const bdoc=document.createElement('canvas'); // backdrop let bctx=null; const vin=document.createElement('canvas'); // vignette ``` Resize: ```js function resize(){ W=innerWidth;H=innerHeight;DPR=Math.min(2,devicePixelRatio||1); cvs.width=W*DPR|0;cvs.height=H*DPR|0; // style set by css 100% ctx.setTransform(DPR,0,0,DPR,0,0)? We'll draw with logical coords; use ctx.setTransform each frame start. buildTerrain(); buildBackdrop(); buildVignette(); } ``` Terrain: ```js const nr=[], build random array len 600. function nz(x){ // value noise, two octaves const s1=11,s2=37; let i1=Math.floor(x/s1),f1=x/s1-i1; a1=nr[i1%600],b1=nr[(i1+1)%600]; smooth t=f1*f1*(3-2*f1); v1=a1+(b1-a1)*t; similar s2 → v2; return (v1*0.7+v2*0.3); } ``` Wait x negative? x≥0 only. Fine. Add amplitude sign. buildTerrain: ```js cx=W*0.52; baseY=H*0.80; peakH=H*0.35; RR=Math.min(W*0.36,H*0.60); cw=Math.max(34,Math.min(70,W*0.045)); ground=new Float32Array(W+10); for x=0..W+9: compute y then store. floorY = baseY - peakH + dipH? Actually crater floor y = baseY - peakH*(pow(1 - tiny dn)^1.6≈peakH) + dipH → summitY = baseY-peakH; floorY=summitY + dipH where dipH = 44 (clamp with H scale: dipH = H*0.055+? use 40 + H*0.02). Let dipH = Math.round(H*0.062). For H=720: 44. ok. Also store ground[]; slope computed: slope[x]=(ground[x+3]-ground[x-3])/6 → need array covering x±3 → ground length W+10 with indices offset? Let ground index x from 0..W+9 where ground[x] corresponds to world x-? Keep simple: ground indexed 0..W (use W = max). Array size W+8; slope for x in [3, W-4] compute, clamp elsewhere. ``` Actually indices: ground[i] = surface y at x=i. For x up to W+7 (particles can be x>W briefly). Compute i from 0..W+7. slope array same size; slope[i] = (g(i+3)-g(i-3))/6 (i from 3..W+4), clamp edges. Noise amp: amp = 4 + 14*dn2 where dn2 = Math.min(1,d/RR)^1.2? At flanks more wobble. Also plain area: when dn>=1: y = baseY + 3 + 5*nz(x) (slight). And hills: ``` hillH at left: hL = H*0.10, center xL = W*0.14, radius RL = W*0.17 hR = H*0.07, xR=W*0.87, RR2=W*0.20 y = Math.min(y, baseY - h * Math.max(0,1-dh/r)^1.5 ... ) using pow(1-dh/r,1.4) ``` But near volcano flanks plain. Also ensure ground not above horizon weirdly. hills rise above baseY (smaller y). fine. Also ground below hills between hill and volcano: baseY plain — visually valley. ok. Crater rim formula check for d slightly > cw: rim bump subtract — with exp may overlap dip region? dip only for u>0 (d floorY-3 & |dx|<24 — if noise raises floor locally above floorY-3? floor at cx: ground[cx] = summitY + dip + noise → could be floorY±6. Absorb threshold y > floorY - 6? Use ground[cx] reference: absY = ground[clamp(cx)]; use absorbY = Math.min(ground[cx], ground[cx±?]) hmm. Simpler: absorbY = floorY - 2 where floorY = ground at exact cx int. Particles landing inside crater land at ground[x] which is ≥ (lower or equal y? ground[x] larger y = lower). Particle at x near cx grounded y=ground[x]; condition p.y > floorY - 3 with floorY = ground[cxInt]: if noise makes ground[x] y 8px above floorY at some x (y smaller), particle standing there has y = floorY-8 → condition y > floorY-3 false → not absorbed → it settles inside crater as rock pile. Fine visually! Just some rocks accumulate in crater. OK realistic (lavacone). But also they'd never absorbed → fine. Hmm but note particles falling into crater along inner wall: land y=ground[x] which near wall > floorY → absorbed ✓ if |dx|<24. If |dx| between 24 and cw they settle on floor pile. Good. Launch x within ±10 → |dx|<10 → falling back absorbed ✓. Now backdrop render: ```js function buildBackdrop(){ bdoc.width=W*DPR;bdoc.height=H*DPR; bctx=bdoc.getContext('2d'); bctx.setTransform(DPR,0,0,DPR,0,0); // sky const sky=bctx.createLinearGradient(0,0,0,baseY); stops... fill rect 0,0,W,baseY+2 // horizon glow radial at cx,baseY radius W*0.45: rgba(255,122,48,.20)→0... draw ellipse? use save scale(1,0.45) trick: gradient circular then scaled — draw via transform. // stars, moon // terrain: path bctx.beginPath(); moveTo(-4,H+4); ... for x step 3: lineTo(x, ground[x]) ... lineTo(W+4,H+4) close fill '#120e0b'? Let's fill color '#141009'. // surface rim stroke: separate path top polyline stroke 'rgba(255,185,120,.08)' lw 1. Actually rim light stronger near horizon glow sides? keep uniform subtle. // speckles // ground darker below baseY: fill rect from baseY with rgba(0,0,0,.25)? gradient? subtle: fillStyle rgba(8,6,5,.5) rect(0, baseY-? ...). Eh: draw translucent black gradient downward from baseY? fine: linear gradient baseY→H alpha .0→.35 overlay. } ``` Wait stars must be behind terrain? stars only above baseY & terrain covers below. draw stars before terrain. Moon before terrain (behind volcano if overlapping? moon high right, no overlap). Vignette sprite: vin width W,H (logical *DPR): radial gradient centered (W/2,H*0.55) inner r H*0.3 alpha 0 → outer r W*0.75 rgba(4,3,2,0.45). Draw once. Sprites builder: ```js function sprite(size, stops){ const c=document.createElement('canvas');c.width=c.height=size;const g=c.getContext('2d');const gr=g.createRadialGradient(size/2,size/2,0? , size/2,size/2,size/2)? radius from 0 to size/2; for stop in stops: gr.addColorStop(o, `rgba(${r},${g},${b},${a})`); g.fillStyle=gr; g.fillRect(0,0,size,size); return c;} ``` Wait variable name conflicts; use c2. Lava sprite stops (per level, as [offset, 'r,g,b', a]): Level configs — I'll write array: ```js const LSTOPS=[ [[0,'255,247,232',1],[.3,'255,226,148',.95],[.62,'255,178,80',.42],[1,'255,150,60',0]], [[0,'255,238,196',1],[.28,'255,204,110',.9],[.6,'255,140,60',.4],[1,'255,120,50',0]], [[0,'255,210,140',1],[.26,'249,150,64',.85],[.58,'238,88,34',.36],[1,'230,70,30',0]], [[0,'252,160,88',.95],[.24,'228,96,40',.8],[.56,'196,54,26',.32],[1,'180,45,22',0]], [[0,'228,110,54',.85],[.24,'150,50,24',.75],[.55,'90,30,16',.3],[1,'70,25,14',0]], [[0,'150,66,36',.8],[.22,'84,36,20',.75],[.55,'46,24,15',.32],[1,'36,20,13',0]], [[0,'96,74,62',.95],[.3,'62,48,40',.9],[.62,'38,30,25',.5],[1,'30,24,20',0]], [[0,'84,66,56',.9],[.3,'52,40,34',.85],[.62,'33,26,22',.5],[1,'27,21,18',0]], ]; ``` Level 6–7 rocks: alpha fairly solid to look like rocks. Under 'lighter' pass only idx ≤3 drawn; rocks idx≥4 normal pass. Sprite size 64. Smoke sprite: make 5 levels warm→gray: ```js const SSTOPS=[ [[0,'214,146,96',.9],[.5,'180,116,74',.45],[1,'170,110,70',0]], [[0,'172,118,84',.85],[.5,'140,98,72',.42],[1,'130,92,68',0]], [[0,'120,102,88',.8],[.5,'96,82,72',.4],[1,'86,74,66',0]], [[0,'98,88,80',.75],[.5,'78,70,64',.38],[1,'70,63,58',0]], [[0,'78,72,68',.7],[.5,'62,57,54',.35],[1,'56,52,50',0]], ]; ``` Glow sprite (vent/flash): stops [[0,'255,190,90',1],[.35,'255,120,40',.5],[.7,'255,60,20',.12],[1,'255,40,10',0]] size 128. Flash sprite white-hot: [[0,'255,240,210',1],[.3,'255,190,110',.6],[1,'255,120,50',0]] size 256. Now main loop. State vars: ```js let p=0, phase='build', cycleRand=Math.random(), rate=100/(9+Math.random()*6) per second... compute each build cycle: ratePS = 100/(9+6*Math.random()); erupting duration etc. let eruptT=0, dur=0, emitAcc=0, smokeAcc=0, postT=0; let flash=0, shake=0, shock=null (or list), tNow=0(ms); ``` Build step: ```js function step(dtms){ tNow... } // integrate in frame ``` Frame: ```js let last=performance.now(), fpsE=60, hudT=0; function frame(t){ requestAnimationFrame(frame); let dtms=Math.min(42, Math.max(0, t-last)); last=t; fps update EMA with 1000/dtmsActual? Use raw delta for fps (unclamped): fpsE += (1000/Math.max(1,delta)-fpsE)*0.08 — careful delta could be 0; guard. const dt=dtms/16.667; // read sliders const power=+sPower.value, windV=+sWind.value, out=+sOut.value; emitRate = 30+out*2.9; cap = 700+out*13; smokeCap=340; // pressure logic if(phase==='build'){ if(postT>0){postT-=dtms/1000;} else{ p += ratePS*dt/60? wait ratePS per second; per frame: p += ratePS * dtms/1000; wobble factor... if(p>=100){startEruption();} } } else { // erupt eruptT -= dtms/1000; p -= 100/(dur) * dtms/1000; clamp ≥0 if(eruptT<=0 || p<=0){ phase='build'; p=0; postT=1.2; ratePS=100/(9+6*Math.random()); } } ``` Hmm during eruption keep p displayed draining. Also during eruption pressure effectively pinned by eruption; after eruption p=0 builds. Emission: ```js if(phase==='erupt'){ const pulse = 0.55+0.55*Math.sin(tNow*0.016)+0.35*Math.sin(tNow*0.0073+2.1); // ~0..1.45, clamp emitAcc += emitRate * Math.max(0.12, pulse*0.8) * dt; // particles per second * dt let n=emitAcc|0... spawn while emitAcc>1? emitAcc accumulate: while(emitAcc>=1){emitAcc--; spawnLava(...)} — but per-step spawn count = floor(acc)? Use: emitAcc += rate*dt; const k=Math.floor(emitAcc); emitAcc-=k; for k: spawn (k could be ~4 per frame at 226/s*16.7ms=3.8). Good. } smokeAcc similar: rate = phase==='erupt'? 60+out*0.55 : (p>75? (p-75)*0.9 : 0); if smoke.length=0;i--){ const q=lava[i]; const isA=q.heat>0.05; // cooling always though // cool q.heat -= (0.0032+q.c0*0.0045)*dt; if(q.heat<=0){ q.heat=0; q.fade-=(0.006+q.c0*0.004)*dt; if(q.fade<=0){remove;continue;} } if(!q.settled){ q.vy+=G*dt; q.vx+=windV*0.012*dt; // gentle drift — only airborne? if settled no. airborne & grounded: grounded sliding also wind? apply only if !grounded q.x+=q.vx*dt; q.y+=q.vy*dt; // bounds if(q.x<8||q.x>W-8||q.y>H+40){remove;continue;} const xi=q.x|0... clamp 0..W-1? use Math.min(W-1, Math.max(0, (q.x+? )|0)) — but ground array covers up to W+7; for x>W-1 clamp to W+6? Let idx=Math.round? Use Math.min(W+6, Math.max(2, q.x|0)); ground array length W+8 covers. const gy=gArr[idx]; // vent absorption if(q.vy>0.5||q.grounded){ if(Math.abs(q.x-cx)<24 && q.y>floorY-3){ p=(p<50?p+0.09:p); remove; continue; } } hmm grounded inside crater handled each frame — fine (above check covers grounded). if(!q.grounded){ if(q.y>gy-1){ // landing q.y=gy-1; if(q.vy>2.4){ q.vy=-q.vy*0.28; q.vx*=0.7; spawnPuff(q.x,q.y,q.vx); q.grounded=false; // bounce continues airborne } else { q.vy=0; q.grounded=true; } } } else { q.y=gy-1; const s=sArr[idx]; q.vx += (-s*0.55)*dt; // slide downhill q.vx *= 1-0.12*dt? use Math.pow? approx q.vx-=q.vx*0.12*dt; if(Math.abs(s)<0.06 && Math.abs(q.vx)<0.15){ q.settled=true; q.vx=0; } // crater absorption check above handles } } else { q.y=gArr[idx]-1; } // settled stick (cheap recompute) ``` Wait settled particles: skip motion; but heat cooling continues; also if terrain... static. Also settled check idx each frame for y — fine. Hmm bounce: after bounce q.grounded stays false, and position y set to gy-1 then next frame moves by vy negative (up) — ok. But we set q.y=gy-1 before bounce velocity assignment — order: compute newY, if y>gy-1: handle. Set y=gy-1 and vy bounce (negative) → next integration moves up. Good. Edge: absorption check placed before landing check so falling into crater absorbed even if y>floor: condition q.y > floorY-3 && |dx|<24 && (q.vy>0.5 || q.grounded). But falling fast might jump past floorY-3? We check after move: if y now > gy-1 (landed) we'd snap... put absorption check before landing check but after move — if y crossed into crater region it satisfies q.y > floorY-3 (since floor region) → absorbed. If fast (vy 10*dt=25px) from above crater: y jump from above crater to below ground → q.y > floorY-3 true (way below) → absorbed ✓. Also lateral: particle sliding along outer slope across vent edge? |dx|<24 catches. Absorb visual: tiny spark: spawn smoke small hot? Let's add: if(Math.random()<0.3) spawn 1 tiny lava spark upward vy -2.5 heat 1 r 1.5? Could chain more particles but bounded (each absorbed 30% chance spawn 1 which itself may fall back... could loop: spark launched vy-2.5 goes up ~9px falls back absorbed → 30% spawn again — geometric: expected spawns 1.43 per absorb — fine, creates constant crackle at vent. But pressure not added by sparks (they're absorbed → +0.09 → could chain-erupt? spark absorb adds pressure if p<50 — tiny. OK.) Hmm risk: infinite loop recursion? spawn occurs outside absorb call — fine. Also puff smoke on hard landing: spawnPuff: smoke.push({x,y:-2, vx:q.vx*0.35, vy:-(0.8+Math.random()*0.8), r:4, rMax:16, a:0.22, ph:rand, age:0, life:60+rand*50, w:1})? Actually reuse smoke object shape with w=1 (warm) & short life. Rate-limit: === answer so far ===