Toybox / nuclear_explosion.html
zxbc2023's picture
init: gallery + nuclear_explosion
6cc9688 verified
Raw
History Blame Contribute Delete
18.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Operation Daybreak — Nuclear Explosion</title>
<style>
html,body{margin:0;height:100%;background:#000;overflow:hidden}
canvas{display:block}
</style>
</head>
<body>
<canvas id="scene"></canvas>
<script>
'use strict';
/* ================= helpers ================= */
const clamp=(v,a=0,b=1)=>Math.max(a,Math.min(b,v));
const lerp=(a,b,f)=>a+(b-a)*f;
const smoothstep=(a,b,x)=>{x=clamp((x-a)/(b-a));return x*x*(3-2*x);};
const easeOutCubic=x=>1-Math.pow(1-x,3);
const easeInQuad=x=>x*x;
const TAU=Math.PI*2;
const cv=document.getElementById('scene');
const ctx=cv.getContext('2d');
let W=0,H=0,DPR=1,groundY=0,cx=0,Rm=0;
let skyCv,farCv,vigCv,groundGrad;
let bldgs=[],stars=[],clouds=[];
const T_PRE=-2.3, T_LOOP=40, T_END=37.5;
let T=T_PRE;
let debrisSpawned=0;
/* ================= pre-rendered sprites (perf: draw, never re-gradient) ================= */
function radialSprite(size,stops){
const c=document.createElement('canvas'); c.width=c.height=size;
const g=c.getContext('2d');
const gr=g.createRadialGradient(size/2,size/2,0,size/2,size/2,size/2);
for(const [o,col] of stops) gr.addColorStop(o,col);
g.fillStyle=gr; g.fillRect(0,0,size,size);
return c;
}
const SPR={
white :radialSprite(256,[[0,'rgba(255,255,255,1)'],[0.3,'rgba(255,246,235,0.85)'],[0.62,'rgba(255,196,130,0.28)'],[1,'rgba(255,150,80,0)']]),
glowW :radialSprite(256,[[0,'rgba(255,252,244,1)'],[0.35,'rgba(255,226,190,0.5)'],[1,'rgba(255,190,140,0)']]),
orange:radialSprite(256,[[0,'rgba(255,176,88,0.95)'],[0.5,'rgba(255,112,52,0.38)'],[1,'rgba(255,80,40,0)']]),
red :radialSprite(256,[[0,'rgba(255,96,52,0.85)'],[1,'rgba(210,44,30,0)']]),
smoke :radialSprite(256,[[0,'rgba(196,200,212,0.5)'],[0.65,'rgba(152,158,172,0.24)'],[1,'rgba(120,126,142,0)']]),
dark :radialSprite(256,[[0,'rgba(96,101,116,0.55)'],[0.65,'rgba(74,78,92,0.26)'],[1,'rgba(60,64,78,0)']]),
};
function puff(spr,x,y,s,a){
if(a<=0.003||s<=0) return;
ctx.globalAlpha=clamp(a);
ctx.drawImage(spr,x-s/2,y-s/2,s,s);
ctx.globalAlpha=1;
}
/* stable noise arrays so cloud shapes don't jitter */
const capNoise=[]; for(let i=0;i<24;i++) capNoise.push(0.78+Math.random()*0.5);
const stemNoise=[]; for(let i=0;i<14;i++) stemNoise.push(Math.random()-0.5);
const rimNoise=[]; for(let i=0;i<8;i++) rimNoise.push(0.5+Math.random()*0.7);
/* ================= static scene builders (rebuild on resize) ================= */
function buildSky(){
skyCv=document.createElement('canvas');
skyCv.width=Math.round(W*DPR); skyCv.height=Math.round(H*DPR);
const g=skyCv.getContext('2d'); g.scale(DPR,DPR);
const gr=g.createLinearGradient(0,0,0,H);
gr.addColorStop(0,'#04060f');
gr.addColorStop(0.42,'#0a1128');
gr.addColorStop(0.72,'#141b3a');
gr.addColorStop(0.9,'#2a2440');
gr.addColorStop(1,'#46303c');
g.fillStyle=gr; g.fillRect(0,0,W,H);
}
function buildFar(){
farCv=document.createElement('canvas');
farCv.width=Math.round(W*DPR); farCv.height=Math.round(H*DPR);
const g=farCv.getContext('2d'); g.scale(DPR,DPR);
let x=-10;
while(x<W+10){
const w=20+Math.random()*60;
const h=(0.06+Math.random()*0.24)*H;
g.fillStyle='#141b30';
g.fillRect(x,groundY-h,w,h);
if(Math.random()<0.3){ g.fillStyle='rgba(140,160,210,0.10)'; g.fillRect(x+w*0.4,groundY-h-6,2,6); }
x+=w+Math.random()*14;
}
const hz=g.createLinearGradient(0,groundY-70,0,groundY);
hz.addColorStop(0,'rgba(58,42,52,0)'); hz.addColorStop(1,'rgba(58,42,52,0.5)');
g.fillStyle=hz; g.fillRect(0,groundY-70,W,70);
}
function buildGroundGrad(){
groundGrad=ctx.createLinearGradient(0,groundY,0,H);
groundGrad.addColorStop(0,'#05060a');
groundGrad.addColorStop(1,'#020308');
}
function buildVig(){
vigCv=document.createElement('canvas');
vigCv.width=Math.round(W*DPR); vigCv.height=Math.round(H*DPR);
const g=vigCv.getContext('2d'); g.scale(DPR,DPR);
const gr=g.createRadialGradient(W/2,H*0.45,Math.min(W,H)*0.35,W/2,H*0.5,Math.max(W,H)*0.78);
gr.addColorStop(0,'rgba(0,0,0,0)');
gr.addColorStop(1,'rgba(0,0,0,0.55)');
g.fillStyle=gr; g.fillRect(0,0,W,H);
}
function genStars(){
stars=[];
for(let i=0;i<150;i++){
stars.push({x:Math.random()*W,y:Math.random()*(groundY-50),r:0.6+Math.random(),
ph:Math.random()*TAU,sp:0.5+Math.random()*2,base:0.25+Math.random()*0.5});
}
}
function genClouds(){
clouds=[];
for(let i=0;i<3;i++){
clouds.push({x0:Math.random()*W,y:H*(0.12+Math.random()*0.2),s:160+Math.random()*220,sp:5+Math.random()*7});
}
}
const WIN=['rgba(255,196,120,','rgba(255,222,170,','rgba(168,206,255,'];
function makeBuilding(w,h){
const c=document.createElement('canvas');
c.width=Math.max(2,Math.round(w*DPR)); c.height=Math.max(2,Math.round((h+18)*DPR));
const g=c.getContext('2d'); g.scale(DPR,DPR);
const gr=g.createLinearGradient(0,0,w,0);
gr.addColorStop(0,'#161c30'); gr.addColorStop(0.5,'#0f1424'); gr.addColorStop(1,'#0a0e1b');
g.fillStyle=gr; g.fillRect(0,18,w,h);
g.fillStyle='rgba(130,150,200,0.14)'; g.fillRect(0,18,w,1.5);
const cols=Math.max(1,Math.floor((w-6)/9));
const rows=Math.max(1,Math.floor((h-8)/11));
for(let r=0;r<rows;r++) for(let col=0;col<cols;col++){
if(Math.random()<0.30){
g.fillStyle=WIN[(Math.random()*WIN.length)|0]+(0.35+Math.random()*0.5)+')';
g.fillRect(4+col*9,23+r*11,4,6);
}
}
if(Math.random()<0.3){
g.fillStyle='#232a40'; g.fillRect(w/2-1,4,2,14);
g.fillStyle='rgba(255,80,70,0.8)'; g.fillRect(w/2-1.5,1,3,3);
}
return c;
}
function genCity(){
bldgs=[];
const n=Math.max(8,Math.round(W/52));
const sp=W/n;
for(let i=0;i<n;i++){
const w=sp*(0.55+Math.random()*0.45);
let h=(0.09+Math.random()*0.20)*H;
if(Math.random()<0.15) h*=1.6;
const x=i*sp+(Math.random()-0.5)*sp*0.25;
bldgs.push({x,w,h,spr:makeBuilding(w,h),destroyed:false,tD:0,rub:false});
}
}
/* ================= explosion geometry ================= */
function liftAmt(t){
if(t<1) return Rm*0.2;
const k=easeOutCubic(clamp((t-1)/13));
return Rm*(0.2+1.15*k)+(t>13?(t-13)*4:0);
}
function windDrift(t){ return 0.045*H*smoothstep(10,32,t); }
function shockR(t){ if(t<0.4) return 0; return 0.62*W*Math.pow(clamp((t-0.4)/7),0.8); }
function fireA(t){ if(t<0.02||t>11) return 0; return 1-smoothstep(6.5,11,t); }
function fireR(t){ return Rm*Math.sqrt(clamp(t/6.5)); }
function capGeom(t){
const R=Rm*(0.5+0.4*easeOutCubic(clamp((t-3)/9)));
return {R, x:cx+windDrift(t)*1.6, y:groundY-liftAmt(t)};
}
/* ================= particles ================= */
let smoke=[],debris=[],embers=[],rubble=[];
function spawnDebris(){
const a=Math.random()*TAU, sp=220+Math.random()*720;
let vx=Math.cos(a)*sp, vy=Math.sin(a)*sp;
if(vy>0) vy*=0.3;
debris.push({x:cx,y:groundY-10,vx,vy,sz:1.5+Math.random()*3.5,age:0,life:5});
}
function spawnEmber(){
const c=capGeom(T), r=Math.max(fireR(T),Rm*0.35);
embers.push({x:c.x+(Math.random()-0.5)*r*1.3, y:c.y+(Math.random()-0.5)*r*1.2,
vx:10+Math.random()*30, vy:-(25+Math.random()*70), sz:5+Math.random()*9, age:0, life:2.5+Math.random()*3});
}
function spawnPlume(){
const c=capGeom(T);
smoke.push({x:c.x+(Math.random()-0.5)*c.R*1.2, y:c.y-c.R*0.5,
vx:18+Math.random()*14, vy:-(10+Math.random()*22),
r:c.R*(0.15+Math.random()*0.15), age:0, life:9+Math.random()*5,
kind:Math.random()<0.5?'smoke':'dark'});
}
function spawnRubble(b){
for(let i=0;i<8;i++){
rubble.push({x:b.x+b.w/2+(Math.random()-0.5)*b.w, y:groundY-b.h*0.5,
vx:(Math.random()-0.5)*120, vy:-(20+Math.random()*80),
rot:Math.random()*TAU, vr:(Math.random()-0.5)*6,
sz:2+Math.random()*4, age:0, life:2.2+Math.random()});
}
}
function update(dt){
const t=T;
if(t>0&&t<0.8){
const target=Math.floor(230*clamp(t/0.8));
while(debrisSpawned<target&&debris.length<240){ spawnDebris(); debrisSpawned++; }
}
if(t>0.8&&t<20&&embers.length<150&&Math.random()<0.5) spawnEmber();
if(t>4&&t<30&&smoke.length<140&&Math.random()<0.35) spawnPlume();
for(let i=smoke.length-1;i>=0;i--){
const p=smoke[i]; p.age+=dt;
if(p.age>=p.life){ smoke.splice(i,1); continue; }
p.x+=(p.vx+windDrift(t)*0.6)*dt; p.y+=p.vy*dt; p.r+=p.r*0.22*dt;
}
for(let i=debris.length-1;i>=0;i--){
const p=debris[i]; p.age+=dt; p.vy+=520*dt; p.x+=p.vx*dt; p.y+=p.vy*dt;
if(p.y>groundY+2||p.age>p.life) debris.splice(i,1);
}
for(let i=embers.length-1;i>=0;i--){
const p=embers[i]; p.age+=dt;
p.x+=(p.vx+windDrift(t))*dt; p.y+=p.vy*dt;
if(p.age>=p.life) embers.splice(i,1);
}
for(let i=rubble.length-1;i>=0;i--){
const p=rubble[i]; p.age+=dt; p.vy+=480*dt; p.x+=p.vx*dt; p.y+=p.vy*dt; p.rot+=p.vr*dt;
if(p.y>groundY){ p.y=groundY; p.vy=0; p.vx*=0.5; }
if(p.age>=p.life) rubble.splice(i,1);
}
}
/* ================= drawing ================= */
function drawStars(t){
for(const s of stars){
const a=s.base*(0.55+0.45*Math.sin(t*s.sp+s.ph));
if(a<=0.02) continue;
ctx.fillStyle='rgba(210,220,255,'+a.toFixed(3)+')';
ctx.fillRect(s.x,s.y,s.r,s.r);
}
}
function drawMoon(){
const mx=W*0.8, my=H*0.16, mr=Math.max(10,H*0.022);
ctx.globalCompositeOperation='lighter';
puff(SPR.glowW,mx,my,mr*7,0.30);
ctx.globalCompositeOperation='source-over';
ctx.fillStyle='rgba(214,220,235,0.92)';
ctx.beginPath(); ctx.arc(mx,my,mr,0,TAU); ctx.fill();
ctx.fillStyle='rgba(150,158,180,0.5)';
ctx.beginPath(); ctx.arc(mx-mr*0.3,my-mr*0.15,mr*0.22,0,TAU); ctx.fill();
ctx.beginPath(); ctx.arc(mx+mr*0.25,my+mr*0.3,mr*0.16,0,TAU); ctx.fill();
}
function drawClouds(t){
for(const c of clouds){
const x=((c.x0+t*c.sp)%(W+500))-250;
puff(SPR.dark,x,c.y,c.s,0.09);
}
}
function drawStreak(t){
if(t<T_PRE||t>0.05) return;
const p=clamp((t-T_PRE)/(0.15-T_PRE));
const x0=cx-0.55*W, y0=-0.15*H;
const x=lerp(x0,cx,p), y=lerp(y0,groundY,p);
ctx.globalCompositeOperation='lighter';
ctx.strokeStyle='rgba(255,240,220,0.85)';
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(lerp(x0,cx,p*0.7),lerp(y0,groundY,p*0.7));
ctx.lineTo(x,y);
ctx.stroke();
puff(SPR.glowW,x,y,30,0.9);
ctx.globalCompositeOperation='source-over';
puff(SPR.smoke,x,y,40,0.12);
}
function drawBuildings(t){
const s=shockR(t);
ctx.save();
ctx.beginPath(); ctx.rect(-4,0,W+8,groundY+1); ctx.clip();
for(const b of bldgs){
if(!b.destroyed&&s>0&&s>Math.abs(b.x+b.w/2-cx)){ b.destroyed=true; b.tD=t; }
const pv=b.destroyed?clamp((t-b.tD)/1.5):0;
const hh=b.h*(1-0.97*easeInQuad(pv));
if(hh<3) continue;
ctx.save();
ctx.beginPath(); ctx.rect(b.x-1,groundY-hh,b.w+2,hh+2); ctx.clip();
ctx.drawImage(b.spr,b.x,groundY-b.h-18,b.w,b.h+18);
ctx.restore();
if(pv>0.15&&!b.rub){ b.rub=true; spawnRubble(b); }
}
ctx.restore();
ctx.globalCompositeOperation='lighter';
for(const b of bldgs){
if(!b.destroyed) continue;
const fa=Math.exp(-(t-b.tD)/2.5);
if(fa>0.02) puff(SPR.orange,b.x+b.w/2,groundY-2,b.w*1.7,0.35*fa);
}
ctx.globalCompositeOperation='source-over';
}
function drawGround(t){
ctx.fillStyle=groundGrad;
ctx.fillRect(0,groundY,W,H-groundY);
ctx.fillStyle='rgba(255,255,255,0.05)';
ctx.fillRect(0,groundY,W,1);
if(t>0){
ctx.globalCompositeOperation='lighter';
const la=0.22*Math.exp(-t/6)+0.10*smoothstep(0,3,t)*Math.exp(-(t-3)/14);
ctx.strokeStyle='rgba(255,170,120,'+la.toFixed(3)+')';
ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(0,groundY); ctx.lineTo(W,groundY); ctx.stroke();
ctx.globalCompositeOperation='source-over';
}
}
function drawShock(t){
const s=shockR(t);
if(s<=0||t>11) return;
const a=Math.exp(-(t-0.4)/2.6);
const n=22;
for(let i=0;i<n;i++){
const ang=i/n*TAU;
const px=cx+Math.cos(ang)*s;
const py=groundY-Math.abs(Math.sin(ang))*s*0.16;
puff(SPR.dark,px,py,Math.max(14,s*0.12),0.22*a);
}
ctx.globalCompositeOperation='lighter';
ctx.strokeStyle='rgba(255,190,130,'+(0.5*a).toFixed(3)+')';
ctx.lineWidth=3;
ctx.beginPath(); ctx.ellipse(cx,groundY,s,s*0.16,0,Math.PI,TAU); ctx.stroke();
ctx.strokeStyle='rgba(255,210,170,'+(0.10*a).toFixed(3)+')';
ctx.lineWidth=10;
ctx.beginPath(); ctx.arc(cx,groundY-60,s*1.85,0,TAU); ctx.stroke();
ctx.globalCompositeOperation='source-over';
}
function drawStem(t){
if(t<2.2) return;
const a=clamp((t-2.2)/1.5);
const c=capGeom(t);
const topY=c.y+c.R*0.55;
const n=14;
for(let i=0;i<n;i++){
const f=i/(n-1);
const y=lerp(groundY-4,topY,f);
const wdt=lerp(Rm*0.38,Rm*0.20,f);
const x=cx+windDrift(t)*f*f*1.8+stemNoise[i]*wdt*0.4;
puff(SPR.smoke,x,y,wdt*2.3,0.42*a);
}
const fa=fireA(t);
if(fa>0){
ctx.globalCompositeOperation='lighter';
puff(SPR.orange,cx,groundY-8,Rm*1.5,0.45*fa);
ctx.globalCompositeOperation='source-over';
}
}
function drawFireball(t){
const fa=fireA(t); if(fa<=0) return;
const c=capGeom(t);
const r=fireR(t);
const x=c.x, y=c.y+c.R*0.12;
ctx.globalCompositeOperation='lighter';
puff(SPR.white,x,y,r*3.2,0.85*fa);
puff(SPR.glowW,x,y,r*2.1,1.0*fa);
puff(SPR.orange,x,y,r*4.0,0.6*fa);
puff(SPR.red,x,y,r*5.0,0.35*fa);
for(let i=0;i<8;i++){
const ang=i/8*TAU+t*0.35;
const rr=r*(0.8+0.35*rimNoise[i]);
puff(SPR.white,x+Math.cos(ang)*rr,y+Math.sin(ang)*rr*0.85,r*0.9,0.45*fa*rimNoise[i]);
}
ctx.globalCompositeOperation='source-over';
}
function drawCap(t){
if(t<2.6) return;
const a=clamp((t-2.6)/2);
const c=capGeom(t);
puff(SPR.dark,c.x,c.y,c.R*2.7,0.8*a);
for(let i=0;i<24;i++){
const ang=i/24*TAU;
const rr=c.R*capNoise[i];
const px=c.x+Math.cos(ang)*rr;
const py=c.y+Math.sin(ang)*rr*0.82;
puff(SPR.smoke,px,py,c.R*(0.62+0.3*capNoise[i]),0.5*a);
}
const fa=fireA(t);
if(fa>0) puff(SPR.orange,c.x,c.y+c.R*0.5,c.R*1.9,0.5*fa*a);
}
function drawCap2(t){
if(t<11) return;
const k=easeOutCubic(clamp((t-11)/10));
const R2=Rm*0.8*k;
if(R2<=1) return;
const y2=groundY-(liftAmt(t)*1.62+Rm*0.5);
const x2=cx+windDrift(t)*2.6;
puff(SPR.dark,x2,y2,R2*2.5,0.5);
for(let i=0;i<12;i++){
const ang=i/12*TAU;
puff(SPR.dark,x2+Math.cos(ang)*R2*capNoise[i],y2+Math.sin(ang)*R2*capNoise[i]*0.8,R2*0.7,0.35);
}
}
function drawSmoke(){
for(const p of smoke){
puff(p.kind==='dark'?SPR.dark:SPR.smoke,p.x,p.y,p.r*2,Math.pow(1-p.age/p.life,1.4)*0.5);
}
}
function drawEmbers(){
ctx.globalCompositeOperation='lighter';
for(const p of embers){
puff(SPR.glowW,p.x,p.y,p.sz,0.8*(1-p.age/p.life));
}
ctx.globalCompositeOperation='source-over';
}
function drawDebris(){
for(const p of debris){
const k=p.age/p.life;
ctx.globalAlpha=clamp(1-Math.max(0,k-0.7)/0.3);
ctx.fillStyle='#241d15';
ctx.fillRect(p.x,p.y,p.sz,p.sz);
}
ctx.globalAlpha=1;
const fa=fireA(T);
if(fa>0){
const c=capGeom(T), r=fireR(T)*1.2, fy=c.y+c.R*0.12;
ctx.globalCompositeOperation='lighter';
for(const p of debris){
const dx=p.x-c.x, dy=p.y-fy;
if(dx*dx+dy*dy<r*r){
ctx.fillStyle='rgba(255,170,90,'+(0.7*fa).toFixed(3)+')';
ctx.fillRect(p.x-1,p.y-1,p.sz+2,p.sz+2);
}
}
ctx.globalCompositeOperation='source-over';
}
}
function drawRubble(){
for(const p of rubble){
ctx.save();
ctx.globalAlpha=clamp(1-p.age/p.life);
ctx.translate(p.x,p.y);
ctx.rotate(p.rot);
ctx.fillStyle='rgba(38,42,54,0.9)';
ctx.fillRect(-p.sz/2,-p.sz/2,p.sz,p.sz*0.7);
ctx.restore();
}
}
function drawHorizonGlow(t){
if(t<0.1) return;
const a=0.12+0.30*Math.exp(-Math.max(0,t-1.5)/9);
ctx.globalCompositeOperation='lighter';
puff(SPR.orange,cx,groundY+6,2.3*Math.max(W,H),a);
ctx.globalCompositeOperation='source-over';
}
function drawFlash(t){
let a=0;
if(t>=0){ if(t<0.1) a=t/0.1; else a=Math.exp(-(t-0.1)/0.8); }
a*=0.97;
if(a>0.004){
ctx.fillStyle='rgba(255,255,255,'+a.toFixed(3)+')';
ctx.fillRect(0,0,W,H);
}
}
function tc(t){
const s=Math.abs(t);
const mm=String(Math.floor(s/60)).padStart(2,'0');
const ss=(s%60).toFixed(1).padStart(4,'0');
return (t<0?'T-':'T+')+mm+':'+ss;
}
function drawUI(t){
const barH=Math.max(44,Math.round(H*0.07));
ctx.fillStyle='rgba(0,0,0,0.92)';
ctx.fillRect(0,0,W,barH);
ctx.fillRect(0,H-barH,W,barH);
ctx.textBaseline='middle';
ctx.font='12px ui-monospace,Menlo,Consolas,monospace';
ctx.textAlign='left';
ctx.fillStyle='rgba(190,196,210,0.8)';
ctx.fillText('OPERATION DAYBREAK \u00B7 TEST SITE 09',16,barH/2);
const tcode=tc(t);
ctx.textAlign='right';
ctx.fillText(tcode,W-16,barH/2);
if(Math.sin(t*TAU)>0){
ctx.fillStyle='#ff4b3e';
ctx.beginPath();
ctx.arc(W-16-ctx.measureText(tcode).width-14,barH/2,3,0,TAU);
ctx.fill();
}
ctx.textAlign='center';
ctx.font='10px ui-monospace,Menlo,Consolas,monospace';
ctx.fillStyle='rgba(150,156,170,0.4)';
ctx.fillText('ARCHIVE RECONSTRUCTION \u00B7 SIMULATED EVENT',W/2,H-barH/2);
}
function render(){
const t=T;
ctx.setTransform(DPR,0,0,DPR,0,0);
ctx.drawImage(skyCv,0,0,W,H);
drawStars(t);
drawMoon();
drawClouds(t);
let sh=0;
if(t>0){ sh=16*Math.exp(-t*1.1); if(t>6) sh=Math.min(sh,1.2); }
const ox=(Math.random()-0.5)*2*sh, oy=(Math.random()-0.5)*2*sh*0.6;
ctx.save();
ctx.translate(ox,oy);
ctx.drawImage(farCv,0,0,W,H);
drawBuildings(t);
drawGround(t);
drawStreak(t);
drawShock(t);
ctx.save();
ctx.beginPath(); ctx.rect(-20,-20,W+40,groundY+23); ctx.clip();
drawStem(t);
drawFireball(t);
drawCap(t);
drawCap2(t);
ctx.restore();
drawSmoke();
drawEmbers();
drawDebris();
drawRubble();
drawHorizonGlow(t);
ctx.restore();
drawFlash(t);
const fin=1-smoothstep(T_PRE,T_PRE+1.2,T);
const fout=smoothstep(T_END,T_LOOP-0.4,T);
const f=Math.max(fin,fout);
if(f>0.004){ ctx.fillStyle='rgba(0,0,0,'+f.toFixed(3)+')'; ctx.fillRect(0,0,W,H); }
ctx.drawImage(vigCv,0,0,W,H);
drawUI(t);
}
function resetSim(){
T=T_PRE;
smoke.length=0; debris.length=0; embers.length=0; rubble.length=0;
debrisSpawned=0;
for(const b of bldgs){ b.destroyed=false; b.tD=0; b.rub=false; }
}
function resize(){
DPR=Math.min(window.devicePixelRatio||1,1.5);
W=window.innerWidth; H=window.innerHeight;
cv.width=Math.round(W*DPR); cv.height=Math.round(H*DPR);
cv.style.width=W+'px'; cv.style.height=H+'px';
groundY=Math.round(H*0.78); cx=Math.round(W*0.5); Rm=Math.min(W,H)*0.26;
buildSky(); buildFar(); buildGroundGrad(); buildVig(); genStars(); genClouds(); genCity();
}
window.addEventListener('resize',resize);
resize();
let last=performance.now();
function frame(now){
const dt=Math.min(0.05,(now-last)/1000); last=now;
T+=dt;
if(T>=T_LOOP) resetSim();
update(dt);
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>