Skip to content

Repository files navigation

三角洲行动动画编辑器(Delta Force Animation Editor)

在线站点:https://df.tfr-studio.top · 仓库:https://github.com/TFR-Studios/DeltaForceAnimation

一个面向《三角洲行动》(Delta Force)HUD 动画的 网页版 Lottie/Bodymovin 编辑器:打开网页就能预览、修改并导出 After Effects + Bodymovin 导出的 JSON 动画,不需要安装 AE 或任何后端服务。

内置八套真实游戏动画(撤离动画 / 位置暴露动画 / 核电站功率动画 / 任务弹窗动画 / 地图标题动画 / 黑潮爆破默认弹窗动画 / 大战场弹窗动画 / 撤离点开启动画),并针对 Bodymovin 在浏览器中的各种坑做了大量兼容处理(轨道蒙版、内嵌字体、图像序列、投影、透明视频导出等)。

制作声明(站点顶栏的徽标全文):网站内动画均由人工使用 AE 制作,未使用 AI 辅助。 素材版权:仓库中的动画素材(JSON/PNG/音频)来源于游戏,开源使用前请自行确认授权。


功能总览

  • 零依赖部署:动画 JSON、音频、字体全部打进静态站点,打开即用;在线站点 https://df.tfr-studio.top 无需注册登录,也不会上传任何素材到服务器;
  • 按需加载:首屏只拉当前动画的数据包,切到其他动画时才下载对应数据包(浮层显示实时进度,浏览器缓存后二次切换秒开);
  • 动画画廊:顶栏一条「当前动画」触发器 + 卡片式选择浮层(真实渲染的预览图、搜索、数字键直选、最近用过的自动排前),动画数量继续增长也不用改 UI —— 新增动画只在 ANIMATIONS 注册表加一项;
  • 实时预览:播放/暂停(空格键)、重播、循环、0.1–3× 速度、SVG/Canvas 双渲染器、时间轴拖动;
  • 可视化编辑:
    • 文字图层:改内容、颜色、不透明度(可逐层重置);
    • 形状图层:改填充/描边颜色与图层不透明度(「蒙版」图层除外;纯红填充 / 纯白描边 / 预合成里的紫色描边属于占位色,不展示对应取色器;地图标题动画的「描边」图层是例外,只显示描边);
    • 主题颜色:两个色块把「默认填充色」与「描边默认色」两族颜色值整体换成新颜色(一次改完整套配色);
    • 预合成内容可编辑:顶层图层与预合成(如任务弹窗动画的「框」)内部图层都会列出,名字带「合成名 ›」前缀;
    • 图片纹理:调色 + 图层不透明度;
    • 核电站功率动画:两条 359 帧叠加序列(模糊效果 / 百叶窗)调色 + 五个数字位颜色联动;
    • 弹窗叠加层(撤离动画);
    • 位置暴露动画:图标更换/上传(位置暴露与二次扫描可分别设置)、显示开关、底框不透明度、主段与二次扫描时长;
    • 任务弹窗动画:图标更换/上传(列表来自 animations/animation_4/icon/)+ 图标不透明度,另可改任务名 / 预期报酬 / 数字等文字层;「报酬信息」一个开关可统一显隐预期报酬 / 数字 / 图标;
    • 大战场弹窗动画:预设一键套用(列表来自 animations/animation_7/preset.txt,一项 = 图标 + 主标题 + 内容)、图标更换/上传(列表来自动画自己的 animations/animation_7/images/)、两行占位文字「主标题 / 内容」的文案与颜色、弹窗形状层颜色;
    • 撤离点开启动画:图标更换/上传(列表来自动画自己的 animations/animation_8/images/)、状态文字与文字颜色、六个可见形状层的填充/描边颜色与不透明度;
    • 地图标题动画:改「地图名称 / 地图地点」两行文字与其颜色,以及「描边」图层的白轮廓颜色;改「地图地点」时底框与描边会按文字宽度自动加宽(见下);
  • 视频导出:MP4 / AVI / 透明 AVI(60fps、含音频、支持当前背景色),与预览所见一致;
  • 为浏览器补全 Bodymovin 能力:AE 轨道蒙版、Drop Shadow 投影、逐帧图像序列、内嵌 TTF 字体在 SVG/Canvas 下都能正常渲染。

网站形态(打开站点会看到什么)

站点是单页应用,从加载到编辑的每个环节都有明确的状态反馈:

环节 站内表现
启动 终端风格启动加载界面(22 格进度条 + 百分比),进度条走满 100% 才淡出并进入编辑器,不会出现"白屏后突然可用"
顶栏 品牌 logo + 「三角洲行动动画编辑器」标题 + 制作声明徽标(动画由人工用 AE 制作,未使用 AI 辅助)+ 动画选择触发器(显示当前动画的预览图 / 名称 / 规格 / 总数)
选择动画 点顶栏触发器打开动画画廊:卡片网格 + 搜索 + 数字键直选;卡片上是真实渲染的预览图与规格,最近用过的排在最前
切换动画 首次切到某套动画时弹出数据包加载浮层(实时百分比);数据包下载后由浏览器缓存,再切回来即秒开
预览区 画布框内是真实画面;框外固定为黑底 + 网格 + 平铺的「超出区域,不予显示」提示(纯装饰,不参与导出),方便判断构图
状态栏 页面底部常驻一行状态:已载入哪套动画、字体加载情况、兼容导出提示、导出百分比、错误原因等
导出 全屏导出浮层:格式标签、阶段文案、进度条与百分比、取消按钮;结束后在状态栏给出重复帧自检结果

站点的所有编辑都在浏览器内存中进行,没有账号、没有云端保存,也不会把素材上传到服务器;刷新页面即回到素材原始状态(见「已知边界」)。


内置动画

