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

36 KiB
Raw Blame History

anime.js 源码深度解析

上游https://github.com/juliangarnier/anime.git 版本v4.3.62026-04-23 clone 作者Julian Garnier LicenseMIT 规模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,导出一个模块级单例 engineL155 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 * engineSpeedL74 —— 每个 tickable 有独立 speed。
    4. paused 的 child 直接 removeChild(this, activeTickable) 退出循环L80-85
    5. 最后 additive.update() 跑一次叠加动画的 recomposeL89
  • 可见性联动L159-162doc.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 / _scheduledTimeL28-38。区分这么多时间戳是为了在 fps 限速、speed 变更、seek 跳转时都能正确推进。
  • requestTick(time) L81-94fps 限速的核心。如果 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 对象)/ ATTRIBUTESVG/非 CSS 的 DOM 属性)/ CSS(样式属性)/ TRANSFORMtransform 六件套)/ CSS_VARCSS 变量)。

决策在 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 —— 数字+单位,如 100px 30% 2turnunitsExecRgx 解析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 / _nextAddadditive 链表(用于 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 一行处理 reversedalternateisOdd 三状态组合。
  • L99 iterationTime = isReversed ? iterationDuration - iterationElapsedTime : iterationElapsedTime
  • L100 playbackEaseTimeline 级别的总体 easing作用于整个时间轴iterationTime = iterationDuration * _ease(iterationTime / iterationDuration)
  • L101 反向方向感知:parent.backwardstime - prevTime 两条路径推断方向

5.2 Tween 循环L156-278

  • 跳帧优化L166-174只有当前时间还没走完 + 当前 tween 没被 overridden + 前/后 siblings 没覆盖 + forcedRender 时才写入。
  • 值类型分支L193-224
    • NUMBERlerp + 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 = 1animation.js:L601-616
    • 只有 _renderTransforms=1 的 tween 被处理时才拼完整 transform: 字符串L268-274—— 一帧一写,而不是每个 tween 写一次
  • DOM 写入分派L236-254OBJECT 走属性赋值ATTRIBUTE 走 setAttributeCSS 走 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/alternateL371-450语义完备。play() 必要时 alternate 一次再 resumealternate() 通过 XOR 切 _reversed 然后 seek 到对称时刻。
  • seekL410-420关键是 reviveTimer(this) 先把取消的 tween 重新 compose 回 siblings 链L86-99。否则 seek 一个已 cancel 的动画会渲染到空。
  • stretchL470-482等比缩放 duration / offset / delay —— 用于 timeline 动态"压缩/拉伸"已添加的 children。
  • Promise 集成L512-528timer.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. 如果有 keyframesgenerateKeyframesL127-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] 占位
  • 长度不等的 complexL506-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 修正 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.AnimationWAAPI和自己的 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 numberL94-97
  • 栅格模式grid: [cols, rows] + axis: 'x' | 'y' —— 欧几里得距离或单轴距离L117-126
  • range staggerstagger([0, 100]) 值域等分而非固定间距L100-103
  • easing:把索引归一化 [0,1] 后过 easing 再映射回去L130
  • 单位继承:自动从 val 提取单位 '2s''2s'L139
  • custom useparams.use: 'data-delay' 从元素属性读 index 值L108
  • 从属 timeline:传入 timeline 时会把 stagger 的 base offset 结算为 timeline 位置语法L135parseTimelinePosition —— 支持 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 JSAnimationL107
  • 每个 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) 压栈式上下文

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) 会被识别L49ReactRef.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=1000L47 K=1e3L96-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.js512 行)。把一段文本拆成 line / word / char 三层元素:

  • Intl.SegmenterL41可用时用 Unicode 正确分词中日韩、emoji否则 fallback 到空格分割。
  • setAriaHidden 给拆出来的副本加 aria-hidden="true"L81 —— 可访问性考虑:原文保留,拆的是副本供屏幕阅读器看不到的动画视觉层。
  • filterLineElementsL109-126按 line 重排时,把不属于此行的 elements 以及相邻的空白 textNode 加入 bin避免重组后出现孤零零空白
  • 模板字符串 {value} / {i} 占位L42-43允许用户自定义包装模板。

15. WAAPI 适配

waapi/waapi.js:L85-120540 行总量。核心亮点是 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. Draggableagent 深读1286 行)

不继承 Timer,而是组合 3 个 Timer + 1 个 Animatabledraggable.js:L384-400

状态机

  • grabbed (L403):指针按下
  • dragged (L404):已移动超阈值
  • released (L406):已松开
  • updated (L405):本帧有更新

事件流(handleEvent 分发 L1248-1278

  • pointerdown → handleDown L888-953绑所有监听 → 初始化 pointer 坐标 → onGrab
  • pointermove → handleMove L958-1020normalizePoint 父元素 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% 时间),再反弹到最终点 —— 让"撞墙回弹"自然。

约束

  • 临时清空祖先 transformL593 transforms.remove())才能用 getBoundingClientRect 拿到准确边界,然后再 revert。同 scroll.js 处理 sticky 一样的思路。
  • snapL509, L527, L702-703number/array/function 三态。
  • containerFrictionL700-701默认 0.8,应用在边界外推压计算(cf = (1 - friction) * dragSpeedL789

性能

  • updateTicker 只在 dragged 期间 resumeL1010release 后改由 Animatable 的 onRender 驱动L426减少无用 rAF。
  • ResizeObserver 节流到 150ms resizeTickerL453

17. Layout FLIPagent 深读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

测量前临时清除 transformL382-392 style.transform = 'none')才能拿到"未变形"的 bbox。

Invert 双引擎(最精彩处)

  • 位置+尺寸 用 anime.js Timelineanimejs 原生补间)
  • transform 用 WAAPI 并行跑timeline.sync(transformAnimation, 0) 同步L1566-1584

注释写明:transform 如果也走 Timeline 会和 translate 抢 style 属性,所以必须拆开。这是一条深水炸弹级的实现细节。

难点处理

  • 嵌套 FLIPanimatedParentL1296-1300—— 子元素继承最近被动画的祖先的 timing自动"跟随"祖先而不独立 invert。
  • display:none ↔ blockhasVisibilitySwap + measuredDisplay 换脸L1219-1230
  • 内联文本hasAdjacentTextisInlined=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

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 里临时禁用祖先 stickyL774-781→ 准确 getBoundingClientRect → revertL840-841。同一思路在 Draggable 里也用(父 transform 反演)。

方向感知

enterForward / enterBackward / syncEnter 四套回调L873-921根据 container.backwardX/Y 判断方向,触发对应分支。

共享容器

scrollContainers = new Map() 全局缓存L116多个 observer 共享一个 ScrollContainerunsubscribe 到空才 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 一行

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 自动关 compositionanimation.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. playbackEaseTimeline 级别的 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 取消的动画需要 reviveTimertimer.js:L86-99 说明 cancel 后再 seek 要先重建 siblings 链,否则渲染空白。库内部已处理,但如果你自己 hack 可能踩。
  • Timer 构造器里直接读 engine._lastTickTimetimer.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 补间)都适用这个模式。
  • 按需 rAFengine.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