{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<title>Volcano Eruption Physics Simulation</title>\n<style>\n  html,body{margin:0;padding:0;background:#05060d;color:#ddd;font-family:'Segoe UI',Arial,sans-serif;overflow:hidden}\n  #wrap{display:flex;flex-direction:column;align-items:center;height:100vh}\n  #controls{display:flex;gap:26px;align-items:center;padding:8px 14px;background:#11141f;\n    border:1px solid #2a2f45;border-radius:10px;margin-top:8px;font-size:13px;flex-wrap:wrap;justify-content:center}\n  #controls label{display:flex;flex-direction:column;align-items:center;gap:2px;color:#9fb0d0}\n  #controls input[type=range]{width:150px;accent-color:#ff7a1a;cursor:pointer}\n  .val{color:#ffb066;font-weight:bold;font-size:12px}\n  #stats{position:absolute;top:12px;right:14px;background:rgba(8,10,20,.65);border:1px solid #333a55;\n    border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.55;pointer-events:none}\n  #stats b{color:#ffcf7a}\n  #stage{position:relative}\n  canvas{display:block;border-radius:10px;margin-top:8px;box-shadow:0 0 40px #000;max-width:97vw;height:auto}\n</style>\n</head>\n<body>\n<div id=\"wrap\">\n  <div id=\"controls\">\n    <label>Eruption power\n      <input id=\"power\" type=\"range\" min=\"30\" max=\"160\" value=\"100\"><span class=\"val\" id=\"powerVal\"></span></label>\n    <label>Wind (\u2190 \u2192)\n      <input id=\"wind\" type=\"range\" min=\"-100\" max=\"100\" value=\"25\"><span class=\"val\" id=\"windVal\"></span></label>\n    <label>Particle amount\n      <input id=\"amount\" type=\"range\" min=\"40\" max=\"600\" value=\"280\"><span class=\"val\" id=\"amountVal\"></span></label>\n  </div>\n  <div id=\"stage\">\n    <canvas id=\"cv\" width=\"960\" height=\"540\"></canvas>\n    <div id=\"stats\"><b id=\"fps\">0</b> fps<br><b id=\"count\">0</b> particles</div>\n  </div>\n</div>\n<script>\n\"use strict\";\nconst W = 960, H = 540;\nconst cv = document.getElementById(\"cv\"), ctx = cv.getContext(\"2d\");\n\n/* ---------- sliders ---------- */\nconst sliders = {\n  power : document.getElementById(\"power\"),\n  wind  : document.getElementById(\"wind\"),\n  amount: document.getElementById(\"amount\")\n};\nconst vals = {\n  power : document.getElementById(\"powerVal\"),\n  wind  : document.getElementById(\"windVal\"),\n  amount: document.getElementById(\"amountVal\")\n};\nfunction syncLabels(){\n  vals.power.textContent  = sliders.power.value + \"%\";\n  vals.wind.textContent   = (sliders.wind.value > 0 ? \"+\" : \"\") + sliders.wind.value;\n  vals.amount.textContent = sliders.amount.value + \" /s\";\n}\nfor (const k in sliders) sliders[k].addEventListener(\"input\", syncLabels);\nsyncLabels();\n\n/* ---------- terrain height map ---------- */\nconst terrain = new Float32Array(W + 1);\n(function buildTerrain(){\n  const ground = H - 52, cx = W * 0.5, halfBase = W * 0.47,\n        coneH = 258, craterR = 46, craterDip = 40;\n  for (let x = 0; x <= W; x++){\n    const d = Math.abs(x - cx);\n    let m = 1 - d / halfBase;\n    m = Math.max(0, m);\n    m = m * m * (3 - 2 * m);               // smooth cone profile\n    let y = ground - coneH * m;\n    if (d < craterR)                        // crater bowl\n      y += craterDip * (1 - (d / craterR) * (d / craterR));\n    y += 5 * Math.sin(x * 0.05) * m;        // slight roughness on the cone\n    terrain[x] = y;\n  }\n  for (let p = 0; p < 4; p++){              // smooth passes\n    const t = terrain.slice();\n    for (let x = 1; x < W; x++) terrain[x] = (t[x-1] + t[x] + t[x+1]) / 3;\n  }\n})();\nfunction terrainAt(x){\n  if (x < 0) x = 0; if (x > W) x = W;\n  return terrain[x | 0];\n}\n\n/* ---------- glow sprites (pre-rendered, cheap to blit) ---------- */\nfunction makeSprite(color, soft){\n  const s = document.createElement(\"canvas\"); s.width = s.height = 48;\n  const c = s.getContext(\"2d\");\n  const g = c.createRadialGradient(24, 24, 0, 24, 24, 24);\n  g.addColorStop(0, color);\n  g.addColorStop(soft ? 0.35 : 0.25, color);\n  g.addColorStop(1, \"rgba(0,0,0,0)\");\n  c.fillStyle = g; c.fillRect(0, 0, 48, 48);\n  return s;\n}\nconst LAVA_COLORS = [\n  \"rgba(255,250,210,1)\",\"rgba(255,235,140,1)\",\"rgba(255,214,70,1)\",\n  \"rgba(255,180,40,1)\",\"rgba(255,138,20,1)\",\"rgba(255,96,14,1)\",\n  \"rgba(230,55,10,1)\",\"rgba(170,30,8,1)\",\"rgba(110,18,6,1)\",\n  \"rgba(66,12,6,1)\",\"rgba(38,10,8,1)\",\"rgba(20,10,10,1)\"\n];\nconst lavaSprites = LAVA_COLORS.map(c => makeSprite(c, false));\nconst ashSprite  = makeSprite(\"rgba(120,120,130,0.55)\", true);\nconst ashSpriteD = makeSprite(\"rgba(70,70,80,0.55)\", true);\n\n/* ---------- stars / sky backdrop (pre-rendered) ---------- */\nconst sky = document.createElement(\"canvas\"); sky.width = W; sky.height = H;\n(function(){\n  const c = sky.getContext(\"2d\");\n  let g = c.createLinearGradient(0, 0, 0, H);\n  g.addColorStop(0, \"#070a1c\"); g.addColorStop(0.6, \"#1b1633\"); g.addColorStop(1, \"#4a2436\");\n  c.fillStyle = g; c.fillRect(0, 0, W, H);\n  for (let i = 0; i < 130; i++){\n    c.globalAlpha = Math.random() * 0.8 + 0.1;\n    c.fillStyle = \"#fff\";\n    c.fillRect(Math.random() * W, Math.random() * H * 0.55, Math.random() < 0.15 ? 2 : 1, 1);\n  }\n  c.globalAlpha = 1;\n  c.fillStyle = \"#e8e4d8\";                       // moon\n  c.beginPath(); c.arc(120, 82, 22, 0, 7); c.fill();\n  c.globalAlpha = 0.08;\n  c.beginPath(); c.arc(120, 82, 42, 0, 7); c.fill();\n  c.globalAlpha = 1;\n})();\n\n/* ---------- particle pools ---------- */\nconst MAX_LAVA = 3500, MAX_ASH = 1800;\nconst lava = [], ash = [];\nfor (let i = 0; i < MAX_LAVA; i++)\n  lava.push({on:false,x:0,y:0,vx:0,vy:0,heat:0,size:1,grounded:false,slide:0});\nfor (let i = 0; i < MAX_ASH; i++)\n  ash.push({on:false,x:0,y:0,vx:0,vy:0,life:0,max:1,size:1,dark:0});\nlet lavaCount = 0, ashCount = 0;\nconst cx = W * 0.5;\nconst craterY = terrainAt(cx);\n\nfunction spawnLava(power){\n  for (let i = 0; i < MAX_LAVA; i++){\n    const p = lava[i]; if (p.on) continue;\n    const a = -Math.PI / 2 + (Math.random() - 0.5) * 0.85;\n    const sp = (230 + Math.random() * 340) * power;\n    p.on = true;\n    p.x = cx + (Math.random() - 0.5) * 34;\n    p.y = craterY - 6;\n    p.vx = Math.cos(a) * sp;\n    p.vy = Math.sin(a) * sp;\n    p.heat = 0.85 + Math.random() * 0.15;\n    p.size = 5 + Math.random() * 7;\n    p.grounded = false; p.slide = 0;\n    lavaCount++;\n    return;\n  }\n}\nfunction spawnAsh(strength){\n  for (let i = 0; i < MAX_ASH; i++){\n    const a = ash[i]; if (a.on) continue;\n    a.on = true;\n    a.x = cx + (Math.random() - 0.5) * 60;\n    a.y = craterY - 10 - Math.random() * 20;\n    a.vx = (Math.random() - 0.5) * 30;\n    a.vy = -(35 + Math.random() * 55) * (0.6 + strength * 0.7);\n    a.max = a.life = 3.5 + Math.random() * 4.5;\n    a.size = 22 + Math.random() * 46;\n    a.dark = Math.random();\n    ashCount++;\n    return;\n  }\n}\n\n/* ---------- eruption state ---------- */\nlet pressure = 35;            // 0..100\nconst GRAV = 400;\n\nfunction update(dt){\n  const power    = sliders.power.value / 100;\n  const wind     = sliders.wind.value / 100;      // -1..1\n  const rate     = +sliders.amount.value;\n\n  /* pressure cycle */\n  let erupting = false, strength = 0;\n  if (pressure >= 100){ pressure = 100; }\n  if (pressure >= 100 || (erupting = pressure > 0 && pressure < 100 && eruptingFlag)){\n    erupting = true;\n  }\n  if (pressure >= 100){ eruptingFlag = true; }\n  if (eruptingFlag){\n    pressure -= 34 * dt;\n    if (pressure <= 0){ pressure = 0; eruptingFlag = false; }\n    erupting = true;\n    strength = Math.min(1, pressure / 80 + 0.35);\n  } else {\n    pressure += 6.5 * dt;\n    if (pressure > 100) pressure = 100;\n  }\n\n  /* spawn */\n  if (erupting){\n    spawnAcc += rate * strength * dt;\n    let n = spawnAcc | 0; spawnAcc -= n;\n    while (n--) spawnLava(power);\n    ashAcc += rate * 0.35 * strength * dt;\n    n = ashAcc | 0; ashAcc -= n;\n    while (n--) spawnAsh(strength);\n  }\n\n  /* lava physics */\n  const windPush = wind * 22;\n  for (let i = 0; i < MAX_LAVA; i++){\n    const p = lava[i]; if (!p.on) continue;\n    if (!p.grounded){\n      p.vy += GRAV * dt;\n      p.vx += windPush * dt;\n      p.x += p.vx * dt;\n      p.y += p.vy * dt;\n      p.heat -= 0.055 * dt;\n      const gy = terrainAt(p.x);\n      if (p.y >= gy - 1 && p.vy > 0){ p.y = gy - 1; p.grounded = true; p.slide = 0; }\n      if (p.y > H + 40 || p.x < -60 || p.x > W + 60){ p.on = false; lavaCount--; continue; }\n    } else {\n      const g = (terrainAt(p.x + 3) - terrainAt(p.x - 3)) / 6;   // slope\n      if (Math.abs(g) > 0.05 && p.heat > 0.06){\n        p.slide += g * 900 * dt * Math.min(1, p.heat * 2);\n        p.slide *= 0.94;\n        p.x += p.slide * dt;\n        p.x -= g * 0; // keep on surface below\n        p.y = terrainAt(p.x) - 1;\n        p.heat -= 0.10 * dt;\n      } else {\n        p.slide *= 0.8;\n        p.y = terrainAt(p.x) - 1;\n        p.heat -= (0.16 + (p.heat < 0.3 ? 0.10 : 0)) * dt;   // settled rock cools faster\n      }\n      if (p.x < 2 || p.x > W - 2) p.slide = 0;\n    }\n    if (p.heat <= 0){ p.on = false; lavaCount--; }\n  }\n\n  /* ash physics */\n  for (let i = 0; i < MAX_ASH; i++){\n    const a = ash[i]; if (!a.on) continue;\n    a.life -= dt;\n    a.vx += wind * 80 * dt;\n    a.vx *= (1 - 0.4 * dt);\n    a.vy -= 12 * dt;                       // buoyancy\n    a.x += a.vx * dt;\n    a.y += a.vy * dt;\n    a.size += 12 * dt;                     // billow\n    if (a.life <= 0 || a.y < -90 || a.x < -140 || a.x > W + 140){ a.on = false; ashCount--; }\n  }\n}\nlet eruptingFlag = false, spawnAcc = 0, ashAcc = 0;\n\n/* ---------- render ---------- */\nfunction render(){\n  ctx.drawImage(sky, 0, 0);\n  const windF = sliders.wind.value / 100;\n\n  /* wind streak hint */\n  ctx.strokeStyle = \"rgba(160,180,220,0.10)\"; ctx.lineWidth = 1;\n  for (let i = 0; i < 6; i++){\n    const y = 40 + i * 22;\n    ctx.beginPath();\n    const off = (perfT * windF * 60) % 80;\n    for (let x = 0; x < W; x += 80){\n      const xx = (x + off + 800) % (W + 80) - 40;\n      ctx.moveTo(xx, y); ctx.lineTo(xx + windF * 34, y);\n    }\n    ctx.stroke();\n  }\n\n  /* ash cloud (behind mountain so it billows up behind rim) */\n  ctx.globalCompositeOperation = \"source-over\";\n  for (let i = 0; i < MAX_ASH; i++){\n    const a = ash[i]; if (!a.on) continue;\n    const t = a.life / a.max;\n    ctx.globalAlpha = Math.min(1, t * 2) * 0.34;\n    const s = a.size;\n    ctx.drawImage(a.dark > 0.5 ? ashSpriteD : ashSprite, a.x - s/2, a.y - s/2, s, s);\n  }\n  ctx.globalAlpha = 1;\n\n  /* mountain */\n  ctx.beginPath();\n  ctx.moveTo(0, H);\n  for (let x = 0; x <= W; x += 2) ctx.lineTo(x, terrain[x]);\n  ctx.lineTo(W, H); ctx.closePath();\n  const rock = ctx.createLinearGradient(0, H - 300, 0, H);\n  rock.addColorStop(0, \"#3a3040\"); rock.addColorStop(0.5, \"#241d2c\"); rock.addColorStop(1, \"#141019\");\n  ctx.fillStyle = rock; ctx.fill();\n\n  /* crater glow when pressurised */\n  const glow = Math.min(1, pressure / 100) * (eruptingFlag ? 1 : 0.7);\n  if (glow > 0.02){\n    ctx.globalCompositeOperation = \"lighter\";\n    const g = ctx.createRadialGradient(cx, craterY, 4, cx, craterY, 110);\n    g.addColorStop(0, \"rgba(255,140,40,\" + (0.75 * glow) + \")\");\n    g.addColorStop(1, \"rgba(255,60,10,0)\");\n    ctx.fillStyle = g;\n    ctx.fillRect(cx - 110, craterY - 100, 220, 160);\n    ctx.globalCompositeOperation = \"source-over\";\n  }\n\n  /* lava particles \u2014 additive glow */\n  ctx.globalCompositeOperation = \"lighter\";\n  const NS = lavaSprites.length;\n  for (let i = 0; i < MAX_LAVA; i++){\n    const p = lava[i]; if (!p.on) continue;\n    const idx = Math.max(0, Math.min(NS - 1, ((1 - p.heat) * NS) | 0));\n    ctx.globalAlpha = Math.min(1, p.heat * 3.2);\n    const s = p.size * (0.7 + p.heat * 0.9);\n    ctx.drawImage(lavaSprites[idx], p.x - s, p.y - s * 0.8, s * 2, s * 2);\n  }\n  ctx.globalAlpha = 1;\n  ctx.globalCompositeOperation = \"source-over\";\n\n  /* pressure meter (left side) */\n  const mx = 20, my = 110, mw = 18, mh = 260;\n  ctx.fillStyle = \"rgba(10,12,22,0.7)\";\n  ctx.fillRect(mx - 4, my - 4, mw + 8, mh + 8);\n  ctx.strokeStyle = \"#5a638a\"; ctx.lineWidth = 2;\n  ctx.strokeRect(mx - 4, my - 4, mw + 8, mh + 8);\n  const ph = mh * pressure / 100;\n  const pg = ctx.createLinearGradient(0, my + mh, 0, my);\n  pg.addColorStop(0, \"#ff3808\"); pg.addColorStop(0.6, \"#ff9a20\"); pg.addColorStop(1, \"#fff2a0\");\n  ctx.fillStyle = pg;\n  ctx.fillRect(mx, my + mh - ph, mw, ph);\n  ctx.fillStyle = \"#cdd6f0\"; ctx.font = \"11px Segoe UI, Arial\"; ctx.textAlign = \"center\";\n  ctx.save();\n  ctx.translate(mx + mw / 2, my - 16);\n  ctx.fillText(\"PRESSURE\", 0, 0);\n  ctx.fillText(eruptingFlag ? \"ERUPTING!\" : Math.round(pressure) + \"%\", mx + mw / 2, my + mh + 20);\n  ctx.restore();\n  ctx.textAlign = \"left\";\n}\n\n/* ---------- main loop ---------- */\nlet lastT = performance.now(), perfT = 0;\nlet frames = 0, fpsTimer = 0;\nconst fpsEl = document.getElementById(\"fps\"), countEl = document.getElementById(\"count\");\n\nfunction loop(now){\n  let dt = (now - lastT) / 1000;\n  lastT = now; perfT += dt;\n  if (dt > 0.05) dt = 0.05;\n\n  update(dt);\n  render();\n\n  frames++; fpsTimer += dt;\n  if (fpsTimer >= 0.4){\n    fpsEl.textContent = Math.round(frames / fpsTimer);\n    countEl.textContent = lavaCount + ashCount;\n    frames = 0; fpsTimer = 0;\n  }\n  requestAnimationFrame(loop);\n}\nrequestAnimationFrame(loop);\n</script>\n</body>\n</html>",
        "reasoning_content": "Create a single HTML file that includes a canvas-based volcano simulation. Required features: pressure meter, eruption cycle, lava particles with gravity, sliding on the terrain slope, color change via cooling, ash cloud, 3 sliders (power, wind, particle amount), counters for particle count and FPS, capable of handling 1000+ particles.\n\nTerrain: heightmap array across the full canvas width. Volcano cone shape with a crater. Define a function groundHeight(x). Volcano: roughly triangular mountain, with a crater dip near the center.\n\nTerrain heightmap: h[x] for x 0..W. Cone: apex at cx, crater is flat between cx-craterW..cx+craterW. Slopes descend down to the ground level.\n\nLava particles: launched from the crater, with position and velocity, with gravity. Landing detection: y >= terrain(x). Then slide: while there's a slope, move along the terrain: vy=0, accelerate in the slope direction via the gradient. Cooling: heat value decreases, color goes yellow\u2192orange\u2192red\u2192dark. Disappears when heat drops below the threshold and it comes to rest, or fade-out after landing.\n\nMaintain performance: use simple arrays, don't use shadows per particle (shadows are slow). Glow effect via additive composite 'lighter' and pre-rendered radial gradient sprites. Pre-render 2\u20133 sprites of different colors? Simpler: pre-render a single white-to-yellow gradient sprite and tint with globalAlpha... canvas tinting isn't easy. Pre-render multiple sprites across the heat gradient (e.g., 16-color sprites). Then drawImage with scaling \u2014 fast.\n\nAsh: gray smoke sprites, alpha based on lifetime.\n\nEruption cycle: pressure 0\u2192100 increases; when full, eruption state for a fixed duration emitting particles at rate * particleSlider; then reset pressure to a low value.\n\nSliders: power (launch speed), wind (ash drift and slight effect on lava, -1 to 1), particles per eruption.\n\nFPS counter: update smoothed FPS roughly every 0.5 seconds. Particle count: total active count.\n\nTerrain collision and sliding: when a particle lands (y >= groundY(x)), set state to \"on ground\": compute gradient from heightmap; if |gradient| > small threshold, move x in the downhill direction, set y = ground; otherwise settle (state 3), keep cooling, then fade. Lava slows down while sliding. Use terrain smoothing.\n\nTerrain construction: W=960, H=540. Ground baseline at y = H-60. Volcano: baseWidth, peakY. height[x] = ... function terrainY(x):\n- Outside the mountain base: groundY.\n- Mountain: linear from base to crater rim, with crater dip in the middle.\n\nh = ground - (mountainHeight * coneFactor). coneFactor = 1 - |x-cx|/halfBase clamped. Crater dip: within craterRadius of cx, lower the terrain to form a bowl: terrainY += craterDepth * (1 - |x-cx|/craterRadius)? Careful: crater interior lower than rim. Launch from crater center just slightly above terrain.\n\nCompute terrainY[x] = groundY - coneH * max(0, 1 - |x-cx|/halfBase) + crater dip (inside craterRadius: dipAmount). Actually the crater dip lowers the summit. Make it smooth.\n\nAfter that, smoothing pass over heightmap.\n\nDrawing: sky gradient, maybe a sun, ground fill below the terrain polygon, distant stars at night? Keep the sky gradient dark, lava glow.\n\nParticle drawing: composite 'lighter', size roughly heat-dependent.\n\nCap: array with alive flag; reuse pool to avoid GC. Max particles probably 4000.\n\nAsh drift: vx += wind*...; y decreases.\n\nCode should be compact and readable. Alright, let's write it.\n\nSprite generation:\n\nfunction makeGlowSprite(color){ offscreen canvas 32x32, radial gradient from center color opaque \u2192 transparent }\n\nLava palette heat stages (0..1): colors: white-yellow #fff8c0 \u2192 #ffd24a \u2192 #ff9a2a \u2192 #ff5a10 \u2192 #c22a08 \u2192 #6a1404 \u2192 #2a0a04 dark. Build LAVA_STAGES = 12 sprites.\n\nAsh sprites: gray radial.\n\nUpdate loop with dt.\n\nPressure: builds at rate; during eruption spawn = dt * spawnRate. Eruption lasts while pressure>0? Simpler: pressure builds 0\u2192100; upon reaching 100 set erupt=true; during eruption rapidly spawns particles and pressure drops to 0; then builds again. Power slider affects speed.\n\nParticle amount slider: spawn rate multiplier, e.g., 20..400/sec... need to keep 1000+ active: with rate 300/s and lifetime ~4-6s, active count ~1500. Good, plus ash too.\n\nLava lifetime: while airborne heat decreases slowly, while grounded cooling is faster. Remove when heat<=0 with alpha fade.\n\nSliding: on ground, slope = (terrain[x+2]-terrain[x-2])/4; if |slope|>0.06, vx = slope * slideSpeed (proportional to gradient and heat, cooled lava slows). x += vx*dt; y = terrain(x)-1. Heat decreases. Otherwise settled: vx\u21920, cooling rate increases, stops when heat<0 \u2192 dead.\n\nGlow on canvas: while erupting, draw an inner glow at the crater.\n\nHUD: update FPS and particle count DOM counter as text every 0.25 seconds.\n\nLet's write the file.\n\nCanvas size: fix canvas at 960x540, scale via CSS for responsiveness.\n\nWind effect on lava: light vx += wind*20*dt; ash: stronger drift.\n\nEdge handling: kill particles if x<0 or x>W and grounded, or if they've gone off-screen.\n\nWriting the code:\n\nconst W=960,H=540; terrain array of size W+1.\n\nbuildTerrain():\nground=H-50; cx=W*0.5; halfBase=W*0.46; coneH=250; craterR=42; craterDip=34;\nfor x: d=abs(x-cx); m = coneH*max(0,1-d/halfBase); apply easing maybe. y=ground-m; if d<craterR: y += craterDip*(1-(d/craterR)^2)... wait, inside the crater y should be lower (y increases downward), yes add the dip. But near the rim, at edge of craterR, terrain dips into a rim bump? A smooth bowl is fine.\nSmoothing: box blur 3 passes.\n\nSpawn: x=cx\u00b1craterR*0.5, y=terrainY(cx)-4; angle nearly straight up: angle = -90\u00b0 \u00b1 spread; speed from power slider: v = 260+power*\u2026 power slider 0.3..1.5. speed = (300+rand*250)*power; angle mostly upward: a = -Math.PI/2 + (rand-0.5)*0.7.\n\nGravity 380 px/s\u00b2.\n\nterrainYAt(x): index into heightmap.\n\nEmission: during eruption also emit ash.\n\nAsh particle: x jitter around crater, upward vy -20..-50 scaled by eruption intensity, size grows, alpha fades, vx from wind and rand.\n\nPressure meter: draw a vertical bar in HUD via DOM or canvas. Draw on canvas.\n\nDraw sky: gradient from #0b1026 to #3a2b4a at horizon? Nice dusk. Stars: pre-generate.\n\nWriting final HTML.\n\nParticle storage: object array with alive flag; pool. spawn() reuses dead ones.\n\nMax pool 4000 lava + 1500 ash.\n\nFPS: count frames, compute every 500ms.\n\nSliders in a control panel at the top of the canvas, with labels showing values.\n\nNow let me write the code fully.\n\nEruption logic:\nstate 'build': pressure += dt * buildRate (e.g., 7/s \u2192 ~14 sec to buildup; maybe 12/s \u2192 ~8 sec). >=100 \u2192 erupting, eruptionTime=0.\nErupting: spawn; pressure = max(0, 100 - eruptionTime*\u2026)? Pressure display drops; after pressure reaches 0, eruption lasts min 2.5s? Simpler: eruption strength = 100 pressure decays at 40/s \u2192 2.5s eruption, spawn rate proportional to pressure. Then transition to build.\n\nspawnRate = 40 + amount*3 particles/s (amount slider 10..400), scaled by pressure/100 during eruption. Ash rate = amount*0.3.\n\nLava particle fields: x,y,vx,vy,heat (1\u21920),cool rate,size,state(0 airborne,1 ground). drawImage(spriteIndex, x-s/2,y-s/2,s,s).\n\nheat stages: sprite index = floor((1-heat)*(N-1))? heat 1 \u2192 hottest. spriteIdx = clamp(floor((1-heat)*N)).\n\nCool rate: airborne 0.05/s? Lifetime about 10s? Need to disappear. Airborne 0.08, on slope 0.12, settled 0.25. On disappearance, alpha fade = min(1, heat*3).\n\nDraw alpha only meaningful for non-lighter; use globalAlpha together with 'lighter' \u2014 that's fine, works.\n\nSettling on ground: when particle has low slope and low velocity, just draw it there; disappears with cooling \u2014 good, no permanent accumulation. Maybe leave faint solidified marks? Not required.\n\nCrater glow when pressure high: radial gradient overlay with alpha = pressure/100.\n\nWriting HTML.\n"
      }
    }
  ],
  "created": 1787818443,
  "model": "~/.cache/huggingface/hub/models--unsloth--Qwen3.8-Flash-Next-GGUF/snapshots/824f539b2710e5a9e47af4952cf6578cf5ee8932/UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf",
  "system_fingerprint": "b10667-213df585b",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 7303,
    "prompt_tokens": 236,
    "total_tokens": 7539,
    "prompt_tokens_details": {
      "cached_tokens": 0
    }
  },
  "id": "chatcmpl-yuPswTdHhkJ5iaDwvg8Avmcw8F0r9en9",
  "timings": {
    "cache_n": 0,
    "prompt_n": 236,
    "prompt_ms": 1092.763,
    "prompt_per_token_ms": 4.630351694915254,
    "prompt_per_second": 215.9663165755063,
    "predicted_n": 7303,
    "predicted_ms": 361027.183,
    "predicted_per_token_ms": 49.442232675979184,
    "predicted_per_second": 20.225623841737146
  }
}