motion.md
基础 · FOUNDATION

动效 Motion

产品内动效 150–250ms,只传递状态。标准缓动 cubic-bezier(.2,.7,.2,1)。戏剧化只留给首次进站的开屏,之后产品永远安静。

重播

zzmSpring1.1s
弹性落定
zzmIn.35s
进场上浮
zzmSlide.28s
侧栏滑入
zzmGrow.55s
展开生长
zzmPulse2.6s ∞
当前节点呼吸
import { useState } from 'react'
import { Btn } from './components/v4/Button.jsx'

const ACCENT = 'var(--zzm-accent, #A6402F)'
const EASE = 'cubic-bezier(.2,.7,.2,1)'
const MONO = "ui-monospace,'SF Mono',Menlo,monospace"

const ROWS = [
  ['zzmSpring', '1.1s', '弹性落定', { width: 20, height: 20, borderRadius: '50%', background: ACCENT, animation: `zzmSpring 1.1s ${EASE} both` }],
  ['zzmIn', '.35s', '进场上浮', { width: 96, height: 20, borderRadius: 4, background: '#1D1D1B', animation: 'zzmIn .35s ease both' }],
  ['zzmSlide', '.28s', '侧栏滑入', { width: 96, height: 20, borderRadius: 4, background: '#8B8B85', animation: 'zzmSlide .28s ease both' }],
  ['zzmGrow', '.55s', '展开生长', { width: 160, height: 20, borderRadius: 4, background: '#D6D6D1', overflow: 'hidden', animation: `zzmGrow .55s ${EASE} both` }],
  ['zzmPulse', '2.6s ∞', '当前节点呼吸', { width: 20, height: 20, borderRadius: '50%', border: `1.5px solid ${ACCENT}`, animation: 'zzmPulse 2.6s ease-in-out infinite' }],
]

export default function MotionReplay() {
  const [n, setN] = useState(0)
  return (
    <div style={{ display: 'grid', gap: 12, width: '100%' }}>
      {ROWS.map(([name, dur, use, style]) => (
        <div key={name} style={{ display: 'grid', gridTemplateColumns: '96px 56px minmax(0,1fr) 96px', gap: 12, alignItems: 'center', minHeight: 28 }}>
          <span style={{ fontFamily: MONO, fontSize: 11, color: '#161616' }}>{name}</span>
          <span style={{ fontFamily: MONO, fontSize: 11, color: '#A2A29C' }}>{dur}</span>
          <div style={{ minWidth: 0 }}><div key={n} style={style} /></div>
          <span style={{ fontSize: 12, color: '#8B8B85' }}>{use}</span>
        </div>
      ))}
      <div><Btn size="sm" onClick={() => setN(n + 1)}>重播</Btn></div>
    </div>
  )
}

缓动与时长

用途
标准缓动cubic-bezier(.2,.7,.2,1)——快起慢落
悬停 / 按压过渡.16s ease(按钮、卡片、链接)
状态切换150–250ms
进场zzmIn .35szzmUp .25s
展开zzmGrow .55s
弹性强调zzmSpring 1.1s

关键帧清单

名称效果用在哪
zzmPulse透明度 .15 ↔ .55 呼吸学习地图当前节点光晕
zzmIn / zzmIn2上移 12px 淡入屏幕进场
zzmUp / zzmUp2上移 10px 淡入卡片、提示进场
zzmSlide左移 14px 淡入侧栏滑入
zzmGrowmax-height 0 → 960 展开折叠面板
zzmSpring横移过冲再回弹弹性演示
zzmFade纯淡入遮罩
zzmPayIn上移 20px + 缩放 .985 淡入付款页
zzmShimmer一道微光横扫骨架屏
zzmBtnSpin旋转一周busy 键旋转环
zzmPillShine快扫一次后长歇档位胶囊反光、busy 键反光

全部带 zzm 前缀,全局注册无副作用;组件里用内联 animation: 引用。@keyframes zzmDialogIn 是提炼层为确认窗新增的一条。

减少动效

prefers-reduced-motion: reduce 时,所有动画与过渡时长压到 0.01ms、只播一次。这段规则在 zzm.css 的基线段里,逐字取自 edu。

css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

铁律

✓ 要动效只传递状态:出现、展开、按下、完成
✗ 不要装饰性循环动画(呼吸光晕只给「当前位置」)
✓ 要动 transform 和 opacity
✗ 不要动 width / height / padding 引起重排
✓ 要尊重减少动效设置
✗ 不要开屏式大动画出现在第二次访问