首页
/ Svelte svelte/easing 模块详解:31 个缓动函数的实现原理与在 motion、transition、animate 中的实战用法

Svelte svelte/easing 模块详解:31 个缓动函数的实现原理与在 motion、transition、animate 中的实战用法

2026-09-06 12:32:36作者:羿妍玫Ivan

本文以 Svelte 官方参考文档 svelte/easing 模块说明为骨架,结合仓库中 缓动函数源码motion 模块实现类型声明 展开:读完你将掌握 svelte/easing 提供的全部 31 个缓动函数的分类体系、统一的 (t: number) => number 契约、每个函数的核心数学实现,以及它在 tweened/Tween、内置 transition 和 flip animate 中的真实调用链路。

模块定位:一套操纵时间值的函数库

官方参考文档对 svelte/easing 的定义是:该模块提供一组函数,允许你以不同方式操纵时间值(manipulate time values),与 motion 模块组合使用时对动画尤其有用。

缓动函数的本质是"时间的重映射":给定动画进度 t ∈ [0, 1],返回一个同样位于 [0, 1] 附近的进度值,从而改变动画的速度曲线——起点/终点减速、回弹、抖动、指数加速等都是通过对同一时间变量的不同变换实现的。

该模块的发布入口定义在 svelte 包的 package.jsonexports 字段中:

"./easing": {
  "types": "./types/index.d.ts",
  "default": "./src/easing/index.js"
}

即:运行时实现位于 packages/svelte/src/easing/index.js,TypeScript 声明位于 packages/svelte/types/index.d.ts 中的 declare module 'svelte/easing' 块。源码文件头部注明其改编自 mattdesl 的 eases 项目(MIT 协议)。

函数全集:10 个函数族 × 3 种变体 + linear

从源码逐行清点,svelte/easing 共导出 31 个具名函数,除 linear 外按"运动学族"组织为 10 个函数族,每个函数族都有 In / Out / InOut 三种变体:

函数族 运动学描述(源码注释) In Out InOut
linear 原样返回,匀速 linear
back 起点/终点有回弹(overshoot/rebound) backIn backOut backInOut
bounce 弹球式反弹 bounceIn bounceOut bounceInOut
circ 圆弧形速度曲线 circIn circOut circInOut
cubic 三次方缩放 cubicIn cubicOut cubicInOut
elastic 弹性振荡 elasticIn elasticOut elasticInOut
expo 指数级加减速 expoIn expoOut expoInOut
quad 二次方缩放 quadIn quadOut quadInOut
quart 四次方缩放 quartIn quartOut quartInOut
quint 五次方缩放 quintIn quintOut quintInOut
sine 正弦式加减速 sineIn sineOut sineInOut

所有函数签名一致,与 types/index.d.ts 中的声明对应:

export function cubicOut(t: number): number;
// 统一契约:输入 t ∈ [0, 1],输出映射后的进度值(边界满足 f(0)=0、f(1)=1)

命名约定与行为方向的对应关系(可对照源码注释逐一验证):

  • *In:开头阶段"加速"(accelerate on start),先慢后快;
  • *Out:结尾阶段"减速"(decelerate towards end),先快后慢;
  • *InOut:两头分别应用 In 与 Out 的拼接,通常是先加速后减速的平滑曲线;
  • back* / bounce* / elastic*:输出会超出 [0, 1] 区间(回弹/过冲),这是与其他族的关键区别,视觉上表现为"过头再拉回"。

源码级解析:代表性实现的数学细节

以下全部取自 packages/svelte/src/easing/index.js,用于说明各族的实现思路。

linear:恒等函数

export function linear(t) {
	return t;
}

它是多处动画 API 的默认缓动函数(见下文 tweened 的实现)。

cubic 族:最简单的幂函数缩放

export function cubicIn(t)  { return t * t * t; }
export function cubicOut(t) {
	const f = t - 1.0;
	return f * f * f + 1.0;
}
export function cubicInOut(t) {
	return t < 0.5 ? 4.0 * t * t * t : 0.5 * Math.pow(2.0 * t - 2.0, 3.0) + 1.0;
}

