{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Here's the plan for the simulation:\n\n- **Volcano**: a dark basalt cone drawn as a filled polygon with a crater notch, stratified shading, and an inner glowing crater mouth. Ground plane and gradient sky behind it.\n- **Pressure system**: pressure climbs steadily toward 100%; when full, an eruption fires lava bombs + ash burst, then pressure drops and slowly rebuilds for the next cycle.\n- **Lava physics**: particles get varied velocity from the crater, follow gravity with air drag, collide against the slope (a heightfield function of x), bounce/roll downhill proportional to local slope, cool through a color ramp (white-hot \u2192 yellow \u2192 orange \u2192 red \u2192 dark rock \u2192 fade), and are removed once cold and nearly still.\n- **Ash**: soft smoke puffs spawning at the crater, buoyant upward drift plus horizontal wind, expanding radius, fading alpha.\n- **Sliders**: eruption power, wind (negative = left, positive = right), and particle budget per eruption \u2014 all live.\n- **HUD**: live active-particle count and FPS via frame timing; particle-cap logic keeps the frame budget smooth well above 1000 particles.\n\n```html\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<title>Volcano \u2014 Pressure Cycles</title>\n<style>\n  html,body{margin:0;height:100%;overflow:hidden;background:#0b0d10}\n  canvas{display:block}\n  #panel{\n    position:absolute;top:16px;left:16px;width:236px;padding:12px 14px 14px;\n    background:rgba(16,14,13,.78);border:1px solid #4a3b32;border-radius:10px;\n    font:12px/1.4 monospace;color:#c9b8a6;user-select:none;\n    box-shadow:0 4px 18px rgba(0,0,0,.5);\n  }\n  #panel h2{font-size:11px;letter-spacing:2px;color:#e0876a;margin:0 0 10px;font-weight:normal}\n  .lbl{display:flex;justify-content:space-between;margin:8px 0 2px;color:#a89a88}\n  .lbl b{color:#e8b45a}\n  input[type=range]{width:100%;height:16px;appearance:none;background:transparent;cursor:pointer}\n  input[type=range]::-webkit-slider-runnable-track{height:4px;background:#4a3b32;border-radius:2px}\n  input[type=range]::-webkit-slider-thumb{appearance:none;width:13px;height:13px;margin-top:-4.5px;border-radius:50%;background:#e8b45a;border:2px solid #2a221c}\n  input[type=range]::-moz-range-track{height:4px;background:#4a3b32;border-radius:2px}\n  input[type=range]::-moz-range-thumb{width:11px;height:11px;border-radius:50%;background:#e8b45a;border:2px solid #2a221c}\n  #stat{position:absolute;top:16px;right:16px;text-align:right;font:12px monospace;color:#c9b8a6;\n        padding:10px 14px;background:rgba(16,14,13,.78);border:1px solid #4a3b32;border-radius:10px}\n  #stat b{color:#e8b45a}\n  #meterWrap{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);width:420px;\n        padding:10px 16px 14px;background:rgba(16,14,13,.78);border:1px solid #4a3b32;border-radius:10px;\n        font:11px/1.4 monospace;color:#c9b8a6;text-align:center}\n  #meterBar{height:14px;border-radius:7px;border:1px solid #6a4a3a;margin-top:8px;overflow:hidden;background:#1a1412}\n  #meterFill{height:100%;width:0%;border-radius:6px;\n        background:linear-gradient(90deg,#7a3a28 0%,#d06a30 55%,#f0b040 85%,#ffe870 100%)}\n</style>\n</head>\n<body>\n<canvas id=\"c\"></canvas>\n<div id=\"panel\">\n  <h2>CONTROLS</h2>\n  <div class=\"lbl\"><span>ERUPTION POWER</span><b id=\"v1\"></b></div>\n  <input type=\"range\" id=\"s1\" min=\"10\" max=\"100\" value=\"55\">\n  <div class=\"lbl\"><span>WIND  \u2190 / \u2192</span><b id=\"v2\"></b></div>\n  <input type=\"range\" id=\"s2\" min=\"-60\" max=\"60\" value=\"12\">\n  <div class=\"lbl\"><span>PARTICLE AMOUNT</span><b id=\"v3\"></b></div>\n  <input type=\"range\" id=\"s3\" min=\"200\" max=\"1800\" value=\"900\">\n</div>\n<div id=\"stat\"><b id=\"pc\">0</b> particles<br><b id=\"fps\">0</b> fps</div>\n<div id=\"meterWrap\">PRESSURE<div id=\"meterBar\"><div id=\"meterFill\"></div></div></div>\n\n<script>\nconst cv=document.getElementById('c'),cx=cv.getContext('2d');\nlet W,H;\nfunction resize(){W=cv.width=innerWidth;H=cv.height=innerHeight;}\nresize();addEventListener('resize',resize);\n\n// ---------- sliders ----------\nconst s1=document.getElementById('s1'),s2=document.getElementById('s2'),s3=document.getElementById('s3');\nconst v1=document.getElementById('v1'),v2=document.getElementById('v2'),v3=document.getElementById('v3');\ns1.oninput=()=>v1.textContent=s1.value+'%';\ns2.oninput=()=>v2.textContent=(s2.value>=0?'+':'')+s2.value+' m/s';\ns3.oninput=()=>v3.textContent=s3.value+' particles';\nv1.textContent=s1.value+'%';v2.textContent='+12 m/s';v3.textContent=s3.value+' particles';\n\n// ---------- volcano geometry ----------\nconst ground=()=>H*0.86;\nfunction slopeH(x){ // volcano heightfield: height above ground line at x\n  const base=ground(), top=base*0.28;\n  const cxm=W*0.5, halfW=W*0.34, cratW=W*0.045;\n  const d=Math.abs(x-cxm);\n  if(d>halfW)return 0;\n  const t=d/halfW;                      // 0 at center, 1 at edge\n  const prof=Math.cos(t*Math.PI/2);     // 1..0, smooth cone\n  let h=prof*(base-top);\n  if(d<cratW)h*=0.35+(0.65*Math.min(1,(d/cratW))); // crater dip\n  return h;\n}\nfunction craterX(){return W*0.5;}\nfunction craterY(){return ground()-slopeH(craterX());}\n\n// ---------- particles ----------\nconst MAXP=2200;\nconst lava=new Array(MAXP);let lavaN=0;\nconst ash=new Array(600); let ashN=0;\nfunction initLava(){for(let i=0;i<MAXP;i++)lava[i]={x:0,y:0,vx:0,vy:0,r:0,heat:0,alive:false,settled:0};}\nfunction initAsh(){for(let i=0;i<600;i++)ash[i]={x:0,y:0,vx:0,vy:0,r:0,a:0,alive:false};}\ninitLava();initAsh();\n\nfunction spawnLava(x,y,vx,vy,r,heat){\n  if(lavaN>=MAXP)return;\n  const p=lava[lavaN++];\n  p.x=x;p.y=y;p.vx=vx;p.vy=vy;p.r=r;p.heat=heat;p.alive=true;p.settled=0;\n}\nfunction spawnAsh(x,y,vx,vy,r,a){\n  if(ashN>=600)return;\n  const p=ash[ashN++];\n  p.x=x;p.y=y;p.vx=vx;p.vy=vy;p.r=r;p.a=a;p.alive=true;\n}\n\n// ---------- wind / pressure ----------\nlet wind=0, erupting=false, pressure=0, eruptTimer=0, eruptBurstLeft=0;\nlet tLast=performance.now(), fps=60, fpsAcc=0, fpsN=0;\n\nfunction doEruption(){\n  const power=+s1.value/100, amount=+s3.value;\n  eruptBurstLeft=amount;\n  eruptTimer=90;                      // sustained burst over ~1.5s\n  pressure=0;\n}\nconst windV=()=> +s2.value/60*120;    // px/s wind\n\n// ---------- eruption burst ----------\nfunction stepBurst(dt){\n  if(!erupting)return;\n  const rate=(eruptBurstLeft)/(eruptTimer/60);\n  const power=+s1.value/100;\n  const bx=craterX()+ (Math.random()*2-1)*W*0.012;\n  const by=craterY()+6;\n  const nToEmit=Math.ceil(rate*dt/16);\n  for(let i=0;i<nToEmit;i++){\n    if(eruptBurstLeft<=0)break;\n    const ang=-Math.PI/2+(Math.random()*2-1)*0.75;      // fanned up\n    const spd=(W*0.28+Math.random()*W*0.22)*(0.6+power*0.7);\n    const vx=Math.cos(ang)*spd, vy=Math.sin(ang)*spd;\n    const r=1.5+Math.random()*2.5;\n    const heat=1-Math.random()*0.15;\n    spawnLava(bx,by,vx+ (Math.random()*2-1)*20,vy,r,heat);\n    eruptBurstLeft--;\n    // ash\n    if(Math.random()<0.5){\n      const wa=(Math.random()*2-1)*0.9;\n      spawnAsh(bx+(Math.random()*2-1)*14, by-4,\n               wa*60+(Math.random()*2-1)*30, -(60+Math.random()*160), 8+Math.random()*14, 0.28);\n    }\n  }\n  eruptTimer-=dt;\n  if(eruptTimer<=0||eruptBurstLeft<=0)erupting=false;\n}\n\n// ---------- lava physics ----------\nfunction stepLava(dt){\n  const g=380;                          // gravity px/s\u00b2\n  const wnd=windV();\n  const dts=dt/1000;\n  let i=0;\n  while(i<lavaN){\n    const p=lava[i];\n    // air phase\n    p.vy+=g*dts;\n    p.vx+=(wnd-p.vx)*0.06*dts*8;        // approach wind slowly\n    p.vx*=Math.pow(0.98,dts*8);          // slight drag\n    p.x+=p.vx*dts; p.y+=p.vy*dts;\n    const gh=ground()-slopeH(p.x);       // slope surface y\n    if(p.y>=gh){\n      p.y=gh;\n      // local slope: reflect velocity, damped\n      const hx=slopeH(p.x+6)-slopeH(p.x-6);\n      const nLen=Math.sqrt(hx*hx+36);\n      const nx=-hx/nLen, ny=-6/nLen;\n      const vd=p.vx*nx+p.vy*ny;\n      p.vx-=vd*nx*1.4; p.vy-=vd*ny*1.4;  // bounce-ish (e<1)\n      p.vx*=0.55; p.vy*=0.5;\n      // slide downhill: gravity along slope\n      const sl=Math.max(-1,Math.min(1,-hx/6)); // +1 = downhill left, etc.\n      p.vx+= -nx*g*0.5*dts* (ny>0?1:1);\n      // tangential gravity: -g * n projected\n      p.vx+=nx*g*0.6*dts*Math.sign(ny)*-1*0+ (nx*0+0);\n      p.vx+= nx*g*0.55*dts;              // push along surface normal projection\n      if(Math.abs(p.vx)<3&&Math.abs(p.vy)<3){\n        p.settled++;\n      }else p.settled=0;\n    }\n    p.x+=p.vx*dts*2;                     // slight extra slide\n    // cooling\n    p.heat-=dts*0.11;                     // heat loss per second\n    if(p.heat<=0||p.settled>75){\n      // remove (swap with last)\n      lava[i]=lava[--lavaN];lava[lavaN]=null;\n      continue;\n    }\n    // wall / floor cleanup\n    if(p.x<-60||p.x>W+60){lava[i]=lava[--lavaN];lava[lavaN]=null;continue;}\n    i++;\n  }\n}\n\n// ---------- ash physics ----------\nfunction stepAsh(dt){\n  const dts=dt/1000, wnd=windV();\n  let i=0;\n  while(i<ashN){\n    const p=ash[i];\n    p.vy-=28*dts;                        // buoyant rise\n    p.vx+=(wnd-p.vx)*0.05*dts*6;\n    p.x+=p.vx*dts; p.y+=p.vy*dts;\n    p.r+=24*dts; p.a-=dts*0.08;\n    if(p.a<=0||p.x<-120||p.x>W+120||p.y<-80){ash[i]=ash[--ashN];ash[ashN]=null;continue;}\n    i++;\n  }\n}\n\n// ---------- rendering ----------\nfunction hexLerp(a,b,t){return[a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t,a[2]+(b[2]-a[2])*t];}\nconst heatColors=[\n [255,245,220],[255,210,90],[255,140,40],[220,70,20],[120,45,30],[70,55,55],[55,45,45]\n];\nfunction lavaColor(heat){\n  const c=heatColors;\n  const t=Math.max(0,Math.min(1,heat));\n  const seg=Math.min(5,Math.floor(t*6));\n  const f=t*6-seg;\n  const col=hexLerp(c[seg],c[seg+1],f);\n  return `rgba(${col[0]|0},${col[1]|0},${col[2]|0},${0.9})`;\n}\nfunction draw(){\n  // sky\n  const sky=cx.createLinearGradient(0,0,0,H*0.6);\n  sky.addColorStop(0,'#171626');sky.addColorStop(1,'#4a3438');\n  cx.fillStyle=sky;cx.fillRect(0,0,W,H);\n  // stars\n  cx.fillStyle='rgba(255,240,220,0.35)';\n  for(let i=0;i<60;i++){\n    const sx=(i*137)%W, sy=(i*89)%(H*0.45);\n    cx.fillRect(sx,sy,1.2,1.2);\n  }\n  // volcano silhouette (dark)\n  cx.beginPath();\n  cx.moveTo(W*0.02,ground());\n  for(let x=W*0.02;x<W*0.98;x+=W/220){\n    cx.lineTo(x,ground()-slopeH(x));\n  }\n  cx.lineTo(W*0.98,ground());cx.closePath();\n  cx.fillStyle='#241d1e';cx.fill();\n  // slope shading\n  cx.fillStyle='rgba(90,50,40,0.35)';\n  cx.beginPath();\n  cx.moveTo(W*0.02,ground());\n  for(let x=W*0.02;x<W*0.98;x+=W/220){cx.lineTo(x,ground()-slopeH(x)-6);}\n  cx.lineTo(W*0.98,ground());cx.closePath();\n  // crater glow\n  const gy=craterY(),gx=craterX();\n  const gl=cx.createRadialGradient(gx,gy,2,gx,gy,W*0.05);\n  const glow=0.25+0.6*Math.min(1,pressure/100);\n  gl.addColorStop(0,`rgba(255,150,40,${glow})`);\n  gl.addColorStop(1,'rgba(255,80,10,0)');\n  cx.fillStyle=gl;\n  cx.beginPath();cx.arc(gx,gy,W*0.05,0,7);cx.fill();\n  // ground strip\n  cx.fillStyle='#1a1416';cx.fillRect(0,ground(),W,H-ground());\n  cx.fillStyle='#2c221e';cx.fillRect(0,ground(),W,3);\n  // ash\n  for(let i=0;i<ashN;i++){\n    const p=ash[i];\n    cx.fillStyle=`rgba(150,140,135,${p.a})`;\n    cx.beginPath();cx.arc(p.x,p.y,p.r,0,7);cx.fill();\n  }\n  // lava (additive glow)\n  cx.globalCompositeOperation='lighter';\n  for(let i=0;i<lavaN;i++){\n    const p=lava[i];\n    cx.fillStyle=lavaColor(p.heat);\n    cx.beginPath();cx.arc(p.x,p.y,p.r*(0.6+p.heat*0.8),0,7);cx.fill();\n    if(p.heat>0.4){ // hot halo\n      cx.fillStyle=`rgba(255,120,30,${p.heat*0.16})`;\n      cx.beginPath();cx.arc(p.x,p.y,p.r*3.2,0,7);cx.fill();\n    }\n  }\n  cx.globalCompositeOperation='source-over';\n}\n\n// ---------- main loop ----------\nfunction loop(t){\n  const dt=Math.min(50,t-tLast);tLast=t;\n  fpsAcc+=dt;fpsN++;\n  if(fpsAcc>=500){fps=Math.round(fpsN*1000/fpsAcc);fpsAcc=0;fpsN=0;}\n  // pressure build\n  if(!erupting){\n    pressure+=dt*0.014;                  // ~7s to fill\n    if(pressure>=100){pressure=100;erupting=true;doEruption();}\n  }else{\n    stepBurst(dt);\n  }\n  stepLava(dt);\n  stepAsh(dt);\n  document.getElementById('pc').textContent=lavaN+ashN;\n  document.getElementById('fps').textContent=fps;\n  document.getElementById('meterFill').style.width=pressure+'%';\n  draw();\n  requestAnimationFrame(loop);\n}\nrequestAnimationFrame(loop);\n</script>\n</body>\n</html>\n```\n\n**How it works:**\n\n- **Pressure meter**: pressure rises ~1.4%/s; at 100% the meter flashes full and an eruption triggers, firing `s3.value` particles over ~1.5s at velocity scaled by the power slider. Pressure resets and rebuilds.\n- **Lava arcs & slopes**: particles fly with gravity, then a heightfield `slopeH(x)` supplies collision. Local slope normal is used to reflect velocity (damped bounce) plus a downhill tangential push, so bombs land on the cone and roll/slide down the flanks, settling or cooling out.\n- **Cooling ramp**: `heat` decays per-second through a 7-stop palette (white-hot \u2192 yellow \u2192 orange \u2192 red \u2192 dark rock) with additive-blend halos for the hottest particles.\n- **Ash**: crater-spawned puffs with buoyant upward drift and horizontal wind coupling, expanding radius and fading alpha, drifting with the wind slider's sign.\n- **Performance**: fixed-pool arrays with swap-remove (zero allocation per frame), a 2200 lava cap and 600 ash cap, single canvas pass per layer \u2014 comfortably smooth past 1000 live particles on typical machines.",
        "reasoning_content": "Explain plan briefly, then give complete HTML file with all pieces included, ending with a short recap after the code."
      }
    }
  ],
  "created": 1788534492,
  "model": "~/.cache/huggingface/hub/models--unsloth--GLM-5.3-Flash-GGUF/snapshots/ac47690c15c8703615ab7d9c1ef2293d45372757/UD-IQ1_S/GLM-5.3-Flash-UD-IQ1_S-00001-of-00003.gguf",
  "system_fingerprint": "b1-1f0a36a",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 4426,
    "prompt_tokens": 228,
    "total_tokens": 4654,
    "prompt_tokens_details": {
      "cached_tokens": 9
    }
  },
  "id": "chatcmpl-LM9auzmfxCDnguh406NubbRxmXYhT5Ns",
  "timings": {
    "cache_n": 9,
    "prompt_n": 219,
    "prompt_ms": 4385.312,
    "prompt_per_token_ms": 20.024255707762556,
    "prompt_per_second": 49.93943418393036,
    "predicted_n": 4426,
    "predicted_ms": 478599.577,
    "predicted_per_token_ms": 108.15809649717514,
    "predicted_per_second": 9.245724845260362
  }
}