跳到主要内容

动效Motion

动效先求利落:高频操作 90–150ms,不回弹;位移落位用弹簧,起步快、落位软;明显的回弹只留给松手和少数强调时刻。所有时长都是令牌,并且乘了全局倍率 --ok-ms。

原则

  1. 有因果从触发点出发:菜单从按钮下方落下,对话框和命令面板从点按处放大,选中块滑向新位置。
  2. 进场软,退场快按下 90ms 立即响应。对话框、抽屉、底部弹层这些悬浮卡片共用一条进退场:303ms 无过冲地落位(spring-soft),160ms 加速离开;提示进场也用弹簧落位;菜单开合频繁,只做 150ms 的淡入下移。退场 90–170ms,比进场快。
  3. 一次只讲一件事同屏只有一个主动效。浮层里的内容跟着容器一次出现,不逐条进场;确实需要错峰的成组元素,间隔不超过 45ms,整体不超过 400ms。
  4. 回弹有度常规过冲 3%(spring、snappy);松手、勾选这类强调时刻 15%(bouncy);33% 的大摆幅(wobbly)只留给品牌时刻,库里的组件目前都没有用它。
  5. 随时可打断位移和缩放只动 transform 与 opacity,颜色和描边用 150ms 的短过渡;全部可以中途打断,连点不排队。
  6. 尊重减弱动效所有时长都乘了 --ok-ms。减弱动效时它是 0:状态直接到位,循环的加载动画停下或改成淡入淡出。

时长

六档。越小、越近、越频繁的变化用越短的档。点任意一行试播;用途一栏是库里组件实际的用法。

缓动与弹簧

四条贝塞尔负责颜色、淡入淡出和退场;五条弹簧负责位移与缩放。弹簧在 CSS 里是一个 linear() 缓动加一个配套的时长令牌(-dur),成对使用;不支持 linear() 的浏览器回退成一条近似的贝塞尔。点卡片试播。

贝塞尔曲线按 520ms 试播;弹簧用自己的时长。

CSS 里写 transition: transform var(--ok-spring-bouncy-dur) var(--ok-spring-bouncy);脚本里写 animate(node, keyframes, { spring: 'bouncy' })。当前浏览器不支持 linear()。五个预设的刚度、阻尼和换算结果见 动效工具。

弹簧实验台

调刚度、阻尼和质量,实时算出时长、过冲和可以直接粘贴的 linear() 缓动。上面一排是库的五个预设;松开滑块时播放一次。

刚度 stiffness
阻尼 damping
质量 mass
时长
454ms
过冲
3.0%
阻尼比 ζ
0.74
1.00454ms
transition: transform 454ms linear(0, 0.023, 0.083, 0.166, 0.262, 0.363, 0.463, 0.558, 0.645, 0.722, 0.79, 0.847, 0.894, 0.933, 0.963, 0.986, 1.003, 1.016, 1.023, 1.028, 1.03, 1.03, 1.029, 1.027, 1.024, 1.021, 1.018, 1.015, 1.012, 1.01, 1.007, 1.006, 1);

六种基本动作

组件里的动效基本都是这六种动作的组合。每个都可以点。

① 按压回弹

按下 90ms 缩到 .955;松开用 bouncy 弹簧弹回。按下时的回缩对带文字的控件也适用。

② 进场软,退场快

进场用弹簧从 .8 放大、淡入;退场 150ms 加速离开。usePresence 等退场播完再卸载节点。

③ 共享指示器

高亮块只有一个,在选项之间滑动,而不是各自淡入淡出。分段、标签页、导航、菜单的高亮都是这样。

④ 错峰

成组元素依次出现,间隔 26ms,顺序就是阅读顺序。用 animate() 的 delay。

123456

⑤ 布局位移

FLIP:元素从旧位置弹到新位置,看得见“它去哪了”。用 playFlip()。

⑥ 描线

对勾和加载尾巴都是画出来的:圆头描边沿路径生长。

加载器和按钮加载状态里的那截尾巴(.ok-tail)可以整体换成别的图案:OinkProvider 的 tail 属性是全局替换,TailProvider 只换一块,见 主题 · 替换尾巴图案。

悬浮卡片:一条进场,一条退场

Dialog、confirm()、Drawer、Sheet、Panel、命令面板和 AppShell 的抽屉都是 FloatingCard。不管停在哪一侧、从触发点放大还是贴边滑入、宽屏还是手机,进退场都读同一组令牌,节奏完全一致。

