Surf Animation Studio commited on
Commit
e8a358e
·
1 Parent(s): aca5ba2

chore: clear repository contents before replacement

Browse files
.gitignore DELETED
@@ -1,7 +0,0 @@
1
- node_modules/
2
- dist/
3
- .env
4
- .env.*
5
- !.env.example
6
- *.log
7
- .DS_Store
 
 
 
 
 
 
 
 
.gitkeep ADDED
File without changes
README.md DELETED
@@ -1,129 +0,0 @@
1
- ---
2
- title: 收猴子的故事 · 经济学动画工作台
3
- emoji: 🍌
4
- colorFrom: yellow
5
- colorTo: indigo
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- # 《收猴子的故事》经济学动画工作台
11
-
12
- > 一个原创的、浏览器内实时渲染的横屏 / 竖屏动画编辑与发布工作台。
13
- >
14
- > 主题:用“收猴子的故事”讲清楚制度、路径依赖、激励与组织文化。
15
- >
16
- > 本项目采用原创的数学可视化讲解语法:用圆、线、坐标、状态变化和镜头推进构成画面;不复制任何频道的片头、角色、口播或素材。
17
-
18
- ## 目标
19
-
20
- - 在网页中直接预览一支约 3 分钟的中文经济学动画。
21
- - 横屏 16:9:适合 B 站;竖屏 9:16:适合抖音,并预留安全区处理刘海与底部 UI 遮挡。
22
- - 画面有连续运动、镜头推进、动态图形、字幕卡、音效与音乐;旁白可由浏览器本地语音朗读。
23
- - 提供本地渲染导出:优先 MP4,当前浏览器不支持 MP4 编码时自动降级 WebM,并给出清晰提示。
24
- - 提供可复制的 B 站 / 抖音标题、简介、标签。
25
- - 提供横屏与竖屏封面图下载。
26
- - 不依赖远程素材也能运行:动画、音乐、音效均由代码生成;外部 TTS 为可选增强。
27
-
28
- ## 故事与创作边界
29
-
30
- 故事使用经济学课堂里常见的“猴子—梯子—香蕉—喷水”寓言作为**隐喻**,不是对真实动物实验的复述。脚本会明确指出:这个寓言常被用来讨论“没有人知道为什么却继续遵守”的制度惯性,但它不应被当作真实实验史料。
31
-
32
- 讲解采用原创的“财经博主式、快节奏、图形化、带反问和暂停”的表达,不复刻任何在世创作者的独特措辞、声音、头像或品牌资产。
33
-
34
- ## 实现大纲
35
-
36
- ### 1. 研究与脚本
37
-
38
- 1. 固定核心命题:规则如何从激励变成惯例,再变成无人追问的禁区。
39
- 2. 将故事拆成 8 幕:钩子、第一次尝试、惩罚机制、群体复制、新人替换、规则遗忘、现实映射、结尾反问。
40
- 3. 为每幕配置旁白、屏幕文字、视觉动作、音效、音乐变化和时长。
41
- 4. 旁白文本与屏幕字幕分离,方便后续换成真实配音或外部 TTS。
42
-
43
- ### 2. 视觉与动画
44
-
45
- 1. 使用 Canvas 2D 作为确定性渲染层,统一时间轴与镜头状态。
46
- 2. 采用“对象形变、向量关系、坐标系、颜色编码”的数学讲解动画语法;不直接复制 3Blue1Brown 的画面或角色。
47
- 3. 用镜头参数驱动:世界坐标、缩放、平移、淡入淡出、擦除、脉冲和分层深度。
48
- 4. 同一套场景适配横屏与竖屏:竖屏时自动布局为上图下文,并保留 `safe-top` / `safe-bottom` 安全区。
49
- 5. 画面包括自绘猴子、梯子、香蕉、喷水装置、制度循环、激励函数、现实映射卡片和封面。
50
-
51
- ### 3. 音频
52
-
53
- 1. 浏览器内用 Web Audio API 合成低音、鼓点、木琴式动机、滴答、喷水、提示和转场音效。
54
- 2. 浏览器的 SpeechSynthesis 提供本地旁白试听,并在播放时随章节自动切换;导出时保证字幕、音乐和音效可录制。
55
- 3. `scripts/hf-tts.mjs` 提供可选 Hugging Face 中文 TTS 生成入口,密钥只从 `HF_TOKEN` 环境变量读取,不写入前端与 Git。生成服务不可用时,网页仍以浏览器本地旁白完整工作。
56
-
57
- ### 4. 网页工作台
58
-
59
- 1. 左侧播放区:实时画布、横 / 竖屏切换、播放 / 暂停、重播、音频开关。
60
- 2. 右侧控制区:章节时间轴、章节选择、导演说明、导出按钮、文案复制。
61
- 3. 底部信息条:当前画幅、时间码、渲染状态、MP4 能力检测。
62
- 4. 复制按钮将标题、描述、标签写入剪贴板,并在失败时提供可选文本区域。
63
-
64
- ### 5. 导出
65
-
66
- 1. 用 `canvas.captureStream()` 和 Web Audio `MediaStreamDestination` 组合本地媒体流。
67
- 2. 优先探测 `video/mp4`;若浏览器支持则直接导出 MP4。
68
- 3. 若不支持 MP4,导出器会在浏览器内按需加载 ffmpeg.wasm,把 WebM 转为 MP4;若网络或浏览器策略阻止加载,则明确降级为高兼容 WebM,不伪称 MP4。
69
- 4. 导出仅在用户点击后进行,不上传画面、旁白或项目数据;转码发生在本地浏览器。若本地存在 `public/audio/narration.wav`,导出会自动把它与音乐、音效混入;否则仍保留字幕和旁白试听。
70
-
71
- ### 6. 质量验收
72
-
73
- - [ ] README 计划先于代码提交。
74
- - [ ] 所有章节可从头播放,时间轴不越界。
75
- - [ ] 横屏主体不被字幕区遮挡。
76
- - [ ] 竖屏内容避开刘海与底部手势 / UI 安全区。
77
- - [ ] 播放、暂停、重播、章节跳转可用。
78
- - [ ] 音效与音乐开关可用;旁白可读。
79
- - [ ] 导出按钮给出真实格式与能力提示,不伪称 MP4。
80
- - [ ] 标题、描述、标签可复制。
81
- - [ ] 横竖屏封面可下载。
82
- - [ ] 不把任何 Hugging Face / GitHub 密钥提交到仓库。
83
-
84
- ## 计划中的大动作与提交节奏
85
-
86
- 1. `plan`: 写入本 README,确认范围、脚本和技术���线。
87
- 2. `scaffold`: 建立网页工程、基础样式、时间轴数据和本地运行入口。
88
- 3. `animation`: 完成横屏 / 竖屏 Canvas 动画、章节和字幕。
89
- 4. `audio`: 完成代码音乐、音效、旁白试听和媒体导出管线。
90
- 5. `publishing`: 完成平台文案复制、封面导出和安全区提示。
91
- 6. `qa`: 完成构建检查、交互检查、格式能力提示与最终说明。
92
-
93
- 每个大动作完成后都应提交一次 Git commit,并尝试推送到 `Ethscriptions/pols`。当前执行环境若没有该仓库的 GitHub 写权限,提交会保留在本地并在最终报告中明确标注阻塞点。
94
-
95
- ## 实施状态(2026-10-06)
96
-
97
- - ✅ `plan`:README 先提交,commit `62afb0a`。
98
- - ✅ `scaffold`:Vite 单页工程、Canvas 画布、8 幕时间轴、横竖屏布局,commit `bd7ebaf`。
99
- - ✅ `animation`:猴子 / 梯子 / 香蕉 / 喷水 / 激励函数 / 现实映射 / 结尾提问的程序化动画已接入。
100
- - ✅ `audio`:Web Audio 音乐与音效、浏览器中文旁白试听、可选 Hugging Face TTS 脚本,commit `74e1af7`。
101
- - ✅ `publishing`:B 站 / 抖音标题、描述、标签复制;横屏 / 竖屏封面在浏览器生成并可下载。
102
- - ✅ `export`:原生 MP4 能力探测;不支持时按需使用浏览器内 ffmpeg.wasm 转码,失败才降级 WebM,commit `f021f6e`。
103
- - ✅ `qa`:`npm run build`、`node --check`、`git diff --check` 已通过。
104
- - ⚠️ 推送阻塞:执行环境没有可用的 GitHub 登录凭证;`Ethscriptions/pols` 的匿名访问返回 404,且 `git push` 无法读取用户名。因此没有伪造“已推送”状态,所有提交目前保留在本地。
105
- - ⚠️ 浏览器自动化验收阻塞:Playwright Chromium 已下载,但运行环境缺少 `libglib-2.0.so.0`,所以无法在此环境截取真实浏览器画面;Vite 开发服务器与生产构建均可启动。
106
- - ⚠️ 本次调用 Hugging Face 推理端点时,运行环境 DNS 无法解析 `api-inference.huggingface.co`;因此未把未经验证的外部音频写进仓库。网页仍可用 SpeechSynthesis 旁白和代码合成音乐 / 音效运行。
107
-
108
- ## 本地运行
109
-
110
- ```bash
111
- npm install
112
- npm run dev
113
- ```
114
-
115
- 打开终端给出的本地地址。项目是 Vite 单页应用,所有核心动画在浏览器中渲染。
116
-
117
- 可选:在网络与模型服务可用时,使用 Hugging Face 生成一段中文旁白测试音频(不要把 token 写入源码或提交到 Git):
118
-
119
- ```bash
120
- HF_TOKEN=hf_xxx npm run tts
121
- ```
122
-
123
- 网页默认不依赖这段外部音频,点击“试听”或播放时间轴即可由浏览器本地语音朗读。
124
-
125
- ## 安全说明
126
-
127
- - 不在源码、README、构建产物或日志里保存 Hugging Face token。
128
- - 若需要调用外部模型,请使用本地 `.env`(且 `.gitignore` 已忽略)或本地服务端代理。
129
- - 本仓库不上传视频、语音、用户剪贴板内容或导出文件。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/index-BCSP7C4S.js DELETED
@@ -1,62 +0,0 @@
1
- (function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const a of o)if(a.type==="childList")for(const l of a.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&r(l)}).observe(document,{childList:!0,subtree:!0});function n(o){const a={};return o.integrity&&(a.integrity=o.integrity),o.referrerPolicy&&(a.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?a.credentials="include":o.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(o){if(o.ep)return;o.ep=!0;const a=n(o);fetch(o.href,a)}})();const ht="modulepreload",gt=function(t){return"/"+t},ot={},at=function(e,n,r){let o=Promise.resolve();if(n&&n.length>0){let p=function(u){return Promise.all(u.map(c=>Promise.resolve(c).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};var l=p;document.getElementsByTagName("link");const i=document.querySelector("meta[property=csp-nonce]"),s=i?.nonce||i?.getAttribute("nonce");o=p(n.map(u=>{if(u=gt(u),u in ot)return;ot[u]=!0;const c=u.endsWith(".css"),d=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${u}"]${d}`))return;const f=document.createElement("link");if(f.rel=c?"stylesheet":ht,c||(f.as="script"),f.crossOrigin="",f.href=u,s&&f.setAttribute("nonce",s),document.head.appendChild(f),c)return new Promise((b,m)=>{f.addEventListener("load",b),f.addEventListener("error",()=>m(new Error(`Unable to preload CSS for ${u}`)))})}))}function a(i){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=i,window.dispatchEvent(s),!s.defaultPrevented)throw i}return o.then(i=>{for(const s of i||[])s.status==="rejected"&&a(s.reason);return e().catch(a)})},xt=30,A=168,j=Math.PI*2,M=[{id:"hook",start:0,end:14,chapter:"01 / 钩子",title:"为什么没人敢拿?",kicker:"一个香蕉,五只猴子,零个解释",color:"#f4b33e",voice:"想象一间笼子,里面有五只猴子。笼子顶上挂着一串香蕉。看起来,它只差一把梯子。可是,为什么没有一只猴子去拿?",note:"用一个极简问题,把观众从“看见规则”带到“追问规则”。"},{id:"first",start:14,end:34,chapter:"02 / 第一次尝试",title:"第一个上梯子的人",kicker:"收益在上面,代价在下面",color:"#80d6c5",voice:"第一只猴子爬上梯子。它看见的,是立刻可得的香蕉;其他猴子看见的,却是即将到来的风险。于是,冷水从上面浇下来,所有猴子都被淋透。",note:"用收益 / 代价的对照解释:理性选择会被群体惩罚改写。"},{id:"punish",start:34,end:56,chapter:"03 / 惩罚机制",title:"群体开始自我管理",kicker:"惩罚一次,规则就记住了一半",color:"#ef6b69",voice:"第二次,另一只猴子试着爬梯子。它还没有碰到香蕉,其他猴子就先把它拽了下来。没有人再等喷水,因为惩罚已经被群体内化成了动作。",note:"红色不是“坏人”,而是一个被复制的反馈回路。"},{id:"replacement",start:56,end:78,chapter:"04 / 新成员",title:"新人不知道发生了什么",kicker:"制度会比记忆活得更久",color:"#a98bf2",voice:"现在,换掉一只老猴子,迎来一只新人。新人当然想爬梯子,可刚伸手,就被四只老猴子打了下来。它不懂冷水,却学会了不许碰梯子。",note:"关键转折:规则脱离原始原因,开始靠“别人都这样”存活。"},{id:"loop",start:78,end:100,chapter:"05 / 规则遗忘",title:"最后,没人见过冷水",kicker:"但所有人都说:不能爬",color:"#70a8ff",voice:"继续换人,直到笼子里已经没有一只猴子经历过冷水。此时,大家仍然在阻止别人爬梯子。问原因?答案通常是:一直都是这样。",note:"“一直都是这样”不是解释,它只是制度的缓存。"},{id:"model",start:100,end:124,chapter:"06 / 经济学模型",title:"这不是猴子的问题",kicker:"激励函数被藏起来了",color:"#ffcc67",voice:"把故事翻译成经济学:一个人做选择,会比较收益和代价。当惩罚足够强,最优策略就从“拿香蕉”变成了“阻止别人拿”。后来,惩罚消失了,策略却留下了。",note:"用坐标轴把“看得见的行为”和“看不见的激励”接上。"},{id:"reality",start:124,end:148,chapter:"07 / 现实映射",title:"我们在哪些笼子里?",kicker:"流程、文化、默认选项",color:"#5dd7ff",voice:"现实里,它可能是一条没人能解释的审批流程,一个“新人别碰”的按钮,或者一句“我们公司一直这么做”。制度不是敌人,但不再适用的制度,会把过去的答案伪装成今天的常识。",note:"不把隐喻简单等同于现实;重点是提出可验证的问题。"},{id:"exit",start:148,end:168,chapter:"08 / 结尾",title:"先问一句:冷水还在吗?",kicker:"好的规则经得起追问",color:"#f16eaa",voice:"所以,下次你听到“不能这样做”,先别急着反抗,也别急着服从。问三件事:原来的风险还在吗?代价是谁承担?有没有更低成本的替代方案?规则最危险的时刻,不是被打破,而是再也没人知道它为何存在。",note:"结尾留下行动问题,而不是把观众带到一个简单的“反制度”结论。"}],N={bilibili:{title:"为什么没人敢拿那串香蕉?|经济学里的“收猴子”故事",description:`一个香蕉、五只猴子、没有人记得的冷水。
2
-
3
- 这是一支原创图形化经济学动画:我们用“收猴子的故事”拆解制度惯性、路径依赖与群体激励。故事是常见寓言隐喻,不是对真实动物实验的复述。
4
-
5
- 看完试着问自己:你所在的流程里,有没有一条“大家都说不能碰”,但没人说得清为什么的规则?
6
-
7
- 如果喜欢这种把抽象机制画出来的讲解,欢迎收藏、转发,评论区留下你的“冷水还在吗”。`,tags:"#经济学 #制度 #组织文化 #路径依赖 #财经科普 #动画 #思维方式"},douyin:{title:"为什么没人敢拿香蕉?收猴子的故事讲透制度惯性",description:`没有人见过冷水,但所有人都说:不能爬。
8
-
9
- 用 3 分钟看懂:激励如何变成规则,规则如何变成习惯,以及我们如何把过去的答案带进今天。
10
-
11
- 寓言是隐喻,不是真实实验史料。你身边有没有“不能碰,但说不清为什么”的流程?`,tags:"#经济学 #收猴子的故事 #职场 #认知 #知识 #科普 #制度惯性"}},St=document.querySelector("#app");St.innerHTML=`
12
- <main class="shell">
13
- <header class="topbar">
14
- <div class="brand"><span class="brand-mark">∿</span><div><strong>MONKEY / RULES</strong><span>经济学动画工作台 · 原创讲解</span></div></div>
15
- <div class="top-status"><span class="live-dot"></span><span id="renderStatus">实时画布</span><span class="status-divider"></span><span id="formatStatus">16:9 横屏</span></div>
16
- </header>
17
- <section class="workspace">
18
- <section class="stage-panel">
19
- <div class="stage-shell" id="stageShell">
20
- <canvas id="stage" width="1280" height="720" aria-label="收猴子的故事动画画布"></canvas>
21
- <div class="stage-safe safe-top">安全区</div>
22
- <div class="stage-safe safe-bottom">安全区 · 平台 UI 避让</div>
23
- <div class="stage-hud"><span id="sceneLabel">01 / 钩子</span><span id="timeLabel">00:00 / 02:48</span></div>
24
- </div>
25
- <div class="controls">
26
- <button class="primary-control" id="playButton" type="button"><span class="play-icon">▶</span><span>播放</span></button>
27
- <button class="icon-control" id="restartButton" type="button" aria-label="从头播放">↺</button>
28
- <button class="icon-control" id="muteButton" type="button" aria-label="打开或关闭声音">◖</button>
29
- <div class="scrub"><input id="scrub" type="range" min="0" max="168" step="0.01" value="0" aria-label="时间轴"/><div class="scrub-progress" id="scrubProgress"></div></div>
30
- <div class="frame-rate">30 FPS</div>
31
- </div>
32
- <div class="orientation-row"><span class="control-label">画幅</span><button class="chip active" data-orientation="landscape" type="button">B站 · 16:9</button><button class="chip" data-orientation="portrait" type="button">抖音 · 9:16</button><span class="safe-note">竖屏自动保留刘海 / 手势安全区</span></div>
33
- </section>
34
- <aside class="inspector">
35
- <div class="inspector-head"><div><span class="eyebrow">EDIT / STORYBOARD</span><h1>收猴子的故事</h1></div><span class="duration-badge">02:48</span></div>
36
- <p class="lede">用一个寓言,拆开“大家都这样做”背后的激励结构。</p>
37
- <div class="chapter-list" id="chapterList"></div>
38
- <div class="inspector-card voice-card"><div class="card-title"><span>本幕旁白</span><button id="speakButton" class="text-button" type="button">试听</button></div><p id="voiceText"></p></div>
39
- <div class="inspector-card note-card"><div class="card-title"><span>导演提示</span><span class="tiny-dot"></span></div><p id="directorNote"></p></div>
40
- <button class="export-button" id="exportButton" type="button"><span>↧</span><div><strong>本地导出视频</strong><small>优先 MP4 · 画面与音乐在浏览器内渲染</small></div><span class="export-arrow">→</span></button>
41
- <div class="export-progress" id="exportProgress" hidden><div class="progress-top"><span id="exportLabel">准备导出…</span><span id="exportPercent">0%</span></div><div class="progress-track"><span id="exportBar"></span></div></div>
42
- </aside>
43
- </section>
44
- <section class="publish-section">
45
- <div class="section-heading"><div><span class="eyebrow">PUBLISH KIT</span><h2>发布文案 & 封面</h2></div><span class="section-caption">每个平台一键复制 · 原创封面可下载</span></div>
46
- <div class="publish-grid">
47
- <article class="publish-card"><div class="platform-head"><span class="platform-icon bilibili-icon">◈</span><div><strong>B站横屏</strong><small>标题 / 简介 / 标签</small></div><button class="copy-button" data-copy="bilibili" type="button">复制整套</button></div><label>标题<textarea readonly id="bilibiliTitle"></textarea></label><label>简介<textarea readonly id="bilibiliDescription"></textarea></label><label>标签<textarea readonly id="bilibiliTags"></textarea></label></article>
48
- <article class="publish-card"><div class="platform-head"><span class="platform-icon douyin-icon">♪</span><div><strong>抖音竖屏</strong><small>标题 / 简介 / 标签</small></div><button class="copy-button" data-copy="douyin" type="button">复制整套</button></div><label>标题<textarea readonly id="douyinTitle"></textarea></label><label>简介<textarea readonly id="douyinDescription"></textarea></label><label>标签<textarea readonly id="douyinTags"></textarea></label></article>
49
- <article class="cover-card"><div class="platform-head"><span class="platform-icon cover-icon">✦</span><div><strong>原创封面</strong><small>随画幅生成 · 无外部素材</small></div></div><div class="cover-previews"><div class="cover-preview landscape-cover"><canvas id="landscapeCover" width="640" height="360"></canvas><span>16:9</span></div><div class="cover-preview portrait-cover"><canvas id="portraitCover" width="360" height="640"></canvas><span>9:16</span></div></div><div class="cover-actions"><button id="downloadLandscape" type="button">下载横屏封面</button><button id="downloadPortrait" type="button">下载竖屏封面</button></div></article>
50
- </div>
51
- </section>
52
- <footer class="footer"><span>原创图形化讲解 · 不上传画面与剪贴板内容</span><span>故事为经济学寓言隐喻 · 不是动物实验史料</span></footer>
53
- </main>
54
- `;const R=document.querySelector("#stage"),w=R.getContext("2d"),rt=document.querySelector("#stageShell"),X=document.querySelector("#playButton"),it=document.querySelector("#scrub"),wt=document.querySelector("#scrubProgress"),kt=document.querySelector("#timeLabel"),Tt=document.querySelector("#sceneLabel"),Y=document.querySelector("#chapterList"),Ct=document.querySelector("#voiceText"),Lt=document.querySelector("#directorNote"),lt=document.querySelector("#muteButton"),H=document.querySelector("#renderStatus"),At=document.querySelector("#formatStatus"),z=document.querySelector("#exportButton"),Mt=document.querySelector("#exportProgress"),F=document.querySelector("#exportBar"),E=document.querySelector("#exportLabel"),_=document.querySelector("#exportPercent");let h="landscape",g=0,L=!1,P=!1,U=0,st=0,O=null;const T=(t,e=0,n=1)=>Math.max(e,Math.min(n,t)),ct=(t,e,n)=>t+(e-t)*n,qt=t=>t*t*(3-2*t),C=t=>qt(T(t)),V=t=>`${String(Math.floor(t/60)).padStart(2,"0")}:${String(Math.floor(t%60)).padStart(2,"0")}`,K=(t=g)=>M.findIndex(e=>t>=e.start&&t<e.end)>=0?M.findIndex(e=>t>=e.start&&t<e.end):M.length-1;function J(){const t=h==="portrait";R.width=t?720:1280,R.height=t?1280:720,rt.classList.toggle("portrait",t),rt.classList.toggle("landscape",!t),At.textContent=t?"9:16 竖屏":"16:9 横屏",B(g)}function $(){const t=K(),e=M[t];t!==st&&(L&&!P&&O&&(O.sfx("tick"),ut()),st=t),Tt.textContent=e.chapter,kt.textContent=`${V(g)} / ${V(A)}`,it.value=g.toFixed(2),wt.style.width=`${g/A*100}%`,Ct.textContent=e.voice,Lt.textContent=e.note,[...Y.children].forEach((n,r)=>n.classList.toggle("active",r===t))}function Et(){Y.innerHTML=M.map((t,e)=>`<button class="chapter-item" data-index="${e}" type="button"><span class="chapter-no">${String(e+1).padStart(2,"0")}</span><span class="chapter-copy"><strong>${t.title}</strong><small>${t.chapter.replace(/^\d+ \/ /,"")}</small></span><span class="chapter-time">${V(t.start)}</span></button>`).join(""),Y.querySelectorAll(".chapter-item").forEach(t=>t.addEventListener("click",()=>{g=M[Number(t.dataset.index)].start,$(),B(g)}))}function I(t,e,n,r,o,a){t.beginPath(),t.roundRect(e,n,r,o,a),t.closePath()}function y(t,e,n,r,o,a,l=500,i="left"){t.font=`${l} ${o}px Inter, "Noto Sans SC", "PingFang SC", sans-serif`,t.fillStyle=a,t.textAlign=i,t.textBaseline="middle",t.fillText(e,n,r)}function k(t,e,n,r,o,a=null,l=1){t.beginPath(),t.arc(e,n,r,0,j),o&&(t.fillStyle=o,t.fill()),a&&(t.strokeStyle=a,t.lineWidth=l,t.stroke())}function x(t,e,n,r,o,a,l=1,i=[]){t.beginPath(),t.moveTo(e,n),t.lineTo(r,o),t.strokeStyle=a,t.lineWidth=l,t.setLineDash(i),t.stroke(),t.setLineDash([])}function Pt(t,e,n,r,o,a,l,i,s=500){t.font=`${s} ${l}px Inter, "Noto Sans SC", "PingFang SC", sans-serif`,t.fillStyle=i,t.textAlign="left",t.textBaseline="top";let p="",u=r;for(const c of e){const d=p+c;t.measureText(d).width>o&&p?(t.fillText(p,n,u),u+=a,p=c):p=d}return p&&t.fillText(p,n,u),u+a}function q(t){const e=Math.sin(t*17.17+3.2)*43758.5453;return e-Math.floor(e)}function Rt(t,e,n,r,o){const a=t.createLinearGradient(0,0,e,n);a.addColorStop(0,"#080b16"),a.addColorStop(.55,"#0c1021"),a.addColorStop(1,"#10192c"),t.fillStyle=a,t.fillRect(0,0,e,n),t.save(),t.globalAlpha=.2;for(let s=0;s<70;s++){const p=q(s)*e,u=q(s+99)*n;k(t,p,u,1+q(s+31)*2,"#b4c4ff")}t.globalAlpha=.12,x(t,e*.08,n*.78,e*.92,n*.78,r.color,1),x(t,e*.08,n*.78,e*.08,n*.18,r.color,1),t.restore();const l=t.createRadialGradient(e*.72,n*.2,0,e*.72,n*.2,Math.max(e,n)*.7);l.addColorStop(0,`${r.color}20`),l.addColorStop(1,"transparent"),t.fillStyle=l,t.fillRect(0,0,e,n);const i=o*18%80;t.save(),t.globalAlpha=.05;for(let s=-80;s<e+80;s+=80)x(t,s+i,0,s-200+i,n,"#fff",1);t.restore()}function $t(t,e,n,r,o){const a=e*(h==="portrait"?.09:.07);y(t,"经济学寓言 / ORIGINAL MOTION ESSAY",a,n*(h==="portrait"?.1:.11),h==="portrait"?13:15,"#a4abc2",650);const l=h==="portrait"?108:126;I(t,e-a-l,n*.075,l,28,14),t.fillStyle=`${r.color}1f`,t.fill(),y(t,r.chapter,e-a-l/2,n*.075+14,11,r.color,700,"center");const i=C(Math.min(1,o/1.25));t.save(),t.globalAlpha=i,y(t,r.title,a,n*(h==="portrait"?.19:.21),h==="portrait"?34:42,"#f6f7fb",800),y(t,r.kicker,a,n*(h==="portrait"?.24:.285),h==="portrait"?15:18,r.color,650),t.restore()}function D(t,e,n,r,o="neutral",a="#d9a776",l=0){t.save(),t.translate(e,n),t.scale(r,r);const i=Math.sin(l*4+e*.01)*2;t.translate(0,i),k(t,-25,-40,17,"#b77c5a"),k(t,25,-40,17,"#b77c5a"),k(t,0,-42,30,"#d9a776"),k(t,0,-36,22,"#f0c79d"),k(t,-9,-47,3,"#111827"),k(t,9,-47,3,"#111827"),o==="scared"?x(t,-10,-28,10,-28,"#422f2a",3):(t.beginPath(),t.arc(0,-29,9,0,Math.PI),t.strokeStyle="#422f2a",t.lineWidth=3,t.stroke()),t.strokeStyle=a,t.lineWidth=8,t.lineCap="round",x(t,-25,0,-35,38,a,8),x(t,25,0,35,38,a,8),x(t,-19,4,-42,20,a,8),x(t,19,4,42,20,a,8),t.fillStyle=a,t.beginPath(),t.ellipse(-35,42,12,5,0,0,j),t.fill(),t.beginPath(),t.ellipse(35,42,12,5,0,0,j),t.fill(),t.restore()}function Q(t,e,n,r,o,a="#e7b55c",l=0){t.save(),t.translate(e,n),t.rotate(l),t.strokeStyle=a,t.lineWidth=7,t.lineCap="round",x(t,-o/2,0,-o/2,-r,a,7),x(t,o/2,0,o/2,-r,a,7);for(let i=-20;i>-r+5;i-=27)x(t,-o/2,i,o/2,i,a,5);t.restore()}function Z(t,e,n,r,o=!1){t.save(),t.translate(e,n),t.scale(r,r),o&&(t.globalAlpha=.18,k(t,0,0,72,"#f4b33e"),t.globalAlpha=1),t.strokeStyle="#f4c453",t.lineWidth=20,t.lineCap="round",t.beginPath(),t.arc(0,0,50,-.2,Math.PI*.85),t.stroke(),t.strokeStyle="#cf8b2f",t.lineWidth=6,t.beginPath(),t.arc(0,0,50,-.2,Math.PI*.85),t.stroke(),k(t,54,-7,6,"#a96b29"),k(t,-38,38,6,"#a96b29"),t.restore()}function G(t,e,n,r,o,a=.8,l=0){t.save(),t.globalAlpha=a,t.strokeStyle="#66d5f6",t.lineWidth=3,t.lineCap="round";for(let i=0;i<12;i++){const s=e+i/11*r,p=Math.sin(l*8+i)*8;x(t,s,n,s+p,n+o*(.5+q(i)*.5),"#66d5f6",3),k(t,s+p,n+o*(.5+q(i)*.5),3,"#b5f0ff")}t.restore()}function Bt(t,e,n,r,o){const a=h==="portrait",l=a?e*.5:e*.69,i=a?n*.37:n*.53,s=n*.78,p=C(Math.min(1,o/2));Z(t,l,i-150,a?1.1:1.35,!0),x(t,l,i-120,l,i+6,"#9a6d4d",4),Q(t,l,i+190,300,80,"#e8bd71"),x(t,a?e*.1:e*.25,s,a?e*.9:e*.88,s,"#5b6a89",5);const u=a?3:5;for(let c=0;c<u;c++){const d=a?e*(.24+c*.26):e*(.3+c*.14);D(t,d,s-42,a?.82:.72,"neutral",c%2?"#ce9d75":"#c48762",o)}if(r.id==="hook"&&(t.save(),t.globalAlpha=p,y(t,"这是一道经济学题",a?e*.5:e*.13,a?n*.91:n*.86,a?18:22,"#dce3fa",700,a?"center":"left"),t.restore()),r.id==="first"){const c=C(T(o/7));D(t,l,s-35-c*180,.75,"neutral","#d09a73",o),G(t,l-100,i-60,200,170,C(T((o-7)/2)),o),y(t,"收益",l+115,i-164,16,"#f4c453",700),y(t,"代价",l+120,i+20,16,"#66d5f6",700)}if(r.id==="punish"){for(let c=0;c<3;c++){const d=-1.3+c*1.3;x(t,l+Math.cos(d)*110,s-100,l+Math.cos(d)*70,s-55,"#ef6b69",7)}G(t,l-100,i-40,200,170,.5,o),y(t,"惩罚变成了动作",l,n*.86,18,"#ef8b88",700,"center")}if(r.id==="replacement"){const c=a?e*.22:e*.34;D(t,c,s-45,.82,"scared","#a98bf2",o),k(t,c,s-160,38,"#a98bf220","#a98bf2",2),y(t,"?",c,s-160,28,"#a98bf2",800,"center")}if(r.id==="loop"){G(t,l-100,i-40,200,170,.08,o);for(let c=0;c<3;c++){const d=a?e*(.2+c*.3):e*(.34+c*.12);x(t,d,s-125,d+(c===1?-24:24),s-70,"#70a8ff",6)}y(t,"冷水已经消失",l,n*.86,18,"#8eb9ff",700,"center")}}function Ot(t,e,n,r,o){const a=h==="portrait",l=e*(a?.1:.09),i=a?n*.66:n*.68,s=l,p=e-l*2,u=a?n*.22:n*.28,c=C(T(o/2));t.save(),t.globalAlpha=c,x(t,s,i-u,s,i,"#7784a4",2),x(t,s,i,s+p,i,"#7784a4",2),y(t,"行为成本",s-12,i-u-10,12,"#96a1bb",500,"right"),y(t,"行动:拿香蕉 →",s+p,i+25,12,"#96a1bb",500,"right");const d=s+p*.54;t.beginPath(),t.moveTo(s,i-u*.18),t.bezierCurveTo(s+p*.18,i-u*.2,d-40,i-u*.2,d,i-u*.85),t.bezierCurveTo(d+50,i-u*.9,s+p*.8,i-u*.1,s+p,i-u*.08),t.strokeStyle=r.color,t.lineWidth=5,t.stroke();const f=ct(s+p*.24,s+p*.7,C(T(o/8))),b=i-u*.2-C(T(o/8))*u*.55;k(t,f,b,10,"#fff",r.color,5),I(t,f-74,b-45,150,28,14),t.fillStyle="#18213a",t.fill(),y(t,o<8?"收益被看见":"惩罚被内化",f,b-31,12,"#f5f7ff",650,"center"),t.restore();const m=a?e*.5:e*.75,v=a?n*.36:n*.45;Q(t,m,v+90,130,42,"#e3b466",Math.sin(o)*.02),Z(t,m,v-10,.6,!0),y(t,"看得见的行为",m,v+140,14,"#dce3fa",600,"center")}function Ut(t,e,n,r,o){const a=h==="portrait",l=[["流程",`审批三次
55
- 因为一直这样`],["按钮",`新人别碰
56
- 没人解释`],["文化",`默认选项
57
- 变成常识`]],i=a?n*.38:n*.48,s=a?e*.29:e*.25,p=a?e*.19:e*.2;l.forEach((u,c)=>{const d=p+c*s,f=C(T((o-c*.8)/1.5));if(t.save(),t.globalAlpha=f,t.translate(d,i+(1-f)*30),I(t,d-70,i-70,140,140,18),t.fillStyle="#17213a",t.fill(),t.strokeStyle=c===1?r.color:"#334365",t.lineWidth=2,t.stroke(),k(t,d,i-32,18,`${r.color}2c`,r.color,2),y(t,c===0?"≡":c===1?"□":"↻",d,i-32,20,r.color,800,"center"),y(t,u[0],d,i+10,17,"#f5f7ff",700,"center"),u[1].split(`
58
- `).forEach((b,m)=>y(t,b,d,i+38+m*18,11,"#aeb8d2",500,"center")),t.restore(),c<2){const b=C(T((o-1.2-c*.8)/1.2));x(t,d+82,i,d+s-82,i,`${r.color}88`,3),y(t,"→",d+s-72,i-3,24,r.color,700,"center"),b>0&&k(t,ct(d+82,d+s-82,b),i,5,r.color)}}),y(t,"过去的风险 → 今天的默认",e*.5,a?n*.72:n*.77,a?20:24,r.color,750,"center")}function Vt(t,e,n,r,o){const a=h==="portrait",l=e*.5,i=a?n*.48:n*.5,s=C(T(o/2)),p=(a?e:n)*(.12+C(T(o/9))*.18);t.save(),t.globalAlpha=s,t.strokeStyle=`${r.color}99`,t.lineWidth=2;for(let c=0;c<3;c++)t.beginPath(),t.arc(l,i,p+c*24,0,j),t.stroke();k(t,l,i,p*.56,"#15233a",r.color,3),y(t,"问",l,i-5,a?44:56,"#f7f4ed",800,"center"),y(t,"冷水还在吗?",l,i+p+60,a?24:32,r.color,750,"center"),t.restore(),["风险还在吗?","代价谁承担?","有更低成本的方案吗?"].forEach((c,d)=>{const f=a?n*(.72+d*.06):n*(.76+(d-1)*.08),b=C(T((o-2-d*.7)/1.2));t.save(),t.globalAlpha=b,I(t,l-(a?135:150),f-18,a?270:300,36,18),t.fillStyle="#18213a",t.fill(),y(t,c,l,f,a?14:16,"#dbe6ff",650,"center"),t.restore()})}function B(t=g){const e=R.width,n=R.height,r=K(t),o=M[r],a=t-o.start;w.clearRect(0,0,e,n),Rt(w,e,n,o,t),$t(w,e,n,o,a),["hook","first","punish","replacement","loop"].includes(o.id)?Bt(w,e,n,o,a):o.id==="model"?Ot(w,e,n,o,a):o.id==="reality"?Ut(w,e,n,o,a):Vt(w,e,n,o,a);const l=e*(h==="portrait"?.09:.07),i=o.voice,s=h==="portrait"?n*.87:n*.91;w.save(),w.globalAlpha=.93,I(w,l,s-28,e-l*2,h==="portrait"?76:58,16),w.fillStyle="#090d1ddd",w.fill(),w.strokeStyle=`${o.color}38`,w.lineWidth=1,w.stroke(),Pt(w,i,l+18,s-17,e-l*2-36,h==="portrait"?20:18,h==="portrait"?13:14,"#e6ebfb",500),w.restore()}function dt(t){U||(U=t);const e=Math.min(.05,(t-U)/1e3);U=t,L&&(g+=e,g>=A&&(g=0,L=!1,X.innerHTML='<span class="play-icon">▶</span><span>播放</span>'),$(),B(g)),requestAnimationFrame(dt)}function pt(t){L=t,U=0,X.innerHTML=L?'<span class="play-icon">Ⅱ</span><span>暂停</span>':'<span class="play-icon">▶</span><span>播放</span>',H.textContent=L?"实时播放":"实时画布",L&&!P&&tt().startPreview()}function tt(){if(O)return O;const t=new(window.AudioContext||window.webkitAudioContext),e=t.createGain();e.gain.value=.18,e.connect(t.destination);const n=t.createMediaStreamDestination();e.connect(n);let r=!1;function o(s="tick",p=t.currentTime){const u=t.createOscillator(),c=t.createGain(),d=s==="spray"?760:s==="drop"?180:420;u.type=s==="spray"?"triangle":"sine",u.frequency.setValueAtTime(d,p),u.frequency.exponentialRampToValueAtTime(d*.55,p+(s==="spray"?.25:.12)),c.gain.setValueAtTime(1e-4,p),c.gain.exponentialRampToValueAtTime(s==="spray"?.15:.09,p+.012),c.gain.exponentialRampToValueAtTime(1e-4,p+(s==="spray"?.28:.16)),u.connect(c).connect(e),u.start(p),u.stop(p+.32)}function a(s=t.currentTime+.05,p=0,u=A){const c=[55,65.41,73.42,82.41,73.42,65.41,49,65.41];for(let d=0;d<Math.ceil(u/1.5);d++){const f=s+d*1.5,b=c[(d+p)%c.length],m=t.createOscillator(),v=t.createGain();m.type=d%4===0?"triangle":"sine",m.frequency.setValueAtTime(b,f),v.gain.setValueAtTime(1e-4,f),v.gain.linearRampToValueAtTime(d%4===0?.055:.025,f+.08),v.gain.exponentialRampToValueAtTime(1e-4,f+1.32),m.connect(v).connect(e),m.start(f),m.stop(f+1.35),d%4===0&&o("drop",f)}}function l(){t.state==="suspended"&&t.resume(),r||(a(),r=!0)}async function i(){const s=new(window.AudioContext||window.webkitAudioContext),p=s.createGain();p.gain.value=.2,p.connect(s.destination);const u=s.createMediaStreamDestination();p.connect(u);const c=[55,65.41,73.42,82.41,73.42,65.41,49,65.41],d=s.currentTime+.08;for(let f=0;f<Math.ceil(A/1.5);f++){const b=d+f*1.5,m=s.createOscillator(),v=s.createGain();m.type=f%4===0?"triangle":"sine",m.frequency.setValueAtTime(c[f%c.length],b),v.gain.setValueAtTime(1e-4,b),v.gain.linearRampToValueAtTime(f%4===0?.055:.025,b+.08),v.gain.exponentialRampToValueAtTime(1e-4,b+1.32),m.connect(v).connect(p),m.start(b),m.stop(b+1.35)}M.forEach((f,b)=>{const m=d+f.start,v=s.createOscillator(),S=s.createGain();v.type="sine",v.frequency.setValueAtTime(b%2?640:420,m),S.gain.setValueAtTime(1e-4,m),S.gain.linearRampToValueAtTime(.04,m+.02),S.gain.exponentialRampToValueAtTime(1e-4,m+.15),v.connect(S).connect(p),v.start(m),v.stop(m+.18)});try{const f=await fetch("/audio/narration.wav",{cache:"no-store"});if(f.ok){const b=await f.arrayBuffer(),m=await s.decodeAudioData(b),v=s.createBufferSource();v.buffer=m,v.connect(p),v.start(d)}}catch(f){console.info("Optional narration asset not found; exporting captions, music and SFX.",f)}return{context:s,dest:u,startAt:d}}return O={ac:t,master:e,startPreview:l,sfx:o,prepareExport:i,toggle(){P=!P,e.gain.value=P?0:.18,P&&"speechSynthesis"in window&&window.speechSynthesis.cancel(),lt.textContent=P?"⊘":"◖"}},O}async function ut(){if(!("speechSynthesis"in window))return;window.speechSynthesis.cancel();const t=new SpeechSynthesisUtterance(M[K()].voice);t.lang="zh-CN",t.rate=.96,t.pitch=1.03,window.speechSynthesis.speak(t)}function It(){return['video/mp4;codecs="avc1.42E01E,mp4a.40.2"',"video/mp4","video/webm;codecs=vp9,opus","video/webm"].find(e=>window.MediaRecorder?.isTypeSupported?.(e))||""}async function Wt(t){const e=await at(()=>import("https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.12.10/+esm"),[]),n=await at(()=>import("https://cdn.jsdelivr.net/npm/@ffmpeg/util@0.12.1/+esm"),[]),r=new e.FFmpeg;r.on("progress",({progress:l})=>{const i=Math.round(100*T(l));E.textContent=`本地转码 MP4 · ${i}%`,_.textContent=`${i}%`,F.style.width=`${i}%`});const o="https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd";await r.load({coreURL:await n.toBlobURL(`${o}/ffmpeg-core.js`,"text/javascript"),wasmURL:await n.toBlobURL(`${o}/ffmpeg-core.wasm`,"application/wasm")}),await r.writeFile("input.webm",await n.fetchFile(t)),await r.exec(["-i","input.webm","-c:v","libx264","-c:a","aac","-pix_fmt","yuv420p","-movflags","faststart","output.mp4"]);const a=await r.readFile("output.mp4");return await r.deleteFile("input.webm"),await r.deleteFile("output.mp4"),new Blob([a.buffer],{type:"video/mp4"})}async function Nt(){if(!window.MediaRecorder||!R.captureStream){E.textContent="当前浏览器不支持画布录制";return}z.disabled=!0,Mt.hidden=!1,F.style.width="0%",_.textContent="0%",E.textContent="正在准备本地音轨…";const t=await tt().prepareExport(),e=R.captureStream(xt),n=new MediaStream([...e.getVideoTracks(),...t.dest.stream.getAudioTracks()]),r=It(),o=[],a=new MediaRecorder(n,r?{mimeType:r,videoBitsPerSecond:7e6}:void 0),l=performance.now(),i=g,s=L,p=h;a.ondataavailable=S=>{S.data.size&&o.push(S.data)};const u=new Promise(S=>{a.onstop=S});a.start(250),H.textContent="正在导出",await new Promise(S=>{performance.now();function et(yt){const W=Math.min(A,(yt-l)/1e3);g=W,B(g),$();const nt=Math.round(W/A*100);F.style.width=`${nt}%`,_.textContent=`${nt}%`,E.textContent=`渲染第 ${V(W)} / ${V(A)} 帧`,W>=A?(a.stop(),S()):requestAnimationFrame(et)}requestAnimationFrame(et)}),await u,e.getTracks().forEach(S=>S.stop()),t.dest.stream.getTracks().forEach(S=>S.stop()),await t.context.close();const c=new Blob(o,{type:r||"video/webm"});let d=c,f=(r||"").includes("mp4");if(!f){E.textContent="浏览器原生不支持 MP4,准备本地转码…";try{d=await Wt(c),f=!0}catch(S){console.warn("Local MP4 conversion unavailable; keeping WebM fallback.",S),E.textContent="本地 MP4 转码不可用,保留兼容 WebM"}}const b=f?"mp4":"webm",m=URL.createObjectURL(d),v=document.createElement("a");v.href=m,v.download=`收猴子的故事-${h==="portrait"?"抖音竖屏":"B站横屏"}.${b}`,v.click(),setTimeout(()=>URL.revokeObjectURL(m),2e3),g=i,L=s,h=p,J(),$(),F.style.width="100%",_.textContent="完成",E.textContent=f?"MP4 已保存到下载目录":"当前浏览器不支持 MP4,已保存兼容 WebM",H.textContent="实时画布",z.disabled=!1}function Ft(){for(const t of["bilibili","douyin"])document.querySelector(`#${t}Title`).value=N[t].title,document.querySelector(`#${t}Description`).value=N[t].description,document.querySelector(`#${t}Tags`).value=N[t].tags}function _t(t,e){const n=N[t],r=`标题:${n.title}
59
-
60
- 简介:${n.description}
61
-
62
- 标签:${n.tags}`;navigator.clipboard?.writeText(r).then(()=>{const o=e.textContent;e.textContent="已复制",setTimeout(()=>e.textContent=o,1600)}).catch(()=>{const o=document.createElement("textarea");o.value=r,document.body.appendChild(o),o.select(),document.execCommand("copy"),o.remove(),e.textContent="已复制",setTimeout(()=>e.textContent="复制整套",1600)})}function ft(t,e){const n=t.getContext("2d"),r=t.width,o=t.height,a=n.createLinearGradient(0,0,r,o);a.addColorStop(0,"#080b16"),a.addColorStop(1,"#19213d"),n.fillStyle=a,n.fillRect(0,0,r,o);for(let s=0;s<22;s++)k(n,q(s)*r,q(s+32)*o,1+q(s+12)*3,"#a8b9ff55");const l=e?r*.5:r*.7,i=e?o*.72:o*.78;Z(n,l,e?o*.22:o*.3,e?.72:.9,!0),Q(n,l,i-40,e?230:180,e?54:46,"#e8bd71");for(let s=0;s<(e?3:5);s++)D(n,e?r*(.25+s*.25):r*(.3+s*.14),i,e?.5:.44,"neutral","#c9946d",0);y(n,"收猴子的故事",e?r*.1:r*.08,o*.82,e?25:28,"#f7f4ed",800),y(n,"为什么没人敢拿?",e?r*.1:r*.08,e?o*.88:o*.9,e?15:17,"#f4b33e",650)}function mt(t,e){t.toBlob(n=>{const r=document.createElement("a");r.href=URL.createObjectURL(n),r.download=e,r.click(),setTimeout(()=>URL.revokeObjectURL(r.href),1e3)},"image/png")}X.addEventListener("click",()=>pt(!L));document.querySelector("#restartButton").addEventListener("click",()=>{g=0,pt(!0),$(),B(g)});document.querySelector("#speakButton").addEventListener("click",ut);lt.addEventListener("click",()=>tt().toggle());it.addEventListener("input",t=>{g=Number(t.target.value),$(),B(g)});document.querySelectorAll("[data-orientation]").forEach(t=>t.addEventListener("click",()=>{h=t.dataset.orientation,document.querySelectorAll("[data-orientation]").forEach(e=>e.classList.toggle("active",e===t)),J()}));z.addEventListener("click",Nt);document.querySelectorAll(".copy-button").forEach(t=>t.addEventListener("click",()=>_t(t.dataset.copy,t)));const bt=document.querySelector("#landscapeCover"),vt=document.querySelector("#portraitCover");ft(bt,!1);ft(vt,!0);document.querySelector("#downloadLandscape").addEventListener("click",()=>mt(bt,"收猴子的故事-横屏封面.png"));document.querySelector("#downloadPortrait").addEventListener("click",()=>mt(vt,"收猴子的故事-竖屏封面.png"));Et();Ft();$();J();B(0);requestAnimationFrame(dt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/index-C7-Og2To.css DELETED
@@ -1 +0,0 @@
1
- @import"https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;600;700;800&display=swap";:root{color-scheme:dark;font-family:Inter,Noto Sans SC,system-ui,sans-serif;background:#080a12;color:#f4f6fb;font-synthesis:none;--bg: #080a12;--panel: #0e1220;--panel-2: #11172a;--stroke: #202941;--muted: #8f9ab6;--muted-2: #65718e;--yellow: #f4b33e;--cyan: #71d6c5}*{box-sizing:border-box}body{margin:0;min-width:320px;background:radial-gradient(circle at 20% 0%,#12182a 0%,var(--bg) 42%)}button,textarea,input{font:inherit}button{cursor:pointer}button:disabled{cursor:wait;opacity:.6}.shell{width:min(1600px,100%);margin:0 auto;padding:0 34px 38px}.topbar{height:76px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #1a2236}.brand{display:flex;align-items:center;gap:12px}.brand-mark{display:grid;place-items:center;width:35px;height:35px;border-radius:12px;background:#f4b33e;color:#111522;font-size:25px;font-weight:800;transform:rotate(-9deg)}.brand strong{display:block;letter-spacing:.12em;font-size:12px}.brand span:not(.brand-mark){display:block;color:var(--muted);font-size:11px;margin-top:3px}.top-status{display:flex;align-items:center;color:#98a4c1;font:500 11px DM Mono,monospace;letter-spacing:.04em}.live-dot{width:7px;height:7px;border-radius:50%;background:#68ddb1;box-shadow:0 0 0 4px #68ddb11c;margin-right:8px}.status-divider{width:1px;height:16px;background:#2a3550;margin:0 13px}.workspace{display:grid;grid-template-columns:minmax(0,1.54fr) minmax(350px,.76fr);gap:28px;padding-top:30px}.stage-panel{min-width:0}.stage-shell{position:relative;width:100%;aspect-ratio:16 / 9;overflow:hidden;border-radius:17px;background:#080b16;border:1px solid #2a3552;box-shadow:0 24px 80px #0008,inset 0 1px #ffffff0e}.stage-shell.portrait{width:min(100%,500px);aspect-ratio:9 / 16;margin:0 auto}#stage{display:block;width:100%;height:100%}.stage-hud{position:absolute;left:18px;right:18px;top:14px;display:flex;justify-content:space-between;color:#d3d9ebcc;font:500 10px DM Mono,monospace;letter-spacing:.04em;pointer-events:none}.stage-safe{position:absolute;left:10%;right:10%;color:#fff2;font:400 8px DM Mono,monospace;pointer-events:none;opacity:0;transition:opacity .2s}.stage-shell.portrait .stage-safe{opacity:1}.safe-top{top:5%;border-top:1px dashed #ffffff26;padding-top:4px}.safe-bottom{bottom:5%;border-bottom:1px dashed #ffffff26;padding-bottom:4px;text-align:right}.controls{display:flex;align-items:center;gap:10px;padding:14px 0 10px}.primary-control,.icon-control,.chip,.copy-button,.cover-actions button{border:1px solid #2b3754;color:#dfe6fb;background:#11182a;border-radius:8px}.primary-control{height:34px;padding:0 13px;display:flex;align-items:center;gap:8px;background:#f4b33e;color:#151a27;border-color:#f4b33e;font-weight:750;font-size:12px}.play-icon{font-size:11px}.icon-control{width:34px;height:34px;font-size:17px;color:#acb8d0}.primary-control:hover,.icon-control:hover,.chip:hover,.copy-button:hover,.cover-actions button:hover{filter:brightness(1.17)}.scrub{position:relative;flex:1;height:24px;display:flex;align-items:center}.scrub input{position:relative;z-index:2;width:100%;appearance:none;background:transparent}.scrub input::-webkit-slider-runnable-track{height:3px;background:#2a3550;border-radius:4px}.scrub input::-webkit-slider-thumb{appearance:none;width:11px;height:11px;margin-top:-4px;border-radius:50%;background:#f4b33e;border:2px solid #f8d98e;box-shadow:0 0 0 3px #f4b33e2a}.scrub-progress{position:absolute;z-index:1;left:0;top:10px;width:0;height:3px;background:#f4b33e;border-radius:4px;pointer-events:none}.frame-rate{color:#687492;font:500 10px DM Mono,monospace}.orientation-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;color:var(--muted)}.control-label{font-size:11px;margin-right:4px}.chip{height:28px;padding:0 10px;color:#96a2be;font-size:11px}.chip.active{color:#f7d891;border-color:#a2742d;background:#f4b33e17}.safe-note{color:#596783;font-size:10px;margin-left:5px}.inspector{border-left:1px solid #202a43;padding-left:28px;min-width:0}.inspector-head{display:flex;justify-content:space-between;align-items:flex-start}.eyebrow{display:block;color:#697693;font:500 10px DM Mono,monospace;letter-spacing:.12em}h1{font-size:clamp(24px,3vw,36px);line-height:1.1;letter-spacing:-.04em;margin:8px 0 0}.duration-badge{border:1px solid #2c3854;color:#aeb9d4;padding:6px 9px;border-radius:7px;font:500 10px DM Mono,monospace}.lede{color:#99a5bf;line-height:1.65;margin:13px 0 20px;font-size:13px;max-width:390px}.chapter-list{display:flex;flex-direction:column;gap:3px;max-height:380px;overflow:auto;padding-right:4px}.chapter-item{display:grid;grid-template-columns:28px 1fr auto;gap:9px;align-items:center;text-align:left;width:100%;min-height:44px;padding:7px 8px;border:1px solid transparent;color:#99a5c0;background:transparent;border-radius:9px}.chapter-item:hover{background:#151d31}.chapter-item.active{color:#f5f7fd;background:#171f34;border-color:#2d3b5e;box-shadow:inset 3px 0 var(--yellow)}.chapter-no{font:500 10px DM Mono,monospace;color:#63708c}.chapter-item.active .chapter-no{color:var(--yellow)}.chapter-copy strong{display:block;font-size:12px;font-weight:650}.chapter-copy small{display:block;color:#65718b;font-size:10px;margin-top:3px}.chapter-time{color:#697590;font:500 10px DM Mono,monospace}.inspector-card{border-top:1px solid #222d47;margin-top:20px;padding-top:15px}.card-title{display:flex;align-items:center;justify-content:space-between;color:#8190ae;font:500 10px DM Mono,monospace;letter-spacing:.07em;text-transform:uppercase}.text-button{border:0;background:transparent;color:#f4b33e;font-size:11px;padding:0}.voice-card p,.note-card p{color:#c1c9dc;font-size:12px;line-height:1.7;margin:9px 0 0}.note-card p{color:#8491ad}.tiny-dot{width:5px;height:5px;border-radius:50%;background:#68ddb1}.export-button{width:100%;margin-top:22px;min-height:57px;border:1px solid #73531f;border-radius:10px;background:linear-gradient(110deg,#f4b33e,#f2c766);color:#171b28;display:grid;grid-template-columns:24px 1fr 20px;align-items:center;text-align:left;padding:9px 13px}.export-button>span:first-child{font-size:22px;font-weight:800}.export-button strong,.export-button small{display:block}.export-button strong{font-size:12px}.export-button small{font-size:10px;margin-top:3px;opacity:.68}.export-arrow{font-size:18px;text-align:right}.export-progress{margin-top:10px;padding:10px 2px 0}.progress-top{display:flex;justify-content:space-between;color:#9aa6c0;font:500 10px DM Mono,monospace}.progress-track{height:4px;margin-top:8px;background:#222c45;border-radius:4px;overflow:hidden}.progress-track span{display:block;height:100%;width:0;background:var(--yellow);transition:width .1s}.publish-section{margin-top:56px;border-top:1px solid #1d263c;padding-top:25px}.section-heading{display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:18px}h2{font-size:22px;letter-spacing:-.04em;margin:7px 0 0}.section-caption{color:#687593;font-size:11px}.publish-grid{display:grid;grid-template-columns:1fr 1fr 1.05fr;gap:14px}.publish-card,.cover-card{border:1px solid #212c45;border-radius:12px;background:#0e1424;padding:15px}.platform-head{display:flex;align-items:center;gap:9px;margin-bottom:14px}.platform-head>div:not(.cover-previews){flex:1}.platform-head strong,.platform-head small{display:block}.platform-head strong{font-size:12px}.platform-head small{color:#6f7b97;font-size:10px;margin-top:3px}.platform-icon{display:grid;place-items:center;width:28px;height:28px;border-radius:8px;font-size:16px}.bilibili-icon{background:#73d7eb20;color:#73d7eb}.douyin-icon{background:#f06fa820;color:#f59bc5}.cover-icon{background:#f4b33e20;color:#f4b33e}.copy-button{font-size:10px;padding:7px 9px;color:#e4b759;border-color:#684f25;background:#2a2114;white-space:nowrap}.publish-card label{display:block;color:#6f7c98;font:500 9px DM Mono,monospace;margin-top:11px}.publish-card textarea{resize:vertical;width:100%;min-height:34px;border:1px solid #202b45;border-radius:7px;background:#0a0f1d;color:#c4ccde;padding:8px;margin-top:5px;line-height:1.55;font:400 11px Noto Sans SC,sans-serif}.publish-card label:nth-of-type(2) textarea{min-height:110px}.cover-previews{display:flex;gap:10px;align-items:center;height:185px}.cover-preview{position:relative;background:#090d18;border:1px solid #29344f;border-radius:8px;overflow:hidden}.cover-preview canvas{display:block;width:100%;height:100%}.landscape-cover{width:66%;aspect-ratio:16 / 9}.portrait-cover{width:30%;height:100%}.cover-preview>span{position:absolute;left:7px;bottom:6px;font:500 9px DM Mono,monospace;color:#d9e2f7;background:#080b16aa;padding:3px 5px;border-radius:4px}.cover-actions{display:flex;gap:8px;margin-top:12px}.cover-actions button{flex:1;padding:8px 4px;font-size:10px;color:#aab6d0}.footer{display:flex;justify-content:space-between;color:#56627d;font:500 10px DM Mono,monospace;margin-top:26px;padding-top:18px;border-top:1px solid #1a2338}@media(max-width:1080px){.workspace{grid-template-columns:1fr}.inspector{border-left:0;border-top:1px solid #202a43;padding:26px 0 0}.chapter-list{display:grid;grid-template-columns:repeat(2,1fr);max-height:none}.publish-grid{grid-template-columns:1fr 1fr}.cover-card{grid-column:1 / -1}}@media(max-width:670px){.shell{padding:0 15px 28px}.topbar{height:64px}.top-status{display:none}.workspace{padding-top:18px;gap:20px}.stage-shell{border-radius:11px}.controls{gap:6px}.frame-rate{display:none}.safe-note{width:100%;margin:4px 0 0 52px}.chapter-list{grid-template-columns:1fr}.publish-section{margin-top:35px}.section-heading{display:block}.section-caption{display:block;margin-top:8px}.publish-grid{grid-template-columns:1fr}.cover-card{grid-column:auto}.footer{display:block;line-height:1.8}.footer span{display:block}}
 
 
index.html DELETED
@@ -1,15 +0,0 @@
1
- <!doctype html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
- <meta name="theme-color" content="#090b15" />
7
- <meta name="description" content="收猴子的故事——经济学动画工作台" />
8
- <title>收猴子的故事 · 经济学动画工作台</title>
9
- <script type="module" crossorigin src="/assets/index-BCSP7C4S.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-C7-Og2To.css">
11
- </head>
12
- <body>
13
- <div id="app"></div>
14
- </body>
15
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.source.html DELETED
@@ -1,14 +0,0 @@
1
- <!doctype html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
- <meta name="theme-color" content="#090b15" />
7
- <meta name="description" content="收猴子的故事——经济学动画工作台" />
8
- <title>收猴子的故事 · 经济学动画工作台</title>
9
- </head>
10
- <body>
11
- <div id="app"></div>
12
- <script type="module" src="/src/main.js"></script>
13
- </body>
14
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package-lock.json DELETED
@@ -1,1122 +0,0 @@
1
- {
2
- "name": "monkey-story-motion",
3
- "version": "1.0.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "monkey-story-motion",
9
- "version": "1.0.0",
10
- "devDependencies": {
11
- "vite": "^7.1.7"
12
- }
13
- },
14
- "node_modules/@esbuild/aix-ppc64": {
15
- "version": "0.28.2",
16
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
17
- "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
18
- "cpu": [
19
- "ppc64"
20
- ],
21
- "dev": true,
22
- "license": "MIT",
23
- "optional": true,
24
- "os": [
25
- "aix"
26
- ],
27
- "engines": {
28
- "node": ">=18"
29
- }
30
- },
31
- "node_modules/@esbuild/android-arm": {
32
- "version": "0.28.2",
33
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
34
- "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
35
- "cpu": [
36
- "arm"
37
- ],
38
- "dev": true,
39
- "license": "MIT",
40
- "optional": true,
41
- "os": [
42
- "android"
43
- ],
44
- "engines": {
45
- "node": ">=18"
46
- }
47
- },
48
- "node_modules/@esbuild/android-arm64": {
49
- "version": "0.28.2",
50
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
51
- "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
52
- "cpu": [
53
- "arm64"
54
- ],
55
- "dev": true,
56
- "license": "MIT",
57
- "optional": true,
58
- "os": [
59
- "android"
60
- ],
61
- "engines": {
62
- "node": ">=18"
63
- }
64
- },
65
- "node_modules/@esbuild/android-x64": {
66
- "version": "0.28.2",
67
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
68
- "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
69
- "cpu": [
70
- "x64"
71
- ],
72
- "dev": true,
73
- "license": "MIT",
74
- "optional": true,
75
- "os": [
76
- "android"
77
- ],
78
- "engines": {
79
- "node": ">=18"
80
- }
81
- },
82
- "node_modules/@esbuild/darwin-arm64": {
83
- "version": "0.28.2",
84
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
85
- "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
86
- "cpu": [
87
- "arm64"
88
- ],
89
- "dev": true,
90
- "license": "MIT",
91
- "optional": true,
92
- "os": [
93
- "darwin"
94
- ],
95
- "engines": {
96
- "node": ">=18"
97
- }
98
- },
99
- "node_modules/@esbuild/darwin-x64": {
100
- "version": "0.28.2",
101
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
102
- "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
103
- "cpu": [
104
- "x64"
105
- ],
106
- "dev": true,
107
- "license": "MIT",
108
- "optional": true,
109
- "os": [
110
- "darwin"
111
- ],
112
- "engines": {
113
- "node": ">=18"
114
- }
115
- },
116
- "node_modules/@esbuild/freebsd-arm64": {
117
- "version": "0.28.2",
118
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
119
- "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
120
- "cpu": [
121
- "arm64"
122
- ],
123
- "dev": true,
124
- "license": "MIT",
125
- "optional": true,
126
- "os": [
127
- "freebsd"
128
- ],
129
- "engines": {
130
- "node": ">=18"
131
- }
132
- },
133
- "node_modules/@esbuild/freebsd-x64": {
134
- "version": "0.28.2",
135
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
136
- "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
137
- "cpu": [
138
- "x64"
139
- ],
140
- "dev": true,
141
- "license": "MIT",
142
- "optional": true,
143
- "os": [
144
- "freebsd"
145
- ],
146
- "engines": {
147
- "node": ">=18"
148
- }
149
- },
150
- "node_modules/@esbuild/linux-arm": {
151
- "version": "0.28.2",
152
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
153
- "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
154
- "cpu": [
155
- "arm"
156
- ],
157
- "dev": true,
158
- "license": "MIT",
159
- "optional": true,
160
- "os": [
161
- "linux"
162
- ],
163
- "engines": {
164
- "node": ">=18"
165
- }
166
- },
167
- "node_modules/@esbuild/linux-arm64": {
168
- "version": "0.28.2",
169
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
170
- "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
171
- "cpu": [
172
- "arm64"
173
- ],
174
- "dev": true,
175
- "license": "MIT",
176
- "optional": true,
177
- "os": [
178
- "linux"
179
- ],
180
- "engines": {
181
- "node": ">=18"
182
- }
183
- },
184
- "node_modules/@esbuild/linux-ia32": {
185
- "version": "0.28.2",
186
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
187
- "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
188
- "cpu": [
189
- "ia32"
190
- ],
191
- "dev": true,
192
- "license": "MIT",
193
- "optional": true,
194
- "os": [
195
- "linux"
196
- ],
197
- "engines": {
198
- "node": ">=18"
199
- }
200
- },
201
- "node_modules/@esbuild/linux-loong64": {
202
- "version": "0.28.2",
203
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
204
- "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
205
- "cpu": [
206
- "loong64"
207
- ],
208
- "dev": true,
209
- "license": "MIT",
210
- "optional": true,
211
- "os": [
212
- "linux"
213
- ],
214
- "engines": {
215
- "node": ">=18"
216
- }
217
- },
218
- "node_modules/@esbuild/linux-mips64el": {
219
- "version": "0.28.2",
220
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
221
- "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
222
- "cpu": [
223
- "mips64el"
224
- ],
225
- "dev": true,
226
- "license": "MIT",
227
- "optional": true,
228
- "os": [
229
- "linux"
230
- ],
231
- "engines": {
232
- "node": ">=18"
233
- }
234
- },
235
- "node_modules/@esbuild/linux-ppc64": {
236
- "version": "0.28.2",
237
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
238
- "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
239
- "cpu": [
240
- "ppc64"
241
- ],
242
- "dev": true,
243
- "license": "MIT",
244
- "optional": true,
245
- "os": [
246
- "linux"
247
- ],
248
- "engines": {
249
- "node": ">=18"
250
- }
251
- },
252
- "node_modules/@esbuild/linux-riscv64": {
253
- "version": "0.28.2",
254
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
255
- "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
256
- "cpu": [
257
- "riscv64"
258
- ],
259
- "dev": true,
260
- "license": "MIT",
261
- "optional": true,
262
- "os": [
263
- "linux"
264
- ],
265
- "engines": {
266
- "node": ">=18"
267
- }
268
- },
269
- "node_modules/@esbuild/linux-s390x": {
270
- "version": "0.28.2",
271
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
272
- "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
273
- "cpu": [
274
- "s390x"
275
- ],
276
- "dev": true,
277
- "license": "MIT",
278
- "optional": true,
279
- "os": [
280
- "linux"
281
- ],
282
- "engines": {
283
- "node": ">=18"
284
- }
285
- },
286
- "node_modules/@esbuild/linux-x64": {
287
- "version": "0.28.2",
288
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
289
- "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
290
- "cpu": [
291
- "x64"
292
- ],
293
- "dev": true,
294
- "license": "MIT",
295
- "optional": true,
296
- "os": [
297
- "linux"
298
- ],
299
- "engines": {
300
- "node": ">=18"
301
- }
302
- },
303
- "node_modules/@esbuild/netbsd-arm64": {
304
- "version": "0.28.2",
305
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
306
- "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
307
- "cpu": [
308
- "arm64"
309
- ],
310
- "dev": true,
311
- "license": "MIT",
312
- "optional": true,
313
- "os": [
314
- "netbsd"
315
- ],
316
- "engines": {
317
- "node": ">=18"
318
- }
319
- },
320
- "node_modules/@esbuild/netbsd-x64": {
321
- "version": "0.28.2",
322
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
323
- "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
324
- "cpu": [
325
- "x64"
326
- ],
327
- "dev": true,
328
- "license": "MIT",
329
- "optional": true,
330
- "os": [
331
- "netbsd"
332
- ],
333
- "engines": {
334
- "node": ">=18"
335
- }
336
- },
337
- "node_modules/@esbuild/openbsd-arm64": {
338
- "version": "0.28.2",
339
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
340
- "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
341
- "cpu": [
342
- "arm64"
343
- ],
344
- "dev": true,
345
- "license": "MIT",
346
- "optional": true,
347
- "os": [
348
- "openbsd"
349
- ],
350
- "engines": {
351
- "node": ">=18"
352
- }
353
- },
354
- "node_modules/@esbuild/openbsd-x64": {
355
- "version": "0.28.2",
356
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
357
- "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
358
- "cpu": [
359
- "x64"
360
- ],
361
- "dev": true,
362
- "license": "MIT",
363
- "optional": true,
364
- "os": [
365
- "openbsd"
366
- ],
367
- "engines": {
368
- "node": ">=18"
369
- }
370
- },
371
- "node_modules/@esbuild/openharmony-arm64": {
372
- "version": "0.28.2",
373
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
374
- "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
375
- "cpu": [
376
- "arm64"
377
- ],
378
- "dev": true,
379
- "license": "MIT",
380
- "optional": true,
381
- "os": [
382
- "openharmony"
383
- ],
384
- "engines": {
385
- "node": ">=18"
386
- }
387
- },
388
- "node_modules/@esbuild/sunos-x64": {
389
- "version": "0.28.2",
390
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
391
- "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
392
- "cpu": [
393
- "x64"
394
- ],
395
- "dev": true,
396
- "license": "MIT",
397
- "optional": true,
398
- "os": [
399
- "sunos"
400
- ],
401
- "engines": {
402
- "node": ">=18"
403
- }
404
- },
405
- "node_modules/@esbuild/win32-arm64": {
406
- "version": "0.28.2",
407
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
408
- "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
409
- "cpu": [
410
- "arm64"
411
- ],
412
- "dev": true,
413
- "license": "MIT",
414
- "optional": true,
415
- "os": [
416
- "win32"
417
- ],
418
- "engines": {
419
- "node": ">=18"
420
- }
421
- },
422
- "node_modules/@esbuild/win32-ia32": {
423
- "version": "0.28.2",
424
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
425
- "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
426
- "cpu": [
427
- "ia32"
428
- ],
429
- "dev": true,
430
- "license": "MIT",
431
- "optional": true,
432
- "os": [
433
- "win32"
434
- ],
435
- "engines": {
436
- "node": ">=18"
437
- }
438
- },
439
- "node_modules/@esbuild/win32-x64": {
440
- "version": "0.28.2",
441
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
442
- "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
443
- "cpu": [
444
- "x64"
445
- ],
446
- "dev": true,
447
- "license": "MIT",
448
- "optional": true,
449
- "os": [
450
- "win32"
451
- ],
452
- "engines": {
453
- "node": ">=18"
454
- }
455
- },
456
- "node_modules/@napi-rs/lzma-linux-x64-gnu": {
457
- "version": "1.5.1",
458
- "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
459
- "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
460
- "cpu": [
461
- "x64"
462
- ],
463
- "dev": true,
464
- "license": "MIT",
465
- "optional": true,
466
- "os": [
467
- "linux"
468
- ],
469
- "engines": {
470
- "node": "^22.20 || ^24.12 || >=25"
471
- }
472
- },
473
- "node_modules/@rollup/rollup-android-arm-eabi": {
474
- "version": "4.64.0",
475
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.0.tgz",
476
- "integrity": "sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==",
477
- "cpu": [
478
- "arm"
479
- ],
480
- "dev": true,
481
- "license": "MIT",
482
- "optional": true,
483
- "os": [
484
- "android"
485
- ]
486
- },
487
- "node_modules/@rollup/rollup-android-arm64": {
488
- "version": "4.64.0",
489
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.0.tgz",
490
- "integrity": "sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==",
491
- "cpu": [
492
- "arm64"
493
- ],
494
- "dev": true,
495
- "license": "MIT",
496
- "optional": true,
497
- "os": [
498
- "android"
499
- ]
500
- },
501
- "node_modules/@rollup/rollup-darwin-arm64": {
502
- "version": "4.64.0",
503
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.0.tgz",
504
- "integrity": "sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==",
505
- "cpu": [
506
- "arm64"
507
- ],
508
- "dev": true,
509
- "license": "MIT",
510
- "optional": true,
511
- "os": [
512
- "darwin"
513
- ]
514
- },
515
- "node_modules/@rollup/rollup-darwin-x64": {
516
- "version": "4.64.0",
517
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.0.tgz",
518
- "integrity": "sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==",
519
- "cpu": [
520
- "x64"
521
- ],
522
- "dev": true,
523
- "license": "MIT",
524
- "optional": true,
525
- "os": [
526
- "darwin"
527
- ]
528
- },
529
- "node_modules/@rollup/rollup-freebsd-arm64": {
530
- "version": "4.64.0",
531
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.0.tgz",
532
- "integrity": "sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==",
533
- "cpu": [
534
- "arm64"
535
- ],
536
- "dev": true,
537
- "license": "MIT",
538
- "optional": true,
539
- "os": [
540
- "freebsd"
541
- ]
542
- },
543
- "node_modules/@rollup/rollup-freebsd-x64": {
544
- "version": "4.64.0",
545
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.0.tgz",
546
- "integrity": "sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==",
547
- "cpu": [
548
- "x64"
549
- ],
550
- "dev": true,
551
- "license": "MIT",
552
- "optional": true,
553
- "os": [
554
- "freebsd"
555
- ]
556
- },
557
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
558
- "version": "4.64.0",
559
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.0.tgz",
560
- "integrity": "sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==",
561
- "cpu": [
562
- "arm"
563
- ],
564
- "dev": true,
565
- "license": "MIT",
566
- "optional": true,
567
- "os": [
568
- "linux"
569
- ]
570
- },
571
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
572
- "version": "4.64.0",
573
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.0.tgz",
574
- "integrity": "sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==",
575
- "cpu": [
576
- "arm"
577
- ],
578
- "dev": true,
579
- "license": "MIT",
580
- "optional": true,
581
- "os": [
582
- "linux"
583
- ]
584
- },
585
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
586
- "version": "4.64.0",
587
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.0.tgz",
588
- "integrity": "sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==",
589
- "cpu": [
590
- "arm64"
591
- ],
592
- "dev": true,
593
- "license": "MIT",
594
- "optional": true,
595
- "os": [
596
- "linux"
597
- ]
598
- },
599
- "node_modules/@rollup/rollup-linux-arm64-musl": {
600
- "version": "4.64.0",
601
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.0.tgz",
602
- "integrity": "sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==",
603
- "cpu": [
604
- "arm64"
605
- ],
606
- "dev": true,
607
- "license": "MIT",
608
- "optional": true,
609
- "os": [
610
- "linux"
611
- ]
612
- },
613
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
614
- "version": "4.64.0",
615
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.0.tgz",
616
- "integrity": "sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==",
617
- "cpu": [
618
- "loong64"
619
- ],
620
- "dev": true,
621
- "license": "MIT",
622
- "optional": true,
623
- "os": [
624
- "linux"
625
- ]
626
- },
627
- "node_modules/@rollup/rollup-linux-loong64-musl": {
628
- "version": "4.64.0",
629
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.0.tgz",
630
- "integrity": "sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==",
631
- "cpu": [
632
- "loong64"
633
- ],
634
- "dev": true,
635
- "license": "MIT",
636
- "optional": true,
637
- "os": [
638
- "linux"
639
- ]
640
- },
641
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
642
- "version": "4.64.0",
643
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.0.tgz",
644
- "integrity": "sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==",
645
- "cpu": [
646
- "ppc64"
647
- ],
648
- "dev": true,
649
- "license": "MIT",
650
- "optional": true,
651
- "os": [
652
- "linux"
653
- ]
654
- },
655
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
656
- "version": "4.64.0",
657
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.0.tgz",
658
- "integrity": "sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==",
659
- "cpu": [
660
- "ppc64"
661
- ],
662
- "dev": true,
663
- "license": "MIT",
664
- "optional": true,
665
- "os": [
666
- "linux"
667
- ]
668
- },
669
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
670
- "version": "4.64.0",
671
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.0.tgz",
672
- "integrity": "sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==",
673
- "cpu": [
674
- "riscv64"
675
- ],
676
- "dev": true,
677
- "license": "MIT",
678
- "optional": true,
679
- "os": [
680
- "linux"
681
- ]
682
- },
683
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
684
- "version": "4.64.0",
685
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.0.tgz",
686
- "integrity": "sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==",
687
- "cpu": [
688
- "riscv64"
689
- ],
690
- "dev": true,
691
- "license": "MIT",
692
- "optional": true,
693
- "os": [
694
- "linux"
695
- ]
696
- },
697
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
698
- "version": "4.64.0",
699
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.0.tgz",
700
- "integrity": "sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==",
701
- "cpu": [
702
- "s390x"
703
- ],
704
- "dev": true,
705
- "license": "MIT",
706
- "optional": true,
707
- "os": [
708
- "linux"
709
- ]
710
- },
711
- "node_modules/@rollup/rollup-linux-x64-gnu": {
712
- "version": "4.64.0",
713
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.0.tgz",
714
- "integrity": "sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==",
715
- "cpu": [
716
- "x64"
717
- ],
718
- "dev": true,
719
- "license": "MIT",
720
- "optional": true,
721
- "os": [
722
- "linux"
723
- ]
724
- },
725
- "node_modules/@rollup/rollup-linux-x64-musl": {
726
- "version": "4.64.0",
727
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.0.tgz",
728
- "integrity": "sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==",
729
- "cpu": [
730
- "x64"
731
- ],
732
- "dev": true,
733
- "license": "MIT",
734
- "optional": true,
735
- "os": [
736
- "linux"
737
- ]
738
- },
739
- "node_modules/@rollup/rollup-openbsd-x64": {
740
- "version": "4.64.0",
741
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.0.tgz",
742
- "integrity": "sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==",
743
- "cpu": [
744
- "x64"
745
- ],
746
- "dev": true,
747
- "license": "MIT",
748
- "optional": true,
749
- "os": [
750
- "openbsd"
751
- ]
752
- },
753
- "node_modules/@rollup/rollup-openharmony-arm64": {
754
- "version": "4.64.0",
755
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.0.tgz",
756
- "integrity": "sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==",
757
- "cpu": [
758
- "arm64"
759
- ],
760
- "dev": true,
761
- "license": "MIT",
762
- "optional": true,
763
- "os": [
764
- "openharmony"
765
- ]
766
- },
767
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
768
- "version": "4.64.0",
769
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.0.tgz",
770
- "integrity": "sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==",
771
- "cpu": [
772
- "arm64"
773
- ],
774
- "dev": true,
775
- "license": "MIT",
776
- "optional": true,
777
- "os": [
778
- "win32"
779
- ]
780
- },
781
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
782
- "version": "4.64.0",
783
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.0.tgz",
784
- "integrity": "sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==",
785
- "cpu": [
786
- "ia32"
787
- ],
788
- "dev": true,
789
- "license": "MIT",
790
- "optional": true,
791
- "os": [
792
- "win32"
793
- ]
794
- },
795
- "node_modules/@rollup/rollup-win32-x64-gnu": {
796
- "version": "4.64.0",
797
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.0.tgz",
798
- "integrity": "sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==",
799
- "cpu": [
800
- "x64"
801
- ],
802
- "dev": true,
803
- "license": "MIT",
804
- "optional": true,
805
- "os": [
806
- "win32"
807
- ]
808
- },
809
- "node_modules/@rollup/rollup-win32-x64-msvc": {
810
- "version": "4.64.0",
811
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.0.tgz",
812
- "integrity": "sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==",
813
- "cpu": [
814
- "x64"
815
- ],
816
- "dev": true,
817
- "license": "MIT",
818
- "optional": true,
819
- "os": [
820
- "win32"
821
- ]
822
- },
823
- "node_modules/@types/estree": {
824
- "version": "1.0.9",
825
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
826
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
827
- "dev": true,
828
- "license": "MIT"
829
- },
830
- "node_modules/esbuild": {
831
- "version": "0.28.2",
832
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
833
- "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
834
- "dev": true,
835
- "hasInstallScript": true,
836
- "license": "MIT",
837
- "bin": {
838
- "esbuild": "bin/esbuild"
839
- },
840
- "engines": {
841
- "node": ">=18"
842
- },
843
- "optionalDependencies": {
844
- "@esbuild/aix-ppc64": "0.28.2",
845
- "@esbuild/android-arm": "0.28.2",
846
- "@esbuild/android-arm64": "0.28.2",
847
- "@esbuild/android-x64": "0.28.2",
848
- "@esbuild/darwin-arm64": "0.28.2",
849
- "@esbuild/darwin-x64": "0.28.2",
850
- "@esbuild/freebsd-arm64": "0.28.2",
851
- "@esbuild/freebsd-x64": "0.28.2",
852
- "@esbuild/linux-arm": "0.28.2",
853
- "@esbuild/linux-arm64": "0.28.2",
854
- "@esbuild/linux-ia32": "0.28.2",
855
- "@esbuild/linux-loong64": "0.28.2",
856
- "@esbuild/linux-mips64el": "0.28.2",
857
- "@esbuild/linux-ppc64": "0.28.2",
858
- "@esbuild/linux-riscv64": "0.28.2",
859
- "@esbuild/linux-s390x": "0.28.2",
860
- "@esbuild/linux-x64": "0.28.2",
861
- "@esbuild/netbsd-arm64": "0.28.2",
862
- "@esbuild/netbsd-x64": "0.28.2",
863
- "@esbuild/openbsd-arm64": "0.28.2",
864
- "@esbuild/openbsd-x64": "0.28.2",
865
- "@esbuild/openharmony-arm64": "0.28.2",
866
- "@esbuild/sunos-x64": "0.28.2",
867
- "@esbuild/win32-arm64": "0.28.2",
868
- "@esbuild/win32-ia32": "0.28.2",
869
- "@esbuild/win32-x64": "0.28.2"
870
- }
871
- },
872
- "node_modules/fdir": {
873
- "version": "6.5.0",
874
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
875
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
876
- "dev": true,
877
- "license": "MIT",
878
- "engines": {
879
- "node": ">=12.0.0"
880
- },
881
- "peerDependencies": {
882
- "picomatch": "^3 || ^4"
883
- },
884
- "peerDependenciesMeta": {
885
- "picomatch": {
886
- "optional": true
887
- }
888
- }
889
- },
890
- "node_modules/fsevents": {
891
- "version": "2.3.3",
892
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
893
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
894
- "dev": true,
895
- "hasInstallScript": true,
896
- "license": "MIT",
897
- "optional": true,
898
- "os": [
899
- "darwin"
900
- ],
901
- "engines": {
902
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
903
- }
904
- },
905
- "node_modules/nanoid": {
906
- "version": "3.3.20",
907
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
908
- "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
909
- "dev": true,
910
- "funding": [
911
- {
912
- "type": "github",
913
- "url": "https://github.com/sponsors/ai"
914
- }
915
- ],
916
- "license": "MIT",
917
- "bin": {
918
- "nanoid": "bin/nanoid.cjs"
919
- },
920
- "engines": {
921
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
922
- }
923
- },
924
- "node_modules/picocolors": {
925
- "version": "1.1.1",
926
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
927
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
928
- "dev": true,
929
- "license": "ISC"
930
- },
931
- "node_modules/picomatch": {
932
- "version": "4.0.7",
933
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
934
- "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
935
- "dev": true,
936
- "license": "MIT",
937
- "engines": {
938
- "node": ">=12"
939
- },
940
- "funding": {
941
- "url": "https://github.com/sponsors/jonschlinkert"
942
- }
943
- },
944
- "node_modules/postcss": {
945
- "version": "8.5.29",
946
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
947
- "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==",
948
- "dev": true,
949
- "funding": [
950
- {
951
- "type": "opencollective",
952
- "url": "https://opencollective.com/postcss/"
953
- },
954
- {
955
- "type": "tidelift",
956
- "url": "https://tidelift.com/funding/github/npm/postcss"
957
- },
958
- {
959
- "type": "github",
960
- "url": "https://github.com/sponsors/ai"
961
- }
962
- ],
963
- "license": "MIT",
964
- "dependencies": {
965
- "nanoid": "^3.3.19",
966
- "picocolors": "^1.1.1",
967
- "source-map-js": "^1.2.2"
968
- },
969
- "engines": {
970
- "node": "^10 || ^12 || >=14"
971
- }
972
- },
973
- "node_modules/rollup": {
974
- "version": "4.64.0",
975
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.0.tgz",
976
- "integrity": "sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==",
977
- "dev": true,
978
- "license": "MIT",
979
- "dependencies": {
980
- "@types/estree": "1.0.9"
981
- },
982
- "bin": {
983
- "rollup": "dist/bin/rollup"
984
- },
985
- "engines": {
986
- "node": ">=18.0.0",
987
- "npm": ">=8.0.0"
988
- },
989
- "optionalDependencies": {
990
- "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
991
- "@rollup/rollup-android-arm-eabi": "4.64.0",
992
- "@rollup/rollup-android-arm64": "4.64.0",
993
- "@rollup/rollup-darwin-arm64": "4.64.0",
994
- "@rollup/rollup-darwin-x64": "4.64.0",
995
- "@rollup/rollup-freebsd-arm64": "4.64.0",
996
- "@rollup/rollup-freebsd-x64": "4.64.0",
997
- "@rollup/rollup-linux-arm-gnueabihf": "4.64.0",
998
- "@rollup/rollup-linux-arm-musleabihf": "4.64.0",
999
- "@rollup/rollup-linux-arm64-gnu": "4.64.0",
1000
- "@rollup/rollup-linux-arm64-musl": "4.64.0",
1001
- "@rollup/rollup-linux-loong64-gnu": "4.64.0",
1002
- "@rollup/rollup-linux-loong64-musl": "4.64.0",
1003
- "@rollup/rollup-linux-ppc64-gnu": "4.64.0",
1004
- "@rollup/rollup-linux-ppc64-musl": "4.64.0",
1005
- "@rollup/rollup-linux-riscv64-gnu": "4.64.0",
1006
- "@rollup/rollup-linux-riscv64-musl": "4.64.0",
1007
- "@rollup/rollup-linux-s390x-gnu": "4.64.0",
1008
- "@rollup/rollup-linux-x64-gnu": "4.64.0",
1009
- "@rollup/rollup-linux-x64-musl": "4.64.0",
1010
- "@rollup/rollup-openbsd-x64": "4.64.0",
1011
- "@rollup/rollup-openharmony-arm64": "4.64.0",
1012
- "@rollup/rollup-win32-arm64-msvc": "4.64.0",
1013
- "@rollup/rollup-win32-ia32-msvc": "4.64.0",
1014
- "@rollup/rollup-win32-x64-gnu": "4.64.0",
1015
- "@rollup/rollup-win32-x64-msvc": "4.64.0",
1016
- "fsevents": "~2.3.2"
1017
- }
1018
- },
1019
- "node_modules/source-map-js": {
1020
- "version": "1.2.2",
1021
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
1022
- "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
1023
- "dev": true,
1024
- "license": "BSD-3-Clause",
1025
- "engines": {
1026
- "node": ">=0.10.0"
1027
- }
1028
- },
1029
- "node_modules/tinyglobby": {
1030
- "version": "0.2.17",
1031
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
1032
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
1033
- "dev": true,
1034
- "license": "MIT",
1035
- "dependencies": {
1036
- "fdir": "^6.5.0",
1037
- "picomatch": "^4.0.4"
1038
- },
1039
- "engines": {
1040
- "node": ">=12.0.0"
1041
- },
1042
- "funding": {
1043
- "url": "https://github.com/sponsors/SuperchupuDev"
1044
- }
1045
- },
1046
- "node_modules/vite": {
1047
- "version": "7.3.7",
1048
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz",
1049
- "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==",
1050
- "dev": true,
1051
- "license": "MIT",
1052
- "dependencies": {
1053
- "esbuild": "^0.27.0 || ^0.28.0",
1054
- "fdir": "^6.5.0",
1055
- "picomatch": "^4.0.3",
1056
- "postcss": "^8.5.6",
1057
- "rollup": "^4.43.0",
1058
- "tinyglobby": "^0.2.15"
1059
- },
1060
- "bin": {
1061
- "vite": "bin/vite.js"
1062
- },
1063
- "engines": {
1064
- "node": "^20.19.0 || >=22.12.0"
1065
- },
1066
- "funding": {
1067
- "url": "https://github.com/vitejs/vite?sponsor=1"
1068
- },
1069
- "optionalDependencies": {
1070
- "fsevents": "~2.3.3"
1071
- },
1072
- "peerDependencies": {
1073
- "@types/node": "^20.19.0 || >=22.12.0",
1074
- "jiti": ">=1.21.0",
1075
- "less": "^4.0.0",
1076
- "lightningcss": "^1.21.0",
1077
- "sass": "^1.70.0",
1078
- "sass-embedded": "^1.70.0",
1079
- "stylus": ">=0.54.8",
1080
- "sugarss": "^5.0.0",
1081
- "terser": "^5.16.0",
1082
- "tsx": "^4.8.1",
1083
- "yaml": "^2.4.2"
1084
- },
1085
- "peerDependenciesMeta": {
1086
- "@types/node": {
1087
- "optional": true
1088
- },
1089
- "jiti": {
1090
- "optional": true
1091
- },
1092
- "less": {
1093
- "optional": true
1094
- },
1095
- "lightningcss": {
1096
- "optional": true
1097
- },
1098
- "sass": {
1099
- "optional": true
1100
- },
1101
- "sass-embedded": {
1102
- "optional": true
1103
- },
1104
- "stylus": {
1105
- "optional": true
1106
- },
1107
- "sugarss": {
1108
- "optional": true
1109
- },
1110
- "terser": {
1111
- "optional": true
1112
- },
1113
- "tsx": {
1114
- "optional": true
1115
- },
1116
- "yaml": {
1117
- "optional": true
1118
- }
1119
- }
1120
- }
1121
- }
1122
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "name": "monkey-story-motion",
3
- "version": "1.0.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite --host 0.0.0.0",
8
- "build": "vite build",
9
- "preview": "vite preview --host 0.0.0.0",
10
- "tts": "node scripts/hf-tts.mjs"
11
- },
12
- "devDependencies": {
13
- "vite": "^7.1.7"
14
- }
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/hf-tts.mjs DELETED
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Optional local narration generator.
4
- *
5
- * Usage:
6
- * HF_TOKEN=hf_... node scripts/hf-tts.mjs
7
- *
8
- * The token is read only from the environment and is never written to the
9
- * repository. The browser app remains usable without generated audio by using
10
- * SpeechSynthesis for local narration playback.
11
- */
12
- import fs from 'node:fs/promises';
13
- import path from 'node:path';
14
-
15
- const token = process.env.HF_TOKEN;
16
- if (!token) {
17
- console.error('Missing HF_TOKEN. Keep it in your shell environment, never in source code.');
18
- process.exit(1);
19
- }
20
-
21
- const endpoint = process.env.HF_TTS_ENDPOINT || 'https://api-inference.huggingface.co/models/facebook/mms-tts-zho';
22
- const output = process.env.HF_TTS_OUT || 'public/audio/narration.wav';
23
- const text = process.env.HF_TTS_TEXT || '想象一间笼子,里面有五只猴子。笼子顶上挂着一串香蕉。看起来,它只差一把梯子。可是,为什么没有一只猴子去拿?';
24
-
25
- const res = await fetch(endpoint, {
26
- method: 'POST',
27
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'audio/wav' },
28
- body: JSON.stringify({ inputs: text })
29
- });
30
- const contentType = res.headers.get('content-type') || '';
31
- const body = Buffer.from(await res.arrayBuffer());
32
- if (!res.ok || contentType.includes('application/json')) {
33
- console.error(`Hugging Face TTS failed (${res.status}): ${body.toString('utf8').slice(0, 800)}`);
34
- process.exit(1);
35
- }
36
- await fs.mkdir(path.dirname(output), { recursive: true });
37
- await fs.writeFile(output, body);
38
- console.log(`Wrote ${output} (${body.length} bytes)`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/main.js DELETED
@@ -1,382 +0,0 @@
1
- import './styles.css';
2
-
3
- const FPS = 30;
4
- const DURATION = 168;
5
- const TAU = Math.PI * 2;
6
-
7
- const scenes = [
8
- {
9
- id: 'hook', start: 0, end: 14, chapter: '01 / 钩子', title: '为什么没人敢拿?', kicker: '一个香蕉,五只猴子,零个解释', color: '#f4b33e',
10
- voice: '想象一间笼子,里面有五只猴子。笼子顶上挂着一串香蕉。看起来,它只差一把梯子。可是,为什么没有一只猴子去拿?',
11
- note: '用一个极简问题,把观众从“看见规则”带到“追问规则”。'
12
- },
13
- {
14
- id: 'first', start: 14, end: 34, chapter: '02 / 第一次尝试', title: '第一个上梯子的人', kicker: '收益在上面,代价在下面', color: '#80d6c5',
15
- voice: '第一只猴子爬上梯子。它看见的,是立刻可得的香蕉;其他猴子看见的,却是即将到来的风险。于是,冷水从上面浇下来,所有猴子都被淋透。',
16
- note: '用收益 / 代价的对照解释:理性选择会被群体惩罚改写。'
17
- },
18
- {
19
- id: 'punish', start: 34, end: 56, chapter: '03 / 惩罚机制', title: '群体开始自我管理', kicker: '惩罚一次,规则就记住了一半', color: '#ef6b69',
20
- voice: '第二次,另一只猴子试着爬梯子。它还没有碰到香蕉,其他猴子就先把它拽了下来。没有人再等喷水,因为惩罚已经被群体内化成了动作。',
21
- note: '红色不是“坏人”,而是一个被复制的反馈回路。'
22
- },
23
- {
24
- id: 'replacement', start: 56, end: 78, chapter: '04 / 新成员', title: '新人不知道发生了什么', kicker: '制度会比记忆活得更久', color: '#a98bf2',
25
- voice: '现在,换掉一只老猴子,迎来一只新人。新人当然想爬梯子,可刚伸手,就被四只老猴子打了下来。它不懂冷水,却学会了不许碰梯子。',
26
- note: '关键转折:规则脱离原始原因,开始靠“别人都这样”存活。'
27
- },
28
- {
29
- id: 'loop', start: 78, end: 100, chapter: '05 / 规则遗忘', title: '最后,没人见过冷水', kicker: '但所有人都说:不能爬', color: '#70a8ff',
30
- voice: '继续换人,直到笼子里已经没有一只猴子经历过冷水。此时,大家仍然在阻止别人爬梯子。问原因?答案通常是:一直都是这样。',
31
- note: '“一直都是这样”不是解释,它只是制度的缓存。'
32
- },
33
- {
34
- id: 'model', start: 100, end: 124, chapter: '06 / 经济学模型', title: '这不是猴子的问题', kicker: '激励函数被藏起来了', color: '#ffcc67',
35
- voice: '把故事翻译成经济学:一个人做选择,会比较收益和代价。当惩罚足够强,最优策略就从“拿香蕉”变成了“阻止别人拿”。后来,惩罚消失了,策略却留下了。',
36
- note: '用坐标轴把“看得见的行为”和“看不见的激励”接上。'
37
- },
38
- {
39
- id: 'reality', start: 124, end: 148, chapter: '07 / 现实映射', title: '我们在哪些笼子里?', kicker: '流程、文化、默认选项', color: '#5dd7ff',
40
- voice: '现实里,它可能是一条没人能解释的审批流程,一个“新人别碰”的按钮,或者一句“我们公司一直这么做”。制度不是敌人,但不再适用的制度,会把过去的答案伪装成今天的常识。',
41
- note: '不把隐喻简单等同于现实;重点是提出可验证的问题。'
42
- },
43
- {
44
- id: 'exit', start: 148, end: 168, chapter: '08 / 结尾', title: '先问一句:冷水还在吗?', kicker: '好的规则经得起追问', color: '#f16eaa',
45
- voice: '所以,下次你听到“不能这样做”,先别急着反抗,也别急着服从。问三件事:原来的风险还在吗?代价是谁承担?有没有更低成本的替代方案?规则最危险的时刻,不是被打破,而是再也没人知道它为何存在。',
46
- note: '结尾留下行动问题,而不是把观众带到一个简单的“反制度”结论。'
47
- }
48
- ];
49
-
50
- const copy = {
51
- bilibili: {
52
- title: '为什么没人敢拿那串香蕉?|经济学里的“收猴子”故事',
53
- description: '一个香蕉、五只猴子、没有人记得的冷水。\n\n这是一支原创图形化经济学动画:我们用“收猴子的故事”拆解制度惯性、路径依赖与群体激励。故事是常见寓言隐喻,不是对真实动物实验的复述。\n\n看完试着问自己:你所在的流程里,有没有一条“大家都说不能碰”,但没人说得清为什么的规则?\n\n如果喜欢这种把抽象机制画出来的讲解,欢迎收藏、转发,评论区留下你的“冷水还在吗”。',
54
- tags: '#经济学 #制度 #组织文化 #路径依赖 #财经科普 #动画 #思维方式'
55
- },
56
- douyin: {
57
- title: '为什么没人敢拿香蕉?收猴子的故事讲透制度惯性',
58
- description: '没有人见过冷水,但所有人都说:不能爬。\n\n用 3 分钟看懂:激励如何变成规则,规则如何变成习惯,以及我们如何把过去的答案带进今天。\n\n寓言是隐喻,不是真实实验史料。你身边有没有“不能碰,但说不清为什么”的流程?',
59
- tags: '#经济学 #收猴子的故事 #职场 #认知 #知识 #科普 #制度惯性'
60
- }
61
- };
62
-
63
- const app = document.querySelector('#app');
64
- app.innerHTML = `
65
- <main class="shell">
66
- <header class="topbar">
67
- <div class="brand"><span class="brand-mark">∿</span><div><strong>MONKEY / RULES</strong><span>经济学动画工作台 · 原创讲解</span></div></div>
68
- <div class="top-status"><span class="live-dot"></span><span id="renderStatus">实时画布</span><span class="status-divider"></span><span id="formatStatus">16:9 横屏</span></div>
69
- </header>
70
- <section class="workspace">
71
- <section class="stage-panel">
72
- <div class="stage-shell" id="stageShell">
73
- <canvas id="stage" width="1280" height="720" aria-label="收猴子的故事动画画布"></canvas>
74
- <div class="stage-safe safe-top">安全区</div>
75
- <div class="stage-safe safe-bottom">安全区 · 平台 UI 避让</div>
76
- <div class="stage-hud"><span id="sceneLabel">01 / 钩子</span><span id="timeLabel">00:00 / 02:48</span></div>
77
- </div>
78
- <div class="controls">
79
- <button class="primary-control" id="playButton" type="button"><span class="play-icon">▶</span><span>播放</span></button>
80
- <button class="icon-control" id="restartButton" type="button" aria-label="从头播放">↺</button>
81
- <button class="icon-control" id="muteButton" type="button" aria-label="打开或关闭声音">◖</button>
82
- <div class="scrub"><input id="scrub" type="range" min="0" max="168" step="0.01" value="0" aria-label="时间轴"/><div class="scrub-progress" id="scrubProgress"></div></div>
83
- <div class="frame-rate">30 FPS</div>
84
- </div>
85
- <div class="orientation-row"><span class="control-label">画幅</span><button class="chip active" data-orientation="landscape" type="button">B站 · 16:9</button><button class="chip" data-orientation="portrait" type="button">抖音 · 9:16</button><span class="safe-note">竖屏自动保留刘海 / 手势安全区</span></div>
86
- </section>
87
- <aside class="inspector">
88
- <div class="inspector-head"><div><span class="eyebrow">EDIT / STORYBOARD</span><h1>收猴子的故事</h1></div><span class="duration-badge">02:48</span></div>
89
- <p class="lede">用一个寓言,拆开“大家都这样做”背后的激励结构。</p>
90
- <div class="chapter-list" id="chapterList"></div>
91
- <div class="inspector-card voice-card"><div class="card-title"><span>本幕旁白</span><button id="speakButton" class="text-button" type="button">试听</button></div><p id="voiceText"></p></div>
92
- <div class="inspector-card note-card"><div class="card-title"><span>导演提示</span><span class="tiny-dot"></span></div><p id="directorNote"></p></div>
93
- <button class="export-button" id="exportButton" type="button"><span>↧</span><div><strong>本地导出视频</strong><small>优先 MP4 · 画面与音乐在浏览器内渲染</small></div><span class="export-arrow">→</span></button>
94
- <div class="export-progress" id="exportProgress" hidden><div class="progress-top"><span id="exportLabel">准备导出…</span><span id="exportPercent">0%</span></div><div class="progress-track"><span id="exportBar"></span></div></div>
95
- </aside>
96
- </section>
97
- <section class="publish-section">
98
- <div class="section-heading"><div><span class="eyebrow">PUBLISH KIT</span><h2>发布文案 & 封面</h2></div><span class="section-caption">每个平台一键复制 · 原创封面可下载</span></div>
99
- <div class="publish-grid">
100
- <article class="publish-card"><div class="platform-head"><span class="platform-icon bilibili-icon">◈</span><div><strong>B站横屏</strong><small>标题 / 简介 / 标签</small></div><button class="copy-button" data-copy="bilibili" type="button">复制整套</button></div><label>标题<textarea readonly id="bilibiliTitle"></textarea></label><label>简介<textarea readonly id="bilibiliDescription"></textarea></label><label>标签<textarea readonly id="bilibiliTags"></textarea></label></article>
101
- <article class="publish-card"><div class="platform-head"><span class="platform-icon douyin-icon">♪</span><div><strong>抖音竖屏</strong><small>标题 / 简介 / 标签</small></div><button class="copy-button" data-copy="douyin" type="button">复制整套</button></div><label>标题<textarea readonly id="douyinTitle"></textarea></label><label>简介<textarea readonly id="douyinDescription"></textarea></label><label>标签<textarea readonly id="douyinTags"></textarea></label></article>
102
- <article class="cover-card"><div class="platform-head"><span class="platform-icon cover-icon">✦</span><div><strong>原创封面</strong><small>随画幅生成 · 无外部素材</small></div></div><div class="cover-previews"><div class="cover-preview landscape-cover"><canvas id="landscapeCover" width="640" height="360"></canvas><span>16:9</span></div><div class="cover-preview portrait-cover"><canvas id="portraitCover" width="360" height="640"></canvas><span>9:16</span></div></div><div class="cover-actions"><button id="downloadLandscape" type="button">下载横屏封面</button><button id="downloadPortrait" type="button">下载竖屏封面</button></div></article>
103
- </div>
104
- </section>
105
- <footer class="footer"><span>原创图形化讲解 · 不上传画面与剪贴板内容</span><span>故事为经济学寓言隐喻 · 不是动物实验史料</span></footer>
106
- </main>
107
- `;
108
-
109
- const canvas = document.querySelector('#stage');
110
- const ctx = canvas.getContext('2d');
111
- const stageShell = document.querySelector('#stageShell');
112
- const playButton = document.querySelector('#playButton');
113
- const scrub = document.querySelector('#scrub');
114
- const scrubProgress = document.querySelector('#scrubProgress');
115
- const timeLabel = document.querySelector('#timeLabel');
116
- const sceneLabel = document.querySelector('#sceneLabel');
117
- const chapterList = document.querySelector('#chapterList');
118
- const voiceText = document.querySelector('#voiceText');
119
- const directorNote = document.querySelector('#directorNote');
120
- const muteButton = document.querySelector('#muteButton');
121
- const renderStatus = document.querySelector('#renderStatus');
122
- const formatStatus = document.querySelector('#formatStatus');
123
- const exportButton = document.querySelector('#exportButton');
124
- const exportProgress = document.querySelector('#exportProgress');
125
- const exportBar = document.querySelector('#exportBar');
126
- const exportLabel = document.querySelector('#exportLabel');
127
- const exportPercent = document.querySelector('#exportPercent');
128
-
129
- let orientation = 'landscape';
130
- let currentTime = 0;
131
- let playing = false;
132
- let muted = false;
133
- let rafId = 0;
134
- let lastStamp = 0;
135
- let currentSceneIndex = 0;
136
- let audio = null;
137
-
138
- const clamp = (n, a = 0, b = 1) => Math.max(a, Math.min(b, n));
139
- const lerp = (a, b, t) => a + (b - a) * t;
140
- const ease = (t) => t * t * (3 - 2 * t);
141
- const smooth = (t) => ease(clamp(t));
142
- const fmt = (s) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
143
- const activeScene = (t = currentTime) => scenes.findIndex((s) => t >= s.start && t < s.end) >= 0 ? scenes.findIndex((s) => t >= s.start && t < s.end) : scenes.length - 1;
144
-
145
- function resizeStage() {
146
- const portrait = orientation === 'portrait';
147
- canvas.width = portrait ? 720 : 1280;
148
- canvas.height = portrait ? 1280 : 720;
149
- stageShell.classList.toggle('portrait', portrait);
150
- stageShell.classList.toggle('landscape', !portrait);
151
- formatStatus.textContent = portrait ? '9:16 竖屏' : '16:9 横屏';
152
- render(currentTime);
153
- }
154
-
155
- function updateUi() {
156
- const index = activeScene();
157
- const scene = scenes[index];
158
- if (index !== currentSceneIndex) {
159
- if (playing && !muted && audio) {
160
- audio.sfx('tick');
161
- speakCurrent();
162
- }
163
- currentSceneIndex = index;
164
- }
165
- sceneLabel.textContent = scene.chapter;
166
- timeLabel.textContent = `${fmt(currentTime)} / ${fmt(DURATION)}`;
167
- scrub.value = currentTime.toFixed(2);
168
- scrubProgress.style.width = `${(currentTime / DURATION) * 100}%`;
169
- voiceText.textContent = scene.voice;
170
- directorNote.textContent = scene.note;
171
- [...chapterList.children].forEach((el, i) => el.classList.toggle('active', i === index));
172
- }
173
-
174
- function buildChapters() {
175
- chapterList.innerHTML = scenes.map((s, i) => `<button class="chapter-item" data-index="${i}" type="button"><span class="chapter-no">${String(i + 1).padStart(2, '0')}</span><span class="chapter-copy"><strong>${s.title}</strong><small>${s.chapter.replace(/^\d+ \/ /, '')}</small></span><span class="chapter-time">${fmt(s.start)}</span></button>`).join('');
176
- chapterList.querySelectorAll('.chapter-item').forEach((el) => el.addEventListener('click', () => {
177
- currentTime = scenes[Number(el.dataset.index)].start;
178
- updateUi();
179
- render(currentTime);
180
- }));
181
- }
182
-
183
- function roundedRect(c, x, y, w, h, r) {
184
- c.beginPath(); c.roundRect(x, y, w, h, r); c.closePath();
185
- }
186
- function alpha(c, value) { c.globalAlpha = value; }
187
- function text(c, str, x, y, size, color, weight = 500, align = 'left') {
188
- c.font = `${weight} ${size}px Inter, "Noto Sans SC", "PingFang SC", sans-serif`;
189
- c.fillStyle = color; c.textAlign = align; c.textBaseline = 'middle'; c.fillText(str, x, y);
190
- }
191
- function circle(c, x, y, r, fill, stroke = null, sw = 1) {
192
- c.beginPath(); c.arc(x, y, r, 0, TAU); if (fill) { c.fillStyle = fill; c.fill(); } if (stroke) { c.strokeStyle = stroke; c.lineWidth = sw; c.stroke(); }
193
- }
194
- function line(c, x1, y1, x2, y2, color, width = 1, dash = []) { c.beginPath(); c.moveTo(x1, y1); c.lineTo(x2, y2); c.strokeStyle = color; c.lineWidth = width; c.setLineDash(dash); c.stroke(); c.setLineDash([]); }
195
- function wrapText(c, str, x, y, maxWidth, lineHeight, size, color, weight = 500) {
196
- c.font = `${weight} ${size}px Inter, "Noto Sans SC", "PingFang SC", sans-serif`;
197
- c.fillStyle = color; c.textAlign = 'left'; c.textBaseline = 'top';
198
- let lineStr = ''; let yy = y;
199
- for (const ch of str) { const test = lineStr + ch; if (c.measureText(test).width > maxWidth && lineStr) { c.fillText(lineStr, x, yy); yy += lineHeight; lineStr = ch; } else lineStr = test; }
200
- if (lineStr) c.fillText(lineStr, x, yy);
201
- return yy + lineHeight;
202
- }
203
- function hashNoise(i) { const x = Math.sin(i * 17.17 + 3.2) * 43758.5453; return x - Math.floor(x); }
204
-
205
- function drawBackground(c, w, h, scene, t) {
206
- const g = c.createLinearGradient(0, 0, w, h); g.addColorStop(0, '#080b16'); g.addColorStop(.55, '#0c1021'); g.addColorStop(1, '#10192c'); c.fillStyle = g; c.fillRect(0, 0, w, h);
207
- c.save(); c.globalAlpha = .2;
208
- for (let i = 0; i < 70; i++) { const x = hashNoise(i) * w; const y = hashNoise(i + 99) * h; circle(c, x, y, 1 + hashNoise(i + 31) * 2, '#b4c4ff'); }
209
- c.globalAlpha = .12; line(c, w * .08, h * .78, w * .92, h * .78, scene.color, 1); line(c, w * .08, h * .78, w * .08, h * .18, scene.color, 1); c.restore();
210
- const glow = c.createRadialGradient(w * .72, h * .2, 0, w * .72, h * .2, Math.max(w, h) * .7); glow.addColorStop(0, `${scene.color}20`); glow.addColorStop(1, 'transparent'); c.fillStyle = glow; c.fillRect(0, 0, w, h);
211
- const drift = (t * 18) % 80; c.save(); c.globalAlpha = .05; for (let x = -80; x < w + 80; x += 80) line(c, x + drift, 0, x - 200 + drift, h, '#fff', 1); c.restore();
212
- }
213
-
214
- function drawHeader(c, w, h, scene, local) {
215
- const pad = w * (orientation === 'portrait' ? .09 : .07);
216
- text(c, '经济学寓言 / ORIGINAL MOTION ESSAY', pad, h * (orientation === 'portrait' ? .1 : .11), orientation === 'portrait' ? 13 : 15, '#a4abc2', 650);
217
- const badgeW = orientation === 'portrait' ? 108 : 126; roundedRect(c, w - pad - badgeW, h * .075, badgeW, 28, 14); c.fillStyle = `${scene.color}1f`; c.fill(); text(c, scene.chapter, w - pad - badgeW / 2, h * .075 + 14, 11, scene.color, 700, 'center');
218
- const p = smooth(Math.min(1, local / 1.25));
219
- c.save(); c.globalAlpha = p; text(c, scene.title, pad, h * (orientation === 'portrait' ? .19 : .21), orientation === 'portrait' ? 34 : 42, '#f6f7fb', 800); text(c, scene.kicker, pad, h * (orientation === 'portrait' ? .24 : .285), orientation === 'portrait' ? 15 : 18, scene.color, 650); c.restore();
220
- }
221
-
222
- function drawMonkey(c, x, y, scale, mood = 'neutral', accent = '#d9a776', t = 0) {
223
- c.save(); c.translate(x, y); c.scale(scale, scale);
224
- const bob = Math.sin(t * 4 + x * .01) * 2; c.translate(0, bob);
225
- circle(c, -25, -40, 17, '#b77c5a'); circle(c, 25, -40, 17, '#b77c5a'); circle(c, 0, -42, 30, '#d9a776'); circle(c, 0, -36, 22, '#f0c79d');
226
- circle(c, -9, -47, 3, '#111827'); circle(c, 9, -47, 3, '#111827');
227
- if (mood === 'scared') { line(c, -10, -28, 10, -28, '#422f2a', 3); } else { c.beginPath(); c.arc(0, -29, 9, 0, Math.PI); c.strokeStyle = '#422f2a'; c.lineWidth = 3; c.stroke(); }
228
- c.strokeStyle = accent; c.lineWidth = 8; c.lineCap = 'round'; line(c, -25, 0, -35, 38, accent, 8); line(c, 25, 0, 35, 38, accent, 8); line(c, -19, 4, -42, 20, accent, 8); line(c, 19, 4, 42, 20, accent, 8);
229
- c.fillStyle = accent; c.beginPath(); c.ellipse(-35, 42, 12, 5, 0, 0, TAU); c.fill(); c.beginPath(); c.ellipse(35, 42, 12, 5, 0, 0, TAU); c.fill();
230
- c.restore();
231
- }
232
- function drawLadder(c, x, y, height, width, color = '#e7b55c', tilt = 0) {
233
- c.save(); c.translate(x, y); c.rotate(tilt); c.strokeStyle = color; c.lineWidth = 7; c.lineCap = 'round'; line(c, -width / 2, 0, -width / 2, -height, color, 7); line(c, width / 2, 0, width / 2, -height, color, 7); for (let yy = -20; yy > -height + 5; yy -= 27) line(c, -width / 2, yy, width / 2, yy, color, 5); c.restore();
234
- }
235
- function drawBanana(c, x, y, scale, glow = false) {
236
- c.save(); c.translate(x, y); c.scale(scale, scale); if (glow) { c.globalAlpha = .18; circle(c, 0, 0, 72, '#f4b33e'); c.globalAlpha = 1; }
237
- c.strokeStyle = '#f4c453'; c.lineWidth = 20; c.lineCap = 'round'; c.beginPath(); c.arc(0, 0, 50, -.2, Math.PI * .85); c.stroke(); c.strokeStyle = '#cf8b2f'; c.lineWidth = 6; c.beginPath(); c.arc(0, 0, 50, -.2, Math.PI * .85); c.stroke(); circle(c, 54, -7, 6, '#a96b29'); circle(c, -38, 38, 6, '#a96b29'); c.restore();
238
- }
239
- function drawWater(c, x, y, w, h, alphaValue = .8, t = 0) {
240
- c.save(); c.globalAlpha = alphaValue; c.strokeStyle = '#66d5f6'; c.lineWidth = 3; c.lineCap = 'round'; for (let i = 0; i < 12; i++) { const xx = x + (i / 11) * w; const sway = Math.sin(t * 8 + i) * 8; line(c, xx, y, xx + sway, y + h * (.5 + hashNoise(i) * .5), '#66d5f6', 3); circle(c, xx + sway, y + h * (.5 + hashNoise(i) * .5), 3, '#b5f0ff'); } c.restore();
241
- }
242
- function drawBananaScene(c, w, h, scene, local) {
243
- const portrait = orientation === 'portrait'; const cx = portrait ? w * .5 : w * .69; const top = portrait ? h * .37 : h * .53; const floor = portrait ? h * .78 : h * .78; const p = smooth(Math.min(1, local / 2));
244
- drawBanana(c, cx, top - 150, portrait ? 1.1 : 1.35, true); line(c, cx, top - 120, cx, top + 6, '#9a6d4d', 4); drawLadder(c, cx, top + 190, 300, 80, '#e8bd71');
245
- line(c, portrait ? w * .1 : w * .25, floor, portrait ? w * .9 : w * .88, floor, '#5b6a89', 5);
246
- const count = portrait ? 3 : 5; for (let i = 0; i < count; i++) { const x = portrait ? w * (.24 + i * .26) : w * (.3 + i * .14); drawMonkey(c, x, floor - 42, portrait ? .82 : .72, 'neutral', i % 2 ? '#ce9d75' : '#c48762', local); }
247
- if (scene.id === 'hook') { const scale = .9 + Math.sin(local * 2) * .03; c.save(); c.globalAlpha = p; text(c, '这是一道经济学题', portrait ? w * .5 : w * .13, portrait ? h * .91 : h * .86, portrait ? 18 : 22, '#dce3fa', 700, portrait ? 'center' : 'left'); c.restore(); }
248
- if (scene.id === 'first') { const climb = smooth(clamp(local / 7)); drawMonkey(c, cx, floor - 35 - climb * 180, .75, 'neutral', '#d09a73', local); drawWater(c, cx - 100, top - 60, 200, 170, smooth(clamp((local - 7) / 2)), local); text(c, '收益', cx + 115, top - 164, 16, '#f4c453', 700); text(c, '代价', cx + 120, top + 20, 16, '#66d5f6', 700); }
249
- if (scene.id === 'punish') { for (let i = 0; i < 3; i++) { const angle = -1.3 + i * 1.3; line(c, cx + Math.cos(angle) * 110, floor - 100, cx + Math.cos(angle) * 70, floor - 55, '#ef6b69', 7); } drawWater(c, cx - 100, top - 40, 200, 170, .5, local); text(c, '惩罚变成了动作', cx, h * .86, 18, '#ef8b88', 700, 'center'); }
250
- if (scene.id === 'replacement') { const newcomerX = portrait ? w * .22 : w * .34; drawMonkey(c, newcomerX, floor - 45, .82, 'scared', '#a98bf2', local); circle(c, newcomerX, floor - 160, 38, '#a98bf220', '#a98bf2', 2); text(c, '?', newcomerX, floor - 160, 28, '#a98bf2', 800, 'center'); }
251
- if (scene.id === 'loop') { drawWater(c, cx - 100, top - 40, 200, 170, .08, local); for (let i = 0; i < 3; i++) { const x1 = portrait ? w * (.2 + i * .3) : w * (.34 + i * .12); line(c, x1, floor - 125, x1 + (i === 1 ? -24 : 24), floor - 70, '#70a8ff', 6); } text(c, '冷水已经消失', cx, h * .86, 18, '#8eb9ff', 700, 'center'); }
252
- }
253
-
254
- function drawModelScene(c, w, h, scene, local) {
255
- const portrait = orientation === 'portrait'; const pad = w * (portrait ? .1 : .09); const graphY = portrait ? h * .66 : h * .68; const graphX = pad; const graphW = w - pad * 2; const graphH = portrait ? h * .22 : h * .28;
256
- const p = smooth(clamp(local / 2)); c.save(); c.globalAlpha = p;
257
- line(c, graphX, graphY - graphH, graphX, graphY, '#7784a4', 2); line(c, graphX, graphY, graphX + graphW, graphY, '#7784a4', 2); text(c, '行为成本', graphX - 12, graphY - graphH - 10, 12, '#96a1bb', 500, 'right'); text(c, '行动:拿香蕉 →', graphX + graphW, graphY + 25, 12, '#96a1bb', 500, 'right');
258
- const pivot = graphX + graphW * .54; c.beginPath(); c.moveTo(graphX, graphY - graphH * .18); c.bezierCurveTo(graphX + graphW * .18, graphY - graphH * .2, pivot - 40, graphY - graphH * .2, pivot, graphY - graphH * .85); c.bezierCurveTo(pivot + 50, graphY - graphH * .9, graphX + graphW * .8, graphY - graphH * .1, graphX + graphW, graphY - graphH * .08); c.strokeStyle = scene.color; c.lineWidth = 5; c.stroke();
259
- const dotX = lerp(graphX + graphW * .24, graphX + graphW * .7, smooth(clamp(local / 8))); const dotY = graphY - graphH * .2 - smooth(clamp(local / 8)) * graphH * .55; circle(c, dotX, dotY, 10, '#fff', scene.color, 5); roundedRect(c, dotX - 74, dotY - 45, 150, 28, 14); c.fillStyle = '#18213a'; c.fill(); text(c, local < 8 ? '收益被看见' : '惩罚被内化', dotX, dotY - 31, 12, '#f5f7ff', 650, 'center'); c.restore();
260
- // tiny ladder as the hidden variable
261
- const bx = portrait ? w * .5 : w * .75; const by = portrait ? h * .36 : h * .45; drawLadder(c, bx, by + 90, 130, 42, '#e3b466', Math.sin(local) * .02); drawBanana(c, bx, by - 10, .6, true); text(c, '看得见的行为', bx, by + 140, 14, '#dce3fa', 600, 'center');
262
- }
263
-
264
- function drawRealityScene(c, w, h, scene, local) {
265
- const portrait = orientation === 'portrait'; const items = [
266
- ['流程', '审批三次\n因为一直这样'], ['按钮', '新人别碰\n没人解释'], ['文化', '默认选项\n变成常识']
267
- ]; const baseY = portrait ? h * .38 : h * .48; const gap = portrait ? w * .29 : w * .25; const start = portrait ? w * .19 : w * .2;
268
- items.forEach((item, i) => { const x = start + i * gap; const rise = smooth(clamp((local - i * .8) / 1.5)); c.save(); c.globalAlpha = rise; c.translate(x, baseY + (1 - rise) * 30); roundedRect(c, x - 70, baseY - 70, 140, 140, 18); c.fillStyle = '#17213a'; c.fill(); c.strokeStyle = i === 1 ? scene.color : '#334365'; c.lineWidth = 2; c.stroke(); circle(c, x, baseY - 32, 18, `${scene.color}2c`, scene.color, 2); text(c, i === 0 ? '≡' : i === 1 ? '□' : '↻', x, baseY - 32, 20, scene.color, 800, 'center'); text(c, item[0], x, baseY + 10, 17, '#f5f7ff', 700, 'center'); item[1].split('\n').forEach((lineText, j) => text(c, lineText, x, baseY + 38 + j * 18, 11, '#aeb8d2', 500, 'center')); c.restore(); if (i < 2) { const arrowP = smooth(clamp((local - 1.2 - i * .8) / 1.2)); line(c, x + 82, baseY, x + gap - 82, baseY, `${scene.color}88`, 3); text(c, '→', x + gap - 72, baseY - 3, 24, scene.color, 700, 'center'); if (arrowP > 0) { circle(c, lerp(x + 82, x + gap - 82, arrowP), baseY, 5, scene.color); } } });
269
- text(c, '过去的风险 → 今天的默认', w * .5, portrait ? h * .72 : h * .77, portrait ? 20 : 24, scene.color, 750, 'center');
270
- }
271
-
272
- function drawExitScene(c, w, h, scene, local) {
273
- const portrait = orientation === 'portrait'; const cx = w * .5; const cy = portrait ? h * .48 : h * .5; const p = smooth(clamp(local / 2)); const r = (portrait ? w : h) * (.12 + smooth(clamp(local / 9)) * .18);
274
- c.save(); c.globalAlpha = p; c.strokeStyle = `${scene.color}99`; c.lineWidth = 2; for (let i = 0; i < 3; i++) { c.beginPath(); c.arc(cx, cy, r + i * 24, 0, TAU); c.stroke(); } circle(c, cx, cy, r * .56, '#15233a', scene.color, 3); text(c, '问', cx, cy - 5, portrait ? 44 : 56, '#f7f4ed', 800, 'center'); text(c, '冷水还在吗?', cx, cy + r + 60, portrait ? 24 : 32, scene.color, 750, 'center'); c.restore();
275
- const qs = ['风险还在吗?', '代价谁承担?', '有更低成本的方案吗?']; qs.forEach((q, i) => { const yy = portrait ? h * (.72 + i * .06) : h * (.76 + (i - 1) * .08); const qP = smooth(clamp((local - 2 - i * .7) / 1.2)); c.save(); c.globalAlpha = qP; roundedRect(c, cx - (portrait ? 135 : 150), yy - 18, portrait ? 270 : 300, 36, 18); c.fillStyle = '#18213a'; c.fill(); text(c, q, cx, yy, portrait ? 14 : 16, '#dbe6ff', 650, 'center'); c.restore(); });
276
- }
277
-
278
- function render(t = currentTime) {
279
- const w = canvas.width; const h = canvas.height; const idx = activeScene(t); const scene = scenes[idx]; const local = t - scene.start;
280
- ctx.clearRect(0, 0, w, h); drawBackground(ctx, w, h, scene, t); drawHeader(ctx, w, h, scene, local);
281
- if (['hook', 'first', 'punish', 'replacement', 'loop'].includes(scene.id)) drawBananaScene(ctx, w, h, scene, local); else if (scene.id === 'model') drawModelScene(ctx, w, h, scene, local); else if (scene.id === 'reality') drawRealityScene(ctx, w, h, scene, local); else drawExitScene(ctx, w, h, scene, local);
282
- // bottom caption strip is intentionally within the safe zone
283
- const pad = w * (orientation === 'portrait' ? .09 : .07); const caption = scene.voice; const captionY = orientation === 'portrait' ? h * .87 : h * .91; ctx.save(); ctx.globalAlpha = .93; roundedRect(ctx, pad, captionY - 28, w - pad * 2, orientation === 'portrait' ? 76 : 58, 16); ctx.fillStyle = '#090d1ddd'; ctx.fill(); ctx.strokeStyle = `${scene.color}38`; ctx.lineWidth = 1; ctx.stroke(); wrapText(ctx, caption, pad + 18, captionY - 17, w - pad * 2 - 36, orientation === 'portrait' ? 20 : 18, orientation === 'portrait' ? 13 : 14, '#e6ebfb', 500); ctx.restore();
284
- }
285
-
286
- function tick(stamp) {
287
- if (!lastStamp) lastStamp = stamp;
288
- const delta = Math.min(.05, (stamp - lastStamp) / 1000); lastStamp = stamp;
289
- if (playing) { currentTime += delta; if (currentTime >= DURATION) { currentTime = 0; playing = false; playButton.innerHTML = '<span class="play-icon">▶</span><span>播放</span>'; } updateUi(); render(currentTime); }
290
- rafId = requestAnimationFrame(tick);
291
- }
292
-
293
- function setPlaying(value) {
294
- playing = value; lastStamp = 0; playButton.innerHTML = playing ? '<span class="play-icon">Ⅱ</span><span>暂停</span>' : '<span class="play-icon">▶</span><span>播放</span>'; renderStatus.textContent = playing ? '实时播放' : '实时画布';
295
- if (playing && !muted) ensureAudio().startPreview();
296
- }
297
-
298
- function ensureAudio() {
299
- if (audio) return audio;
300
- const ac = new (window.AudioContext || window.webkitAudioContext)(); const master = ac.createGain(); master.gain.value = .18; master.connect(ac.destination); const mediaDest = ac.createMediaStreamDestination(); master.connect(mediaDest);
301
- let started = false;
302
- function blip(type = 'tick', at = ac.currentTime) { const osc = ac.createOscillator(); const g = ac.createGain(); const freq = type === 'spray' ? 760 : type === 'drop' ? 180 : 420; osc.type = type === 'spray' ? 'triangle' : 'sine'; osc.frequency.setValueAtTime(freq, at); osc.frequency.exponentialRampToValueAtTime(freq * .55, at + (type === 'spray' ? .25 : .12)); g.gain.setValueAtTime(.0001, at); g.gain.exponentialRampToValueAtTime(type === 'spray' ? .15 : .09, at + .012); g.gain.exponentialRampToValueAtTime(.0001, at + (type === 'spray' ? .28 : .16)); osc.connect(g).connect(master); osc.start(at); osc.stop(at + .32); }
303
- function scheduleMusic(startAt = ac.currentTime + .05, offset = 0, duration = DURATION) { const notes = [55, 65.41, 73.42, 82.41, 73.42, 65.41, 49, 65.41]; for (let i = 0; i < Math.ceil(duration / 1.5); i++) { const at = startAt + i * 1.5; const root = notes[(i + offset) % notes.length]; const osc = ac.createOscillator(); const g = ac.createGain(); osc.type = i % 4 === 0 ? 'triangle' : 'sine'; osc.frequency.setValueAtTime(root, at); g.gain.setValueAtTime(.0001, at); g.gain.linearRampToValueAtTime(i % 4 === 0 ? .055 : .025, at + .08); g.gain.exponentialRampToValueAtTime(.0001, at + 1.32); osc.connect(g).connect(master); osc.start(at); osc.stop(at + 1.35); if (i % 4 === 0) blip('drop', at); } }
304
- function startPreview() { if (ac.state === 'suspended') ac.resume(); if (!started) { scheduleMusic(); started = true; } }
305
- async function prepareExport() { const exportAc = new (window.AudioContext || window.webkitAudioContext)(); const out = exportAc.createGain(); out.gain.value = .2; out.connect(exportAc.destination); const dest = exportAc.createMediaStreamDestination(); out.connect(dest); const notes = [55, 65.41, 73.42, 82.41, 73.42, 65.41, 49, 65.41]; const start = exportAc.currentTime + .08; for (let i = 0; i < Math.ceil(DURATION / 1.5); i++) { const at = start + i * 1.5; const osc = exportAc.createOscillator(); const g = exportAc.createGain(); osc.type = i % 4 === 0 ? 'triangle' : 'sine'; osc.frequency.setValueAtTime(notes[i % notes.length], at); g.gain.setValueAtTime(.0001, at); g.gain.linearRampToValueAtTime(i % 4 === 0 ? .055 : .025, at + .08); g.gain.exponentialRampToValueAtTime(.0001, at + 1.32); osc.connect(g).connect(out); osc.start(at); osc.stop(at + 1.35); }
306
- scenes.forEach((s, i) => { const at = start + s.start; const osc = exportAc.createOscillator(); const g = exportAc.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(i % 2 ? 640 : 420, at); g.gain.setValueAtTime(.0001, at); g.gain.linearRampToValueAtTime(.04, at + .02); g.gain.exponentialRampToValueAtTime(.0001, at + .15); osc.connect(g).connect(out); osc.start(at); osc.stop(at + .18); });
307
- try { const response = await fetch('/audio/narration.wav', { cache: 'no-store' }); if (response.ok) { const buffer = await response.arrayBuffer(); const narration = await exportAc.decodeAudioData(buffer); const source = exportAc.createBufferSource(); source.buffer = narration; source.connect(out); source.start(start); } } catch (error) { console.info('Optional narration asset not found; exporting captions, music and SFX.', error); }
308
- return { context: exportAc, dest, startAt: start };
309
- }
310
- audio = { ac, master, startPreview, sfx: blip, prepareExport, toggle() { muted = !muted; master.gain.value = muted ? 0 : .18; if (muted && 'speechSynthesis' in window) window.speechSynthesis.cancel(); muteButton.textContent = muted ? '⊘' : '◖'; } };
311
- return audio;
312
- }
313
-
314
- async function speakCurrent() { if (!('speechSynthesis' in window)) return; window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(scenes[activeScene()].voice); utterance.lang = 'zh-CN'; utterance.rate = .96; utterance.pitch = 1.03; window.speechSynthesis.speak(utterance); }
315
-
316
- function supportedMime() { const types = ['video/mp4;codecs="avc1.42E01E,mp4a.40.2"', 'video/mp4', 'video/webm;codecs=vp9,opus', 'video/webm']; return types.find((t) => window.MediaRecorder?.isTypeSupported?.(t)) || ''; }
317
- async function transcodeWebmToMp4(webmBlob) {
318
- // Lazy-load only when the browser cannot encode MP4 natively. Conversion stays in-browser.
319
- const ffmpegModule = await import(/* @vite-ignore */ 'https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.12.10/+esm');
320
- const util = await import(/* @vite-ignore */ 'https://cdn.jsdelivr.net/npm/@ffmpeg/util@0.12.1/+esm');
321
- const ffmpeg = new ffmpegModule.FFmpeg();
322
- ffmpeg.on('progress', ({ progress }) => {
323
- const pct = Math.round(100 * clamp(progress));
324
- exportLabel.textContent = `本地转码 MP4 · ${pct}%`;
325
- exportPercent.textContent = `${pct}%`;
326
- exportBar.style.width = `${pct}%`;
327
- });
328
- const coreBase = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd';
329
- await ffmpeg.load({
330
- coreURL: await util.toBlobURL(`${coreBase}/ffmpeg-core.js`, 'text/javascript'),
331
- wasmURL: await util.toBlobURL(`${coreBase}/ffmpeg-core.wasm`, 'application/wasm')
332
- });
333
- await ffmpeg.writeFile('input.webm', await util.fetchFile(webmBlob));
334
- await ffmpeg.exec(['-i', 'input.webm', '-c:v', 'libx264', '-c:a', 'aac', '-pix_fmt', 'yuv420p', '-movflags', 'faststart', 'output.mp4']);
335
- const data = await ffmpeg.readFile('output.mp4');
336
- await ffmpeg.deleteFile('input.webm');
337
- await ffmpeg.deleteFile('output.mp4');
338
- return new Blob([data.buffer], { type: 'video/mp4' });
339
- }
340
- async function exportVideo() {
341
- if (!window.MediaRecorder || !canvas.captureStream) { exportLabel.textContent = '当前浏览器不支持画布录制'; return; }
342
- exportButton.disabled = true; exportProgress.hidden = false; exportBar.style.width = '0%'; exportPercent.textContent = '0%'; exportLabel.textContent = '正在准备本地音轨…';
343
- const audioPack = await ensureAudio().prepareExport(); const videoStream = canvas.captureStream(FPS); const combined = new MediaStream([...videoStream.getVideoTracks(), ...audioPack.dest.stream.getAudioTracks()]); const mime = supportedMime(); const chunks = []; const recorder = new MediaRecorder(combined, mime ? { mimeType: mime, videoBitsPerSecond: 7_000_000 } : undefined);
344
- const startedAt = performance.now(); const previousTime = currentTime; const previousPlaying = playing; const previousOrientation = orientation;
345
- recorder.ondataavailable = (e) => { if (e.data.size) chunks.push(e.data); };
346
- const finished = new Promise((resolve) => { recorder.onstop = resolve; });
347
- recorder.start(250); renderStatus.textContent = '正在导出';
348
- await new Promise((resolve) => { let last = performance.now(); function frame(now) { const elapsed = Math.min(DURATION, (now - startedAt) / 1000); currentTime = elapsed; render(currentTime); updateUi(); const pct = Math.round(elapsed / DURATION * 100); exportBar.style.width = `${pct}%`; exportPercent.textContent = `${pct}%`; exportLabel.textContent = `渲染第 ${fmt(elapsed)} / ${fmt(DURATION)} 帧`; if (elapsed >= DURATION) { recorder.stop(); resolve(); } else { last = now; requestAnimationFrame(frame); } } requestAnimationFrame(frame); });
349
- await finished; videoStream.getTracks().forEach((track) => track.stop()); audioPack.dest.stream.getTracks().forEach((track) => track.stop()); await audioPack.context.close();
350
- const sourceBlob = new Blob(chunks, { type: mime || 'video/webm' });
351
- let outputBlob = sourceBlob;
352
- let isMp4 = (mime || '').includes('mp4');
353
- if (!isMp4) {
354
- exportLabel.textContent = '浏览器原生不支持 MP4,准备本地转码…';
355
- try {
356
- outputBlob = await transcodeWebmToMp4(sourceBlob);
357
- isMp4 = true;
358
- } catch (error) {
359
- console.warn('Local MP4 conversion unavailable; keeping WebM fallback.', error);
360
- exportLabel.textContent = '本地 MP4 转码不可用,保留兼容 WebM';
361
- }
362
- }
363
- const ext = isMp4 ? 'mp4' : 'webm'; const url = URL.createObjectURL(outputBlob); const a = document.createElement('a'); a.href = url; a.download = `收猴子的故事-${orientation === 'portrait' ? '抖音竖屏' : 'B站横屏'}.${ext}`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 2000);
364
- currentTime = previousTime; playing = previousPlaying; orientation = previousOrientation; resizeStage(); updateUi(); exportBar.style.width = '100%'; exportPercent.textContent = '完成'; exportLabel.textContent = isMp4 ? 'MP4 已保存到下载目录' : '当前浏览器不支持 MP4,已保存兼容 WebM'; renderStatus.textContent = '实时画布'; exportButton.disabled = false;
365
- }
366
-
367
- function fillCopy() { for (const key of ['bilibili', 'douyin']) { document.querySelector(`#${key}Title`).value = copy[key].title; document.querySelector(`#${key}Description`).value = copy[key].description; document.querySelector(`#${key}Tags`).value = copy[key].tags; } }
368
- function copyPack(key, button) { const pack = copy[key]; const val = `标题:${pack.title}\n\n简介:${pack.description}\n\n标签:${pack.tags}`; navigator.clipboard?.writeText(val).then(() => { const original = button.textContent; button.textContent = '已复制'; setTimeout(() => button.textContent = original, 1600); }).catch(() => { const ta = document.createElement('textarea'); ta.value = val; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); button.textContent = '已复制'; setTimeout(() => button.textContent = '复制整套', 1600); }); }
369
- function drawCover(target, isPortrait) { const c = target.getContext('2d'); const w = target.width; const h = target.height; const s = scenes[0]; const grad = c.createLinearGradient(0, 0, w, h); grad.addColorStop(0, '#080b16'); grad.addColorStop(1, '#19213d'); c.fillStyle = grad; c.fillRect(0, 0, w, h); for (let i = 0; i < 22; i++) circle(c, hashNoise(i) * w, hashNoise(i + 32) * h, 1 + hashNoise(i + 12) * 3, '#a8b9ff55'); const x = isPortrait ? w * .5 : w * .7; const floor = isPortrait ? h * .72 : h * .78; drawBanana(c, x, isPortrait ? h * .22 : h * .3, isPortrait ? .72 : .9, true); drawLadder(c, x, floor - 40, isPortrait ? 230 : 180, isPortrait ? 54 : 46, '#e8bd71'); for (let i = 0; i < (isPortrait ? 3 : 5); i++) drawMonkey(c, isPortrait ? w * (.25 + i * .25) : w * (.3 + i * .14), floor, isPortrait ? .5 : .44, 'neutral', '#c9946d', 0); text(c, '收猴子的故事', isPortrait ? w * .1 : w * .08, isPortrait ? h * .82 : h * .82, isPortrait ? 25 : 28, '#f7f4ed', 800); text(c, '为什么没人敢拿?', isPortrait ? w * .1 : w * .08, isPortrait ? h * .88 : h * .9, isPortrait ? 15 : 17, '#f4b33e', 650); }
370
- function downloadCanvas(target, name) { target.toBlob((blob) => { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }, 'image/png'); }
371
-
372
- playButton.addEventListener('click', () => setPlaying(!playing));
373
- document.querySelector('#restartButton').addEventListener('click', () => { currentTime = 0; setPlaying(true); updateUi(); render(currentTime); });
374
- document.querySelector('#speakButton').addEventListener('click', speakCurrent);
375
- muteButton.addEventListener('click', () => ensureAudio().toggle());
376
- scrub.addEventListener('input', (e) => { currentTime = Number(e.target.value); updateUi(); render(currentTime); });
377
- document.querySelectorAll('[data-orientation]').forEach((button) => button.addEventListener('click', () => { orientation = button.dataset.orientation; document.querySelectorAll('[data-orientation]').forEach((b) => b.classList.toggle('active', b === button)); resizeStage(); }));
378
- exportButton.addEventListener('click', exportVideo);
379
- document.querySelectorAll('.copy-button').forEach((button) => button.addEventListener('click', () => copyPack(button.dataset.copy, button)));
380
- const landscapeCover = document.querySelector('#landscapeCover'); const portraitCover = document.querySelector('#portraitCover'); drawCover(landscapeCover, false); drawCover(portraitCover, true); document.querySelector('#downloadLandscape').addEventListener('click', () => downloadCanvas(landscapeCover, '收猴子的故事-横屏封面.png')); document.querySelector('#downloadPortrait').addEventListener('click', () => downloadCanvas(portraitCover, '收猴子的故事-竖屏封面.png'));
381
-
382
- buildChapters(); fillCopy(); updateUi(); resizeStage(); render(0); rafId = requestAnimationFrame(tick);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/styles.css DELETED
@@ -1,105 +0,0 @@
1
- @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;600;700;800&display=swap');
2
-
3
- :root { color-scheme: dark; font-family: Inter, "Noto Sans SC", system-ui, sans-serif; background: #080a12; color: #f4f6fb; font-synthesis: none; --bg: #080a12; --panel: #0e1220; --panel-2: #11172a; --stroke: #202941; --muted: #8f9ab6; --muted-2: #65718e; --yellow: #f4b33e; --cyan: #71d6c5; }
4
- * { box-sizing: border-box; }
5
- body { margin: 0; min-width: 320px; background: radial-gradient(circle at 20% 0%, #12182a 0%, var(--bg) 42%); }
6
- button, textarea, input { font: inherit; }
7
- button { cursor: pointer; }
8
- button:disabled { cursor: wait; opacity: .6; }
9
- .shell { width: min(1600px, 100%); margin: 0 auto; padding: 0 34px 38px; }
10
- .topbar { height: 76px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #1a2236; }
11
- .brand { display: flex; align-items: center; gap: 12px; }
12
- .brand-mark { display: grid; place-items: center; width: 35px; height: 35px; border-radius: 12px; background: #f4b33e; color: #111522; font-size: 25px; font-weight: 800; transform: rotate(-9deg); }
13
- .brand strong { display: block; letter-spacing: .12em; font-size: 12px; }
14
- .brand span:not(.brand-mark) { display: block; color: var(--muted); font-size: 11px; margin-top: 3px; }
15
- .top-status { display: flex; align-items: center; color: #98a4c1; font: 500 11px "DM Mono", monospace; letter-spacing: .04em; }
16
- .live-dot { width: 7px; height: 7px; border-radius: 50%; background: #68ddb1; box-shadow: 0 0 0 4px #68ddb11c; margin-right: 8px; }
17
- .status-divider { width: 1px; height: 16px; background: #2a3550; margin: 0 13px; }
18
- .workspace { display: grid; grid-template-columns: minmax(0, 1.54fr) minmax(350px, .76fr); gap: 28px; padding-top: 30px; }
19
- .stage-panel { min-width: 0; }
20
- .stage-shell { position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 17px; background: #080b16; border: 1px solid #2a3552; box-shadow: 0 24px 80px #0008, inset 0 1px #ffffff0e; }
21
- .stage-shell.portrait { width: min(100%, 500px); aspect-ratio: 9 / 16; margin: 0 auto; }
22
- #stage { display: block; width: 100%; height: 100%; }
23
- .stage-hud { position: absolute; left: 18px; right: 18px; top: 14px; display: flex; justify-content: space-between; color: #d3d9ebcc; font: 500 10px "DM Mono", monospace; letter-spacing: .04em; pointer-events: none; }
24
- .stage-safe { position: absolute; left: 10%; right: 10%; color: #ffffff22; font: 400 8px "DM Mono", monospace; pointer-events: none; opacity: 0; transition: opacity .2s; }
25
- .stage-shell.portrait .stage-safe { opacity: 1; }
26
- .safe-top { top: 5%; border-top: 1px dashed #ffffff26; padding-top: 4px; }
27
- .safe-bottom { bottom: 5%; border-bottom: 1px dashed #ffffff26; padding-bottom: 4px; text-align: right; }
28
- .controls { display: flex; align-items: center; gap: 10px; padding: 14px 0 10px; }
29
- .primary-control, .icon-control, .chip, .copy-button, .cover-actions button { border: 1px solid #2b3754; color: #dfe6fb; background: #11182a; border-radius: 8px; }
30
- .primary-control { height: 34px; padding: 0 13px; display: flex; align-items: center; gap: 8px; background: #f4b33e; color: #151a27; border-color: #f4b33e; font-weight: 750; font-size: 12px; }
31
- .play-icon { font-size: 11px; }
32
- .icon-control { width: 34px; height: 34px; font-size: 17px; color: #acb8d0; }
33
- .primary-control:hover, .icon-control:hover, .chip:hover, .copy-button:hover, .cover-actions button:hover { filter: brightness(1.17); }
34
- .scrub { position: relative; flex: 1; height: 24px; display: flex; align-items: center; }
35
- .scrub input { position: relative; z-index: 2; width: 100%; appearance: none; background: transparent; }
36
- .scrub input::-webkit-slider-runnable-track { height: 3px; background: #2a3550; border-radius: 4px; }
37
- .scrub input::-webkit-slider-thumb { appearance: none; width: 11px; height: 11px; margin-top: -4px; border-radius: 50%; background: #f4b33e; border: 2px solid #f8d98e; box-shadow: 0 0 0 3px #f4b33e2a; }
38
- .scrub-progress { position: absolute; z-index: 1; left: 0; top: 10px; width: 0; height: 3px; background: #f4b33e; border-radius: 4px; pointer-events: none; }
39
- .frame-rate { color: #687492; font: 500 10px "DM Mono", monospace; }
40
- .orientation-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; color: var(--muted); }
41
- .control-label { font-size: 11px; margin-right: 4px; }
42
- .chip { height: 28px; padding: 0 10px; color: #96a2be; font-size: 11px; }
43
- .chip.active { color: #f7d891; border-color: #a2742d; background: #f4b33e17; }
44
- .safe-note { color: #596783; font-size: 10px; margin-left: 5px; }
45
- .inspector { border-left: 1px solid #202a43; padding-left: 28px; min-width: 0; }
46
- .inspector-head { display: flex; justify-content: space-between; align-items: flex-start; }
47
- .eyebrow { display: block; color: #697693; font: 500 10px "DM Mono", monospace; letter-spacing: .12em; }
48
- h1 { font-size: clamp(24px, 3vw, 36px); line-height: 1.1; letter-spacing: -.04em; margin: 8px 0 0; }
49
- .duration-badge { border: 1px solid #2c3854; color: #aeb9d4; padding: 6px 9px; border-radius: 7px; font: 500 10px "DM Mono", monospace; }
50
- .lede { color: #99a5bf; line-height: 1.65; margin: 13px 0 20px; font-size: 13px; max-width: 390px; }
51
- .chapter-list { display: flex; flex-direction: column; gap: 3px; max-height: 380px; overflow: auto; padding-right: 4px; }
52
- .chapter-item { display: grid; grid-template-columns: 28px 1fr auto; gap: 9px; align-items: center; text-align: left; width: 100%; min-height: 44px; padding: 7px 8px; border: 1px solid transparent; color: #99a5c0; background: transparent; border-radius: 9px; }
53
- .chapter-item:hover { background: #151d31; }
54
- .chapter-item.active { color: #f5f7fd; background: #171f34; border-color: #2d3b5e; box-shadow: inset 3px 0 var(--yellow); }
55
- .chapter-no { font: 500 10px "DM Mono", monospace; color: #63708c; }
56
- .chapter-item.active .chapter-no { color: var(--yellow); }
57
- .chapter-copy strong { display: block; font-size: 12px; font-weight: 650; }
58
- .chapter-copy small { display: block; color: #65718b; font-size: 10px; margin-top: 3px; }
59
- .chapter-time { color: #697590; font: 500 10px "DM Mono", monospace; }
60
- .inspector-card { border-top: 1px solid #222d47; margin-top: 20px; padding-top: 15px; }
61
- .card-title { display: flex; align-items: center; justify-content: space-between; color: #8190ae; font: 500 10px "DM Mono", monospace; letter-spacing: .07em; text-transform: uppercase; }
62
- .text-button { border: 0; background: transparent; color: #f4b33e; font-size: 11px; padding: 0; }
63
- .voice-card p, .note-card p { color: #c1c9dc; font-size: 12px; line-height: 1.7; margin: 9px 0 0; }
64
- .note-card p { color: #8491ad; }
65
- .tiny-dot { width: 5px; height: 5px; border-radius: 50%; background: #68ddb1; }
66
- .export-button { width: 100%; margin-top: 22px; min-height: 57px; border: 1px solid #73531f; border-radius: 10px; background: linear-gradient(110deg, #f4b33e, #f2c766); color: #171b28; display: grid; grid-template-columns: 24px 1fr 20px; align-items: center; text-align: left; padding: 9px 13px; }
67
- .export-button > span:first-child { font-size: 22px; font-weight: 800; }
68
- .export-button strong, .export-button small { display: block; }
69
- .export-button strong { font-size: 12px; }
70
- .export-button small { font-size: 10px; margin-top: 3px; opacity: .68; }
71
- .export-arrow { font-size: 18px; text-align: right; }
72
- .export-progress { margin-top: 10px; padding: 10px 2px 0; }
73
- .progress-top { display: flex; justify-content: space-between; color: #9aa6c0; font: 500 10px "DM Mono", monospace; }
74
- .progress-track { height: 4px; margin-top: 8px; background: #222c45; border-radius: 4px; overflow: hidden; }
75
- .progress-track span { display: block; height: 100%; width: 0; background: var(--yellow); transition: width .1s; }
76
- .publish-section { margin-top: 56px; border-top: 1px solid #1d263c; padding-top: 25px; }
77
- .section-heading { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 18px; }
78
- h2 { font-size: 22px; letter-spacing: -.04em; margin: 7px 0 0; }
79
- .section-caption { color: #687593; font-size: 11px; }
80
- .publish-grid { display: grid; grid-template-columns: 1fr 1fr 1.05fr; gap: 14px; }
81
- .publish-card, .cover-card { border: 1px solid #212c45; border-radius: 12px; background: #0e1424; padding: 15px; }
82
- .platform-head { display: flex; align-items: center; gap: 9px; margin-bottom: 14px; }
83
- .platform-head > div:not(.cover-previews) { flex: 1; }
84
- .platform-head strong, .platform-head small { display: block; }
85
- .platform-head strong { font-size: 12px; }
86
- .platform-head small { color: #6f7b97; font-size: 10px; margin-top: 3px; }
87
- .platform-icon { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; font-size: 16px; }
88
- .bilibili-icon { background: #73d7eb20; color: #73d7eb; }
89
- .douyin-icon { background: #f06fa820; color: #f59bc5; }
90
- .cover-icon { background: #f4b33e20; color: #f4b33e; }
91
- .copy-button { font-size: 10px; padding: 7px 9px; color: #e4b759; border-color: #684f25; background: #2a2114; white-space: nowrap; }
92
- .publish-card label { display: block; color: #6f7c98; font: 500 9px "DM Mono", monospace; margin-top: 11px; }
93
- .publish-card textarea { resize: vertical; width: 100%; min-height: 34px; border: 1px solid #202b45; border-radius: 7px; background: #0a0f1d; color: #c4ccde; padding: 8px; margin-top: 5px; line-height: 1.55; font: 400 11px "Noto Sans SC", sans-serif; }
94
- .publish-card label:nth-of-type(2) textarea { min-height: 110px; }
95
- .cover-previews { display: flex; gap: 10px; align-items: center; height: 185px; }
96
- .cover-preview { position: relative; background: #090d18; border: 1px solid #29344f; border-radius: 8px; overflow: hidden; }
97
- .cover-preview canvas { display: block; width: 100%; height: 100%; }
98
- .landscape-cover { width: 66%; aspect-ratio: 16 / 9; }
99
- .portrait-cover { width: 30%; height: 100%; }
100
- .cover-preview > span { position: absolute; left: 7px; bottom: 6px; font: 500 9px "DM Mono", monospace; color: #d9e2f7; background: #080b16aa; padding: 3px 5px; border-radius: 4px; }
101
- .cover-actions { display: flex; gap: 8px; margin-top: 12px; }
102
- .cover-actions button { flex: 1; padding: 8px 4px; font-size: 10px; color: #aab6d0; }
103
- .footer { display: flex; justify-content: space-between; color: #56627d; font: 500 10px "DM Mono", monospace; margin-top: 26px; padding-top: 18px; border-top: 1px solid #1a2338; }
104
- @media (max-width: 1080px) { .workspace { grid-template-columns: 1fr; } .inspector { border-left: 0; border-top: 1px solid #202a43; padding: 26px 0 0; } .chapter-list { display: grid; grid-template-columns: repeat(2, 1fr); max-height: none; } .publish-grid { grid-template-columns: 1fr 1fr; } .cover-card { grid-column: 1 / -1; } }
105
- @media (max-width: 670px) { .shell { padding: 0 15px 28px; } .topbar { height: 64px; } .top-status { display: none; } .workspace { padding-top: 18px; gap: 20px; } .stage-shell { border-radius: 11px; } .controls { gap: 6px; } .frame-rate { display: none; } .safe-note { width: 100%; margin: 4px 0 0 52px; } .chapter-list { grid-template-columns: 1fr; } .publish-section { margin-top: 35px; } .section-heading { display: block; } .section-caption { display: block; margin-top: 8px; } .publish-grid { grid-template-columns: 1fr; } .cover-card { grid-column: auto; } .footer { display: block; line-height: 1.8; } .footer span { display: block; } }