| <!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'; |
| |
| 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; |
| |
| |
| 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; |
| } |
| |
| |
| 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); |
| |
| |
| 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}); |
| } |
| } |
| |
| |
| 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)}; |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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> |
|
|