从 0 到 1 给 Three.js 装上 Timeliner:一条可拖、可存、可调试的 3D 动画时间线
2026/9/20 6:41:39 网站建设 项目流程

从 0 到 1 给 Three.js 装上 Timeliner:一条可拖、可存、可调试的 3D 动画时间线

【免费下载链接】Book3_Elements-of-MathematicsBook_3_《数学要素》 | 鸢尾花书:从加减乘除到机器学习;上架;欢迎继续纠错,纠错多的同学还会有赠书!项目地址: https://gitcode.com/GitHub_Trending/bo/Book3_Elements-of-Mathematics

如果嫌 Three.js 动画时间线只能靠代码硬写,Timeliner 是目前我见过最轻的解法:一个 JS 单文件,关键帧、缓动补间、幽灵帧、JSON 导入导出全内置,3D 关键帧动画从此能拖、能存、能调试,不用再对着 rAF 循环里的一串魔法数字干瞪眼。这篇不抄文档,按"先跑通最小闭环 → 逐层加料 → 收尾调优"的顺序走一遍,每一步都写清楚你刚做了什么、下一步干什么。

一、先把最小闭环跑通:让 cube 跟住时间线 🏁

接入本身没有仪式感:仓库里 src 是拆分的主程序和 UI 组件,build 里是打包好的单文件,test.html 就是能直接打开的样板;引一下 build 产物就行,自己改过源码就跑一下npm run mini重新出包,完事。

Timeliner 最香的一点是它根本不碰 three:它只负责往一个普通 JS 对象里写数字,Three.js 那边由你在渲染循环里把数字搬到 mesh 上。整条链路就一个最小闭环:

const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100); camera.position.z = 4; const renderer = new THREE.WebGLRenderer(); renderer.setSize(innerWidth, innerHeight); document.body.appendChild(renderer.domElement); const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({ color: 0x4488ff })); scene.add(cube); const rig = { rotX: 0, posY: 0 }; // 普通对象就是动画目标 const tl = new Timeliner(rig); tl.addLayer('rotX'); tl.addLayer('posY'); (function tick() { requestAnimationFrame(tick); cube.rotation.x = rig.rotX; // 漏掉这两行,cube 就永远不动 cube.position.y = rig.posY; renderer.render(scene, camera); })();

这段在干什么:前半截是 Three.js 场景三件套,照抄即可;rig是动画目标,tl接管它;tick 里那两行赋值才是整篇文章最重要的一行半代码。

踩坑提醒:cube 不跟着时间线动,九成两种原因——要么漏了 tick 里的赋值,要么图层名和 rig 的属性名对不上(addLayer('rotX')就必须在 rig 里有rotX)。它能驱动的属性没有限制,凡是 number 都行:position、rotation、scale、材质的 opacity,甚至你自己定义的进度参数。

二、双击时间线加关键帧,批量打点交给代码

时间线上双击就是加一个关键帧,拖点挪时间,拖数值改幅度,这套交互不需要教程。真正省事的是程序化批量打点,比如"入场 0.5 秒、翻转 2 秒、回落"这种固定套路,手点又慢又飘:

tl.addKeyframe('rotX', 0, 0); tl.addKeyframe('rotX', 2, Math.PI); tl.addKeyframe('posY', 1, 1.5);

三个参数就是"哪个图层、第几秒、什么值"。我一般先在 UI 上双击打两个点找手感,确认节奏对味,再换成这种代码批量铺。

一个时间线管不管得了多个对象:管。不同对象用不同图层名区分(cubeA_rotXcubeB_posY),一条时间轴统管整个小剧场;只有当两组动画要完全独立播放时,才需要各 new 一个 Timeliner 实例。

三、缓动曲线怎么挑:给关键帧配一条 tween

缓动(补间)是逐关键帧配的,决定的是"从这个点走到下一个点"的路径形状。常见的就几档:

  • linear:匀速,适合机械臂、传送带这类"就该这么走"的运动;
  • quad / cubic 系:起手收尾带惯性,镜头推移、UI 弹层的基本盘;
  • bounce / elastic:物理回弹,球落地、按钮按压的专属味道。

每个关键帧长这样,tween 字段就是那条曲线的名字:

{ "time": 2, "value": 3.14, "tween": "quadEaseOut" }

我的经验:3D 场景里大位移优先 cubic 系,稳;elastic 留给点缀,一屏出现两次以上就开始闹。

四、开幽灵帧:把前后几帧叠进场景里 👻

