### PARTIAL OUTPUT (time limit hit; the model was still generating) ### === thinking so far === The user wants a 2D volcano physics simulation in a single self-contained HTML file. Let me plan this carefully because there's a lot to get right: **Core requirements:** 1. Side view: volcano cone with crater, ground, sky 2. Pressure meter that builds over time, maxes out → eruption, resets, builds again 3. Lava particles launched with varied velocities, gravity arcs, land on slopes, slide/settle downhill 4. Lava cooling: bright yellow-orange → dark rock → disappear 5. Ash cloud smoke particles rising from crater, drifting with wind 6. Three sliders: eruption power, wind strength/direction, particle amount 7. Live counter: active particles + FPS, smooth at 1000+ particles **Technical design decisions:** *Terrain/slope collision:* The volcano needs a heightmap function `terrainY(x)` that defines the cone profile. For performance with thousands of particles, I'll pre-sample the terrain into a Float32Array (e.g., one sample per pixel or every 2px) and do fast lookups. Particles land when y >= terrainY(x), then they slide downhill based on slope. *Cone profile:* A classic stratovolcano shape — concave slopes steepening toward summit, with a crater notch at the top. I can build it from a function: base height plus a cone shape `h = coneHeight * (1 - |x - cx|/baseRadius)^k` with power k ~ 1.6 for concave flanks, then carve a crater dip near the summit. Also add some gentle noise for natural look, but keep it smooth so sliding works. Actually keep terrain smooth-ish (small noise) — jitter in heightmap can make particles jitter. I'll add slight noise but smooth it. *Particle system performance:* For 1000+ active particles (the "particle amount" slider should go up to maybe 2500), I need: - Object pools (no GC churn). Pre-allocate arrays. - Use typed-array-style plain arrays or classes in a pool. Class instances are fine; the key is reusing them. - Render: draw each particle as a small rect/circle. `fillRect` is fast. Circles via arc are slower. For glowing hot lava, I could draw with fillRect and rely on slight alpha. For a nice look, hot particles get a radial glow — but radial gradients per particle per frame are expensive. Better approach: pre-render glow sprites (offscreen canvases) at several heat levels, then `drawImage` them. drawImage of small sprites is fast. I'll pre-render maybe 8 heat-level sprites for lava and a few for smoke, then drawImage scaled. With 2500 particles that's 2500 drawImages/frame — that's generally OK on modern hardware at 60fps, especially small sprites. Alternatively, use direct pixel manipulation via ImageData for lava particles — very fast but no glow blending. Hmm. Actually a solid approach: composite rendering in two passes: - Terrain, sky: draw each frame (or cache sky + static terrain as offscreen canvas, redraw only when needed — terrain is static! I can cache terrain+sky to an offscreen canvas and just drawImage it once per frame. Good optimization.) - Lava particles: drawImage sprites with per-particle size/heat. - Smoke: drawImage sprites with alpha. To make lava look like it accumulates on slopes: when a lava particle settles, instead of deleting it immediately, I could "paint" it into a persistent lava field canvas (a static accumulation layer) with its current cooled color. That gives a beautiful visual record of eruptions — dark rock builds up on the slopes over time. Then particles get recycled. This is a great "signature moment": the mountain visibly darkens with each eruption's deposits. I'll paint the settled particle into an offscreen "deposit" canvas (same size as terrain area) as a small dark blob. Over many eruptions it accumulates. Maybe occasionally let it fade slightly? No — rock persists; that's realistic and memorable. But it could fully cover the mountain... I could paint it with some transparency and dark color, and maybe slowly fade the deposit layer very gradually over minutes (like weathering). A slow fade of like 1/(many seconds) toward transparent would keep it from saturating. Or just let it build — the user can reload. I'll apply an extremely slow fade (destination-out with tiny alpha every few seconds) so it reaches an equilibrium. Actually simpler: when painting deposits, use low alpha (~0.25), and every N seconds do a very low-alpha clear of the deposit layer (destination-out at 0.01). That gives dynamic equilibrium — old rock weathers away as new lava arrives. Nice. Hmm, but deposit layer interacts with sliding: settled lava particles paint then vanish, so terrain doesn't change. Fine — real lava films are thin anyway. *Sliding behavior:* When a particle lands (y >= terrainY), it enters "sliding" state: it moves along the surface downhill. Downhill direction = sign of slope at its x. Velocity along surface: give it initial residual velocity, damp with friction, and only keep sliding if the slope is steep enough relative to friction (angle of repose). Simple approach: while sliding, apply gravity's tangential component `g * sin(theta)` projected onto the surface, with friction opposing motion; particle sticks when speed < threshold and slope is shallow. Steeper slopes near summit → lava flows further down. I'll compute slope from heightmap samples. Also give cooling: while sliding it cools faster (spreading out), eventually freezes (stops) — then paint deposit and recycle. Also viscosity: as it cools, friction increases, so flows start fast and stop. That's physically evocative. Also particles can bounce off the surface when impact velocity is high (molten spatter): on landing, if vertical speed high, reflect with restitution and lose energy; else start sliding. This makes the ballistic phase look right — some rocks bounce down the slope. *Cooling lifetime:* Each lava particle has `heat` from 1 → 0. Heat decays over time, faster when sliding (spreading) or based on random per-particle rate. Color maps heat: - heat ~1: white-yellow core (255, 240, 150) - ~0.7: orange (255, 150, 40) - ~0.4: deep red (200, 60, 20) - ~0.15: dark red crust (90, 30, 20) - 0: near-black rock (40, 28, 24) When heat hits 0 (or particle age ends), if it's in flight, just fade out; if settled on surface, paint deposit then recycle. Also flying particles that cool in air become dark "bombs" — they can still fall and land, painting a dark blob. Nice touch: volcanic bombs stay dark. So flying lava that cooled should still collide with terrain and paint. Yes — keep collision for all lava particles regardless of heat; cooling only changes color and lifetime. A cooled airborne particle hitting ground → paint dark deposit, recycle. Heat sprite lookup: precompute sprite index from heat (quantize into, say, 20 levels) — or compute color directly and draw colored circles? For glow, sprite approach with pre-rendered radial-gradient blobs at each of N heat levels is best. I'll pre-render ~24 lava heat sprites (radial gradient, white-hot core → transparent) and ~12 smoke sprites (soft gray puffs). drawImage with per-particle scale. To avoid `Math.floor` costs... negligible. *Ash/smoke:* Smoke particles spawn at crater during and after eruption, rise (buoyancy: negative gravity partial), expand over time, fade out, drift with wind. Wind slider (-10..+10) pushes smoke and also slightly affects lava arcs (drag). Smoke should also billow: size grows, alpha decreases. Spawn rate tied to eruption intensity and pressure. Also a persistent weak plume when pressure is high (pre-eruption fumaroles) — nice storytelling detail: as pressure builds, smoke wisps escape the crater. *Pressure meter:* Builds at a rate (maybe slightly randomized, with small tremor jitter near high pressure). When it hits 1 → eruption event: over an "eruption duration" (~2–4s), spawn lava particles in bursts from crater with velocities scaled by power slider (slider 0.5–2×). Velocity cone: mostly upward with spread; higher power → higher speeds and more violent spread. Pressure resets to 0 quickly during eruption then rebuilds. Also the meter UI: a vertical/horizontal gauge with needle, and during eruption it spikes to max then drops. I'll draw the meter on canvas (part of the HUD) — or in DOM? Canvas HUD keeps it unified and I control typography. I'll draw HUD in canvas: pressure bar with tick marks, FPS, particle count. Actually mixing DOM for sliders + canvas HUD is fine. Let me draw pressure meter on canvas near the volcano base (like an instrument), and stats top-left in canvas. Sliders in DOM overlay panel. *Eruption sequence:* Eruption is a timed event with intensity envelope (fast attack, decay): intensity = f(t since eruption start). Lava spawn rate = base * intensity * particleAmountMultiplier. Crater "flash" at start (bright glow at vent), screen shake maybe subtle (translate canvas slightly — a small, brief shake adds impact; keep it short and modest). Also initial shockwave? Could do a translucent expanding ring... maybe a bright vent flash + smoke plume surge is enough. Add rumbling: particles on slopes get slight jitter while eruption active (tremor). Nice touch. *Wind:* Slider from -10 to +10. Affects smoke strongly, lava slightly (small drag acceleration proportional to wind on airborne particles). Also wind should be visualized: maybe drifting streaks in the sky? Could add faint wind streak particles... Keep scope sane; smoke drifting shows wind. Maybe a small flag/indicator in HUD showing wind arrow. I'll draw a small wind gauge arrow in the HUD that responds live. *Particle amount slider:* Controls lava spawn multiplier AND smoke count; but the requirement says simulation must stay smooth with at least 1000 active particles. I'll make slider range e.g. 200–3000 particles (as a budget/cap for lava). Default maybe 1400. Pool size = max (3000 lava + 800 smoke). With pooling and sprite rendering it'll hold 60fps. Let me also ensure "particle amount" affects the eruption immediately — spawn rate scales with the setting, and effective cap. *FPS counter:* exponential moving average of 1/dt, update display every ~0.25s. **Visual design — making it look authored:** I want this to feel like a crafted natural scene, not a flat diagram. Ideas: - Time-of-day: a dusk sky? A deep twilight sky with warm horizon gradient... wait, I avoid decorative gradients, but a sky gradient is a *representational* gradient (an actual sky), which is legitimate — it depicts atmosphere, not decoration. A dusk sky (deep blue-gray at top → warm amber near horizon) with the glowing lava contrast is gorgeous and justified. Yes: dusk/dawn scene gives dark backdrop making the glowing lava pop dramatically. The dark mountain silhouette with incandescent fissures is the classic Arenal/Mt. Pelée imagery. - Stars? Faint stars in the upper sky — tiny dots, subtle. Adds atmosphere cheaply. A few dozen faint stars drawn into the cached sky layer. - Moon? Maybe a thin crescent — could be nice but risk of kitsch. A small pale moon disc low near horizon... I'll include a small moon with slight glow, subtle. Hmm, keep it minimal — stars only, plus maybe a very subtle moon. I'll do stars; skip moon to keep the composition focused on the volcano. - Ground: dark plains with subtle texture, maybe faint distant ridges silhouette layers for depth (two parallax-ish silhouette hills behind the volcano — they're static, drawn into the cached background). - The volcano: dark rocky silhouette with subtle stratification bands (darker strata lines) and a slightly lighter rim on the windward... Actually since the scene is dark, the cone should be a dark charcoal mass with faint ambient detail: strata lines, some rocky texture noise. Plus a warm rim light on crater edge? A faint ember glow inside the vent (visible magma in the crater throat that brightens as pressure rises!) — that's a great storytelling detail: the crater throat glows progressively as pressure builds, telegraphing the eruption. YES — this is the "pressure made visible" idea: the vent glow intensity = pressure. Plus faint smoke wisps at high pressure. Signature moment. - Also maybe faint ember particles drifting from vent at high pressure (pre-eruption sparks). - Ash cloud: smoke sprites from grayish-dark to light gray as they expand (ash clouds lighten as they widen? Actually volcanic ash plumes are dark brown-gray at base, lighter at top). I'll give smoke a color ramp from dark (young, dense) to pale gray (expanded), plus slight warm tint near vent. - Eruption lightning! Volcanic lightning is a real, spectacular phenomenon. Occasional lightning bolts inside the ash plume during peak eruption — jagged lines drawn with quick fade. That's a memorable touch and cheap to implement (random polyline from plume region, drawn 2-3 frames). I'll add it — it's a genuine volcanic feature (pyrochromatic lightning). Subtle, brief, inside the plume only. **UI layout:** - Canvas full-window. - DOM overlay: a control panel (bottom-left or right) with three sliders. Style: dark glass panel, custom-styled range inputs (thin track, custom thumb), labels with values. Typography: I'll pull a distinctive font from Google Fonts CDN (allowed — external CDN references are fine). Something with character for the title, e.g. "Fraunces" or a display serif for the title "VULCAN" ... The user said "No external libraries: everything inline" — that refers to JS libraries; a font link is a stylesheet not a library... risky interpretation. "everything inline, rendered on a canvas" — I think they mean no JS libraries; the canvas does the rendering. A Google Fonts is an external resource though. To be safe and honor "no external" strictly, I'll use no external anything: pure system stack but crafted — but I dislike Arial/system-ui as identity font... The constraint conflict: user explicitly says no external libraries, everything inline. Fonts aren't libraries, but spirit of the request is self-contained. I'll respect the strictest reading: no external requests at all. For typography, I can use font stacks with character: `Georgia, 'Times New Roman', serif` for the title gives a classic naturalist/journal feel, and letter-spaced small caps for labels via `font-variant: small-caps` or uppercase + tracking. A serif title ("Vulcan / a pressure study") in warm ivory on the dark scene looks authored. For the HUD numerals on canvas, use a monospace stack (`'Courier New'`... hmm) — canvas HUD text: I'll use `font: '13px ui-monospace, Menlo, Consolas, monospace'` — fine for readouts (instrument feel). Title in DOM panel: serif with wide tracking. That's a deliberate typographic voice without external fonts. Actually — since everything's rendered on canvas per the user, maybe they'd like HUD on canvas too. But sliders must be DOM inputs (input[type=range]) unless I build custom slider interactions on canvas. Building custom canvas sliders is more work and less accessible; DOM sliders overlaid are standard and the user explicitly asked for "three sliders the user can move while it runs" — DOM range inputs are the sane choice. I'll do DOM panel for sliders + title, canvas for scene + readouts (pressure gauge, stats). Panel position: top-right or bottom-left. The volcano is centered; HUD stats top-left; panel bottom-right maybe, or right side. Bottom-right panel with sliders. Pressure gauge drawn on canvas — where? Left side vertical gauge looks instrument-like, or bottom-center horizontal. I'll draw a vertical pressure gauge on the left edge with tick marks and a needle — like an old brass manometer. Label "PRESSURE" vertically. And during eruption it maxes, flashes red-ish... use warm amber. Colors of the UI: warm ivory text, ember amber accents, dark translucent panels — coherent with the scene. Wind control: slider -10..+10, center 0. Show current value and direction arrow in panel (or canvas gauge). In the panel, show "wind −4" with a small arrow character (→/←) — text arrows are fine (not emoji). Or draw the wind needle on canvas near the gauge. I'll add a small canvas wind vane next to pressure gauge: a horizontal line with a moving indicator. Actually simpler: show numeric value in the DOM label that updates, and let the smoke show it. Plus I'll draw wind streaks in the sky (thin faint horizontal streaks moving with wind) — this makes wind perceptible even without eruptions and animates the sky. Subtle: 20 streak particles, alpha ~0.05-0.1, length proportional to |wind|. When wind is 0 they vanish. Nice. **Physics details:** Gravity g = ~900 px/s² (tune). Lava launch from crater vent at (cx, ventY): - speed = base * power * (0.7 + 0.6*rand) - angle: mostly vertical with spread: θ = -90° ± ~35°, biased random (gaussian-ish). Higher power → narrower, faster column plus some wide spatter. I'll mix: 70% "jet" (narrow cone ±15°), 30% "burst" (±55°). Also lateral randomness from wind. - Actually realistic fountaining: velocities 250–650 px/s vertical. Tune so with high power lava reaches near top of screen and lands on slopes at varying distances. Airborne integration: semi-implicit Euler with dt clamped. Add wind drag: ax += wind * k (k small, ~2–8 px/s² per wind unit... make wind slider -10..10 and drag = wind * 6 px/s² for lava; smoke uses more). Also maybe slight drag proportional to velocity for lava (negligible; skip). Collision: sample terrainY(x). If y >= terrain, check impact speed. If vy > threshold (e.g. 140) and heat > 0.3 (still molten — molten splats, rock bounces? actually rock bounces, molten splats!). Hmm: molten lava hitting slope at speed → splats and sticks (starts sliding with damped velocity); cooled bomb → bounce with restitution 0.3, some friction on vx. But visually, bouncing dark bombs look great. Molten hot splat: kill vertical velocity, convert to sliding with tangential velocity = projection of velocity onto slope surface * 0.6 (viscous damping). Let me do: if hot (heat>0.35): splat — enter sliding with strong damping. If cool: bounce with restitution 0.35, tangential friction 0.7; when bounce energy too low, enter rolling/sliding briefly then stop & paint. Sliding state: position on surface (x, terrainY(x)). Compute slope s = (terrainY(x+2)-terrainY(x-2))/4 — note canvas y grows downward, so terrain height decreases... let me define h(x) = ground elevation above sea level in px, and terrainSurfaceY(x) = H - h(x) for drawing. For physics, work with elevation. Downhill = direction of decreasing elevation. Tangential gravity accel = g * sin(θ) where sinθ ≈ -s/√(1+s²) in the downhill direction. Simpler: vx += g_elevation_effect... Let me just do: slopeAngle-based: downhillAccel = g * (dh/dx in downhill direction)/... Cleanest: let e(x) = elevation. Particle at x moving with velocity u (dx/dt). Acceleration along surface: a = -g * sinθ * sign? The tangential component of gravity is -g * ∂e/∂x / √(1+(∂e/∂x)²) (pointing toward decreasing e). So u += (-g * e'(x) / √(1+e'²)) * dt. On flat ground e'=0 → no accel, friction stops it. Good — this naturally handles both slopes and flats. Friction: u -= u * μ_eff * dt (viscous). μ_eff grows as heat drops (cooler = stickier): μ = 1.5 + (1-heat)*6. Also static condition: if |u| < 4 and |g*tanθ|-ish below friction threshold → freeze: paint deposit, recycle. Threshold: slope must be shallow enough; approximate: if |u|<4 and |e'(x)| < 0.35 (about 19°, angle of repose-ish) → freeze. On steep cone flanks e' can be ~2-3 (60°+) near summit — lava slides far. But the base apron is shallow → freezes there. Also while sliding, keep y glued to surface: y = terrainSurfaceY(x) - particleRadius. Paint deposit when frozen (or when heat reaches 0 while sliding → freeze immediately). Heat decay: heat -= dt * coolRate; coolRate random per particle in [0.15, 0.45]; sliding multiplies by ~2.2 (spreads & crusts). Lifetime also capped (e.g. 6–14s) so particles eventually vanish even if flying forever (with wind they can drift off-screen anyway — recycle when off-screen x or below ground... below ground can't happen with collision; off screen sides or above top → just let them fly, recycle when out of bounds by margin). Wait — must handle smoke similarly: rises, expands, fades; recycle when alpha < 0.01 or off-screen. Deposit painting: draw a small circle (r ~ particle size * 1.1) on deposit canvas at (x, y) with color = cooled rock color with alpha 0.28 + slight variation. Deposit canvas starts transparent; composited between background and particles? Order: sky/stars (cached bg incl. terrain silhouette? terrain drawn first, then deposits ON TOP of terrain, then hot lava particles above, smoke above all but behind... Let me define layering: 1. Sky cached layer (sky gradient, stars, distant ridges). 2. Foreground terrain (cached separate canvas: the cone + ground with strata) — includes vent glow hole? The vent glow changes with pressure, so draw terrain first, then vent glow dynamically over the crater area (radial gradient with alpha = pressure) with 'lighter' composite, clipped to crater region. 3. Deposit layer canvas (accumulated cooled rock) — drawn over terrain. 4. Lava particles (sprites, composite 'lighter' for hot ones so they glow additively over dark scene; cool ones normal composite). Hmm mixing composite modes per particle: set globalCompositeOperation 'lighter' only when heat > 0.55, else 'source-over'. Batch: sort not needed; just switch when needed. Simpler: always draw hot with lighter, cooled with source-over — just set the op per particle (state changes are okay-ish; could sort by op but 2500 state changes per frame is fine on modern browsers honestly, though it does cost. Alternative: two passes over particles? Iterating the pool twice doubles loop cost but loops are cheap. I'll do: pass A draws hot (lighter), pass B draws rest (source-over). One branch-free loop each. Actually just one loop with an op check is fine; browsers handle composite op changes OK. Hmm, to be safe for 60fps at 3000 particles, I'll do two loops — memory-local pool iteration is trivial. Fine, two passes. 5. Smoke: source-over with alpha, drawn... smoke plume should render behind hot lava bombs? Ash plume rises above crater; bombs punch through it. Draw smoke BEFORE lava so bombs appear in front. But smoke should be over terrain obviously. Also smoke over deposits. Order: deposits → smoke → lava. Good. 6. Vent glow, eruption flash ('lighter'). 7. Lightning. 8. Wind streaks (very faint, 'lighter' or source-over low alpha). 9. HUD (pressure gauge, stats) — canvas-drawn. 10. Screen shake transform applied to world layers (1–9 world content), HUD unshaken. Implement via ctx.translate for world drawing. Sprite pre-render: - Lava heat ramp sprite set: for i in 0..23, heat = i/23, create canvas 32×32 (scaled at draw time), radial gradient: core color (white-yellow at high heat → dark red → near black), radius with soft falloff. Actually I want particles to look like glowing blobs with hot core: gradient stops: 0: bright core, 0.35: mid color, 1: transparent-ish darker. For cooled particles, they become dull rock — sprite with soft dark edges. Let me define color ramp function heatColor(t): t=1: rgb(255, 245, 190) t=0.85: rgb(255, 200, 80) t=0.6: rgb(255, 120, 30) t=0.4: rgb(215, 65, 18) t=0.2: rgb(120, 32, 14) t=0: rgb(52, 36, 30) Interpolate through stops. Sprite gradient: center = lighten(color, +30%), edge = darken(color), alpha edge 0 for hot (glow fade) — for hot sprites use radial gradient from color → color(darker) → transparent. For deposit color use heat at freeze time. Smoke sprites: 10 variants of soft noise puff: radial gradient with slightly irregular shape (draw several overlapping blurred circles) in gray tones. Tint per particle from dark (young) to pale (old). I'll pre-render one or two neutral puff sprites and use globalAlpha + maybe per-particle tint via two pre-built ramps (dark puff sprite set and light puff sprite set, cross-fade by age — or just 12 ramp sprites like lava). Do ramp: smoke age t 0→1: color from rgb(70,60,58) → rgb(180,175,170). 12 sprites with soft alpha. Smoke physics: vy: buoyancy -g*0.25 rising plus initial upward velocity from eruption; expand size 6→60px; alpha in/out (fadeIn 0.15, hold, fadeOut as size grows: alpha ~ (1-age)^1.2 * 0.5). Wind: vx += wind * 14 * dt (stronger response than lava) plus slight turbulence: vx += sin(noise) jitter — use cheap pseudo-noise: sin(age*3 + seed)*k. Crater geometry: vent opening width ~46px at summit. Carve crater: elevation dips in [cx-w, cx+w] with smoothstep to a floor below rim. Particles spawn at vent floor center. Vent glow: radial gradient centered slightly below rim, radius ~55, alpha 0.15+0.75*pressure² in 'lighter', warm color. **Pressure model:** pressure ∈ [0,1]. Build rate: base 0.055/s * (1 + 0.35*sin(t*0.13) jitter) → full build ~18s. Slight acceleration as it grows? Volcanoes: pressure accelerates near failure — rate increases nonlinearly: dp = rate * (0.5 + pressure). When pressure ≥ 1 → erupt(). During eruption: eruptionT += dt; envelope env = exp(-eruptionT/1.6) * smooth attack (min(1, eruptionT/0.25)); pressure drains: pressure = max(0, 1 - eruptionT/1.4) roughly — drains over ~1.4s then rebuild from 0 (plus maybe slight residual). Eruption lasts until env < 0.03 → eruptionActive=false. Lava spawn during eruption: spawnAccumulator += dt * spawnRate * env * powerMult; spawnRate base ~ e.g. 420 particles/s scaled by particleAmount slider (slider 0.2–2.5 multiplier; default 1). Cap active lava at budget: if activeCount >= budget, skip spawn (pool full). Budget = round(200 + slider 0..1 * 2800)? Slider "particle amount" 0–100 → budget 250–3000. Default ~60 → 1900. Plus smoke budget separate ~600, spawn smoke: rate ~ env * 60/s * slider. Also tremor: while eruptionActive, camera shake amplitude ~ env * 5px; also slope particles jitter. Pre-eruption signs: pressure > 0.55 → weak fumarole smoke wisps (rate ~ (pressure-0.55)*3/s), vent glow rising, occasional tiny ember sparks (small hot particles with low velocity that pop out and die quickly). **Lightning:** during eruption when env high, small probability per frame (e.g. if rand < env * 0.04 per frame at 60fps → frequent-ish; tune to ~ every 0.5-1.5s at peak). Bolt: generate jagged polyline from a point in the plume (above vent, within smoke region) downward/branching, 6–9 segments, random offsets; draw with 'lighter', lineWidth 2 core white-violet (volcanic lightning is bluish!) — pale blue-white (200, 210, 255) fits and looks distinct from lava's warm palette; that contrast is authentic. Bolt life ~0.12s with flicker alpha. Also maybe a secondary branch. Keep subtle. **Terrain construction:** Canvas W×H = window size (handle resize: rebuild terrain caches on resize; particles positions may exceed — acceptable, terrain heightmap resampled; particles in flight adjust fine). Elevation function e(x) (px above bottom): - ground baseline: groundH = 0.09*H + gentle noise ±4px (rolling ground). - Cone: cx = 0.5W (maybe 0.52W slightly off-center for composition — off-center is more authored; let's put summit at 0.46W so there's breathing room on the right where wind-blown ash drifts. Hmm but panel bottom-right... fine). - coneH = 0.42H. baseR = 0.40W. - shape: cone(x) = coneH * (1 - (|x-cx|/baseR))^1.7 for |x-cx| < baseR, else 0. This gives concave-up flanks? (1-u)^1.7: at u→0 derivative 1.7 — steep near summit (slope infinite at summit in this param), gentle at base. Good stratovolcano profile. - crater: subtract a smooth notch: craterDepth = 0.055H; width cw = 0.045W... Let me carve: for |dx| < craterW: e -= craterDepth * (cos(π dx/craterW)*0.5+0.5)? That makes a dip deepest at center. Actually crater should be a bowl: rim high at |dx|=craterW edges, low center. So subtract bumpAtCenter: e -= craterDepth * (0.5+0.5*cos(π * dx/craterW)) — at dx=0 subtract full depth, at edges 0. But then summit peak height: cone at |dx| small ≈ coneH(1-|dx|/baseR)^1.7 minus bowl → rim height slightly below coneH. Fine — the crater rim will have two little peaks. Good look. - noise: add smooth noise: sum of sines: n(x) = a1*sin(x*0.011+φ1)+a2*sin(x*0.023+φ2)+a3*sin(x*0.05+φ3), amplitudes ~ (7, 4, 2.5) px, but scale noise down on the cone (×0.6) to keep slopes clean? Slight roughness on slopes is good for lava catching. Keep small everywhere: total ±8px on ground, ±4 on cone. Multiply noise by mask 1 everywhere; small values fine. - Sample into heightmap array with step 2px for lookup speed: `elev[i]`, plus for slope compute from neighbors at draw/physics time (finite difference of the array — cheap). terrainSurfaceY(x) = H - elev(x) (y coordinate). For physics I use elevation directly. Ground to the left/right continues to screen edges; the cone base blends. Also distant ridges behind: two silhouette layers at lower height, drawn in sky cache with bluish dark tones (atmospheric perspective). Drawing the terrain into cache: fill path along surface down to bottom; color: very dark warm gray-brown (#17120e-ish) with subtle vertical strata: draw strata as slightly lighter/darker horizontal-ish curved bands clipped to cone... simpler: after filling cone silhouette, apply a few semi-transparent darker strokes following elevation contours (for several elevation levels, draw a line along surface offset downward by fixed px with low alpha). And faint noise speckle (random dots) for rock texture at low alpha. Edge highlight: 1px lighter stroke along surface top (rim lighting from sky) with warm faint color, stronger on crater rim. This gives crafted look. Ground texture: sparse dark speckles. Sky: gradient from #0b1026 (deep blue) top → #1a1430 mid → #3d2a2a...? Let me craft dusk palette: top #070b1a, mid #141830, horizon #4a2f24 → thin warm line #8a5a33 near horizon behind ridges. Subtle. Stars: ~90 dots random in upper 60%, alpha varying 0.2–0.8, size 1–1.6, slight twinkle? Static in cache (twinkle would need dynamic draw — could add 6 twinkling stars dynamic; skip for perf simplicity, static fine). **HUD design (canvas):** - Top-left: "PARTICLES 1 842" and "FPS 60" — monospace, small caps labels, values in amber. Also maybe eruption status line: "STATUS: BUILDING / ERUPTING". - Left edge: vertical pressure gauge: a slim column ~18px wide, 140px tall with border, ticks every 10%, fill from bottom with warm color intensifying to red at top... gradient inside the meter — a meter fill going amber→red as it fills is representational (pressure danger), acceptable and standard for gauges. I'll fill solid amber with the top portion (last 25%) turning redder — implement as two-segment fill or gradient — fine, it's a gauge. Needle line at current level. Label "PRESSURE" rotated or below. Add small marker at eruption threshold. - Also mini wind indicator next to gauge? The panel shows wind value; plus sky streaks show it. Enough. DOM panel (bottom-right): dark translucent (rgba(10,9,12,0.72)), 1px border rgba(255,255,255,0.08), border-radius small (10px), backdrop blur slight. Title block: small caps "VULCAN · 2D" ... Title: "VESUVIUS" or invented "MONTE EMBER"? I'll title it "VULCANO — pressure study" hmm. Keep tasteful: serif italic title "Volcano" with letterpress feel: font-family Georgia 'Playfair'-less. Title: "Vulcan's Forge"? Simpler: "ERUPTION" small-caps serif with thin rule under, subtitle "a live pressure study". Then three sliders: - ERUPTION POWER: 0.5–2.0 (default 1.0) → affects launch speed & burst rate. - WIND: −10..10 (default 0? slight 2 gives life; default 2) — actually default 0 cleaner; but a gentle default breeze makes ash drift immediately → choose default +3. - PARTICLES: 250–3000, default 1400. Display value. Each slider row: label row (name left, value right in mono amber), input range full width. Custom styling: -webkit-slider-thumb: 14px circle ivory; track 3px rgba white 0.15 with filled portion? Filled portion needs JS to set background gradient on track (linear-gradient two-stop hard cut — that's a functional fill indicator, fine and standard). I'll do it: track background: linear-gradient(90deg, amber p%, rgba(255,255,255,.14) p%) updated on input. For wind (bidirectional) keep simple neutral fill from center? Simpler: just uniform track, value shown numerically. OK do the filled track for all, from left; for wind it still reads fine. Also a small hint line: "eruptions trigger automatically when pressure maxes" tiny muted text. Maybe also a "TRIGGER NOW" button? Not requested... "when it maxes out, the volcano erupts" — automatic. Adding a manual trigger button is extra but useful and harmless — small text button "Trigger now". It's beyond spec but genuinely useful for interactivity. I'll add it subtly (also maybe a "reset slope" to clear deposits? skip). Keep one small button. **Performance considerations recap:** - Terrain cached: 2 offscreen canvases (sky+ridges, terrain). - Deposit canvas: offscreen, additive painting; slow weathering: every 4s apply 'destination-out' fill alpha 0.02? That fades deposits uniformly. Equilibrium between paint rate and fade. Painting during heavy eruption: thousands of small arcs — use fillRect with rotation? Small circles via arc per frozen particle — freezing happens maybe hundreds per eruption, spread over time; fine. - Particle pool: preallocate arrays: use plain objects in array with `active` flag, freelist stack for recycling. Iterate full pool each frame (3000 lava + 900 smoke = 3900 objects) — trivial. - Sprite drawImage: lava sprite base 28px, draw scaled to particle size (varies 3–9px * 2 for glow ~ 2.2× radius). Hmm glow radius bigger: drawImage size = size*3.2 for hot glow. Fine. - Avoid per-particle save/restore; use setTransform? drawImage(img, x-w/2, y-h/2, w, h) simplest. Alpha via globalAlpha per particle (cheap). - Text HUD: draw every frame; text rendering is fine (few strings). Update FPS text every 0.25s to avoid layout thrash (canvas has no layout; just avoid re-reading; cheap anyway). - devicePixelRatio: cap at 1.5 for perf; scale canvas. I'll support dpr up to 2 but scale sprite drawing accordingly... Simpler: use dpr = min(devicePixelRatio, 2) and set canvas width = W*dpr, ctx.scale(dpr,dpr) once (reset transform each frame with setTransform(dpr,0,0,dpr,0,0) + shake translate). drawImage positions in CSS pixels. Good. Resize: rebuild caches; keep particle pool size fixed (based on max budget 3000+smoke). On resize, remap particle x positions? Just clamp out-of-range; minor artifact acceptable. Debounce resize. **Numbers tuning (important for feel):** - H ≈ 800 typical. Gravity 1000 px/s². - Vent position: cx = 0.47W, vent elevation = elevation at crater center. Launch y = surfaceY(cx) - 4. - Power slider p ∈ [0.5, 2]. Launch speed: v = (260 + 340*p) * (0.75 + 0.5*r) → at p=1: 600*(0.75..1.25) = 450–750 px/s vertical-ish → apex h = v²/2g ≈ 100–280px above vent. Hmm I want dramatic fountains at high power reaching maybe 400+ px. At p=2: v up to (260+680)*1.25=1175 → apex ~690px. Good range. Angle: jet: θ = 90° ± 12° (from vertical), burst: ±50°. Let me define: u = random(); if u<0.72 → narrow. Also slight bias: multiply horizontal by 1. - Lava particle radius: 2.2 + 3.2*rand (visual size; a few big "bombs" up to 7px: 8% chance size 5–8). - Smoke spawn: at vent, initial vy -40..-140, vx ±30 + wind*8; size start 8–16 growing to 30–90; life 3–7s; alpha peak 0.42. - Wind effect on lava: ax += wind * 9. Smoke: wind * 26 plus turbulence. Sliding tuning: friction viscous: u *= exp(-μ*dt)? Use u -= u*μ*dt with μ = 2 + (1-heat)*7. Sliding heat decay ×2. Freeze condition: |u| < 6 && |slope e'| < 0.30 → freeze (paint, recycle). Also freeze if heat <= 0 regardless (crusted in place — but on steep slope it should still creep? if heat 0 and moving on steep slope, real rock would... it's molten outside? Simplify: heat 0 → freeze instantly). Also cap slide duration. Edge case: particle lands on steep flank near summit, slides down gaining speed — with viscous friction terminal velocity ≈ g*tan-ish/μ; near summit slope huge (e' maybe 3+) → sinθ ~ 0.95 → terminal ~ 1000*0.95/ (2+…) → fast; then flattens near base → friction kills it → freezes on apron. Also sliding particles can re-heat? No. Tremor jitter for settled... settled ones are recycled (painted), so no jitter needed except airborne unaffected. During eruption, sliding particles get small random lateral kick — adds life. OK. **Screen composition:** - Volcano summit around 0.42–0.5 vertical from top? coneH 0.42H means summit at H - (groundH + coneH + noise) ≈ H - (0.09H+0.42H) = 0.49H. So summit roughly mid-screen — leaves sky above for fountain. Good. Distant ridges: heights 0.16H and 0.10H at edges. - Horizon line: ground fills bottom ~9% + cone. **Eruption pressure reset:** "pressure resets and builds again on its own" — after eruption, pressure drains to 0 during first ~1.2s of eruption then rebuilds. Implement: during eruption phase, pressure = max(0, pressure - dt*0.8); after eruption ends, rebuild resumes. Also small chance of double eruption? Keep simple. **Status text:** BUILDING PRESSURE → ERUPTION (with live env) — displayed in HUD. Let me also add heat shimmer? Skip — perf. **Wind streaks:** 26 streak particles: each has x,y in sky region, len = 30+|wind|*10, speed = wind*22 + 40*sign... when wind≈0 hide. Draw thin 1px lines alpha 0.05–0.09 ivory. They wrap around screen. Also streaks only above ridge tops (y < some line) or anywhere — anywhere above ground is fine, subtle. Also near summit they'd cross the cone — over the cone silhouette they'd be in front (weird). Restrict to y < summitY-ish region and y > 40? Just draw them before terrain? Then they're hidden behind cone and ridges — but also behind... sky cache includes ridges; streaks drawn after sky but before terrain → occluded by cone (correct, wind in front of far ridges but behind the mountain — physically streaks are atmosphere nearer than mountain? whatever, occluded looks fine). Actually simpler: draw streaks right after sky layer, before cone. They'll show in open sky. **Ash color & rendering:** smoke sprite ramp 14 levels. Particle: age, life, size grows: s = s0 + (s1-s0)*age^0.7. alpha = 0.5 * sin(π*ageCurve)? Use alpha = 0.55 * (age<0.12 ? age/0.12 : 1 - (age-0.12)/0.88 * 0.95). Composited source-over (dark smoke over dark sky needs to be visible: dark gray on dark blue is low contrast — make plume lean lighter gray-brown: ramp from rgb(58,50,48) young → rgb(165,150,140) old? Old ash clouds catch moonlight/sky light → lighter. Also add slight warm underlit tint near vent for young particles: could tint via second sprite... simpler: young smoke sprite uses warm-dark (rgb(85,60,45)), mid neutral dark, old pale. So ramp: warm dark brown → neutral gray → pale gray. Draw with alpha; on dark sky pale smoke reads well. Also 'source-over'. Maybe draw plume base glow from vent during eruption (lighter radial) to underlight the plume — the vent flash does that: big soft radial 'lighter' gradient behind smoke? Draw vent glow AFTER smoke so it underlights? 'lighter' over smoke brightens them — yes draw vent glow after smoke, giving backlit plume. Order fix: deposits → smoke → ventGlow(lighter) → lava. Hmm vent glow over smoke brightens smoke near vent: nice. And lava drawn after with lighter — glowing bombs pop. OK. **Flash at eruption onset:** at eruption start, spawn a bright expanding radial flash: radius grows 40→180 over 0.4s, alpha decays. Draw 'lighter'. **Lightning generation:** function spawnBolt(): origin = vent + (rand offset ±60, up 60–160 into plume); points: n=7+; each step: x += (rand-0.5)*38, y += 12+rand*18, slight downward bias; maybe one branch from mid point (short 4 segments). Store bolts array with life 0.12, draw with lineWidth 2.2, strokeStyle rgba(190,200,255, alpha), plus glow: draw again lineWidth 6 alpha*0.25. Fine. **Counting:** active lava count tracked via counter variable updated on spawn/recycle. Display active = lavaCount (and maybe smoke count separately: "PARTICLES 1842 + 213 ash"). Show total. Requirement: counter of active particle count and FPS. I'll show both: "LAVA 1842 · ASH 213 · FPS 60". Also cap indicator. **Pool implementation:** ``` const lava = new Array(MAXLAVA); for i: lava[i] = {active:false, ...}; freelist = [indices...]; spawn: idx = freelist.pop(); if undefined → skip. recycle: active=false; freelist.push(idx). ``` freelist as plain array with pop/push — LIFO fine. Smoke same with its own pool/freelist. **Main loop:** requestAnimationFrame with dt = clamp(now-last, 0, 0.033). Fixed-ish step fine. **Slope sampling for particle:** x → i = x>>1? Use heightmap with step STEPS=2: idx = clamp(round(x/2)). e(x) interpolate linearly between samples for smoothness. slope = (e[i+1]-e[i-1])/(2*2). Provide functions elevAt(x), slopeAt(x). Elevation array covers x from 0..W. Build once per resize with the analytic function (including noise with fixed random phases chosen at load — regenerate on resize keeping same phases so shape is stable). Also crater: I need the vent to be a visible notch: bowl depth ~26px, width ~70px. Plus a subtle inner glow. **Drawing terrain silhouette details:** - fill color: create slight vertical shading? Solid very dark with strata lines & speckles & rim light. - Also draw a subtle warm reflection light on cone flanks during eruption? When env high, faint amber rim on crater rim area — the vent glow covers that. OK. Deposit painting color: from heat at freeze: use heatColor(h) darkened: deposit uses rock color: mix heatColor(h) toward dark. For simplicity: depositColor = heat<0.25 ? very dark : slightly-reddish dark crust. Actually the deposit records how cooled the lava was when it froze — flowing lava freezes at heat ~0 → dark. Early splats (heat high) would be bright — but splats slide and cool before freezing mostly. Paint color = heatColor(heat*0.35) — always darkish. Use alpha 0.3, radius size*1.15 + 1. Weathering: every frame with small probability? Use accumulator: every 3.5s: depositCtx.globalCompositeOperation='destination-out'; fillStyle='rgba(0,0,0,0.045)'; fillRect over deposit canvas → fades ~ equally. At that rate deposits halve every ~50s. During eruption painting may outpace. OK. **HUD pressure gauge details:** position left: x=26, y = H*0.5 - 90? Place vertical gauge left-center: x 24–40, y from H*0.5-80 to H*0.5+80 (160 tall). Frame: 1px stroke rgba(255,255,255,0.18), bg rgba(0,0,0,0.35). Fill height = pressure * innerH, from bottom. Fill color: amber #ffb347 up to 0.7, then shift to #ff5a3c above 0.7 (danger). Implement: fill full with amber then overfill top part with red using clip — or gradient per frame (fine, one gradient per frame OK). Ticks: 11 ticks, labels every 0/50/100? tiny. Needle: horizontal line at level extending right 10px, ivory. Threshold mark at top. Label below: "PRESSURE" small caps letter-spaced. Also small pulse when erupting (gauge flashes). Also small text under gauge: state word. Stats top-left: ``` LAVA 1240 ASH 231 FPS 60 ``` mono 12px, labels dim ivory, values amber/ivory. Maybe also "ERUPTION 0.8×" when active. Wind readout also top-left? Panel has it. Fine. **DOM structure:** ```

