Remix UI Tween API 全解析:基于生成器的三次贝塞尔补间动画
2026/9/10 15:00:13 网站建设 项目流程

Remix UI Tween API 全解析:基于生成器的三次贝塞尔补间动画

【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix

tween是 Remix UI(@remix-run/uianimation模块中基于 ES 生成器实现的补间动画原语,用于在指定时长内以三次贝塞尔缓动曲线将数值从from插值到to,并通过requestAnimationFrame逐帧驱动。本指南以 Tween API 文档 为主线,结合 tween.ts 源码,讲解其生成器协议、贝塞尔数学内核、内置缓动预设与自定义曲线,并给出命令式动画、组件内自动清理以及多属性动画的完整实战方案。读完本文,你将能熟练用tween驱动 Canvas/WebGL、非 CSS 属性与序列化复杂动画,并在 Remix UI 组件中安全接入handle.signal生命周期。

认识tween:一次一行,得到一个动画生成器

tween的调用形态非常简洁——传入fromtodurationcurve四个选项,返回一个可逐步推进的生成器(generator):

import { tween, easings } from 'remix/ui/animation' let animation = tween({ from: 0, to: 100, duration: 1000, curve: easings.easeInOut, }) // Initialize generator animation.next() function animate(timestamp: number) { let { value, done } = animation.next(timestamp) element.style.transform = `translateX(${value}px)` if (!done) requestAnimationFrame(animate) } requestAnimationFrame(animate)

注意两个关键步骤:

  1. 先手动调用一次animation.next()(不带参数)完成生成器初始化,让 tween 有机会先yield出起始值from
  2. 之后每一帧调用animation.next(timestamp),把requestAnimationFrame回调收到的时间戳传回生成器内部,作为计算进度的依据。

从 tween.ts 的实现看,生成器内部维护startTime(第一次收到时间戳时记录)与当前value,然后进入while (true)循环:先yield value交出当前插值结果,等外部调用next(timestamp)时再计算elapsed,从而形成“帧回调 -> 生成器 -> 新值”的双向数据流。

工作原理:生成器协议与三次贝塞尔数学

文档用三条规则概括tween的行为:

  1. 每次迭代yield当前插值后的数值;
  2. 通过next(timestamp)接收当前时间戳;
  3. duration耗尽后返回done: true

其核心思想是“用三次贝塞尔曲线把线性的时间进度映射为缓动后的数值进度”,这与 CSS 的cubic-bezier()时间函数完全一致。源码将这一数学过程拆成三个函数:

  • cubicBezier(t, p1, p2)(tween.ts#L38-L42):三次贝塞尔求值公式B(t) = 3(1-t)²t·p1 + 3(1-t)t²·p2 + t³。由于 CSS 风格贝塞尔固定以(0,0)为起点、(1,1)为终点,只需传入两个中间控制点的坐标;
  • cubicBezierDerivative(t, p1, p2)(tween.ts#L51-L55):贝塞尔导数B'(t) = 3(1-t)²·p1 + 6(1-t)t·(p2-p1) + 3t²·(1-p2),为求根迭代提供斜率;
  • solveCubicBezierX(x1, x2, targetX)(tween.ts#L9-L27):已知 x 轴(时间进度)反解参数 t,使用Newton-Raphson 迭代,以t = targetX作初始猜测,通常 4~8 次迭代即可收敛(源码固定迭代 8 次,并在斜率或误差小于1e-6时提前退出),最后把结果钳制在[0, 1]区间。

主生成器 tween 的推进逻辑为:

let elapsed = timestamp - startTime let linearProgress = Math.min(elapsed / duration, 1) // x 轴 = 时间,y 轴 = 数值 let t = solveCubicBezierX(x1, x2, linearProgress) let easedProgress = cubicBezier(t, y1, y2) value = from + (to - from) * easedProgress if (linearProgress >= 1) { return to }

可以看到:linearProgress被钳制在1以内,当进度到达1时生成器直接return to,此时donetruevalue精确等于目标值,不会出现过冲(overshoot)——这正是tween与下文物理弹簧spring的本质区别。

内置缓动预设easings

easings是一个包含常用三次贝塞尔控制点的常量对象,控制点数值与 CSS 时间函数一一对应(定义见 tween.ts#L75-L81):

import { easings } from 'remix/ui/animation' easings.linear // { x1: 0, y1: 0, x2: 1, y2: 1 } easings.ease // { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 } easings.easeIn // { x1: 0.42, y1: 0, x2: 1, y2: 1 } easings.easeOut // { x1: 0, y1: 0, x2: 0.58, y2: 1 } easings.easeInOut // { x1: 0.42, y1: 0, x2: 0.58, y2: 1 }
预设控制点 (x1, y1, x2, y2)行为描述
linear(0, 0, 1, 1)无缓动,匀速
ease(0.25, 0.1, 0.25, 1)CSS 默认 ease
easeIn(0.42, 0, 1, 1)慢启动,快结束
easeOut(0, 0, 0.58, 1)快启动,慢结束
easeInOut(0.42, 0, 0.58, 1)首尾都慢

源码用as const声明,所有控制点为字面量类型,可安全地直接赋值给BezierCurve

自定义曲线:直接书写 CSS cubic-bezier 控制点

当内置预设不够用时,可以像写 CSScubic-bezier(x1, y1, x2, y2)一样定义任意曲线。注意 CSS 语法允许 y 轴控制点超出[0,1]以产生回弹效果,tween同样支持:

let customCurve = { x1: 0.68, y1: -0.55, x2: 0.265, y2: 1.55, } let animation = tween({ from: 0, to: 100, duration: 500, curve: customCurve, })

从类型定义看,BezierCurve 仅约束四个控制点字段,x1/x2通常落在0~1(时间轴),而y1/y2可以越界(数值轴),这为“带轻微回弹”的缓动效果留出了空间。

在组件中使用:用handle.signal自动清理

tween属于命令式动画,在 Remix UI 组件中运行requestAnimationFrame循环时,必须处理组件销毁后的清理。文档给出的模式是:在每一帧tick开始时检查handle.signal.aborted,一旦组件卸载立即停止,避免对已卸载 DOM 的无效写入:

function AnimatedValue(handle: Handle) { let value = 0 function animateTo(target: number) { let animation = tween({ from: value, to: target, duration: 300, curve: easings.easeOut, }) animation.next() // Initialize function tick(timestamp: number) { if (handle.signal.aborted) return let result = animation.next(timestamp) value = result.value handle.update() if (!result.done) { requestAnimationFrame(tick) } } requestAnimationFrame(tick) } return () => ( <div> <div style={{ transform: `translateX(${value}px)` }}>Moving</div> <button mix={[ on('click', () => { animateTo(200) }), ]} > Animate </button> </div> ) }

要点拆解:

  • 组件每次点击按钮都会从当前value起做 300ms 的easeOut补间,handle.update()触发一次 Remix UI 重渲染;
  • handle.signal是 Remix UI 提供给组件处理器的中止信号(AbortSignal),tick内首行检查aborted即可在卸载时立即终止动画循环;
  • 通过mixon('click', ...)绑定事件,是 Remix UI 组件原生的声明式事件写法。

多属性动画:多个 tween 并行驱动

tween只负责单个数值的插值,组合多个属性时创建多个生成器、在同一个requestAnimationFrame回调里并行推进即可:

let xAnimation = tween({ from: 0, to: 100, duration: 500, curve: easings.easeOut }) let yAnimation = tween({ from: 0, to: 50, duration: 500, curve: easings.easeOut }) let scaleAnimation = tween({ from: 1, to: 1.5, duration: 500, curve: easings.easeOut }) xAnimation.next() yAnimation.next() scaleAnimation.next() function animate(timestamp: number) { let x = xAnimation.next(timestamp) let y = yAnimation.next(timestamp) let scale = scaleAnimation.next(timestamp) element.style.transform = `translate(${x.value}px, ${y.value}px) scale(${scale.value})` if (!x.done || !y.done || !scale.done) { requestAnimationFrame(animate) } } requestAnimationFrame(animate)

由于三个 tween 共享同一duration与曲线,且都以首个时间戳为各自起点,多值动画天然同步;循环终止条件需要同时检查所有生成器的done。若希望每个属性采用不同的时长或缓动曲线,只需在创建时各自指定即可,仍可在同一回调中并行推进。同样的并行思路也出现在 animation/README.md 的示例里。

API 参考

tween(options)

创建一个随时间在数值间插值的生成器。完整选项类型(TweenOptions):

interface TweenOptions { from: number // Starting value to: number // Ending value duration: number // Duration in milliseconds curve: BezierCurve // Easing curve } interface BezierCurve { x1: number // First control point X (0-1) y1: number // First control point Y x2: number // Second control point X (0-1) y2: number // Second control point Y }

返回类型:Generator<number, number, number>——yield当前插值,动画结束时return最终值(即to),next()的返回值中done: true表示动画完成。duration单位为毫秒,首次收到时间戳后开始计时。

easings

包含预设贝塞尔曲线的对象(即上文的五个预设)。

何时使用tween,何时改用其他方案

tween适合以下场景(原文 "When to Use" 归纳):

  • requestAnimationFrame驱动的命令式动画;
  • Canvas/WebGL 动画(DOM 之外没有 CSS transition 可用);
  • 动画非 CSS 属性(如数值状态、图表数据、滚动位置等);
  • 复杂的有序动画序列(多个阶段串联时逐帧控制进度)。

而对于大多数 UI 动画,文档明确建议优先使用动画 mixin(animateEntranceanimateExitanimateLayout)或配合 Spring API 的 CSS transition——详见 animation 模块总览 与 Spring API 文档。二者定位差异清晰:tween时间驱动的贝塞尔补间,严格按时长执行、不会过冲;spring物理驱动的弹簧动画,基于duration + bounce参数生成 CSSlinear()缓动并可迭代出带过冲的进度序列。选择原则可概括为:能用 CSS transition 表达就用spring/mixin,需要逐帧掌控数值变化(尤其 Canvas、非 CSS 属性、序列动画)就用tween

延伸阅读

  • Spring API 文档:物理弹簧动画,可字符串化为 CSS transition、展开为 WAAPI 参数或迭代为 0~1 进度
  • animation 模块 README:entrance/exit/layout/spring/tween 全套原语总览
  • tween 源码实现:贝塞尔求解与生成器完整实现
  • animation 导出入口:tweeneasings及类型TweenOptionsBezierCurve的统一出口

【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询