to-spring-or-not-to-spring

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

To Spring or Not To Spring

用Spring动画,还是不用?

Review animation code for correct timing function selection based on interaction type.
根据交互类型,审核动画代码以选择正确的计时函数。

How It Works

工作原理

  1. Read the specified files (or prompt user for files/pattern)
  2. Check against all rules below
  3. Output findings in
    file:line
    format
  1. 读取指定文件(或提示用户输入文件/匹配模式)
  2. 对照以下所有规则进行检查
  3. file:line
    格式输出检查结果

Rule Categories

规则分类

PriorityCategoryPrefix
1Spring Selection
spring-
2Easing Selection
easing-
3Duration
duration-
4No Animation
none-
优先级分类前缀
1Spring选择
spring-
2Easing选择
easing-
3时长
duration-
4无动画
none-

Decision Framework

决策框架

Ask: Is this motion reacting to the user, or is the system speaking?
Motion TypeBest ChoiceWhy
User-driven (drag, flick, gesture)SpringSurvives interruption, preserves velocity
System-driven (state change, feedback)EasingClear start/end, predictable timing
Time representation (progress, loading)Linear1:1 relationship between time and progress
High-frequency (typing, fast toggles)NoneAnimation adds noise, feels slower
问:这个动效是响应用户操作,还是系统主动触发的?
动效类型最佳选择原因
用户驱动(拖拽、轻扫、手势)Spring可在中断后继续,保留速度
系统驱动(状态变化、反馈)Easing起止清晰,时间可预测
时间表示(进度、加载)Linear(线性)时间与进度呈1:1对应关系
高频操作(打字、快速切换)无动画动画会增加干扰,让操作感觉更慢

Rules

规则

Spring Selection Rules

Spring选择规则

spring-for-gestures

spring-for-gestures

Gesture-driven motion (drag, flick, swipe) must use springs.
Fail:
tsx
<motion.div
  drag="x"
  transition={{ duration: 0.3, ease: "easeOut" }}
/>
Pass:
tsx
<motion.div
  drag="x"
  transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
手势驱动的动效(拖拽、轻扫、滑动)必须使用Spring。
错误示例:
tsx
<motion.div
  drag="x"
  transition={{ duration: 0.3, ease: "easeOut" }}
/>
正确示例:
tsx
<motion.div
  drag="x"
  transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>

spring-for-interruptible

spring-for-interruptible

Motion that can be interrupted must use springs.
Fail:
tsx
// User can click again mid-animation
<motion.div
  animate={{ x: isOpen ? 200 : 0 }}
  transition={{ duration: 0.3 }}
/>
Pass:
tsx
<motion.div
  animate={{ x: isOpen ? 200 : 0 }}
  transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>
可被中断的动效必须使用Spring。
错误示例:
tsx
// 用户可在动画进行中再次点击
<motion.div
  animate={{ x: isOpen ? 200 : 0 }}
  transition={{ duration: 0.3 }}
/>
正确示例:
tsx
<motion.div
  animate={{ x: isOpen ? 200 : 0 }}
  transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>

spring-preserves-velocity

spring-preserves-velocity

When velocity matters, use springs to preserve input energy.
Fail:
tsx
// Fast flick and slow flick animate identically
onDragEnd={(e, info) => {
  animate(target, { x: 0 }, { duration: 0.3 });
}}
Pass:
tsx
// Fast flick moves faster than slow flick
onDragEnd={(e, info) => {
  animate(target, { x: 0 }, {
    type: "spring",
    velocity: info.velocity.x,
  });
}}
当速度很重要时,使用Spring来保留输入的动能。
错误示例:
tsx
// 快速轻扫和慢速轻扫的动画效果完全相同
onDragEnd={(e, info) => {
  animate(target, { x: 0 }, { duration: 0.3 });
}}
正确示例:
tsx
// 快速轻扫的移动速度比慢速轻扫更快
onDragEnd={(e, info) => {
  animate(target, { x: 0 }, {
    type: "spring",
    velocity: info.velocity.x,
  });
}}

spring-params-balanced

spring-params-balanced

Spring parameters must be balanced; avoid excessive oscillation.
Fail:
tsx
transition={{
  type: "spring",
  stiffness: 1000,
  damping: 5, // Too low - excessive bounce
}}
Pass:
tsx
transition={{
  type: "spring",
  stiffness: 500,
  damping: 30, // Balanced - settles quickly
}}
Spring参数必须平衡;避免过度振荡。
错误示例:
tsx
transition={{
  type: "spring",
  stiffness: 1000,
  damping: 5, // 过低 - 过度弹跳
}}
正确示例:
tsx
transition={{
  type: "spring",
  stiffness: 500,
  damping: 30, // 平衡 - 快速稳定
}}

