{
  "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 Simulation</title>\n<style>\n  html,body{margin:0;padding:0;background:#0b0910;height:100%;overflow:hidden;\n    font-family:\"Segoe UI\",Arial,sans-serif;color:#e8dcd0;}\n  #wrap{position:relative;display:flex;align-items:center;justify-content:center;height:100%;}\n  canvas#c{background:#000;box-shadow:0 0 40px #000;border:1px solid #241a24;border-radius:6px;max-width:100vw;max-height:100vh;}\n  #ui{position:absolute;top:14px;left:14px;background:rgba(18,12,20,.82);\n      border:1px solid #4a3540;border-radius:8px;padding:12px 16px;width:230px;\n      font-size:12px;user-select:none;}\n  #ui h1{font-size:14px;margin:0 0 8px 0;color:#ffb066;letter-spacing:1px;}\n  .row{margin:8px 0;}\n  .row label{display:flex;justify-content:space-between;margin-bottom:2px;color:#d9c3b0;}\n  .row input[type=range]{width:100%;accent-color:#ff7a20;cursor:pointer;}\n  #stats{margin-top:10px;padding-top:8px;border-top:1px solid #3a2a33;\n      display:flex;justify-content:space-between;color:#9fe08a;font-variant:numeric;}\n  #pmeterWrap{position:absolute;top:20px;right:24px;width:26px;height:220px;text-align:center;}\n  #pmeter{position:absolute;top:22px;left:0;right:0;bottom:0;border:1px solid #6a4a3a;\n      border-radius:6px;background:rgba(10,6,10,.7);overflow:hidden;}\n  #pfill{position:absolute;left:0;right:0;bottom:0;height:20%;background:#e05a20;transition:none;}\n  #plabel{position:absolute;top:0;left:-12px;right:-12px;font-size:11px;color:#ffbf80;letter-spacing:1px;}\n</style>\n</head>\n<body>\n<div id=\"wrap\">\n  <canvas id=\"c\" width=\"960\" height=\"600\"></canvas>\n\n  <div id=\"ui\">\n    <h1>VOLCANO CONTROLS</h1>\n    <div class=\"row\">\n      <label>Eruption power <span id=\"powVal\">100%</span></label>\n      <input id=\"pow\" type=\"range\" min=\"40\" max=\"200\" step=\"5\" value=\"100\">\n    </div>\n    <div class=\"row\">\n      <label>Wind <span id=\"windVal\">&rarr; 60</span></label>\n      <input id=\"wind\" type=\"range\" min=\"-250\" max=\"250\" step=\"10\" value=\"60\">\n    </div>\n    <div class=\"row\">\n      <label>Particles per eruption <span id=\"amtVal\">1400</span></label>\n      <input id=\"amt\" type=\"range\" min=\"300\" max=\"3500\" step=\"100\" value=\"1400\">\n    </div>\n    <div id=\"stats\">\n      <span>Particles: <b id=\"pcount\">0</b></span>\n      <span><b id=\"fps\">60</b> fps</span>\n    </div>\n  </div>\n\n  <div id=\"pmeterWrap\">\n    <div id=\"plabel\">PRESSURE</div>\n    <div id=\"pmeter\"><div id=\"pfill\"></div></div>\n  </div>\n</div>\n\n<script>\n(function(){\n\"use strict\";\n\nvar cvs = document.getElementById('c');\nvar ctx = cvs.getContext('2d');\nvar W = cvs.width, H = cvs.height;\n\n/* ---------- terrain ---------- */\nvar CX = W/2, PEAK = 178, CRATER_Y = 194, BASE = 492, CH = 46, BH = 430;\n\nfunction terrainY(x){\n  var d = Math.abs(x - CX), y;\n  if (d < CH){\n    y = CRATER_Y;\n  } else if (d < BH){\n    var t = (d - CH)/(BH - CH);\n    var s = t*t*(3-2*t);\n    y = CRATER_Y + (BASE - CRATER_Y)*s;\n  } else {\n    y = BASE;\n  }\n  y += Math.sin(x*0.045)*2.2 + Math.sin(x*0.013+2.3)*1.6;\n  return y;\n}\n\n/* ---------- background prerender ---------- */\nvar bg = document.createElement('canvas');\nbg.width = W; bg.height = H;\n(function paintBG(){\n  var b = bg.getContext('2d');\n  // dusk sky\n  var sky = b.createLinearGradient(0,0,0,H*0.85);\n  sky.addColorStop(0,'#141024');\n  sky.addColorStop(0.55,'#3a2340');\n  sky.addColorStop(0.85,'#7a4040');\n  sky.addColorStop(1,'#b06a3a');\n  b.fillStyle = sky; b.fillRect(0,0,W,H);\n  // stars\n  b.fillStyle = '#ffffff';\n  for(var i=0;i<70;i++){\n    var sx = Math.random()*W, sy = Math.random()*H*0.45;\n    b.globalAlpha = 0.25 + Math.random()*0.55;\n    b.fillRect(sx, sy, 1.4, 1.4);\n  }\n  b.globalAlpha = 1;\n  // moon\n  var mg = b.createRadialGradient(810,80,4,810,80,46);\n  mg.addColorStop(0,'rgba(255,240,215,0.95)');\n  mg.addColorStop(0.35,'rgba(255,235,200,0.55)');\n  mg.addColorStop(1,'rgba(255,230,190,0)');\n  b.fillStyle = mg; b.beginPath(); b.arc(810,80,46,0,6.2832); b.fill();\n  // distant ridge\n  b.fillStyle = '#2a1e34';\n  b.beginPath(); b.moveTo(0,H*0.72);\n  for(var x=0;x<=W;x+=20){\n    b.lineTo(x, H*0.72 - 38*Math.sin(x*0.006+1.5) - 20*Math.sin(x*0.017));\n  }\n  b.lineTo(W,H); b.lineTo(0,H); b.fill();\n  // volcano cone\n  var vg = b.createLinearGradient(0,PEAK,0,H);\n  vg.addColorStop(0,'#4a3138');\n  vg.addColorStop(0.4,'#382530');\n  vg.addColorStop(1,'#1c1218');\n  b.fillStyle = vg;\n  b.beginPath();\n  b.moveTo(0, terrainY(0));\n  for(var x2=1;x2<=W;x2+=2) b.lineTo(x2, terrainY(x2));\n  b.lineTo(W,H); b.lineTo(0,H); b.closePath(); b.fill();\n  // subtle ridges on cone\n  b.strokeStyle = 'rgba(0,0,0,0.28)';\n  b.lineWidth = 3;\n  for(var k=-4;k<=4;k++){\n    if(k===0) continue;\n    b.beginPath();\n    var startX = CX + k*52;\n    var x3 = Math.max(0, Math.min(W, startX - k*26));\n    var x4 = Math.max(0, Math.min(W, startX + k*52));\n    b.moveTo(x3, terrainY(x3)+8);\n    for(var xx=x3; xx<x4; xx+=14) b.lineTo(xx, terrainY(Math.min(W,Math.max(0,xx)))+8+Math.sin(xx*0.09)*3);\n    b.stroke();\n  }\n  // crater rim highlight\n  b.strokeStyle = 'rgba(255,140,60,0.35)';\n  b.lineWidth = 2;\n  b.beginPath();\n  b.moveTo(CX-CH+6, CRATER_Y-1);\n  b.lineTo(CX+CH-6, CRATER_Y-1);\n  b.stroke();\n  // molten glow inside crater\n  var cg = b.createRadialGradient(CX,CRATER_Y+4,2,CX,CRATER_Y+4,52);\n  cg.addColorStop(0,'rgba(255,190,80,0.95)');\n  cg.addColorStop(0.5,'rgba(255,90,20,0.5)');\n  cg.addColorStop(1,'rgba(255,60,10,0)');\n  b.fillStyle = cg;\n  b.fillRect(CX-58, CRATER_Y-24, 116, 46);\n})();\n\n/* ---------- sprites ---------- */\nfunction makeSprite(size, stops){\n  var s = document.createElement('canvas');\n  s.width = s.height = size;\n  var sc = s.getContext('2d');\n  var g = sc.createRadialGradient(size/2,size/2,0,size/2,size/2,size/2);\n  for(var i=0;i<stops.length;i++) g.addColorStop(stops[i][0], stops[i][1]);\n  sc.fillStyle = g; sc.fillRect(0,0,size,size);\n  return s;\n}\nvar glowSprite = makeSprite(48, [\n  [0,'rgba(255,235,160,1)'],\n  [0.3,'rgba(255,150,50,0.75)'],\n  [1,'rgba(255,60,0,0)']\n]);\nvar ashSprite = makeSprite(48, [\n  [0,'rgba(150,140,145,0.55)'],\n  [0.55,'rgba(115,105,115,0.32)'],\n  [1,'rgba(90,85,95,0)']\n]);\n\n/* ---------- lava color buckets ---------- */\nvar keys = [\n  [0.00,[30,26,32]],[0.12,[58,42,42]],[0.25,[125,32,18]],\n  [0.42,[235,82,22]],[0.62,[255,152,42]],[0.82,[255,228,120]],[1.00,[255,255,196]]\n];\nvar BUCKETS = 24, coreStyles = [];\nfor(var bi=0; bi<BUCKETS; bi++){\n  var t = bi/(BUCKETS-1), seg=0;\n  while(seg < keys.length-2 && keys[seg+1][0] < t) seg++;\n  var a = keys[seg], c = keys[seg+1];\n  var f = (t - a[0])/(c[0]-a[0] || 1);\n  if(f<0) f=0; if(f>1) f=1;\n  var r = (a[1][0]+(c[1][0]-a[1][0])*f)|0;\n  var gg= (a[1][1]+(c[1][1]-a[1][1])*f)|0;\n  var bb= (a[1][2]+(c[1][2]-a[1][2])*f)|0;\n  coreStyles[bi] = 'rgb('+r+','+gg+','+bb+')';\n}\nfunction coreStyle(temp){\n  var i = (temp*23)|0;\n  if(i<0)i=0; if(i>23)i=23;\n  return coreStyles[i];\n}\n\n/* ---------- state ---------- */\nvar lava = [], ash = [];\nvar pressure = 0.15, state = 'building', eruptT = 0, emitAcc = 0, ashAcc = 0;\nvar PRESSURE_TIME = 8.0, ERUPT_DUR = 2.4;\nvar flash = 0;\n\nvar G = 560;\n\n/* ---------- controls ---------- */\nfunction $(id){ return document.getElementById(id); }\nvar powEl=$('pow'), windEl=$('wind'), amtEl=$('amt');\nvar powVal=$('powVal'), windVal=$('windVal'), amtVal=$('amtVal');\nvar pcountEl=$('pcount'), fpsEl=$('fps'), pfill=$('pfill');\nfunction windVal2(){ return parseFloat(windEl.value); }\nfunction updateLabels(){\n  powVal.textContent = powEl.value + '%';\n  var wv = parseFloat(windEl.value);\n  windVal.textContent = (wv===0 ? 'calm' : (wv>0?'\\u2192 ':'\\u2190 ') + Math.abs(wv));\n  amtVal.textContent = amtEl.value;\n}\npowEl.addEventListener('input',updateLabels);\nwindEl.addEventListener('input',updateLabels);\namtEl.addEventListener('input',updateLabels);\nupdateLabels();\n\n/* ---------- spawning ---------- */\nfunction spawnLava(mult, wind){\n  var a = -Math.PI/2 + (Math.random()-0.5)*1.15;\n  var sp = (150 + Math.random()*300) * mult * (0.55 + Math.random()*0.9);\n  lava.push({\n    x: CX + (Math.random()-0.5)*64,\n    y: CRATER_Y - 4 + Math.random()*6,\n    vx: Math.cos(a)*sp + wind*0.06,\n    vy: Math.sin(a)*sp,\n    temp: 1,\n    grounded: false,\n    vxg: 0\n  });\n}\nfunction spawnAsh(strength){\n  ash.push({\n    x: CX + (Math.random()-0.5)*70,\n    y: CRATER_Y - 6 - Math.random()*10,\n    vx: (Math.random()-0.5)*40,\n    vy: -(45 + Math.random()*90)*strength,\n    r: 8 + Math.random()*14,\n    grow: 14 + Math.random()*22,\n    alpha: 0.55 + Math.random()*0.35,\n    decay: 1/(4.5 + Math.random()*4)\n  });\n}\n\n/* ---------- main loop ---------- */\nvar last = performance.now();\nvar fpsSmooth = 60, uiTimer = 0;\n\nfunction frame(now){\n  var dt = (now - last)/1000;\n  last = now;\n  if(dt > 0.05) dt = 0.05;\n  if(dt <= 0) dt = 0.016;\n  var instFps = 1/dt;\n  fpsSmooth += (instFps - fpsSmooth)*0.08;\n\n  step(dt);\n  draw(dt);\n\n  uiTimer += dt;\n  if(uiTimer > 0.2){\n    uiTimer = 0;\n    pcountEl.textContent = lava.length + ash.length;\n    fpsEl.textContent = Math.round(fpsSmooth);\n    pfill.style.height = Math.max(3, pressure*100) + '%';\n    var hue = 130 - 130*Math.min(1,pressure);\n    pfill.style.background = (state==='erupting')\n      ? '#ffd23c'\n      : 'hsl('+hue+',85%,52%)';\n  }\n  requestAnimationFrame(frame);\n}\n\nfunction step(dt){\n  var powerMult = parseFloat(powEl.value)/100;\n  var wind = windVal2();\n  var amount = parseFloat(amtEl.value);\n\n  /* pressure / eruption state machine */\n  if(state === 'building'){\n    pressure += dt/PRESSURE_TIME;\n    // small ambient fumerole ash\n    ashAcc += dt*4;\n    while(ashAcc >= 1){ ashAcc -= 1; spawnAsh(0.35); }\n    if(pressure >= 1){\n      pressure = 1;\n      state = 'erupting';\n      eruptT = 0; emitAcc = 0; ashAcc = 0;\n      flash = 1;\n    }\n  } else {\n    eruptT += dt;\n    var prog = eruptT/ERUPT_DUR;\n    pressure = Math.max(0, 1 - prog);\n    var rate = amount/ERUPT_DUR * (1.6 - prog);   // front-loaded\n    emitAcc += dt*rate;\n    while(emitAcc >= 1 && lava.length < 6000){ emitAcc -= 1; spawnLava(powerMult, wind); }\n    ashAcc += dt*(amount*0.075);\n    while(ashAcc >= 1 && ash.length < 2600){ ashAcc -= 1; spawnAsh(1 + powerMult*0.4); }\n    if(eruptT >= ERUPT_DUR){\n      state = 'building';\n      pressure = 0.02;\n    }\n  }\n  if(flash > 0) flash = Math.max(0, flash - dt*1.8);\n\n  var i, p;\n\n  /* lava */\n  for(i = lava.length-1; i >= 0; i--){\n    p = lava[i];\n    if(!p.grounded){\n      p.vy += G*dt;\n      p.vx += wind*0.14*dt;\n      p.x += p.vx*dt;\n      p.y += p.vy*dt;\n      p.temp -= dt*0.045;\n      var ty = terrainY(p.x);\n      if(p.y >= ty){\n        p.grounded = true;\n        p.y = ty;\n        var slope = (terrainY(p.x+4)-terrainY(p.x-4))/8;\n        p.vxg = p.vx*0.25 + slope*90;\n        p.temp *= 0.93;\n      }\n    } else {\n      // slide downhill along local slope, viscous friction\n      var slope2 = (terrainY(p.x+4)-terrainY(p.x-4))/8;\n      p.vxg += slope2 * 1050 * dt;\n      var cool = p.temp; // hotter lava is runnier\n      p.vxg *= Math.exp(-(2.6 - 1.7*cool)*dt);\n      if(p.vxg >  260) p.vxg =  260;\n      if(p.vxg < -260) p.vxg = -260;\n      p.x += p.vxg*dt;\n      p.y = terrainY(p.x);\n      p.temp -= dt*(0.075 + 0.02*(1-p.temp));\n    }\n    if(p.x < -60 || p.x > W+60 || p.y > H+60 || p.temp < 0.018){\n      lava[i] = lava[lava.length-1]; lava.pop();\n    }\n  }\n\n  /* ash */\n  for(i = ash.length-1; i >= 0; i--){\n    p = ash[i];\n    p.vy -= 26*dt;                      // buoyancy\n    if(p.vy < -110) p.vy = -110;\n    p.vx += (wind*0.85 - p.vx)*dt*0.7;  // wind relaxation\n    p.x += p.vx*dt;\n    p.y += p.vy*dt;\n    p.r += p.grow*dt;\n    p.alpha -= p.decay*dt;\n    if(p.alpha <= 0.02 || p.y < -80){\n      ash[i] = ash[ash.length-1]; ash.pop();\n    }\n  }\n}\n\nfunction draw(dt){\n  ctx.drawImage(bg, 0, 0);\n\n  var i, p;\n\n  /* ash cloud (behind hot lava glow) */\n  for(i = 0; i < ash.length; i++){\n    p = ash[i];\n    ctx.globalAlpha = Math.max(0, Math.min(0.8, p.alpha));\n    ctx.drawImage(ashSprite, p.x - p.r, p.y - p.r, p.r*2, p.r*2);\n  }\n  ctx.globalAlpha = 1;\n\n  /* lava glow \u2014 additive */\n  ctx.globalCompositeOperation = 'lighter';\n  for(i = 0; i < lava.length; i++){\n    p = lava[i];\n    if(p.temp > 0.30){\n      var s = (p.grounded ? 14 : 10) + p.temp*16;\n      ctx.globalAlpha = Math.min(0.85, p.temp*0.8) * (p.grounded ? 0.75 : 1);\n      ctx.drawImage(glowSprite, p.x - s, p.y - s, s*2, s*2);\n    }\n  }\n  ctx.globalAlpha = 1;\n  ctx.globalCompositeOperation = 'source-over';\n\n  /* lava cores */\n  var x2, y2, sz;\n  for(i = 0; i < lava.length; i++){\n    p = lava[i];\n    ctx.fillStyle = coreStyle(p.temp);\n    if(p.temp < 0.18){\n      ctx.globalAlpha = Math.max(0.15, p.temp/0.18);\n    }\n    if(p.grounded){ ctx.fillRect(p.x-2, p.y-2.5, 4, 4); }\n    else          { ctx.fillRect(p.x-1.5, p.y-1.5, 3, 3); }\n    if(p.temp < 0.18) ctx.globalAlpha = 1;\n  }\n\n  /* eruption flash overlay */\n  if(flash > 0){\n    ctx.fillStyle = 'rgba(255,140,50,' + (0.22*flash) + ')';\n    ctx.fillRect(0,0,W,H);\n  }\n\n  /* crater glow pulse while erupting */\n  if(state === 'erupting'){\n    var pulse = 0.5 + 0.5*Math.sin(performance.now()*0.02);\n    var cg = ctx.createRadialGradient(CX, CRATER_Y, 4, CX, CRATER_Y, 90 + pulse*40);\n    cg.addColorStop(0, 'rgba(255,180,70,' + (0.4+0.2*pulse) + ')');\n    cg.addColorStop(1, 'rgba(255,80,10,0)');\n    ctx.fillStyle = cg;\n    ctx.fillRect(CX-150, CRATER_Y-150, 300, 180);\n  }\n\n  /* status text */\n  ctx.textAlign = 'center';\n  ctx.font = 'bold 16px Segoe UI, Arial';\n  if(state === 'erupting'){\n    ctx.fillStyle = '#ffd54a';\n    ctx.fillText('! ERUPTION !', CX, 40);\n  } else if(pressure > 0.8){\n    ctx.fillStyle = '#ff7a50';\n    ctx.fillText('ERUPTION IMMINENT', CX, 40);\n  }\n  ctx.textAlign = 'left';\n}\n\nrequestAnimationFrame(frame);\n})();\n</script>\n</body>\n</html>",
        "reasoning_content": "Need to create a volcano eruption simulation using canvas. Single file, no external libraries. Features:\n\n- Side-view volcano cone with crater, ground, and sky\n- Pressure meter that builds up and triggers an eruption when it reaches max, then resets\n- Lava particles fired from the crater at various velocities, gravity physics, landing on slopes, sliding/settling\n- Lava cooling: yellow \u2192 orange \u2192 red \u2192 dark rock color, then disappears\n- Ash cloud rising from crater, drifting with wind\n- Three sliders: eruption power, wind strength & direction, particle count\n- FPS + particle count counter, smoothly handles 1000+ particles\n\nDesign: canvas full window or fixed size (e.g., 960x600). Volcano profile defined by a ground height function \u2014 piecewise: slope from left base to crater rim, crater depression, right slope. Height function y(x) for terrain: a cone centered at cx with summit, crater between cx-w and cx+w.\n\nTerrain height function:\n```\nfunction terrainY(x) {\n  // cone: peak near summit at cx, crater is flat notch\n  const cx=W/2, base=H*0.85, peak=H*0.35;\n  const halfW = W*0.38;\n  const dx = Math.abs(x-cx);\n  let y;\n  if (dx < craterHalf) return craterY; // flat crater floor\n  else if (dx < halfBase) {\n    // slope linear from crater rim to base\n    t = (dx - craterHalf)/(halfBase - craterHalf);\n    y = craterY + t*(baseY-craterY)\n  } else baseY;\n}\n```\nAdd slight noise for realism, but keep height function deterministic and simple for physics.\n\nLava particles: state 'flight' and 'grounded'. During flight: vx += wind*small factor? Maybe lava is barely affected by wind; wind only affects ash. Grounded lava: slide down slope based on local terrain gradient, viscous, eventually solidifies and fades after lifetime.\n\nCooling: color based on temperature value, temp decays over lifetime. Color interpolation: bright yellow (#fff8a0) \u2192 orange \u2192 red \u2192 dark gray rock. When fully cold and settled, fade out and remove.\n\nAsh particles: rise, expand, alpha fade, wind push.\n\nEruption: pressure builds up over time, when >=1, eruption burst: spawn N lava particles and ash puffs over about 1.5 seconds. After eruption, pressure resets to 0 and builds up again.\n\nSliders: power (eruption force), wind (-10 to 10), particle count (lava particle count per eruption, e.g., 200\u20133000). Need 1000+ simultaneously \u2014 set slider max high enough.\n\nPerformance: use single canvas, avoid per-particle createShadowBlur (slow). Use radial gradient for lava glow? Too slow for 1000+. Instead draw glow with additive compositing: draw circles with globalCompositeOperation 'lighter'. Cheap: fillRect or arc. Use 'lighter' on canvas for pre-rendered glow sprite drawn at different sizes (drawImage). Pre-render glow sprite offscreen canvas (radial gradient), drawImage per particle \u2014 fast enough for ~2000.\n\nFPS: measure with requestAnimationFrame timestamps, compute smoothed dt, display.\n\nCap dt to avoid spiral.\n\nLava physics:\n- Spawn at crater center with random angle near vertical \u00b1 spread, speed depends on power slider and randomness.\n- Gravity ~ 500 px/s\u00b2.\n- Wind slightly affects airborne lava: vx += wind*5*dt (small).\n- Each frame check terrain collision: if y >= terrainY(x), land. Set grounded, place on surface, compute slope: gradient between terrainY(x+4) and terrainY(x-4). Slide: vx += gradient*g*slideFactor*dt, friction. Actually simpler: along slope vx proportional to slope.\n\nGrounded lava: cool faster, slide down slope until velocity slows enough (flatter ground) to settle, then stays, darkens, lifetime ticks then disappears? Requirement: \"fade to dark rock, then disappear\". So after some seconds of settling, fade out and delete. OK.\n\nAsh: spawn continuously during eruption from crater, vy upward ~ -30 to -80, vx = wind*factor + random, size grows, alpha decays. Gray color, use normal compositing, drawImage with soft sprites. Draw ash first (behind), then glow of lava.\n\nSky: gradient background, maybe sun, dark ground. Draw each frame \u2014 pre-render static scene (sky + volcano) on an offscreen canvas and drawImage on each frame. Terrain static \u2192 pre-render once (rebuild not needed; volcano shape fixed). Pre-render background on an offscreen canvas.\n\nUI: HTML control panel with sliders (power, wind, particle count) and readouts. Pressure meter: draw on canvas or HTML div? Draw on canvas side (right side) or use HTML element with CSS width. Easier as HTML div. Draw a vertical pressure bar on canvas, labeled.\n\nPressure building: pressure += dt / period, period ~ 6-10 seconds maybe. When >=1, trigger eruption that lasts a fixed duration during which emission occurs. During eruption pressure drains rapidly.\n\nEmission: at eruption start, emit particleCount of lava over eruptionDuration (e.g., 2 seconds) \u2014 spawn rate = count/duration. Plus continuous ash.\n\nAlso during pause, minor continuous ash from crater.\n\nParticle storage: arrays with object pool or simple arrays; splice removal or swap with last. Use arrays for lava and ash with swap-removal for performance.\n\nCounter: display activeParticles = lava.length + ash.length, FPS.\n\nLet's write the code.\n\nCanvas size: fixed 960x600, CSS centered, page background dark. Or make responsive. Fixed is fine.\n\nTerrain function:\n```\nconst W=960,H=600, groundY=H*0.82 (=492), peakY=H*0.30(=180)\ncx=W/2=480, craterHalf=42, baseHalf=400\nterrainY(x):\n d=|x-cx|\n if d<craterHalf: return peakY + (d/craterHalf)*10? crater floor slightly below rim: return peakY+14 - something; keep flat: peakY+12\n else if d<baseHalf: t=(d-craterHalf)/(baseHalf-craterHalf); eased? use t^1.15 maybe. y= peakY + (peak? hmm at d=craterHalf y=peakY(peak); slope down to groundY. y = peakY + (groundY-peakY)*easeOut(t)... y = peakY + (groundY-peakY)*t*t*(3-2t)? smoothstep gives S-curve, looks volcano-ish.\n else groundY + small noise.\n```\nAdd deterministic hash-based noise for roughness; but physics uses terrainY \u2014 noise could trap particles; keep noise small amplitude (2px) and same function for drawing & physics. Use sin-based noise: sin(x*0.05)*1.5 etc. OK.\n\nCrater rim: for d<craterHalf return peakY+10 (crater floor). But spawn from crater floor.\n\nPre-render background: sky gradient (dark stormy blue?), maybe skip stars; distant mountains; volcano cone filled with dark brown gradient and snow? Just dark brown with slight gradient, glow inside crater. Ground: dark strip below groundY.\n\nErupting state machine:\n- state 'building': pressure += dt/pressureTime; minor ash emissions.\n- when pressure>=1 \u2192 state 'erupting', eruptTimer=0.\n- during 'erupting': emit lava at rate, ash at rate, pressure = 1 - timer/duration*... decrease pressure to 0 across eruption; after timer done \u2192 'building', pressure=0.\n\nSliders:\n- power: 0.4\u20132.0 (speed factor)\n- wind: -200 to 200 px/s equivalent; display value and arrow direction.\n- amount: lava particles per eruption: 300\u20133000.\n\nAsh drawn with pre-rendered radial soft gray sprites; lava drawn with additive-composited glow sprite plus a core dot.\n\nLava color from temperature t (0..1): interpolate colors: white-yellow (255,245,180) \u2192 orange (255,150,40) \u2192 red (180,40,10) \u2192 dark (40,30,30). Compute rgb and fillStyle string. Setting fillStyle per particle for 1000+ is OK-ish but costly due to string creation. Alternative: quantize temperature to buckets (e.g., 8 levels), precompute color strings, and draw rects (fillRect 3x3) per particle. 2000 fillRect is fine. Plus glow sprite drawn with 'lighter' only on hotter ones (temp>0.4) \u2014 drawImage with scaled sprite. drawImage 1000 times fine.\n\nAsh sprite: 64px soft gray circle, drawn with scaling and globalAlpha. globalAlpha is cheap.\n\nLet's write the HTML.\n\nStructure:\n```\n<div id=\"wrap\">\n <canvas id=\"c\" width=960 height=600></canvas>\n <div id=\"ui\"> sliders + readout </div>\n</div>\n```\nUI overlaid top-left, pressure bar drawn on canvas (right side) or HTML? Pressure meter in DOM as vertical bar? \"Pressure meter\" \u2014 HTML works. Draw pressure bar on canvas (right) with label and glow near top at eruption. Actually easier with HTML: fixed-height container with div that fills from bottom, colored orange, width set in JS.\n\nWrite JS:\n\n```\nconst cvs=..., ctx=cvs.getContext('2d');\nconst W=960,H=600;\n// terrain\nfunction terrainY(x){...}\n// background pre-render\nconst bg=document.createElement('canvas'); ...\n// sprites\nfunction makeGlow(color stops) -> 64px radial canvas\nglowSprite = makeGlow('255,200,90');\nashSprite = makeGlow with gray.\n// state\nlet pressure=0.2, state='building', eruptT=0, pressureTime=7;\nlet lava=[], ash=[];\nlet lastTime=performance.now(), fps=60 smoothed.\nSliders: powEl, windEl, amtEl.\n\nspawnLava(power): pos at crater x=cx+rand(-30,30), y=craterFloorY-2; angle up spread: angle = -PI/2 + (rand-0.5)*1.1; speed = (200+rand*260)*power; maybe some speed scale with vertical bias. vx=cos*speed + wind*0.1? ; vy=sin*speed.\nlava item: {x,y,vx,vy,temp:1,grounded:false,vxg, life, settled}\ncooling: temp -= dt*(grounded?0.06+... :0.12). Lifetime: total ~ 4 + rand*6 seconds; after grounded and temp<0.2 start fade alpha.\n\nUpdate lava per dt:\n if !grounded:\n   vy+=g*dt (g=560)\n   vx+= wind*0.15*dt? Wind force small\n   x+=vx*dt; y+=vy*dt\n   if y>=terrainY(x): grounded=true; y=terrainY(x); temp*=0.9; kill bounce: vx = slope stuff; maybe bounce if fast and temp hot: vy=0; compute slope, grounded velocity vx = slope* something.\n   remove if off-canvas\n else:\n   slope=(terrainY(x+3)-terrainY(x-3))/6? gradient dy/dx; slide: vx += slope*g*0.55*dt (if slope positive downhill right \u2192 moves right)\n   vx *= pow(0.4,dt) friction? vx *= Math.exp(-1.6*dt)... wait, viscosity: if slope near zero, velocity dies fast.\n   x+=vx*dt; y=terrainY(x)\n   temp -= dt*(0.10 + 0.02)\n   life -=dt; if life<=0 \u2192 remove with alpha fade; store alpha via fadeLife.\nSimpler: track temp; after grounded, temp decays; when temp<0.15 start shrinking/removing \"cooled rock\" by temp -=dt*0.06, remove when <0.0? But temp used for color including rock color; remove when temp<-0.25 with alpha=... use alpha = clamp(temp/(-0.25)...) hmm: if temp<0.18 alpha=temp/0.18. remove if temp<0.\n\nAsh update:\n vy -= buoy*dt (accel upward, decreasing)\n vx += (windTarget - vx)*dt*0.8 wind push\n x+=vx*dt; y+=vy*dt; r+=grow*dt; alpha-=dt/life\n remove when alpha<=0 or y< -50\nAsh spawn during eruption also from crater with initial upward velocity; also small ambient.\n\nWind slider value in px/s (-250 to 250). Ash vx relaxes toward wind*0.9.\n\nRender:\n ctx.drawImage(bg)\n // ambient glow over crater when erupting: draw glow sprite large with 'lighter', scaled by intensity\n ash: ctx.globalCompositeOperation='source-over'? draw ash with normal alpha. Draw ash sprites: ctx.globalAlpha=a; drawImage(ashSprite, x-r,y-r,2r,2r)\n lava: ctx.globalCompositeOperation='lighter'; for hot ones draw glow sprite size = 10+temp*14, alpha=temp*0.7; then reset to 'source-over' for core rect: fillStyle=bucketColors[b]; fillRect(x-1.5,y-1.5,3,3); maybe core 2x2 with size for grounded lava 3-4px.\n draw pressure bar in DOM.\n \nBucket colors: temp 0..1 clamp: index=floor(temp*7)... map colors:\n t>=0.75: (255,250,190)\n ... define lerp between keyframe colors:\n keys: [[255,255,210],[255,210,90],[255,120,20],[160,30,10],[45,35,40],[30,28,32]]\n precompute 32 strings for temp buckets including darker for t<0 (cooled) (alpha handles fade).\n\nFPS: smoothed = smoothed*0.9 + (1/dt)*0.1; show Math.round, update every 0.25s.\n\nParticle count shown: lava.length+ash.length.\n\nEmission rate: lavaRate = amount/eruptDuration (2.2s). accumulate emitAcc += rate*dt; while >=1 spawn. Ash rate = amount*0.12 per second during eruption... e.g., 90/s. Plus during building ~5/s.\n\nCap: if lava.length > 4000 skip spawning? Slider max amount 3500. Grounded lava cools and disappears ~ life total 6-12s after launch. Peak count ~ slider amount. Fine.\n\nErupting visual flash: screen flash at start? Optional small glow. Also draw pressure text \"ERUPTION IMMINENT!\" when >0.8.\n\nCrater rim: floor at peakY+14, but rim slightly higher: for terrain, add slight rim bump for d in [craterHalf-10, craterHalf]? Skip.\n\nterrainY with noise:\n```\nfunction terrainY(x){\n const cx=W/2;\n const d=Math.abs(x-cx);\n const peak=178, floor=peak+16, base=492, ch=46, bh=430;\n let y;\n if(d<ch){ y=floor; }\n else if(d<bh){ const t=(d-ch)/(bh-ch); const s=t*t*(3-2*t); y=floor+(base-floor)*s; }\n else y=base;\n y += Math.sin(x*0.045)*2.2 + Math.sin(x*0.013+2)*1.5; // roughness\n return y;\n}\n```\nWait, near ground flat (d>bh) noise also applies, good \u2014 slight undulation of ground. But crater floor noise: fine.\n\nDraw background: sky gradient dark blue with lighter horizon; some stars? Sunset sky: gradient from #2b2340 top to #7a4b5a horizon? Eruptive ambience: dusk sky with orange glow at horizon. Distant ridges silhouette dark purple. Volcano body: path polygon along terrainY from x=0..W to bottom filled, fill with vertical gradient dark brown (#3a2a2a) \u2192 (#1d1418). Add inner glow at crater: draw radial orange in crater (part of bg) to hint molten pool. Ground: extend terrain beyond, fill to bottom H, dark color.\n\nAlso draw lava channel glow inside crater: orange blob at crater floor in bg.\n\nPre-render background on offscreen 'bgCanvas'.\n\nPressure meter DOM:\n```\n<div id=\"pmeter\"><div id=\"pfill\"></div><div id=\"plabel\">PRESSURE</div></div>\n```\nPosition absolute right: pfill height = pressure*100%.\n\nControl panel: label with span that displays value.\n\nWind slider: -250..250 step 10, label shows e.g. \"\u2192 120\" / \"\u2190 120\".\n\nAmount slider: 300..3500 step 100.\n\nPower slider: 40..200 (%) speed = pow/100.\n\nOK let's write. Also handle tab visibility clamping dt (dt=min(dt,0.05)).\n\nSwap removal: write helper that iterates backwards and splice(i,1)? splice is O(n) per removal \u2014 could be many removals per frame. Iterate backwards with arr.splice(i,1) for removal \u2014 with high removal count, worst O(n^2). Better: filter-style compaction: iterate backward and swap with last:\n```\nfor(let i=arr.length-1;i>=0;i--){ if(dead){ arr[i]=arr[arr.length-1]; arr.pop(); } }\n```\nDo update in loop that may swap-remove if i becomes last \u2014 process in backward loop; swap with last (already updated) then pop. If we go i--, the swapped-in element from the end is already updated? Going backward, indices greater than i are updated; arr[last] is updated \u2014 good.\n\nDraw ash behind lava. Also draw ash above volcano (yes, above).\n\nGlow sprite make:\n```\nfunction radial(sprite color inner) { size=64; c=canvas 64; g=ctx.createRadialGradient(32,32,0,32,32,32); g.addColorStop(0,'rgba(255,220,120,1)'); ... }\n```\nAsh sprite: 'rgba(120,115,120,0.6)' center to transparent.\n\nPerf for drawImage of glow sprite for each hot lava particle: fine.\n\nAlso eruption lightning/glow overlay: when erupting, subtle orange overlay: ctx.fillStyle=`rgba(255,120,30,${0.08*eruptIntensity})`; fillRect whole. Erupt intensity = decaying pulse.\n\nPressure building speed: base period ~8 seconds, maybe faster? pressure += dt/8.\n\nAsh spawn also during building (small puffs at crater with low alpha).\n\nEdge cases: lava spawn: some low-speed lava fountains: speed range (140+Math.random()*300)*speedMult, speedMult=power/100. Angle: -90\u00b0 \u00b1 35\u00b0.\n\nWind on airborne lava: vx += wind*dt*0.12 (small).\n\nGrounded slide: slope = (terrainY(x+4)-terrainY(x-4))/8; vx += slope*900*dt; vx*=Math.exp(-2.2*dt); cap |vx|. On steep slopes slide down, stasis near crater floor. Cool faster: temp -= dt*(0.055 + 0.05*(1-temp)). Also slide near-flat ground off canvas side \u2192 remove when x<-50||x>W+50.\n\nGrounded lifetime: total temp starts at 1; hot decays during flight 0.055/s, grounded 0.09/s \u2192 ~11s to 0, then alpha=temp<0.2? remove at temp<0.02, alpha=min(1,temp/0.18).\n\nWait temp decays during flight too: 0.045/s. Total life from spawn ~13-18s. Peak count ~ amount (fine, they disappear before next eruption in ~8+2 sec? eruption every ~10s; grounded rock lasts ~11s after launch... may overlap somewhat with next eruption, boosting count above 1000 \u2014 good for requirement.)\n\nColor keys (temp t 0..1):\nt=1: 255,255,190\n0.8: 255,230,120\n0.6: 255,150,40\n0.4: 235,80,20\n0.25: 120,30,15\n0.1: 55,40,40\n0: 30,26,30\nBelow: same dark.\n\nPrecompute 24 buckets for t in [0,1] and index = clamp(floor(t*23)).\n\nBucket for core size: size = grounded? 4:3 px. Use fillRect(x-2,y-2,4,4) for grounded and 3x3 for airborne. Batch: iterate particles, set fillStyle string per particle (string reuse from bucket array, cheap). fillStyle change is somewhat costly but 2000 fillRects OK.\n\nSort by bucket to reduce state changes? Overkill.\n\nAsh drawing: alpha per particle, globalAlpha per draw. 600-1500 ash sprites fine.\n\nOK, background also includes a faint moon. Keep moderate.\n\nAlso add \"ERUPTING!\" indicator text drawn on canvas when erupting.\n\nWrite HTML file. Ensure sliders styled to look decent.\n\nAlso add pressure label at eruption: fill turns red pulsing? Simple: pfill background gradient; when state is erupting show fill=orange, else green\u2192red? Simpler: fill color based on pressure: hue = 120 - 120*pressure. Set style.background = `hsl(...)`.\n\nLet's write it out.\n\nFinal check on requirement: \"slide down slope or settle\" \u2014 done. \"live on-screen counter of active particles and fps\" \u2014 DOM counter updated. \"stay smooth above 1000 particles\" \u2014 default amount slider 1500, plus pooling for perf.\n\nOne more: ash drifts with wind \u2014 yes.\n\nCode:\n\n```html\n<!DOCTYPE html>\n<html>...\n```\n\nWrite it in full.\n"
      }
    }
  ],
  "created": 1787816972,
  "model": "~/.cache/huggingface/hub/models--unsloth--Qwen3.8-Flash-Next-GGUF/snapshots/824f539b2710e5a9e47af4952cf6578cf5ee8932/UD-Q3_K_XL/Qwen3.8-Flash-Next-UD-Q3_K_XL-00001-of-00003.gguf",
  "system_fingerprint": "b10667-213df585b",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 10947,
    "prompt_tokens": 236,
    "total_tokens": 11183,
    "prompt_tokens_details": {
      "cached_tokens": 0
    }
  },
  "id": "chatcmpl-C8xAFJlb7ZOfSXhroDOpamQLECgzdwls",
  "timings": {
    "cache_n": 0,
    "prompt_n": 236,
    "prompt_ms": 1092.64,
    "prompt_per_token_ms": 4.629830508474576,
    "prompt_per_second": 215.99062820325082,
    "predicted_n": 10947,
    "predicted_ms": 560605.036,
    "predicted_per_token_ms": 51.21551580486022,
    "predicted_per_second": 19.52533298327345
  }
}