InOut 变体的通用手法是:前半段取 In 公式并乘以 2 倍速,后半段取 Out 公式(先平移 t 到 [0,1] 再缩放),保证曲线在中点 C1 连续。

sine 族:三角函数 + 浮点边界修正

export function sineIn(t) {
	const v = Math.cos(t * Math.PI * 0.5);
	if (Math.abs(v) < 1e-14) return 1;   // t=1 时 cos(π/2) 存在浮点误差,强制收敛到 1
	else return 1 - v;
}
export function sineOut(t)  { return Math.sin((t * Math.PI) / 2); }
export function sineInOut(t) { return -0.5 * (Math.cos(Math.PI * t) - 1); }

注意 sineIn 中对 Math.abs(v) < 1e-14 的特判:t = 1 时理论上应精确返回 1,但浮点计算会引入微小偏差,源码显式将其钳制,避免动画末帧出现可见的"跳一下"。

expo 族:端点短路

export function expoIn(t)  { return t === 0.0 ? t : Math.pow(2.0, 10.0 * (t - 1.0)); }
export function expoOut(t) { return t === 1.0 ? t : 1.0 - Math.pow(2.0, -10.0 * t); }
export function expoInOut(t) {
	return t === 0.0 || t === 1.0
		? t
		: t < 0.5
			? +0.5 * Math.pow(2.0, 20.0 * t - 10.0)
			: -0.5 * Math.pow(2.0, 10.0 - t * 20.0) + 1.0;
}

指数族在端点用严格相等短路,既保证边界精确性,也避免 Math.pow 在极小值域的数值噪声。

bounceOut:分段多项式拼出"弹跳"

export function bounceOut(t) {
	const a = 4.0 / 11.0;
	const b = 8.0 / 11.0;
	const c = 9.0 / 10.0;
	const ca = 4356.0 / 361.0;
	const cb = 35442.0 / 1805.0;
	const cc = 16061.0 / 1805.0;
	const t2 = t * t;
	return t < a
		? 7.5625 * t2
		: t < b
			? 9.075 * t2 - 9.9 * t + 3.4
			: t < c
				? ca * t2 - cb * t + cc
				: 10.8 * t * t - 20.52 * t + 10.72;
}

bounceOut 用四段抛物线在 t = 4/11、8/11、9/10 处拼接,模拟物体落地反弹的三次衰减;bounceIn 则是其镜像:1.0 - bounceOut(1.0 - t)bounceInOut 同样以 0.5 中点做对称缩放。backelastic 族同理——backIn/backOut 使用常数 s = 1.70158 控制过冲幅度,elastic*Math.sinMath.pow(2, ±10t) 的乘积构造衰减振荡。

实战一:与 svelte/motion 组合——tweened 与 Tween

文档明确指出该模块"与 motion 模块组合使用尤其实用"。在 packages/svelte/src/motion/tweened.js 中可以看到完整的消费链路。

默认值与覆盖优先级

import { linear } from '../easing/index.js';

let {
	delay = 0,
	duration = 400,
	easing = linear,          // 默认匀速
	interpolate = get_interpolator
} = { ...defaults, ...opts };  // 构造参数为默认,set() 参数可逐项覆盖

三个关键事实(均可在源码中确认):

  1. easing 的默认值是 linear,不是某个"更好看"的曲线;想要平滑减速必须显式传入;
  2. duration 支持数字或 (from, to) => number 函数(按起止值动态计算时长);duration === 0 时直接跳变,不启动动画任务;
  3. 覆盖优先级为 defaults(创建 store 时传入)< opts(每次 set 时传入)。

缓动函数在每帧 tick 中的位置

task = loop((now) => {
	// ...
	const elapsed = now - start;
	if (elapsed > duration) {
		store.set((value = new_value));
		return false;
	}
	store.set((value = fn(easing(elapsed / duration))));  // 缓动在这里生效
	return true;
});

