morphing-icons

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Morphing Icons

可变形图标

Build icons that transform through actual shape transformation, not crossfades. Any icon can morph into any other because they share the same underlying 3-line structure.
构建通过实际形状变换而非淡入淡出实现过渡的图标。由于所有图标共享相同的底层三线结构,任意图标都可以变形为其他任意图标。

Core Concept

核心概念

Every icon is composed of exactly three SVG lines. Icons that need fewer lines collapse the extras to invisible center points. This constraint enables seamless morphing between any two icons.
每个图标都由恰好三条SVG线条组成。对于不需要三条线的图标,将多余的线条收缩为不可见的中心点。这一约束条件使得任意两个图标之间都能实现无缝变形。

Architecture

架构设计

1. Line Definition

1. 线条定义

Each line has coordinates and optional opacity:
ts
interface IconLine {
  x1: number;
  y1: number;
  x2: number;
  y2: number;
  opacity?: number;
}
每条线包含坐标和可选的透明度:
ts
interface IconLine {
  x1: number;
  y1: number;
  x2: number;
  y2: number;
  opacity?: number;
}

2. Collapsed Lines

2. 收缩线条

Icons needing fewer than 3 lines use collapsed lines—zero-length lines at the center:
ts
const CENTER = 7; // Center of 14x14 viewbox

const collapsed: IconLine = {
  x1: CENTER,
  y1: CENTER,
  x2: CENTER,
  y2: CENTER,
  opacity: 0,
};
对于不需要三条线的图标,使用收缩线条——即位于中心的零长度线条:
ts
const CENTER = 7; // 14x14视口的中心点

const collapsed: IconLine = {
  x1: CENTER,
  y1: CENTER,
  x2: CENTER,
  y2: CENTER,
  opacity: 0,
};

3. Icon Definition

3. 图标定义

Each icon has exactly 3 lines, optional rotation, and optional group:
ts
interface IconDefinition {
  lines: [IconLine, IconLine, IconLine];
  rotation?: number;
  group?: string;
}
每个图标包含恰好三条线条、可选的旋转角度以及可选的分组:
ts
interface IconDefinition {
  lines: [IconLine, IconLine, IconLine];
  rotation?: number;
  group?: string;
}

4. Rotation Groups

4. 旋转分组

Icons sharing a
group
animate rotation when transitioning between them. Icons without matching groups jump to the new rotation instantly:
ts
// These rotate smoothly between each other
{ lines: plusLines, rotation: 0, group: "plus-cross" }   // plus
{ lines: plusLines, rotation: 45, group: "plus-cross" }  // cross

// These rotate smoothly between each other
{ lines: arrowLines, rotation: 0, group: "arrow" }       // arrow-right
{ lines: arrowLines, rotation: 90, group: "arrow" }      // arrow-down
{ lines: arrowLines, rotation: 180, group: "arrow" }     // arrow-left
{ lines: arrowLines, rotation: -90, group: "arrow" }     // arrow-up
属于同一
group
的图标在过渡时会带动画旋转。分组不匹配的图标则会直接跳转到新的旋转角度:
ts
// 以下图标之间可平滑旋转过渡
{ lines: plusLines, rotation: 0, group: "plus-cross" }   // 加号
{ lines: plusLines, rotation: 45, group: "plus-cross" }  // 叉号

// 以下图标之间可平滑旋转过渡
{ lines: arrowLines, rotation: 0, group: "arrow" }       // 右箭头
{ lines: arrowLines, rotation: 90, group: "arrow" }      // 下箭头
{ lines: arrowLines, rotation: 180, group: "arrow" }     // 左箭头
{ lines: arrowLines, rotation: -90, group: "arrow" }     // 上箭头

Implementation Rules

实现规则

morphing-three-lines

morphing-three-lines

