36 KiB
anime.js 源码深度解析
上游:https://github.com/juliangarnier/anime.git 版本:v4.3.6(2026-04-23 clone) 作者:Julian Garnier License:MIT 规模:11,121 行(不含 dist/test),17 个模块 定位:轻量级多用途 JavaScript 动画引擎(CSS / SVG / DOM attrs / JS objects),v4 重写了整个内核
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,非浏览器 fallbacksetImmediate,以支持 Node.js 测试。 - Tick 入口(L168-175):闭包函数
tickEngine判断engine._head(链表非空)才续命 rAF;空了就把reqId = 0让循环自然终止。这是按需暂停策略 —— 没有正在跑的动画就不消耗帧。 - 每帧逻辑(L62-91
update):this.requestTick(time)走 Clock 的 fps 限速(L81-94)。- 遍历双向链表
_head → _next,每个 active tickable 调tick(activeTickable, localTime, ...)。 localTime = (globalTime - _startTime) * _speed * engineSpeed(L74) —— 每个 tickable 有独立 speed。- paused 的 child 直接
removeChild(this, activeTickable)退出循环(L80-85)。 - 最后
additive.update()跑一次叠加动画的 recompose(L89)。
- 可见性联动(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。算法可以跳多帧(L92frameDelta < 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:
!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—— 数字+单位,如100px30%2turn(unitsExecRgx 解析,consts.js:L114)COLOR—— hex/rgb/hsl(colors.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 playbackEase(Timeline 级别的总体 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 - UNIT:NUMBER 的基础上加
${n}${_unit} - COLOR:对 RGBA 四路分别 lerp 再 clamp(0,255)
- COMPLEX:遍历
_toNumberslerp + 交错拼接_strings
- NUMBER:
- Transform 批写(L242-275):
- 每个 transform tween 写入
target[transformsSymbol][property](缓存属性) - Animation 构造时标记链中最后一个 transform tween 的
_renderTransforms = 1(animation.js:L601-616) - 只有
_renderTransforms=1的 tween 被处理时才拼完整transform:字符串(L268-274)—— 一帧一写,而不是每个 tween 写一次
- 每个 transform tween 写入
- DOM 写入分派(L236-254):OBJECT 走属性赋值;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 时强制 onComplete,backward 时补 onComplete)
- L376
childTime = (tlChildrenTime - child._offset) * child._speed—— child 有自己的 offset 和 speed - L386-395 所有 children
completed才触发 TimelineonComplete
6. Timer 基类:生命周期
timer.js:L106-530。
- scope 注册(L137):构造时自动注册到
scope.current(如有),这让 Scope.revert() 能级联清理所有 Timer。 - offset 计算(L162-170):Timeline 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)返回 Promise,resolve 时机是 onComplete。L516-518 的this.then = nullhack 防async/await返回这个 thenable 导致的无限递归(引用 GitHub issue #26)。
7. JSAnimation 构造器:747 行大工程
animation.js:L210-740,整个构造函数就是 442 行的流水线:
7.1 输入归一
registerTargets(targets)把'.btn'/ Element / NodeList / ReactRef 等归一成数组(targets.js)。- 如果有
keyframes,generateKeyframes(L127-208)把数组形式或百分比对象形式的 keyframes 展开成 per-property 的数组。 getTweenType分类每个属性。- 每个 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)
const tComposition = isUnd(composition) && targetsLength >= K
? compositionTypes.none : ...
当 targets >= 1000,默认关掉 composition(不做 overwrite 处理)。这是"stagger 3000 个点"这种场景的救命配置。
7.6 修正 iterationDelay(L624-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)
叠加模式下:
- 给 target-prop 建一个额外的
_addWeakMap siblings 链。 - 第一个 tween 时创建 lookupTween(相当于"累加器")。
- 每个新 tween 的
_fromNumber = lookup._fromNumber - toNumber,_toNumber = 0—— 本 tween 补间的其实是相对于上次状态的 delta。 - 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)—— 多态 adddefaults: 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 都:
- 新建
staggeredChildParams(避免 mutate 原 params) - 重置
this.duration = tlDuration; this.iterationDuration = tlIterationDuration(关键,否则每轮 stagger 起点漂移) - 用 stagger 函数计算这个 target 的位置,调
addTlChild - 每个 target 都创建一个独立 JSAnimation
这样 tl.add('.item', { opacity: [0,1] }, stagger(100, { from: 'center' })) 就为 N 个元素创建 N 个 Animation,每个有独立的 start time。
sync(L256-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 控制这类"逐帧被外部驱动"的动画。
用法:
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)
- 无参时 return 当前值(
- 有个隐藏的
callbacksJSAnimation({v: 0, v: 1}dummy,L90)用来统一触发 begin/pause/complete —— 因为真实动画有 N 个独立 Animation,哪一个跑完不代表全部完成。pauseHandlerL52-65 扫所有 children paused 状态,全 paused 才 complete。
这个 API 配合 blend composition,就能实现"多输入源推同一个对象,平滑叠加"。
12. Scope:React/Angular 生命周期桥
scope/scope.js:L41-253。核心概念是 scope.execute(cb) 压栈式上下文:
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 inconstructorsOnce).keepTime(fn)—— 返回一个 tickable,refresh 时保留 currentTime不重置(L201-213,配合 utils/time.js:keepTime).refresh()—— revert + re-run constructors(media query 变化时触发).revert()—— 倒序 revert 所有 revertibles(L226-252)
React 集成:root: useRef(ref) 会被识别(L49,ReactRef.current)—— 在 useEffect(() => { const scope = createScope({ root: ref }).add(...); return () => scope.revert() }, []) 就完成了。
mediaQueries(L85-91 / L218-223)
{ mediaQueries: { mobile: '(max-width: 800px)' } } + change event → refresh → 不同 media 下重建动画。不用手写 resize listener + 重启动画。
13. SVG 三件套
13.1 morphTo(svg/morphto.js:L25-65)
变形不同数量顶点的 path。算法:
- 两个 path 的
getTotalLength() - 目标采样点数
maxPoints = Math.ceil(max(L1, L2) * precision)(默认 precision=0.33) - 按 t ∈ [0,1] 等间距调
getPointAtLength(L*t)得到新顶点 - 返回
[v1Str, v2Str]给 anime 做字符串 tween - 把 v2 缓存到
$path[morphPointsSymbol](consts.js:L53,Symbol 属性)供下次使用
precision 权衡:小 = 更平滑但更贵;0 = 用原始 d 属性(命中相同 command 结构时最省)。
13.2 motionPath(svg/motionpath.js:L80-88)
返回三个 FunctionValue:{ translateX, translateY, rotate }。每个都是闭包绑定 path,运行期 modifier: progress => getPathPoint + ctm 变换。
- L61-64:rotate 算法是采样前后两点
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 drawable(svg/drawable.js)
画线效果(模拟 Vivus.js)。精妙之处:
- 强行设置
pathLength=1000(L47K=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-35,getScaleFactor):从 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() 降级:
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. Draggable(agent 深读,1286 行)
不继承 Timer,而是组合 3 个 Timer + 1 个 Animatable(draggable.js:L384-400)。
状态机
grabbed(L403):指针按下dragged(L404):已移动超阈值released(L406):已松开updated(L405):本帧有更新
事件流(handleEvent 分发 L1248-1278)
pointerdown → handleDownL888-953:绑所有监听 → 初始化 pointer 坐标 → onGrabpointermove → handleMoveL958-1020:normalizePoint父元素 transform 反演 → touch 祖先可滚动检测(L972-984)→ drag threshold 门限 → 启动 updateTicker → onDragpointerup → handleUpL1022-1151:速度采样 → 弹道预测 → spring/easing 二选一驱动回位 → onRelease
物理
- 速度栈 3 帧循环缓冲取最大值(L363-365 + L489-493)—— 不是线性平均,是取 max,避免最后几帧减速导致惯性偏弱。
- spring 模式(L274-284):mass=1, stiffness=80, damping=20 默认。
- 过冲双阶段(L1090-1109):非 spring 模式下若反弹超边界,先动画到物理计算的过冲点(65% 时间),再反弹到最终点 —— 让"撞墙回弹"自然。
约束
- 临时清空祖先 transform(L593
transforms.remove())才能用getBoundingClientRect拿到准确边界,然后再 revert。同 scroll.js 处理 sticky 一样的思路。 - snap(L509, L527, L702-703):number/array/function 三态。
- containerFriction(L700-701):默认 0.8,应用在边界外推压计算(
cf = (1 - friction) * dragSpeed,L789)。
性能
updateTicker只在 dragged 期间 resume(L1010),release 后改由 Animatable 的 onRender 驱动(L426),减少无用 rAF。- ResizeObserver 节流到 150ms
resizeTicker(L453)。
17. Layout FLIP(agent 深读,1607 行)
是 anime.js 里最大的单文件,实现的是 First / Last / Invert / Play 动画。
触发
显式三步 API(无 MutationObserver):
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 Timeline(animejs 原生补间)
- 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 干扰:动画期间强行改 block(L1408)。
- swapAt 中途改变:双段 easing,后半段用
inverseEased = t => 1 - ease(1 - t)(L1530-1545),在 50% 时用tl.call()切换 DOM 值(L1505-1528)。
18. Scroll / 滚动驱动(agent 深读,986 行)
混合架构:原生 scroll 事件 + 3 层 Timer 合批 + 主动轮询而非 IntersectionObserver。
三层 Timer(events/scroll.js)
scrollTickerL162-170:rAF Timer,合批 all observersdataTimerL172-189:30Hz 独立定时器算速度/方向,不抢 rAF 带宽wakeTickerL201-210:500ms 防抖,scroll 事件不直接触发 scrollTicker,而是 restart wakeTicker,它再启动 scrollTicker —— 滚动停止后延迟休眠
scroll 事件本身只做 wakeTicker.restart()(L284-285),真实工作在 rAF 帧里批量处理。
Scrub
进度映射纯函数(L581-584):
const p = (this.scroll - this.offsetStart) / this.distance
return round(clamp(p, 0, 1), 6)
单向:scroll → timeline.seek,不支持反向驱动 scroll。
Offset 解析
字符串模板 'end start' / 'top 50%' 等 —— parseBoundValue(L358-387)支持百分比、单位、相对运算符;'top 50%' → offset = rect.top + viewportHeight * 0.5。
Sticky 处理
updateBounds 里临时禁用祖先 sticky(L774-781)→ 准确 getBoundingClientRect → revert(L840-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-263),Tween 按 _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 一行
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)
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 变成 delta,engine 每帧聚合一次(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 |