Easing Selection Rules

Easing选择规则

easing-for-state-change

easing-for-state-change

System-initiated state changes should use easing curves.
Fail:
tsx
// Toast notification using spring
<motion.div
  animate={{ y: 0 }}
  transition={{ type: "spring" }}
/>
// Feels restless for a simple announcement
Pass:
tsx
<motion.div
  animate={{ y: 0 }}
  transition={{ duration: 0.2, ease: "easeOut" }}
/>
系统触发的状态变化应使用缓动曲线(Easing)。
错误示例:
tsx
// 提示通知使用Spring
<motion.div
  animate={{ y: 0 }}
  transition={{ type: "spring" }}
/>
// 对于简单的通知来说,会显得不稳定
正确示例:
tsx
<motion.div
  animate={{ y: 0 }}
  transition={{ duration: 0.2, ease: "easeOut" }}
/>

easing-entrance-ease-out

easing-entrance-ease-out

Entrances must use ease-out (arrive fast, settle gently).
Fail:
css
.modal-enter {
  animation-timing-function: ease-in;
}
Pass:
css
.modal-enter {
  animation-timing-function: ease-out;
}
入场动效必须使用ease-out(快速进入,平缓收尾)。
错误示例:
css
.modal-enter {
  animation-timing-function: ease-in;
}
正确示例:
css
.modal-enter {
  animation-timing-function: ease-out;
}

easing-exit-ease-in

easing-exit-ease-in

Exits must use ease-in (build momentum before departure).
Fail:
css
.modal-exit {
  animation-timing-function: ease-out;
}
Pass:
css
.modal-exit {
  animation-timing-function: ease-in;
}
退场动效必须使用ease-in(先积累动量,再离开)。
错误示例:
css
.modal-exit {
  animation-timing-function: ease-out;
}
正确示例:
css
.modal-exit {
  animation-timing-function: ease-in;
}

easing-transition-ease-in-out

easing-transition-ease-in-out

View/mode transitions use ease-in-out for neutral attention.
Pass:
css
.page-transition {
  animation-timing-function: ease-in-out;
}
视图/模式切换使用ease-in-out,以保持中性的注意力。
正确示例:
css
.page-transition {
  animation-timing-function: ease-in-out;
}

easing-linear-only-progress

easing-linear-only-progress

Linear easing only for progress bars and time representation.
Fail:
css
.card-slide {
  transition: transform 200ms linear; /* Mechanical feel */
}
Pass:
css
.progress-bar {
  transition: width 100ms linear; /* Honest time representation */
}
线性缓动(Linear)仅用于进度条和时间表示。
错误示例:
css
.card-slide {
  transition: transform 200ms linear; /* 机械感 */
}
正确示例:
css
.progress-bar {
  transition: width 100ms linear; /* 真实的时间表示 */
}

Duration Rules

时长规则

duration-press-hover

duration-press-hover

Press and hover interactions: 120-180ms.
Fail:
css
.button:hover {
  transition: background-color 400ms;
}
Pass:
css
.button:hover {
  transition: background-color 150ms;
}
按压和悬停交互:120-180毫秒。
错误示例:
css
.button:hover {
  transition: background-color 400ms;
}
正确示例:
css
.button:hover {
  transition: background-color 150ms;
}

duration-small-state

duration-small-state

Small state changes: 180-260ms.
Pass:
css
.toggle {
  transition: transform 200ms ease;
}
小型状态变化:180-260毫秒。
正确示例:
css
.toggle {
  transition: transform 200ms ease;
}

duration-max-300ms

duration-max-300ms

User-initiated animations must not exceed 300ms.
Fail:
tsx
<motion.div transition={{ duration: 0.5 }} />
Pass:
tsx
<motion.div transition={{ duration: 0.25 }} />
用户触发的动画时长不得超过300毫秒。
错误示例:
tsx
<motion.div transition={{ duration: 0.5 }} />
正确示例:
tsx
<motion.div transition={{ duration: 0.25 }} />

duration-shorten-before-curve

duration-shorten-before-curve

