《Threejs入门》Ch14 Vite 工程化 + 补间动画:从“网页片段“到“项目“
本篇第 4 篇 · 让场景动起来 配套示例examples/ch14/本书第一个完整 Vite 工程下载项目完整代码运行即可。!DOCTYPEhtmlhtmllangzh-CNheadmetacharsetUTF-8metanameviewportcontentwidthdevice-width, initial-scale1.0titleCh14 · Vite 工程补间动画实验室/titlestylehtml, body{margin:0;height:100%;overflow:hidden;background:#0d1117;}#stage{display:block;}#ui, #panel{position:fixed;padding:10px 12px;border:1px solid #2d3646;border-radius:8px;background:rgba(13,17,23,.88);color:#b8cfe8;font:12px/1.6 Consolas,monospace;}#ui{top:12px;left:12px;}#ui button{margin:3px 4px 0 0;padding:5px 9px;border:1px solid #2d3646;border-radius:6px;background:#161f2c;color:#b8cfe8;font:12px Consolas,monospace;cursor:pointer;}#panel{top:12px;right:12px;width:400px;max-height:92vh;overflow:auto;white-space:pre-wrap;}#panel .ok{color:#9fd59f;}#panel .seg{color:#e8b34b;}/style/headbodycanvasidstage/canvasdividuibuttonidbtnRace赛跑Linear vs Cubic vs Elastic/buttonbuttonidbtnTour相机运镜补间 target/buttonbuttonidbtnReset复位/buttondivstylemargin-top:6px;color:#6c7f96;点任意方块补间飞到红色展台brCh13 拾取 × Ch14 补间的合体/div/divdividpanel装载中…/divscripttypemodulesrc/src/main.js/script/body/html本章目标把项目从importmap 单文件升级成真正的工程npm 依赖、模块拆分、热更新、构建产物理解每一样解决的是什么痛点。掌握补间tween的数学值 起点 (终点-起点) × easing(t/T)——它是 Ch12 时间驱动动画的工业封装。会背/会用常用缓动函数Linear、Quad/Cubic 的 In/Out/InOut、Elastic/Back 过冲族能手算任意时刻的取值。用 tween.js 三板斧new Tween(对象).to(目标值, 毫秒).easing(...).start() 每帧update()onComplete理解补间任意普通对象的设计为什么能直接改mesh.position。工程收官动作点一个物体让它飞到展台——Ch13 拾取 × Ch14 补间合体第 4 篇闭环。14.1 importmap 的三堵墙前 13 章的单文件写法其实一直是最简可用CDN importmap 免安装、双击即学。但当你要做真项目三堵墙会依次撞上依赖墙想用 npm 上的库物理引擎、状态管理、甚至离线版 three——importmap 只能手写 URL 清单版本、传递依赖A 库又 import B 库全得自己操心。组织墙一个 1500 行的script是灾难。ES 模块的import { createWorld } from ./scene.js拆分在file://下会被 CORS 拦Ch1 就说过在 CDN importmap 下又只能整体维护。工具墙没有热更新改一行全手动刷新、没有压缩混淆、没有按需打包、没有 TypeScript/JSX/样式预处理的位置。Vite法语快一次解决三堵npm管理依赖浏览器原生 ES 模块做开发服务器拆文件天然支持改代码模块级热替换 HMRvite build用打包器产出上线文件。它不是必须的——本书后续章节示例仍回到单文件保打开即看——但工程化是从业余到职业的门槛值得在第 4 篇收尾时专门踩一遍。14.2 五分钟上手一个 three 工程npm create vitelatest ch14 -- --template vanilla # 脚手架只要 HTML/JS 空壳 cd ch14 npm i npm i three得到的目录与本书示例的对应关系ch14/ ├── index.html ← 入口不再写 importmap只留 script typemodule src/src/main.js ├── package.json ← 依赖清单 scriptsdev/build/preview ├── node_modules/ ← npm 装进来的库three 就在这里版本被 package.json 锁死 ├── public/ ← 原样拷贝的静态资源模型、贴图不走打包器 └── src/ ├── main.js ← 入口模块装配 循环 事件接线 ├── scene.js ← 场景怎么搭导出 createWorld() └── tweens.js ← 动画怎么播导出 createTweens()裸导入的魔法import * as THREE from three没有 URL、没有路径——解析规则写在node_modules/three/package.json的exports表里. → ./build/three.module.js、./addons/* → ./examples/jsm/*。所以import { OrbitControls } from three/addons/controls/OrbitControls.js在 Vite 工程里照常工作和前 13 章 importmap 的写法逐字相同——我们只换了从 CDN 找到从本地 node_modules 找代码本身零改动。这也回答了那 importmap 还有用吗演示页、CodePen、不装 Node 的场合它是好选项工程一律走 npm。跑起来npm run dev -- --host --port 18000--host让手机同 WiFi 也能访问本书标配电脑 localhost 手机局域网 IP。端口被占时 Vite 不报错、自动顺延18000 占用 → 起在 18001看终端最后一行给的实际地址Windows 还有一段系统保留端口会直接报 EACCES撞上就换更高端口——环境坑详见附录 D 的环境与工具小节。三兄弟命令分清楚dev开发服务器HMR、不合并、源码即产物build产出dist/压缩、带 hash 文件名上线用preview起个静态服务器看dist/——验收性能用 preview别用 devdev 下未压缩的 three 会吓你一跳。npm run build会打一条警告“chunk 大于 500 kB”——正常整个 three 打进去了。three 是无副作用模块打包器已经帮你剔掉了没 import 的 API但库本身 60 万字符起步进阶手段是动态 import/分包Ch20 性能篇再谈本章知道这警告无害即可。14.3 补间的数学一条曲线控制一段旅程补间tween中间值的行话把 Ch12 的时间驱动思想产品化你只说起点、终点、时长、缓动每帧的值它算。核心公式一行k(t) clamp(t / T, 0, 1) // 归一化进度0 出发、1 到站 value A (B − A) × easing(k) // easing 把时间比例弯成进度比例easing: [0,1] → [0,1]且easing(0)0、easing(1)1本章用 k 表示这个归一化进度参数与 Ch8 材质光照系数区分——上下文不同。对最常见的Out 族先快后慢UI/交互首选像被递到手上函数公式k0.25k0.5k0.75性格Lineark0.25000.50000.7500机械、匀速Quadratic.Outk(2−k)0.43750.75000.9375起步冲刺Cubic.Out1−(1−k)³0.57810.87500.9844更陡的到站缓冲Quartic.Out1−(1−k)⁴0.68360.93750.9961近乎瞬移微调Exponential.Out1−2^(−10k)0.82320.96880.9945极端收尾Cubic.InOutk0.5: 4k³0.06250.50000.9375两头慢中间快运镜Elastic.Out2^(−10k)·sin((10k−0.75)·2π/3)11.12501.00000.9961过冲回弹振荡Back.Out1 c₃(k−1)³ c₁(k−1)²c₁1.701580.81741.08771.0641先越站再退回三条读表法①In 与 Out 互为时间镜像out(k) 1 − in(1−k)Cubic.Ink³ 镜像过来正是 1−(1−k)³InOut 前半 In 后半 Out 的拼接所以InOut(0.5)0.5恒成立任何对称缓动都是。②导数读手感Out 族k0处斜率最大 起步猛Elastic/Back 出现easing1 位置冲过终点再荡回——面板上进度 112.5%就是这么来的不是 bug 是性格。③选族口诀物体被放置用 Out被抛出/启动用 In镜头与长距离用 InOut俏皮 UI 才用 Elastic/Back。手算例题每帧面板复核Cubic.OutT1600ms问 t800msk0.5时 9 米赛跑走了几米1−(1−0.5)³ 0.875 → 9×0.875 7.875 米。同一时刻 Linear 才 4.5 米Elastic 更绝——k0.25 时已冲到 10.125 米越过终点 12.5%k0.5 正好荡回 9.0 米。示例三车道赛跑就是这个对照的动画版。14.4 tween.js把任意普通对象当动画轨道three 官方 addons 里就带了一份 tween.jsthree/addons/libs/tween.module.js本书 0.186.1 内版本 23.1.1不装额外依赖就能用。它的哲学一句话补间引擎不认识3D它只会对一个对象的若干数字字段按公式改值——而mesh.positionVector3恰好就是带 x/y/z 数字的对象于是万物皆可补间newTween(mesh.position)// 被补间的宿主任何对象.to({x:4.5},1600)// 目标值 时长(ms)——注意是毫秒.easing(Easing.Cubic.Out).start();// 注册进全局主组// 渲染循环里updateTweens();// 不传参 用内置 performance 时钟推进所有补间它每帧做的事就是 14.3 公式记下起始值{x: -4.5}按elapsed/1600求easing(k)把-4.5 9·easing(k)写回pos.x。对象被改 → Ch10 矩阵链 → Ch11 投影链全自动——前 11 章的机器在这里合拢成一次.to()。常用件.onComplete(fn)到站回调落台音效、解锁下一步.chain(t2)串轨道先位移后旋转的接力.yoyo(true).repeat(1)往返一次相机巡逻、呼吸灯repeat(Infinity) 永续.delay(300)起跑前等 300ms。工程纪律两条每个补间是一次性消耗品播完自动从主组移除要重复就得每次new——所以按钮处理器里重建是常态别复用旧实例循环里只调一次update()同 Ch12getDelta纪律时间线只有一个多处推进会互相偷时间。为什么用毫秒而 Clock 用秒两套时钟别换算混用——tween 时长一律 msCh12 的 delta 一律 s示例里不手动对表tween 内部自己拿performance.now()你只管 start/update。14.5 最小可运行示例examples/ch14/跑法见 14.2 最后。三条车道赛跑Linear / Cubic.Out / Elastic.Out 同 1600ms 同 9 米——同一个时刻三种位置面板实时显示进度百分比Elastic 那条会先冲破 100% 再荡回14.3 表格的活体。方阵里 6 个方块随便点Ch13 式intersectObjects命中后位置 Cubic.InOut 900ms 旋转 Quadratic.InOut 900ms 双轨并行飞到红色展台控制台打出落台onComplete。相机运镜按钮演示 14.4 的深层含义补间对象是controls.target——一个没人觉得能动画的普通对象yoyorepeat(1)让镜头看左→缓移到右→原路返回全程阻尼照常生效Ch11 的controls.update()与补间和平共处补间改目标、update 平滑追随两股力合成丝滑运镜。读源码顺序main.js60 行接线→tweens.js三个函数对应三个按钮→scene.js你已完全有能力自己读完——Ch7–Ch11 的知识全在这。拆分标准就一条一个文件说一件事。14.6 改造实验把赛跑第三条换成Back.Out面板进度 108.77%出现在 k0.5 附近——与 14.3 表格逐值对账。给flyTo的位置轨加.delay(200)并在其 onComplete 里对同一方块再开一条.to({ y: 1.6 }, 400)的上浮练.chain接力。把cameraTour的 target 换成camera.position直接补间 controls.enabledfalse对比两种运镜的观感差别为什么直接补位置会更硬阻尼没了14.4 两条力没了。在tweens.js里写一条自定义缓动easing((k) 1 - Math.pow(1 - k, 2.5))分数指数介于 Quad 与 Cubic 之间——公式手感立增。把工程npm run build后npm run preview对比 dev/preview 的首屏加载与控制台警告无害打开dist/看看产物文件名里的 hash 是干嘛的缓存指纹。14.7 常见坑坑 1忘了每帧updateTweens()。补间全部定格在起点。症状识别物体纹丝不动但 onComplete 也不触发——时间线根本没推进。坑 2.to()的时长写成秒。to({x:4.5}, 1.6) 1.6 毫秒瞬间到不是 1.6 秒。tween.js 全 API 毫秒和performance.now()同单位Ch12 的 Clock 才是秒。单位制混用时在手记里把换算写死在注释里。坑 3复用同一个 Tween 实例再播一次。播完即注销再start()旧实例行为不可靠它可能留着终点值当起点。规则重播 重新new示例的 race() 每次都全量重建。坑 4对同一个数字字段并行开两条补间。两条都每帧写position.x互相覆盖、跳变。要么.chain排队要么拆字段一条管 x 一条管 y。这是 Ch5乘法不可交换之外的另一种两笔改一页事故。坑 5把被 HMR 替换的模块对象继续补间。dev 下改了scene.js保存Vite 热替换会生成新对象旧补间还攥着旧mesh.position引用——现象是改了代码某物不动了。调试期一律刷新页面CtrlF5再判定 bug。坑 6npm run dev -- --port 18000忘了--。npm run dev --port 18000参数被 npm 自己吃掉Vite 起在默认 5173/顺延端口。--是后面的参数转交给脚本的分隔符npm 的老规矩。14.8 练习手算Back.Out公式14.3 表算k0.25的值与表格 0.8174 对账c₁1.70158, c₃c₁1。手算Cubic.InOut 在 k0.25 的值用前半段 4k³和镜像拼接两种口径各算一次应相等。推理14.5 里补间controls.target 每帧controls.update()与补间camera.position的观感差别用 Ch11 阻尼方程解释——谁在拖谁编程不用 tween.js按 14.3 公式手写一个 40 行内的makeTween(obj, target, ms, easing)返回 start()/update(nowMs)内部自动 clamp 与 onComplete。与 bundled 版对比功能差在哪。参考答案k−1 −0.75c₃(k−1)³ 2.70158 × (−0.421875) −1.13968c₁(k−1)² 1.70158 × 0.5625 0.957141 − 1.13968 0.95714 0.81746 ≈ 0.8174✓表格取整到 4 位。两个观察0.8174 远超 Linear 的 0.25——Out 族在 k0.25 就该大幅领先这是递到手手感的来源而真正的过冲值 1发生在后段k0.5 时 1.0877Back 的越站再退回藏在终点附近。口径一4 × 0.25³ 4 × 0.015625 0.0625。口径二InOut 前半 in(2k)/2 (0.5)³/2 0.0625✓ 相等。0.0625 离 0.25 很远——InOut 两头黏得厉害中段才放行这就是运镜用它的原因启停像有人扶着。补 target 时相机不直接动OrbitControls 的阻尼方程1−e^(−λΔt)家族每帧把相机→新球面位置平滑过去——补间是意图阻尼是肌肉两级低通合成丝滑。补 camera.position 则每帧被硬写、再被 update() 的阻尼往回拽update 会把 position 拉向 target 的球面解两股力打架画面发黏甚至抖动——坑 4两条补间改同一字段的变体凡是 controls 管的自由度动画 target别动画相机本体。骨架要点ms 时间线、clamp、一次性、onCompletefunctionmakeTween(obj,target,ms,easing){letstartVnull,t0null,donefalse;return{start(now){startV{...obj};t0now;donefalse;TweenList.add(this);},update(now){constkMath.min(1,(now-t0)/ms),eeasing(k);for(constkeyintarget)obj[key]startV[key](target[key]-startV[key])*e;if(k1!done){donetrue;this.onComplete?.();TweenList.remove(this);}returnk1;},};}与 tween.js 差的delay/chain/yoyo/repeat、共享时间组、字符串属性路径如rotation.z嵌套。写完你会尊重它 400 行实现了什么。本章小结importmap 三堵墙依赖/组织/工具→ Vitenpm create、裸导入走exports表、HMR、build出dist/、验收用 preview。补间 Ch12 时间驱动的封装值 A (B−A)·easing(t/T)out(k)1−in(1−k)InOut 对称、Elastic/Back 过冲进度可以 100%。tween.js 补任意普通对象position/rotation/controls.target都是数字袋每帧一次 update、每次重播重新 new、时长单位毫秒。拾取Ch13× 补间Ch14 交互的最小闭环点中→飞到这条链在 Ch19 GLTF、Ch22 产品配置器里会一再升级复用。第 4 篇完结世界会搭Ch7–11、时间有数Ch12、鼠标能点Ch13、项目像样Ch14。第 5 篇视觉进阶开场我们要处理一万个点——粒子系统以及让你亲手写出 three 自动生成的那段 GLSL着色器入门数学篇 Ch3–Ch6 的终极兑现。