声明式状态与 variants
命令式动画是一句句下指令:「现在把 scale 从 1 弹到 1.1」。它精确,但交互一复杂,「谁在什么时机播什么」就散落在各处事件回调里。
声明式换一个思路:预先声明一组命名状态(variants),每个状态只是一张目标属性表。运行时只做一件事——切换当前状态;每个属性各自用一根弹簧,从它此刻的值过渡到新状态的目标。
1 · 四个状态之间的流动
状态切换时,引擎并不为每个属性从头播一条新曲线,而是读出该属性此刻的值与速度,以它为起点新建一根指向新目标的弹簧。所以连续切换 hover 到 press 到 active,上一段过渡哪怕没走完,新过渡也会在当前值无缝接管。
这与中断与重定向是同一机制:variants 只是把它从命令式的「打断旧动画」包装成声明式的「切换状态」。
2 · 每个属性一条独立通道
x、scale、rotate 各自持有一根弹簧与一个计时原点,互不干扰。这一点很重要:状态切换时三个属性的「剩余距离」不同,若共用一根弹簧、一个进度,短距离的属性会被长距离的拖慢。
注 · 背景色没有走弹簧,而是一条 280 ms 的线性混合。弹簧的输出会过冲,而颜色过冲意味着分量越界——#f76707 到 #37b24d 的过程中红色分量若冲过头会被裁掉,回落时又跳回来,看上去像闪了一下。位移与缩放过冲是弹性,颜色过冲是故障,所以这一路单独处理。
import { createSpring } from '@vega/anim';
const variants = {
rest: { x: 0, scale: 1.0, rotate: 0 },
hover: { x: 0, scale: 1.12, rotate: -4 },
press: { x: 0, scale: 0.92, rotate: 0 },
active: { x: 38, scale: 1.05, rotate: 8 },
};
// 每个属性一根弹簧, 各自记住自己的值与速度
function setState(name) {
const v = variants[name];
for (const prop of ['x', 'scale', 'rotate']) {
const ch = channels[prop];
ch.spring = createSpring({
stiffness, damping,
from: ch.value(now()), // 从此刻的值出发
to: v[prop], // 指向新状态的目标
velocity: ch.velocity(now()), // 接管当前速度, 无缝重定向
});
ch.t0 = now();
}
}
弹簧能接住任意初速度,这是 variants 顺滑切换的物理前提,见弹簧。同一套接管机制的命令式版本见中断与重定向,收成一等 API 的版本见飞行中重定向。