{
  "id": "chatcmpl-1e135899d58a",
  "object": "chat.completion",
  "created": 1787806808,
  "model": "unsloth/Qwen3.8-27B-GGUF",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>Volcano Eruption \u2014 2D Physics</title>\n<style>\n  html,body{margin:0;padding:0;background:#111;color:#ddd;font-family:Segoe UI,Arial,sans-serif;display:flex;flex-direction:column;align-items:center;}\n  #wrap{width:960px;max-width:100%;}\n  canvas{width:100%;display:block;background:#000;border-radius:6px;}\n  #ui{display:flex;flex-wrap:wrap;gap:14px;background:#1c1c24;padding:10px 14px;border-radius:0 0 8px 8px;font-size:13px;}\n  .ctl{flex:1 1 240px;display:flex;flex-direction:column;gap:3px;}\n  .ctl label{display:flex;justify-content:space-between;color:#aaa;}\n  .ctl label span{color:#ffd27f;}\n  input[type=range]{width:100%;accent-color:#ff6a2a;cursor:pointer;}\n  h1{font-size:15px;margin:8px 0 4px;color:#ffb27a;letter-spacing:2px;}\n</style>\n</head>\n<body>\n<div id=\"wrap\">\n  <h1>VOLCANO ERUPTION SIMULATION</h1>\n  <canvas id=\"c\" width=\"960\" height=\"540\"></canvas>\n  <div id=\"ui\">\n    <div class=\"ctl\">\n      <label>Eruption power <span id=\"vPow\">1.0</span></label>\n      <input type=\"range\" id=\"sPow\" min=\"0.2\" max=\"3\" step=\"0.1\" value=\"1\">\n    </div>\n    <div class=\"ctl\">\n      <label>Wind (left &#8592; right) <span id=\"vWind\">15</span></label>\n      <input type=\"range\" id=\"sWind\" min=\"-100\" max=\"100\" step=\"1\" value=\"15\">\n    </div>\n    <div class=\"ctl\">\n      <label>Particle amount <span id=\"vAmt\">1500</span></label>\n      <input type=\"range\" id=\"sAmt\" min=\"300\" max=\"4000\" step=\"50\" value=\"1500\">\n    </div>\n  </div>\n</div>\n<script>\n(function(){\n\"use strict\";\nconst cv = document.getElementById('c'), ctx = cv.getContext('2d');\nconst W = cv.width, H = cv.height;\n\n/* ---------- controls ---------- */\nconst sPow = document.getElementById('sPow'), sWind = document.getElementById('sWind'), sAmt = document.getElementById('sAmt');\nconst vPow = document.getElementById('vPow'), vWind = document.getElementById('vWind'), vAmt = document.getElementById('vAmt');\nlet power = 1, wind = 15, maxP = 1500;\nsPow.oninput = ()=>{ power = +sPow.value; vPow.textContent = power.toFixed(1); };\nsWind.oninput = ()=>{ wind = +sWind.value; vWind.textContent = wind; };\nsAmt.oninput = ()=>{ maxP = +sAmt.value; vAmt.textContent = maxP; };\n\n/* ---------- terrain ---------- */\nconst cx = W/2, groundY = 470, coneH = 290, baseW = 310, craterHalf = 42, craterDepth = 30;\nconst rimY = groundY - coneH;\nconst tH = new Float32Array(W);\nfor(let x=0;x<W;x++){\n  const ad = Math.abs(x-cx);\n  if(ad < craterHalf){\n    const t = ad/craterHalf;\n    tH[x] = rimY + craterDepth*t*t;\n  } else if(ad < baseW){\n    tH[x] = rimY + (groundY-rimY)*((ad-craterHalf)/(baseW-craterHalf));\n  } else tH[x] = groundY;\n}\nfunction terrY(x){\n  let i = x|0; if(i<0)i=0; if(i>W-1)i=W-1; return tH[i];\n}\nfunction slope(x){\n  let a = Math.max(0,Math.min(W-1,(x-2)|0)), b = Math.max(0,Math.min(W-1,(x+2)|0));\n  return tH[b]-tH[a];\n}\nconst emitX = cx, emitY = rimY + craterDepth - 4;\nconst G = 420;\n\n/* ---------- color table (lava cooling) ---------- */\nconst stops = [\n  [255,244,170],[255,214,90],[255,160,40],[255,110,25],[230,70,18],[160,40,14],[90,30,18],[55,24,20]\n];\nconst NB = 32, colorTab = [];\nfor(let i=0;i<NB;i++){\n  const t = i/(NB-1)*(stops.length-1);\n  const k = Math.min(stops.length-2, t|0), f = t-k;\n  const a = stops[k], b = stops[k+1];\n  colorTab.push('rgb('+((a[0]+(b[0]-a[0])*f)|0)+','+((a[1]+(b[1]-a[1])*f)|0)+','+((a[2]+(b[2]-a[2])*f)|0)+')');\n}\n\n/* ---------- particles ---------- */\nconst lava = [], ash = [];\nfunction spawnLava(){\n  if(lava.length + ash.length >= maxP) return;\n  const a = -Math.PI/2 + (Math.random()-0.5)*1.25;\n  const s = (230 + Math.random()*270) * (0.55 + 0.45*power);\n  lava.push({ x: emitX + (Math.random()-0.5)*16, y: emitY,\n    vx: Math.cos(a)*s + (Math.random()-0.5)*30,\n    vy: Math.sin(a)*s,\n    age:0, life: 5 + Math.random()*4,\n    r: 1.5 + Math.random()*2.2, st:0 });\n}\nfunction spawnAsh(){\n  if(lava.length + ash.length >= maxP) return;\n  ash.push({ x: cx + (Math.random()-0.5)*50, y: rimY - 5,\n    vx:(Math.random()-0.5)*40, vy:-(35+Math.random()*70),\n    age:0, life: 5 + Math.random()*4, ph: Math.random()*6.28 });\n}\n\n/* ---------- eruption state machine ---------- */\nlet pressure = 0, eruptT = 0, eruptDur = 0, eruptGlow = 0;\nconst PRESSURE_RATE = 0.16;      // ~6.2s to full\nlet emitAcc = 0, ashAcc = 0;\n\n/* ---------- pre-rendered scene bits ---------- */\nconst skyGrad = ctx.createLinearGradient(0,0,0,H);\nskyGrad.addColorStop(0,'#0a0f24');\nskyGrad.addColorStop(0.6,'#1a1631');\nskyGrad.addColorStop(1,'#3a2030');\nconst mtnGrad = ctx.createLinearGradient(0,rimY,0,groundY);\nmtnGrad.addColorStop(0,'#4a4148');\nmtnGrad.addColorStop(1,'#241f24');\nconst groundGrad = ctx.createLinearGradient(0,groundY,0,H);\ngroundGrad.addColorStop(0,'#2c2428');\ngroundGrad.addColorStop(1,'#171216');\nconst meterGrad = ctx.createLinearGradient(0,0,0,1);\nmeterGrad.addColorStop(0,'#ff3322');\nmeterGrad.addColorStop(0.5,'#ffaa22');\nmeterGrad.addColorStop(1,'#55dd55');\n\nfunction drawTerrain(){\n  ctx.beginPath();\n  ctx.moveTo(0,H);\n  for(let x=0;x<W;x+=2) ctx.lineTo(x, tH[x]);\n  ctx.lineTo(W,H);\n  ctx.closePath();\n  ctx.fillStyle = mtnGrad;\n  ctx.fill();\n  // flat ground base\n  ctx.fillStyle = groundGrad;\n  ctx.fillRect(0, groundY, W, H-groundY);\n  // crater rim highlight\n  ctx.beginPath();\n  for(let d=-craterHalf; d<=craterHalf; d+=2) ctx.lineTo(cx+d, tH[(cx+d)|0]);\n  ctx.strokeStyle='rgba(150,120,110,0.35)';\n  ctx.lineWidth=2;\n  ctx.stroke();\n}\n\n/* ---------- HUD ---------- */\nfunction drawHUD(fps){\n  // pressure meter\n  const mx=20, my=50, mw=22, mh=300;\n  ctx.fillStyle='rgba(0,0,0,0.5)';\n  ctx.fillRect(mx-3,my-3,mw+6,mh+6);\n  ctx.strokeStyle='#555';\n  ctx.strokeRect(mx-3,my-3,mw+6,mh+6);\n  const ph = mh*pressure;\n  ctx.fillStyle = meterGrad;\n  ctx.save();\n  ctx.beginPath(); ctx.rect(mx, my, mw, mh); ctx.clip();\n  ctx.fillRect(mx, my+mh-ph, mw, ph);\n  ctx.restore();\n  ctx.fillStyle='#ccc';\n  ctx.font='10px monospace';\n  ctx.textAlign='center';\n  ctx.fillText('PRES', mx+mw/2, my+mh+16);\n  ctx.fillText((pressure*100|0)+'%', mx+mw/2, my-10);\n\n  // stats\n  ctx.textAlign='left';\n  ctx.font='13px monospace';\n  ctx.fillStyle='#8fd8ff';\n  ctx.fillText('particles: ' + (lava.length + ash.length), 14, 24);\n  ctx.fillText('fps: ' + fps, 14, 40);\n  // wind indicator\n  ctx.fillStyle='#aaa';\n  ctx.fillText('wind: ', 14, 56);\n  ctx.fillStyle = wind<0 ? '#8fd8ff' : '#ffb27a';\n  ctx.fillText((wind<0?'&#9664;'.replace(/&#\\d+;/g,'<') : '>') + ' ' + Math.abs(wind), 56, 56);\n  // eruption state\n  ctx.fillStyle = eruptT>0 ? '#ff8844' : '#666';\n  ctx.fillText(eruptT>0 ? '>> ERUPTING <<' : 'building pressure...', 14, 72);\n  ctx.textAlign='left';\n}\n\n/* ---------- main loop ---------- */\nlet last = performance.now(), fps = 0, fCount = 0, fTime = 0, simTime = 0;\n\nfunction frame(now){\n  let dt = (now-last)/1000; last = now;\n  if(dt > 1/30) dt = 1/30;\n  simTime += dt;\n  fCount++; fTime += dt;\n  if(fTime >= 0.5){ fps = Math.round(fCount/fTime); fCount=0; fTime=0; }\n\n  const windX = wind/100 * 100; // px/s^2\n\n  /* --- state machine --- */\n  if(eruptT > 0){\n    eruptT -= dt;\n    eruptGlow = Math.min(1, eruptGlow + dt*4);\n    // emission\n    emitAcc += 230 * power * (maxP/1500) * dt;\n    ashAcc  += 55  * (maxP/1500) * dt;\n    let n = emitAcc|0; emitAcc -= n;\n    while(n-- > 0) spawnLava();\n    n = ashAcc|0; ashAcc -= n;\n    while(n-- > 0) spawnAsh();\n    if(eruptT <= 0){\n      pressure = 0;\n      eruptDur = 2.2 + Math.random()*1.5 + power*0.9;\n    }\n  } else {\n    eruptGlow = Math.max(0, eruptGlow - dt*1.5);\n    pressure += PRESSURE_RATE * dt;\n    if(pressure >= 1){\n      pressure = 1;\n      eruptT = eruptDur;\n    }\n  }\n\n  /* --- update lava --- */\n  for(let i=lava.length-1;i>=0;i--){\n    const p = lava[i];\n    p.age += dt;\n    if(p.age >= p.life){\n      lava[i] = lava[lava.length-1]; lava.pop(); continue;\n    }\n    if(p.st === 0){ // airborne\n      p.vy += G*dt;\n      p.vx += windX*0.25*dt;\n      p.x += p.vx*dt;\n      p.y += p.vy*dt;\n      if(p.x < 2) { p.x = 2; p.vx *= -0.3; }\n      if(p.x > W-2){ p.x = W-2; p.vx *= -0.3; }\n      const gy = terrY(p.x);\n      if(p.y >= gy){\n        p.y = gy;\n        p.vy = 0;\n        p.vx *= 0.5;\n        p.st = 1;\n      }\n    } else if(p.st === 1){ // rolling downhill\n      const s = slope(p.x);\n      const dir = s > 0.02 ? 1 : (s < -0.02 ? -1 : 0);\n      p.vx += dir * 340 * Math.min(1, Math.abs(s)/2) * dt;\n      p.vx *= Math.exp(-1.6*dt);\n      if(dir === 0) p.vx *= Math.exp(-3*dt);\n      p.x += p.vx*dt;\n      if(p.x<2){p.x=2;p.vx=0;} if(p.x>W-2){p.x=W-2;p.vx=0;}\n      p.y = terrY(p.x);\n      p.age += dt*1.1; // cools faster while on ground\n      if(Math.abs(p.vx) < 6 && Math.abs(s) < 0.05) p.st = 2;\n    }\n    // settled: nothing moves\n  }\n\n  /* --- update ash --- */\n  for(let i=ash.length-1;i>=0;i--){\n    const a = ash[i];\n    a.age += dt;\n    if(a.age >= a.life){\n      ash[i] = ash[ash.length-1]; ash.pop(); continue;\n    }\n    a.vy += (-32 - a.vy)*0.9*dt;         // buoyancy toward rise speed\n    a.vx += windX*dt;                    // wind push\n    a.vx += Math.sin(simTime*1.3 + a.ph)*14*dt; // turbulence\n    a.vx *= Math.exp(-0.35*dt);\n    a.x += a.vx*dt;\n    a.y += a.vy*dt;\n    if(a.y < 4 || a.x < -60 || a.x > W+60){\n      ash[i] = ash[ash.length-1]; ash.pop();\n    }\n  }\n\n  /* ================= render ================= */\n  ctx.fillStyle = skyGrad;\n  ctx.fillRect(0,0,W,H);\n\n  // eruption tint\n  if(eruptGlow > 0){\n    ctx.fillStyle = 'rgba(255,80,20,' + (0.10*eruptGlow).toFixed(3) + ')';\n    ctx.fillRect(0,0,W,H);\n  }\n\n  drawTerrain();\n\n  // crater glow\n  if(eruptGlow > 0.02){\n    const g = ctx.createRadialGradient(emitX, emitY, 2, emitX, emitY, 90*eruptGlow+20);\n    g.addColorStop(0,'rgba(255,200,80,'+(0.9*eruptGlow)+')');\n    g.addColorStop(0.4,'rgba(255,110,30,'+(0.5*eruptGlow)+')');\n    g.addColorStop(1,'rgba(255,60,10,0)');\n    ctx.fillStyle = g;\n    ctx.beginPath(); ctx.arc(emitX, emitY, 120, 0, 6.283); ctx.fill();\n  }\n\n  // ash cloud (behind lava)\n  for(let i=0;i<ash.length;i++){\n    const a = ash[i];\n    const t = a.age/a.life;\n    const r = 5 + a.age*4.5;\n    const al = 0.38 * (t<0.15 ? t/0.15 : 1-t);\n    const shade = 60 + (1-t)*40|0;\n    ctx.fillStyle = 'rgba('+shade+','+(shade-6)+','+(shade-10)+','+al.toFixed(3)+')';\n    ctx.beginPath();\n    ctx.arc(a.x, a.y, r, 0, 6.283);\n    ctx.fill();\n  }\n\n  // glow pass for hot lava (additive)\n  ctx.globalCompositeOperation = 'lighter';\n  for(let i=0;i<lava.length;i++){\n    const p = lava[i];\n    const t = p.age/p.life;\n    if(t < 0.3){\n      ctx.fillStyle = 'rgba(255,150,40,' + (0.22*(1-t/0.3)).toFixed(3) + ')';\n      ctx.beginPath();\n      ctx.arc(p.x, p.y, p.r*3.2, 0, 6.283);\n      ctx.fill();\n    }\n  }\n  ctx.globalCompositeOperation = 'source-over';\n\n  // lava bodies\n  for(let i=0;i<lava.length;i++){\n    const p = lava[i];\n    let t = p.age/p.life; if(t>1)t=1;\n    let b = (t*(NB-1))|0; if(b>NB-1)b=NB-1;\n    let al = 1;\n    if(t > 0.82) al = 1 - (t-0.82)/0.18;\n    ctx.globalAlpha = al;\n    ctx.fillStyle = colorTab[b];\n    ctx.beginPath();\n    ctx.arc(p.x, p.y, p.r, 0, 6.283);\n    ctx.fill();\n  }\n  ctx.globalAlpha = 1;\n\n  drawHUD(fps);\n\n  requestAnimationFrame(frame);\n}\nrequestAnimationFrame(frame);\n})();\n</script>\n</body>\n</html>",
        "refusal": null,
        "reasoning_content": "Create a single HTML file with a canvas, a volcano, and a physics simulation. Main features:\n\n- Terrain: a cone with a crater. Precompute the ground height function for collision detection.\n- Pressure meter: increases over time, and erupts when full.\n- Lava particles: emitted from the crater during eruption, gravity, collision with terrain, sliding downhill (simple slope following: after landing, move in the downhill direction, friction), cool from yellow to dark red, then fade.\n- Ash particles: rise, wind drift, grow larger, fade.\n- Sliders: eruption power, wind (direction + strength \u2014 a single slider from -100 to 100), particle count (affects emission rate / max count).\n- HUD: active particle count, FPS.\n- Maintain smoothness with 1000+ particles: use a simple array, avoid per-frame object creation; precompute terrain heights into a column array for fast lookup; simple circle vs. ground line-segment collision (treat ground as a heightmap \u2014 sufficient for a cone).\n\nDesign: width 960, height 540 (or responsive). Volcano centered. Ground baseline y = H-60. Volcano: base half-width ~260, crater at the top. Height function: for |x-cx| < baseW, y = base - height * (1 - (d/baseW)^0.85) or thereabouts; crater: a flat dip near the top. Easier: define the cone as a linear slope from the base to the rim, with a slightly concave crater at the top.\n\nterrainHeight(x): return the y coordinate of the top surface. Outside the volcano: flat ground. Cone: y = groundY - coneH * (1 - d/baseW). Crater: in the central region |d| < craterHalf, dip it down: y = rimY + craterDepth*(d/craterHalf)^2.\n\nLava landing: if py >= groundY(x), place the particle at groundY, give it velocity along the slope: compute slope from finite differences, and move in the downhill direction with friction. Cooling: lifetime timer; color interpolation: yellow (255,220,50) \u2192 orange \u2192 red (200,30,10) \u2192 dark (60,20,15), then alpha fades.\n\nDraw lava as small circles; additive glow using 'lighter' compositing for hot particles. For performance with 1000+ particles, drawing an arc per particle is fine, but glow shadows should be avoided. Use two passes: dark settled rock particles in normal compositing, hot lava in 'lighter'. Actually even simpler: a single pass with fillStyle per particle; grouping by color bucket could work \u2014 bucket by cooling stage (e.g., 24 buckets) to reduce state changes. That's good: quantize color and group by bucket index.\n\nEruption behavior: pressure goes 0\u21921 over ~6 seconds (rate). When it hits 1, eruption: for a duration depending on power, emit at a high rate. The particle-count slider sets the max count cap, or emission multiplier / target. The particle-count slider scales the number of particles emitted per eruption, and the wind slider -100..100 affects the horizontal drift of ash and the in-flight horizontal wind force on particles.\n\nEmission: while erupting, spawnRate = power * amount * e.g. 8 per frame? Cap total particle count at maxParticles (from the slider, minimum 1000? The requirement is that the simulation should stay smooth with at least 1000 active \u2014 so the slider range is around 500\u20134000, default 1500).\n\nAsh: spawn at a lower rate during eruption; rise (negative vy), affected by wind, expand, fade, dark gray.\n\nFPS: moving average.\n\nPerformance: fixed array pool? Use an array with a swap-remove technique. Particles as plain objects in an array; swap-remove on death. 2000 particles is fine.\n\nDraw order: sky gradient, ash (back), volcano silhouette (dark), lava particles, ground highlight, ash in front? Easier: sky, then ash, then terrain, then lava. The terrain drawn after ash hides ash behind the volcano \u2014 acceptable. In reality, the ash should appear above the crater. Draw the terrain first, then ash, then lava. Lava behind the mountain? Particles are mainly above ground level; particles on slopes should be drawn on top of the terrain. So: sky, terrain, ash, lava. Good.\n\nGlow: at eruption start, draw the crater glow as a radial gradient (flashing). Also a background ember glow.\n\nSliding: when a particle lands, state = rolling: each frame, find the downhill direction (finite difference of the terrain), add acceleration in the downhill direction, vx += slopeForce, friction, small random jitter; stop when speed is low and slope is flat. While rolling, continue cooling, but cool faster while on the ground (multiplier). When lifetime expires, fade alpha over about 1 second, then remove.\n\nCollision details: the particle is in the air (state 0): apply gravity, wind slightly, integrate; if y >= terrainHeight(x), it lands \u2192 y = terrain, state 1 (settled/rolling), compute normal? Simply: keep horizontal velocity scaled, start rolling downhill.\n\nAlso, particles might fly off the edge of the canvas \u2014 remove when x < -50 or > W+50.\n\nPressure meter: vertical bar on the left, label \"PRESSURE\", gradient fill.\n\nEruption state machine:\n- building: pressure += rate*dt (e.g., 0.18/sec \u2192 about 5.5 seconds).\n- erupting: duration ~ 2.5\u20134 seconds * power factor; emission; ash emission; after it ends, pressure=0 \u2192 building.\n\nPower slider 0.2\u20133, default 1: affects initial launch speed, duration, and particle count per emission.\n\nParticle count slider 200\u20134000, default 1500: max active particle count (lava + ash); emission is capped.\n\nWind slider -100..100: windX = value/100 * 120 px/s^2 as a force on ash; also a small force on airborne lava.\n\nEmission velocity: from the crater center, angle within a cone of \u00b135\u00b0 upward, speed = 250 + rand*250, scaled by power.\n\nColor buckets: cooling progress t is 0..1 over lifetime (e.g., 6 sec airborne, 8 sec ground \u2192 use age with a multiplier). Define stops:\nt0: (255,240,150)\nt0.2: (255,170,40)\nt0.45: (255,90,20)\nt0.7: (150,40,15)\nt1: (60,25,20)\nQuantize into 16 buckets; group drawing: iterate buckets, beginPath, arc for each particle, fill. Per-bucket per-particle arc is fine.\n\nAlpha fade: the last 15% of lifetime, alpha = 1 - (t-0.85)/0.15. Per-bucket alpha: since the fade is continuous, either set globalAlpha per particle or include a fade bucket. Easier: set globalAlpha per particle (fast enough), group only by color bucket, fill each particle individually. 2000 fill calls is fine.\n\nActually even simpler: just draw each particle with its own fillStyle string computed from a precomputed table (strings for 32 buckets), set globalAlpha when fading. 2000 state changes \u2014 modern browsers are fine.\n\nAsh: draw as gray circles with low alpha, size grows with age. Draw with 'source-over'.\n\nTerrain: fill the path from the terrain heightmap with dark brown/gray. Add subtle crater glow during eruption.\n\nSky: gradient from dark blue to a slightly reddish near the horizon during eruption.\n\nHUD text at top-left: particles, fps, pressure %.\n\nLet me write the code.\n\nconst W=960,H=540, fixed canvas size, CSS width 100% to scale the canvas? Keep it fixed but style the canvas with max-width:100%.\n\nTerrain function:\ncx = W/2, groundY = H-70, baseW=300, coneH=300, craterHalf=45, craterDepth=28.\nFor |d|<craterHalf: rimY = groundY - coneH + craterDepth*(d/craterHalf)^2 ... wait, the crater is a dip: the rim is at the top of the cone; the crater bottom is lower. y = rimTopY + craterDepth*(d/craterHalf)^2, rimTopY = groundY - coneH? Actually, rim top = top of the cone = groundY - coneH. The crater bottom is in the center: at d=0, y = rimTopY + craterDepth. At d=craterHalf, y=rimTopY. Good.\nOtherwise, for |d|<baseW: y = groundY - coneH*(1 - |d|/baseW), maybe use exponent 0.9 to make it slightly concave: (1 - (|d|/baseW))**1.1? Use pow 0.95 for a slightly convex shape. Keep it linear for simple slope computation; a small exponent is fine.\n\nPrecompute the terrain array: tH[x] for x=0..W (px). Collision uses nearest-neighbor index. Finite differences for slope.\n\nEmission origin: the crater bottom at (cx, groundY - coneH + craterDepth).\n\nAsh: emit at the crater rim, small vy -30..-80, vx is the wind, buoyancy: vy -= 20*dt (rise), damping, vx += wind*dt, size 4\u219230, alpha 0.5\u21920 over about 6 seconds.\n\nAlso, continuous small smoke during building? Optional, skip.\n\nParticle cap: while spawning, skip if lava.length + ash.length > maxParticles.\n\nEmission during eruption: perFrame = base * power * amountFactor. amountFactor = amount/1500. base ~ 40/sec... to reach 1500 active with lifetime ~7 sec, we need a spawn rate of ~200/sec at full. Hmm. Lifetime: airborne ~2-3 sec, then ground 6 sec. Set ground life to 4 sec \u2192 total ~7 sec. Rate 150\u2013250/sec \u2192 1000\u20131700 active. Set spawnRate = 90 * power * amount/1500 per second? Default: 90/sec * 7 sec = 630 active. Not enough. Bump it up: spawnRate = 220 * power * (amount/1500). Default \u2192 220/sec. With 6-sec average lifetime \u2248 1300 active. Good. The amount slider goes up to 4000: 4000/1500*220 = 586/sec, capped by the maxParticles check.\n\nUse an accumulator: emitAcc += rate*dt; n = floor(emitAcc); emitAcc -= n.\n\nEruption duration: 2.0 + power*1.2 seconds? Plus a continuous emission.\n\nFPS: count frames every 0.5 sec.\n\nWind force on airborne lava: vx += windX*0.3*dt.\n\nRolling: downhill direction: slope = tH[x+2]-tH[x-2]; if slope>0, downhill is +x (y is downward, so increasing terrain y = downhill). accel = 250 * slopeSign * |slope|/4 clamped... simpler: g component along the slope. dir = sign(slope); vx += dir*300*dt; vx *= friction (0.985 per frame \u2192 use pow(0.4, dt) form: vx *= Math.exp(-1.5*dt)); when |slope| is small, also base friction: vx *= exp(-2*dt). y = tH[x] - 1. Stop condition: speed < 8 and |slope| < 0.05 \u2192 settled: vx=0.\n\nRolling life: age advances faster: while rolling, age += dt*1.6 (cools faster while moving), plus a ground-cool multiplier.\n\nColor table: 20 buckets with RGB interpolation between stops. Precompute a string array, plus a separate alpha handling.\n\nAdd glow: for t<0.3, draw a second larger fainter circle in 'lighter' mode. To save performance, do a glow pass only for hot particles in a single 'lighter' pass using rgba fill.\n\nAlso, crater glow during eruption: orange radial gradient.\n\nAsh wind slider: range -100..100, default 15. windX = val/100 * 90 (px/s^2). Display the value.\n\nPressure rate: 0.16/sec \u2192 6.25 sec to full.\n\nLet me write the HTML.\n\nControl panel: sliders with labels and value display, styled dark.\n\nCode:\n\n```html\n<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\"><title>Volcano</title>\n<style>...</style></head>\n<body>\n<div id=\"wrap\">\n<canvas id=\"c\" width=\"960\" height=\"540\"></canvas>\n<div id=\"ui\"> sliders </div>\n</div>\n<script>...</script>\n</body></html>\n```\n\nMain loop is requestAnimationFrame with dt clamped to 1/30.\n\nParticle objects: use an array of objects with fields: x,y,vx,vy,age,life,state (0 airborne, 1 rolling, 2 settled), r (radius). Ash is separate: x,y,vx,vy,age,life,size.\n\nRemove via swap-pop: iterate in reverse.\n\nDrawing lava: loop, compute t = age/life, bucket = min(19, t*20|0), set fillStyle to colorTab[bucket], globalAlpha is fade if t>0.85, else 1. arc.\n\nGlow pass: 'lighter', hot ones with t<0.25: fill rgba(255,140,40,0.25) circle r*3.\n\nDraw the volcano: build the path from the tH array, fill with a gradient. Also a crater rim highlight.\n\nSky: vertical linear gradient: top #0b1026 \u2192 horizon #2b1c3a; add an eruption-tinted overlay: a reddish overlay with alpha proportional to eruptGlow.\n\nEruption glow variable: rises quickly during eruption, decays.\n\nMeter: at x=18, y=60, width 22, height 300. Fill from the bottom, gradient green\u2192yellow\u2192red. Text.\n\nHUD: \"Particles: N | FPS: M\".\n\nWrite it all out. Keep it concise but complete.\n\nEdge case: particle lands off-canvas? x clamped to [1, W-2].\n\nRolling off the volcano base: once the terrain becomes flat, slope=0, friction stops it. Good \u2014 the particles come to rest on the slope.\n\nAmbient ember glow at the crater during building? Skip.\n\nAlso, slight initial horizontal spread on launch.\n\nLet me write the final code.\n\nconst tH = new Float32Array(W);\nCompute for x: dx = x - cx; ad=abs;\nif ad < craterHalf: t = ad/craterHalf; y = rimY + craterDepth*t*t;\nelse if ad < baseW: y = rimY + (groundY-rimY)*((ad-craterHalf)/(baseW-craterHalf)); // slope from rim to base\nelse y = groundY.\n\nAdd a little roughness noise? Skip, or a small deterministic jitter: + Math.sin(x*12.9898)*... skip for clean slopes.\n\nconeH: rimY = groundY - coneH, coneH = 290, baseW = 310, craterHalf = 42, craterDepth = 30. groundY = 470 (H=540). rimY = 180. Emitter is at (cx, rimY + craterDepth - 4).\n\nLaunch speed: s = (240 + Math.random()*260) * (0.7 + 0.3*power). power is 0.2..3. Angle: -90\u00b0 \u00b1 30\u00b0: a = -PI/2 + (rand-0.5)*1.2.\n\nGravity 420 px/s^2.\n\nAsh: life 5+rand*4; vy = -(40+rand*60); vx=(rand-0.5)*30; buoyancy: vy += (-25 - vy*0.15)*dt? Simpler: vy += (targetRise - vy)*0.8*dt, targetRise = -35. vx += windX*dt + small turbulence: vx += (Math.random()-0.5)*20*dt... use sin noise. size = 4 + age*4.\n\nWind force on airborne lava: vx += windX*0.25*dt.\n\nMax particle count from slider: maxP.\n\nOK, let me write it.\n",
        "tool_calls": null
      },
      "finish_reason": "stop",
      "logprobs": null
    }
  ],
  "usage": {
    "prompt_tokens": 236,
    "completion_tokens": 8387,
    "total_tokens": 8623,
    "prompt_tokens_details": {
      "cached_tokens": 0,
      "audio_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0,
      "audio_tokens": 0,
      "accepted_prediction_tokens": 0,
      "rejected_prediction_tokens": 0
    }
  },
  "system_fingerprint": null
}