SSGOI LogoSSGOI

元素动画

为单个DOM元素应用动画

SSGOI
from0
to1
spring

过渡效果示例

如何使用各种过渡效果:

import { transition } from '@ssgoi/react';
import { fade, scale, blur, slide, fly, rotate, bounce } from '@ssgoi/react/transitions';

/**
 * [React] Auto Key Plugin
 *
 * 使用插件时可以省略 key 属性。
 * 基于 文件:行:列 自动生成唯一的 key。
 *
 * Next.js 配置:
 * ```ts
 * // next.config.ts
 * import SsgoiAutoKey from "@ssgoi/react/unplugin/webpack";
 *
 * const nextConfig = {
 *   webpack: (config) => {
 *     config.plugins.push(SsgoiAutoKey());
 *     return config;
 *   },
 * };
 * ```
 *
 * ⚠️ 注意: .map() 列表中只需要 JSX key - 插件会自动处理
 *
 * // .map() 列表中的正确用法:
 * {items.map((item) => (
 *   <div
 *     key={item.id}  // JSX key 就足够了
 *     ref={transition(fade())}
 *   >
 *     {item.name}
 *   </div>
 * ))}
 *
 * // 不使用插件时,key 是必需的
 */

// Fade过渡
<div ref={transition(fade({ physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Fade效果
</div>

// Scale过渡
<div ref={transition(scale({ physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Scale效果
</div>

// Blur过渡
<div ref={transition(blur({ amount: 10, physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Blur效果
</div>

// Slide过渡(方向指定)
<div ref={transition(slide({ direction: 'left', physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Slide效果
</div>

// Fly过渡(自定义位置)
<div ref={transition(fly({ x: 200, y: -50, physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Fly效果
</div>

// Rotate过渡
<div ref={transition(rotate({ physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Rotate效果
</div>

// Bounce过渡
<div ref={transition(bounce({ physics: { spring: { stiffness: 300, damping: 30 } } }))}>
  Bounce效果
</div>

// 明确指定 key(不使用插件时必须)
<div ref={transition({
  key: "custom-element",
  ...fade({ physics: { spring: { stiffness: 300, damping: 30 } } })
})}>
  自定义 Key
</div>

基本结构

TransitionConfig接口

interface TransitionConfig {
  physics?: {
    // Spring: 弹簧物理 (ease-out 效果, 默认)
    spring?: {
      stiffness: number;  // 弹簧刚度 (默认: 300)
      damping: number;    // 阻尼系数 (默认: 30)
      doubleSpring?: boolean | number; // 双弹簧效果 (默认: false)
    };
    // Inertia: 惯性物理 (ease-in 效果)
    inertia?: {
      acceleration: number; // 加速度 (默认: 500)
      resistance: number;   // 阻力系数 (默认: 10)
    };
    // Custom Integrator: 自定义物理引擎
    integrator?: () => Integrator;
  };
  // 选择两种动画模式之一(不能同时使用)
  tick?: (progress: number) => void; // 基于RAF的动画
  css?: (progress: number) => StyleObject; // 基于Web Animation API(推荐)
  prepare?: (element: HTMLElement) => void; // 动画开始前的初始设置
  onStart?: () => void;
  onEnd?: () => void;
}

物理引擎选项

SSGOI 支持多种物理引擎来实现自然动画:

Spring (弹簧物理)

ease-out 效果的默认物理引擎。动画快速开始,到达目标点时自然减速。

{
  in: (element) => ({
    physics: {
      spring: {
        stiffness: 300,  // 弹簧刚度 (越高越快)
        damping: 30,     // 阻尼系数 (越高弹性越小)
        doubleSpring: false  // 双弹簧效果
      }
    },
    css: (progress) => ({
      opacity: progress,
      transform: `translateY(${(1 - progress) * 20}px)`,
    }),
  })
}

Inertia (惯性物理)

提供 ease-in 效果的物理引擎。动画缓慢开始然后逐渐加速。

{
  in: (element) => ({
    physics: {
      inertia: {
        acceleration: 500,  // 加速度 (越高越快加速)
        resistance: 10      // 阻力系数 (越高越减速)
      }
    },
    css: (progress) => ({
      opacity: progress,
      transform: `scale(${0.8 + progress * 0.2})`,
    }),
  })
}

Custom Integrator (自定义物理引擎)

您可以实现并使用自己的物理引擎。

{
  in: (element) => ({
    physics: {
      integrator: () => ({
        next: (dt, state) => {
          // 自定义物理计算逻辑
          return newState;
        }
      })
    },
    css: (progress) => ({
      opacity: progress,
    }),
  })
}

动画模式: css vs tick

SSGOI支持两种动画模式:

css模式(推荐)

{
  in: (element) => ({
    physics: { spring: { stiffness: 300, damping: 30 } },
    css: (progress) => ({
      opacity: progress,
      transform: `translateY(${(1 - progress) * 20}px)`,
    }),
  })
}
  • 使用Web Animation API最小化主线程负载
  • 预计算弹簧物理并转换为关键帧
  • GPU加速实现流畅动画
  • 性能更好,大多数情况下推荐使用

tick模式

{
  in: (element) => ({
    physics: { spring: { stiffness: 300, damping: 30 } },
    tick: (progress) => {
      element.style.opacity = String(progress);
      element.style.transform = `translateY(${(1 - progress) * 20}px)`;
    },
  })
}
  • 基于requestAnimationFrame,每帧执行回调
  • 适用于需要直接DOM操作的复杂动画
  • 当动画期间需要动态更改值时很有用

csstick不能同时使用。请选择其中一个。

过渡定义

interface Transition {
  in?: (element: HTMLElement) => TransitionConfig;
  out?: (element: HTMLElement) => TransitionConfig;
}

工作原理

  1. 挂载时: 当元素添加到DOM时执行in函数
  2. 卸载时: 在元素被移除前执行out函数
  3. 动画: 弹簧物理引擎生成进度
    • in: 0 → 1
    • out: 1 → 0
  4. tick回调: 每帧调用以更新样式

过渡预设

import { fade, scale /** etc */ } from "@ssgoi/react/transitions";

框架特定用法

import { transition } from "@ssgoi/react";

<div
  ref={transition({
    key: "unique-key",
    in: (element) => ({
      tick: (progress) => {
        element.style.opacity = progress;
        element.style.transform = `translateY(${20 * (1 - progress)}px)`;
      },
    }),
    out: (element) => ({
      tick: (progress) => {
        element.style.opacity = 1 - progress;
      },
    }),
  })}
>
  内容
</div>

进度行为

key

  • key必须在页面内唯一(这样即使DOM被创建后删除或删除后创建,动画状态也可以被跟踪)
  • React中可以使用Auto Key Plugin自动生成key(详见上方React标签页中的说明)
  • 如果key未设置且未使用插件,则根据调用位置生成默认的自动键

in动画

  • progress: 0 → 1
  • 当元素出现时执行
  • 不透明度从0到1,从小尺寸到原始尺寸

out动画

  • progress: 1 → 0
  • 当元素消失时执行
  • 不透明度从1到0,从原始尺寸到小尺寸
// 示例:in和out的区别
{
  in: (element) => ({
    tick: (progress) => {
      // progress: 0 → 1
      element.style.opacity = progress;  // 0 → 1
    }
  }),
  out: (element) => ({
    tick: (progress) => {
      // progress: 1 → 0
      element.style.opacity = progress;  // 1 → 0
    }
  })
}

prepare回调

动画开始前准备DOM元素的阶段:

{
  in: {
    prepare: (element) => {
      // 在tick执行前设置初始状态
      element.style.willChange = 'opacity, transform';
    },
    tick: (progress) => ({
      opacity: progress,
      transform: `translateY(${20 * (1 - progress)}px)`
    })
  }
}

TransitionScope

TransitionScope是一个控制子元素动画行为的容器。当父元素和子元素同时挂载/卸载时,可以跳过不必要的动画。

使用场景

  • 页面过渡: 防止页面过渡时各个元素单独动画
  • 模态框/对话框: 防止与容器一起出现的内容重复动画
  • 列表: 当整个列表挂载/卸载时跳过单个项目的动画

scope选项

interface TransitionOptions {
  // ...
  scope?: 'global' | 'local';
}
  • global(默认): 始终执行动画
  • local: 与TransitionScope同时挂载/卸载时跳过动画

使用示例

import { TransitionScope, transition } from '@ssgoi/react';

function MyComponent() {
  const [show, setShow] = useState(true);

  return (
    <>
      <button onClick={() => setShow(!show)}>Toggle</button>

      {show && (
        <TransitionScope>
          <div style={{ padding: '2rem', border: '1px dashed #ccc' }}>
            {/* local: 与Scope同时挂载/卸载时跳过动画 */}
            <div
              ref={transition({
                key: 'local-child',
                scope: 'local',
                in: () => ({
                  css: (p) => ({ opacity: p, transform: `scale(${0.5 + p * 0.5})` }),
                }),
                out: () => ({
                  css: (p) => ({ opacity: p, transform: `scale(${0.5 + p * 0.5})` }),
                }),
              })}
            >
              Local Scope(与Scope一起显示/隐藏)
            </div>

            {/* global: 始终执行动画 */}
            <div
              ref={transition({
                key: 'global-child',
                // scope: 'global'是默认值
                in: () => ({
                  css: (p) => ({ opacity: p, transform: `scale(${0.5 + p * 0.5})` }),
                }),
                out: () => ({
                  css: (p) => ({ opacity: p, transform: `scale(${0.5 + p * 0.5})` }),
                }),
              })}
            >
              Global Scope(始终动画)
            </div>
          </div>
        </TransitionScope>
      )}
    </>
  );
}

行为

情况scope: 'local'scope: 'global'
与Scope同时挂载跳过动画执行动画
与Scope同时卸载跳过动画执行动画
在Scope内单独切换执行动画执行动画