feat: build responsive short-video feed and Docker deployment

This commit is contained in:
2026-10-08 14:05:37 +08:00
commit 36d8215f0b
18 changed files with 1166 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules
.next
.git
test-results
*.tsbuildinfo
+29
View File
@@ -0,0 +1,29 @@
name: Build and deploy Docker
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: tiktok-ui-production
cancel-in-progress: false
jobs:
deploy:
runs-on: ces-control
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check Docker deployment environment
run: |
docker version
docker compose version
docker info --format 'Docker host: {{.Name}}'
docker ps --format 'table {{.Names}}\t{{.Ports}}'
- name: Build tested production image
run: docker build -t tiktok-ui:${{ gitea.sha }} -t tiktok-ui:latest .
- name: Deploy persistent service
env:
IMAGE_TAG: ${{ gitea.sha }}
run: |
docker compose up -d --no-build --wait --wait-timeout 120
docker compose ps
echo 'Application is published on the Docker host at port 18080.'
+6
View File
@@ -0,0 +1,6 @@
node_modules/
.next/
.env*
!.env.example
*.tsbuildinfo
test-results/
+22
View File
@@ -0,0 +1,22 @@
FROM node:22-alpine AS base
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
FROM base AS deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile --ignore-scripts
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm typecheck && pnpm build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nextjs && adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nextjs /app/public ./public
USER nextjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD node -e "fetch('http://127.0.0.1:3000').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "server.js"]
+50
View File
@@ -0,0 +1,50 @@
# 瞬间 FLOW
Next.js Pages Router + React + TypeScript 的抖音风格单页演示。无登录、无数据库、无后端业务接口。
## 本地启动
Node.js 22,pnpm 10.28.2:
```sh
corepack enable
pnpm install --frozen-lockfile
pnpm dev
```
浏览器打开 http://localhost:3000 。手机与电脑自适应:手机全屏触摸上下切换,电脑宽屏播放,支持滚轮、上下方向键及右侧切换按钮。空格暂停,M 切换声音。
## 交互
- 三段本地 MP4 无限循环切换,单击暂停、双击点赞、拖动进度、静音/开启声音、桌面全屏。
- 点赞、收藏、关注按视频保存在本机 localStorage,无需账号。
- 搜索默认数据、标签搜索、分享视频专属链接、评论抽屉及本次会话评论。
- 其他导航提供演示提示,不包含其他业务页面。
## 重新生成视频
```sh
pnpm generate:videos
```
`scripts/generate-videos.mjs` 使用 Canvas 绘图和 FFmpeg 生成日落、海岸、霓虹三段 8 秒 H.264/AAC 视频及封面。无外部素材下载;成品已提交,正常构建和运行无需 FFmpeg。音轨为程序生成的轻声合成音,初始静音。
## 检查与构建
```sh
pnpm typecheck
pnpm build
pnpm start
```
## Docker 持久化部署
```sh
docker compose up -d --build --wait
```
默认发布到宿主机 `18080` 端口,可用 `PUBLIC_PORT` 覆盖。容器以非 root 用户运行,具有健康检查、日志轮转及 `unless-stopped` 重启策略。
推送 main 触发 `.gitea/workflows/deploy.yml`,使用 `ces-control` 标签 runner 构建并部署。该 runner 必须能访问目标宿主机 Docker daemon 并具备 Compose v2;安全组需允许公网访问所选端口。运行持久性由宿主机 Docker 提供,runner 任务结束不会删除应用容器。若同标签有多个 runner,应由管理员保证标签指向预期部署主机。
后续更新:提交并推送 main 即可。镜像保留提交 SHA 标签,可用 `IMAGE_TAG=<旧提交SHA> docker compose up -d --no-build` 回滚到该宿主机已保留的镜像。
+18
View File
@@ -0,0 +1,18 @@
name: tiktok-ui
services:
web:
image: tiktok-ui:${IMAGE_TAG:-latest}
build: .
restart: unless-stopped
ports:
- "${PUBLIC_PORT:-18080}:3000"
init: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
+7
View File
@@ -0,0 +1,7 @@
export const videos = [
{id:'afterglow', title:'把日落,留在这一刻。', description:'山野没有答案,但晚风会让一切慢下来。', author:'山野来信', handle:'outdoor.notes', avatar:'山', color:'#ed9772', tags:['日落收集计划','治愈系风景','慢下来'], music:'Golden hour · 原创氛围音乐', likes:12840, saves:1860, comments:328, src:'/videos/afterglow.mp4', poster:'/videos/afterglow.jpg', location:'山野之间', label:'CHASE THE LAST LIGHT', subtitle:'日落不落幕'},
{id:'ocean', title:'今天的烦恼,就交给海吧。', description:'听见浪的声音,就像世界按下了暂停键。', author:'蓝色航线', handle:'blue.route', avatar:'海', color:'#55b9c3', tags:['看海日记','蓝色时刻','自由的风'], music:'Ocean breathing · 原创氛围音乐', likes:23670, saves:3201, comments:516, src:'/videos/ocean.mp4', poster:'/videos/ocean.jpg', location:'蔚蓝海岸', label:'SOMEWHERE, BY THE SEA', subtitle:'去有风的地方'},
{id:'night', title:'城市睡了,浪漫还没有。', description:'沿着霓虹的方向,开启一场没有目的地的夜游。', author:'午夜放映室', handle:'midnight.film', avatar:'夜', color:'#b594ed', tags:['城市夜游','霓虹浪漫','赛博空间'], music:'Midnight drive · 原创氛围音乐', likes:9652, saves:987, comments:164, src:'/videos/night.mp4', poster:'/videos/night.jpg', location:'午夜之城', label:'THE NIGHT IS STILL YOUNG', subtitle:'夜色正好'},
];
export type VideoItem = typeof videos[number];
export const compact = (n:number) => n >= 10000 ? `${(n/10000).toFixed(1)}万` : n.toLocaleString('zh-CN');
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
+3
View File
@@ -0,0 +1,3 @@
import type { NextConfig } from 'next';
const config: NextConfig = { output: 'standalone', poweredByHeader: false, reactStrictMode: true };
export default config;
+7
View File
@@ -0,0 +1,7 @@
{
"name": "tiktok-ui", "version": "1.0.0", "private": true,
"scripts": {"dev":"next dev --hostname 0.0.0.0", "build":"next build", "start":"next start --hostname 0.0.0.0", "typecheck":"tsc --noEmit", "generate:videos":"node scripts/generate-videos.mjs"},
"dependencies": {"next":"^15.5.0", "react":"^19.1.0", "react-dom":"^19.1.0", "lucide-react":"^0.468.0"},
"devDependencies": {"typescript":"^5.9.0", "@types/node":"^22.0.0", "@types/react":"^19.0.0", "@types/react-dom":"^19.0.0", "@napi-rs/canvas":"^0.1.80", "ffmpeg-static":"^5.2.0"},
"packageManager":"pnpm@10.28.2"
}
+3
View File
@@ -0,0 +1,3 @@
import type { AppProps } from 'next/app';
import '../styles/globals.css';
export default function App({ Component, pageProps }: AppProps) { return <Component {...pageProps} />; }
+2
View File
@@ -0,0 +1,2 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() { return <Html lang="zh-CN"><Head /><body><Main /><NextScript /></body></Html>; }
+46
View File
@@ -0,0 +1,46 @@
import Head from 'next/head';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Heart, MessageCircle, Bookmark, Share2, Plus, Play, Pause, Volume2, VolumeX, ChevronUp, ChevronDown, Search, Home, Compass, Users, Music2, Maximize, X, Check, MapPin, Send, ArrowUpRight, TvMinimal, Ellipsis, UserRound } from 'lucide-react';
import { videos, compact } from '../lib/videos';
type Reaction = { liked?:boolean; saved?:boolean; followed?:boolean };
type Comment = {name:string; text:string};
const initialComments:Comment[]=[{name:'小岛同学',text:'这一刻,连时间都慢下来了。'},{name:'今天也要开心',text:'好喜欢这个氛围 ✨'},{name:'路过人间',text:'收藏了,留给忙碌的一天。'}];
export default function Feed(){
const [index,setIndex]=useState(0),[playing,setPlaying]=useState(true),[muted,setMuted]=useState(true),[progress,setProgress]=useState(0),[duration,setDuration]=useState(8),[reactions,setReactions]=useState<Record<string,Reaction>>({}),[ready,setReady]=useState(false),[panel,setPanel]=useState<'comments'|'search'|null>(null),[toast,setToast]=useState(''),[burst,setBurst]=useState(false),[query,setQuery]=useState(''),[draft,setDraft]=useState(''),[comments,setComments]=useState<Record<string,Comment[]>>({}),[tab,setTab]=useState('推荐'),[error,setError]=useState(false);
const video=useRef<HTMLVideoElement>(null),stage=useRef<HTMLElement>(null),touch=useRef({x:0,y:0}),lastScroll=useRef(0),clickTimer=useRef<ReturnType<typeof setTimeout>|null>(null),toastTimer=useRef<ReturnType<typeof setTimeout>|null>(null),burstTimer=useRef<ReturnType<typeof setTimeout>|null>(null);
const item=videos[index],reaction=reactions[item.id]||{};
const notify=useCallback((text:string)=>{setToast(text);if(toastTimer.current)clearTimeout(toastTimer.current);toastTimer.current=setTimeout(()=>setToast(''),2400);},[]);
useEffect(()=>{try{const data=JSON.parse(localStorage.getItem('flow-reactions')||'{}');if(data && typeof data==='object' && !Array.isArray(data))setReactions(data);}catch{}setReady(true);const id=new URLSearchParams(location.search).get('video');const found=videos.findIndex(v=>v.id===id);if(found>=0)setIndex(found);return()=>{if(clickTimer.current)clearTimeout(clickTimer.current);if(toastTimer.current)clearTimeout(toastTimer.current);if(burstTimer.current)clearTimeout(burstTimer.current);};},[]);
useEffect(()=>{if(ready)try{localStorage.setItem('flow-reactions',JSON.stringify(reactions));}catch{}},[reactions,ready]);
const next=useCallback((direction:number)=>{setIndex(i=>(i+direction+videos.length)%videos.length);setProgress(0);setPlaying(true);setError(false);setBurst(false);setDraft('');if(clickTimer.current)clearTimeout(clickTimer.current);},[]);
useEffect(()=>{const el=video.current;if(!el)return;if(playing){el.play().catch(()=>setPlaying(false));}else el.pause();},[index,playing]);
useEffect(()=>{function key(e:KeyboardEvent){if((e.target as HTMLElement).closest('input,textarea,button')||panel)return;if(e.code==='ArrowDown'||e.code==='ArrowUp'){e.preventDefault();next(e.code==='ArrowDown'?1:-1);}if(e.code==='Space'){e.preventDefault();setPlaying(p=>!p);}if(e.code==='KeyM')setMuted(m=>!m);}window.addEventListener('keydown',key);return()=>window.removeEventListener('keydown',key);},[next,panel]);
useEffect(()=>{const el=stage.current;if(!el)return;const wheel=(e:WheelEvent)=>{if(panel||Math.abs(e.deltaY)<12||e.ctrlKey)return;e.preventDefault();if(Date.now()-lastScroll.current<750)return;lastScroll.current=Date.now();next(e.deltaY>0?1:-1);};el.addEventListener('wheel',wheel,{passive:false});return()=>el.removeEventListener('wheel',wheel);},[next,panel]);
useEffect(()=>{if(!panel)return;const before=document.activeElement as HTMLElement;const dialog=document.querySelector<HTMLElement>('[role="dialog"]');dialog?.querySelector<HTMLElement>('button,input')?.focus();const key=(e:KeyboardEvent)=>{if(e.key==='Escape')setPanel(null);if(e.key==='Tab'){const list=dialog?.querySelectorAll<HTMLElement>('button,input,[tabindex="0"]');if(!list?.length)return;const first=list[0],last=list[list.length-1];if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}}};document.addEventListener('keydown',key);return()=>{document.removeEventListener('keydown',key);before?.focus();};},[panel]);
function toggle(field:keyof Reaction){setReactions(r=>({...r,[item.id]:{...r[item.id],[field]:!r[item.id]?.[field]}}));}
function like(){setReactions(r=>({...r,[item.id]:{...r[item.id],liked:true}}));setBurst(true);if(burstTimer.current)clearTimeout(burstTimer.current);burstTimer.current=setTimeout(()=>setBurst(false),800);}
async function share(){const url=`${location.origin}/?video=${item.id}`;try{if(navigator.share){await navigator.share({title:item.title,url});}else{await navigator.clipboard.writeText(url);notify('视频链接已复制');}}catch(e){if((e as Error).name!=='AbortError')notify('请复制地址栏链接进行分享');}}
const time=(s:number)=>`00:${Math.floor(s).toString().padStart(2,'0')}`;
const results=videos.filter(v=>`${v.title}${v.author}${v.tags.join('')}`.includes(query.trim()));
return <><Head><title>瞬间 FLOW · 发现生活的另一面</title><meta name="description" content="沉浸式短视频体验,记录每一个值得停留的瞬间。"/><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/><meta name="theme-color" content="#0b0b0e"/></Head>
<div className="app-shell">
<aside className="sidebar"><a className="brand" href="/" aria-label="瞬间首页"><span className="brand-mark"><Music2 size={28}/></span>瞬间<span className="brand-en">FLOW</span></a><button className="search-box" onClick={()=>setPanel('search')}><Search size={19}/><span>搜索感兴趣的内容</span><kbd>/</kbd></button><nav>{[{name:'推荐',icon:Home},{name:'发现',icon:Compass},{name:'关注',icon:Users}].map(({name,icon:Icon})=><button key={name} className={`nav-item ${tab===name?'active':''}`} onClick={()=>{setTab(name);if(name==='发现')setPanel('search');if(name==='关注')notify('关注作者后,关注标记会保存在此设备');}}><Icon size={23}/>{name}{name==='推荐'&&<span className="active-dot"/>}</button>)}</nav><div className="sidebar-line"/><p className="sidebar-label">精选频道</p>{['生活的浪漫','自然放映厅','城市漫游'].map((label,i)=><button key={label} className="channel" onClick={()=>{setIndex(i);setProgress(0);setPlaying(true);setError(false);}}><span className={`channel-dot dot-${i}`}/>{label}<ArrowUpRight size={14}/></button>)}<div className="sidebar-bottom"><div className="mini-brand">每一刻,都值得被看见<span>MAKE EVERY MOMENT COUNT.</span></div><p>关于瞬间 · 使用指南 · 联系我们</p><p>© 2026 瞬间 FLOW</p><span className="demo-pill"><span/>本地演示 · 无需登录</span></div></aside>
<main className="main"><header className="desktop-header"><div><span className="eyebrow">FOR YOU</span><h1>发现生活的另一面<span>精选推荐</span></h1></div><span className="header-note"><span/> 偶遇一个好瞬间</span></header>
<section ref={stage} className={`stage scene-${item.id}`} aria-label="短视频播放器" onTouchStart={e=>{touch.current={x:e.touches[0].clientX,y:e.touches[0].clientY};}} onTouchEnd={e=>{if(panel||(e.target as HTMLElement).closest('button,input'))return;const dy=e.changedTouches[0].clientY-touch.current.y,dx=e.changedTouches[0].clientX-touch.current.x;if(Math.abs(dy)>55&&Math.abs(dy)>Math.abs(dx))next(dy<0?1:-1);}}>
<video key={item.id} ref={video} className="video" src={item.src} poster={item.poster} loop playsInline muted={muted} autoPlay preload="auto" onTimeUpdate={e=>setProgress(e.currentTarget.currentTime)} onLoadedMetadata={e=>setDuration(e.currentTarget.duration)} onPlay={()=>setPlaying(true)} onPause={()=>setPlaying(false)} onError={()=>{setError(true);setPlaying(false);}}/>
<div className="scrim"/><button className="video-hit" aria-label={playing?'暂停视频':'播放视频'} onClick={()=>{if(clickTimer.current)clearTimeout(clickTimer.current);clickTimer.current=setTimeout(()=>setPlaying(p=>!p),220);}} onDoubleClick={()=>{if(clickTimer.current)clearTimeout(clickTimer.current);like();}}/>
<div className="mobile-top"><TvMinimal size={23}/><div><button onClick={()=>notify('点击作者头像即可关注')}>关注</button><button className="selected" onClick={()=>setTab('推荐')}>推荐</button></div><button aria-label="搜索视频" onClick={()=>setPanel('search')}><Search size={24}/></button></div>
<div className="stage-top"><span className="curated"><span/> 今日精选</span><span className="location"><MapPin size={14}/>{item.location}</span><button aria-label={muted?'开启声音':'静音'} className="sound-top" onClick={()=>setMuted(m=>!m)}>{muted?<VolumeX size={19}/>:<Volume2 size={19}/>}</button></div>
<div className="scene-copy" aria-hidden="true"><span>{item.label}</span><strong>{item.subtitle}</strong><i>THE LITTLE THINGS. THE BIG FEELINGS.</i></div>
{!playing&&!error&&<span className="paused"><Play size={54} fill="currentColor"/></span>}{burst&&<span className="heart-burst"><Heart size={110} fill="currentColor"/></span>}{error&&<div className="video-error">视频暂时无法加载<button onClick={()=>{setError(false);video.current?.load();setPlaying(true);}}>重新加载</button></div>}
<div className="video-info"><span className="editor-label">瞬间精选 <span>·</span> 风景与生活</span><h2>@{item.author}<span className="verified"><Check size={11}/></span></h2><h3>{item.title}</h3><p>{item.description}</p><div className="tags">{item.tags.map(t=><button key={t} onClick={()=>{setQuery(t);setPanel('search');}}>#{t}</button>)}</div><div className="music"><Music2 size={15}/><span>{item.music}</span><span className="music-bars">▂ ▅ ▃ ▆ ▂</span></div></div>
<div className="actions"><button className="avatar" style={{background:item.color}} aria-label={reaction.followed?'取消关注作者':'关注作者'} aria-pressed={!!reaction.followed} onClick={()=>toggle('followed')}>{item.avatar}<span>{reaction.followed?<Check size={13}/>:<Plus size={13}/>}</span></button><button className={`action ${reaction.liked?'liked':''}`} aria-label="点赞" aria-pressed={!!reaction.liked} onClick={()=>toggle('liked')}><Heart fill="currentColor"/><span>{compact(item.likes+(reaction.liked?1:0))}</span></button><button className="action" aria-label="查看评论" onClick={()=>setPanel('comments')}><MessageCircle fill="currentColor"/><span>{item.comments+(comments[item.id]?.length||0)}</span></button><button className={`action ${reaction.saved?'saved':''}`} aria-label="收藏" aria-pressed={!!reaction.saved} onClick={()=>toggle('saved')}><Bookmark fill="currentColor"/><span>{compact(item.saves+(reaction.saved?1:0))}</span></button><button className="action" aria-label="分享视频" onClick={share}><Share2 fill="currentColor"/><span>分享</span></button><span className={`record ${playing?'spinning':''}`}><span style={{background:item.color}}><Music2 size={16}/></span></span></div>
<div className="player-controls"><input className="progress" type="range" aria-label="播放进度" min="0" max={duration||8} step="0.05" value={progress} style={{'--progress':`${progress/(duration||8)*100}%`} as React.CSSProperties} onChange={e=>{const value=Number(e.target.value);if(video.current)video.current.currentTime=value;setProgress(value);}}/><div className="control-row"><button aria-label={playing?'暂停':'播放'} onClick={()=>setPlaying(p=>!p)}>{playing?<Pause size={18} fill="currentColor"/>:<Play size={18} fill="currentColor"/>}</button><span className="time">{time(progress)} <span>/ {time(duration)}</span></span><span className="control-hint">滚动发现更多精彩</span><button className="quality" onClick={()=>notify('当前为本地生成的高清演示视频')}>高清 <span>HD</span></button><button aria-label={muted?'开启声音':'静音'} onClick={()=>setMuted(m=>!m)}>{muted?<VolumeX size={20}/>:<Volume2 size={20}/>}</button><button aria-label="全屏" onClick={()=>{if(document.fullscreenElement)document.exitFullscreen();else stage.current?.requestFullscreen?.().catch(()=>notify('此浏览器暂不支持全屏'));}}><Maximize size={18}/></button></div></div>
</section><div className="desktop-footer"><span><span className="key">↑</span><span className="key">↓</span> 切换视频 <span className="footer-divider"/> <span className="key space">Space</span> 暂停 / 播放</span><span><span className="current-number">0{index+1}</span> / 0{videos.length} <span className="footer-divider"/> 好风景,循环看</span></div>
<div className="next-controls"><button aria-label="上一个视频" onClick={()=>next(-1)}><ChevronUp/></button><span/ ><button aria-label="下一个视频" onClick={()=>next(1)}><ChevronDown/></button></div>
</main><nav className="mobile-bottom"><button className="selected" onClick={()=>{setTab('推荐');setPanel(null);}}><Home fill="currentColor"/>首页</button><button onClick={()=>setPanel('search')}><Compass/>发现</button><button className="create" aria-label="发布提示" onClick={()=>notify('当前为浏览演示,暂未开放发布')}><Plus/></button><button onClick={()=>setPanel('comments')}><MessageCircle/>消息</button><button onClick={()=>notify('无需登录,点赞与收藏保存在此设备')}><UserRound/>我</button></nav>
</div>
{panel&&<div className="modal-backdrop" onClick={()=>setPanel(null)}><section className="panel" role="dialog" aria-modal="true" aria-labelledby="panel-title" onClick={e=>e.stopPropagation()}><header><h2 id="panel-title">{panel==='comments'?`评论 · ${item.comments+(comments[item.id]?.length||0)}`:'发现好瞬间'}</h2><button aria-label="关闭" onClick={()=>setPanel(null)}><X/></button></header>{panel==='comments'?<><p className="panel-note">演示评论 · 新评论仅在本次浏览中保留</p><div className="comments">{[...initialComments,...comments[item.id]||[]].map((c,i)=><article key={i}><span className="comment-avatar">{c.name[0]}</span><div><strong>{c.name}</strong><p>{c.text}</p><small>刚刚 · 回复</small></div><Heart size={15}/></article>)}</div><form onSubmit={e=>{e.preventDefault();if(!draft.trim())return;setComments(c=>({...c,[item.id]:[...c[item.id]||[],{name:'访客',text:draft.trim()}]}));setDraft('');}}><input aria-label="评论内容" placeholder="留下你的心情…" value={draft} maxLength={200} onChange={e=>setDraft(e.target.value)}/><button aria-label="发送评论" disabled={!draft.trim()}><Send size={20}/></button></form></>:<><div className="search-input"><Search size={20}/><input aria-label="搜索内容" placeholder="搜索标题、作者或标签" value={query} onChange={e=>setQuery(e.target.value)}/></div><div className="search-results">{results.map(v=><button key={v.id} onClick={()=>{setIndex(videos.findIndex(x=>x.id===v.id));setPlaying(true);setProgress(0);setError(false);setPanel(null);}}><img src={v.poster} alt=""/><span><strong>{v.title}</strong><small>@{v.author} · {v.tags[0]}</small></span><Play size={18}/></button>)}{results.length===0&&<p>还没有找到,试试“日落”或“海”。</p>}</div></>}</section></div>}
<div className={`toast ${toast?'visible':''}`} role="status">{toast}</div>
</>;
}
+916
View File
@@ -0,0 +1,916 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
lucide-react:
specifier: ^0.468.0
version: 0.468.0(react@19.3.0)
next:
specifier: ^15.5.0
version: 15.5.27(@types/node@22.20.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react:
specifier: ^19.1.0
version: 19.3.0
react-dom:
specifier: ^19.1.0
version: 19.3.0(react@19.3.0)
devDependencies:
'@napi-rs/canvas':
specifier: ^0.1.80
version: 0.1.100
'@types/node':
specifier: ^22.0.0
version: 22.20.5
'@types/react':
specifier: ^19.0.0
version: 19.3.0
'@types/react-dom':
specifier: ^19.0.0
version: 19.3.0(@types/react@19.3.0)
ffmpeg-static:
specifier: ^5.2.0
version: 5.3.0
typescript:
specifier: ^5.9.0
version: 5.9.3
packages:
'@derhuerst/http-basic@8.2.4':
resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==}
engines: {node: '>=6.0.0'}
'@emnapi/runtime@1.11.3':
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.35.5':
resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.35.5':
resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
'@img/sharp-freebsd-wasm32@0.35.5':
resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.3.4':
resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.3.4':
resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.3.4':
resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.4':
resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.4':
resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.4':
resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.4':
resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.4':
resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.3.4':
resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.4':
resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.35.5':
resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.35.5':
resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.35.5':
resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.35.5':
resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.35.5':
resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.35.5':
resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.35.5':
resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.5':
resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.35.5':
resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.5':
resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.35.5':
resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.35.5':
resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.35.5':
resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas-android-arm64@0.1.100':
resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.100':
resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.100':
resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.100':
resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@napi-rs/canvas@0.1.100':
resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
engines: {node: '>= 10'}
'@next/env@15.5.27':
resolution: {integrity: sha512-WC5hvVqiuFOcRyz6gKPeJkiVvSMf3scNeAV49CdWtVqxmUlzh8UKR6YHt9yiNkh8LngtzqSlVz07/Ma3ZAzchA==}
'@next/swc-darwin-arm64@15.5.27':
resolution: {integrity: sha512-fBA9QurmK4+uj4wNhmRTuO5zMVRaBHCvp6H93zTG/5i6Yi6TewbvbesXB8YQNj8T99Cwrow87hHiFV6wL/hyMA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.5.27':
resolution: {integrity: sha512-FGZwJwry8qZNHxD/nfwrCwCHHrqx6epqItkkxbYcBQEQ2uw5S43bY83fQB5zbAsZE7TELMjoYkbhsQ4oKM7wwg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.5.27':
resolution: {integrity: sha512-GQulAaJYNF1RYuatlsw9/uzbKz4if2V7Ic9nVO+P0ljYPm4yBRTFIl6g5YkKTw40lMLbpy9edLoH44g3jSHMYA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.5.27':
resolution: {integrity: sha512-URFFsUmu8Pi0/1qdnUI7tx7keZw1Mt5zHfrIX1RfDG9Nih8DYxK54WZsDJUX2ZTlii1jgotL7qcbwKnSAES6xQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.5.27':
resolution: {integrity: sha512-aJp74z7uvU5jQhyH7lyk+4qSRcTdKbKT9NQE3jiO9kWYkoHZjKi0zzHDbCoZ+v2sJDEiJ/z58R0UlWMGWNF3HA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.5.27':
resolution: {integrity: sha512-ILl2bQ8MbpvyMpY8AXljb3BXs0MJSC8HXZ6QV5AWpVS7ZELqNPtrHvhUk+SNarvjvICXX8bCS287TCi+gUFVYg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.5.27':
resolution: {integrity: sha512-1MB9Rh5y8TOPN2x7naNTJNkCZQav9c70AI7EBYXxsEewwOffC4DjjwBwKf20NBvcA+u+5jDv7nVmHxunlP2d7Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.5.27':
resolution: {integrity: sha512-XtYgpGOswgnyBWXzOEG6q9RhvnSw0CEZ0MbWqCsl5PISgt9EzvJvNW5mU5n1PsphX/lnamzxjgQXH2GUJL8nqg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@types/node@10.17.60':
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
'@types/node@22.20.5':
resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==}
'@types/react-dom@19.3.0':
resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==}
peerDependencies:
'@types/react': ^19.3.0
'@types/react@19.3.0':
resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
caniuse-lite@1.0.30001815:
resolution: {integrity: sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==}
caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
concat-stream@2.0.0:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
ffmpeg-static@5.3.0:
resolution: {integrity: sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==}
engines: {node: '>=16'}
http-response-object@3.0.2:
resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
lucide-react@0.468.0:
resolution: {integrity: sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.20:
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
next@15.5.27:
resolution: {integrity: sha512-F82CrlPZ8GRxBH9RIIoR3qU1t5RT3dv1K8moe8SjlDEquiFipvEI13R1Om90TaUOEwShkWCMlg8+gvTNzfMhvg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.51.1
babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
'@playwright/test':
optional: true
babel-plugin-react-compiler:
optional: true
sass:
optional: true
parse-cache-control@1.0.1:
resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
progress@2.0.3:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
react-dom@19.3.0:
resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==}
peerDependencies:
react: ^19.3.0
react@19.3.0:
resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
scheduler@0.28.0:
resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
sharp@0.35.5:
resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
peerDependencies:
'@babel/core': '*'
babel-plugin-macros: '*'
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
peerDependenciesMeta:
'@babel/core':
optional: true
babel-plugin-macros:
optional: true
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
snapshots:
'@derhuerst/http-basic@8.2.4':
dependencies:
caseless: 0.12.0
concat-stream: 2.0.0
http-response-object: 3.0.2
parse-cache-control: 1.0.1
'@emnapi/runtime@1.11.3':
dependencies:
tslib: 2.8.1
optional: true
'@img/colour@1.1.0':
optional: true
'@img/sharp-darwin-arm64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.3.4
optional: true
'@img/sharp-darwin-x64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.3.4
optional: true
'@img/sharp-freebsd-wasm32@0.35.5':
dependencies:
'@img/sharp-wasm32': 0.35.5
optional: true
'@img/sharp-libvips-darwin-arm64@1.3.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.3.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.3.4':
optional: true
'@img/sharp-libvips-linux-arm@1.3.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.3.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.3.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.3.4':
optional: true
'@img/sharp-libvips-linux-x64@1.3.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.3.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.3.4':
optional: true
'@img/sharp-linux-arm64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.3.4
optional: true
'@img/sharp-linux-arm@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.3.4
optional: true
'@img/sharp-linux-ppc64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.3.4
optional: true
'@img/sharp-linux-riscv64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.3.4
optional: true
'@img/sharp-linux-s390x@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.3.4
optional: true
'@img/sharp-linux-x64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.3.4
optional: true
'@img/sharp-linuxmusl-arm64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
optional: true
'@img/sharp-linuxmusl-x64@0.35.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
optional: true
'@img/sharp-wasm32@0.35.5':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
'@img/sharp-webcontainers-wasm32@0.35.5':
dependencies:
'@img/sharp-wasm32': 0.35.5
optional: true
'@img/sharp-win32-arm64@0.35.5':
optional: true
'@img/sharp-win32-ia32@0.35.5':
optional: true
'@img/sharp-win32-x64@0.35.5':
optional: true
'@napi-rs/canvas-android-arm64@0.1.100':
optional: true
'@napi-rs/canvas-darwin-arm64@0.1.100':
optional: true
'@napi-rs/canvas-darwin-x64@0.1.100':
optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.100':
optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
optional: true
'@napi-rs/canvas@0.1.100':
optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.100
'@napi-rs/canvas-darwin-arm64': 0.1.100
'@napi-rs/canvas-darwin-x64': 0.1.100
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
'@napi-rs/canvas-linux-arm64-gnu': 0.1.100
'@napi-rs/canvas-linux-arm64-musl': 0.1.100
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
'@napi-rs/canvas-linux-x64-gnu': 0.1.100
'@napi-rs/canvas-linux-x64-musl': 0.1.100
'@napi-rs/canvas-win32-arm64-msvc': 0.1.100
'@napi-rs/canvas-win32-x64-msvc': 0.1.100
'@next/env@15.5.27': {}
'@next/swc-darwin-arm64@15.5.27':
optional: true
'@next/swc-darwin-x64@15.5.27':
optional: true
'@next/swc-linux-arm64-gnu@15.5.27':
optional: true
'@next/swc-linux-arm64-musl@15.5.27':
optional: true
'@next/swc-linux-x64-gnu@15.5.27':
optional: true
'@next/swc-linux-x64-musl@15.5.27':
optional: true
'@next/swc-win32-arm64-msvc@15.5.27':
optional: true
'@next/swc-win32-x64-msvc@15.5.27':
optional: true
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
'@types/node@10.17.60': {}
'@types/node@22.20.5':
dependencies:
undici-types: 6.21.0
'@types/react-dom@19.3.0(@types/react@19.3.0)':
dependencies:
'@types/react': 19.3.0
'@types/react@19.3.0':
dependencies:
csstype: 3.2.3
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
buffer-from@1.1.2: {}
caniuse-lite@1.0.30001815: {}
caseless@0.12.0: {}
client-only@0.0.1: {}
concat-stream@2.0.0:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 3.6.2
typedarray: 0.0.6
csstype@3.2.3: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
detect-libc@2.1.2:
optional: true
env-paths@2.2.1: {}
ffmpeg-static@5.3.0:
dependencies:
'@derhuerst/http-basic': 8.2.4
env-paths: 2.2.1
https-proxy-agent: 5.0.1
progress: 2.0.3
transitivePeerDependencies:
- supports-color
http-response-object@3.0.2:
dependencies:
'@types/node': 10.17.60
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
inherits@2.0.4: {}
lucide-react@0.468.0(react@19.3.0):
dependencies:
react: 19.3.0
ms@2.1.3: {}
nanoid@3.3.20: {}
next@15.5.27(@types/node@22.20.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
'@next/env': 15.5.27
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001815
postcss: 8.4.31
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
styled-jsx: 5.1.6(react@19.3.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.5.27
'@next/swc-darwin-x64': 15.5.27
'@next/swc-linux-arm64-gnu': 15.5.27
'@next/swc-linux-arm64-musl': 15.5.27
'@next/swc-linux-x64-gnu': 15.5.27
'@next/swc-linux-x64-musl': 15.5.27
'@next/swc-win32-arm64-msvc': 15.5.27
'@next/swc-win32-x64-msvc': 15.5.27
sharp: 0.35.5(@types/node@22.20.5)
transitivePeerDependencies:
- '@babel/core'
- '@types/node'
- babel-plugin-macros
parse-cache-control@1.0.1: {}
picocolors@1.1.1: {}
postcss@8.4.31:
dependencies:
nanoid: 3.3.20
picocolors: 1.1.1
source-map-js: 1.2.2
progress@2.0.3: {}
react-dom@19.3.0(react@19.3.0):
dependencies:
react: 19.3.0
scheduler: 0.28.0
react@19.3.0: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
safe-buffer@5.2.1: {}
scheduler@0.28.0: {}
semver@7.8.5:
optional: true
sharp@0.35.5(@types/node@22.20.5):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.35.5
'@img/sharp-darwin-x64': 0.35.5
'@img/sharp-freebsd-wasm32': 0.35.5
'@img/sharp-libvips-darwin-arm64': 1.3.4
'@img/sharp-libvips-darwin-x64': 1.3.4
'@img/sharp-libvips-linux-arm': 1.3.4
'@img/sharp-libvips-linux-arm64': 1.3.4
'@img/sharp-libvips-linux-ppc64': 1.3.4
'@img/sharp-libvips-linux-riscv64': 1.3.4
'@img/sharp-libvips-linux-s390x': 1.3.4
'@img/sharp-libvips-linux-x64': 1.3.4
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
'@img/sharp-linux-arm': 0.35.5
'@img/sharp-linux-arm64': 0.35.5
'@img/sharp-linux-ppc64': 0.35.5
'@img/sharp-linux-riscv64': 0.35.5
'@img/sharp-linux-s390x': 0.35.5
'@img/sharp-linux-x64': 0.35.5
'@img/sharp-linuxmusl-arm64': 0.35.5
'@img/sharp-linuxmusl-x64': 0.35.5
'@img/sharp-webcontainers-wasm32': 0.35.5
'@img/sharp-win32-arm64': 0.35.5
'@img/sharp-win32-ia32': 0.35.5
'@img/sharp-win32-x64': 0.35.5
'@types/node': 22.20.5
optional: true
source-map-js@1.2.2: {}
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
styled-jsx@5.1.6(react@19.3.0):
dependencies:
client-only: 0.0.1
react: 19.3.0
tslib@2.8.1: {}
typedarray@0.0.6: {}
typescript@5.9.3: {}
undici-types@6.21.0: {}
util-deprecate@1.0.2: {}
+2
View File
@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- ffmpeg-static
+36
View File
@@ -0,0 +1,36 @@
import { createCanvas } from '@napi-rs/canvas';
import ffmpeg from 'ffmpeg-static';
import { spawn } from 'node:child_process';
import { mkdir, writeFile } from 'node:fs/promises';
import { once } from 'node:events';
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<FPS*SECONDS;frame++){draw(scene,frame/(FPS*SECONDS)*Math.PI*2);if(frame===0)await writeFile(`public/videos/${scene}.jpg`,canvas.toBuffer('image/jpeg'));if(!child.stdin.write(canvas.toBuffer('image/png')))await once(child.stdin,'drain');}
child.stdin.end();const [code]=await finished;if(code!==0)throw new Error(errors);console.log(`Generated ${scene}.mp4`);
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"compilerOptions":{"target":"ES2017","lib":["dom","dom.iterable","esnext"],"allowJs":true,"skipLibCheck":true,"strict":true,"noEmit":true,"esModuleInterop":true,"module":"esnext","moduleResolution":"bundler","resolveJsonModule":true,"isolatedModules":true,"jsx":"preserve","incremental":true},"include":["next-env.d.ts","**/*.ts","**/*.tsx"],"exclude":["node_modules"]}