单帧预览判断不了节奏——弹跳的间距、翻转的弧线,只看一帧永远分不清"快了一点还是慢了一点"。这时候上洋葱皮:把当前时刻附近几帧同时画出来。

机制一句话:tl.getValues(3, 0.12)取出当前时刻附近 3 帧、间隔 0.12 秒的值;你预建几个半透明克隆,每帧把值贴上去就行。

// 幽灵预建,材质必须 clone,否则透明度互相串 const ghosts = [0, 1, 2].map(() => { const g = cube.clone(); g.material = cube.material.clone(); g.material.transparent = true; g.material.opacity = 0.25; scene.add(g); return g; }); // 每帧取附近 3 帧的值贴上去 const vals = tl.getValues(3, 0.12); ghosts.forEach((g, i) => { g.rotation.x = vals[i].rotX; g.position.y = vals[i].posY; });

注意两个细节:ghosts 必须在进场前预建好,不能每帧 new;数量压到 4~6 个封顶,叠太多画面就糊了,节奏反而看不准。

五、save / load:把时间线存成 JSON 复制走 📦

调好的动画别锁在页面里。tl.save()返回整条时间线的数据对象,tl.load(data)把它灌回去,UI 跟着刷新——一存一读,3D 动画补间就能像文件一样搬。

{ "version": "1.2.0", "title": "cube 翻滚", "layers": [ { "name": "rotX", "values": [ { "time": 0, "value": 0, "tween": "quadEaseIn" }, { "time": 2, "value": 3.14, "tween": "quadEaseOut" } ] }, { "name": "posY", "values": [ { "time": 0, "value": 0 }, { "time": 1, "value": 1.5, "tween": "cubicEaseInOut" }, { "time": 3, "value": 0, "tween": "cubicEaseInOut" } ] } ] }

version、title、layers 三层结构一目了然:每层按名字对应 rig 的属性,values 里每个点带 time 和 value,可选 tween。这份 JSON 丢进 localStorage、后端接口或纯文本发给同事都行——换项目、换设备接着调,不用重画。顺带一提,Timeliner 本身有 localStorage 自动保存,关页重开手稿不丢。

六、调试三板斧:快捷键、鼠标拨片、偷看内部数据 🔍

日常调动画基本不用碰代码:空格播放/暂停,时间线上点按跳转。真正让我调复杂序列时离不开的是"鼠标拨片"——把鼠标横扫当成推子,手感和调音台一样:

addEventListener('mousemove', (e) => tl.setTime((e.clientX / innerWidth) * 8) // 8 秒时间轴跟着鼠标走 );

第三板斧是给控制台留个后门:页面里写一句window._tl = tl;,之后随时在 console 看tl.currentTime、翻_tl.data里每层的原始关键帧。图层名和 rig 属性对不上、tween 拼错这类问题,一眼就能从数据里看出来,比肉眼看 UI 快得多。

再解释一个高频现象:切后台再切回来,时间轴"冻结"了——不是库的问题,浏览器对后台 tab 的 rAF 做了节流,切回来自动恢复,不用处理。

七、大场景不卡的三个习惯 ⚡

动画对象一多,掉帧基本逃不掉,但九成是写法问题。按这三条来:

  1. 图层最小化:只开真正要动的属性。7 层全开的成本远高于 2 层,没人盯着的图层就关掉。
  2. 预建、复用、帧内零 new:ghosts、材质、geometry 全部进场景前建好,tick 里只改 transform 和数值,任何new都挪出循环。
  3. 一帧一次写入:rig 到 mesh 的搬运集中放在 tick 末尾一次完成,同帧对同一属性赋值两次是纯白烧。

做到这三条,几十个对象加一组幽灵帧的场面,中端机器跑 60 帧没什么压力。

八、收尾:还能往哪接

时间线通了之后,玩法是开放的:手调曲线包络线做更刁钻的节奏;用setTime把动画和音频时间轴对齐;甚至把setTime接到 WebSocket 上,让远程客户端直接拨你这边 3D 场景的时间轴。

延伸一句:缓动的本质是插值,关键帧之间的每一步都是多项式在说话。想把这套动画数学底子补齐——从向量、导数一路看到它们怎么服务机器学习,同系列的《数学要素》是个顺路的选择。

【免费下载链接】Book3_Elements-of-MathematicsBook_3_《数学要素》 | 鸢尾花书:从加减乘除到机器学习;上架;欢迎继续纠错,纠错多的同学还会有赠书!项目地址: https://gitcode.com/GitHub_Trending/bo/Book3_Elements-of-Mathematics

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询