在线站点: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/)+ 图标不透明度,另可改任务名 / 预期报酬 / 数字等文字层; - 地图标题动画:改「地图名称 / 地图地点」两行文字与其颜色,以及「描边」图层的白轮廓颜色(方形画布,见「内置动画」);
- 视频导出: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 带动入场/退场;纯文字 + 形状,没有图片资源/音频/叠加序列 |
五套动画均为 60fps;画布尺寸:撤离动画 1920×1080,位置暴露动画 3840×1080(内容按实测中心平移居中,导出分辨率与之相同),核电站功率动画 1920×1080,任务弹窗动画 1920×1080,地图标题动画 1024×1024(327 帧 ≈ 5.45s;方形画布,内容按宽度 80% 摆放)。
上面这些信息(名称、搜索关键词、尺寸、帧率、帧数、特性标签)都声明在
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 重新导出"覆盖): ① 画布由 1920×1080 改为方形 1024×1024,内容放大到占画布宽度 80% —— 原始导出里内容只占画面 18.3%, 摆在方形画布上会显得又小又空。实现方式是只改根空对象
ind=1的缩放与位置(缩放 257.6% → 952.58%, 位置 960,540 → 514.73,474.35),所有图层都是它的后代、跟着等比缩放,动画关键帧一个都没动; 备份animation_data.pre-1x1-canvas.json。改画布尺寸会同时改变导出视频的分辨率(站点按data.w/h出片); ② 「描边」图层在侧栏只显示描边取色器(规则见「右侧编辑面板」)。 另外该动画的chars只烘焙了「地图点名称」五个字,改其它字时由dropCharsIfUncovered自动退回浏览器文本引擎,不会消失。
要求:Node.js ≥ 18(推荐 20/22,仓库自带 .nvmrc = 22)。
npm install
npm run dev然后打开 http://localhost:5173。
不想本地跑?直接访问线上站点 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% 并在状态栏提示,之后可用滑块调回;
- 预合成内容也能编辑:顶层图层的文字/形状/图片照常列出,被引用的预合成(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,列表不会变空); - 任务弹窗动画的单图标位形态:没有选项卡与「显示图标」开关,默认选中
MallIcon_HafuCoins(即动画自带的那枚货币图标), 下方多一条 「图标不透明度」 滑块 —— 图标是一张普通位图图层,滑块按比例缩放它自己的透明度关键帧(素材是「淡入 → 80% → 淡出」,滑块代表最亮时的透明度);
- 动画时长(位置暴露):主段时长(默认 1.25s)、二次扫描开关与时长(默认 3.2s);
- 文字宽度自适应(位置暴露,两段独立):改字后底框 X 缩放按文字宽度增量同比放大(≤4 字保持原大小),图标随文字左缘平移保持固定间距;二次扫描段的「光.png」是绝对层(不在「空 2」组内),底框加宽不会带动它,因此它的位移关键帧会按与底框完全相同的仿射变换改写(x′ = 枢轴 + (x − 枢轴) × k,枢轴 = 底框缩放定点),光带始终扫过整块底框;文字还原时 k=1,关键帧自动回到基准值;
- 弹窗图层(撤离):勾选「显示弹窗」叠加弹窗;弹窗文字内容/颜色/对齐可编辑,底板与感叹号随文字宽度自适应,弹窗与主动画同帧同步。
所有会“整段重渲染”的操作(改字、改色、微调、开关二次扫描等)都已做单飞队列 + 重建完成后再恢复播放位置处理,连点/拖动滑块不会闪烁或卡在半成品状态。
导出为 60fps,携带当前背景色与音频:
| 选项 | 格式 | 说明 |
|---|---|---|
| 未勾选透明 | MP4 / AVI | H.264 30 Mbps,分辨率跟随当前动画(撤离 1920×1080 / 位置暴露 3840×1080 / 核电站功率 1920×1080 / 任务弹窗 1920×1080);AVI 为 H.264-in-AVI(带 avcC),PotPlayer/VLC/剪映/FFmpeg 系通用 |
| (自动)本机无 WebCodecs | AVI(MJPEG) | 无法用 WebCodecs 编码 H.264 时,自动改用 JPEG(质量 0.92)逐帧压缩并自行封装为 MJPEG-AVI,状态栏显示「导出 AVI(MJPEG)」;兼容性最好,但体积与画质弱于 H.264 |
| 勾选「透明背景」 | AVI(自动锁定) | 无压缩 32 位 BGRA 预乘 alpha,保留真实透明通道,可直接导入 AE/PR(体积约 8MB/帧) |
下载文件名分别为 animation.mp4 / animation.avi / animation_transparent.avi。
含「共用轨道遮罩」的动画(被遮罩层 tt 多于遮罩源 td,如核电站功率动画)导出时自动改用 SVG 渲染器渲染、 再逐帧光栅化后交给编码器,页面里没有任何开关;其余动画仍走原有 canvas 管线(它们的遮罩是标准配对,canvas 结果正确)。
为什么需要它: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 自身的问题,站点侧无法在预览里绕过。
注意事项:
- 普通播放器不会合成 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 级); - 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:两个文字层 + 底框/描边,无图片与音频)
├── 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
├── 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 里读出全部动画,新动画自动纳入); - 重新构建。顶栏触发器、动画画廊的卡片 / 搜索 / 数字键直选、侧栏区块的显隐全部自动跟上,不需要再改
switchAnimation,也不需要动index.html;
meta里的尺寸/帧率只是"未载入时的声明值",动画一旦载入过就会用实测值覆盖(见measureAnim),所以填个大概即可;caps决定侧栏哪些区块对这套动画开放:timing(动画时长区块)、nextScan(二次扫描开关)、defaultDuration(首次载入套用的默认时长);不写就是都没有;- 若新动画的图片资源在 JSON 里是相对路径(形如
u:"images/"+p:"xxx.png"),载入时要把该资源改写为?url导入的打包地址(见ensureMissionData),否则站点里会 404;
- 把新动画的 JSON(含可选的弹窗 JSON 与音频)放进
- 重新生成动画预览图:
node tools/build-anim-posters.mjs(dev server 没起会自己起一个);预览图在posters/<key>.webp,统一 640×360,由真实渲染截取并自动裁到内容范围。某套动画的展示帧不理想时,改脚本顶部的FRAME_RATIO; - 改造动画选择器(改界面本身,不是改动画):只需要碰三个地方 ——
- 动画有哪些 / 叫什么 / 什么规格 / 哪些关键词能被搜到 →
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。 脚本里还有一条**「分组筛选 / 收藏 / 简介不该存在」**的断言 —— 这三样是有意去掉的,如果哪天加回来,这条会红,提醒同步更新文档与样式; - 替换百叶窗叠加序列:把 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, 列表不会变空;此外用户也能在页面里直接上传自定义图标(仅当前会话有效); - 换主题色:直接改动画 JSON 里对应颜色的数值,或在页面「主题颜色」区一次换掉(判据是颜色值,见上);
- 更换/新增图标:位置暴露动画的图标放进
animations/animation_2/icon/,任务弹窗动画的放进animations/animation_4/icon/,重启 dev 后自动出现在「图标选择」里 (同名同时存在 PNG 与 WebP 时只显示 WebP)。建议转成 WebP 减小体积:python tools/png-to-webp.py --delete-png(q=95,alpha 无损,体积约为 PNG 的 43%)。
- 把仓库导入 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),且体积很大;
- Canvas 渲染器依赖站点的 fillText 兜底,若动画包含未被处理的特殊文字排版,可能需要扩展兜底逻辑;
- 该兜底里的「重算逐字母位置」只对没有字形轮廓表(
chars)的动画有效:自带 chars 的动画 lottie 已算好间距,再补偿一次会把字距放大近一倍(实测地图标题动画文字从 347 撑到约 694 合成单位、盖过底框并被画布裁掉)。判定见advanceMissing—— 以letters[i].l是否为 0 为据,两类动画各走各的;导出走的就是 Canvas 渲染器,所以这条会直接影响成片;
- 该兜底里的「重算逐字母位置」只对没有字形轮廓表(
- 轨道遮罩在 canvas 渲染器下不可靠:遮罩源被多图层共用时合成会偏暗/丢内容(见「兼容导出」); 预览请用 SVG 渲染器(默认);导出无需任何操作,站点检测到「共用轨道遮罩」时会自动切换为 SVG 逐帧光栅化并在状态栏提示。这是 lottie-web canvas 渲染器自身的问题,站点侧只能绕过;
- 仓库中的调试转储(
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 授予范围内,开源 / 商用前请自行确认授权(见文首「注意」)。