Every icon MUST use exactly 3 lines. No more, no fewer.
Fail:
ts
const checkIcon = {
  lines: [
    { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
    { x1: 5.5, y1: 11, x2: 12, y2: 3 },
  ], // Only 2 lines
};
Pass:
ts
const checkIcon = {
  lines: [
    { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
    { x1: 5.5, y1: 11, x2: 12, y2: 3 },
    collapsed, // Third line collapsed
  ],
};
每个图标必须恰好使用三条线条,不能多也不能少。
错误示例:
ts
const checkIcon = {
  lines: [
    { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
    { x1: 5.5, y1: 11, x2: 12, y2: 3 },
  ], // 仅使用了2条线
};
正确示例:
ts
const checkIcon = {
  lines: [
    { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
    { x1: 5.5, y1: 11, x2: 12, y2: 3 },
    collapsed, // 第三条线为收缩线条
  ],
};

morphing-use-collapsed

morphing-use-collapsed

Unused lines must use the collapsed constant, not omission or null.
Fail:
ts
const minusIcon = {
  lines: [
    { x1: 2, y1: 7, x2: 12, y2: 7 },
    null,
    null,
  ],
};
Pass:
ts
const minusIcon = {
  lines: [
    { x1: 2, y1: 7, x2: 12, y2: 7 },
    collapsed,
    collapsed,
  ],
};
未使用的线条必须使用预定义的收缩线条常量,不能省略或设为null。
错误示例:
ts
const minusIcon = {
  lines: [
    { x1: 2, y1: 7, x2: 12, y2: 7 },
    null,
    null,
  ],
};
正确示例:
ts
const minusIcon = {
  lines: [
    { x1: 2, y1: 7, x2: 12, y2: 7 },
    collapsed,
    collapsed,
  ],
};

morphing-consistent-viewbox

morphing-consistent-viewbox

All icons must use the same viewBox (14x14 recommended).
Fail:
ts
// Mixing viewbox scales
const icon1 = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] }; // 14x14
const icon2 = { lines: [{ x1: 4, y1: 14, x2: 24, y2: 14 }, ...] }; // 28x28
Pass:
ts
const VIEWBOX_SIZE = 14;
const CENTER = 7;
// All coordinates within 0-14 range
所有图标必须使用相同的viewBox(推荐使用14x14)。
错误示例:
ts
// 混合使用不同大小的视口
const icon1 = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] }; // 14x14
const icon2 = { lines: [{ x1: 4, y1: 14, x2: 24, y2: 14 }, ...] }; // 28x28
正确示例:
ts
const VIEWBOX_SIZE = 14;
const CENTER = 7;
// 所有坐标都在0-14范围内

morphing-group-variants

morphing-group-variants

Icons that are rotational variants MUST share the same group and base lines.
Fail:
ts
// Different line definitions for arrows
const arrowRight = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] };
const arrowDown = { lines: [{ x1: 7, y1: 2, x2: 7, y2: 12 }, ...] }; // Different!
Pass:
ts
const arrowLines: [IconLine, IconLine, IconLine] = [
  { x1: 2, y1: 7, x2: 12, y2: 7 },
  { x1: 7.5, y1: 2.5, x2: 12, y2: 7 },
  { x1: 7.5, y1: 11.5, x2: 12, y2: 7 },
];

const icons = {
  "arrow-right": { lines: arrowLines, rotation: 0, group: "arrow" },
  "arrow-down": { lines: arrowLines, rotation: 90, group: "arrow" },
  "arrow-left": { lines: arrowLines, rotation: 180, group: "arrow" },
  "arrow-up": { lines: arrowLines, rotation: -90, group: "arrow" },
};
属于旋转变体的图标必须共享相同的分组和基础线条定义。
错误示例:
ts
// 箭头图标使用不同的线条定义
const arrowRight = { lines: [{ x1: 2, y1: 7, x2: 12, y2: 7 }, ...] };
const arrowDown = { lines: [{ x1: 7, y1: 2, x2: 7, y2: 12 }, ...] }; // 线条定义不同!
正确示例:
ts
const arrowLines: [IconLine, IconLine, IconLine] = [
  { x1: 2, y1: 7, x2: 12, y2: 7 },
  { x1: 7.5, y1: 2.5, x2: 12, y2: 7 },
  { x1: 7.5, y1: 11.5, x2: 12, y2: 7 },
];