令牌值用途
--ok-float-in-dur303ms进场位移(放大或滑入)
--ok-float-in-easevar(--ok-spring-soft)进场位移的曲线(不过冲);拖拽松手后的回弹也用它
--ok-float-fade-dur100ms进场淡入(配 ease-out)
--ok-float-out-dur160ms退场的位移、透明度和遮罩;遮罩淡入也用这一档
--ok-float-out-easevar(--ok-ease-in)退场的曲线

进场位移用 --ok-spring-soft 的曲线:没有过冲,贴边的抽屉、面板不会冲出边距再弹回来;时长 303ms(原来 454ms 的 2/3),起步更快。退场位移、透明度和遮罩一起在 160ms 内加速离开。手机宽度下只是滑入的距离缩短到三分之一,曲线和时长不变。脚本里同样的数值在 floatMotion({ in: 303, fade: 100, out: 160 }),FloatingCard 用它算卸载的兜底时长。

从触发点进场

居中的对话框、confirm() 和命令面板默认从打开它的地方放大出来(scale .6 → 1,spring-soft 曲线 303ms),关闭时缩回同一个点:用指针点开的从点按处,用键盘打开的从焦点所在元素的中心,用快捷键打开的从屏幕中心。

传 origin="center" 或一个视口坐标可以改掉起点,见 Dialog。菜单、下拉这类锚定在按钮上的浮层不放大,只从按钮那一侧落下 6px 并淡入。

悬停与按压

悬停只换颜色:底色、线色变深一档,不改字号、字重,带文字的元素也不位移。纯图形(色板、评分星星、滑块圆钮)可以轻微放大;按下时的回缩不受限制。

悬停样式一律包在 @media (hover: hover) 里,触屏点按之后不会残留悬停态。

减弱动效

系统开启“减弱动态效果”时,--ok-ms 变成 0:所有用时长令牌的过渡和 animate() 直接到位,循环的加载动画停下,打点加载改成淡入淡出。打开下面的开关就能看到效果(它改的是整站的设置,看完记得关掉)。

--ok-ms = 1motion = auto
加载中加载尾巴

组件和自己写的样式都不需要单独判断 prefers-reduced-motion:只要时长用令牌、脚本动画走 animate(),就会自动跟上。偏好有三种:auto 跟随系统、full 保留完整动效、reduced 强制减弱,由 OinkProvider 的 defaultMotion 和 useOink().setMotion 设置,见 主题 · 动效偏好。

动效对照表

拿不准用哪条曲线时查这张表。数值取自库里组件的实际样式。

场景变化属性时长曲线
按下scale → .955dur-1 90msease-out
松开回弹scale → 1542msspring-bouncy
悬停、变色background · color · 描边色dur-2 150msease-out
开关、分段、滑块落位translate454msspring
菜单、下拉进场opacity + translateY 6pxdur-2 150msease-out
菜单、下拉退场opacity + translateY 6pxdur-1 90msease-in
菜单高亮跟随translatedur-1 90msease-out
文字提示进场 / 热切换opacity + translate · scale454ms / 325msspring / spring-snappy
悬浮卡片进场(对话框、命令面板、抽屉、底部弹层、面板)从触发点 scale .6 → 1,或贴边滑入 · 淡入 100msfloat-in-dur 303msfloat-in-ease(= spring-soft)
悬浮卡片退场transform · opacity · 遮罩一起float-out-dur 160msfloat-out-ease(= ease-in)
高度展开 / 折叠grid-template-rows587ms(折叠面板 391ms)spring-soft(无过冲)
布局位移FLIP translate454msspring
描线(对勾)stroke-dashoffsetdur-3 / dur-4ease-out
校验失败translateX 抖动dur-5 520msease-out
深浅色切换从点击处圆形揭开520mscubic-bezier(.4, 0, .2, 1)
点赞scale · rotate 关键帧dur-6 800msease-out

在代码里用

先用 CSS 过渡;需要量位置、或由事件一次性触发的动画再用脚本工具。

tsx
/* 能用 CSS 过渡就用 CSS:时长和弹簧都是令牌,成对使用 */
.thing { transition: transform var(--ok-spring-dur) var(--ok-spring), opacity var(--ok-dur-2) var(--ok-ease-out); }

// 要量位置、或者由事件一次性触发的动画,才用脚本
import { animate, playFlip, usePresence } from 'oink-ui';
animate(node, [{ transform: 'scale(.6)' }, { transform: 'none' }], { spring: 'bouncy' });

animate()、spring()、presets、playFlip() / flip()、usePresence、wait() 的用法和属性见 动效工具。