{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>Volcano Eruption Simulator</title>\n<style>\n  html,body{margin:0;padding:0;background:#111;color:#ddd;font-family:Segoe UI,Arial,sans-serif;height:100%;}\n  #wrap{max-width:980px;margin:10px auto;padding:0 10px;}\n  canvas{width:100%;display:block;background:#000;border-radius:8px;box-shadow:0 0 30px #000;}\n  #controls{display:flex;flex-wrap:wrap;gap:18px;margin-top:12px;justify-content:center;}\n  .ctl{flex:1 1 240px;max-width:320px;background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:8px 12px;}\n  .ctl label{display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px;color:#f7a85c;}\n  .ctl input{width:100%;accent-color:#ff6a00;cursor:pointer;}\n  h1{font-size:16px;color:#f7a85c;text-align:center;margin:8px 0;}\n</style>\n</head>\n<body>\n<div id=\"wrap\">\n  <h1>VOLCANO ERUPTION SIMULATOR</h1>\n  <canvas id=\"cv\" width=\"960\" height=\"540\"></canvas>\n  <div id=\"controls\">\n    <div class=\"ctl\"><label>Eruption Power <span id=\"powV\">1.0</span></label>\n      <input id=\"pow\" type=\"range\" min=\"0.3\" max=\"2.0\" step=\"0.05\" value=\"1.0\"></div>\n    <div class=\"ctl\"><label>Wind (\u2190 / \u2192) <span id=\"windV\">0.15</span></label>\n      <input id=\"wind\" type=\"range\" min=\"-1\" max=\"1\" step=\"0.01\" value=\"0.15\"></div>\n    <div class=\"ctl\"><label>Particle Amount <span id=\"amtV\">1.0</span></label>\n      <input id=\"amt\" type=\"range\" min=\"0.2\" max=\"2.0\" step=\"0.05\" value=\"1.0\"></div>\n  </div>\n</div>\n<script>\n(function(){\n\"use strict\";\nconst cv=document.getElementById('cv'), ctx=cv.getContext('2d');\nconst W=cv.width, H=cv.height;\n\n/* ---------- scene geometry ---------- */\nconst groundY=430, topY=138, craterHalf=34, craterDepth=16;\nconst cx=W/2, craterL=cx-craterHalf, craterR=cx+craterHalf, craterFloor=topY+craterDepth;\nconst mLeft=(topY-groundY)/craterL;                 // negative (left slope)\nconst mRight=(groundY-topY)/(W-craterR);            // positive (right slope)\nconst G=0.16;\n\nfunction surfY(x){\n  if(x<0||x>W) return groundY+200;\n  if(x<craterL) return groundY+mLeft*x;\n  if(x<=craterR) return craterFloor;\n  if(x<=W) return topY+mRight*(x-craterR);\n  return groundY;\n}\nfunction surfM(x){\n  if(x<craterL) return mLeft;\n  if(x<=craterR) return 0;\n  return mRight;\n}\n\n/* ---------- controls ---------- */\nconst slPow=document.getElementById('pow'), slWind=document.getElementById('wind'), slAmt=document.getElementById('amt');\nconst vPow=document.getElementById('powV'), vWind=document.getElementById('windV'), vAmt=document.getElementById('amtV');\nfunction labels(){\n  vPow.textContent=(+slPow.value).toFixed(2);\n  vWind.textContent=(slWind.value>0?'\u2192 ':(slWind.value<0?'\u2190 ':''))+Math.abs(+slWind.value).toFixed(2);\n  vAmt.textContent=(+slAmt.value).toFixed(2);\n}\n[slPow,slWind,slAmt].forEach(s=>s.addEventListener('input',labels)); labels();\n\n/* ---------- sprites ---------- */\nfunction softSprite(size,stops){\n  const c=document.createElement('canvas'); c.width=c.height=size;\n  const g=c.getContext('2d');\n  const grad=g.createRadialGradient(size/2,size/2,0,size/2,size/2,size/2);\n  stops.forEach(s=>grad.addColorStop(s[0],s[1]));\n  g.fillStyle=grad; g.fillRect(0,0,size,size);\n  return c;\n}\nconst hotPalette=[[70,15,8],[120,25,10],[168,40,12],[205,62,15],[225,95,22],[242,145,35],[255,192,80],[255,242,150]];\nconst hotSprites=hotPalette.map(c=>softSprite(40,[\n  [0,`rgba(${c[0]},${c[1]},${c[2]},1)`],\n  [0.35,`rgba(${c[0]},${c[1]},${c[2]},0.75)`],\n  [1,`rgba(${c[0]},${c[1]},${c[2]},0)`]\n]));\nconst rockSprite=softSprite(36,[\n  [0,'rgba(52,48,46,0.95)'],[0.6,'rgba(38,34,32,0.85)'],[1,'rgba(30,28,26,0)']]);\nconst ashSprite=softSprite(72,[\n  [0,'rgba(150,145,140,0.55)'],[0.55,'rgba(120,115,112,0.30)'],[1,'rgba(100,98,96,0)']]);\nconst glowSprite=softSprite(128,[\n  [0,'rgba(255,150,40,0.9)'],[0.4,'rgba(255,80,20,0.35)'],[1,'rgba(255,60,10,0)']]);\n\n/* ---------- particles ---------- */\nconst lava=[], ash=[];\nconst MAX_LAVA=4000, MAX_ASH=900;\n\nfunction spawnLava(power,amount){\n  const n=Math.max(1,Math.round(13*amount));\n  for(let i=0;i<n;i++){\n    if(lava.length>=MAX_LAVA) lava.shift();\n    lava.push({\n      x:cx+(Math.random()-0.5)*craterHalf*1.6,\n      y:topY+6,\n      vx:(Math.random()-0.5)*2.6*power,\n      vy:-(5.5+Math.random()*7)*power,\n      size:2+Math.random()*3.5,\n      temp:1, landed:false, settled:false, alpha:1, m:0\n    });\n  }\n}\nfunction spawnAsh(count){\n  for(let i=0;i<count;i++){\n    if(ash.length>=MAX_ASH) ash.shift();\n    ash.push({\n      x:cx+(Math.random()-0.5)*craterHalf*1.8,\n      y:topY+2,\n      vx:(Math.random()-0.5)*0.8,\n      vy:-(0.8+Math.random()*1.6),\n      r:6+Math.random()*8,\n      alpha:0.35+Math.random()*0.3,\n      wob:Math.random()*Math.PI*2\n    });\n  }\n}\n\n/* ---------- state ---------- */\nlet pressure=0, erupting=false, eruptTimer=0;\nlet frames=0, fps=0, fCount=0, fTimer=performance.now();\n\n/* ---------- sky gradient (prebuilt) ---------- */\nconst skyGrad=ctx.createLinearGradient(0,0,0,H);\nskyGrad.addColorStop(0,'#1b2a4a'); skyGrad.addColorStop(0.55,'#40517a'); skyGrad.addColorStop(1,'#7b6a63');\n\n/* ---------- main loop ---------- */\nfunction step(){\n  requestAnimationFrame(step);\n  const power=+slPow.value, wind=+slWind.value, amount=+slAmt.value;\n\n  /* pressure / eruption cycle */\n  if(!erupting){\n    pressure+=0.0022+0.0008*power;\n    if(pressure>=1){ pressure=1; erupting=true; eruptTimer=70+power*50; }\n    // ambient seep smoke as pressure builds\n    if(pressure>0.45 && Math.random()<Math.pow(pressure,3)*0.9*amount) spawnAsh(1);\n  }else{\n    pressure=Math.max(0,1-eruptTimer/(70+power*50));\n    eruptTimer--;\n    spawnLava(power,amount);\n    if(Math.random()<0.75*amount) spawnAsh(Math.round(2*amount));\n    if(eruptTimer<=0){ erupting=false; pressure=0; }\n  }\n\n  /* update lava */\n  let activeLava=0;\n  for(let i=lava.length-1;i>=0;i--){\n    const p=lava[i];\n    if(!p.landed){\n      p.vy+=G;\n      p.vx+=wind*0.004;\n      p.x+=p.vx; p.y+=p.vy;\n      p.temp=Math.max(0,p.temp-0.0009);\n      const sy=surfY(p.x);\n      if(p.y>=sy && p.x>0 && p.x<W){\n        p.y=sy; p.landed=true; p.m=surfM(p.x);\n        p.vx*=0.55;\n      }\n      if(p.y>H+40||p.x<-30||p.x>W+30){ lava.splice(i,1); continue; }\n    }else if(!p.settled){\n      // slide downhill along the surface\n      p.vx+=G*p.m*2/(1+p.m*p.m);\n      p.vx*=0.985;\n      p.x+=p.vx; p.y=surfY(p.x);\n      p.temp=Math.max(0,p.temp-0.0030);\n      if(Math.abs(p.vx)<0.03){ p.settled=true; p.vx=0; }\n      if(p.x<-20||p.x>W+20){ lava.splice(i,1); continue; }\n    }else{\n      p.temp=Math.max(0,p.temp-0.0038);\n      if(p.temp<=0.02){\n        p.alpha-=0.004;\n        if(p.alpha<=0){ lava.splice(i,1); continue; }\n      }\n    }\n    activeLava++;\n  }\n\n  /* update ash */\n  for(let i=ash.length-1;i>=0;i--){\n    const a=ash[i];\n    a.wob+=0.05;\n    a.vx+=wind*0.012+Math.sin(a.wob)*0.01;\n    a.vx*=0.995; a.vy*=0.995;\n    a.x+=a.vx; a.y+=a.vy;\n    a.r+=0.35;\n    a.alpha-=0.0025;\n    if(a.alpha<=0||a.y<-60||a.x<-80||a.x>W+80){ ash.splice(i,1); continue; }\n  }\n\n  draw(power,wind,activeLava+lava.length*0+ash.length);\n\n  /* fps */\n  fCount++;\n  const now=performance.now();\n  if(now-fTimer>=500){ fps=Math.round(fCount*1000/(now-fTimer)); fCount=0; fTimer=now; }\n}\n\n/* ---------- drawing ---------- */\nfunction draw(power,wind,particleCount){\n  ctx.globalCompositeOperation='source-over';\n  ctx.globalAlpha=1;\n\n  /* sky */\n  ctx.fillStyle=skyGrad; ctx.fillRect(0,0,W,H);\n\n  /* distant hills */\n  ctx.fillStyle='#3a4152';\n  ctx.beginPath();\n  ctx.moveTo(0,430); ctx.lineTo(140,330); ctx.lineTo(300,430); ctx.closePath(); ctx.fill();\n  ctx.beginPath();\n  ctx.moveTo(560,430); ctx.lineTo(760,320); ctx.lineTo(960,430); ctx.closePath(); ctx.fill();\n\n  /* ash cloud (behind cone) */\n  for(const a of ash){\n    ctx.globalAlpha=Math.max(0,a.alpha);\n    ctx.drawImage(ashSprite,a.x-a.r,a.y-a.r,a.r*2,a.r*2);\n  }\n  ctx.globalAlpha=1;\n\n  /* crater glow */\n  const glowAmt=erupting?1:Math.pow(pressure,3);\n  if(glowAmt>0.02){\n    ctx.globalAlpha=glowAmt;\n    const s=60+40*glowAmt;\n    ctx.drawImage(glowSprite,cx-s/2,topY-s/2+8,s,s);\n    ctx.globalAlpha=1;\n  }\n\n  /* ground */\n  const gg=ctx.createLinearGradient(0,groundY,0,H);\n  gg.addColorStop(0,'#4a3f38'); gg.addColorStop(1,'#241f1c');\n  ctx.fillStyle=gg; ctx.fillRect(0,groundY,W,H-groundY);\n\n  /* volcano cone */\n  ctx.fillStyle='#332c2b';\n  ctx.beginPath();\n  ctx.moveTo(0,groundY);\n  ctx.lineTo(craterL,topY);\n  ctx.lineTo(craterR,topY);\n  ctx.lineTo(W,groundY);\n  ctx.lineTo(W,H); ctx.lineTo(0,H); ctx.closePath();\n  ctx.fill();\n  /* rim shading */\n  ctx.fillStyle='#221c1c';\n  ctx.beginPath();\n  ctx.moveTo(craterL,topY); ctx.lineTo(craterR,topY);\n  ctx.lineTo(craterR+14,topY+20); ctx.lineTo(craterL-14,topY+20); ctx.closePath();\n  ctx.fill();\n  ctx.fillStyle='#15100f';\n  ctx.beginPath();\n  ctx.moveTo(craterL+4,topY+craterDepth+2); ctx.lineTo(craterL+10,topY+6);\n  ctx.lineTo(craterR-10,topY+6); ctx.lineTo(craterR-4,topY+craterDepth+2); ctx.closePath();\n  ctx.fill();\n\n  /* cooled lava rock (normal blend) */\n  for(const p of lava){\n    if(p.temp>0.12) continue;\n    const s=p.size*2.2;\n    ctx.globalAlpha=Math.max(0,p.alpha*0.9);\n    ctx.drawImage(rockSprite,p.x-s,p.y-s,s*2,s*2);\n  }\n  ctx.globalAlpha=1;\n\n  /* hot lava (additive glow) */\n  ctx.globalCompositeOperation='lighter';\n  for(const p of lava){\n    if(p.temp<=0.12) continue;\n    let b=Math.floor(p.temp*7.999); if(b<0)b=0; if(b>7)b=7;\n    const s=p.size*(1.6+p.temp*1.6);\n    ctx.globalAlpha=Math.min(1,0.35+p.temp)*p.alpha;\n    ctx.drawImage(hotSprites[b],p.x-s,p.y-s,s*2,s*2);\n  }\n  ctx.globalCompositeOperation='source-over';\n  ctx.globalAlpha=1;\n\n  /* ---------- HUD ---------- */\n  /* pressure meter */\n  const bx=W-220, by=18, bw=190, bh=22;\n  ctx.fillStyle='rgba(0,0,0,0.55)'; ctx.fillRect(bx-8,by-8,bw+16,66);\n  ctx.strokeStyle='#888'; ctx.lineWidth=1; ctx.strokeRect(bx,by,bw,bh);\n  const pg=ctx.createLinearGradient(bx,0,bx+bw,0);\n  pg.addColorStop(0,'#ffd94a'); pg.addColorStop(0.6,'#ff7a00'); pg.addColorStop(1,'#ff1a00');\n  ctx.fillStyle=pg; ctx.fillRect(bx+2,by+2,(bw-4)*pressure,bh-4);\n  ctx.fillStyle='#ddd'; ctx.font='12px monospace'; ctx.textAlign='left';\n  ctx.fillText('PRESSURE',bx,by+bh+16);\n  ctx.fillText(erupting?'*** ERUPTION! ***':'Building...',bx+bw-100,by+bh+16);\n\n  /* stats */\n  ctx.fillStyle='rgba(0,0,0,0.55)'; ctx.fillRect(10,10,170,48);\n  ctx.fillStyle='#9f9';\n  ctx.font='13px monospace';\n  ctx.fillText('Particles: '+(lava.length+ash.length),18,28);\n  ctx.fillText('FPS: '+fps,18,48);\n}\n\nstep();\n})();\n</script>\n</body>\n</html>\n</body>\n</html>",
        "reasoning_content": "Create a volcano eruption simulation using canvas. Single HTML file. Sliders (power, wind, particle count), pressure meter, lava particles using gravity physics, slope collision, cooling color, ash particles, FPS counter, particle count counter.\n\nDesign:\n- Canvas size probably 900x500, responsive.\n- Volcano: mountain shape with a crater in the middle. Two slopes from the edges to the crater rim. Simple polygon: ground at y = H, peak (crater) at (W/2, topY). Crater opening between peakX-30 and peakX+30.\n- Pressure builds over time; when it reaches max, erupt: spawn N lava particles with random velocities mostly upward; reset pressure.\n- Lava particle: x, y, vx, vy, life, temperature. Gravity g = 0.15. Collision with slopes: defined as line segments of the triangle. When it lands, it flows downhill: along the slope, with velocity influenced by wind? Gravity along the slope, keep flowing along the slope surface. Settle once velocity is low. Cooling: lifetime-based color: yellow \u2192 orange \u2192 red \u2192 dark grey. Settle duration, then fade.\n- Slope function: height at x: left slope is from (0, H) to (craterLeftX, craterTopY)... actually simpler: volcano defined as piecewise: left slope from (0,H) to (cx - craterHalfW, topY), right slope from (cx+craterHalfW, topY) to (W,H). Crater between them at topY: particles spawned inside crater fall out and land. Use slope surface: surfaceY(x). height at x: left slope from (0,H) to (cx-craterHalfW, topY), right slope from (cx+craterHalfW, topY) to (W,H). Crater between is at topY: particles spawned inside crater exit crater and land. Use surfaceY(x): if x between crater edges return crater bottom (topY + some depth? maybe make crater floor slightly lower). If x < craterL: H - (H - topY) * (x - 0)/(craterL - 0)? slope from ground on left to peak at left edge of crater. Actually the slope from ground to peak... left slope from (0,H) to (craterL, topY). So surfaceY = H - (H - topY)*(x)/(craterL). Outside craterL..craterR it's H, but topY inside crater.\n\nLava landed: if particle crosses surfaceY, snap to it, velocity along slope proportional to tan. On slopes there's gravity along slope. Slope: left slope angle \u2014 surface decreases with x... slope s = (topY - H)/(craterL - 0), negative (higher as x increases). Particle on left slope flows downhill to left (decreasing x). On right slope to right. So tangential acceleration = g * sin(theta)* direction toward base. Compute per side.\n\nSimplify: when on surface, vx += slopeAccel*dir, position y = surfaceY(x). Friction. If |velocity| tiny, settle: vy component stops \u2014 set landed=true, keep sliding with friction, then when speed < eps stop, becoming \"rock\", fade after coolTime.\n\nAsh: spawn from crater during eruption (also small ambient?), rise, get random walk, drift with wind, expand, fade.\n\nWind slider: -1 to 1 (arbitrary units). Power slider: eruption strength. Particle amount slider: density multiplier for eruption density and ash.\n\nPressure meter: bar on screen, drawn in canvas or HTML. HTML panel: draw in top corner on canvas. Simpler: canvas draw.\n\nFPS: compute per second.\n\nCap particles at some max like 4000 for performance. Use simple arrays.\n\nColors: glow via composite \"lighter\" for hot lava. Avoid per-particle shadowBlur (slow). Precompute? Draw radial gradient sprite once offscreen and drawImage \u2014 fast. Make sprite of white circle gradient, colorize with globalAlpha and... colorization cost. Approach: draw lava as circles with fillStyle chosen from palette buckets via fillRect \u2014 decent. For glow, draw a bigger circle of low alpha on top. To keep fast, precompute offscreen canvases: e.g., 6 sprites (different colors) of radial gradient, and drawImage with scale. Colors: interpolate along cooling palette but sprite per color bucket (e.g., 8 buckets). Sprites generated at load time.\n\nStructure: Lava particle holds temperature t (0..1 decreasing over lifetime). Color bucket = floor(t*7). Draw sprite accordingly with \"lighter\". Settle rocks drawn normal dark fill, no lighter \u2014 maybe fade.\n\nSimpler approach: keep all lava particles drawn with \"lighter\" additive. Settle rocks fade out, dark rock color is barely visible with additive. Hmm additive of dark red is fine. Use two passes: hot particles (t>0.15) additive, cooled ones drawn normally as dark rock. Good.\n\nAsh: grey circle drawn with globalAlpha, normal composite, drawn after lava? Ash behind (first)? Ash rises from crater, drawn before lava \u2014 drawn after? Draw ash below (first) then lava on top. Ash rises up so overlap minimal. Draw ash then lava. Also draw ash on top of volcano shape. Ash behind lava.\n\nVolcano drawing: sky gradient background, then ash cloud, then volcano cone (polygon) drawn on top of landed lava? Landed lava should be on top of cone. Draw order: sky, ash, volcano, lava (rock then hot with additive), UI.\n\nWait, ash should drift across sky \u2014 draw after volcano is behind volcano. Fine: ash behind cone looks fine (smoke behind cone visible above crater). Good.\n\nPressure: builds at rate, eruption occurs when pressure==1: burst spawn, plus continuous spawn during short eruption duration for realism: eruptionTimer, rate depends on particle amount. After timer ends, pressure resets to 0 and starts building again.\n\nWind affects ash: vx += wind*0.02; affects lava slightly? In flight only affect ash (realistic).\n\nCounter: text at top-left: \"Particles: N   FPS: F\".\n\nSliders: HTML input range below canvas in a controls div, dark styling.\n\nCanvas size: fixed 960x540, scale by CSS.\n\nCode specifics:\n\n```js\nconst W=960,H=540; groundY = H (bottom). volcano: cx=W/2, craterHalf=34, topY=140; baseY=H (ground). Left slope from (0,H) to (cx-craterHalf, topY)? then slopes reach ground at edges. Might add a second smaller peak. Simple cone with wide base is fine: baseY = H+? Ground level for lava settling: groundY=430 with ground rectangle below. Actually side view: ground line at y=430, below it is earth-brown ground fill. Volcano cone from groundY to topY.\n\nsurfaceY(x):\nif x < craterL: if x<=0 return groundY (but slope reaches at x=0) \u2014 cone base spans entire width. surfaceY = groundY + (topY - groundY) * (x - 0)/(craterL - 0) \u2192 at x=0 groundY, at craterL topY. Clamp.\nif x within crater: return craterFloorY = topY+14.\nif x > craterR: mirror. Beyond cone base (x out of [0,W]) still fine.\n```\n\nLava physics: while in flight, vy += G; vx += wind*0.005 (tiny air drag on lava too, small). If y >= surfaceY(x): landed. If inside crater (x within craterX range) settle there. Compute slope s (surfaceY derivative): left slope sL = (topY-groundY)/craterL (negative). Tangential gravity: on slope, acceleration along x = G * s * something... For surface y = a + m*x, gravity along slope acceleration = g*m/sqrt(1+m\u00b2)*... direction: particle accelerates in direction of downhill = if m<0 downhill means decreasing x. Use ax = -G*m / (1+m\u00b2)?? For 1D sliding with slope y = m x, acceleration along x = g\u00b7sin \u03b8\u00b7cos \u03b8 direction downhill = -g\u00b7m/(1+m\u00b2)... check: downhill = decreasing y. If m negative (left slope: y decreases as x increases? left slope: as x increases toward peak y decreases (upward)). Downhill side: toward x=0. So acceleration should be negative. With m negative, -g\u00b7m/(1+m\u00b2): -g\u00b7m is positive \u2192 wrong. Use ax = g\u00b7m/(1+m\u00b2): negative \u2192 leftward. Hmm derivation: acceleration along slope: ax = -g\u00b7m/(1+m\u00b2)? Test: m=0 flat \u2192 0 good. Steep negative m: should accelerate left (negative). ax = g\u00b7m/(1+m\u00b2) \u2192 negative \u2713. For positive m (right side slope rises as x increases then... right slope: from peak (craterR, topY) to (W, groundY): m = (groundY-topY)/(W-craterR) positive) downhill = increasing x \u2192 positive ax \u2713 with ax = g\u00b7m/(1+m\u00b2). Wait but left slope in y-down coords: y = groundY at x=0, y=topY at craterL: m = (topY-groundY)/craterL <0 since topY<groundY. Downhill at left: toward x=0 \u2192 negative direction. ax = g\u00b7m/(1+m\u00b2) negative \u2713. But gravity along slope magnitude = g\u00b7sin\u03b8 = g\u00b7|m|/\u221a(1+m\u00b2)... ax = -g sin\u03b8 * (slope direction)... well, the approximation ax = g\u00b7m\u00b7? let me derive: unit tangent t = (1, m)/\u221a(1+m\u00b2). Gravity vector in y-down coords = (0, +g). Acceleration along slope = (g_vec\u00b7t) t = g\u00b7m/(1+m\u00b2) \u00b7 (1,m). So ax = g\u00b7m/(1+m\u00b2) \u2713 and ay = g\u00b7m\u00b2/(1+m\u00b2) but we'll constrain to surface: just set x += vx, y = surfaceY(x). Add friction: vx *= 0.99, plus when |vx| < 0.02 settle.\n\nCooling: temperature decreases by coolRate; when landed cooling doubles (contact). lifeT: temperature starts at 1, decays at 0.003/frame in flight, 0.008 on ground. Once landed and settled and temperature below about 0.1, becomes rock: dark grey with alpha fading over some seconds, then removed. But removing rock leaves no accumulated lava \u2014 fine (particles disappear so \"settles\").\n\nAlso constrain particles to canvas.\n\nSprites: build 8 color sprites for hot colors, palette from white-yellow to dark red. Plus rock sprite (grey soft circle). Precompute radial gradients on offscreen canvas, size 32px. Draw with drawImage scaled.\n\nAsh particles: x,y,vx,vy,r,alpha,life. Spawn from crater during eruptions: vy = -(1+rand), vx = wind + random. Expand: r += 0.3. Alpha fades. Draw grey radial gradient sprite, scaled. Wind pushes ash: vx += wind*0.01.\n\nWind slider: -1..1 (arbitrary units). Power slider: eruption power. Particle amount slider: eruption density and ash.\n\nErupt logic:\n```\nif(!erupting){ pressure += 0.0015 * ? maybe scaled by power? Build rate constant; when >=1: erupting=true, eruptTimer = 90 + power*... frames; }\nwhile erupting: spawn per frame = particleAmount scaled count; if timer done: erupting=false; pressure=0;\n```\n\nEruption spawn: from crater: x = cx + rand*craterHalf, y = topY+10; vx = (rand-0.5)*2 * spread*powerFactor... vx = (Math.random()-0.5)*4, vy = -(6 + Math.random()*6)*power. Power slider 0.3..2.\n\nCap: if lava array length > MAX (like 6000) remove oldest.\n\nFPS: frame counter, update every 500ms.\n\nDraw UI: pressure meter: bar at top-left, vertical or horizontal. Draw horizontal bar labeled with gradient.\n\nLet me also add slight glow in crater before eruption when pressure high.\n\nVolcano shape draw: path polygon (0,groundY)\u2192(craterL,topY)\u2192(craterR,topY)\u2192(W,groundY) plus below to bottom corners, filled with mountain gradient. Ground rect below groundY. Crater glow: when erupting or high pressure, draw radial at crater.\n\nAlso small background mountains optional; skip, keep sky gradient + maybe sun? Keep sky gradient simple + ground.\n\nLava draw: hot ones additive pass. Cooled rock (settled, dark): draw dark grey sprite normal. Manage: each lava p has settled flag, temp. Bucket: temp>0.05 \u2192 hot sprite (bucket = clamp(floor(temp*8))). Else rock.\n\nLava draw size: r = p.size (2..5) \u2192 drawImage(sprite, x-s,y-s,2s,2s) where sprite is soft circle; hot core brighter.\n\nAsh draw sprite grey.\n\nNow HTML/CSS: dark page, centered canvas, slider row with labels showing values.\n\nLet me write code.\n\nPerformance: with \"lighter\" drawImage for up to ~2000 particles fine.\n\nAlso prevent ash buildup from eating memory: cap ash at 800.\n\nLet me write it.\n\nCounter text at top-left: \"Particles: N   FPS: F\".\n\nLet me code now.\n\nsurfaceY(x) function:\n\n```js\nconst groundY=430, topY=140, craterHalf=34, cx=W/2;\nconst craterL=cx-craterHalf, craterR=cx+craterHalf, craterFloor=topY+16;\nconst mLeft=(topY-groundY)/craterL; // per px\nconst mRight=(groundY-topY)/(W-craterR);\nfunction surfaceY(x){\n if(x<=0)return groundY; // slope extends to edge; if x<0 offscreen\n if(x<craterL) return groundY+mLeft*(x-0) ... wait slope from (0,groundY) to (craterL, topY): y=groundY+mLeft*x. mLeft=(topY-groundY)/craterL. \u2713\n if(x<=craterR) return craterFloor;\n if(x<=W) return topY+mRight*(x-craterR);\n return groundY; // beyond\n}\nslopeM(x): same ranges mLeft / 0 / mRight.\n```\n\nSlope gradient is steep: height 430-140=290 over craterL\u2248446-34=412 \u2192 m\u2248-0.70. Tangential accel = g*m/(1+m\u00b2) \u2248 0.15*0.7/1.49 \u2248 0.07 \u2192 slides fine.\n\nCrater floor: lava landing inside crater stays (m=0) \u2192 settles at bottom of crater. OK but lava pools at crater; fine.\n\nLanding: after flight if p.y >= surfaceY(p.x): landed = true. y=surfaceY(x). Keep tangential: keep vx but constrain.\n\nAlso during flight don't check collision below ground bottom.\n\nAsh spawn from crater: x = cx \u00b1 craterHalf*rand, y=topY+4.\n\nWind slider label shows left/right.\n\nUpdate pressure meter drawing: rectangle with border, fill proportion, glow when near full.\n\nDraw text.\n\nMain loop via requestAnimationFrame; fixed-ish dt per frame (assume 60). Could use dt scale = min(2, delta/16.67). Simpler: assume 60fps.\n\nFPS compute: frames++, every 500ms fps = frames*1000/elapsed.\n\nAlso \"at least 1000 active particles\" needs to hold: with particle amount slider to max, eruption spawns like 40/frame for 150 frames \u2192 6000 \u2192 cap MAX 5000. Performance: 5000 drawImages might be heavy but with sprite draws okay. Set MAX_LAVA=4000.\n\nAlso ambient smoke from crater between eruptions: light seeping to show pressure: spawn rate proportional to pressure^3 * small. Nice.\n\nWrite final HTML.\n\nColor palette hot (t near 1 \u2192 white/yellow; low t \u2192 dark red):\npalette[i] i=0..7 from cool to hot:\n0: (60,20,10) ... build: t=1 bright yellow (255,240,150). Sprite: radial gradient, center color bright, edge transparent-ish.\n\nDefine palette from t=0..1 (8 buckets):\nbucketColors = [ [70,15,8],[120,25,10],[170,40,12],[200,60,15],[220,90,20],[240,140,30],[255,190,70],[255,240,140] ]\n\nSprite creation: radial gradient, center color rgba(r,g,b,1) \u2192 mid rgba*0.6 \u2192 edge transparent. Size 40px.\n\nRock sprite: dark grey soft blob.\n\nAsh sprite: grey soft blob, size 64.\n\nDraw ash with globalAlpha = p.alpha, drawImage scaled by r.\n\nUI text and sliders. Slider ids: power (0.3\u20132 step .05 default 1), wind (-1\u20131 default 0.15), amount (0.2\u20132 default 1). Value displays.\n\nErupt spawn count per frame: base = Math.round(14*amount) plus power influence on velocity.\n\nAsh spawn during eruption: ~4*amount per frame; ambient: pressure>0.5 \u2192 pressure^3*2 per frame.\n\nOK let me code it.\n\nAlso draw stars? Daytime, skip.\n\nEdge case: particle below canvas if past W+50 etc. remove.\n\nRemoval: settled rocks: after settled, fade: p.alpha -= 0.002. When alpha<=0 remove. Actually keep rocks visible for a while: alpha starts at 1 after settling, fade after cooldownTimer.\n\nSimplify: temperature keeps dropping even in flight; when landed cooling doubles (contact). temp decreases: flight coolRate 0.0009 + landed 0.0025. temp <= 0.02 \u2192 rock fade: alpha decreases 0.0015 \u2192 remove at <=0.\n\nHot draw condition temp > 0.12, else rock.\n\nSettled condition: landed && |vx|<0.03 \u2192 settled=true, vx=0.\n\nSlide on landed while not settled: vx += g*m/(1+m\u00b2)*2 (2x more dramatic), friction 0.99, then y = surfaceY(x).\n\nLava in crater with m=0: vx *= 0.9 \u2192 quickly settles. Good.\n\nLet me code it.\n\nAlso add subtle background hills for aesthetics: 2 dark triangles. Optional; add cheaply.\n\nLet me generate final HTML.\n"
      }
    }
  ],
  "created": 1787805570,
  "model": "~/.cache/huggingface/hub/models--unsloth--Qwen3.8-Flash-Next-GGUF/snapshots/178b998806b7b406c311a3e6174aa99cf6304eaf/UD-Q2_K_XL/Qwen3.8-Flash-Next-UD-Q2_K_XL-00001-of-00003.gguf",
  "system_fingerprint": "b10667-213df585b",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 8435,
    "prompt_tokens": 236,
    "total_tokens": 8671,
    "prompt_tokens_details": {
      "cached_tokens": 0
    }
  },
  "id": "chatcmpl-9CZGPWCd54zvCcaT525kRz0L3d5VLbNp",
  "timings": {
    "cache_n": 0,
    "prompt_n": 236,
    "prompt_ms": 1585.248,
    "prompt_per_token_ms": 6.717152542372881,
    "prompt_per_second": 148.87260542199076,
    "predicted_n": 8435,
    "predicted_ms": 363654.76,
    "predicted_per_token_ms": 43.11770927199431,
    "predicted_per_second": 23.192326700192236
  }
}