动效设计规范(local/anim-delight)¶
分支定位:自用分支,主题只有一个——动效。目标是把「丝滑、有回应、不喧哗」的观感铺到各个角落,
方向对齐 Apple 一类系统级动效的手感。本文件是该分支的写法约定,由 tools/motion-test.mjs
(77 条断言)在提交前把关。
1. 为什么需要一份规范¶
动效最容易失控的地方不是"做不出来",而是做出来之后无法长期保持:
- 时长、曲线各处硬编码,同一类交互在不同页面手感不一致;
- 入场动画用
animation-fill-mode: both,动画结束帧把transform锁在终值, 压过:hover/:active,按压回弹与悬浮抬起整体失效(本轮实测踩到过); - 为了让动画"看得见"而不断加长时长、加大位移,页面从顺滑变成拖沓;
- 忘记
prefers-reduced-motion,对晕动症用户不友好。
因此约定写成工具可校验的形式,而不是靠记忆。
2. 五条硬纪律¶
| 编号 | 纪律 | 校验方式 |
|---|---|---|
| M1 | 只动 transform / opacity |
解析全部 @keyframes 体,出现 width/height/top/left/margin/padding/font-size 即失败 |
| M2 | 时长与曲线走令牌,四档分级 | --dur-1..4 存在、递增、均 ≤ 600ms;CSS 内无超过 700ms 的动画 |
| M3 | 入场动画用 backwards,不用 both |
both 仅允许出现在退出动画(提示条淡出、描边勾) |
| M4 | 尊重系统「减弱动态效果」 | CSS 降级块 + JS 侧 prefersReducedMotion() 双份 |
| M5 | 重效果分级:模糊只在桌面,涟漪只在真机 | backdrop-filter 被 hover:hover and pointer:fine 包住;涟漪判 is-phone |
| M6 | 不覆盖既有动画选择器 | 扫描 global.css 与 motion.css 中"定义了 animation 的选择器",交集必须落在允许清单内,且每条允许项写明理由 |
补充:无限循环动画仅限白名单三项——骨架流光 shimmer、状态点呼吸 m-breathe、
进度斜纹 m-stripes。其余动画一律只播一次。
3. 令牌¶
apps/desktop/src/styles/motion.css 顶部定义,命名固定。MD3 原语(§3.6)是唯一真源,
下面那组 --dur-* / --ease-* 是兼容层(组件继续用旧名,全应用一次性接上新曲线):
--md-sys-motion-easing-emphasized (0.2, 0, 0, 1) 大容器 / 页面转场
--md-sys-motion-easing-emphasized-decelerate (0.05, 0.7, 0.1, 1) 进场(起步快、尾巴长)
--md-sys-motion-easing-emphasized-accelerate (0.3, 0, 0.8, 0.15) 退场(起步慢、收得快)
--md-sys-motion-easing-standard (0.2, 0, 0, 1) 常规状态切换
--md-sys-motion-easing-standard-decelerate (0, 0, 0, 1)
--md-sys-motion-easing-standard-accelerate (0.3, 0, 1, 1)
--md-sys-motion-duration-short-2/4 100 / 200ms
--md-sys-motion-duration-medium-2/4 300 / 400ms
--md-sys-motion-duration-long-2 500ms
兼容层(组件里的写法不变):
--dur-1: 140ms 微反馈(按压、悬浮)——保留原值,MD3 的 100/200 档在这太钝
--dur-2: short-4 (200ms) 常规(淡入、状态切换);原 220ms
--dur-3: medium-2 (300ms) 容器(弹层、页面);原 340ms
--dur-4: long-2 (500ms) 大场景(首屏揭示);原 520ms
--ease-out = emphasized-decelerate 进场曲线
--ease-in-out = standard 对称过渡
--ease-spring 轻弹簧(MD3 没有,本项目的品牌抖动,只给"被点/被弹出来"的元素)
--ease-ios = emphasized 页面与大容器转场
--rise-1/2/3 位移基准 4 / 8 / 14px
--stagger 列表逐项间隔 26ms
进出场用不同曲线是硬纪律:进场一律 decelerate,退场一律 accelerate(.is-closing 相位、
sheet 下滑、toast 淡出)。若两者共用同一条曲线,退场会读作进场的倒放,方向感消失。
错峰出场(stagger):给容器加 stagger 类,其直接子元素按 26ms 递增上浮
(前 14 项零 JS 走 nth-child,更长的列表由 JS 给 --i)。只在一层用——
父子同时进场会糊成一团。"待办"页 (tasks-learn) 就是这一层:筛选行 → 卡片流 → 统计/入口;
卡片流内部的卡片不加动画,那层是拖拽 transform 的地盘,动画会和手势抢属性。
4. 页面转场:只走 CSS 进场¶
App.tsx 的 .page-anim 以 key={page} 重新挂载,播一次淡入上浮;旧页直接卸载。
方向由 useNavDirection 比较"上一页与当前页"得出:进二级页为前进,回列表为后退,
分别对应不同曲线;二级页额外走横向滑入(data-level="sub")。
4.1 为什么不用 View Transitions 快照转场¶
2026-09-22 实测(用户报障):整页 tab 切换走快照交叉淡入时,旧页快照会在新页下层 以半透明残留——两张整页截图叠加,观感是"旧页面在下面闪一下",每次切换都闪。 快照转场适合"列表卡 → 详情页"这类有共享元素的场景,不适合整页替换。
因此该路径连同 withViewTransition 一并移除,并加回归护栏:
tools/motion-test.mjs 断言源码中不再出现 startViewTransition、::view-transition
与 withViewTransition。后续若要做共享元素转场,按"列表卡 ↔ 详情页"单独设计,
不复用整页替换这条路。
5. 覆盖到的位置¶
| 位置 | 动效 |
|---|---|
| 按钮 / 图标钮 / 芯片 / 可点行 | 统一按压回弹,时长 140ms 弹簧曲线 |
| 桌面 hover | 只有"点进去会跳转"的入口大卡上浮;数字卡片、列表行不位移,只改边框/底色 |
| 列表 / 统计卡 / 网格 | 逐项进场,间隔 26ms,前 14 项由 nth-child 提供延迟 |
| 侧栏当前项 | 左侧强调条「长出来」+ 图标轻微放大 |
| 抽屉导航 | 面板沿用 global.css 的 drawer-in/out(不覆盖),仅导航项逐项进场 |
| 页签(信息 / 生活 / 预约 / 收藏夹) | 容器保持挂载,只切类名重放一次进场(页签状态不丢) |
| 弹层(确认框 / 表单 / 插件 Sheet / 订阅 / 排课 / 导览 / 论坛) | 遮罩淡入 + 面板弹簧 |
| 文件预览 | 面板同款进场 + PDF/pptx 逐页淡入 |
| 提示条 | 进入弹簧下落,退出先播 200ms 淡出再卸载(状态机 closing 相位) |
| 统计数字 | 数值型从旧值滚到新值;–、¥12.34 一类字符串原样显示 |
| 主题切换 | <html> 短暂挂 .theme-anim,颜色 320ms 过渡,420ms 后摘除 |
| 移动端顶栏 | 滚过 8px 浮起(阴影渐显) |
| 折叠箭头 / 开关旋钮 | 旋转与缩放走弹簧曲线 |
| 触摸 | 涟漪,仅在 is-phone 密度层,全局单监听 |
6. 覆盖既有动画的两个真实事故(M6 的由来)¶
2026-09-22 用户实测报障,两起都是"动效层覆盖了既有动画"引起的:
抽屉消失:.drawer 的基态是 transform: translateX(-100%)(入场前的藏身态),
可见位置完全靠 global.css 的 drawer-in … forwards 钉住。动效层把该规则覆盖成
backwards 后,动画一结束基态立即生效——展开动画播完抽屉当场消失;关闭时
drawer-closing 又被同特异度的后位规则压过,于是出现"重现一次再播退场"。
提示条偏心:.toast-host 基态是 transform: translateX(-50%)(水平居中)。
新动画的位移里没有带上 -50%,入场期间提示条会先跳到偏右再弹回中间。
同时修掉一处选择器错配:退出动画原本写成 .toast.is-closing,而组件渲染的是
.toast-host.is-closing,退出相位从未生效。
结论:动效层只新增,不改既有动画;确需覆盖时进允许清单并写明理由
(当前仅 .content 与 .toast-host),由 M6 在提交前拦截。
7. 验证¶
node tools/motion-test.mjs # 77 条断言:纪律 + 接线 + 无障碍 + 不越权覆盖
node tools/theme-plugin-sync-test.mjs # 主题切换动效不得影响主题本身
pnpm --filter @onethu/desktop build # CSS 语法与打包
pnpm exec tsc --noEmit # 类型
桌面端资源为编译期内嵌,只改前端时 cargo 不会自动重编,需要先 touch src/lib.rs src/main.rs
再执行 cargo xwin build --release --target x86_64-pc-windows-msvc --features tauri/custom-protocol,
否则部署出来的仍是旧界面。