Files
animejs-source-analysis/.memory/source-analysis.md
2026-04-23 23:19:29 +08:00

672 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# anime.js 源码深度解析
> **上游**https://github.com/juliangarnier/anime.git
> **版本**v4.3.62026-04-23 clone
> **作者**Julian Garnier
> **License**MIT
> **规模**11,121 行(不含 dist/test17 个模块
> **定位**:轻量级多用途 JavaScript 动画引擎CSS / SVG / DOM attrs / JS objectsv4 重写了整个内核
---
## 0. TL;DR 一分钟看懂
anime.js v4 内核是一棵**四层继承树**
```
Clock (core/clock.js) ← 107 行,只管 time/fps/speed/deltaTime 和双向链表 children
Timer (timer/timer.js) ← 535 行加生命周期play/pause/seek/reverse/complete/then
├── JSAnimation (animation/animation.js) ← 747 行,管 Tween 链表 + 预解析值
└── Timeline (timeline/timeline.js) ← 362 行,管 Timer/Animation children + 位置语法
Engine (engine/engine.js) ← 181 行,也 extends Clock 的单例,唯一持有 rAF 的节点
```
渲染全部走 `core/render.js` 的两个函数:`render()`(单 Tickable+ `tick()`(递归 timeline。所有 Tween 值在创建时就被**预解析**为 number/unit/color-rgba/complex 四类,运行期只做 `lerp + clamp + round + 字符串拼接`,没有正则。
单一 `requestAnimationFrame` 驱动整个应用所有动画engine 单例),通过 `document.visibilitychange` 自动暂停。
---
## 1. 对外 API 架构
`src/index.js` 只有 18 行,全是 re-export。每个能力是独立子目录`index.js` 多为空文件,真实代码在同名 `.js`)。外部 API 按"**工厂函数**"风格暴露:
| 工厂 | 类 | 文件 |
|---|---|---|
| `animate(targets, params)` | `JSAnimation` | animation.js:L747 |
| `createTimer(params)` | `Timer` | timer.js:L536 |
| `createTimeline(params)` | `Timeline` | timeline.js:L362 |
| `createAnimatable(targets, params)` | `Animatable` | animatable.js:L160 |
| `createDraggable($el, params)` | `Draggable` | draggable.js |
| `createScope(params)` | `Scope` | scope.js:L259 |
| `createLayout(root, params)` | `AutoLayout` | layout.js:L1607 |
| `createMotionPath(path, offset)` | 对象3 个 FunctionValue | motionpath.js:L80 |
| `createDrawable(sel)` | `Proxy<SVGGeometryElement>[]` | drawable.js:L111 |
| `onScroll(params)` | `ScrollObserver` | events/scroll.js |
| `stagger(val, params)` | `StaggerFunction` | utils/stagger.js:L82 |
| `waapi.animate(targets, params)` | `WAAPIAnimation` | waapi/waapi.js |
| `svg.morphTo(path2, precision)` | `FunctionValue` | morphto.js:L25 |
| `text.split($el, params)` | 三套 proxy | text/split.js |
全部支持链式:`.play() / .pause() / .reverse() / .seek() / .stretch() / .revert() / .then()`
---
## 2. 核心循环Engine + rAF
**engine.js:L47-165** 定义 `class Engine extends Clock`,导出一个**模块级单例** `engine`L155 IIFE`/*#__PURE__*/` 让 bundler tree-shake。关键
- **Tick 方法选择**L44-45浏览器用 `requestAnimationFrame`,非浏览器 fallback `setImmediate`,以支持 Node.js 测试。
- **Tick 入口**L168-175闭包函数 `tickEngine` 判断 `engine._head`(链表非空)才续命 rAF空了就把 `reqId = 0` 让循环自然终止。这是**按需暂停**策略 —— 没有正在跑的动画就不消耗帧。
- **每帧逻辑**L62-91 `update`
1. `this.requestTick(time)` 走 Clock 的 fps 限速L81-94
2. 遍历双向链表 `_head → _next`,每个 active tickable 调 `tick(activeTickable, localTime, ...)`
3. `localTime = (globalTime - _startTime) * _speed * engineSpeed`L74 —— 每个 tickable 有独立 speed。
4. paused 的 child 直接 `removeChild(this, activeTickable)` 退出循环L80-85
5. 最后 `additive.update()` 跑一次叠加动画的 recomposeL89
- **可见性联动**L159-162`doc.hidden` 自动 pause/resume可通过 `pauseOnDocumentHidden` 关掉。
- **timeUnit 切换**L126-142支持 `'ms' | 's'`,切换时对所有 existing durations 按 `globals.timeScale=0.001` 缩放。这是 v4 新增特性,作者为此付出了整个代码库 `round(_, 12)` 抗浮点误差的代价。
---
## 3. Clock 基类:时钟抽象
**clock.js:L23-107**,仅 107 行。是所有时间驱动对象的基类。
- **状态**`_currentTime / _lastTickTime / _startTime / _lastTime / _scheduledTime`L28-38。区分这么多时间戳是为了在 fps 限速、speed 变更、seek 跳转时都能正确推进。
- **`requestTick(time)` L81-94**fps 限速的核心。如果 `time < _scheduledTime` 直接返回 `tickModes.NONE`(跳帧),否则把 `_scheduledTime` 前推至少一个 `frameDuration`。算法可以跳多帧L92 `frameDelta < frameDuration ? frameDuration : frameDelta`),避免 tab 切回后疯狂补帧。
- **链表**`_head / _tail` 就是直接的 Tickable 链表头尾L47-50。helpers.js:L255-263 的 `addChild(parent, child, sortMethod?)` 支持**有序插入**`sortMethod` 用于让 Tween 在 siblings 链里按 `_absoluteStartTime` 排序)。
Clock 本身不感知动画,纯粹就是"会走的钟"。Engine 继承它就自动有了 child 管理Timer 继承它再加生命周期Animation/Timeline 再继承 Timer 加业务语义。**一条继承线四级语义分层** —— 这是 v4 架构最干净的地方。
---
## 4. Tween 值系统:预解析四象限
`core/values.js` 是整个库的"解析中心"。
### 4.1 tweenTypes 五分法consts.js:L17-23
根据**目标类型 × 属性类型**分五档:`OBJECT`(普通 JS 对象)/ `ATTRIBUTE`SVG/非 CSS 的 DOM 属性)/ `CSS`(样式属性)/ `TRANSFORM`transform 六件套)/ `CSS_VAR`CSS 变量)。
决策在 `getTweenType` values.js:L94-107
```js
!target[isDomSymbol] ? OBJECT : // 非 DOM
target[isSvgSymbol] && isValidSVGAttr ? ATTRIBUTE : // SVG attr
validTransforms.includes(prop) || shortTransforms.get(prop) ? TRANSFORM : // x/y/z/rotate/scale/...
prop.startsWith('--') ? CSS_VAR :
prop in style ? CSS :
prop in target ? OBJECT :
ATTRIBUTE
```
注意 **TRANSFORM 独立分支**(不走 CSS—— 这样 `translateX / scaleY / rotate` 可以独立补间,再在 render.js:L268-274 合并成一条 `transform: translateX(...) scaleY(...) rotate(...)` 字符串。缓存在 DOM 节点的 Symbol 属性(`transformsSymbol`, consts.js:L52避免每帧重读。
### 4.2 valueTypes 四分法consts.js:L26-31
- `NUMBER` —— 纯数字
- `UNIT` —— 数字+单位,如 `100px` `30%` `2turn`unitsExecRgx 解析consts.js:L114
- `COLOR` —— hex/rgb/hslcolors.js 转 RGBA 数组 `[r,g,b,a]`
- `COMPLEX` —— 任意混合字符串如 `rgb(0,0,0) 10px 10px 20px / inset calc(100% - 10px)`
`decomposeRawValue` values.js:L170-218 就是把原始值拆成:
```
{ t: valueType, n: 主数字, u: 单位, o: 运算符(+=/-=/*=), d: 数字数组, s: 字符串片段数组 }
```
**精妙之处**COMPLEX 类型会把字符串按数字切分成 `s[]`(字符串碎片)+ `d[]`(数字数组),运行期只需 `lerp(d_from[i], d_to[i], t)` 然后和 `s[]` 交错拼接render.js:L214-224—— **正则只在创建期执行一次,帧率临界路径是纯算术**
### 4.3 Operators+=/-=/*=
values.js:L146-150 的 `getRelativeValue` 处理 `+=10` `*=2` 这类相对值,在 composition 阶段animation.js:L460-473基于 prevSibling 的 `_toNumber` 或原值计算。
### 4.4 Tween 结构animation.js:L524-562
每个 Tween 是个 plain 对象(非 class省 prototype 开销29 个字段。关键:
- `_ease`:已经 parseEase 过的函数,运行期直接 `_ease(progress)`
- `_fromNumbers / _toNumbers / _strings`:预解析数组
- `_startTime / _changeDuration / _updateDuration / _absoluteStartTime`:时间四元组(支持 composition 剪切)
- `_prev / _next`:在 Animation 内部链表
- `_prevRep / _nextRep`:在 target-property siblings 链表里(用于 replace composition
- `_prevAdd / _nextAdd`additive 链表(用于 blend composition
一个 tween 同时存在于**三条链表**里。
---
## 5. 渲染管线 render.js
398 行单文件。`export const render = (tickable, time, muteCallbacks, internalRender, tickMode)` 是内核中的内核。
### 5.1 时间推进L66-104
- L88 `~~(tickableCurrentTime / (iterationDuration + _loopDelay))` —— **位运算 NOT 取整**比 `Math.floor` 略快(作者注释 L87
- L97 `isReversed = _reversed ^ (_alternate && isOdd)` —— XOR 一行处理 `reversed``alternate``isOdd` 三状态组合。
- L99 `iterationTime = isReversed ? iterationDuration - iterationElapsedTime : iterationElapsedTime`
- L100 playbackEaseTimeline 级别的总体 easing作用于**整个时间轴**`iterationTime = iterationDuration * _ease(iterationTime / iterationDuration)`
- L101 反向方向感知:`parent.backwards``time - prevTime` 两条路径推断方向
### 5.2 Tween 循环L156-278
- **跳帧优化**L166-174只有当前时间还没走完 + 当前 tween 没被 overridden + 前/后 siblings 没覆盖 + `forcedRender` 时才写入。
- **值类型分支**L193-224
- NUMBER`lerp + modifier + round`
- UNITNUMBER 的基础上加 `${n}${_unit}`
- COLOR对 RGBA 四路分别 lerp 再 clamp(0,255)
- COMPLEX遍历 `_toNumbers` lerp + 交错拼接 `_strings`
- **Transform 批写**L242-275
- 每个 transform tween 写入 `target[transformsSymbol][property]`(缓存属性)
- Animation 构造时标记链中**最后一个 transform tween** 的 `_renderTransforms = 1`animation.js:L601-616
- 只有 `_renderTransforms=1` 的 tween 被处理时才拼完整 `transform:` 字符串L268-274—— **一帧一写**,而不是每个 tween 写一次
- **DOM 写入分派**L236-254OBJECT 走属性赋值ATTRIBUTE 走 `setAttribute`CSS 走 `style[prop]`CSS_VAR 走 `style.setProperty('--x', ...)`TRANSFORM 先存 Symbol cache
### 5.3 tick 递归L338-398
- L341 如果是 Timeline按 children 链表递归
- L353-372 跨 iteration 时的 skipped callback 补触发forward 时强制 onCompletebackward 时补 onComplete
- L376 `childTime = (tlChildrenTime - child._offset) * child._speed` —— child 有自己的 offset 和 speed
- L386-395 所有 children `completed` 才触发 Timeline `onComplete`
---
## 6. Timer 基类:生命周期
**timer.js:L106-530**
- **scope 注册**L137构造时自动注册到 `scope.current`(如有),这让 Scope.revert() 能级联清理所有 Timer。
- **offset 计算**L162-170Timeline child 用 parent-relative 位置;顶层 Timer 用 `(engine._lastTickTime - engine._startTime) * globals.timeScale` 作为初始偏移,保证"创建即生效"的动画时间线连续。
- **play/pause/resume/reverse/alternate**L371-450语义完备。`play()` 必要时 alternate 一次再 resume`alternate()` 通过 XOR 切 `_reversed` 然后 seek 到对称时刻。
- **seek**L410-420关键是 `reviveTimer(this)` 先把取消的 tween 重新 compose 回 siblings 链L86-99。否则 seek 一个已 cancel 的动画会渲染到空。
- **stretch**L470-482等比缩放 duration / offset / delay —— 用于 timeline 动态"压缩/拉伸"已添加的 children。
- **Promise 集成**L512-528`timer.then(cb)` 返回 Promiseresolve 时机是 onComplete。L516-518 的 `this.then = null` hack 防 `async/await` 返回这个 thenable 导致的无限递归(引用 GitHub issue #26)。
---
## 7. JSAnimation 构造器747 行大工程
**animation.js:L210-740**,整个构造函数就是 442 行的流水线:
### 7.1 输入归一
1. `registerTargets(targets)``'.btn'` / Element / NodeList / ReactRef 等归一成数组targets.js
2. 如果有 `keyframes`generateKeyframesL127-208把**数组形式**或**百分比对象形式**的 keyframes 展开成 per-property 的数组。
3. `getTweenType` 分类每个属性。
4. 每个 property 的 value 归一成 `keyframes[]` —— `{to: v}` / `[from, to]` / `[v1, v2, v3]` / `{to, from, duration, ease}[]` 等各种语法最终都化简到同一结构L305-332
### 7.2 composition 处理L393-409
`composition === replace`
-`siblings = getTweenSiblings(target, propName)` —— WeakMap<Target, {prop: {_head, _tail}}>composition.js:L45-69
- 遍历 siblings 链找插入点,**后续 siblings 直接 override**(标记 `_isOverridden=1` + `_changeDuration=minValue`)。这让**重复对同一属性的动画自动接管前一个**(类 GSAP 的 overwrite
### 7.3 值解析L411-512
- `decomposeRawValue(fromValue, fromTargetObject)` / `decomposeTweenValue(prevTween, ...)`(先用 sibling 作 from 省一次 getComputedStyle
- **类型不匹配自动对齐**L475-494
- complex vs number → 把 number 拍成 complex所有数字位都填这个 number
- unit 不同 → `convertValueUnit` 调一次 getComputedStyle 换算units.js
- color vs non-color → 假色填 `[0,0,0,1]` 占位
- **长度不等的 complex**L506-511把短的补齐到长的长度空位填 0。
### 7.4 Tween 创建L524-576
字面对象工厂,非 class省 prototype 开销;创建后 `addChild(this, tween)` 挂到 Animation 链表,若需要 compose 则 `composeTween(tween, siblings)` 挂到 siblings 链。
### 7.5 大量 targets 的优化L263
```js
const tComposition = isUnd(composition) && targetsLength >= K
? compositionTypes.none : ...
```
当 targets >= 1000默认关掉 composition不做 overwrite 处理)。这是"**stagger 3000 个点**"这种场景的救命配置。
### 7.6 修正 iterationDelayL624-635
所有 tween 都扫一遍 `startTime`,把最小的 delay 从整个 Animation 提取出来当 `this._delay`,其他 tween 减掉这个值。这样 Animation.duration 等于"真实的有效动画时长",不把前导 delay 算进去。**这个 trim pass 非常重要**,否则 `iterationProgress` 会不准。
---
## 8. Composition 三态 + Additive
**composition.js:L95-262**。三种模式consts.js:L41-45
- **replace (0)**:默认。新 tween 进 siblings 链override 掉后续 siblings + 把前面 siblings 的 `_changeDuration` 剪到 overlap 点L142-158—— "前面的动画被截短,后面的被覆盖"。
- **none (1)**:不 compose纯粹写入。超多 targets 或性能关键场景用。
- **blend (2)**叠加式。多个动画同时影响一个属性Animatable 用这个实现"推送力合成")。
### blend 的精妙composition.js:L216-258
叠加模式下:
1. 给 target-prop 建一个额外的 `_add` WeakMap siblings 链。
2. 第一个 tween 时创建 lookupTween相当于"累加器")。
3. 每个新 tween 的 `_fromNumber = lookup._fromNumber - toNumber``_toNumber = 0` —— 本 tween 补间的其实是**相对于上次状态的 delta**。
4. engine loop 每帧结束调 `additive.update()`engine.js:L89—— 遍历所有 additive siblings`_number` 累加到 lookup tween 的 `_toNumber`,然后 force-render lookup tween 一次additive.js:L53-78
这就是**多动画叠加不互相覆盖**的实现机理。典型应用:鼠标悬停 scale up + 持续 idle wobble + 点击 shake三者同时生效。
---
## 9. Timeline位置语法 + stagger add
**timeline.js:L135-356**。继承 Timer主要加三件
- `labels: Record<string, number>` —— 具名锚点
- `add(targetsOrTimerParams, paramsOrPosition, positionOrStagger)` —— 多态 add
- `defaults: DefaultsParams` —— 子项默认值
### 位置语法timeline/position.js:L50-73
`parseTimelinePosition` 支持:
- 数字 → 绝对位置
- `undefined` → tlDuration追加到末尾
- `'label'``labels[label]`
- `'<'` → 上一个 child 的**起点**`_offset + _delay`
- `'<<'` → 上一个 child 的**终点**`_offset + _delay + duration`
- `'+=500'` `'label-=200'` `'<*=2'` → 运算符 + 基准
- 基准选择:有 sibling 就 sibling 起点;有 label 就 label否则 tlDuration
### add 的 stagger 分支timeline.js:L186-215
`a3` 是函数stagger 返回值)时,遍历 targets每个 target 都:
1. 新建 `staggeredChildParams`(避免 mutate 原 params
2. **重置** `this.duration = tlDuration; this.iterationDuration = tlIterationDuration`(关键,否则每轮 stagger 起点漂移)
3. 用 stagger 函数计算这个 target 的位置,调 `addTlChild`
4. 每个 target 都创建一个独立 JSAnimation
这样 `tl.add('.item', { opacity: [0,1] }, stagger(100, { from: 'center' }))` 就为 N 个元素创建 N 个 Animation每个有独立的 start time。
### syncL256-265
能同步原生 `globalThis.Animation`WAAPI和自己的 Tickable —— 创建一个 tweening `currentTime` from 0 to duration 的 Animation 驱动外部对象。非常机智。
---
## 10. Stagger复合生成器
**utils/stagger.js:L82-142**。返回一个 `(target, index, total, timeline?) => number|string` 函数。支持:
- **起始位置**`'first' | 'last' | 'center' | 'random' | index number`L94-97
- **栅格模式**`grid: [cols, rows]` + `axis: 'x' | 'y'` —— 欧几里得距离或单轴距离L117-126
- **range stagger**`stagger([0, 100])` 值域等分而非固定间距L100-103
- **easing**:把索引归一化 [0,1] 后过 easing 再映射回去L130
- **单位继承**:自动从 val 提取单位 `'2s'``'2s'`L139
- **custom use**`params.use: 'data-delay'` 从元素属性读 index 值L108
- **从属 timeline**:传入 `timeline` 时会把 stagger 的 base offset 结算为 timeline 位置语法L135`parseTimelinePosition` —— 支持 `tl.add('.item', ..., stagger(100, { start: 'label1' }))`
values 数组是**懒初始化 + 缓存**L112整个 stagger 生成器只算一次距离 map。
---
## 11. Animatable超高频 setter
**animatable.js:L41-153**。场景鼠标跟随、陀螺仪、3D 控制这类"**逐帧被外部驱动**"的动画。
用法:
```js
const a = createAnimatable(el, { x: 200, y: 200, ease: 'outQuad' })
document.addEventListener('mousemove', e => a.x(e.clientX).y(e.clientY))
```
实现:
- 构造时为**每个 property 创建一个独立的 autoplay:false JSAnimation**L107
- 每个 property 的 setter 函数L110-139
- 无参时 return 当前值(`_number / _numbers`
- 有参时更新所有 children tween 的 `_fromNumber = 当前值``_toNumber = 新目标`L129-130
- `animation.reset(true).resume()` —— 从头开始补间L136
- 有个隐藏的 `callbacks` JSAnimation`{v: 0, v: 1}` dummyL90用来统一触发 begin/pause/complete —— 因为真实动画有 N 个独立 Animation哪一个跑完不代表全部完成。`pauseHandler` L52-65 扫所有 children paused 状态,全 paused 才 complete。
这个 API 配合 blend composition就能实现"多输入源推同一个对象,平滑叠加"。
---
## 12. ScopeReact/Angular 生命周期桥
**scope/scope.js:L41-253**。核心概念是 `scope.execute(cb)` 压栈式**上下文**
```js
scope.current = this
scope.root = this.root // DOM 查询 fallback root
globals.defaults = this.defaults // 这个 scope 内创建的动画继承此默认
cb(this) // 回调里创建的 animation 全注册到 this.revertibles
// 还原
```
Timer constructor 里timer.js:L137自动 `scope.current.register(this)`,所以在 scope.add 的 cb 里创建的动画会自动被 scope 管理。
### 关键 API
- `.add(fn)` —— 每次 refresh 时重跑
- `.add(name, fn)` —— 注册方法(调用时会用 scope context
- `.addOnce(fn)` —— 只跑一次cache in `constructorsOnce`
- `.keepTime(fn)` —— 返回一个 tickablerefresh 时**保留 currentTime**不重置L201-213配合 utils/time.js:keepTime
- `.refresh()` —— revert + re-run constructorsmedia query 变化时触发)
- `.revert()` —— 倒序 revert 所有 revertiblesL226-252
React 集成:`root: useRef(ref)` 会被识别L49`ReactRef.current`)—— 在 `useEffect(() => { const scope = createScope({ root: ref }).add(...); return () => scope.revert() }, [])` 就完成了。
### mediaQueriesL85-91 / L218-223
`{ mediaQueries: { mobile: '(max-width: 800px)' } }` + `change` event → refresh → 不同 media 下重建动画。不用手写 resize listener + 重启动画。
---
## 13. SVG 三件套
### 13.1 morphTosvg/morphto.js:L25-65
变形不同数量顶点的 path。算法
1. 两个 path 的 `getTotalLength()`
2. 目标采样点数 `maxPoints = Math.ceil(max(L1, L2) * precision)`(默认 precision=0.33
3. 按 t ∈ [0,1] 等间距调 `getPointAtLength(L*t)` 得到新顶点
4. 返回 `[v1Str, v2Str]` 给 anime 做字符串 tween
5. 把 v2 缓存到 `$path[morphPointsSymbol]`consts.js:L53Symbol 属性)供下次使用
**precision 权衡**:小 = 更平滑但更贵0 = 用原始 d 属性(命中相同 command 结构时最省)。
### 13.2 motionPathsvg/motionpath.js:L80-88
返回三个 FunctionValue`{ translateX, translateY, rotate }`。每个都是**闭包**绑定 path运行期 `modifier: progress => getPathPoint + ctm 变换`
- L61-64rotate 算法是采样前后两点 `atan2(dy, dx) * 180/PI`(中心差分)
- L66-69坐标变换处理 SVG vs HTML —— 如果目标在 SVG 内就用 path 局部坐标;否则乘 CTM 投到 viewport 坐标(`p.x * ctm.a + p.y * ctm.c + ctm.e`
### 13.3 drawablesvg/drawable.js
画线效果(模拟 Vivus.js。精妙之处
- 强行设置 `pathLength=1000`L47 `K=1e3`L96-97 —— 这样 `stroke-dasharray` / `stroke-dashoffset` 不管 path 真实长度多少,都在 [0, 1000] 规范化空间里操作。
-**ES6 Proxy 拦截** `setAttribute('draw', '0 0.5')`L58-85—— 不是 polyfill 新属性,而是运行时代理:用户写 `drawable.setAttribute('draw', '0 0.5')`,实际写入的是 `stroke-dasharray` + `stroke-dashoffset`
- `vector-effect: non-scaling-stroke` 的补偿L31-35getScaleFactor从 CTM 解出缩放系数,补偿回去。
Proxy 让 anime 直接动画 `draw: [0, 1]` 像 CSS 属性一样用(`proxyTargetSymbol` 在 consts.js:L54让 anime 能认出这是 proxy
---
## 14. Text Split
**text/split.js**512 行)。把一段文本拆成 line / word / char 三层元素:
- **Intl.Segmenter**L41可用时用 Unicode 正确分词中日韩、emoji否则 fallback 到空格分割。
- `setAriaHidden` 给拆出来的副本加 `aria-hidden="true"`L81 —— **可访问性考虑**:原文保留,拆的是副本供屏幕阅读器看不到的动画视觉层。
- `filterLineElements`L109-126按 line 重排时,把不属于此行的 elements 以及相邻的空白 textNode 加入 bin避免重组后出现孤零零空白
- 模板字符串 `{value}` / `{i}` 占位L42-43允许用户自定义包装模板。
---
## 15. WAAPI 适配
**waapi/waapi.js:L85-120**540 行总量。核心亮点是 **custom easing → CSS linear() 降级**
```js
const easingToLinear = (fn, samples = 100) => {
const points = [];
for (let i = 0; i <= samples; i++) points.push(round(fn(i / samples), 4));
return `linear(${points.join(', ')})`;
}
```
anime 的自定义 easing比如 `'outElastic(1, 0.3)'``spring({mass, stiffness})`)在 WAAPI 侧没有对应语法。作者采样 100 个点,用 CSS Level 4 的 `linear(v0, v1, ..., v100)` 合成一条分段线性 easing —— **无需 JS 驱动就能 off-main-thread 跑任意曲线**。WAAPIEasesLookups 缓存结果L91避免重复采样。
这招只在支持 `linear()` 的浏览器上有效Safari 17.2+ / Chrome 113+)。
---
## 16. Draggableagent 深读1286 行)
**不继承 Timer**,而是**组合 3 个 Timer + 1 个 Animatable**draggable.js:L384-400
### 状态机
- `grabbed` (L403):指针按下
- `dragged` (L404):已移动超阈值
- `released` (L406):已松开
- `updated` (L405):本帧有更新
### 事件流(`handleEvent` 分发 L1248-1278
- `pointerdown → handleDown` L888-953绑所有监听 → 初始化 pointer 坐标 → onGrab
- `pointermove → handleMove` L958-1020`normalizePoint` 父元素 transform 反演 → touch 祖先可滚动检测L972-984→ drag threshold 门限 → 启动 updateTicker → onDrag
- `pointerup → handleUp` L1022-1151速度采样 → 弹道预测 → spring/easing 二选一驱动回位 → onRelease
### 物理
- **速度栈 3 帧循环缓冲取最大值**L363-365 + L489-493—— 不是线性平均,是取 max避免最后几帧减速导致惯性偏弱。
- **spring 模式**L274-284mass=1, stiffness=80, damping=20 默认。
- **过冲双阶段**L1090-1109非 spring 模式下若反弹超边界先动画到物理计算的过冲点65% 时间),再反弹到最终点 —— 让"撞墙回弹"自然。
### 约束
- **临时清空祖先 transform**L593 `transforms.remove()`)才能用 `getBoundingClientRect` 拿到准确边界,然后再 revert。同 scroll.js 处理 sticky 一样的思路。
- **snap**L509, L527, L702-703number/array/function 三态。
- **containerFriction**L700-701默认 0.8,应用在边界外推压计算(`cf = (1 - friction) * dragSpeed`L789
### 性能
- `updateTicker` 只在 dragged 期间 resumeL1010release 后改由 Animatable 的 onRender 驱动L426减少无用 rAF。
- ResizeObserver 节流到 150ms `resizeTicker`L453
---
## 17. Layout FLIPagent 深读1607 行)
是 anime.js 里**最大的单文件**,实现的是 **F**irst / **L**ast / **I**nvert / **P**lay 动画。
### 触发
**显式三步 API**(无 MutationObserver
```js
layout.record() // First
// 用户自己改 DOM
layout.animate(params) // Last + Invert + Play
```
或一步到位:`layout.update(cb, params)`L1595-1599
### 测量
每个节点 `properties` 六维layout.js:L318-328
- `transform` 字符串
- `x/y` 相对父坐标
- `left/top` 绝对屏幕坐标
- `width/height` 盒模型尺寸
- 用户自定义的 `opacity/color/fontSize`
**测量前临时清除 transform**L382-392 `style.transform = 'none'`)才能拿到"未变形"的 bbox。
### Invert 双引擎(最精彩处)
- **位置+尺寸** 用 anime.js Timelineanimejs 原生补间)
- **transform** 用 WAAPI **并行跑**`timeline.sync(transformAnimation, 0)` 同步L1566-1584
注释写明:`transform` 如果也走 Timeline 会和 `translate` 抢 style 属性,所以必须拆开。这是一条深水炸弹级的实现细节。
### 难点处理
- **嵌套 FLIP**`animatedParent`L1296-1300—— 子元素继承最近被动画的祖先的 timing自动"跟随"祖先而不独立 invert。
- **display:none ↔ block**`hasVisibilitySwap` + `measuredDisplay` 换脸L1219-1230
- **内联文本**`hasAdjacentText``isInlined=true` → 跳过位置动画L368-379, L1406—— 否则 inline span 包 translate 会全乱。
- **display:grid 干扰**:动画期间强行改 blockL1408
- **swapAt 中途改变**:双段 easing后半段用 `inverseEased = t => 1 - ease(1 - t)`L1530-1545在 50% 时用 `tl.call()` 切换 DOM 值L1505-1528
---
## 18. Scroll / 滚动驱动agent 深读986 行)
**混合架构**:原生 scroll 事件 + 3 层 Timer 合批 + **主动轮询**而非 IntersectionObserver。
### 三层 Timerevents/scroll.js
- `scrollTicker` L162-170rAF Timer合批 all observers
- `dataTimer` L172-18930Hz 独立定时器算速度/方向,不抢 rAF 带宽
- `wakeTicker` L201-210500ms 防抖scroll 事件**不直接**触发 scrollTicker而是 restart wakeTicker它再启动 scrollTicker —— 滚动停止后延迟休眠
scroll 事件本身只做 `wakeTicker.restart()`L284-285真实工作在 rAF 帧里批量处理。
### Scrub
进度映射纯函数L581-584
```js
const p = (this.scroll - this.offsetStart) / this.distance
return round(clamp(p, 0, 1), 6)
```
单向scroll → timeline.seek**不支持反向驱动 scroll**。
### Offset 解析
字符串模板 `'end start'` / `'top 50%'` 等 —— parseBoundValueL358-387支持百分比、单位、相对运算符`'top 50%'``offset = rect.top + viewportHeight * 0.5`
### Sticky 处理
updateBounds 里**临时禁用祖先 sticky**L774-781→ 准确 getBoundingClientRect → revertL840-841。同一思路在 Draggable 里也用(父 transform 反演)。
### 方向感知
`enterForward / enterBackward / syncEnter` 四套回调L873-921根据 container.backwardX/Y 判断方向,触发对应分支。
### 共享容器
`scrollContainers = new Map()` 全局缓存L116多个 observer 共享一个 `ScrollContainer`unsubscribe 到空才 revert 容器L965-978
---
## 19. 精彩设计合集
### 1. `/*#__PURE__*/` pragma 满天飞
IIFE 创建的 maps / singletons 都带 `/*#__PURE__*/`engine.js:L44-45, consts.js:L68-74让 Rollup / Esbuild tree-shake —— 你只用 `animate()`,整个 timeline/draggable/scroll 都能被干掉。
### 2. 双向链表全家桶
Engine children / Timeline children / Animation tweens / siblings replace / siblings add / Timer 队列全部用同一套 `_prev / _next` 链表helpers.js:L217-263。helpers.js 里 80 行实现的原语复用整个库。**每次 addChild 可选 sortMethod 做有序插入**L255-263Tween 按 `_absoluteStartTime` 排序也是用这个。
### 3. WeakMap 缓存 + Symbol 标记
- `transformsSymbol` 存 transform tween 缓存(不污染正经属性)
- `isDomSymbol / isSvgSymbol` 标记 target 类型(不每次 instanceof
- `morphPointsSymbol` 缓存上次 morph 结果
- `proxyTargetSymbol` 让代理能被识破drawable
- `lookups._rep = new WeakMap()` / `_add = new Map()` —— 主动回收
### 4. decomposeRawValue 的四类 + 运行期零正则
创建期解析一次,运行期只算术。最典型是 COMPLEX字符串 `rgb(0,0,0) 10px 10px 20px` 被切成 `s[]` + `d[]`60fps 下只做数组 lerp + 字符串模板拼接。
### 5. render.js:L268 的 transform 字符串一帧只写一次
多个 transform tween 通过 `transformsSymbol` cache + 链尾 `_renderTransforms=1` 的 marker tween 触发最终写入。对 60 个 transform 的大 stagger每帧只有 1 次 `style.transform = ...`
### 6. Engine 按需 wake
rAF 不空转tickEngine 看 `_head` 空就把 `reqId=0` 自然终止engine.js:L168-175。新 Animation resume 时 `engine.wake()` 才重启 rAF。**没有动画时零开销**。
### 7. visibility 自动 pause
`visibilitychange` listener 自动挂L159-162可关 `pauseOnDocumentHidden = false`
### 8. alternate 的 XOR 一行
```js
const isReversed = _reversed ^ (_alternate && isOdd) // render.js:L97
```
三状态组合用 XOR 解决。
### 9. Proxy 实现的 drawable
不引入新 CSS 属性,用 Proxy 拦截 `setAttribute('draw', ...)` 转译成 `stroke-dasharray` + `stroke-dashoffset`。用户写得像在用原生属性。
### 10. WAAPI 的 `easingToLinear(100 samples)`
自定义 easing 降级到 CSS `linear(v0,...,v100)`,让 GPU 线程跑 spring / elastic 成为可能。
### 11. **1000 targets 自动关 composition**animation.js:L263
```js
const tComposition = isUnd(composition) && targetsLength >= K
? compositionTypes.none : ...
```
巨量元素 stagger 时自动关闭昂贵的 sibling lookup —— 用户无感的性能兜底。
### 12. Scope.keepTime
`scope.keepTime(cb)` 在 media query refresh 时保留 timeline 的 currentTime不从 0 开始)。这个 UX 细节很少有动画库做。
### 13. **playbackEase**Timeline 级别的 warp
render.js:L100 `iterationTime = iterationDuration * _ease(iterationTime / iterationDuration)` —— 不是单个 tween 的 easing而是把 timeline 的**整个时间流**扭曲。playback 特效用。
### 14. additive 的反向累加
multi-input 同时影响 scale不会互相覆盖反而**叠加**。blend composition 把每个 tween 变成 deltaengine 每帧聚合一次additive.js
### 15. stagger(customTotal) + stagger(use)
`stagger(100, { total: 10 })``stagger(100, { use: 'data-delay' })`:前者支持"N 个元素但假装是 M 个",后者支持"从元素属性读 index"data-attribute 驱动)。
---
## 20. 可能的坑 / 可借鉴点
### 坑
- **毫秒 vs 秒切换** (timeUnit):切到 's' 后内部全部乘 0.001,造成一堆 round(_, 12) 抗浮点 —— 不要中途切换单位,应用启动时定好。
- **seek 取消的动画需要 reviveTimer**timer.js:L86-99 说明 cancel 后再 seek 要先重建 siblings 链,否则渲染空白。库内部已处理,但如果你自己 hack 可能踩。
- **Timer 构造器里直接读 engine._lastTickTime**timer.js:L167如果在动画循环外手动 `new Animation()` 然后立刻检查 currentTime 可能拿到"冷"数据 —— L166 特地 `engine.requestTick(now())` 热身。
- **Draggable 在 transformed 祖先里**:依赖 `transforms.remove()` 临时清 transform 测量draggable.js:L593有 CSS 变量或复杂 transform 时可能不完美。
### 可借鉴
- **linked-list children + addChild sortMethod** 这套 40 行原语,可以直接拷到任何需要排序链表的地方。
- **预解析 + 运行期无正则**:任何需要字符串补间的场景(比如 CSS gradient 动画、SVG path 补间)都适用这个模式。
- **按需 rAF**engine.js tickEngine 模式):`_head` 空就自然终止循环,有需求时再启动。大多数自研动画库漏掉这个。
- **visibility auto-pause**:一行事件监听,避免后台 tab 疯狂吃 CPU。
- **Proxy 做虚拟属性**drawable 的 'draw' 属性):在不能扩展原生 API 时给 API 设计者的后门。
- **WAAPI linear() 降级**:任何需要 "main-thread 动画 + off-main-thread 采样" 协同的库都能学。
- **scope 作用域 + revertibles**给框架集成React/Vue/Angular用的标准 pattern值得抄。
- **阈值自动降级**targets >= 1000 关 composition性能护栏用户不用自己做决定。
---
## 21. 模块依赖图(概要)
```
consts.js (enums + regex + Symbols) ← 所有模块
helpers.js (math/type/linked-list) ← 所有模块
globals.js (defaults/scope.current) ← timer/animation/scope
core/values.js (decompose/getTweenType) ← animation/animatable
core/render.js (render + tick) ← timer/engine
core/clock.js ← engine/timer
timer/timer.js ← animation/timeline
engine/engine.js ← timer/animatable/scroll/draggable
animation/animation.js ← timeline/animatable
animation/composition.js ← animation/timeline
animation/additive.js ← engine/composition
timeline/timeline.js ← 用户
timeline/position.js ← timeline/stagger
easings/* (parser) ← animation/timeline/waapi/stagger/draggable
utils/stagger.js ← 用户
utils/target.js (revert utilities) ← timeline/animatable
scope/scope.js ← 用户(通常 React
animatable/animatable.js ← 用户mousemove 场景)
draggable/draggable.js ← 用户
events/scroll.js ← 用户
layout/layout.js ← 用户FLIP 场景)
svg/* ← 用户svg 场景)
text/split.js ← 用户
waapi/waapi.js ← 用户off-main-thread 场景)
```
---
## 附录:文件规模
| 模块 | 行数 |
|---|---:|
| layout/layout.js | 1607 |
| draggable/draggable.js | 1286 |
| events/scroll.js | 986 |
| animation/animation.js | 747 |
| types/index.js | 652 |
| waapi/waapi.js | 540 |
| timer/timer.js | 535 |
| text/split.js | 512 |
| core/render.js | 398 |
| animation/composition.js | 390 |
| timeline/timeline.js | 362 |
| scope/scope.js | 259 |
| core/values.js | 235 |
| engine/engine.js | 181 |
| utils/chainable.js | 171 |
| core/targets.js | 138 |
| utils/stagger.js | 142 |
| core/styles.js | 118 |
| svg/drawable.js | 118 |
| core/helpers.js | 263 |
| core/consts.js | 118 |
| core/clock.js | 107 |
| core/colors.js | 103 |
| svg/motionpath.js | 88 |
| utils/number.js | 84 |
| waapi/composition.js | 84 |
| animation/additive.js | 81 |
| core/globals.js | 74 |
| timeline/position.js | 72 |
| svg/morphto.js | 65 |
| utils/random.js | 63 |
| core/units.js | 63 |
| utils/time.js | 57 |
| core/transforms.js | 45 |
| svg/helpers.js | 24 |
| 其他 | 27 |
| **total** | **11,121** |