const icons = {
  "arrow-right": { lines: arrowLines, rotation: 0, group: "arrow" },
  "arrow-down": { lines: arrowLines, rotation: 90, group: "arrow" },
  "arrow-left": { lines: arrowLines, rotation: 180, group: "arrow" },
  "arrow-up": { lines: arrowLines, rotation: -90, group: "arrow" },
};

morphing-spring-rotation

morphing-spring-rotation

Rotation between grouped icons should use spring physics for natural motion.
Fail:
tsx
<motion.g animate={{ rotate: rotation }} transition={{ duration: 0.3 }} />
Pass:
tsx
const rotation = useSpring(definition.rotation ?? 0, activeTransition);

<motion.g style={{ rotate: rotation, transformOrigin: "center" }} />
同组图标之间的旋转过渡应使用弹簧物理动画,以实现自然的运动效果。
错误示例:
tsx
<motion.g animate={{ rotate: rotation }} transition={{ duration: 0.3 }} />
正确示例:
tsx
const rotation = useSpring(definition.rotation ?? 0, activeTransition);

<motion.g style={{ rotate: rotation, transformOrigin: "center" }} />

morphing-reduced-motion

morphing-reduced-motion

Respect
prefers-reduced-motion
by disabling animations.
Fail:
tsx
function MorphingIcon({ icon }: Props) {
  return <motion.line animate={...} transition={{ duration: 0.4 }} />;
}
Pass:
tsx
function MorphingIcon({ icon }: Props) {
  const reducedMotion = useReducedMotion() ?? false;
  const activeTransition = reducedMotion ? { duration: 0 } : transition;
  
  return <motion.line animate={...} transition={activeTransition} />;
}
应尊重
prefers-reduced-motion
设置,在用户偏好减少动画时禁用动画。
错误示例:
tsx
function MorphingIcon({ icon }: Props) {
  return <motion.line animate={...} transition={{ duration: 0.4 }} />;
}
正确示例:
tsx
function MorphingIcon({ icon }: Props) {
  const reducedMotion = useReducedMotion() ?? false;
  const activeTransition = reducedMotion ? { duration: 0 } : transition;
  
  return <motion.line animate={...} transition={activeTransition} />;
}

morphing-jump-non-grouped

morphing-jump-non-grouped

When transitioning between icons NOT in the same group, rotation should jump instantly.
Fail:
tsx
// Always animating rotation regardless of group
useEffect(() => {
  rotation.set(definition.rotation ?? 0);
}, [definition]);
Pass:
tsx
useEffect(() => {
  if (shouldRotate) {
    rotation.set(definition.rotation ?? 0); // Animate
  } else {
    rotation.jump(definition.rotation ?? 0); // Instant
  }
}, [definition, shouldRotate]);
在不同分组的图标之间过渡时,旋转角度应直接跳转,不带动画。
错误示例:
tsx
// 无论分组是否匹配,始终带动画旋转
useEffect(() => {
  rotation.set(definition.rotation ?? 0);
}, [definition]);
正确示例:
tsx
useEffect(() => {
  if (shouldRotate) {
    rotation.set(definition.rotation ?? 0); // 带动画
  } else {
    rotation.jump(definition.rotation ?? 0); // 立即跳转
  }
}, [definition, shouldRotate]);

morphing-strokelinecap-round

morphing-strokelinecap-round

Lines should use
strokeLinecap="round"
for polished endpoints.
Fail:
tsx
<motion.line strokeLinecap="butt" />
Pass:
tsx
<motion.line strokeLinecap="round" />
线条应设置
strokeLinecap="round"
,以实现圆润的端点效果。
错误示例:
tsx
<motion.line strokeLinecap="butt" />
正确示例:
tsx
<motion.line strokeLinecap="round" />