每帧先算出线性进度 elapsed / duration,再交给 easing 重新映射,最后由插值器 fn 计算当前值写入 store——这正是文档所说"操纵时间值"的落地方式。新的 Tween 类(Svelte 5.8+)在 tweened.js 中以同样的参数结构与调用顺序实现,easing(elapsed / duration) 位于其 set() 的 raf 循环中。

完整示例:用 cubicOut 让数值平滑减速

<script>
	import { tweened } from 'svelte/motion';
	import { cubicOut } from 'svelte/easing';

	const count = tweened(0, { easing: cubicOut, duration: 600 });
</script>

<button onclick={() => count.set(Math.random() * 100)}>随机目标值</button>
<p>当前值:{$count.toFixed(2)}</p>

由于 tweened 的插值器 get_interpolator 支持数字、数组、对象(含 Date)的递归插值,缓动函数同样适用于这些复合类型的补间。

实战二:svelte/transition 内置过渡的默认缓动

easing 也是所有 transition 配置项之一(见 packages/svelte/src/transition/public.d.ts,每个 *Params 接口都有 easing?: EasingFunction)。内置过渡实现 各自选定了不同的默认缓动函数,这解释了不同内置动画"手感"的差异:

过渡函数 默认 easing
blur cubic_in_out
fade linear
fly cubic_out
slide cubic_out
scale cubic_out
draw cubic_in_out
flip(animate) cubicOut

自定义覆盖写法:

<script>
	import { fly } from 'svelte/transition';
	import { backOut } from 'svelte/easing';

	let visible = $state(true);
</script>

{#if visible}
	<p transition:fly={{ y: 200, easing: backOut }}>带回弹地飞入</p>
{/if}

实战三:svelte/animate 与自定义 animate 函数

packages/svelte/src/animate/index.js 中的 flip 默认 easing = cubicOutduration = (d) => Math.sqrt(d) * 120(时长按位移距离开方缩放)。animate: 指令允许传入自定义 animate 函数,返回的动画配置同样接受 easing 字段——仓库测试用例 animation-js-easing 演示了这一点:

<script>
	export let things;

	export function linear(t) {
		return t;
	}

	function flip(node, animation, params) {
		const dx = animation.from.left - animation.to.left;
		const dy = animation.from.top - animation.to.top;

		return {
			duration: 100,
			easing: linear,   // 自定义 animate 函数中直接指定缓动
			tick: (t, u) => {
				node.dx = u * dx;
				node.dy = u * dy;
			}
		};
	}
</script>

{#each things as thing (thing.id)}
	<div animate:flip>{thing.name}</div>
{/each}

选型与使用建议

基于上述源码事实,给出可验证的选型参考:

  • 数值补间(tweened/Tween):想要"先快后慢"的自然感,选 cubicOut/quadOut 一类 Out 变体;追求极简匀速保留默认 linear
  • 元素位移/缩放(transition:fly/slide/scale、animate:flip):仓库默认值即 cubic_out 系,一般无需改动;
  • 需要戏剧性过冲backOut(小幅度回弹)、elasticOut(多次振荡)、bounceOut(落球反弹)——注意这三者输出会越过 1,在 opacity 这类有物理上限的属性上可能被运行时钳制,用于 transform 位移最为直观;
  • 写自定义曲线:只需实现 (t) => number 并保证 f(0) ≈ 0f(1) ≈ 1,即可直接传给 easing 选项(如测试用例中的局部 linear 函数)。

小结

svelte/easing 是 Svelte 动画体系的"底层积木":31 个缓动函数以统一的 (t: number) => number 契约组织成 10 个运动学族;motion 的 tweened/Tweeneasing(elapsed / duration) 的方式在每帧消费它,transition 与 animate 内置实现 则预置了 linearcubic_outcubic_in_out 等默认值。理解"缓动 = 时间重映射"这一核心模型后,你可以在 Svelte 的任意动画入口——补间 store、过渡函数、自定义 animate 函数——按需替换速度曲线,并获得源码可追溯的行为保证。

登录后查看全文
热门项目推荐
相关项目推荐