动画 数据 音频 说明
撤离动画 animations/animation_1/animation_data.json animations/animation_1/gunmuchenggong.mp3 含弹窗叠加层,侧栏可编辑弹窗文字/颜色/宽度
位置暴露动画 animations/animation_2/animation_data.json(+二次扫描段) animations/animation_2/UI_C201_Energy_Scout_Bow_Scout_02.wav 主段 + 可选「二次扫描」段;两段图标可分别替换/隐藏、底框随文字自适应
核电站功率动画 animations/animation_3/animation_data.json + animations/animation_3/png resources/{99999,百叶窗}/ animations/animation_3/反应堆音效.wav 底层 HUD 动画上叠加两条 359 帧 PNG 序列(默认开启);两条序列在侧栏「图片图层」里显示为模糊效果(99999)与百叶窗,可改色/调透明度;另有五个数字位文字层(第一位~第五位)颜色联动
任务弹窗动画 animations/animation_4/animation_data.json + animations/animation_4/images/MallIcon_HafuCoins.png + animations/animation_4/icon/*.png(可选图标,另有 AE 视频图层素材,站点不使用) 无(导出只出视频轨) 行动开始 / 任务名 / 预期报酬 / 数字 四个文字层 + 一枚哈夫币图标;侧栏「图标选择」可换图标、上传自定义图标并调图标不透明度
地图标题动画 animations/animation_5/animation_data.json 无(导出只出视频轨) 「地图名称」+「地图地点」两个文字层(左对齐),配一条底框与一层描边衬板(两层都带闪烁的不透明度关键帧,且随文字宽度自适应),整组由两个 NULL CONTROL 带动入场/退场;纯文字 + 形状,没有图片资源/音频/叠加序列
黑潮爆破默认弹窗动画 animations/animation_6/animation_data.json + animations/animation_6/images/(4 张位图 + 2 张整幅图 + 696 帧逐帧序列;同目录的 .gif 是 AE 视频图层素材,站点不使用) 无(导出只出视频轨) 「主标题 / 副标题」两个文字层 + 一条 HUD 底条 + 三张提示位图 + 一段 696 帧逐帧序列(AE 视频素材导出成 Bodymovin image sequence),再用毛边/方框模糊形状做轨道遮罩叠出「爆破」观感;侧栏可改两行文字;没有「动画时长」区块,时间轴与关键帧完全按 AE 导出原样呈现(258 帧 / 4.30s,见下)
大战场弹窗动画 animations/animation_7/animation_data.json + animations/animation_7/images/(图标素材:当前 27 / 38 / 155 / 167 / car / clol / flag.png)+ animations/animation_7/preset.txt(预设表,当前 5 条:区域烟幕 / 制导导弹 / 战略信标 / 空投载具 / 炮兵齐射) 无(导出只出视频轨) 两行占位文字「主标题 / 内容」(都有投影效果,默认文案是手改本地数据的结果,见下)+ 一条由底板 / 底框 / 虚线 / 蒙版叠出来的 HUD 弹窗 + 一张小图标(默认 167.png,第 30~36 帧淡入);定位是可换图标 + 可套预设的通用弹窗模板 —— 侧栏「预设」一键套用「图标 + 两行文案」(预设表就是那个 txt,改它即可增删),「文字图层」里两行文字与颜色可直接编辑、「图标选择」可在 images/ 的几张图标间切换(也可上传自定义图标,图标一律等比完整显示、不裁切),弹窗本体的形状层(底板 / 底框 / 虚线 / 蒙版效果 / 底线)在「形状图层」列表里可改填充色与透明度;卡片封面特意用高亮圈标出「图标可换」(见 POSTER_HIGHLIGHT);没有「动画时长」区块,时间轴按 AE 导出原样呈现(180 帧 / 3.00s,画面第 144 帧淡完,末尾约 0.6s 空尾)
撤离点开启动画 animations/animation_8/animation_data.json + animations/animation_8/images/(8 张 48×48 图标:23 / 83 / 96 / 106 / 125 / 182 / 188 / 191.png,数据里引用的是 83.png) 无(导出只出视频轨) 一条居中状态文字「紧急停堆撤离点已打开」+ 一张 48×48 小图标(与文字同步,第 3237 帧淡入、第 118124 帧淡出)+ 几层浅灰形状(#cecece / #a3a5a8 的边框与角标,用轨道遮罩做出扫描/开合效果)+ 一层整幅 66% 黑底;侧栏「文字图层」可改文字与文字颜色,「形状图层」列表里六个可见形状层都带填充/描边取色器与不透明度滑块;「图标选择」可在动画自己的 images/ 八张图标间切换、也可上传自定义图标(单图标位形态;上传的任意长宽比图标一律等比完整显示、不裁切);「主题颜色」区块不出现(它只跟踪站点约定的两族 HUD 色,本套是灰白配色);没有「动画时长」区块,时间轴按 AE 导出原样呈现(197 帧 ≈ 3.28s)

八套动画均为 60fps;画布尺寸:撤离动画 1920×1080,位置暴露动画 3840×1080(内容按实测中心平移居中,导出分辨率与之相同),核电站功率动画 1920×1080,任务弹窗动画 1920×1080,地图标题动画 2560×1024(327 帧 ≈ 5.45s;宽画布,内容贴左侧,右侧留白供长地名把底框撑开),黑潮爆破默认弹窗动画 1920×1080(258 帧 = 4.30s —— 数据本身已裁切,不是站点运行时的自动时长;画面约在 3.8s 处淡完,末尾留了约半秒收尾,不存在硬切),大战场弹窗动画 1920×1080(180 帧 = 3.00s,第 130 帧起整体淡出、第 144 帧淡完,末尾约 0.6s 空尾),撤离点开启动画 1920×1080(197 帧 ≈ 3.28s,文字与图标第 3237 帧淡入、第 118~124 帧整体淡出)。

上面这些信息(名称、搜索关键词、尺寸、帧率、帧数、特性标签)都声明在 src/main.ts 的 ANIMATIONS 注册表里, 顶栏触发器与动画画廊的卡片直接读它 —— 所以新增动画不需要动任何界面代码(见「二次开发常见任务 · 新增一套动画」); 尺寸/帧率在动画载入后会用实测值自动校正,声明值只要求大致准确。

任务弹窗动画的特殊之处:它的图标资源在 JSON 里是带目录前缀的相对路径(u:"images/" 加 p:"MallIcon_HafuCoins.png"), 站点产物里没有 images/ 目录,直接渲染会 404(画面缺图标),因此载入时会把该资源改写成打包后的静态资源地址 (ensureMissionData,与 animations/animation_2 的图标同做法);该动画没有音效,导出时不挂音轨(MP4/AVI 均只含视频轨)。

该动画的数据在 Bodymovin 5.12.2 下重新导出过(旧版 5.6.10 导出的那份轨道遮罩信息不完整: 5 个 tt 却只有一个孤儿 td、没有 tp、还缺了 3 个遮罩源图层,导致"某层该消失却不消失"之类的层级问题)。 现在的数据每个被遮罩图层都带 tp,4 个遮罩源(mask / mask 2 / mask 3 / mask 4)也都在,配对由文件自己决定, 站点无需再对遮罩做任何补丁。旧版文件保留在 animations/animation_4/animation_data.pre-reexport.json, tools/compare-animation4-exports.mjs 可以把两版逐帧对比(新版 165 帧后弹窗正常消失,旧版不会)。

数据上有两处本地微调(都在 animations/animation_4/ 下留了备份,可 --revert 还原): ① 颜色归一:可见形状图层的描边/填充按「离哪个参考色更近就取哪个」统一成 #77B0F0 或 #78C5F3 (占位色 —— 纯红填充 / 纯白描边 —— 与蒙版图层不动),脚本 tools/normalize-animation4-colors.mjs, 备份 animation_data.pre-color-normalize.json;页面上的「主题颜色」区可以在浏览器里再次整体换色。 ② 预合成「框 › 形状图层 6」默认不透明度 = 80%(原关键帧 0→100 按峰值等比缩放到 0→80, 淡入过程不变),脚本 tools/set-animation4-precomp-opacity.mjs,备份 animation_data.pre-precomp-opacity.json。 两者都会被"从 AE 重新导出"覆盖 —— 若在 AE 里改了这两处,重导后重跑脚本或删掉它们即可。

该动画的 AE 工程里还有一个视频图层(ty:9,引用 images/*.mp4,工程里本身是隐藏的)。 lottie-web 不支持视频图层,而且这类素材动辄 GB 级(仓库里那份约 2GB,已在 .gitignore 里排除), 所以站点在载入时会把视频图层和它独占的资源一起丢掉(见 ensureMissionData),避免多出一次必然 404 的请求。

核电站功率动画的特殊之处:AE 的「百叶窗(Venetian Blinds)」效果 lottie-web 不支持(JSON 里只有效果参数,渲染不出纹理), 因此纹理在 AE 里逐帧渲染成 PNG 序列(两条各 359 帧,1920×1080 带 alpha),由编辑器按「图片序列图层」叠回动画之上。 叠加层默认开启、无需开关;改色用 SVG feColorMatrix 滤镜即时生效(SVG 与 Canvas 渲染器、视频导出同样生效), 不逐帧重新编码图片。替换素材:把新序列放进 animations/animation_3/png resources/ 下的对应文件夹(文件名保持 名前缀_00000.png 递增)。

地图标题动画的特殊之处:原始导出的合成入点 ip 是 4,而「地图名称」的淡入关键帧是从第 0 帧开始的 —— 于是开场那 4 帧的淡入会被合成入点切掉,第一个可见帧上文字已经淡到 44%,"看起来一开场就没有透明度"。 已把 ip 归一为 0(时长随之由 323 帧变为 327 帧),备份保留在 animations/animation_5/animation_data.pre-ip-fix.json; 从 AE 重新导出会覆盖这一处,重导后按需再改一次即可。其余四套动画的 ip 本来就是 0,不受影响。

同一套动画还有三处本地调整(都是渲染观感层面的,同样会被"从 AE 重新导出"覆盖): ① 画布改为宽幅 2560×1024,内容放大约 3.70× 并贴在左侧 —— 原始导出是 1920×1080、内容只占画面 18.3%, 又小又空;改成宽画布并把内容放大后,右侧空出来的部分正好留给长地名把底框撑开。 放大只改根空对象 ind=1 的缩放与位置(缩放 257.6% → 952.58%,位置 960,540 → 514.73,474.35), 所有图层都是它的后代、跟着等比缩放,动画关键帧一个都没动;改画布尺寸会同时改变导出视频的分辨率(站点按 data.w/h 出片)。 备份 animation_data.pre-1x1-canvas.json(方形阶段)与 animation_data.pre-leftalign.json(改左对齐之前); ② 两个文字层由「居中对齐」改为「左对齐」,并整体左移使最左边缘留在原位 —— 改 j 会让整行文字在文字空间里右移半个行宽(居中时被左移过),所以位置要反向补偿: 实测偏移 377.7 画布单位,换算到图层位置是 −39.65(注意位置在父级空间里,自身缩放不参与换算); ③ 「描边」图层在侧栏只显示描边取色器(规则见「右侧编辑面板」)。 另外该动画的 chars 只烘焙了「地图点名称」五个字,改其它字时由 dropCharsIfUncovered 自动退回浏览器文本引擎,不会消失。

黑潮爆破默认弹窗动画的特殊之处(三件事都在 ensureBlastData 里处理): ① 位图资源是相对路径(u:"images/" + p:"xxx.png"),站点产物里没有 images/ 目录,原样渲染必然 404; 载入时按文件基名把 animations/animation_6/images/*.png 换成本地打包地址(与任务弹窗动画同做法)。 ② 视频图层(ty:9)及其素材必须整个丢掉,这次不是"可丢可不丢":数据里除了一堆引用 GIF 的 ty:9 图层, 还带了 10 个 xt:1 的外链预合成(数值 id,内容全是视频层)。lottie 的 searchExtraCompositions 会对每个 xt 资源直接 createComp,而这些资源在浏览器侧是残缺的,构建时抛 TypeError;异常正好落在 AnimationItem.configAnimation 的 try/catch 里被吞掉 —— 表现为 DOMLoaded 永不触发、预览整帧全黑。 做法:递归删掉 ty:9 图层,再删掉没有任何图层再引用的资源,外链预合成与视频素材(GIF / GB 级 mp4)一并消失 (mp4 已在 .gitignore 里排除,不进仓库也不进产物)。 ③ 含亮度遮罩(tt:3),只能走 SVG 渲染器:lottie-web 5.13.0 的 canvas 渲染器对亮度遮罩是坏的 (源码里 lumaLoader 是函数表达式却漏了调用括号,assetLoader.loadLumaCanvas 是 undefined), 构建这类图层时抛异常、被同一个 try/catch 吞掉 → 同样全黑。所以站点用 hasLumaMatte() 统一判定: 切到该动画时自动把渲染器切回 SVG(并在状态栏说明),导出自动改走 SVG 逐帧光栅化。 其余七套动画都没有 tt:3,不受影响。 ④ 逐帧序列要按文件基名改写 700 多条资源地址:AE 里的视频素材导出成了 Bodymovin 的 image sequence (assets[] 里的 sequence_0 + 696 个 t:"seq" 资源,由「预合成 1」里一条 ty:0 图层引用), 于是 images/ 目录从 5 个文件涨到 700 多个。载入时逐条改写成打包地址(基名匹配,一个都不能漏, 漏掉就是那几帧空白);glob 必须写 ?url&no-inline —— 这些帧单张只有 1~3KB,默认会被 Vite 内联成 data URI、把首屏 JS 从 613KB 顶到 2.1MB(实测),加 no-inline 后主 chunk 回到 707KB, 帧图仍是切到本动画时才按需加载(本机实测切换耗时 0.48s,700 张小图走 HTTP/2 无压力)。 Vite 会按内容哈希给相同内容的帧去重(703 张源图 → 产物里 333 个文件)。

另外:原始 AE 工程的时间轴是 600 帧(10s),但画面在第 ~230 帧就淡完了,后面 6 秒是空尾(在 AE 里同样是空的)。 所以这份数据被直接裁到 258 帧 = 4.30s(op: 600 → 258,超出新出点的图层 op 一并夹齐),共夹了 471 个图层, 备份在 animations/animation_6/animation_data.pre-trim-258.json —— 从 AE 重新导出会覆盖这次裁切,需要重做。 裁切只动 op 与图层 op 上界,没有碰任何关键帧,所以画面与 AE 逐帧一致。 这套动画的 caps 是空的:侧栏不出现「动画时长」区块,也就没有任何改时长的入口。 为什么连滑杆都不给:改时长的实现是把各图层末尾那段淡出关键帧整体搬到新的出点(见 applyMainDuration), 对这套动画就等于改数据 —— 提示条 image_1(ind10/ind22)的 192→216 淡出、图标 image_3(ind12)的 114→145 淡出 都会被搬走(实测搬到 234→258 / 227→258),画面上的淡出时刻与侧栏里看到的透明度关键帧就和 AE 对不上了。 同理也没有 nextScan —— 这是单段弹窗动画,没有第二段可接,侧栏不会出现「二次扫描」开关。 (日后需要给用户改时长的能力,把 timing: true 加回 caps 即可,区块会自动出现。)

大战场弹窗动画(180 帧 = 3.00s,画面第 144 帧淡完、末尾约 0.6s 空尾;ensureBattlefieldData): 画面是两行占位文字「主标题」(ProjectDTypeCurve-Bold,先淡入)+「内容」(ProjectDType-Medium,后淡入), 加一条由底板 / 底框 / 虚线 / 蒙版叠出来的 HUD 弹窗,以及一张 91×92 小图标 (第 30~36 帧淡入、第 130 帧起与底框一起淡出)。 两行文字的内容与颜色都由侧栏「文字图层」列表直接编辑, 弹窗本体的形状层(底板 / 底框 / 虚线 / 蒙版效果 / 底线)也照常出现在「形状图层」列表里,可以改填充色与透明度 (两个 td=1 的形状图层是轨道蒙版源、不渲染,按规则不列出); ⚠ 默认文字是手改本地数据的结果(animations/animation_7/animation_data.json 里 ind 4 的 nm/t 改成「主标题」、ind 3 改成「内容」;这份导出的图层名与文字内容是同一个字符串,所以两处一起改), 从 AE 重新导出会把它覆盖回原来的游戏文案。另外数据自带的 chars 字形表只烘焙了原文案的字, 改成占位文案后盖不住(缺 主/题/内/容),运行时由 dropCharsIfUncovered 自动丢掉字形表、 退回浏览器文本引擎(字体仍是 ProjectDType 系列,肉眼一致)。 预设(「预设」区块):列表来自 animations/animation_7/preset.txt(用 ?raw 打包进 JS 后在浏览器里解析, 见 parseBattlefieldPresets)—— 点一项就把图标 + 主标题 + 内容三样一起套用,再走一次重建。 列表默认收起(每条预设带两行文案预览,5 条全铺开会占掉近半屏侧栏):点标题行展开 / 收起, 折叠状态记在内存里(会话内有效)。因为「收起」容易被埋没,这一行刻意做成卡片式按钮而不是和别的区块 一样的灰标题(面板底色 + 圆角边框 + 强调色箭头,悬停 / 展开时边框变强调色),右侧常驻一句状态: 命中预设时显示**「当前:区域烟幕」(强调色),没命中时显示「点开选择」** —— 收起也知道现在用的是哪条、 也知道这里能点。文件格式是给人手改的小 DSL(当前 5 条:区域烟幕 / 制导导弹 / 战略信标 / 空投载具 / 炮兵齐射):

preset#1 {
  icon:27.png;                        // animations/animation_7/images/ 下的文件名
  name:"区域烟幕";                     // 列表上显示的名字
  title:"我军已呼叫烟幕";               // 写到「主标题」图层
  detail:"利用烟幕掩护,迅速隐蔽行动";   // 写到「内容」图层
}

解析写得宽松(块头 preset#N { ... } 里按 键: 值; 取,值可带单/双引号,键名大小写不敏感、 多余字段忽略、空块跳过);图标文件名在素材目录里找不到时只跳过图标并在状态栏点名,文案照常套用。 「主标题 / 内容」两个图层优先按图层名定位(页面上改字只改内容、不改图层名),名字对不上时 退回「按首个不透明度关键帧排序」—— 先淡入的当主标题;所以从 AE 重导出换掉图层名后预设照样能用。 选中态判定是「图标与两行文字跟哪条预设完全一致」,不是记住点了哪条:手动改过文字 / 图标之后高亮会自己消失。 图标可以在「图标选择」里切换:候选就是动画自己的素材目录 animations/animation_7/images/ 下的 PNG (当前 27 / 38 / 155 / 167 / car / clol / flag.png,默认选中数据里引用的 167.png),也可以上传自定义图标(≤2MB,会话内有效)。 图标一律「等比完整显示」而不是裁切:lottie 对图片资源的默认规则是 xMidYMid slice(按资源声明的 91×92 画框铺满后居中裁切),只有接近正方形的图标能完整显示,27 / 38 / flag 这些非正方形图标会被切掉一条边 —— 所以载入时给图标资源写了 pr = 'xMidYMid meet'(BATTLEFIELD_ICON_FIT,SVG 与 Canvas 两条渲染路径都认), 图标会等比缩放到完整塞进画框、居中显示,宽高比不符时留透明边而不裁切; 对原本就 1:1 的 167.png 来说两种规则结果完全相同,所以默认画面不变。 这套动画的图标位其实有两条位图图层(ind 5 / ind 6,一前一后承接同一次切换),但两份引用的是同一份资源 image_0,所以改写时按 refId 去重后逐个写 —— 重新导出换了资源 id 或加减图层都不会漏改; 选中的图标在切换动画后仍然保留(与任务弹窗动画一样记在模块状态里)。 除了这两项(图标 / 预设),这条动画在数据侧不需要任何专用编辑逻辑, 载入时只做两件事: ① 位图资源是相对路径(u:"images/" + p:"167.png"),按文件基名换成本地打包地址 (p 指向 ?url 产物、u 置空、e=1,与任务弹窗 / 黑潮爆破同规则;只认相对路径,已是 data: URI 或绝对地址就不动); ② 防御性丢掉 ty:9 视频图层与「删完之后没有任何图层再引用」的资源 —— 本套导出里没有这两样, 但 AE 工程里隐藏的视频素材照样会被导出,留着就是 DOMLoaded 永不触发的整帧黑屏(成因见上面黑潮爆破那一条)。 这套动画的 caps 同样是空的:侧栏不出现「动画时长」区块(改时长会搬动末尾淡出关键帧,属于改数据), 也没有 nextScan(单段弹窗,没有第二段可接)。 字体与其它动画共用(fName = ProjectDType-Medium / ProjectDTypeCurve-Bold,运行时从 animations/animation_1/fonts/ 注册);chars 只烘焙了导出时的那些字,改字后由 dropCharsIfUncovered 自动退回浏览器文本引擎,不会出现「新字消失」。

撤离点开启动画(197 帧 ≈ 3.28s,ensureEvacPointData): 画面是一条居中状态文字「紧急停堆撤离点已打开」(ProjectDTypeCurve-Bold,白色)+ 一张 48×48 小图标 (第 3237 帧与文字同步淡入、第 118124 帧一起淡出)+ 几层浅灰形状(#cecece / #a3a5a8 的边框与角标, 第 20~25 帧淡入)+ 一层整幅纯色层(29 帧后稳定在 66% 不透明度当黑底),整组由「空 1」空对象带动; 形状层之间用轨道遮罩(tt=1/2,遮罩源由 tp 显式指定;3 个遮罩目标配 3 个遮罩源,数量相等, 所以不需要强制 SVG 逐帧光栅化导出)做出「扫描 / 开合」的效果。 可编辑项由通用面板 + 一处专用逻辑提供: · 文字与文字颜色 —— 侧栏「文字图层」列表; · 画面颜色 —— 「形状图层」列表里六个可见形状层的填充/描边取色器(本套是 #cecece / #a3a5a8 / #fdfdfd 一族,不是站点判定的占位色);「主题颜色」区块不会出现,因为它只跟踪站点约定的两族 HUD 色 (#77b0f0 / #78c5f3,见 countThemeColors),本套的灰白配色不属于那两族 —— 要一次换掉整族颜色时, 逐个用形状层取色器改即可; · 每层不透明度 —— 各列表项自带的滑块; · 图标 —— 侧栏「图标选择」在动画自己素材目录 animations/animation_8/images/ 的八张 48×48 PNG (23 / 83 / 96 / 106 / 125 / 182 / 188 / 191.png)之间切换,也可以上传自定义图标(单图标位形态: 没有分段选项卡 /「显示图标」/「居中微调」/「图标不透明度」,见上面「图标选择」一节), 候选列表由 EVACPOINT_ICON_OPTIONS 从那个 glob 生成,默认选中数据里已引用的 83.png。 载入时做三件与其它弹窗类动画相同的事:① 位图资源是相对路径(u:"images/" + p:"83.png"), 按文件基名换成本地打包地址;② 防御性丢掉 ty:9 视频图层与「删完之后没人引用」的资源(本套导出里没有); ③ 给图标资源写上 pr = EVACPOINT_ICON_FIT(xMidYMid meet),让上传的非正方图标「等比完整显示」而不被裁切 (八张内置图标本来就是 48×48、与画框同尺寸,所以这一步对默认画面没有任何影响)。 这套动画的 caps 同样是空的:没有「动画时长」区块(改时长会搬动末尾淡出关键帧,属于改数据), 也没有 nextScan(单段动画,没有第二段可接)。


快速开始

方式一:一键启动(Windows,推荐)

双击仓库根目录的 启动网站.bat。它会自动完成:检查 Node.js → 首次运行时安装依赖 → 挑选可用端口(5173 被占用就自动顺延到 5174、5175…)→ 启动服务 → 打开浏览器。

停止服务:在那个窗口按 Ctrl+C,或直接关掉窗口。

端口不再写死 5173:vite.config.ts 里的 server.port 只是默认值,脚本用 --port <实际端口> --strictPort 覆盖它,--strictPort 保证端口真被抢走时立刻报错而不是悄悄换一个(否则窗口里显示的地址会和实际不符)。

方式二:命令行

要求:Node.js ≥ 18(推荐 20/22,仓库自带 .nvmrc = 22)。

npm install
npm run dev

然后打开 http://localhost:5173。

用 Chrome 或 Edge 打开:预览与导出依赖 WebCodecs、File System Access 等能力。

不想本地跑?直接访问线上站点 https://df.tfr-studio.top(与本地 dev 同源构建,纯静态托管在 Cloudflare Pages)。

其他命令:

npm run build    # 类型检查 + 产出 dist/
npm run preview  # 本地预览构建产物(与线上一致)

使用指南

顶栏

  • 品牌与制作声明:左侧为站点 logo + 「三角洲行动动画编辑器」标题 + 「编辑 · 预览 · 导出」标签;紧随其后的徽标写明动画均由人工使用 AE 制作、未使用 AI 辅助(纯展示元素,不参与编辑与导出);
  • 动画选择器:顶栏右侧是一条当前动画的触发器(缩略图 + 名称 + 分辨率/帧率 + 「N 套」总数),点它打开动画画廊并切换。之所以不做成原生下拉:动画会越来越多,下拉列表只能显示纯文本,没法搜索和预览,还会把顶栏撑得忽长忽短。画廊刻意做得很"薄"——没有分组、没有收藏、没有简介,打开就是一个按「最近用过的在前」排好的列表,多数情况下第一眼就是想要的:
    • 卡片网格:每张卡片是一套动画 —— 真实渲染的预览图 + 名称 + 规格(分辨率 · 帧率 · 时长)+ 特性标签(含音效 / 无音效、二次扫描、图标可换…);当前正在编辑的那套带蓝色描边与右上角「当前」角标。整张卡片都是按钮,点哪儿都算,不用瞄准封面;
    • 搜索:打开即聚焦搜索框,直接打字过滤,命中范围是名称、key、搜索关键词与特性标签(例如「扫描」「报酬」「blinds」);
    • 数字键直选:卡片左上角的序号就是快捷键,按 1–9 一步切换;开始输入搜索词后序号自动隐藏(那时数字键还给搜索框);
    • 最近使用:切过的动画自动排到最前(存 localStorage,纯自动,没有对应的界面元素);排序规则:最近使用 > 注册表顺序;
    • 键盘全程不用挪焦点:/ 或 Ctrl+K 打开,方向键移动、Enter 切换、1–9 直选、Home/End 跳首尾、Esc 关闭;鼠标在卡片上移动时键盘光标会跟着走,滚轮在面板任意位置都能滚动网格;
    • 选完即走:选中后画廊立刻关闭、焦点主动交还给页面(所以紧接着按空格是播放/暂停,而不是又把画廊打开);
    • 编辑面板会随所选动画自动显隐;首次切到某套动画会先下载其数据包(浮层显示进度)。

预览控制条

  • 播放/暂停、回到开头(均为内联 SVG 图标按钮,随播放状态切换)、循环开关;
    • 播放/暂停:暂停→继续播放;停在最后一帧时按播放键会从头重播(lottie 在末帧不会自行回绕);
    • 回到开头:无论当前在播放还是暂停,一律定格到第 0 帧并暂停(按钮随即显示播放三角);
  • 速度滑杆(0.1–3×)、时间轴拖动;
  • 渲染器:SVG(默认)与 Canvas,两者画面应一致,可互相验证;
  • 适配:包含 / 铺满 / 原始;滚轮缩放、拖拽平移、双击复位视图;
  • 背景色:预览背景(同时用于导出);勾选「透明背景」后预览显示棋盘格,导出强制为透明 AVI;
  • 导出格式:MP4 / AVI(勾选透明后自动锁定 AVI);导出完成后会在完成提示里给出重复帧自检(如 自检:4/131 帧与上一帧完全相同),用于区分"渲染丢帧"与"编码/播放器问题"。

快捷键与鼠标操作

操作 效果
空格 播放 / 暂停(焦点在输入框、下拉框或按钮上时不触发,方便一边改文字一边预览)
/ 或 Ctrl+K 打开动画画廊(焦点在输入框里时不触发,否则文字图层里打不出 /)
画廊内 ↑ ↓ ← → 在卡片之间移动光标(鼠标在卡片上移动也会同步光标)
画廊内 Enter 切换到光标所在的动画
画廊内 1–9 直接切换到第 N 张卡片(搜索框为空时生效)
画廊内 Home / End 跳到第一张 / 最后一张
画廊内 Esc 关闭画廊
停在末帧时再点播放 从头重播(lottie 停在末帧不会自行回绕)
拖动时间轴 逐帧预览,松手后恢复原来的播放 / 暂停状态
滚轮(预览区) 以光标附近为中心缩放视图
按住拖动(预览区) 平移视图,光标变为抓取样式
双击(预览区) 复位视图,等同于「复位视图」按钮

右侧编辑面板(按动画显示)

  • 文字图层:改内容/颜色/不透明度,「重置」按钮(SVG 图标)一键还原;核电站功率动画的五个数字位(第一位~第五位)颜色视为同一组 —— 改任意一位即五位同时改色(侧栏取色器同步刷新),文字内容仍各自独立;
    • 改字不会消失:有的导出会带「字形轮廓表」(chars,Bodymovin 的 Include Glyphs),里面只烘焙了导出那一刻用到的字 —— lottie 一旦发现有 chars 就按字查表画轮廓,查不到的字会画成空路径(表现就是"一改字,字就没了")。站点在每次重建前判定一次:字表盖得住当前文字就保留(字形与 AE 完全一致),盖不住就丢掉 chars、退回浏览器文本引擎(用的是站点已注册的同一套字体,肉眼一致),所以随便改字都不会消失;判据见 dropCharsIfUncovered;
  • 形状图层:填充/描边颜色 + 图层不透明度。三条约定:
    • 「蒙版」图层不列出(不参与渲染;预合成里的遮罩源 td=1 同样跳过);
    • 占位色不展示对应取色器 —— 填充为纯红 #ff0000 时只显示描边;描边为纯白 #ffffff 或 #5400ff(任务弹窗动画预合成「框 › 形状图层 5」的紫色占位描边)时只显示填充。判据是精确颜色值而不是颜色系,所以 #d92c36 这类真正的设计红不受影响。理由:这些颜色在 AE 里是"不可见底 / 默认描边",给改色入口只会误导(改完看不出变化);
    • 特例:地图标题动画的「描边」图层只显示描边(判据 isMaptitleStrokeLayer)—— 它是一黑一白两层同尺寸矩形叠出来的描边效果,黑填充只是衬底,真正看得见的是那圈 1.5 宽的白轮廓;所以这一个图层上白描边放开编辑、黑填充反过来隐藏。判据同时要求「当前动画 + 图层名 + 该图层同时有填充与描边」,因此同动画的「底框」图层不受影响(它的白描边与黑填充完全重合、被填充盖住,露出来同样是改了没效果的入口);
  • 主题颜色(统一换色):侧栏「主题颜色」区两个色块 —— 默认填充颜色(出厂 #77B0F0)与描边默认颜色(出厂 #78C5F3)。改一下就把当前动画里所有等于该颜色值的填充与描边一起换掉(含预合成内部,静态色与关键帧色都覆盖;蒙版源图层与未被引用的冗余预合成跳过),标题旁显示两族各有多少处;可连续改(第二次是在新颜色上继续替换),「重置为默认色」回到出厂两色。判据是颜色值而不是图层名 —— 这套素材里 #77B0F0 有 5 处落在描边、3 处落在填充,只换"填充项"是换不干净的;
  • 图片图层:纹理调色 + 图层不透明度,含位置暴露动画的「百叶窗.png / 百叶窗2.png / 光.png」、核电站功率动画的两条 359 帧叠加序列(模糊效果 / 百叶窗),以及黑潮爆破默认弹窗动画的提示条与图标位图;若当前不透明度下看不出改色效果,改色时会把该纹理的透明度自动提到 100% 并在状态栏提示,之后可用滑块调回;
    • 黑潮爆破默认弹窗(见下)的「图片图层」把该动画的全部图片图层都列出来,共 7 项:6 张位图(提示条 / 图标 / 亮度遮罩源 / HUD 底条 / 两张整幅图)+ 1 条 696 帧逐帧序列。三类图层的调色实现不同:
      • 带 AE「填充」效果的图层(HUD 底条、提示条的遮罩源与显示层)改的是填充色:画面里看到的就是这个颜色,所以取色器直连它、重置还原动画原值。这条同时补上了站点缺失的 AE 效果 —— getFillEffect 解析 ty:21(取颜色与不透明度,遇到蒙版/羽化/反转等不支持的形态就跳过),落地方式是把颜色烘焙进位图资源(bakeFillColor,与「百叶窗.png」调色同一套;填充挂在预合成上时着色它内部的位图),预览与导出读同一份 asset.p,天然一致。注意不透明度单位:Bodymovin 里既可能是 01 小数也可能是 0100 百分数,判据是"值 > 1 才当百分数"——早期一律 /100 会把 0.5 变成 0.005,填充几乎透明,表现就是"改了颜色没反应";
      • 位图走"换资源":把资源换成从原始素材重新着色得到的 data URI(所以随时能完整还原),并且用的是乘色法(目标色 × 源像素亮度)而不是站点原有的"保留亮度换色相"——那些 UI 素材是纯灰度的(实测 100% 灰度像素、饱和度 0),"保留饱和度"的算法对灰色等于什么都不做(拖了颜色画面纹丝不动);乘色法则天然适配"白描边 + 半透明底"的 UI 图:白描边保持色相、灰底被染成所选颜色、暗部仍是暗部。同一个资源被多层引用时(提示条被 6 层引用、HUD 底条被 4 层引用)只列一项 —— 它们共享同一个资源,改一次全部生效;
      • 逐帧序列(AE 视频素材导出成的 image sequence:696 帧)走"滤镜":按图层名记色、给承载它的预合成挂 SVG feColorMatrix 滤镜,一次着色覆盖 696 帧,不逐帧重编码。它的取色键用宿主图层名而不是 ind —— 预合成内的 ind 会被 normalizePrecompInds 重新编号(实测同一条序列在两条遍历路径下分别是 100001 与 1),用名字才稳定。696 个帧资源不会逐个列出,只列"这一条序列";
  • 预合成内容也能编辑:顶层图层的文字/形状/图片照常列出,被引用的预合成(Bodymovin 放在 assets[] 里、由图层 refId 引用)内部的图层同样列出,名字带「合成名 › 」前缀(如「框 › 形状图层 6」)。实现要点见「技术实现」一节;
  • 图标选择(位置暴露 / 任务弹窗动画 / 大战场弹窗动画 / 撤离点开启动画):
    • 顶部「位置暴露 / 二次扫描」选项卡(仅位置暴露动画):点缩略图即为当前这一段切换图标,两段互不影响;
    • 「二次扫描跟随位置暴露」(默认勾选,仅二次扫描选项卡显示):勾选时第二段沿用第一段图标;在二次扫描选项卡点选任意图标会自动取消勾选,改为单独指定;
    • 二次扫描尚未开启时,选项卡下方提示「设置会保留」,勾选「动画时长 → 二次扫描」后自动生效;
    • 「上传自定义图标」(前置 SVG 加号图标):上传 PNG/JPG/WebP(文件选择器也接受 GIF,≤2MB,仅在当前会话有效),应用到当前选项卡对应的段;
    • 「显示图标」:取消勾选后隐藏两段图标,文字与主段底框一起回到画面中心(左右留白对称);
    • 「居中微调」(左右方向 SVG 箭头按钮):隐藏图标状态下对文字(及其跟随底框)做 ±8px 手动微调,用于抵消字体自身的字形光学偏差,重置 归零;
    • 各套动画的内置图标互不混用:位置暴露动画用 animations/animation_2/icon/ 下的 Hero_Sp_* 技能图标;任务弹窗动画用 animations/animation_4/icon/ 下的 MallIcon_*.png(目录为空时回落到动画自带的 images/MallIcon_HafuCoins.png,列表不会变空);大战场弹窗动画用动画自己的素材目录 animations/animation_7/images/ 下的 PNG(当前 27 / 38 / 155 / 167 / car / clol / flag.png,默认选中数据里已引用的 167.png,往目录里丢新图重启即出现在列表里);撤离点开启动画同样用动画自己的素材目录 animations/animation_8/images/ 下的 PNG(当前 23 / 83 / 96 / 106 / 125 / 182 / 188 / 191.png,默认选中数据里已引用的 83.png);
    • 任务弹窗动画的单图标位形态:没有选项卡与「显示图标」开关,默认选中 MallIcon_HafuCoins(即动画自带的那枚货币图标), 下方多一条 「图标不透明度」 滑块 —— 图标是一张普通位图图层,滑块按比例缩放它自己的透明度关键帧(素材是「淡入 → 80% → 淡出」,滑块代表最亮时的透明度);
    • 大战场弹窗动画的单图标位形态:同样没有选项卡 /「显示图标」开关,也没有「图标不透明度」滑块(那条滑块仍只属于任务弹窗动画); 它的图标位其实有两条位图图层(ind 5 / ind 6,一前一后承接同一张图的切换),但两份引用的是同一份资源 image_0, 所以换图标时按 refId 去重后逐个改写,重新导出换了资源 id 或加减图层都不会漏改; 选中的图标在切换动画后仍然保留(与任务弹窗动画一样记在模块状态里); 换任何图标都不会被裁切 —— 载入时给图标资源写了 pr = 'xMidYMid meet'(BATTLEFIELD_ICON_FIT): lottie 的默认值是 xMidYMid slice,会按资源声明的 91×92 画框铺满后居中裁切,于是只有接近正方形的图标 能完整显示(167.png 91×92 完全不裁、155.png 104×104 只裁 1px),而 27.png(1.14:1)、38.png(0.81:1)、 flag.png(1.22:1)会被切掉一条边 —— 用户选了这几张就会看到「缺一块 / 显示不正常」。 改成 meet 后图标等比缩放到完整塞进画框并居中:宽高比不符时在某个方向留透明边,不裁切也不拉伸, 位置与尺寸档位不变;对本来就是 1:1 的 167.png 来说 meet 与 slice 结果完全相同(所以默认画面不变)。 实测(见下「验证大战场弹窗动画」):slice 下可见内容的长宽比恒等于画框的 0.989,meet 下则恒等于图标自身的长宽比;
    • 撤离点开启动画的单图标位形态:也没有选项卡 /「显示图标」/「图标不透明度」三样(前两样只属于位置暴露动画,后者只属于任务弹窗动画); 它的图标位是数据里唯一那张位图图层(ind 11,资源 image_0),换图标时按 refId 去重后改写 —— 重新导出换了资源 id / 加减图层都不会漏改; 选中的图标在切换动画后仍然保留(与大战场 / 任务弹窗一样记在模块状态里); 内置的八张素材都是 48×48,与资源声明的画框完全一致,所以换来换去都不会变形;上传任意长宽比的自定义图标也不会被裁切 —— 载入时给图标资源写上 pr = 'xMidYMid meet'(EVACPOINT_ICON_FIT,与大战场同规则):默认的 slice 会按 48×48 画框铺满后居中裁切, 非正方形的上传图会被切掉一条边,meet 则一律等比缩放到完整塞进画框并居中(宽高比不符时只留透明边,不裁切也不拉伸); 对本来就是 1:1 的八张内置图标来说 meet 与 slice 结果完全相同,所以默认画面不变;
  • 底框随文字自适应(地图标题动画):改「地图地点」的文字,底框与描边一起按文字宽度加宽。三条约定:
    • 左边缘钉死、只向右生长 —— 文字是左对齐的,所以底框左边缘固定在基准位置(合成 x ≈ 102),变长只往右扩;实现上不动图层缩放,而是直接改形状里圆角矩形的尺寸与位置(宽 +Δ、矩形中心右移 Δ/2),矩形左边缘天然不动;
    • 两个衬板必须同步 —— 底框与描边是一对同尺寸矩形,只改一个会让黑白两层错位;另外描边图层多一条修剪路径(只保留矩形顶边那条白线),它的 s/e 是周长百分比:矩形一加宽周长就变长、同样的百分比会切得更靠里,白线左端会往右缩。所以代码会重算 s,并且必须保留原始的那点内缩量(导出值 67.2% 比顶边起点 67.101% 多出约 0.086 个单位,是白线相对转角的留白)——若抹掉它、让 s 正好压在转角上,线帽会落在拐角并向外溢出半个描边宽度,表现为描边左边多出约 21px;
    • 只增不减 —— 文字改长才加宽,改短不回缩(与位置暴露动画同规则);点「重置」把文字改回原文时,底框精确回到基准宽度;
  • 报酬信息(任务弹窗动画):一个开关统一显隐「预期报酬」「数字」与货币图标 —— 这三者在画面上就是同一条报酬文案(文字 + 金额 + 币种图标)。实现走图层的 hd(隐藏)标记而不是把不透明度压成 0, 原因有二:三者在侧栏各有独立的不透明度滑块(两个文字层在「文字图层」、图标在「图标不透明度」), 靠改不透明度来隐藏一碰滑块就会被冲掉;而且隐藏前用户调好的不透明度会被覆盖。 用 hd 则完全不动 o,重新勾选原样恢复,文案 / 颜色 / 不透明度设置全部保留; 侧栏列表的遍历不过滤 hd,所以隐藏后三个图层仍留在列表里可继续编辑,只是画面不显示;
  • 动画时长(仅位置暴露):主段时长(默认 1.25s)、二次扫描开关与时长(默认 3.2s);黑潮爆破默认弹窗与大战场弹窗不显示这个区块(见「内置动画」一节:改时长会搬动末尾淡出关键帧,这两套弹窗动画要求与 AE 完全一致);
    • 两个默认时长各按动画 key 只应用一次(defaultDurationApplied 是 Set 而不是布尔):改字/改色引起的重建不会再把时长冲回默认值,也不会出现"先载入的那套吃掉另一套默认时长";
  • 文字宽度自适应(位置暴露,两段独立):改字后底框 X 缩放按文字宽度增量同比放大(≤4 字保持原大小),图标随文字左缘平移保持固定间距;二次扫描段的「光.png」是绝对层(不在「空 2」组内),底框加宽不会带动它,因此它的位移关键帧会按与底框完全相同的仿射变换改写(x′ = 枢轴 + (x − 枢轴) × k,枢轴 = 底框缩放定点),光带始终扫过整块底框;文字还原时 k=1,关键帧自动回到基准值;
  • 弹窗图层(撤离):勾选「显示弹窗」叠加弹窗;弹窗文字内容/颜色/对齐可编辑,底板与感叹号随文字宽度自适应,弹窗与主动画同帧同步。

所有会“整段重渲染”的操作(改字、改色、微调、开关二次扫描等)都已做单飞队列 + 重建完成后再恢复播放位置处理,连点/拖动滑块不会闪烁或卡在半成品状态。


视频导出

导出为 60fps,携带当前背景色与音频:

选项 格式 说明
未勾选透明 MP4 / AVI H.264 30 Mbps,分辨率跟随当前动画(撤离 1920×1080 / 位置暴露 3840×1080 / 核电站功率 1920×1080 / 任务弹窗 1920×1080 / 黑潮爆破默认弹窗 1920×1080);AVI 为 H.264-in-AVI(带 avcC),PotPlayer/VLC/剪映/FFmpeg 系通用
导出分辨率 自动 / 原始 / 75% / 50% 「自动」= 桌面按原始分辨率、移动端按 1080p 级;实际编码尺寸还会再按本机编码器能力向下压(见下)
浮层标题实时显示真正在用的规格,如 MP4 · H.264 · 60fps → 960×540@60
(自动)编不了这个尺寸 降档重试 / MJPEG 硬编码器拒绝当前分辨率时,沿 100% → 75% → 50% → 37.5% 的阶梯自动降档;AVI 还能再退 MJPEG(JPEG 质量 0.92 逐帧压缩后自封装);状态栏会写清走的是哪条路
(自动)本机无 WebCodecs 兼容录制 / MJPEG 没有可用的 H.264 编码器时:MP4 改用 MediaRecorder 现场录制(能录 MP4 就录 MP4,否则录 WebM,可直接在手机上播放,代价是无音轨、按真实时间录制);AVI 退回 MJPEG
勾选「透明背景」 AVI(自动锁定) 无压缩 32 位 BGRA 预乘 alpha,保留真实透明通道,可直接导入 AE/PR(体积约 8MB/帧)

下载文件名分别为 animation.mp4 / animation.avi / animation_transparent.avi(兼容录制可能产出 animation.webm)。

导出结束后浮层上会出现**「保存到手机 / 分享保存」与「重新下载」两个按钮:移动端浏览器把 blob 自动下载到相册/文件目录并不总是可靠, 前者走系统分享面板(Web Share),是手机上真正把视频落地最稳的一条路;后者用于自动下载被拦截时再来一次。 移动端完成浮层不会自动关闭**,就是为了留出点这两个按钮的时间。

移动端加固(手机导出)

桌面 Chrome/Edge 的能力是「有或没有」两态,手机却是连续谱(同为 Chrome,中低端机型可能只支持 1080p 甚至 720p 的 H.264 硬编、不支持 AAC、内存只有桌面十几分之一),所以导出管线不假设任何能力,一律先探测、不行就降级,并把真实原因回显出来:

  • 「是不是手机」用多路信号判定,UA 只是其中一路(IS_MOBILE,见 src/main.ts):手机浏览器切「电脑模式 / 请求桌面网站」后 UA 会被改写成桌面字符串(安卓 Chrome 变成 X11; Linux x86_64,iOS Safari 变成 Macintosh),只看 UA 就会把手机当桌面 —— 导出按原始分辨率跑、逐帧像素自检开着、透明 AVI 也不拦,手机上正是「点了导出卡住」的样子。所以叠加了与 UA 无关的硬件信号:

    • Chromium 的 navigator.userAgentData.mobile(Sec-CH-UA-Mobile,电脑模式下仍为 true);
    • (pointer: coarse) 主指针为粗指针 + navigator.maxTouchPoints > 1(触摸屏是主输入;桌面触屏笔记本主指针是鼠标 fine,不会被误判);
    • iPadOS 电脑模式:Macintosh/MacIntel + 触点数 > 1(真 Mac 触点恒为 0);
    • 兜底:粗指针 + 多触点 + 屏幕短边 ≤ 1000(宁可把小尺寸触屏一体机算成移动端,也不能漏掉手机);
    • 开发模式下这些信号挂在 window.__mobileDetect 上,便于确认到底靠哪一路判出来的(回归脚本见下面「验证移动端判定」);
  • 编码方案先探测再开跑:开工前用 isConfigSupported 沿「分辨率 × Level × Profile」逐档试探,选出本机真正能编的规格;移动端「自动」档还会先压到 1080p 级,避免动辄十几分钟的 4K 满载导出把页面拖到过热/被系统回收;

    • 探测本身限次(40 次)限时(15s,单次 1.5s):硬编码器探测不是纯查询(系统会真的起一次编码器),个别机型会长时间不回调 —— 没有上限时最坏要试近 300 个组合,表现就是「一直卡在探测本机编码能力」。超限直接改走兼容路径,不再干等;
  • 降档重试:少数浏览器会先答「支持」再在 configure() / encode() 时拒绝(Chrome 静默关编码器的老问题),MP4 这时会自动按更低一档重来一次;

  • 音频不再拖垮整段导出:AAC 编不了就导成无音轨的 MP4(原来是整段失败),AVI 的音轨本来就是自包含 PCM;音轨的 fetch / decodeAudioData / OfflineAudioContext 重采样全部限时,超时就按「无音轨」继续;

  • 屏幕常亮:导出期间申请 Wake Lock,手机息屏会让系统冻结标签页(表现是「进度卡住不动」),切回前台会自动补申请;

  • 跳过逐帧像素自检:自检每帧要把画布从 GPU 读回 CPU,手机上代价明显,而它只是排障信息,不影响成片;

  • 内存护栏:无压缩透明 AVI 需要把全部帧放进内存(1080p 十秒约 5 GB),移动端会在开跑前拦下并说明改用 MP4 或去电脑上导,而不是让用户等几分钟后看页面崩掉;

  • 失败信息带本机能力摘要:错误浮层会附上 WebCodecs=有/无 · 安全上下文=是/否 · 音频编码=有/无,便于远程排障。

「永不卡死」护栏(导出全程可诊断)

浏览器里有一批不报错、只是没有回调的 API,手机上是导出「卡在初始化渲染器 0% 不动」的主因: 硬编码器的 isConfigSupported / flush 不 settle、图片 decode() 悬空、SVG 作为 <img> 既不 load 也不 error、 canvas.toBlob 回调不来、移动网络下 fetch 挂住……这些 Promise 永远不 settle,没有超时就无法区分 「正在慢慢跑」和「已经死了」。所以:

  • 所有可能不返回的等待都走 raceTimeout(返回 TIMED_OUT 哨兵,由调用点决定降级还是报错): 编码能力探测、音轨解码/重采样/AAC 探测、字体取回、序列图 decode()、 逐帧光栅化的图片内联与 <img> 解码、编码器 flush、MediaRecorder.stop、canvas.toBlob; 缺了它只影响细节的(音轨、字体、单张序列图)一律降级继续,会影响画面正确性的(首帧就栅格化不出来)则给出可读错误;
  • 进度浮层有心跳:每秒在明细行后面追加「已用时 Ns」,并且第一帧就开始回报进度 (正在编码帧 1 / N)—— 慢设备上再慢也在动,用户知道该等而不是反复点击(导出中按钮是锁着的);
  • 阶段文案细化:探测编码能力 → 准备导出渲染器 → 准备音轨 / 编码器 → 逐帧编码,不再是笼统的「正在初始化渲染器」;
  • 手机端逐帧光栅化会提前告知耗时:含共用轨道遮罩的动画在手机上导出前,浮层直接说明「比桌面慢很多、可能要几分钟、请保持前台不锁屏」;
  • 导出结束释放栅格化图片缓存(黑潮爆破这类动画会有几十 MB 的 data URI),连续导出不会持续涨内存;
  • 透明 AVI 的内存护栏改成走失败收尾:原来它在 try 之外 throw,会让 exporting 永久为真、导出按钮永久禁用(之后再点就是「没反应」)。

兼容导出(SVG 逐帧光栅化,自动启用)

含「共用轨道遮罩」的动画(被遮罩层 tt 多于遮罩源 td,如核电站功率动画)导出时自动改用 SVG 渲染器渲染、 再逐帧光栅化后交给编码器,页面里没有任何开关;其余动画仍走原有 canvas 管线(它们的遮罩是标准配对,canvas 结果正确)。

判定函数是 needsSvgRasterExport,它还有第二个条件:含亮度遮罩(tt:3)的动画也必须走 SVG —— lottie-web 5.13.0 的 canvas 渲染器根本构建不出亮度遮罩图层(上游少了调用括号, assetLoader.loadLumaCanvas 不存在),异常被 lottie 自己吞掉,结果是整帧全黑。 目前只有黑潮爆破默认弹窗动画用到 tt:3;该动画在预览侧也会自动切到 SVG 渲染器(见上面那条说明)。

逐帧光栅化必须内联外链资源:导出会把当帧 SVG 序列化成独立文档再用 <img>/Blob 加载, 该文档禁止加载外部资源 —— <image> 的 http/blob 地址会被静默丢弃(导出缺图), 所以 svgRasterInlineImage 把当帧每张 <image> 内联成 data URI。

滤镜在这份文档里是可用的(实测复核:feColorMatrix、feGaussianBlur、 以及被 <use> 引用的 filter="url(#…)" 都正确出像素)。唯一要求是滤镜定义必须在这份文档内, 定义挂在站点隐藏 svg 里就必须 clone 进来 —— 序列调色的 df-seq-tint-* 就是这么做的(见 rasterizeSvgFrameToCtx)。 AE「填充」则改由 bakeFillColor 烘焙进位图,预览 / 导出 / 取色器三条路径共用同一份像素。

⚠ defs image 必须写成直接子选择器 defs > image(踩得最狠的一次):lottie 的图片预载 <image> 是 <defs> 的直接子节点,而轨道遮罩的遮罩源图层挂在 <defs> 里 <g id=…> 之下。 用后代选择器清理预载图,会把遮罩源一并删掉 → 蒙版变空 → 导出视频里整条蒙版动画直接消失 (预览完全正常,只有导出坏,因为被删的是导出实例自己的 DOM)。 这个清理两处都要用直接子选择器,只修一处不够: · rasterizeSvgFrameToCtx 里克隆出来的那份 SVG; · exportVideo 建好导出实例后,对它自己 DOM 的那次清理(漏了这里 = 后面每一帧都在错的 DOM 上光栅化)。 判据:导出视频抽帧与预览同帧截图做亮度差,每帧 ≤3.2;修复前 f20 全黑、f30 的蒙版缺口整条消失。

为什么需要它:lottie-web 的 canvas 渲染器用「整画布缓冲 + source-in/destination-over」实现轨道遮罩, 一份遮罩源被多个图层共用时会逐层把内容乘暗甚至抹掉(实测同帧亮红 (216,47,40) → (78,17,14)、白字变灰), 而 SVG 渲染器用 mask/text DOM,结果正确。站点部署在 Cloudflare Pages(纯静态、必须跑在访客浏览器里), 既没有服务端也不能用离线脚本,所以只能在浏览器内把 SVG 光栅化。

实现要点与提速过程(核电站功率动画 359 帧 1920×1080,端到端导出):

环节 做法 效果
取帧 克隆 SVG → 剔除 lottie 的图片预载元素(720 张)与字体 style → 内联当帧序列图 → 连同叠加序列调色的 SVG 滤镜定义一起复制(滤镜挂在站点隐藏 svg 里,不复制的话 <img> 加载时 filter:url(#…) 解析不到,导出会退回素材原色)→ Blob URL → img → drawImage 序列化从 22MB / 263ms 降到 ~30KB / 0ms
字体 字体另存 WOFF2(animations/animation_1/fonts/*.woff2,约为 TTF 一半),在 SVG 里用 blob URL 引用而不是 data URI 关键一步:浏览器按 URL 复用已解析字体,每帧字体解析 44ms → 4ms
静止帧 序列化结果与上一帧完全一致时直接复用上一帧位图 省掉约 1/5 帧的解析
实测 90s → 45s → 16s(每帧光栅化 96ms → 18ms),画面与 SVG 预览逐像素一致 —
  • 透明背景、音频、进度、取消、H.264 选级全部沿用原管线,兼容导出同样支持透明 AVI;
  • 图片不能照搬 blob URL 这一招(SVG 作为 img 加载时 blob:/外部图片会被拦掉,只有字体会被复用),序列图仍内联 data URI;
  • 预览请用 SVG 渲染器(默认):canvas 预览对这类动画同样偏暗,这是 lottie 自身的问题,站点侧无法在预览里绕过;
  • 含亮度遮罩的动画(黑潮爆破默认弹窗动画)在切进去时会把渲染器自动切回 SVG:canvas 渲染器对 tt:3 是直接构建失败(上游 bug),不走 SVG 就是全黑。

注意事项:

  • 普通播放器不会合成 alpha,半透明效果请在剪辑软件中预览;
  • 弹窗开启时导出会自动把弹窗合成进视频;
  • 导出为逐帧渲染 + 音视频封装,大时长/透明导出请耐心等待,浮层显示进度并可取消;
  • H.264 的 AVC Level 会按分辨率自动选择(撤离 1920×1080 → Level 4.2,位置暴露 3840×1080 → Level 5.0+),开跑前还会向浏览器确认一次该配置可用;若本机确实不支持,会直接给出可读原因,而不是编码到一半才报错。

技术实现(写给想改源码的人)

源码导览:想改什么,去哪个文件

想改什么 去哪儿
新增 / 删除一套动画 src/main.ts 中部的 ANIMATIONS 注册表(加一项即可,界面全自动跟上;详见「二次开发常见任务」)
动画选择器的交互(排序、搜索、快捷键、卡片字段) src/main.ts 末尾的「动画选择器」区块,开头列了阅读顺序与「常见改动从哪儿下手」
动画选择器的外观 src/style.css 的「动画画廊」段,开头有完整类名地图
动画选择器的骨架节点 index.html 的 #animPicker(id 与 main.ts 的 DOM 引用一一对应)
编辑面板(文字 / 形状 / 图片 / 图标 / 弹窗 / 时长) src/main.ts 的 loadData 与各 render*List、updateInfo 一带
播放、缩放、时间轴等预览行为 src/main.ts 的「显示适配 / 缩放 / 平移」与 loadData 里的 lottie 实例化
导出管线 src/main.ts 的「视频导出」区块(WebCodecs MP4 / 自封装 AVI / MJPEG 回退)
页面的长解释性注释 各文件开头都有总览:src/main.ts(整体架构)、src/style.css(设计令牌与阅读顺序)、index.html(结构顺序)
验证改动没坏 tools/ 下的脚本,文件名基本就是它验证的东西(如 verify-anim-picker.mjs、verify-mission-animation.mjs);多数需要 dev server 已跑起来
  • 技术栈:Vite + TypeScript + lottie-web + mp4-muxer;无后端,纯静态;
  • 素材打包:动画 JSON、音频、图标、序列图统一用 ?url 输出为独立静态资源(配合按需加载,首屏不会拉走其他动画的数据包),字体用 new URL(..., import.meta.url) 让 Vite 静态分析后产出带 hash 的资源;发布只部署 dist/ 即可;
  • 字体:JSON 里 fonts.list[].fPath 内嵌 TTF 时,编辑器用 FontFace 预加载注册,两种渲染器都用真实字体;
  • Canvas 文字兜底:撤离/位置暴露/核电站三套动画的 JSON 只有字体文件、没有字形轮廓(chars),lottie-web 原生 Canvas 文字会崩;编辑器在渲染管线里用原生 fillText 替换文字绘制,保留逐字母动画。 ⚠️ 兜底里那份「用 measureText 重算逐字母位置」的补偿只在 lottie 自己算不出 advance 时才该生效(判据是 letters[i].l 是否为 0); 若动画自带 chars(地图标题动画 chars=5、任务弹窗动画 chars=13),lottie 已经算出正确间距,再补偿一次会把字距放大近一倍 —— 实测地图标题动画的文字会从 347 撑到约 694 合成单位,盖过底框并被画布裁掉。导出走 Canvas 渲染器,所以这个坑会直接进成片;
  • 图像序列:带逐帧关键帧的图片层(SVG/Canvas/导出)由 prepareFrame 驱动逐帧换图,并在导出前预解码;
  • 投影(ADBE Drop Shadow):lottie-web 不渲染 AE 效果,编辑器解析「投影」参数:SVG 用 CSS drop-shadow,Canvas 用双重绘制;
  • 轨道蒙版与纹理:对 lottie 的 td/tp 蒙版做兼容:为「既当蒙版又可见」的层补可见副本、用 tp 显式指定蒙版源、合并第二段时对路径手柄做绝对化转换(复刻 convertPathsToAbsoluteValues),避免相对手柄被误当绝对控制点导致竖条弯曲;
  • 时长压缩与重置一致性:应用默认时长(把末尾淡出关键帧平移到压缩后出点)之后才捕获各层原始状态,保证点「重置」恢复的是压缩后的淡出时刻,不会与整体渐隐脱节;
  • 图标一律用内联 SVG:播放/暂停、回到开头、居中微调、重置、上传、导出完成对勾等都不再使用 emoji 或文字符号(不同系统字体渲染差异大、且容易缺字形)。统一走 .bi 类(14px / fill: currentColor / pointer-events: none),播放与暂停两枚图标叠在同一网格格子里切换,按钮宽度不跳动;ICON_RESET / ICON_CHECK 常量供 innerHTML 拼接复用;
  • 重渲染稳定性:整段重渲染串行化(单飞队列),期间隐藏预览避免半成品帧闪烁,新实例 DOMLoaded 后再恢复原帧与播放/暂停状态;
  • 按需加载与加载浮层:各套动画的数据都用 ?url 打包成独立静态资源,首屏只拉默认动画。切换动画时由注册项的 load()(内部是 ensure*Data,走 fetchJsonText 流式读取并按 Content-Length 驱动浮层进度)按需下载并解析,加载完成后再构建动画与弹窗;switchAnimation 本身不含任何 key 分支 —— 加载方式、是否已载入、编辑数据取哪份,全部由注册项的 load() / loaded() / data() 决定(启动流程见文件末尾的 bootApp)。同一份数据被并发请求时靠各自的 *DataPromise 合流,不会重复下载;
  • 原生音频工厂:lottie 默认依赖 window.Howl,缺失时它会返回缺 pause/volume 的桩对象并在暂停时抛 this.audio.pause is not a function;站点注入基于 HTMLAudioElement 的工厂,缓存音频元素复用请求,并在首次用户交互时重试被自动播放策略拦截的 play(),统一音量 0.3;
  • 数字位联动:核电站功率动画的五个数字文字层(第一位~第五位)按图层名归为一组,改色时整组一起改,侧栏其余取色器同步刷新;
  • 预合成内容可编辑(两处坑):① 预合成内部的 ind 只在自己合成内唯一,会和顶层撞号(侧栏控件靠 data-ind 回查图层,撞号就会改错层),所以载入时把被引用的预合成内部 ind/parent/tp 一起平移到独立号段(PRECOMP_IND_BASE = 100000,幂等);这三者都只在同一合成内解析,渲染结果不受影响(逐像素对比脚本 tools/verify-precomp-ind-shift.mjs);② lottie 的 completeData 会把预合成图层数组内联到引用它的图层上(同一批对象),列表遍历必须按对象身份去重,否则同一个图层会出现两份(且 ind 相同);未被引用的冗余预合成(如任务弹窗动画里的 51)不列出;
  • 主题颜色按颜色值替换:两个色块的判据是「该族当前的颜色值」而不是写死的出厂值 —— 改过一次后数据里的颜色已经变了,第二次替换要从新颜色接续(状态见 themeFillCurrent/themeStrokeCurrent);替换时填充与描边都换(同一族颜色两种位置都出现过),并跳过蒙版源图层(td=1,不渲染)与未被引用的预合成;
  • 字形表(chars)兜底:导出的 chars 只覆盖导出时的那些字,lottie 会用 getCharData 查表、查不到就返回 emptyChar(空路径)。站点在 loadData 与「保留状态重渲染」两条入口都调用 dropCharsIfUncovered:遍历所有文字层(含预合成)比对 字|style|family,一旦有表外的字就删掉 chars,让渲染走浏览器文本引擎 —— 这也是另外三套无 chars 动画一直以来的路径,所以 SVG / Canvas(站点 fillText 补丁)/ 导出(SVG 光栅化时内联 WOFF2 字体)三条路都已验证可用;
  • 任务弹窗动画的载入处理(见 ensureMissionData):图标资源在 JSON 里是带目录前缀的相对路径(u:"images/"),载入时按文件基名改写成打包后的静态资源地址(重新导出时素材若被归到子目录也能兜住);数据里若有 AE 视频图层(ty:9)则连同它独占的资源一起丢弃(lottie-web 不支持,且配套素材动辄 GB 级);
  • 黑潮爆破默认弹窗动画的载入处理(见 ensureBlastData):位图资源同样是相对路径,按文件基名改写成打包地址;除 ty:9 视频图层外,还要删掉没有任何图层再引用的资源 —— 这份数据里有 10 个 xt:1 外链预合成,lottie 的 searchExtraCompositions 会对它们直接 createComp 并抛 TypeError,而异常被 configAnimation 的 try/catch 吞掉,表现为 DOMLoaded 永不触发、预览整帧全黑(实测);此外该动画含 tt:3 亮度遮罩,预览与导出都必须走 SVG(见 hasLumaMatte);
  • 大战场弹窗动画 / 撤离点开启动画的载入处理(见 ensureBattlefieldData / ensureEvacPointData):与任务弹窗 / 黑潮爆破同一套做法 —— 位图资源是相对路径,按文件基名改写成打包地址;再防御性递归删掉 ty:9 视频图层与「删完之后没有任何图层再引用」的资源(这两套导出里都没有,但 AE 里隐藏的视频素材照样会被导出,留着就会让 lottie 构建失败、预览整帧全黑);最后给图标资源写上 pr = 'xMidYMid meet',让非正方的自定义图标等比完整显示而不被裁切(大战场用 BATTLEFIELD_ICON_FIT、撤离点开启用 EVACPOINT_ICON_FIT);两套动画的文字与颜色都由通用面板编辑:文字与文字颜色走「文字图层」列表,形状层颜色走「形状图层」列表(撤离点开启的灰白配色不属于站点跟踪的两族 HUD 色,所以那套的「主题颜色」区块不出现);两套的图标都走「图标选择」区块的专用逻辑(候选列表分别来自各自的 images/,单图标位形态、注释态保留,见 applyIconUrlToScope / iconLayerOf);
  • MJPEG 回退:没有 WebCodecs 的浏览器导出 AVI 时,改用 canvas.toBlob('image/jpeg', 0.92) 逐帧压缩再封装为 MJPEG-AVI,保证老浏览器也能导出;
  • H.264 自动选级:WebCodecs 的 codec string 里写死的 AVC Level 决定了可编码的最大宏块面积(Level 4.2 上限约 1920×1080),超出时 Chrome 会在 configure() 之后静默关闭编码器,下一次 encode() 就抛 Cannot call 'encode' on a closed codec。现在按「宽高各向上取整到 16 的倍数 × 帧率」算出所需的最低 Level,并用 VideoEncoder.isConfigSupported() 逐档确认后才 configure,同时把 error 回调里的真实原因转成可 catch 的异常(见 runWebCodecsExport)。

目录结构

.
├── animations/       # 八套动画素材的总目录(原 animation/ 更名为 animation_1 后整体归入)
│   ├── animation_1/  # 撤离动画(JSON/音频/弹窗/序列图)
│   ├── animation_2/  # 位置暴露动画(JSON/WAV/可选图标 WebP)
│   ├── animation_3/  # 核电站功率动画(JSON + png resources/ 下两条 PNG 序列)
│   ├── animation_4/  # 任务弹窗动画(JSON + images/ 素材 + icon/ 可选图标;AE 视频图层素材已 gitignore)
│   ├── animation_5/  # 地图标题动画(单份 JSON:两个文字层 + 底框/描边,无图片与音频)
│   ├── animation_6/  # 黑潮爆破默认弹窗动画(JSON + images/ 下 4 张位图 + 696 帧逐帧序列 + 2 张整幅图;AE 视频图层素材已 gitignore)
│   ├── animation_7/  # 大战场弹窗动画(JSON + images/ 下若干图标 PNG + preset.txt 预设表;预设 / 文字 / 图标都可在页面上直接改)
│   └── animation_8/  # 撤离点开启动画(JSON + images/ 下 8 张 48×48 图标;状态文字与图标都可在页面上直接改)
├── posters/          # 动画画廊卡片的预览图(每套动画一张 WebP,由 tools/build-anim-posters.mjs 生成)
├── src/
│   ├── main.ts       # 编辑器全部逻辑(入口)。文件开头是总览;动画注册表 ANIMATIONS 在文件中部;
│   │                 #   动画选择器(顶栏触发器 + 画廊)在文件末尾 bootApp 之前,那一块开头有阅读顺序
│   ├── style.css     # 全局样式。顶部是设计令牌与阅读顺序;「动画画廊」那一段开头有类名地图
│   └── vite-env.d.ts
├── tools/            # 开发/排障用脚本(多数非必需)。与动画选择器相关的三个:
│                     #   build-anim-posters.mjs  生成画廊卡片的预览图
│                     #   verify-anim-picker.mjs  动画选择器的端到端验证
│                     #   png-to-webp.py          图标素材批量转 WebP
├── 启动网站.bat       # Windows 一键启动:装依赖 + 挑可用端口 + 起服务 + 开浏览器
├── index.html
├── vite.config.ts    # 数据包 chunk 拆分;dev 期 /save-avi 落盘钩子(排障用)
├── package.json
└── README.md

二次开发常见任务

  • 更换某套动画数据:替换 animations/animation_1/animation_data.json(撤离)或 animations/animation_2/animation_data.json(位置暴露)后重新构建;若用 AE 重新导出位置暴露动画,需按仓库现有做法补「底框(可见)」副本、统一黑矩形、路径手柄绝对化等(见上);
  • 新增一套动画:界面上不会再出现"下拉框里加一项"这种手工活,全部由 src/main.ts 里的 ANIMATIONS 注册表驱动。四步:
    1. 把新动画的 JSON(含可选的弹窗 JSON 与音频)放进 animations/ 下的一个新目录,按 ?url 导入(音频同理),并写一个 ensure*Data 按需加载函数(参考 ensureMissionData);
    2. 在 ANIMATIONS 里加一项 AnimDef:key / label / meta(搜索 tags、封面主色 accent、画布尺寸与帧率帧数、特性 features)/ poster / data() / popup() / audioUrl / loaded() / load() / caps;
    3. 跑一次 node tools/build-anim-posters.mjs 生成卡片预览图(脚本会从兼容 select 里读出全部动画,新动画自动纳入;只想补新动画这一张时加环境变量 POSTER_KEYS=<key>,不会顺手把其它封面按当前实现重裁一遍)。展示帧不理想就改脚本顶部的 FRAME_RATIO —— 比如黑潮爆破默认弹窗的时间轴有很长的空尾,按默认 0.46 取帧会是一张空图,所以单独设成了 0.45;大战场弹窗第 72 帧才把两行字淡入完、第 130144 帧整体淡出(时间轴 180 帧、末尾还有空尾),所以设成 0.6;撤离点开启第 37 帧淡入完、第 118124 帧淡出,所以设成 0.45; 想在封面上标出「这块能换」(高亮圈 + 一句说明),在脚本顶部的 POSTER_HIGHLIGHT 里给该 key 加一条:label 是说明文字,find 是一个自包含函数(puppeteer 会把它投进页面执行),返回目标元素相对 stage 的 CSS 矩形(找不到就返回 null,封面按老样子生成)。大战场弹窗动画就用了这条来圈出图标位;
    4. 重新构建。顶栏触发器、动画画廊的卡片 / 搜索 / 数字键直选、侧栏区块的显隐全部自动跟上,不需要再改 switchAnimation,也不需要动 index.html;
    • meta 里的尺寸/帧率只是"未载入时的声明值",动画一旦载入过就会用实测值覆盖(见 measureAnim),所以填个大概即可;
    • caps 决定侧栏哪些区块对这套动画开放:timing(动画时长区块)、nextScan(「二次扫描」勾选行 + 二次扫描时长行)、defaultDuration(该动画首次载入套用的默认时长);不写就是都没有。 三者互相独立:只开 timing 是「有时长滑杆、没有第二段」(黑潮爆破默认弹窗就是这样),只开 nextScan 而不开 timing 则整个区块都不显示;
    • timing 只是"给不给滑杆";defaultDuration 是"载入时自动设一个时长",而自动设时长的实现会搬动图层末尾的淡出关键帧(applyMainDuration),画面上的淡出时刻会随之改变 —— 对"必须与 AE 逐帧一致"的动画(如黑潮爆破默认弹窗)宁可不配,把它留给用户手动操作;
    • 若新动画的图片资源在 JSON 里是相对路径(形如 u:"images/" + p:"xxx.png"),载入时要把该资源改写为 ?url 导入的打包地址(见 ensureMissionData / ensureBlastData / ensureBattlefieldData / ensureEvacPointData),否则站点里会 404;
    • 资源是成百上千张逐帧图(Bodymovin 的 image sequence / AE 视频素材)时,glob 要写成 ?url&no-inline:否则小于 4KB 的小图会被内联进首屏 JS(黑潮爆破默认弹窗的 703 张帧图实测把主 chunk 从 613KB 顶到 2.1MB),加 no-inline 后它们照常产出独立文件、按需加载;
    • 重导出的数据里如果混进了别的东西(视频图层 ty:9、外链预合成 xt:1、没人引用的资源),照着 ensureBlastData 的做法在载入时清掉:这类残留轻则多一次 404,重则让 lottie 在 searchExtraCompositions 里抛异常、整个动画渲染不出来;
    • 数据里出现 tt:3(亮度遮罩)时记得确认渲染器:canvas 渲染器对它是构建失败的(上游 lottie bug),预览要落回 SVG、导出要落进 SVG 逐帧光栅化 —— 两处判定都在 hasLumaMatte/needsSvgRasterExport;
  • 重新生成动画预览图:node tools/build-anim-posters.mjs(dev server 没起会自己起一个);预览图在 posters/<key>.webp,统一 640×360,由真实渲染截取并自动裁到内容范围。某套动画的展示帧不理想时,改脚本顶部的 FRAME_RATIO;想在封面上给某个「可换的部位」加高亮圈 + 说明文字(如大战场弹窗的图标位),加一条 POSTER_HIGHLIGHT;
  • 改造动画选择器(改界面本身,不是改动画):只需要碰三个地方 ——
    • 动画有哪些 / 叫什么 / 什么规格 / 哪些关键词能被搜到 → src/main.ts 中部的 ANIMATIONS 注册表;
    • 卡片怎么生成、搜索与键鼠怎么响应、排序规则 → src/main.ts 的「动画选择器(顶栏触发器 + 画廊浮层)」区块,那一块开头列了阅读顺序和「常见改动从哪儿下手」;
    • 尺寸、配色、动效、窄屏表现 → src/style.css 的「动画画廊」段,开头有完整的类名地图(哪个类对应界面哪一块)。
    • 骨架节点(搜索框 / 网格容器 / 底部提示)在 index.html 的 #animPicker,那里的 id 与 main.ts 的 DOM 引用一一对应,改名必须两边同步(与本文件顶部那条约定一样,写错不会报错,只会静默失效)。
    • 几个具体例子:调一屏放几张 → 改 .ap-grid 的最小列宽;卡片上增减信息 → 改 renderAnimGallery 里拼 HTML 的那段;换排序规则 → 改 animVisibleList 末尾的 sort;加快捷键 → 改区块里「画廊内的键盘」那个 document keydown;
  • 验证动画选择器:dev server 跑着的前提下执行 node tools/verify-anim-picker.mjs(触发器、卡片生成、搜索、数字键直选、键盘导航、鼠标跟随、滚轮转发、选完焦点交还、隐藏 select 兼容层、窄屏),截图落在 tools/anim-picker-*.png。 脚本里还有一条**「分组筛选 / 收藏 / 简介不该存在」**的断言 —— 这三样是有意去掉的,如果哪天加回来,这条会红,提醒同步更新文档与样式;
  • 验证大战场弹窗动画:dev server 跑着的前提下执行 node tools/verify-battlefield-ui.mjs —— 断言总帧数/帧率/画布尺寸、侧栏列出的两个文字层就是新的默认占位文案「内容 / 主标题」、文字框里也是它们、形状图层列表与填充取色器、「动画时长」与「弹窗叠加层」区块按 caps 隐藏、「预设」区块按 preset.txt 列出 5 条且名字与顺序一致、列表默认收起且标题行是「够高 + 有圆角边框 + 手型光标」的卡片按钮(不是灰标题)、未命中预设时提示「点开选择」、点标题行展开、套用某条后文字框 / 画面文案 / 图标资源 / 图标列表选中态 / 预设高亮全部跟着变且标题行显示「当前:预设名」、手动改字后预设高亮自动消失、图标资源带 pr = xMidYMid meet 且画面里 <image> 的 preserveAspectRatio 跟着走、「图标选择」列出 animations/animation_7/images/ 里全部图标且默认选中 167.png、点其它图标后资源地址与画面里的 <image> 同步换掉、切换动画往返后选择仍在、上传自定义图标生效、位图资源已改写成打包地址(且全程无 4xx)、改文字后画面里出现新文案、改颜色后图层 fc 与画面填充同步、画廊卡片带 posters/battlefield.webp 封面;截图落在 tools/.battlefield-check/(含 preset-panel.png、icon-panel.png)。 脚本里还有一条**「图标可见内容实测」:把每一张内置图标按浏览器真实的 <image preserveAspectRatio> 规则光栅化(4× 超采样后量非透明包围盒),断言 meet 下可见内容的长宽比 == 图标自身长宽比(slice 下则恒等于画框的 91:92 —— 非正方形图标就是被裁掉一条边),并把两列并排存成 tools/.battlefield-check/icon-fit-compare.png,另有放大约 3.8× 的 icon-flag-closeup.png 供人工比对。测里那一步必须先把图标取成 data URI 再嵌进 SVG** —— 用 <img> 加载 SVG 时外部 href 会被浏览器拦住不加载(站点的 SVG 逐帧光栅化导出也是这么内联的)。 也可用 DEV_ORIGIN=http://127.0.0.1:4173 指向 npm run preview 的构建产物验证生产路径(此时图标地址是 /assets/155-<hash>.png,脚本两种形式都能认);
  • 验证撤离点开启动画:node tools/verify-evacpoint-ui.mjs —— 除上面同类的断言外,另加「轨道遮罩 3 目标 / 3 源成对」与「改形状层填充色后画面出现新填充」两条,明确断言「主题颜色」区块不出现(灰白配色不属于那两族 HUD 色);图标选择部分断言:区块对这套动画显示、「图标选择」列出 animations/animation_8/images/ 里全部 8 张且默认选中 83.png、单图标位形态(分段选项卡 /「显示图标」/「居中微调」/「图标不透明度」都隐藏)、图标资源带 pr = xMidYMid meet、点 106.png 后资源地址与画面里的 <image> 同步换掉且选中态转移、切换动画往返后选择仍在、上传自定义图标进入列表并生效(状态栏点名「撤离点开启动画」)、画廊卡片特性标签含「图标可换」;截图落在 tools/.evacpoint-check/(含 after-icon-106.png);
  • 验证移动端判定:node tools/verify-mobile-detect.mjs —— 7 个场景(安卓/iOS 普通模式、两者的电脑模式、仅 Chromium Sec-CH-UA-Mobile 提示、Windows 桌面、Windows 触屏笔记本),逐条断言「是否被判成手机」。 这是防回归用的:IS_MOBILE 漏判会让手机按桌面规格导出、且不拦无压缩透明 AVI,表现就是「点了导出卡住」;
  • 验证导出护栏:node tools/verify-export-resilience.mjs —— 把 VideoEncoder.isConfigSupported / AudioEncoder.isConfigSupported 注入成永不 settle,断言导出仍能限时降级出片;再验证手机端透明 AVI 护栏失败后导出按钮能立刻再点(不会被"卡死标志"挡住);
  • 验证手机端四种导出环境:node tools/verify-mobile-export.mjs all(桌面基准 / 编码器降档 / 无 WebCodecs 兼容录制 / 不支持 AAC / AVI 两种),产物落在 tools/.mobile-test/(注意:该脚本会先清空这个目录,它同时是历史基准样本的存放处,跑完记得 git checkout -- tools/.mobile-test 恢复);
  • 替换百叶窗叠加序列:把 PNG 序列放进 animations/animation_3/png resources/<文件夹>/,在 src/main.ts 的 BLINDS_SEQUENCES 里按「文件夹名 + 帧序」登记即可(帧数/尺寸变化不用改代码);
  • 更换音频:替换对应 .mp3/.wav(保持文件名),或改 src/main.ts 的导入;
  • 更换字体:在动画 JSON 的 fonts.list 中提供 fPath(base64 TTF),编辑器会自动加载;无内嵌字体则回退系统字体;
  • 任务弹窗动画换图标:图标位配置在 src/main.ts 的 MISSION_ICON_OPTIONS(列表来自 animations/animation_4/icon/)/ iconLayerOf 一带 —— 图标位是数据里唯一的那张位图图层,默认选中 MallIcon_HafuCoins(动画自带的那枚);目录为空时会回落到打包的 images/MallIcon_HafuCoins.png, 列表不会变空;此外用户也能在页面里直接上传自定义图标(仅当前会话有效);
  • 大战场弹窗动画换图标:候选列表是 BATTLEFIELD_ICON_OPTIONS(直接由 animations/animation_7/images/ 的 glob 生成,目录里有几张就列几张), 默认项固定为数据里已引用的 167.png(BATTLEFIELD_ICON_DEFAULT_NAME)—— 默认项必须对上,否则光是切进这套动画就会把资源地址改掉、画面凭空变化; 选中项记在 iconBattlefieldName;iconLayerOf 对这套动画只回答「有没有图标位」,真正改写资源的是 applyIconUrlToScope 里的 battlefield 分支 (按 refId 去重逐个写)。这条动画不显示「图标不透明度」滑块(那条仍只属于任务弹窗动画); 另外它会在载入时给图标资源写 pr = BATTLEFIELD_ICON_FIT('xMidYMid meet')——不加这一条,换成非正方形的图标就会被裁掉一条边 (lottie 默认 xMidYMid slice);换图标时只改 p/u/e,pr 一直留在资源上,所以怎么换都不裁切;
  • 撤离点开启动画换图标:与大战场弹窗动画同一条路子 —— 候选列表是 EVACPOINT_ICON_OPTIONS (animations/animation_8/images/ 的 glob,目录里有几张就列几张,当前八张),默认项固定为数据里已引用的 83.png (EVACPOINT_ICON_DEFAULT_NAME,理由同上:默认项必须对上,否则光是切进这套动画就会改掉画面); 选中项记在 iconEvacPointName;iconLayerOf 对这套动画只回答「有没有图标位」,真正改写资源的是 applyIconUrlToScope 里的 evacpoint 分支(同样按 refId 去重逐个写);它同样在载入时给资源写 pr = EVACPOINT_ICON_FIT, 所以上传任意长宽比的自定义图标也等比完整显示、不裁切。这套动画不显示「显示图标」与「图标不透明度」; 要往列表里加图,直接把 PNG 丢进 animations/animation_8/images/ 重启即可(默认项仍是 83.png,不受影响);
  • 大战场弹窗动画改默认文字:改 animations/animation_7/animation_data.json 里两个文字图层的 nm(图层名)与 t.d.k[0].s.t(文字内容) —— 这份导出的图层名与文字内容是同一个字符串,只改一处会让侧栏列表与画面文案对不上; 改完不用动代码(侧栏「文字图层」列表是自动发现的)。⚠ 从 AE 重新导出会覆盖这次改动, 而且数据自带的 chars 字形表只烘焙了导出时的字,改成表外的字时由 dropCharsIfUncovered 自动退回浏览器文本引擎(不会「改了就消失」); 当前仓库里的默认文案就是占位用的「主标题 / 内容」(ind 4 / ind 3);
  • 大战场弹窗动画增删改预设:直接编辑 animations/animation_7/preset.txt(格式见「内置动画」一节的小例子: preset#N { icon:文件名; name:"预设名"; title:"主标题文案"; detail:"内容文案"; }),四条字段都可省 —— 少了哪条就少改哪一样;不用碰任何代码(站点用 ?raw 导入后自己解析)。dev 下保存即热更新, 生产环境要重新构建。图标名写错时只跳过图标、文案照常套用,并在状态栏点名; 预设列表默认收起(点标题行展开,状态记在 presetExpanded),所以预设加到十几条也不会把侧栏撑长; 预设的选中态判定是「图标与两行文字完全一致」,所以手动改过之后高亮会自己消失,不需要维护额外状态;
  • 换主题色:直接改动画 JSON 里对应颜色的数值,或在页面「主题颜色」区一次换掉(判据是颜色值,见上);
  • 更换/新增图标:位置暴露动画的图标放进 animations/animation_2/icon/,任务弹窗动画的放进 animations/animation_4/icon/,大战场弹窗动画的放进 animations/animation_7/images/,重启 dev 后自动出现在「图标选择」里 (同名同时存在 PNG 与 WebP 时只显示 WebP)。建议转成 WebP 减小体积:python tools/png-to-webp.py --delete-png (q=95,alpha 无损,体积约为 PNG 的 43%)。大战场弹窗的图标放进 images/ 后还能直接写进 preset.txt 当预设图标用;

部署到 Cloudflare Pages(或其他静态托管)

  1. 把仓库导入 Cloudflare Pages(或连接 GitHub 自动构建);
  2. 构建设置:
    • 构建命令:npm run build
    • 输出目录:dist
    • Node.js 版本:22;
  3. 部署后即可在公网编辑与导出,无需任何后端。线上实例:https://df.tfr-studio.top(部署在 Cloudflare Pages,自定义域名)。

Pages 单文件上限 25 MiB:动画 JSON 较大,vite.config.ts 已把它们拆成独立的 data-* chunk,单个产物远低于上限。


已知边界

  • 编辑结果不落盘:站点没有任何保存 / 导出 JSON 的功能 —— 所有修改只存在于当前页面的内存里,刷新即回到素材原始状态,自定义上传的图标也只在当前会话有效;需要带走效果请导出视频;
  • 预合成内容的编辑同样只改内存:侧栏里「合成名 › 图层名」的条目改的是 assets[] 里那份数据,渲染立刻生效,但同样刷新即还原;导出视频包含这些改动(导出用的是同一份内存数据);
  • 主题颜色/占位色规则按颜色值判定:主题色若被改成纯红 #ff0000 或纯白/紫描边,相关图层会因"占位色不展示取色器"的规则而从列表里隐藏编辑项(画面改色仍然生效);主题色改完后,单个图层点「重置」是还原到载入时的原始色,不是当前主题色;
    • 注意:改了主题色只是为了让界面干净,并不会解除地图标题动画「描边」图层那条按图层名走的例外规则;
  • 切换动画需联网下载对应数据包(数百 KB ~ 十余 MB,视动画数据量而定),下载完成后由浏览器缓存;
  • 透明导出必须选 AVI(MP4 无 alpha),且体积很大;
    • 手机上无压缩透明 AVI 会被直接拦下(体积按 宽×高×4×帧数 估算,超过约 800 MB 即拒绝启动):手机标签页的内存配额放不下这些帧,继续跑只会崩页面;
  • 移动端导出会按本机能力调整规格:手机默认按 1080p 级导出,编码器不支持时还会自动继续降档 —— 想要原始分辨率,把「分辨率」下拉改成「原始」即可(改完仍可能被编码器能力再次降档);
  • 手机上「逐帧光栅化」的动画会明显慢:含共用轨道遮罩 / 亮度遮罩的动画(任务弹窗、黑潮爆破等)必须在浏览器里逐帧把 SVG 转成位图,这步是单线程 CPU 活,手机大约比桌面慢一个数量级(几百帧可能要几分钟)。浮层会提前说明,并持续显示「正在编码帧 x / N · 已用时 Ns」——慢不等于卡死,进度在动就请等(已申请屏幕常亮;切到后台会让浏览器节流,建议保持前台)。若前 3 帧一张都没能栅格化,会直接报错而不是空转;
  • 没有 H.264 编码器的机型走兼容录制:录出的文件无音轨、时长按真实录制时间走(渲染跟不上时可能比原动画略长),浮层里会明确写出「兼容录制模式」;
  • Canvas 渲染器依赖站点的 fillText 兜底,若动画包含未被处理的特殊文字排版,可能需要扩展兜底逻辑;
    • 该兜底里的「重算逐字母位置」只对没有字形轮廓表(chars)的动画有效:自带 chars 的动画 lottie 已算好间距,再补偿一次会把字距放大近一倍(实测地图标题动画文字从 347 撑到约 694 合成单位、盖过底框并被画布裁掉)。判定见 advanceMissing —— 以 letters[i].l 是否为 0 为据,两类动画各走各的;导出走的就是 Canvas 渲染器,所以这条会直接影响成片;
  • 轨道遮罩在 canvas 渲染器下不可靠:遮罩源被多图层共用时合成会偏暗/丢内容(见「兼容导出」); 预览请用 SVG 渲染器(默认);导出无需任何操作,站点检测到「共用轨道遮罩」时会自动切换为 SVG 逐帧光栅化并在状态栏提示。这是 lottie-web canvas 渲染器自身的问题,站点侧只能绕过;
  • 亮度遮罩(tt:3)在 canvas 渲染器下是直接失败:lottie-web 5.13.0 的 assetLoader.loadLumaCanvas 因上游漏写调用括号而不存在, 构建这类图层就抛异常、异常又被 lottie 自己吞掉,表现是整帧全黑。含 tt:3 的动画(目前只有黑潮爆破默认弹窗动画) 在切进去时会被自动切到 SVG 渲染器、导出也自动走 SVG 逐帧光栅化;若手动把渲染器切回 Canvas,画面会变黑 —— 那是这个上游 bug,不是数据坏了;
  • 黑潮爆破默认弹窗动画的数据被本地裁到 258 帧(4.30s):原始 AE 导出是 600 帧,但画面在第 ~230 帧就淡完了,后 6 秒是空尾。 裁切只改 op 与图层 op 上界、不动任何关键帧,备份在 animation_data.pre-trim-258.json;从 AE 重新导出会覆盖它,需要重做;
  • 仓库中的调试转储(tools/*.raw/.pcm/.png 等)体积巨大,其中超大的(如 tools/.frames.raw)已加入 .gitignore,请勿再提交;
  • 素材版权归原游戏所有,开源使用请自行确认授权。

License

License: GPL v3

Copyright © 2026 TFR-Studios

本项目以 GNU General Public License v3.0(GPL-3.0) 开源,完整条款见 LICENSE:可以自由使用、修改与再分发,但基于本仓库代码的衍生作品必须同样以 GPL-3.0 发布,且程序不提供任何担保。

注意:LICENSE 仅适用于仓库自研的编辑器代码;仓库内的游戏动画素材(JSON/PNG/音频)版权归原游戏版权方所有,不在 GPL-3.0 授予范围内,开源 / 商用前请自行确认授权(见文首「注意」)。

Releases

Packages

Contributors

Languages