在线站点: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 小图标(与文字同步,第 32images/ 八张图标间切换、也可上传自定义图标(单图标位形态;上传的任意长宽比图标一律等比完整显示、不裁切);「主题颜色」区块不出现(它只跟踪站点约定的两族 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(单段动画,没有第二段可接)。
双击仓库根目录的 启动网站.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 个帧资源不会逐个列出,只列"这一条序列";
- 带 AE「填充」效果的图层(HUD 底条、提示条的遮罩源与显示层)改的是填充色:画面里看到的就是这个颜色,所以取色器直连它、重置还原动画原值。这条同时补上了站点缺失的 AE 效果 ——
- 黑潮爆破默认弹窗(见下)的「图片图层」把该动画的全部图片图层都列出来,共 7 项:6 张位图(提示条 / 图标 / 亮度遮罩源 / HUD 底条 / 两张整幅图)+ 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 而不是布尔):改字/改色引起的重建不会再把时长冲回默认值,也不会出现"先载入的那套吃掉另一套默认时长";
- 两个默认时长各按动画 key 只应用一次(
- 文字宽度自适应(位置暴露,两段独立):改字后底框 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上,便于确认到底靠哪一路判出来的(回归脚本见下面「验证移动端判定」);
- Chromium 的
-
编码方案先探测再开跑:开工前用
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永久为真、导出按钮永久禁用(之后再点就是「没反应」)。
含「共用轨道遮罩」的动画(被遮罩层 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注册表驱动。四步:- 把新动画的 JSON(含可选的弹窗 JSON 与音频)放进
animations/下的一个新目录,按?url导入(音频同理),并写一个ensure*Data按需加载函数(参考ensureMissionData); - 在
ANIMATIONS里加一项AnimDef:key/label/meta(搜索 tags、封面主色 accent、画布尺寸与帧率帧数、特性 features)/poster/data()/popup()/audioUrl/loaded()/load()/caps; - 跑一次
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,封面按老样子生成)。大战场弹窗动画就用了这条来圈出图标位; - 重新构建。顶栏触发器、动画画廊的卡片 / 搜索 / 数字键直选、侧栏区块的显隐全部自动跟上,不需要再改
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;
- 把新动画的 JSON(含可选的弹窗 JSON 与音频)放进
- 重新生成动画预览图:
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 普通模式、两者的电脑模式、仅 ChromiumSec-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(或连接 GitHub 自动构建);
- 构建设置:
- 构建命令:
npm run build - 输出目录:
dist - Node.js 版本:22;
- 构建命令:
- 部署后即可在公网编辑与导出,无需任何后端。线上实例: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,请勿再提交; - 素材版权归原游戏所有,开源使用请自行确认授权。
Copyright © 2026 TFR-Studios
本项目以 GNU General Public License v3.0(GPL-3.0) 开源,完整条款见 LICENSE:可以自由使用、修改与再分发,但基于本仓库代码的衍生作品必须同样以 GPL-3.0 发布,且程序不提供任何担保。
注意:LICENSE 仅适用于仓库自研的编辑器代码;仓库内的游戏动画素材(JSON/PNG/音频)版权归原游戏版权方所有,不在 GPL-3.0 授予范围内,开源 / 商用前请自行确认授权(见文首「注意」)。