If animation feels slow, shorten duration before adjusting curve.
Fail (common mistake):
css
/* Trying to fix slowness with sharper curve */
.element {
  transition: 400ms cubic-bezier(0, 0.9, 0.1, 1);
}
Pass:
css
/* Fix slowness with shorter duration */
.element {
  transition: 200ms ease-out;
}
如果动画感觉缓慢,先缩短时长再调整曲线。
错误示例(常见误区):
css
/* 试图用更陡的曲线解决缓慢问题 */
.element {
  transition: 400ms cubic-bezier(0, 0.9, 0.1, 1);
}
正确示例:
css
/* 通过缩短时长解决缓慢问题 */
.element {
  transition: 200ms ease-out;
}

No Animation Rules

无动画规则

none-high-frequency

none-high-frequency

High-frequency interactions should have no animation.
Fail:
tsx
// Animated on every keystroke
function SearchInput() {
  return (
    <motion.div animate={{ scale: [1, 1.02, 1] }}>
      <input onChange={handleSearch} />
    </motion.div>
  );
}
Pass:
tsx
function SearchInput() {
  return <input onChange={handleSearch} />;
}
高频操作不应添加动画。
错误示例:
tsx
// 每次按键都触发动画
function SearchInput() {
  return (
    <motion.div animate={{ scale: [1, 1.02, 1] }}>
      <input onChange={handleSearch} />
    </motion.div>
  );
}
正确示例:
tsx
function SearchInput() {
  return <input onChange={handleSearch} />;
}

none-keyboard-navigation

none-keyboard-navigation

Keyboard navigation should be instant, no animation.
Fail:
tsx
function Menu() {
  return items.map(item => (
    <motion.li
      whileFocus={{ scale: 1.05 }}
      transition={{ duration: 0.2 }}
    />
  ));
}
Pass:
tsx
function Menu() {
  return items.map(item => (
    <li className={styles.menuItem} /> // CSS :focus-visible only
  ));
}
键盘导航应即时响应,无动画。
错误示例:
tsx
function Menu() {
  return items.map(item => (
    <motion.li
      whileFocus={{ scale: 1.05 }}
      transition={{ duration: 0.2 }}
    />
  ));
}
正确示例:
tsx
function Menu() {
  return items.map(item => (
    <li className={styles.menuItem} /> // 仅使用CSS :focus-visible
  ));
}

none-context-menu-entrance

none-context-menu-entrance

Context menus should not animate on entrance (exit only).
Fail:
tsx
<motion.div
  initial={{ opacity: 0, scale: 0.95 }}
  animate={{ opacity: 1, scale: 1 }}
  exit={{ opacity: 0 }}
/>
Pass:
tsx
<motion.div exit={{ opacity: 0, scale: 0.95 }} />
右键菜单(上下文菜单)入场时不应有动画(仅退场时可加)。
错误示例:
tsx
<motion.div
  initial={{ opacity: 0, scale: 0.95 }}
  animate={{ opacity: 1, scale: 1 }}
  exit={{ opacity: 0 }}
/>
正确示例:
tsx
<motion.div exit={{ opacity: 0, scale: 0.95 }} />

Output Format

输出格式

When reviewing files, output findings as:
file:line - [rule-id] description of issue

Example:
components/drawer/index.tsx:45 - [spring-for-gestures] Drag interaction using easing instead of spring
components/modal/styles.module.css:23 - [easing-entrance-ease-out] Modal entrance using ease-in
审查文件时,输出结果格式如下:
file:line - [规则ID] 问题描述

示例:
components/drawer/index.tsx:45 - [spring-for-gestures] 拖拽交互使用了Easing而非Spring
components/modal/styles.module.css:23 - [easing-entrance-ease-out] 模态框入场使用了ease-in

Summary Table

汇总表格

After findings, output a summary:
RuleCountSeverity
spring-for-gestures
2HIGH
easing-entrance-ease-out
1MEDIUM
duration-max-300ms
3MEDIUM
输出检查结果后,需输出汇总表:
规则数量严重程度
spring-for-gestures
2
easing-entrance-ease-out
1
duration-max-300ms
3

Quick Reference

快速参考

InteractionTimingType
Drag releaseSpring
stiffness: 500, damping: 30
Button press150ms
ease
Modal enter200ms
ease-out
Modal exit150ms
ease-in
Page transition250ms
ease-in-out
Progress barvaries
linear
Typing feedback0msnone
交互类型时长类型
拖拽释放Spring
stiffness: 500, damping: 30
按钮按压150ms
ease
模态框入场200ms
ease-out
模态框退场150ms
ease-in
页面切换250ms
ease-in-out
进度条可变
linear
打字反馈0ms

References

参考资料