morphing-aria-hidden

morphing-aria-hidden

Icon SVGs should be
aria-hidden
since they're decorative.
Fail:
tsx
<svg width={size} height={size}>...</svg>
Pass:
tsx
<svg width={size} height={size} aria-hidden="true">...</svg>
由于图标仅起装饰作用,SVG图标应设置
aria-hidden
属性。
错误示例:
tsx
<svg width={size} height={size}>...</svg>
正确示例:
tsx
<svg width={size} height={size} aria-hidden="true">...</svg>

Common Icon Patterns

常见图标模式

Two-Line Icons (check, minus, equals, chevron)

两线图标(对勾、减号、等号、Chevron)

Use one or two collapsed lines:
ts
const check = {
  lines: [
    { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
    { x1: 5.5, y1: 11, x2: 12, y2: 3 },
    collapsed,
  ],
};
使用一条或两条收缩线条:
ts
const check = {
  lines: [
    { x1: 2, y1: 7.5, x2: 5.5, y2: 11 },
    { x1: 5.5, y1: 11, x2: 12, y2: 3 },
    collapsed,
  ],
};

Three-Line Icons (menu, asterisk, play)

三线图标(菜单、星号、播放)

Use all three lines:
ts
const menu = {
  lines: [
    { x1: 2, y1: 3.5, x2: 12, y2: 3.5 },
    { x1: 2, y1: 7, x2: 12, y2: 7 },
    { x1: 2, y1: 10.5, x2: 12, y2: 10.5 },
  ],
};
使用全部三条线条:
ts
const menu = {
  lines: [
    { x1: 2, y1: 3.5, x2: 12, y2: 3.5 },
    { x1: 2, y1: 7, x2: 12, y2: 7 },
    { x1: 2, y1: 10.5, x2: 12, y2: 10.5 },
  ],
};

Point Icons (more, grip)

点状图标(更多、 grip)

Use zero-length lines as dots:
ts
const more = {
  lines: [
    { x1: 3, y1: 7, x2: 3, y2: 7 },
    { x1: 7, y1: 7, x2: 7, y2: 7 },
    { x1: 11, y1: 7, x2: 11, y2: 7 },
  ],
};
使用零长度线条作为圆点:
ts
const more = {
  lines: [
    { x1: 3, y1: 7, x2: 3, y2: 7 },
    { x1: 7, y1: 7, x2: 7, y2: 7 },
    { x1: 11, y1: 7, x2: 11, y2: 7 },
  ],
};

Recommended Transition

推荐过渡效果

Use exponential ease-out for smooth morphing:
ts
const defaultTransition: Transition = {
  ease: [0.19, 1, 0.22, 1],
  duration: 0.4,
};
使用指数缓出曲线实现平滑的变形效果:
ts
const defaultTransition: Transition = {
  ease: [0.19, 1, 0.22, 1],
  duration: 0.4,
};

Output Format

输出格式

When auditing morphing icon implementations, output findings as:
file:line - [rule-id] description of issue

Example:
components/icon/index.tsx:45 - [morphing-three-lines] Icon "check" has only 2 lines, needs collapsed third
components/icon/index.tsx:78 - [morphing-group-variants] arrow-down uses different line definitions than arrow-right
审核可变形图标实现时,应按以下格式输出问题:
file:line - [rule-id] 问题描述

示例:
components/icon/index.tsx:45 - [morphing-three-lines] 图标“check”仅使用了2条线,需添加第三条收缩线条
components/icon/index.tsx:78 - [morphing-group-variants] arrow-down使用的线条定义与arrow-right不同

Summary Table

汇总表格

After findings, output a summary:
RuleCountSeverity
morphing-three-lines
2HIGH
morphing-group-variants
1HIGH
morphing-reduced-motion
1MEDIUM
输出问题后,应附带汇总表格:
规则数量严重程度
morphing-three-lines
2
morphing-group-variants
1
morphing-reduced-motion
1

References

参考资料