import { createCanvas } from '@napi-rs/canvas'; import installer from '@ffmpeg-installer/ffmpeg'; import { spawn } from 'node:child_process'; import { mkdir, writeFile } from 'node:fs/promises'; import { once } from 'node:events'; const ffmpeg=installer.path; const W=960,H=960,FPS=24,SECONDS=8; await mkdir('public/videos',{recursive:true}); const canvas=createCanvas(W,H), c=canvas.getContext('2d'); function gradient(top,bottom) {const g=c.createLinearGradient(0,0,0,H);g.addColorStop(0,top);g.addColorStop(1,bottom);c.fillStyle=g;c.fillRect(0,0,W,H);} function hill(y,color,phase,height){c.beginPath();c.moveTo(0,H);for(let x=0;x<=W;x+=4){c.lineTo(x,y+Math.sin(x/190+phase)*height+Math.cos(x/77+phase)*height*.22);}c.lineTo(W,H);c.fillStyle=color;c.fill();} function draw(scene,t){ if(scene==='afterglow'){ gradient('#273d65','#f9b990');const glow=c.createRadialGradient(560,335,10,560,335,370);glow.addColorStop(0,'#ffd9a4aa');glow.addColorStop(1,'#ffd9a400');c.fillStyle=glow;c.fillRect(0,0,W,H); c.fillStyle='#ffe1ad';c.beginPath();c.arc(560,338+Math.sin(t)*3,68,0,Math.PI*2);c.fill(); for(let i=0;i<7;i++)hill(490+i*66,['#b798aa','#947f9d','#706d8e','#52617a','#3c5267','#2c4255','#1b303e'][i],i*.8+Math.sin(t)*.025,42+i*10); c.strokeStyle='#262f49';c.lineWidth=2;for(let i=0;i<6;i++){let x=130+i*25+Math.sin(t)*28,y=300+Math.sin(i)*14;c.beginPath();c.moveTo(x-6,y);c.quadraticCurveTo(x,y-6,x+6,y);c.quadraticCurveTo(x+12,y-5,x+18,y);c.stroke();} } else if(scene==='ocean'){ gradient('#456f8a','#f5d4b5');c.fillStyle='#f9e2bf';c.beginPath();c.arc(660,260,44,0,7);c.fill(); const sea=c.createLinearGradient(0,400,0,H);sea.addColorStop(0,'#438597');sea.addColorStop(1,'#092e44');c.fillStyle=sea;c.fillRect(0,400,W,560); for(let row=0;row<75;row++){let y=405+row*7.7;c.beginPath();for(let x=0;x<=W;x+=6)c.lineTo(x,y+Math.sin(x/(60-row*.5)+t*2+row*.55)*(2+row*.10));c.strokeStyle=`rgba(174,229,221,${.12+row/280})`;c.lineWidth=1+row/60;c.stroke();} for(let i=0;i<90;i++){let y=410+i*5,x=660+Math.sin(i*4+t)*i*.7;c.fillStyle=`rgba(255,219,158,${.28-i*.002})`;c.fillRect(x,y,10+i*.8,2);} hill(860,'#172e3c',2,60);hill(930,'#10232e',4,65); } else { gradient('#0c112a','#4f315d');c.fillStyle='#d9c9ff';c.beginPath();c.arc(700,210,48,0,7);c.fill(); for(let i=0;i<90;i++){let x=(i*137.3)%W,y=(i*51.7)%450;c.globalAlpha=.3+.5*Math.abs(Math.sin(t+i));c.fillRect(x,y,2,2);}c.globalAlpha=1; for(let i=0;i<24;i++){let x=i*44-30,h=80+((i*137)%210);c.fillStyle=i%2?'#171c38':'#1b2343';c.fillRect(x,620-h,40,h);for(let j=0;j<9;j++)for(let k=0;k<3;k++){if((i+j+k)%3){c.fillStyle=(i+j)%2?'#ce84bc':'#63b9c6';c.globalAlpha=.45;c.fillRect(x+7+k*10,625-h+j*22,3,7);}}}c.globalAlpha=1; c.fillStyle='#0d1326';c.fillRect(0,620,W,340);c.strokeStyle='#734277';for(let i=-10;i<=10;i++){c.beginPath();c.moveTo(480+i*9,620);c.lineTo(480+i*150,H);c.stroke();}for(let i=0;i<18;i++){let p=((i/18+t/(Math.PI*2))%1);let y=620+p*p*340;c.strokeStyle=`rgba(176,93,164,${p*.6})`;c.beginPath();c.moveTo(0,y);c.lineTo(W,y);c.stroke();} c.strokeStyle='#ff709d';c.lineWidth=3;c.beginPath();c.moveTo(440,620);c.lineTo(135,H);c.stroke();c.strokeStyle='#6cdbdf';c.beginPath();c.moveTo(520,620);c.lineTo(825,H);c.stroke(); } } for(const scene of ['afterglow','ocean','night']){ const child=spawn(ffmpeg,['-y','-f','image2pipe','-vcodec','png','-r',String(FPS),'-i','pipe:0','-f','lavfi','-i',`sine=frequency=${scene==='afterglow'?220:scene==='ocean'?174:261}:sample_rate=44100`,'-t',String(SECONDS),'-af','volume=0.025,afade=t=in:d=1,afade=t=out:st=7:d=1','-c:v','libx264','-preset','fast','-crf','24','-pix_fmt','yuv420p','-c:a','aac','-movflags','+faststart',`public/videos/${scene}.mp4`],{stdio:['pipe','ignore','pipe']}); let errors='';child.stderr.on('data',d=>errors+=d);const finished=once(child,'close'); for(let frame=0;frame