ERUPTION

an autonomous pressure study

label row + input range ×3

Pressure builds on its own — the volcano erupts when the gauge maxes.

``` Panel styling: position fixed right:20 bottom:20; width 260px. Fonts: title font-family: Georgia,'Times New Roman',serif; letter-spacing .35em; font-weight 400; small size 15px; color #e8dcc8. Subtitle italic 11px muted. Labels: 10px uppercase letterspaced, color rgba(232,220,200,.6); values mono amber #ffb35c. Slider CSS: appearance none; height 26px background transparent; track via ::-webkit-slider-runnable-track height 3px radius; thumb 13px circle #e8dcc8, margin-top -5px; hover thumb border amber. Firefox: ::-moz-range-track/thumb. Fill via JS-updated background gradient on the track... For webkit, styling the track background works via the element's background with appearance:none on input and using background on input itself with border-radius, height 3px centered? Common trick: style input itself as the track: input {appearance:none; height:3px; background: linear-gradient(...); border-radius:2px}; thumb needs bigger hit area: give input height 26px, background drawn via background-image with a centered 3px band? Simpler: use box-shadow? Let me do: input {appearance:none; width:100%; height:26px; background:transparent} input::-webkit-slider-runnable-track {height:3px; background: var(--fill) } and set CSS var --fill per input via style.setProperty on input event: `linear-gradient(90deg,#ffb35c 0 p%, rgba(255,255,255,.14) p% 100%)`. Thumb margin-top:-5px. Firefox: ::-moz-range-track height 3px background: same var; ::-moz-range-thumb similar. Works. Button: ghost style — transparent bg, 1px border rgba(255,255,255,.16), ivory text small caps, hover border amber + text amber. Hide panel on small screens? Keep, add media query for width<560 → panel bottom full-width-ish smaller. **Interaction extra:** clicking on canvas could... splash? Not required. Maybe click canvas → small manual burst of lava at crater? Cute but scope. Skip — sliders + trigger button suffice. Actually a tiny delight: pressing the trigger button counts as eruption. Fine. **Now, code.** Let me write it carefully and completely. I'll structure JS clearly with comments. Must be careful about correctness — no framework, all vanilla. Let me draft key functions: ```js const TAU = Math.PI*2; let W, H, DPR; const canvas = document.getElementById('c'); const ctx = canvas.getContext('2d'); // ---- terrain ---- let elev; // Float32Array length Math.ceil(W/STEP)+1 const STEP = 2; const CXOFF = 0.47; // summit x fraction let cx, ventElev, groundBase; function buildTerrain(){ cx = W*CXOFF; const baseR = Math.min(W*0.42, W*0.46); const coneH = H*0.44; groundBase = H*0.085; const p1=Math.random()*9, p2=Math.random()*9, p3=Math.random()*9; const n = W/STEP+2; elev = new Float32Array(n); for(let i=0;i= surfY(x) - r... Since elev in px and y in px consistent. Sliding physics with elevation: u = dx/dt (px/s, +x right). Tangential gravity accel = -G * slope / sqrt(1+slope²) — check sign: elevation e(x); gravitational potential decreases with e. Force along +x proportional to -de/dx * G / sqrt(1+slope²) (component of gravity along surface tangent). If slope>0 (going uphill to the right), accel negative (pulls left, downhill) ✓. Freeze: |u|<6 && |slope|<0.32, or heat<=0. On freeze: paint deposit, deactivate. Also particle stuck on steep slope with |u| tiny but slope steep — keeps sliding (gravity re-accelerates) ✓. Airborne → sliding transition: when y > surfY(x)-r: compute impact. If heat>0.35 → splat: sliding=1; u = vx*0.45 (damped tangential); vy=0; y snapped to surface. Else cooled bomb: if vy>90 && bounces<3: reflect: vy = -vy*0.42; vx = vx*0.72; y = surf - r - 0.5; else → sliding with u = vx*0.5 (thud) — cooled rock rolling briefly then freezing. Also cooled splat shouldn't "melt-slide" — cooled bombs bounce/roll ✓. While sliding: y = surfY(x) - r (glue). x += u*dt. Keep x in bounds; if x out → recycle. Heat: heat -= dt*cool*(sliding?2.4:1) where cool ∈ [0.14,0.4]. Lifetime: age += dt; if age>maxAge → if sliding → paint+recycle else fade out (just recycle; airborne cooled bombs are dark, vanishing midair is a bit odd — make maxAge generous 12–20s so they land first; with our speeds they land quickly anyway). Wind on airborne lava: vx += wind*8*dt; also for smoke more. Off-screen: x < -30 || x > W+30 || y < -40 (flying above) — y<0 fine, will come back down. x off sides → recycle (no deposit). **Spawning during eruption:** ```js function erupt(){ erupting = true; eT = 0; flash = 1; shake = 1; } update: if erupting: eT += dt; env = Math.min(1, eT/0.22) * Math.exp(-eT/2.1); pressure = Math.max(0, pressure - dt*0.75); if(env < 0.02 || eT > 9){ erupting=false; } spawnLavaRate: acc += dt * (150 + 330*power) * env * amountMul? ``` Hmm — "particle amount" slider sets the budget (cap); spawn rate should scale too so more particles actually appear. Let amount slider A ∈ [250..3000], default 1400. spawn rate = base 90 * (A/1400)^0.85 * power^0.7 * env particles/sec... At env peak with A=1400, power=1: 90/s * env... over eruption (~4 effective seconds) → ~350 lava. Cap 1400 rarely reached — fine, cap is a ceiling not target. But to make slider clearly do something, tie rate linearly: rate = 55 * A/350? Let me define rate = (40 + 90*(A/3000)*?) ... Let me just do: lavaRate = 26 + A*0.075 → at A=3000: 251/s at peak env → an eruption dumps ~600–800 lava; at A=250: 45/s → ~130. Reasonable and clearly responsive. Multiply by env and (0.7+0.5*power). Smoke rate = 14 + A*0.05 → 164/s max at peak, times env^1.4. Also while erupting pressure drain: dt*0.7 (full drain ~1.4s). When not erupting: pressure += dt * buildRate * (0.45+pressure) ; buildRate = 0.03 → time to erupt: solve p: dp/dt = 0.03*(0.45+p) → exponential, time from 0→1 = ln((0.45+1)/0.45)/0.03 ≈ ln(3.22)/0.03 ≈ 38.8s. Bit long; use 0.045 → ~26s. Good pacing. Add ±15% wobble over time. Also pre-eruption fumaroles when pressure>0.5: smoke spawn rate 4*(p-0.5); embers: rare tiny lava particles with low velocity from vent (v ~ 30–80, they pop out and fall back — cute) rate 1.2*(p-0.5). They're just lava particles with small size & fast cool. **Flash:** flashT: at eruption set flashT=0; flash alpha = max(0, 1 - flashT/0.45), radius = 60+flashT*320. Drawn 'lighter' radial at vent. **Screen shake:** shakeT decays: amp = env*6 (during eruption) — apply translate( (rand-0.5)*amp, (rand-0.5)*amp*0.7 ) to world layers. Random per frame jitter is fine for rumble. **Particles render:** Lava sprite set: LSPR = 24 canvases 40×40. Build: ```js function heatColor(t){ // t 0..1 stops: 0.00: [46,32,27] 0.18: [86,30,18] 0.38: [193,52,18] 0.60: [252,110,24] 0.82: [255,190,70] 1.00: [255,246,200] } ``` Interp linearly between stops. Sprite at heat h: radial gradient r=20: stops: 0 → lighten(color, 1.35) alpha1; 0.25 → color; 0.55 → darken(color,0.8) alpha 0.85*h... hmm need transparent edge: stop 1 → rgba(color.r, color.g, color.b, 0). Also for very hot, add white core: if h>0.85 inner stop near-white. Compose: ``` g = ctx2.createRadialGradient(20,20,0,20,20,20) g.addColorStop(0, rgba(mix(color, white, 0.55*h +0.25), 1)) g.addColorStop(0.3, rgba(color,0.95)) g.addColorStop(0.62, rgba(darken(color,0.75), 0.55*h)) // glow only if hot g.addColorStop(1, rgba(color,0)) ``` For low h, the whole thing is dark and glow weak — good, cooled particles become dull rocks with soft dark edge. Draw with 'lighter' only when h>0.5? If drawn lighter when hot, dark sprites would be invisible with lighter (black adds nothing) — that's why two passes: pass1 h>0.5 lighter; pass2 else source-over. At h=0.5 sprite is deep red — 'lighter' with dark red adds subtle glow over dark bg ✓. Smoke sprite set: SSPR = 14, 64×64 puffs: base radial gradient soft; to make puffy, draw 5 overlapping circles random offsets with per-circle gradients... simpler: one radial gradient with noisy edge: draw main circle alpha 1 center → 0 edge; then multiply slight irregularity: draw 6 random smaller blobs. Build once (seeded random ok). Color ramp t: 0 → [92,66,50] warm dark; 0.5 → [120,112,108]; 1 → [186,180,175]. Alpha in sprite: center 1 → edge 0 with soft falloff (stop .45 → .85?). We apply globalAlpha per particle. Smoke particle fields: x,y,vx,vy,age,life,s0,s1,ramp seed, rot? (draw sprite same orientation; fine). Smoke update: vy += (-G*0.22)*dt? buoyancy up: vy -= G*0.16*dt but with drag: vy *= ... Let me do: vy += (-60)*dt (constant lift) hmm — want plume to rise then spread: initial vy -90..-160; lift fades with age: vy += (lift - 22*age)?? Simpler: vy += (-46)*dt; vy *= (1-0.35*dt) drag; so it decays toward terminal rise. Also turbulence: vx += (Math.sin(age*2.1+seed)*14 + wind*30)*dt... wind accel wind*26*dt. vx *= (1-0.5*dt) mild. y also gets wind-driven lean naturally via vx. size grows: s = s0+(s1-s0)*(age/life)^0.65. alpha = 0.5*envelope. Recycle smoke when age>life or alpha tiny or off-screen far. **Now sizes:** smoke s1 up to 95px — 900 smoke * 95px drawImages — could be heavy but ok. Reduce smoke cap to 700 and s1 max 80. **Frame budget check:** ~3000 lava drawImage (small) + ~700 smoke (medium) + terrain drawImage full screen + few gradients. Should hold 60fps on typical hardware. The fillRect for HUD minimal. **Deposit canvas:** same size as main (W×H at DPR 1 to save memory? Use DPR 1 for deposits — slightly softer, fine). Paint circle: fillStyle rgba(rockColor, 0.30). rockColor from heatColor(heat*0.30) then darken ×0.85. Also paint landing splats for hot splats with slight splash mark? The frozen paint covers it. **Weathering:** every weatherAcc += dt; if > 4: destination-out alpha 0.05 fillRect → visible slow fade. hmm alpha 0.05/4s → per minute ×0.47 — equilibrium fine. **FPS:** fps = 0.9*fps + 0.1*(1/rawDt); display rounded. Also show particle counts live each frame (cheap text). To reduce text cost, update HUD text at 10Hz. Store strings, redraw always (canvas text cheap for few strings). **Trigger button:** sets pressure = 1 → erupts next frame (or call erupt directly if not erupting). **Panel HTML/CSS:** write it clean. Values update: power (×1.0), wind (+3 → "wind +3 →" arrow? use "→"/"←"/"·" text glyphs — text arrows fine). Particles value integer. Wind display: `+3.0 →` etc. **Colors / palette final:** - Sky top #05070f → #10142e → #2a2036 → horizon #5a3526 with a thin #a06a38 band? Let me craft: gradient stops: 0:#04050d, .45:#10152f, .75:#2b2135, .93:#55352a, 1:#7a4a2c? Horizon at ground level (H-groundBase). The gradient covers full screen; below horizon terrain covers. Add subtle warm horizontal band near horizon via second overlay gradient — the main gradient handles it. - Ridges: far ridge fill #151a2c-ish (bluish dark) at 0.75 alpha... they sit against horizon warm glow — silhouette dark blue-gray #1a1c30. nearer ridge #12121e. - Stars ivory-white various alpha. - Cone fill: #120e0c base? Against horizon glow, silhouette reads. Strata lines rgba(255,190,120,0.04)? Careful: subtle. Rim light: stroke along surface: rgba(230,150,80,0.22) 1.5px on upper parts? Do full surface stroke with vertical alpha variation — complicated; do: stroke surface path with rgba(210,140,90,0.18), then a second shorter stroke limited to crater vicinity brighter. Also speckle dots rgba(255,255,255,0.03). - Ground fill same rock color, maybe slightly different: #0e0c0a. - Deposits show as darker patches with reddish tinge initially — visible especially when fresh. Fresh deposit paint also briefly emits glow? A frozen bright-orange splat: painted dark immediately kills glow abruptly — instead paint deposit AND the particle keeps rendering as cooling rock for extra 1.5s at the frozen spot before recycle? That doubles complexity; simpler: deposit paint color uses heat at freeze so freshly frozen bright splats paint a still-glowing-ish color that fades via weathering — acceptable and looks like crust. Also on freeze, spawn 0–2 tiny spark particles? skip. Actually one more consideration: when lava freezes on steep flank it paints; but consecutive eruptions layer up — good. **Ash color ramp check on dark sky:** pale gray over #10152f reads ✓. **Lightning:** bolts array; spawn chance per frame during eruption: if Math.random() < env*0.05 → spawn (at 60fps env1 → ~3/s; a bit much; use 0.03 → ~1.8/s peak; each lasts 0.1s. ok). Also faint chance during high pressure non-eruption? No. Also draw bolt with flicker: alpha *= (0.7+0.3*random per frame). **Structure of main update:** ``` function frame(t){ dt = min(...); updateSim(dt); render(); raf } updateSim: pressure build / eruption state spawn lava & smoke update lava pool update smoke pool update streaks bolts aging weathering flash timers ``` Rendering order recap with shake translate for world: 1. clear (fillRect sky cache drawImage covers all — sky cache is full-screen so no clear needed). 2. drawImage skyCache. 3. wind streaks. 4. drawImage terrainCache. 5. drawImage depositCanvas. 6. smoke particles. 7. vent glow (lighter, radial at vent; radius/alpha by pressure & env) + eruption flash. 8. lava pass1 (hot, lighter), pass2 (cooler, source-over). 9. bolts (lighter). Then reset transform → HUD (gauge, stats). Wait: vent glow drawn after smoke brightens smoke near vent (backlight ✓) but also brightens terrain near vent ✓ good. Hmm, deposits drawn over terrain: deposits on slopes — ✓. But deposit also shouldn't appear in sky (can't, particles only exist on surface). **Wind streaks:** init 24: {x,y (y in 20..H*0.45), len base 26+rand*40}. speed: vx = wind*30; when |wind|<0.3 skip drawing. Also alpha 0.04+0.05*rand. Draw as line with slight downward-none slope. Wrap x. Also they should respond instantly to slider ✓ (vx from current wind). Also subtle: streaks vertical position drift slowly? Keep static y. **Numbers for gauge position:** gauge x: 28, width 16, height 150, y centered at H*0.52. On small screens fine. **Stats position:** x: 24, y: 28 lines 18px apart. Panel z-index above canvas; canvas fixed inset 0. **Accessibility bits:** labels with `for`, sliders keyboard-operable natively ✓. **Reduced motion?** skip. Let me now also double check the "particle amount slider while it runs" — budget changes: clamp spawns to budget = active lava < budget. Also pool max = 3000 (slider max). If slider lower, budget lower. ✓. Pool MAXLAVA = 3000; smoke pool 700 fixed. **Potential issue: lava spawn position** — spawn at vent floor: x = cx + (rand-0.5)*ventWidth*0.5, y = surfY(x) - 2. With vent width ~40. Also give some spawn slightly staggered depth — fine. **Eruption envelope & spawn loop:** use accumulator spawnAcc; while(spawnAcc>=1){spawnAcc--; spawnLava();} with spawnAcc += rate*dt. Also at eruption start, initial violent burst: at eT<0.2, extra rate ×2.5 — incorporated via env attack? env rises in 0.22s; add burst multiplier (eT<0.35? 2.2:1). **Velocity formula:** ``` const p = power; // 0.5..2 let sp = (300 + 380*p) * (0.55 + 0.75*Math.random()); // at p=2: (1060)*(0.55..1.3) = 583..1378 → apex up to ~950px — too high? apex = sp²/2g = 1378²/2000 = 949px → flies off top of screen and comes back? If launched above top of canvas, y<0, gravity brings back ✓ fine, dramatic. // at p=0.5: (490)*(0.55..1.3)=270..637 → apex 36..200px above vent ✓ modest eruption. angle: from vertical: a = (Math.random()<0.7 ? gauss()*0.24 : (Math.random()*2-1)*0.9); // radians vx = Math.sin(a)*sp; vy = -Math.cos(a)*sp; ``` gauss approx: (Math.random()+Math.random()+Math.random())/1.5-1 → range ±1, bell. a = g*0.28. Also wind adds: vx += wind*10. Sizes: r = 2.0+Math.random()*2.6; 7% chance big bomb: r = 4.5+Math.random()*3.5. Draw diameter = r*3.4 (glow halo) for hot; for cooler draw smaller? drawImage size = r*3.2 always (sprite halo included). Hot with lighter looks big glow; ok. Max age: 9+rand*9. Cool rate: 0.16+rand*0.22. **Sliding init:** after splat: u = vx*0.5 + (slope-based?) fine. Tremor: during erupting, sliding particles: u += (Math.random()-0.5)*env*30*dt? tiny. **paint deposit:** ``` dctx.fillStyle = depositColor(heat); // cached few dctx.beginPath(); dctx.arc(x,y,r*1.25+1,0,TAU); dctx.fill() with globalAlpha 0.3 ``` Batch by color? Simple per-particle is fine (freeze events moderate). **One more render nicety:** hot lava particles below heat 0.5 drawn source-over with sprite that includes dark body — dark bodies on dark terrain invisible... add slight rim? They should read as cooling rocks: heatColor(0.3) = (110,35,18)-ish dark red — faint but visible on near-black cone. Acceptable — glowing embers dominate; cooled rocks blend into the dark cone (realistic at dusk). **HUD gauge gradient:** create per frame: grad vertical from bottom amber #ff9d3c to top #ff4a2a; fill height = pressure*(gh). Plus flash: when erupting, gauge border pulses alpha. Also gauge shows threshold notch at top and label "P" ... label "PRESSURE" under, 9px letterspaced. **Status line:** under stats: state: erupting? "ERUPTING" (amber) : pressure>0.75 ? "CRITICAL" : "DORMANT·BUILDING". **Fonts on canvas:** use `12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`. Labels letterspaced manually? Use plain. Alright — also handle the case W small (mobile): terrain baseR = min(0.42W, ...) still ok; panel bottom-right may overlap gauge on narrow screens — media query moves panel to bottom sheet full width. Fine-ish. Now write the full file. I'll aim for clean, commented, organized code (~600-700 lines). Language: user wrote in English → everything in English. Let me write the heatColor interpolation helper: ```js const HEAT_STOPS = [ [0.00, 46, 34, 30], [0.16, 92, 34, 20], [0.34, 190, 52, 16], [0.58, 250, 108, 22], [0.80, 255, 188, 64], [1.00, 255, 246, 205] ]; function heatColor(t, out){ // out array3 if(t<=0){...} loop stops. } ``` Sprite creation: ```js const LSPRN=24, LSPR=[]; (function(){ for(let i=0;i0.75 ? [255,250,225] : [Math.min(255,col[0]+70), Math.min(255,col[1]+70), Math.min(255,col[2]+70)]; const grd=g2.createRadialGradient(24,24,0,24,24,24); grd.addColorStop(0, rgba(core,1)); grd.addColorStop(0.22, rgba(col,0.98)); grd.addColorStop(0.5, rgba(col2, 0.35+0.5*h)); grd.addColorStop(1, rgba(col2,0)); g2.fillStyle=grd; g2.fillRect(0,0,48,48); LSPR.push(c); } })(); ``` Wait for h=0 sprite: col dark, core = col+70 → (116,104,100) grayish — cooled rock sprite slightly lighter center — ok, visible dimly. Fine. rgba helper: `rgba(c,a)` string builder. Smoke sprites: ```js const SSPRN=14, SSPR=[]; for i: t=i/13; color ramp: c0 = t<0.5? mix([96,64,46],[122,116,112], t*2) : mix([122,116,112],[188,182,176], (t-0.5)*2); canvas 72x72; draw ~7 blobs: main radial at center r=30 alpha... ``` Build puff: for k in 0..6: angle rand, dist rand*14, r 12+rand*16; each blob radial gradient color→transparent alpha 0.5.. Let me: first fill center blob radius 30 gradient alpha 1→0; then 6 blobs alpha 0.35. Soft enough. Per-particle: sprIdx = min(13, (t*13)|0). **Rendering particle loops:** ```js // pass hot ctx.globalCompositeOperation='lighter'; for(i...) if(p.active && p.heat>0.5){ const d=p.r*3.3; ctx.globalAlpha=0.55+0.45*p.heat; ctx.drawImage(LSPR[(p.heat*23)|0], p.x-d/2? ...)} ``` Index: `(p.heat*(LSPRN-1))|0` careful heat can exceed 1? clamped at spawn to ≤1. ok. Alpha: hot core alpha 1-ish; globalAlpha 0.85+0.15h? With 'lighter' stacking many particles could blow out white where dense — actually dense fountain center blowing to white-hot is gorgeous. But too much? Each particle sprite center alpha 1 additive → overlapping 3-4 particles saturates. It'll look intense at vent — good, that's the look. Maybe scale alpha 0.5 for lighter pass to avoid full white everywhere: 0.35+0.5*h. Tune: 0.4+0.5*p.heat. Pass2 source-over alpha 0.9. For very hot big glow, draw size = r*3.3; for heat<0.5 size r*2.6. **Smoke draw:** alpha calc; also could draw with slight composite normal. **Vent glow:** ``` const glow = 0.16 + 0.65*pressure*pressure + (erupting? env*0.8:0); radius 46 + pressure*30 + env*50; 'lighter' radial gradient rgba(255,140,50, glow*0.55) → transparent. Also inner white-hot small when env high. ``` **Flash:** separate bright: alpha (1-flashT/0.5)² * 0.9, radius 30+flashT*380, color rgba(255,220,150). Also vertical light column? skip. **Bolt spawn origin:** x = cx + (rand-0.5)*140; y = ventY - 60 - rand*150. **Stats text:** build strings; draw with ctx.fillText, mono font. Also `LAVA` count = lavaCount; `ASH` = smokeCount. Let me now also think about init sequence: sizeCanvas() → buildTerrain() → build caches (sky, terrain) → pools init (fixed max — do once, not per resize; on resize keep particles, clamp). Pools independent of W. buildSprites once (colors fixed). resize handler: debounce 150ms; recompute W,H,DPR; rebuild elev, caches; deposit canvas recreate (loses deposits — acceptable; or copy old? copy via drawImage scaled — nice touch: create new deposit canvas and drawImage old onto it scaled. Do that.) DPR handling: canvas.width = W*DPR etc; ctx.setTransform in render: `ctx.setTransform(DPR,0,0,DPR,shakeX*DPR, shakeY*DPR)` — do world transform = DPR scale + shake offset in device px: setTransform(DPR,0,0,DPR, (sx)*DPR, (sy)*DPR). Then HUD: setTransform(DPR,0,0,DPR,0,0). Caches: skyCache canvas at DPR too (crisp stars) — create with width W*DPR, draw with scale. Then drawImage(skyCache,0,0,W,H) under world transform ✓. Deposit canvas at DPR1 (soft), drawImage(deposit,0,0,W,H). **Terrain cache draw details:** ```js function buildTerrainCache(){ tCan = canvas W*DPR,H*DPR; tc scale DPR. // ground + cone as one path: move along surface from x=0..W then down corners. tc.beginPath(); tc.moveTo(0,H); for(x=0;x<=W;x+=2){ tc.lineTo(x, H-elevAt(x)); } tc.lineTo(W,H); closePath; fill '#100d0b'; // strata: clip to path; for levels: e from 40..coneH step 34: draw polyline of (x, H-elevAt(x)-e) hmm strata as offset curves: stroke rgba(255,180,110,0.05) — subtle warm bands? Or darker bands: rgba(0,0,0,0.25) lines offset below surface — carved look. I'll do dark offset contours: for off in [16, 34, 55, 80, 110]: stroke surface shifted down by off with alpha 0.18 dark, lineWidth 3, but only where elev > off+6 (else skip segment) — implement by checking per segment. // speckles: 400 random points where elevAt>8: tiny 1px rects rgba(255,255,255,0.04) & some rgba(0,0,0,0.3). // rim light: stroke surface path rgba(216,140,84,0.20) lineWidth 1.5 — full length; plus near crater (|x-cx|<90) extra stroke alpha 0.35. } ``` Also crater interior shading: inner glow baked? Vent glow dynamic covers it. Add slight darkening inside crater bowl: gradient? skip, dynamic glow handles. Sky cache: ``` grad stops as planned; fill; stars: 110 random x, y in 0..H*0.55, avoid below? alpha 0.15+0.55*rand^2, size 0.8+rand; a few larger 1.8. horizon glow band: extra radial? The gradient's warm bottom is enough. Maybe faint sun-below-horizon glow: radial gradient centered (W*0.7, H) radius W*0.5 rgba(255,120,50,0.10)→0 'lighter'-ish — adds dusk warmth on right side. Subtle ✓ (representational: afterglow). ridges: far: path along noise ridge height ~ H*0.16 at varying: y = H - (H*0.13 + 30*sin(x*0.006+2)+14*sin(x*0.013)); fill #1b1e33 alpha .9? On top of gradient; near ridge: H*0.075 base fill #141627. Wait ridges should be behind volcano but in front of sky — drawn in sky cache before terrain cache is drawn (terrain cache drawn after sky cache covers ridges where overlapping ✓). ``` Order in skyCache: gradient → afterglow → ridges → stars? Stars behind ridges? Stars above horizon area — draw stars before ridges so ridges occlude ✓. Order: gradient, stars, afterglow (glow over stars ok, lighter), ridges. **Panel HTML:** ```html ``` Track fill CSS var: on input set `e.style.setProperty('--p', pct+'%')` and track background uses `linear-gradient(90deg, var(--amber) var(--p), rgba(255,255,255,.13) var(--p))`. Works in webkit & firefox (background on track element; CSS var inherits ✓). Wind display arrow: function windLabel(v): sign→ '→','←','·'. **JS wiring:** read values each frame or on input? On input update vars + labels. Also fill update. Fine. Let me also add a slight "readout" for budget: when lavaCount > budget*0.98, spawn throttled naturally. Edge: slider changes budget mid-eruption — clamp handles. **Also performance of sliding y-snap:** y = surfY(x) - r each frame — cheap. **Check sliding direction across crater rim:** slopeAt uses array — near rim discontinuities smoothed by 2px sampling ✓. Now, potential bug: particle launched nearly straight up from crater falls back into vent → lands inside crater at vent floor, splats, slides slightly, freezes, paints — realistic crater-filling! Nice. The vent floor is flat-ish (bowl bottom) so slope small → freezes quickly ✓. **Ash plume buoyancy check:** vy starts -100..-170; lift -46*dt + drag(1-0.35dt) → terminal rise where -46 + 0.35*v = 0 → v = -131?? drag reduces magnitude: dv = (-46 + (-vy)*0.35 ... let me define vy += LIFT*dt; vy -= vy*0.5*dt. Terminal: vy_t = LIFT/0.5 = -92 px/s rising forever. Plume would rise off-screen after ~8s — with life 3–7s it fades before. Also wind pushes vx. And size grows → classic mushroom. Also add slight spread: vx += sin jitter. Good. Actually eruption plume should initially rocket fast then slow: initial vy = -(120+rand*140)*env-ish; fine. **Smoke spawn position:** vent + small radius offset, y slightly above vent floor: y = ventY - 4 - rand*10. **ventY** = surfY(cx) (canvas y at crater floor center). Also spawn smoke slightly wider when env high (column width). **Counting FPS with dt=0 guard.** **Write final code now.** I'll be careful with every function. Also include ``. Title "Eruption — a volcano pressure study". One more check — "must stay smooth with at least 1000 active particles": default 1400 budget, eruption dumps maybe 500–900 active simultaneously (spawn 45-250/s over ~5s eff) — hmm at default might not hit 1000 concurrent. Should I ensure demonstrably 1000+? Raise spawn rates: lavaRate = (30 + A*0.16) → A=1400 → 254/s peak env... eruption effective integral of env ≈ ∫ min(1,t/0.22)e^{-t/2.1} ≈ ~2.0s → ~500 particles per eruption at default. Lifetime of lava: cooling 0.16–0.38/s → molten phase lasts until heat 0: ~3–6s airborne+sliding; plus sliding cooling ×2.4 → shorter. So concurrent ≈ spawn rate × avg lifetime ≈ 254 × 4 ≈ 1000 at peak eruption ✓ nice, default setting shows ~600–1100 active. Max slider 3000 → rate 510/s × 4 ≈ 2000 concurrent ✓. And FPS counter proves smoothness. Smoke: rate = 10 + A*0.06 → default 94/s × env² maybe; lifetime ~5 → ~300 concurrent, max 190*7... cap pool 700 ✓. Set pool MAXLAVA = 3400 (headroom), MAXSMOKE=750. Deposit paint on freeze — with many freezing, hundreds of arc fills during a second — okay. Let me write out everything now carefully. ```html Eruption — a volcano pressure study ``` Also custom range styling for webkit + moz. Then script. Let me carefully write the script in full (mentally compile): ```js 'use strict'; const canvas = document.getElementById('scene'); const ctx = canvas.getContext('2d'); const TAU = Math.PI*2; let W=0,H=0,DPR=1; // ---------- state (sliders) ---------- let power=1, wind=3, budget=1400; // ---------- simulation state ---------- let pressure=0, erupting=false, eT=0, env=0, flashT=9, tSince=0; // ---------- terrain ---------- const STEP=2; let elev=null, cx=0, ventY=0, groundTop=0; function elevAt(x){...} function slopeAt(x){...} function surfY(x){ return H - elevAt(x); } function buildTerrain(){ cx = W*0.47; const baseR = Math.max(W*0.30, Math.min(W*0.44, W*0.42)); const coneH = H*0.44; const gb = H*0.085; const ph1=Math.random()*10, ph2=Math.random()*10, ph3=Math.random()*10; const n = (W/STEP|0)+3; elev = new Float32Array(n); const cw = Math.max(36, W*0.034); for(let i=0;ip.maxAge){ kill(i, false); continue; } if(p.slide){ const sl=slopeAt(p.x); // tangential gravity p.u += (-G*sl/Math.sqrt(1+sl*sl))*dt; const mu = 2.2 + (1-p.heat)*7.5; p.u -= p.u*Math.min(1, mu*dt); if(erupting) p.u += (Math.random()-0.5)*env*40*dt; p.x += p.u*dt; if(p.x<2||p.x>W-2){ killLava(i); continue; } p.y = surfY(p.x)-p.r; p.heat -= dt*p.cool*2.4; if(p.heat<=0 || (Math.abs(p.u)<6 && Math.abs(sl)<0.32)){ paintDeposit(p); killLava(i); continue; } } else { p.vy += G*dt; p.vx += wind*8*dt; p.x += p.vx*dt; p.y += p.vy*dt; p.heat -= dt*p.cool; if(p.heat<0) p.heat=0; const groundY = surfY(p.x) - p.r; if(p.y >= groundY){ if(p.heat>0.35){ // molten splat p.slide=true; p.u=p.vx*0.5; p.vy=0; p.y=groundY; } else { // cooled bomb if(p.vy>110 && p.bounces<3){ p.vy=-p.vy*0.42; p.vx*=0.72; p.y=groundY-0.5; p.bounces++; } else { p.slide=true; p.u=p.vx*0.4; p.y=groundY; } } } if(p.x<-40||p.x>W+40){ killLava(i); continue; } } } ``` killLava(i): lava[i].active=false; lavaCount--; lavaFree.push(i). Hmm — but note when p.slide set inside else-branch we shouldn't also re-check... fine, next frame handles. kill on maxAge while sliding: paint deposit too (it was on ground): killLava without paint leaves no mark — better: if sliding → paint then kill. Let me restructure: on maxAge: if(p.slide){paint; kill} else kill. ok. paintDeposit(p): ```js depCtx.globalAlpha=0.34; depCtx.fillStyle=rockColor(p.heat); depCtx.beginPath(); depCtx.arc(p.x,p.y,p.r*1.35+1.2,0,TAU); depCtx.fill(); ``` rockColor(h): heatColor(h*0.32) then darken 0.9 — compute string. Precompute string each call — string building per freeze event fine. Smoke: ```js function spawnSmoke(strong){ idx...; s.active=true; smokeCount++; s.x=cx+(Math.random()-0.5)*(30+env*70); s.y=ventY-6-Math.random()*14; s.vx=(Math.random()-0.5)*24 + wind*12; s.vy=-(90+Math.random()*150)*(0.5+0.5*(strong?env:0.6)); s.age=0; s.life=3.5+Math.random()*4; s.s0=7+Math.random()*9; s.s1=34+Math.random()*52; s.seed=Math.random()*10; } ``` update: ```js s.age+=dt; if(s.age>s.life||...) kill; const a=s.age/s.life; s.vy += -46*dt; s.vy -= s.vy*0.5*dt; s.vx += wind*26*dt + Math.sin(s.age*2.3+s.seed)*16*dt; s.vx -= s.vx*0.35*dt; s.x+=s.vx*dt; s.y+=s.vy*dt; ``` draw: size = s0+(s1-s0)*Math.pow(a,0.65); alpha = 0.5*(a<0.1? a/0.1 : 1-Math.pow((a-0.1)/0.9,1.25)); idx=(a*13)|0. Also if y < -80 kill; x off 100 kill. Wind streaks: ```js const streaks=[]; for 26: {x:Math.random()*W?, y: 30+Math.random()*H*0.42, l:26+Math.random()*44, a:0.03+Math.random()*0.06, sp:0.6+Math.random()*0.8} ``` But created before W known? Create in sizeCanvas. update in draw: x += wind*34*sp*dt (do inside render where dt available — pass dt to render or update in sim). I'll update in sim. Draw: if |wind|>0.25: strokeStyle rgba(233,220,198, a* min(1,|wind|/6)); line (x,y)-(x - wind*0.9*l*?..). Length direction along wind sign: len = l*(0.4+|wind|/10). dx = wind>0? len : -len. Draw line horizontal. Wrap x modulo W. Bolts: ```js const bolts=[]; function spawnBolt(){ const pts=[]; let x=cx+(Math.random()-0.5)*120, y=ventY-60-Math.random()*140; pts.push([x,y]); const n=6+(Math.random()*4|0); for(let k=0;klife. draw: alpha=(1-age/life)*(0.6+0.4*Math.random()); two strokes. Main loop: ```js let last=performance.now(), fpsS=60, hudT=0, weatherT=0, spawnAccL=0, spawnAccS=0, emberAcc=0, fumAcc=0; function frame(now){ let dt=(now-last)/1000; last=now; if(dt>0.05) dt=0.05; if(dt<=0) dt=0.0001; fpsS += (1/dt - fpsS)*0.08; // careful: dt tiny → huge fps; clamp step(dt); draw(dt); requestAnimationFrame(frame); } ``` fps clamp: raw=1/dt; if raw>240 raw=240. step(dt): ```js tSince+=dt; if(erupting){ eT+=dt; env=Math.min(1,eT/0.22)*Math.exp(-eT/2.1 новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых новых === answer so far ===