auto-save 2026-04-23 23:19 (~2)

This commit is contained in:
2026-04-23 23:19:29 +08:00
parent 764b3951d6
commit 6acf582751
2 changed files with 671 additions and 9 deletions

View File

@@ -1,16 +1,671 @@
# anime.js 源码解析 源码解析
# anime.js 源码深度解析
> 创建日期2026-04-23
> 上游版本:待填写
> **上游**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** |

View File

@@ -27,6 +27,13 @@
"message": "auto-save 2026-04-23 23:08 (~1)",
"hash": "8e8f977",
"files_changed": 1
},
{
"ts": "2026-04-23T23:14:02+08:00",
"type": "commit",
"message": "auto-save 2026-04-23 23:13 (~1)",
"hash": "764b395",
"files_changed": 1
}
]
}