spring-animation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

When to use

适用场景

Use this skill when creating Remotion video compositions that need spring physics -- natural, organic motion with bounce, overshoot, and elastic settling.
Use spring when you need:
  • Bouncy/elastic entrances (overshoot + settle)
  • Organic deceleration (not linear, not eased -- physically modeled)
  • Staggered trails with spring physics per element
  • Number counters that overshoot then settle
  • Scale/rotate with natural weight and inertia
  • Enter + exit animations with spring math (
    in - out
    )
  • Multi-property orchestration with different spring configs per property
Use Remotion native
interpolate()
when:
  • Linear or eased motion with no bounce (fade, slide, wipe)
  • Exact timing control (must end at precisely frame N)
  • Clip-path animations
  • Progress bars / deterministic counters
Use GSAP (gsap-animation skill) when:
  • Text splitting (SplitText: chars/words/lines with mask)
  • SVG stroke drawing (DrawSVG)
  • SVG morphing (MorphSVG)
  • Complex timeline orchestration with labels and position parameters
  • ScrambleText decode effects
  • Registered reusable effects
Note:
@react-spring/web
is NOT compatible with Remotion (it uses requestAnimationFrame internally). This skill uses Remotion's native
spring()
function which provides the same physics model in a frame-deterministic way.

当你创建需要弹簧物理效果的Remotion视频合成内容时,可以使用该技能——弹簧物理效果能实现带有弹跳、过冲和弹性稳定的自然有机运动。
适合使用弹簧效果的场景:
  • 弹性/弹跳式入场动画(过冲后稳定)
  • 自然减速运动(非线性、非缓动——基于物理模型)
  • 每个元素都带有弹簧物理的交错轨迹
  • 先过冲后稳定的数字计数器
  • 带有自然重量和惯性的缩放/旋转动画
  • 使用弹簧算法的入场+退场动画(
    in - out
  • 为每个属性配置不同弹簧参数的多属性协同动画
适合使用Remotion原生
interpolate()
的场景:
  • 无弹跳的线性或缓动运动(淡入、滑动、擦除)
  • 需要精确时间控制(必须在第N帧精确结束)
  • 裁剪路径动画
  • 进度条/确定性计数器
适合使用GSAP(gsap-animation技能)的场景:
  • 文本拆分(SplitText:按字符/单词/行拆分并添加遮罩)
  • SVG描边绘制(DrawSVG)
  • SVG变形(MorphSVG)
  • 带有标签和位置参数的复杂时间轴编排
  • 乱码文本解码效果
  • 可复用的注册式特效
注意:
@react-spring/web
与Remotion不兼容(它内部使用requestAnimationFrame)。本技能使用Remotion原生的
spring()
函数,该函数以帧确定性的方式提供相同的物理模型。

Core API

核心API

spring()

spring()

Returns a value from 0 to 1 (can overshoot past 1 with low damping) based on spring physics simulation.
tsx
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';

const frame = useCurrentFrame();
const { fps } = useVideoConfig();

const value = spring({
  frame,
  fps,
  config: {
    damping: 10,     // 1-200: higher = less bounce
    stiffness: 100,  // 1-200: higher = faster snap
    mass: 1,         // 0.1-5: higher = more inertia
  },
});
基于弹簧物理模拟返回0到1之间的值(低阻尼设置下可能会超过1)。
tsx
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';

const frame = useCurrentFrame();
const { fps } = useVideoConfig();

const value = spring({
  frame,
  fps,
  config: {
    damping: 10,     // 1-200:值越高,弹跳越少
    stiffness: 100,  // 1-200:值越高,响应越快
    mass: 1,         // 0.1-5:值越高,惯性越大
  },
});

Config Parameters

配置参数

ParameterRangeDefaultEffect
damping
1-20010Resistance. Low = bouncy, high = smooth
stiffness
1-200100Snap speed. High = fast, low = slow
mass
0.1-51Weight/inertia. High = sluggish, low = light
overshootClamping
boolfalseClamp at target (no overshoot)
参数范围默认值作用
damping
1-20010阻力:值越低,弹跳越强;值越高,运动越平滑
stiffness
1-200100响应速度:值越高,运动越快;值越低,运动越慢
mass
0.1-51重量/惯性:值越高,运动越迟缓;值越低,运动越轻盈
overshootClamping
布尔值false限制在目标值(禁止过冲)

Additional Options

额外选项

OptionTypeEffect
delay
numberDelay start by N frames (returns 0 until delay elapses)
durationInFrames
numberForce spring to settle within N frames
reverse
boolAnimate from 1 to 0
from
numberStarting value (default 0)
to
numberEnding value (default 1)
选项类型作用
delay
数字延迟N帧后开始动画(延迟期间返回0)
durationInFrames
数字强制弹簧动画在N帧内完成稳定
reverse
布尔值从1到0反向动画
from
数字起始值(默认0)
to
数字结束值(默认1)

measureSpring()

measureSpring()

Calculate how many frames a spring config takes to settle. Essential for
<Sequence>
and composition duration.
tsx
import { measureSpring } from 'remotion';

const frames = measureSpring({
  fps: 30,
  config: { damping: 10, stiffness: 100 },
}); // => number of frames until settled

计算弹簧配置完成稳定所需的帧数,对
<Sequence>
和合成内容时长设置至关重要。
tsx
import { measureSpring } from 'remotion';

const frames = measureSpring({
  fps: 30,
  config: { damping: 10, stiffness: 100 },
}); // => 稳定所需的帧数

Physics Presets

物理预设

tsx
// src/spring-presets.ts
import { SpringConfig } from 'remotion';

export const SPRING = {
  // Smooth, no bounce -- subtle reveals, background motion
  smooth: { damping: 200 } as Partial<SpringConfig>,

  // Snappy, minimal bounce -- UI elements, clean entrances
  snappy: { damping: 20, stiffness: 200 } as Partial<SpringConfig>,

  // Bouncy -- playful entrances, attention-grabbing
  bouncy: { damping: 8 } as Partial<SpringConfig>,

  // Heavy, slow -- dramatic reveals, weighty objects
  heavy: { damping: 15, stiffness: 80, mass: 2 } as Partial<SpringConfig>,

  // Wobbly -- elastic, cartoon-like overshoot
  wobbly: { damping: 4, stiffness: 80 } as Partial<SpringConfig>,

  // Stiff -- fast snap with tiny bounce
  stiff: { damping: 15, stiffness: 300 } as Partial<SpringConfig>,

  // Gentle -- slow, dreamy, organic
  gentle: { damping: 20, stiffness: 40, mass: 1.5 } as Partial<SpringConfig>,

  // Molasses -- very slow, heavy, barely bounces
  molasses: { damping: 25, stiffness: 30, mass: 3 } as Partial<SpringConfig>,

  // Pop -- strong overshoot for scale-in effects
  pop: { damping: 6, stiffness: 150 } as Partial<SpringConfig>,

  // Rubber -- exaggerated elastic bounce
  rubber: { damping: 3, stiffness: 100, mass: 0.5 } as Partial<SpringConfig>,
} as const;
tsx
// src/spring-presets.ts
import { SpringConfig } from 'remotion';

export const SPRING = {
  // 平滑无弹跳——微妙的显示效果、背景运动
  smooth: { damping: 200 } as Partial<SpringConfig>,

  // 快速响应、轻微弹跳——UI元素、简洁入场动画
  snappy: { damping: 20, stiffness: 200 } as Partial<SpringConfig>,

  // 弹跳感强——活泼的入场动画、吸引注意力
  bouncy: { damping: 8 } as Partial<SpringConfig>,

  // 沉重缓慢——戏剧性的显示效果、有重量感的物体
  heavy: { damping: 15, stiffness: 80, mass: 2 } as Partial<SpringConfig>,

  // 摇晃感——弹性、卡通风格的过冲
  wobbly: { damping: 4, stiffness: 80 } as Partial<SpringConfig>,

  // 刚性——快速响应、微小弹跳
  stiff: { damping: 15, stiffness: 300 } as Partial<SpringConfig>,

  // 柔和——缓慢、梦幻、自然
  gentle: { damping: 20, stiffness: 40, mass: 1.5 } as Partial<SpringConfig>,

  // 极慢沉重——几乎无弹跳
  molasses: { damping: 25, stiffness: 30, mass: 3 } as Partial<SpringConfig>,

  // 弹出感——强烈过冲的缩放入场效果
  pop: { damping: 6, stiffness: 150 } as Partial<SpringConfig>,

  // 橡胶感——夸张的弹性弹跳
  rubber: { damping: 3, stiffness: 100, mass: 0.5 } as Partial<SpringConfig>,
} as const;

Preset Visual Reference

预设视觉参考

PresetBounceSpeedFeelBest For
smooth
NoneMediumButterBackground, subtle reveals
snappy
MinimalFastCrispUI elements, buttons
bouncy
StrongMediumPlayfulTitles, icons, attention
heavy
SmallSlowWeightyDramatic reveals, large objects
wobbly
ExtremeMediumCartoonPlayful, humorous
stiff
TinyVery fastMechanicalData viz, precise motion
gentle
MinimalSlowDreamyLuxury, calm, organic
molasses
Almost noneVery slowHeavyCinematic, suspense
pop
StrongFastPunchyScale-in, badge, icon pop
rubber
ExtremeFastElasticExaggerated, cartoon, fun

预设弹跳程度速度质感最佳适用场景
smooth
中等丝滑背景、微妙显示效果
snappy
轻微快速利落UI元素、按钮
bouncy
强烈中等活泼标题、图标、吸引注意力
heavy
轻微缓慢沉重戏剧性显示效果、大型物体
wobbly
极强中等卡通活泼、幽默风格
stiff
极微极快机械感数据可视化、精确运动
gentle
轻微缓慢梦幻高端风格、平静、自然
molasses
几乎无极慢沉重电影感、悬疑风格
pop
强烈快速有力缩放入场、徽章、图标弹出
rubber
极强快速弹性夸张、卡通、趣味风格

1. Spring Entrance Patterns

1. 弹簧入场动画模式

Basic Spring Entrance

基础弹簧入场

tsx
import { spring, interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill } from 'remotion';
import { SPRING } from './spring-presets';

const SpringEntrance: React.FC<{
  children: React.ReactNode;
  preset?: keyof typeof SPRING;
  delay?: number;
}> = ({ children, preset = 'bouncy', delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({ frame, fps, delay, config: SPRING[preset] });
  const translateY = interpolate(progress, [0, 1], [60, 0]);

  return (
    <AbsoluteFill style={{
      opacity: progress,
      transform: `translateY(${translateY}px)`,
    }}>
      {children}
    </AbsoluteFill>
  );
};
tsx
import { spring, interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill } from 'remotion';
import { SPRING } from './spring-presets';

const SpringEntrance: React.FC<{
  children: React.ReactNode;
  preset?: keyof typeof SPRING;
  delay?: number;
}> = ({ children, preset = 'bouncy', delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({ frame, fps, delay, config: SPRING[preset] });
  const translateY = interpolate(progress, [0, 1], [60, 0]);

  return (
    <AbsoluteFill style={{
      opacity: progress,
      transform: `translateY(${translateY}px)`,
    }}>
      {children}
    </AbsoluteFill>
  );
};

Scale Pop

缩放弹出

tsx
const ScalePop: React.FC<{
  children: React.ReactNode;
  delay?: number;
}> = ({ children, delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // pop preset overshoots past 1, creating natural scale bounce
  const scale = spring({ frame, fps, delay, config: SPRING.pop });
  const opacity = spring({ frame, fps, delay, config: SPRING.smooth });

  return (
    <div style={{
      transform: `scale(${scale})`,
      opacity,
    }}>
      {children}
    </div>
  );
};
tsx
const ScalePop: React.FC<{
  children: React.ReactNode;
  delay?: number;
}> = ({ children, delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // pop预设会超过1,产生自然的缩放弹跳
  const scale = spring({ frame, fps, delay, config: SPRING.pop });
  const opacity = spring({ frame, fps, delay, config: SPRING.smooth });

  return (
    <div style={{
      transform: `scale(${scale})`,
      opacity,
    }}>
      {children}
    </div>
  );
};

Enter + Exit (Spring Math)

入场+退场(弹簧算法)

tsx
const EnterExit: React.FC<{
  children: React.ReactNode;
  enterDelay?: number;
  exitBeforeEnd?: number; // frames before composition end to start exit
}> = ({ children, enterDelay = 0, exitBeforeEnd = 30 }) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  const enter = spring({ frame, fps, delay: enterDelay, config: SPRING.bouncy });
  const exit = spring({
    frame, fps,
    delay: durationInFrames - exitBeforeEnd,
    config: SPRING.snappy,
  });

  const scale = enter - exit; // 0 -> 1 -> 0
  const opacity = enter - exit;

  return (
    <div style={{ transform: `scale(${scale})`, opacity }}>
      {children}
    </div>
  );
};

tsx
const EnterExit: React.FC<{
  children: React.ReactNode;
  enterDelay?: number;
  exitBeforeEnd?: number; // 合成内容结束前N帧开始退场
}> = ({ children, enterDelay = 0, exitBeforeEnd = 30 }) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  const enter = spring({ frame, fps, delay: enterDelay, config: SPRING.bouncy });
  const exit = spring({
    frame, fps,
    delay: durationInFrames - exitBeforeEnd,
    config: SPRING.snappy,
  });

  const scale = enter - exit; // 0 -> 1 -> 0
  const opacity = enter - exit;

  return (
    <div style={{ transform: `scale(${scale})`, opacity }}>
      {children}
    </div>
  );
};

2. Trail / Stagger Patterns

2. 轨迹/交错动画模式

Spring Trail (staggered entrance)

弹簧轨迹(交错入场)

Mimics React Spring's
useTrail
-- each element enters with a frame delay.
tsx
const SpringTrail: React.FC<{
  items: React.ReactNode[];
  staggerFrames?: number;
  preset?: keyof typeof SPRING;
  direction?: 'up' | 'down' | 'left' | 'right';
}> = ({ items, staggerFrames = 4, preset = 'bouncy', direction = 'up' }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const getOffset = (progress: number) => {
    const distance = 50;
    const remaining = interpolate(progress, [0, 1], [distance, 0]);
    switch (direction) {
      case 'up': return { transform: `translateY(${remaining}px)` };
      case 'down': return { transform: `translateY(${-remaining}px)` };
      case 'left': return { transform: `translateX(${remaining}px)` };
      case 'right': return { transform: `translateX(${-remaining}px)` };
    }
  };

  return (
    <>
      {items.map((item, i) => {
        const delay = i * staggerFrames;
        const progress = spring({ frame, fps, delay, config: SPRING[preset] });
        return (
          <div key={i} style={{ opacity: progress, ...getOffset(progress) }}>
            {item}
          </div>
        );
      })}
    </>
  );
};
模拟React Spring的
useTrail
——每个元素按帧延迟入场。
tsx
const SpringTrail: React.FC<{
  items: React.ReactNode[];
  staggerFrames?: number;
  preset?: keyof typeof SPRING;
  direction?: 'up' | 'down' | 'left' | 'right';
}> = ({ items, staggerFrames = 4, preset = 'bouncy', direction = 'up' }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const getOffset = (progress: number) => {
    const distance = 50;
    const remaining = interpolate(progress, [0, 1], [distance, 0]);
    switch (direction) {
      case 'up': return { transform: `translateY(${remaining}px)` };
      case 'down': return { transform: `translateY(${-remaining}px)` };
      case 'left': return { transform: `translateX(${remaining}px)` };
      case 'right': return { transform: `translateX(${-remaining}px)` };
    }
  };

  return (
    <>
      {items.map((item, i) => {
        const delay = i * staggerFrames;
        const progress = spring({ frame, fps, delay, config: SPRING[preset] });
        return (
          <div key={i} style={{ opacity: progress, ...getOffset(progress) }}>
            {item}
          </div>
        );
      })}
    </>
  );
};

Character Trail (text animation)

字符轨迹(文本动画)

Manual character splitting with spring stagger. For advanced text splitting (mask reveals, line wrapping), use gsap-animation skill instead.
tsx
const CharacterTrail: React.FC<{
  text: string;
  staggerFrames?: number;
  preset?: keyof typeof SPRING;
  fontSize?: number;
}> = ({ text, staggerFrames = 2, preset = 'pop', fontSize = 80 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <div style={{ display: 'flex', justifyContent: 'center', overflow: 'hidden' }}>
      {text.split('').map((char, i) => {
        const delay = i * staggerFrames;
        const progress = spring({ frame, fps, delay, config: SPRING[preset] });
        const translateY = interpolate(progress, [0, 1], [fontSize, 0]);

        return (
          <span key={i} style={{
            display: 'inline-block',
            fontSize,
            fontWeight: 'bold',
            color: '#fff',
            opacity: progress,
            transform: `translateY(${translateY}px)`,
            whiteSpace: 'pre',
          }}>
            {char === ' ' ? '\u00A0' : char}
          </span>
        );
      })}
    </div>
  );
};
手动拆分字符并添加弹簧交错效果。如需高级文本拆分(遮罩显示、换行),请使用gsap-animation技能。
tsx
const CharacterTrail: React.FC<{
  text: string;
  staggerFrames?: number;
  preset?: keyof typeof SPRING;
  fontSize?: number;
}> = ({ text, staggerFrames = 2, preset = 'pop', fontSize = 80 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <div style={{ display: 'flex', justifyContent: 'center', overflow: 'hidden' }}>
      {text.split('').map((char, i) => {
        const delay = i * staggerFrames;
        const progress = spring({ frame, fps, delay, config: SPRING[preset] });
        const translateY = interpolate(progress, [0, 1], [fontSize, 0]);

        return (
          <span key={i} style={{
            display: 'inline-block',
            fontSize,
            fontWeight: 'bold',
            color: '#fff',
            opacity: progress,
            transform: `translateY(${translateY}px)`,
            whiteSpace: 'pre',
          }}>
            {char === ' ' ? '\u00A0' : char}
          </span>
        );
      })}
    </div>
  );
};

Word Trail

单词轨迹

tsx
const WordTrail: React.FC<{
  text: string;
  staggerFrames?: number;
  preset?: keyof typeof SPRING;
  fontSize?: number;
}> = ({ text, staggerFrames = 5, preset = 'bouncy', fontSize = 64 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const words = text.split(' ');

  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3em', justifyContent: 'center' }}>
      {words.map((word, i) => {
        const delay = i * staggerFrames;
        const progress = spring({ frame, fps, delay, config: SPRING[preset] });
        const scale = spring({ frame, fps, delay, config: SPRING.pop });

        return (
          <span key={i} style={{
            display: 'inline-block',
            fontSize,
            fontWeight: 'bold',
            color: '#fff',
            opacity: progress,
            transform: `scale(${scale})`,
          }}>
            {word}
          </span>
        );
      })}
    </div>
  );
};
tsx
const WordTrail: React.FC<{
  text: string;
  staggerFrames?: number;
  preset?: keyof typeof SPRING;
  fontSize?: number;
}> = ({ text, staggerFrames = 5, preset = 'bouncy', fontSize = 64 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const words = text.split(' ');

  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3em', justifyContent: 'center' }}>
      {words.map((word, i) => {
        const delay = i * staggerFrames;
        const progress = spring({ frame, fps, delay, config: SPRING[preset] });
        const scale = spring({ frame, fps, delay, config: SPRING.pop });

        return (
          <span key={i} style={{
            display: 'inline-block',
            fontSize,
            fontWeight: 'bold',
            color: '#fff',
            opacity: progress,
            transform: `scale(${scale})`,
          }}>
            {word}
          </span>
        );
      })}
    </div>
  );
};

Grid Stagger (center-out)

网格交错(从中心向外)

tsx
const GridStagger: React.FC<{
  items: React.ReactNode[];
  columns: number;
  cellSize?: number;
  gap?: number;
}> = ({ items, columns, cellSize = 120, gap = 16 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const rows = Math.ceil(items.length / columns);
  const centerCol = (columns - 1) / 2;
  const centerRow = (rows - 1) / 2;

  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: `repeat(${columns}, ${cellSize}px)`,
      gap,
    }}>
      {items.map((item, i) => {
        const col = i % columns;
        const row = Math.floor(i / columns);
        // Distance from center determines delay
        const dist = Math.sqrt((col - centerCol) ** 2 + (row - centerRow) ** 2);
        const delay = Math.round(dist * 4);
        const progress = spring({ frame, fps, delay, config: SPRING.pop });

        return (
          <div key={i} style={{
            width: cellSize, height: cellSize,
            opacity: progress,
            transform: `scale(${progress})`,
          }}>
            {item}
          </div>
        );
      })}
    </div>
  );
};

tsx
const GridStagger: React.FC<{
  items: React.ReactNode[];
  columns: number;
  cellSize?: number;
  gap?: number;
}> = ({ items, columns, cellSize = 120, gap = 16 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const rows = Math.ceil(items.length / columns);
  const centerCol = (columns - 1) / 2;
  const centerRow = (rows - 1) / 2;

  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: `repeat(${columns}, ${cellSize}px)`,
      gap,
    }}>
      {items.map((item, i) => {
        const col = i % columns;
        const row = Math.floor(i / columns);
        // 与中心的距离决定延迟时间
        const dist = Math.sqrt((col - centerCol) ** 2 + (row - centerRow) ** 2);
        const delay = Math.round(dist * 4);
        const progress = spring({ frame, fps, delay, config: SPRING.pop });

        return (
          <div key={i} style={{
            width: cellSize, height: cellSize,
            opacity: progress,
            transform: `scale(${progress})`,
          }}>
            {item}
          </div>
        );
      })}
    </div>
  );
};

3. Chain / Sequence Patterns

3. 链式/序列动画模式

Spring Chain (sequential animations)

弹簧链式动画(顺序触发)

Mimics React Spring's
useChain
-- animations trigger in sequence using
measureSpring
for timing.
tsx
import { spring, measureSpring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';

const SpringChain: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Step 1: Container scales in
  const step1Config = SPRING.bouncy;
  const step1 = spring({ frame, fps, config: step1Config });
  const step1Duration = measureSpring({ fps, config: step1Config });

  // Step 2: Title fades up (starts when step1 is 80% done)
  const step2Delay = Math.round(step1Duration * 0.8);
  const step2Config = SPRING.snappy;
  const step2 = spring({ frame, fps, delay: step2Delay, config: step2Config });
  const step2Duration = measureSpring({ fps, config: step2Config });

  // Step 3: Subtitle appears (starts when step2 finishes)
  const step3Delay = step2Delay + step2Duration;
  const step3 = spring({ frame, fps, delay: step3Delay, config: SPRING.gentle });

  return (
    <AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      {/* Container */}
      <div style={{
        transform: `scale(${step1})`,
        opacity: step1,
        background: '#1e293b',
        padding: 60,
        borderRadius: 24,
        textAlign: 'center',
      }}>
        {/* Title */}
        <h1 style={{
          fontSize: 72, fontWeight: 'bold', color: '#fff',
          opacity: step2,
          transform: `translateY(${interpolate(step2, [0, 1], [30, 0])}px)`,
        }}>Spring Chain</h1>
        {/* Subtitle */}
        <p style={{
          fontSize: 28, color: 'rgba(255,255,255,0.7)', marginTop: 16,
          opacity: step3,
          transform: `translateY(${interpolate(step3, [0, 1], [20, 0])}px)`,
        }}>Sequential spring orchestration</p>
      </div>
    </AbsoluteFill>
  );
};
模拟React Spring的
useChain
——使用
measureSpring
计算时间,按顺序触发动画。
tsx
import { spring, measureSpring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';

const SpringChain: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // 步骤1:容器缩放入场
  const step1Config = SPRING.bouncy;
  const step1 = spring({ frame, fps, config: step1Config });
  const step1Duration = measureSpring({ fps, config: step1Config });

  // 步骤2:标题淡入上移(在步骤1完成80%时开始)
  const step2Delay = Math.round(step1Duration * 0.8);
  const step2Config = SPRING.snappy;
  const step2 = spring({ frame, fps, delay: step2Delay, config: step2Config });
  const step2Duration = measureSpring({ fps, config: step2Config });

  // 步骤3:副标题显示(在步骤2完成时开始)
  const step3Delay = step2Delay + step2Duration;
  const step3 = spring({ frame, fps, delay: step3Delay, config: SPRING.gentle });

  return (
    <AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      {/* 容器 */}
      <div style={{
        transform: `scale(${step1})`,
        opacity: step1,
        background: '#1e293b',
        padding: 60,
        borderRadius: 24,
        textAlign: 'center',
      }}>
        {/* 标题 */}
        <h1 style={{
          fontSize: 72, fontWeight: 'bold', color: '#fff',
          opacity: step2,
          transform: `translateY(${interpolate(step2, [0, 1], [30, 0])}px)`,
        }}>Spring Chain</h1>
        {/* 副标题 */}
        <p style={{
          fontSize: 28, color: 'rgba(255,255,255,0.7)', marginTop: 16,
          opacity: step3,
          transform: `translateY(${interpolate(step3, [0, 1], [20, 0])}px)`,
        }}>Sequential spring orchestration</p>
      </div>
    </AbsoluteFill>
  );
};

useSpringChain Hook

useSpringChain Hook

Reusable hook for chaining multiple springs with overlap control.
tsx
import { spring, measureSpring, SpringConfig, useCurrentFrame, useVideoConfig } from 'remotion';

type ChainStep = {
  config: Partial<SpringConfig>;
  overlap?: number; // 0-1, how much to overlap with previous step (default 0)
};

function useSpringChain(steps: ChainStep[]) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  let currentDelay = 0;
  return steps.map((step, i) => {
    if (i > 0) {
      const prevDuration = measureSpring({ fps, config: steps[i - 1].config });
      const overlap = step.overlap ?? 0;
      currentDelay += Math.round(prevDuration * (1 - overlap));
    }
    return spring({ frame, fps, delay: currentDelay, config: step.config });
  });
}

// Usage:
const [container, title, subtitle, cta] = useSpringChain([
  { config: SPRING.bouncy },
  { config: SPRING.snappy, overlap: 0.2 },
  { config: SPRING.gentle, overlap: 0.3 },
  { config: SPRING.pop, overlap: 0.1 },
]);

用于链式多弹簧动画的可复用Hook,支持控制重叠程度。
tsx
import { spring, measureSpring, SpringConfig, useCurrentFrame, useVideoConfig } from 'remotion';

type ChainStep = {
  config: Partial<SpringConfig>;
  overlap?: number; // 0-1,与上一步动画的重叠比例(默认0)
};

function useSpringChain(steps: ChainStep[]) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  let currentDelay = 0;
  return steps.map((step, i) => {
    if (i > 0) {
      const prevDuration = measureSpring({ fps, config: steps[i - 1].config });
      const overlap = step.overlap ?? 0;
      currentDelay += Math.round(prevDuration * (1 - overlap));
    }
    return spring({ frame, fps, delay: currentDelay, config: step.config });
  });
}

// 使用示例:
const [container, title, subtitle, cta] = useSpringChain([
  { config: SPRING.bouncy },
  { config: SPRING.snappy, overlap: 0.2 },
  { config: SPRING.gentle, overlap: 0.3 },
  { config: SPRING.pop, overlap: 0.1 },
]);

4. Multi-Property Springs

4. 多属性弹簧动画

Different physics per property

为不同属性设置不同物理参数

tsx
const MultiPropertySpring: React.FC<{ delay?: number }> = ({ delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Position: snappy (arrives fast)
  const position = spring({ frame, fps, delay, config: SPRING.snappy });
  // Scale: bouncy (overshoots then settles)
  const scale = spring({ frame, fps, delay, config: SPRING.bouncy });
  // Rotation: wobbly (elastic wobble)
  const rotation = spring({ frame, fps, delay, config: SPRING.wobbly });
  // Opacity: smooth (no bounce)
  const opacity = spring({ frame, fps, delay, config: SPRING.smooth });

  const translateX = interpolate(position, [0, 1], [-300, 0]);
  const rotate = interpolate(rotation, [0, 1], [-15, 0]);

  return (
    <div style={{
      transform: `translateX(${translateX}px) scale(${scale}) rotate(${rotate}deg)`,
      opacity,
    }}>
      Multi-Property
    </div>
  );
};

tsx
const MultiPropertySpring: React.FC<{ delay?: number }> = ({ delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // 位置:快速响应(快速到达)
  const position = spring({ frame, fps, delay, config: SPRING.snappy });
  // 缩放:弹跳感(过冲后稳定)
  const scale = spring({ frame, fps, delay, config: SPRING.bouncy });
  // 旋转:摇晃感(弹性晃动)
  const rotation = spring({ frame, fps, delay, config: SPRING.wobbly });
  // 透明度:平滑无弹跳
  const opacity = spring({ frame, fps, delay, config: SPRING.smooth });

  const translateX = interpolate(position, [0, 1], [-300, 0]);
  const rotate = interpolate(rotation, [0, 1], [-15, 0]);

  return (
    <div style={{
      transform: `translateX(${translateX}px) scale(${scale}) rotate(${rotate}deg)`,
      opacity,
    }}>
      Multi-Property
    </div>
  );
};

5. Spring Counter

5. 弹簧计数器

Number counter with spring physics -- overshoots the target then settles.
tsx
const SpringCounter: React.FC<{
  endValue: number;
  prefix?: string;
  suffix?: string;
  preset?: keyof typeof SPRING;
  delay?: number;
}> = ({ endValue, prefix = '', suffix = '', preset = 'bouncy', delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({ frame, fps, delay, config: SPRING[preset] });
  // With bouncy config, progress overshoots past 1.0 before settling
  // This means the counter briefly shows a number > endValue, then settles
  const value = Math.round(progress * endValue);

  return (
    <div style={{
      fontSize: 96, fontWeight: 'bold', color: '#fff',
      fontVariantNumeric: 'tabular-nums',
    }}>
      {prefix}{value.toLocaleString()}{suffix}
    </div>
  );
};
Comparison with linear counter:
  • interpolate()
    counter: smoothly reaches exact target, no overshoot
  • Spring counter: overshoots then settles -- feels more energetic and alive

带有弹簧物理效果的数字计数器——先过冲目标值再稳定。
tsx
const SpringCounter: React.FC<{
  endValue: number;
  prefix?: string;
  suffix?: string;
  preset?: keyof typeof SPRING;
  delay?: number;
}> = ({ endValue, prefix = '', suffix = '', preset = 'bouncy', delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({ frame, fps, delay, config: SPRING[preset] });
  // 使用bouncy配置时,进度会超过1.0后再稳定
  // 这意味着计数器会短暂显示大于目标值的数字,然后稳定
  const value = Math.round(progress * endValue);

  return (
    <div style={{
      fontSize: 96, fontWeight: 'bold', color: '#fff',
      fontVariantNumeric: 'tabular-nums',
    }}>
      {prefix}{value.toLocaleString()}{suffix}
    </div>
  );
};
与线性计数器的对比:
  • interpolate()
    计数器:平滑到达精确目标值,无过冲
  • 弹簧计数器:先过冲再稳定——更具活力和生动感

6. 3D Transform Patterns

6. 3D变换模式

Spring Card Flip

弹簧卡片翻转

tsx
const SpringCardFlip: React.FC<{
  frontContent: React.ReactNode;
  backContent: React.ReactNode;
  flipDelay?: number;
}> = ({ frontContent, backContent, flipDelay = 15 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const flipProgress = spring({
    frame, fps, delay: flipDelay,
    config: { damping: 15, stiffness: 80 }, // slow, weighty flip
  });
  const rotateY = interpolate(flipProgress, [0, 1], [0, 180]);

  return (
    <AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ perspective: 800 }}>
        <div style={{
          width: 500, height: 320, position: 'relative',
          transformStyle: 'preserve-3d',
          transform: `rotateY(${rotateY}deg)`,
        }}>
          <div style={{
            position: 'absolute', inset: 0, backfaceVisibility: 'hidden',
            background: '#1e293b', borderRadius: 16,
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32,
          }}>{frontContent}</div>
          <div style={{
            position: 'absolute', inset: 0, backfaceVisibility: 'hidden',
            background: '#3b82f6', borderRadius: 16, transform: 'rotateY(180deg)',
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32,
          }}>{backContent}</div>
        </div>
      </div>
    </AbsoluteFill>
  );
};
tsx
const SpringCardFlip: React.FC<{
  frontContent: React.ReactNode;
  backContent: React.ReactNode;
  flipDelay?: number;
}> = ({ frontContent, backContent, flipDelay = 15 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const flipProgress = spring({
    frame, fps, delay: flipDelay,
    config: { damping: 15, stiffness: 80 }, // 缓慢、有重量感的翻转
  });
  const rotateY = interpolate(flipProgress, [0, 1], [0, 180]);

  return (
    <AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ perspective: 800 }}>
        <div style={{
          width: 500, height: 320, position: 'relative',
          transformStyle: 'preserve-3d',
          transform: `rotateY(${rotateY}deg)`,
        }}>
          <div style={{
            position: 'absolute', inset: 0, backfaceVisibility: 'hidden',
            background: '#1e293b', borderRadius: 16,
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32,
          }}>{frontContent}</div>
          <div style={{
            position: 'absolute', inset: 0, backfaceVisibility: 'hidden',
            background: '#3b82f6', borderRadius: 16, transform: 'rotateY(180deg)',
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32,
          }}>{backContent}</div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

Perspective Tilt

透视倾斜

tsx
const PerspectiveTilt: React.FC<{
  children: React.ReactNode;
  rotateX?: number;
  rotateY?: number;
  delay?: number;
}> = ({ children, rotateX = -20, rotateY = 15, delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({ frame, fps, delay, config: SPRING.heavy });
  const rx = interpolate(progress, [0, 1], [rotateX, 0]);
  const ry = interpolate(progress, [0, 1], [rotateY, 0]);
  const translateZ = interpolate(progress, [0, 1], [-200, 0]);

  return (
    <div style={{
      perspective: 1000,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <div style={{
        transform: `perspective(1000px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(${translateZ}px)`,
        opacity: progress,
      }}>
        {children}
      </div>
    </div>
  );
};

tsx
const PerspectiveTilt: React.FC<{
  children: React.ReactNode;
  rotateX?: number;
  rotateY?: number;
  delay?: number;
}> = ({ children, rotateX = -20, rotateY = 15, delay = 0 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({ frame, fps, delay, config: SPRING.heavy });
  const rx = interpolate(progress, [0, 1], [rotateX, 0]);
  const ry = interpolate(progress, [0, 1], [rotateY, 0]);
  const translateZ = interpolate(progress, [0, 1], [-200, 0]);

  return (
    <div style={{
      perspective: 1000,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <div style={{
        transform: `perspective(1000px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(${translateZ}px)`,
        opacity: progress,
      }}>
        {children}
      </div>
    </div>
  );
};

7. Spring Transitions

7. 弹簧过渡动画

Crossfade with Spring

弹簧交叉淡入

tsx
const SpringCrossfade: React.FC<{
  outgoing: React.ReactNode;
  incoming: React.ReactNode;
  switchFrame: number;
}> = ({ outgoing, incoming, switchFrame }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const outOpacity = frame < switchFrame ? 1 : 1 - spring({
    frame: frame - switchFrame, fps, config: SPRING.smooth,
  });
  const inOpacity = frame < switchFrame ? 0 : spring({
    frame: frame - switchFrame, fps, config: SPRING.smooth,
  });
  const inScale = frame < switchFrame ? 0.95 : interpolate(
    spring({ frame: frame - switchFrame, fps, config: SPRING.bouncy }),
    [0, 1], [0.95, 1]
  );

  return (
    <AbsoluteFill>
      <AbsoluteFill style={{ opacity: outOpacity }}>{outgoing}</AbsoluteFill>
      <AbsoluteFill style={{ opacity: inOpacity, transform: `scale(${inScale})` }}>
        {incoming}
      </AbsoluteFill>
    </AbsoluteFill>
  );
};
tsx
const SpringCrossfade: React.FC<{
  outgoing: React.ReactNode;
  incoming: React.ReactNode;
  switchFrame: number;
}> = ({ outgoing, incoming, switchFrame }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const outOpacity = frame < switchFrame ? 1 : 1 - spring({
    frame: frame - switchFrame, fps, config: SPRING.smooth,
  });
  const inOpacity = frame < switchFrame ? 0 : spring({
    frame: frame - switchFrame, fps, config: SPRING.smooth,
  });
  const inScale = frame < switchFrame ? 0.95 : interpolate(
    spring({ frame: frame - switchFrame, fps, config: SPRING.bouncy }),
    [0, 1], [0.95, 1]
  );

  return (
    <AbsoluteFill>
      <AbsoluteFill style={{ opacity: outOpacity }}>{outgoing}</AbsoluteFill>
      <AbsoluteFill style={{ opacity: inOpacity, transform: `scale(${inScale})` }}>
        {incoming}
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

Slide Transition with Spring

弹簧滑动过渡

tsx
const SpringSlide: React.FC<{
  outgoing: React.ReactNode;
  incoming: React.ReactNode;
  switchFrame: number;
  direction?: 'left' | 'right' | 'up' | 'down';
}> = ({ outgoing, incoming, switchFrame, direction = 'left' }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = frame < switchFrame ? 0 : spring({
    frame: frame - switchFrame, fps, config: SPRING.snappy,
  });

  const getTransform = (isOutgoing: boolean) => {
    const offset = isOutgoing ? interpolate(progress, [0, 1], [0, -100]) : interpolate(progress, [0, 1], [100, 0]);
    switch (direction) {
      case 'left': return `translateX(${offset}%)`;
      case 'right': return `translateX(${-offset}%)`;
      case 'up': return `translateY(${offset}%)`;
      case 'down': return `translateY(${-offset}%)`;
    }
  };

  return (
    <AbsoluteFill style={{ overflow: 'hidden' }}>
      <AbsoluteFill style={{ transform: getTransform(true) }}>{outgoing}</AbsoluteFill>
      <AbsoluteFill style={{ transform: getTransform(false) }}>{incoming}</AbsoluteFill>
    </AbsoluteFill>
  );
};

tsx
const SpringSlide: React.FC<{
  outgoing: React.ReactNode;
  incoming: React.ReactNode;
  switchFrame: number;
  direction?: 'left' | 'right' | 'up' | 'down';
}> = ({ outgoing, incoming, switchFrame, direction = 'left' }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = frame < switchFrame ? 0 : spring({
    frame: frame - switchFrame, fps, config: SPRING.snappy,
  });

  const getTransform = (isOutgoing: boolean) => {
    const offset = isOutgoing ? interpolate(progress, [0, 1], [0, -100]) : interpolate(progress, [0, 1], [100, 0]);
    switch (direction) {
      case 'left': return `translateX(${offset}%)`;
      case 'right': return `translateX(${-offset}%)`;
      case 'up': return `translateY(${offset}%)`;
      case 'down': return `translateY(${-offset}%)`;
    }
  };

  return (
    <AbsoluteFill style={{ overflow: 'hidden' }}>
      <AbsoluteFill style={{ transform: getTransform(true) }}>{outgoing}</AbsoluteFill>
      <AbsoluteFill style={{ transform: getTransform(false) }}>{incoming}</AbsoluteFill>
    </AbsoluteFill>
  );
};

8. Templates

8. 模板

Spring Title Card

弹簧标题卡片

tsx
const SpringTitleCard: React.FC<{
  title: string;
  subtitle?: string;
  accent?: string;
}> = ({ title, subtitle, accent = '#3b82f6' }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Background shape
  const bgScale = spring({ frame, fps, config: SPRING.heavy });
  // Title words stagger
  const words = title.split(' ');
  // Divider
  const dividerWidth = spring({ frame, fps, delay: 8, config: SPRING.snappy });
  // Subtitle
  const subtitleProgress = spring({ frame, fps, delay: 15, config: SPRING.gentle });

  return (
    <AbsoluteFill style={{
      background: '#0f172a',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      {/* Accent circle */}
      <div style={{
        position: 'absolute', width: 300, height: 300, borderRadius: '50%',
        background: `${accent}20`, transform: `scale(${bgScale})`,
      }} />

      <div style={{ textAlign: 'center', position: 'relative', zIndex: 1 }}>
        {/* Title with word stagger */}
        <div style={{ display: 'flex', gap: '0.3em', justifyContent: 'center', flexWrap: 'wrap' }}>
          {words.map((word, i) => {
            const delay = i * 4;
            const progress = spring({ frame, fps, delay, config: SPRING.pop });
            const y = interpolate(progress, [0, 1], [40, 0]);
            return (
              <span key={i} style={{
                fontSize: 80, fontWeight: 'bold', color: '#fff',
                display: 'inline-block',
                opacity: progress,
                transform: `translateY(${y}px) scale(${progress})`,
              }}>{word}</span>
            );
          })}
        </div>

        {/* Divider */}
        <div style={{
          width: 80, height: 3, background: accent, margin: '20px auto',
          transform: `scaleX(${dividerWidth})`, transformOrigin: 'center',
        }} />

        {/* Subtitle */}
        {subtitle && (
          <p style={{
            fontSize: 28, color: 'rgba(255,255,255,0.7)',
            opacity: subtitleProgress,
            transform: `translateY(${interpolate(subtitleProgress, [0, 1], [15, 0])}px)`,
          }}>{subtitle}</p>
        )}
      </div>
    </AbsoluteFill>
  );
};
tsx
const SpringTitleCard: React.FC<{
  title: string;
  subtitle?: string;
  accent?: string;
}> = ({ title, subtitle, accent = '#3b82f6' }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // 背景形状
  const bgScale = spring({ frame, fps, config: SPRING.heavy });
  // 标题单词交错
  const words = title.split(' ');
  // 分隔线
  const dividerWidth = spring({ frame, fps, delay: 8, config: SPRING.snappy });
  // 副标题
  const subtitleProgress = spring({ frame, fps, delay: 15, config: SPRING.gentle });

  return (
    <AbsoluteFill style={{
      background: '#0f172a',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      {/* 强调圆 */}
      <div style={{
        position: 'absolute', width: 300, height: 300, borderRadius: '50%',
        background: `${accent}20`, transform: `scale(${bgScale})`,
      }} />

      <div style={{ textAlign: 'center', position: 'relative', zIndex: 1 }}>
        {/* 单词交错的标题 */}
        <div style={{ display: 'flex', gap: '0.3em', justifyContent: 'center', flexWrap: 'wrap' }}>
          {words.map((word, i) => {
            const delay = i * 4;
            const progress = spring({ frame, fps, delay, config: SPRING.pop });
            const y = interpolate(progress, [0, 1], [40, 0]);
            return (
              <span key={i} style={{
                fontSize: 80, fontWeight: 'bold', color: '#fff',
                display: 'inline-block',
                opacity: progress,
                transform: `translateY(${y}px) scale(${progress})`,
              }}>{word}</span>
            );
          })}
        </div>

        {/* 分隔线 */}
        <div style={{
          width: 80, height: 3, background: accent, margin: '20px auto',
          transform: `scaleX(${dividerWidth})`, transformOrigin: 'center',
        }} />

        {/* 副标题 */}
        {subtitle && (
          <p style={{
            fontSize: 28, color: 'rgba(255,255,255,0.7)',
            opacity: subtitleProgress,
            transform: `translateY(${interpolate(subtitleProgress, [0, 1], [15, 0])}px)`,
          }}>{subtitle}</p>
        )}
      </div>
    </AbsoluteFill>
  );
};

Spring Lower Third

弹簧下三分之一栏

tsx
const SpringLowerThird: React.FC<{
  name: string;
  title: string;
  accent?: string;
  hold?: number;
}> = ({ name, title, accent = '#3b82f6', hold = 90 }) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  // Enter
  const barIn = spring({ frame, fps, config: SPRING.snappy });
  const nameIn = spring({ frame, fps, delay: 6, config: SPRING.bouncy });
  const titleIn = spring({ frame, fps, delay: 10, config: SPRING.gentle });

  // Exit (spring math subtraction)
  const exitDelay = durationInFrames - 20;
  const barOut = spring({ frame, fps, delay: exitDelay, config: SPRING.stiff });
  const nameOut = spring({ frame, fps, delay: exitDelay - 4, config: SPRING.stiff });
  const titleOut = spring({ frame, fps, delay: exitDelay - 8, config: SPRING.stiff });

  return (
    <AbsoluteFill>
      <div style={{ position: 'absolute', bottom: 80, left: 60 }}>
        {/* Bar */}
        <div style={{
          background: accent, padding: '12px 24px', borderRadius: 4,
          transform: `scaleX(${barIn - barOut})`,
          transformOrigin: 'left',
          opacity: barIn - barOut,
        }}>
          <div style={{
            fontSize: 28, fontWeight: 'bold', color: '#fff',
            opacity: nameIn - nameOut,
            transform: `translateX(${interpolate(nameIn - nameOut, [0, 1], [-20, 0])}px)`,
          }}>{name}</div>
          <div style={{
            fontSize: 18, color: 'rgba(255,255,255,0.8)',
            opacity: titleIn - titleOut,
            transform: `translateX(${interpolate(titleIn - titleOut, [0, 1], [-15, 0])}px)`,
          }}>{title}</div>
        </div>
      </div>
    </AbsoluteFill>
  );
};
tsx
const SpringLowerThird: React.FC<{
  name: string;
  title: string;
  accent?: string;
  hold?: number;
}> = ({ name, title, accent = '#3b82f6', hold = 90 }) => {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  // 入场
  const barIn = spring({ frame, fps, config: SPRING.snappy });
  const nameIn = spring({ frame, fps, delay: 6, config: SPRING.bouncy });
  const titleIn = spring({ frame, fps, delay: 10, config: SPRING.gentle });

  // 退场(弹簧算法减法)
  const exitDelay = durationInFrames - 20;
  const barOut = spring({ frame, fps, delay: exitDelay, config: SPRING.stiff });
  const nameOut = spring({ frame, fps, delay: exitDelay - 4, config: SPRING.stiff });
  const titleOut = spring({ frame, fps, delay: exitDelay - 8, config: SPRING.stiff });

  return (
    <AbsoluteFill>
      <div style={{ position: 'absolute', bottom: 80, left: 60 }}>
        {/* 栏 */}
        <div style={{
          background: accent, padding: '12px 24px', borderRadius: 4,
          transform: `scaleX(${barIn - barOut})`,
          transformOrigin: 'left',
          opacity: barIn - barOut,
        }}>
          <div style={{
            fontSize: 28, fontWeight: 'bold', color: '#fff',
            opacity: nameIn - nameOut,
            transform: `translateX(${interpolate(nameIn - nameOut, [0, 1], [-20, 0])}px)`,
          }}>{name}</div>
          <div style={{
            fontSize: 18, color: 'rgba(255,255,255,0.8)',
            opacity: titleIn - titleOut,
            transform: `translateX(${interpolate(titleIn - titleOut, [0, 1], [-15, 0])}px)`,
          }}>{title}</div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

Spring Feature Grid

弹簧功能网格

tsx
const SpringFeatureGrid: React.FC<{
  features: Array<{ icon: string; label: string }>;
  columns?: number;
}> = ({ features, columns = 3 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill style={{
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: '#0f172a',
    }}>
      <div style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${columns}, 200px)`,
        gap: 32,
      }}>
        {features.map(({ icon, label }, i) => {
          const delay = i * 5;
          const scale = spring({ frame, fps, delay, config: SPRING.pop });
          const opacity = spring({ frame, fps, delay, config: SPRING.smooth });

          return (
            <div key={i} style={{
              textAlign: 'center', padding: 24,
              background: 'rgba(255,255,255,0.05)', borderRadius: 16,
              transform: `scale(${scale})`, opacity,
            }}>
              <div style={{ fontSize: 48 }}>{icon}</div>
              <div style={{ fontSize: 18, color: '#fff', marginTop: 12 }}>{label}</div>
            </div>
          );
        })}
      </div>
    </AbsoluteFill>
  );
};
tsx
const SpringFeatureGrid: React.FC<{
  features: Array<{ icon: string; label: string }>;
  columns?: number;
}> = ({ features, columns = 3 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill style={{
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: '#0f172a',
    }}>
      <div style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${columns}, 200px)`,
        gap: 32,
      }}>
        {features.map(({ icon, label }, i) => {
          const delay = i * 5;
          const scale = spring({ frame, fps, delay, config: SPRING.pop });
          const opacity = spring({ frame, fps, delay, config: SPRING.smooth });

          return (
            <div key={i} style={{
              textAlign: 'center', padding: 24,
              background: 'rgba(255,255,255,0.05)', borderRadius: 16,
              transform: `scale(${scale})`, opacity,
            }}>
              <div style={{ fontSize: 48 }}>{icon}</div>
              <div style={{ fontSize: 18, color: '#fff', marginTop: 12 }}>{label}</div>
            </div>
          );
        })}
      </div>
    </AbsoluteFill>
  );
};

Spring Outro

弹簧结尾画面

tsx
const SpringOutro: React.FC<{
  headline: string;
  tagline?: string;
  ctaText?: string;
  accent?: string;
}> = ({ headline, tagline, ctaText, accent = '#3b82f6' }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const headlineProgress = spring({ frame, fps, config: SPRING.heavy });
  const taglineProgress = spring({ frame, fps, delay: 12, config: SPRING.gentle });
  const ctaProgress = spring({ frame, fps, delay: 20, config: SPRING.pop });

  return (
    <AbsoluteFill style={{
      background: '#0f172a',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <div style={{ textAlign: 'center' }}>
        <h1 style={{
          fontSize: 72, fontWeight: 'bold', color: '#fff',
          opacity: headlineProgress,
          transform: `scale(${headlineProgress})`,
        }}>{headline}</h1>

        {tagline && (
          <p style={{
            fontSize: 28, color: 'rgba(255,255,255,0.6)', marginTop: 16,
            opacity: taglineProgress,
            transform: `translateY(${interpolate(taglineProgress, [0, 1], [15, 0])}px)`,
          }}>{tagline}</p>
        )}

        {ctaText && (
          <div style={{
            display: 'inline-block', marginTop: 32,
            background: accent, padding: '16px 40px', borderRadius: 8,
            fontSize: 24, fontWeight: 'bold', color: '#fff',
            transform: `scale(${ctaProgress})`, opacity: ctaProgress,
          }}>{ctaText}</div>
        )}
      </div>
    </AbsoluteFill>
  );
};

tsx
const SpringOutro: React.FC<{
  headline: string;
  tagline?: string;
  ctaText?: string;
  accent?: string;
}> = ({ headline, tagline, ctaText, accent = '#3b82f6' }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const headlineProgress = spring({ frame, fps, config: SPRING.heavy });
  const taglineProgress = spring({ frame, fps, delay: 12, config: SPRING.gentle });
  const ctaProgress = spring({ frame, fps, delay: 20, config: SPRING.pop });

  return (
    <AbsoluteFill style={{
      background: '#0f172a',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <div style={{ textAlign: 'center' }}>
        <h1 style={{
          fontSize: 72, fontWeight: 'bold', color: '#fff',
          opacity: headlineProgress,
          transform: `scale(${headlineProgress})`,
        }}>{headline}</h1>

        {tagline && (
          <p style={{
            fontSize: 28, color: 'rgba(255,255,255,0.6)', marginTop: 16,
            opacity: taglineProgress,
            transform: `translateY(${interpolate(taglineProgress, [0, 1], [15, 0])}px)`,
          }}>{tagline}</p>
        )}

        {ctaText && (
          <div style={{
            display: 'inline-block', marginTop: 32,
            background: accent, padding: '16px 40px', borderRadius: 8,
            fontSize: 24, fontWeight: 'bold', color: '#fff',
            transform: `scale(${ctaProgress})`, opacity: ctaProgress,
          }}>{ctaText}</div>
        )}
      </div>
    </AbsoluteFill>
  );
};

9. Utility: useSpringTrail

9. 工具函数:useSpringTrail

Reusable hook for trail animations.
tsx
import { spring, SpringConfig, useCurrentFrame, useVideoConfig } from 'remotion';

function useSpringTrail(
  count: number,
  config: Partial<SpringConfig>,
  staggerFrames = 4,
  baseDelay = 0,
) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return Array.from({ length: count }, (_, i) => {
    const delay = baseDelay + i * staggerFrames;
    return spring({ frame, fps, delay, config });
  });
}

// Usage:
const trail = useSpringTrail(5, SPRING.pop, 4);
// trail = [0.98, 0.85, 0.5, 0.1, 0] -- each item at different progress
用于轨迹动画的可复用Hook。
tsx
import { spring, SpringConfig, useCurrentFrame, useVideoConfig } from 'remotion';

function useSpringTrail(
  count: number,
  config: Partial<SpringConfig>,
  staggerFrames = 4,
  baseDelay = 0,
) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return Array.from({ length: count }, (_, i) => {
    const delay = baseDelay + i * staggerFrames;
    return spring({ frame, fps, delay, config });
  });
}

// 使用示例:
const trail = useSpringTrail(5, SPRING.pop, 4);
// trail = [0.98, 0.85, 0.5, 0.1, 0] -- 每个元素处于不同的动画进度

10. Utility: useSpringEnterExit

10. 工具函数:useSpringEnterExit

Reusable hook for enter + exit pattern.
tsx
function useSpringEnterExit(
  enterConfig: Partial<SpringConfig>,
  exitConfig: Partial<SpringConfig>,
  enterDelay = 0,
  exitBeforeEnd = 30,
) {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  const enter = spring({ frame, fps, delay: enterDelay, config: enterConfig });
  const exit = spring({
    frame, fps,
    delay: durationInFrames - exitBeforeEnd,
    config: exitConfig,
  });

  return enter - exit;
}

// Usage:
const progress = useSpringEnterExit(SPRING.bouncy, SPRING.stiff, 0, 25);

用于入场+退场模式的可复用Hook。
tsx
function useSpringEnterExit(
  enterConfig: Partial<SpringConfig>,
  exitConfig: Partial<SpringConfig>,
  enterDelay = 0,
  exitBeforeEnd = 30,
) {
  const frame = useCurrentFrame();
  const { fps, durationInFrames } = useVideoConfig();

  const enter = spring({ frame, fps, delay: enterDelay, config: enterConfig });
  const exit = spring({
    frame, fps,
    delay: durationInFrames - exitBeforeEnd,
    config: exitConfig,
  });

  return enter - exit;
}

// 使用示例:
const progress = useSpringEnterExit(SPRING.bouncy, SPRING.stiff, 0, 25);

11. Combining with Other Skills

11. 与其他技能结合使用

tsx
const CombinedScene: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const bgOpacity = spring({ frame, fps, config: SPRING.smooth });

  return (
    <AbsoluteFill>
      {/* react-animation: visual atmosphere */}
      <div style={{ opacity: bgOpacity }}>
        <Aurora colorStops={['#3A29FF', '#FF94B4']} />
      </div>

      {/* spring-animation: bouncy title entrance */}
      <SpringTitleCard title="Natural Motion" subtitle="Physics-driven beauty" />

      {/* gsap-animation: text splitting that spring can't do */}
      <GSAPTextReveal text="Advanced Typography" />
    </AbsoluteFill>
  );
};
SkillBest For
spring-animationBouncy entrances, elastic trails, organic physics, overshoot effects, spring counters
gsap-animationText splitting (SplitText), SVG drawing (DrawSVG), SVG morphing, complex timeline labels
react-animationVisual backgrounds (Aurora, Silk, Particles), shader effects

tsx
const CombinedScene: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const bgOpacity = spring({ frame, fps, config: SPRING.smooth });

  return (
    <AbsoluteFill>
      {/* react-animation:视觉氛围 */}
      <div style={{ opacity: bgOpacity }}>
        <Aurora colorStops={['#3A29FF', '#FF94B4']} />
      </div>

      {/* spring-animation:弹跳标题入场 */}
      <SpringTitleCard title="Natural Motion" subtitle="Physics-driven beauty" />

      {/* gsap-animation:弹簧无法实现的文本拆分 */}
      <GSAPTextReveal text="Advanced Typography" />
    </AbsoluteFill>
  );
};
技能最佳适用场景
spring-animation弹跳入场、弹性轨迹、有机物理效果、过冲特效、弹簧计数器
gsap-animation文本拆分(SplitText)、SVG绘制(DrawSVG)、SVG变形、复杂时间轴标签
react-animation视觉背景(Aurora、Silk、Particles)、着色器特效

12. Composition Registration

12. 合成内容注册

tsx
export const RemotionRoot: React.FC = () => (
  <>
    <Composition id="SpringTitleCard" component={SpringTitleCard}
      durationInFrames={90} fps={30} width={1920} height={1080}
      defaultProps={{ title: 'SPRING PHYSICS', subtitle: 'Natural motion for video' }} />
    <Composition id="SpringLowerThird" component={SpringLowerThird}
      durationInFrames={180} fps={30} width={1920} height={1080}
      defaultProps={{ name: 'Jane Smith', title: 'Creative Director' }} />
    <Composition id="SpringFeatureGrid" component={SpringFeatureGrid}
      durationInFrames={90} fps={30} width={1920} height={1080}
      defaultProps={{ features: [
        { icon: '🚀', label: 'Fast' },
        { icon: '🎯', label: 'Precise' },
        { icon: '✨', label: 'Beautiful' },
      ]}} />
    <Composition id="SpringOutro" component={SpringOutro}
      durationInFrames={120} fps={30} width={1920} height={1080}
      defaultProps={{ headline: 'GET STARTED', tagline: 'Try it free today', ctaText: 'Sign Up →' }} />
  </>
);

tsx
export const RemotionRoot: React.FC = () => (
  <>
    <Composition id="SpringTitleCard" component={SpringTitleCard}
      durationInFrames={90} fps={30} width={1920} height={1080}
      defaultProps={{ title: 'SPRING PHYSICS', subtitle: 'Natural motion for video' }} />
    <Composition id="SpringLowerThird" component={SpringLowerThird}
      durationInFrames={180} fps={30} width={1920} height={1080}
      defaultProps={{ name: 'Jane Smith', title: 'Creative Director' }} />
    <Composition id="SpringFeatureGrid" component={SpringFeatureGrid}
      durationInFrames={90} fps={30} width={1920} height={1080}
      defaultProps={{ features: [
        { icon: '🚀', label: 'Fast' },
        { icon: '🎯', label: 'Precise' },
        { icon: '✨', label: 'Beautiful' },
      ]}} />
    <Composition id="SpringOutro" component={SpringOutro}
      durationInFrames={120} fps={30} width={1920} height={1080}
      defaultProps={{ headline: 'GET STARTED', tagline: 'Try it free today', ctaText: 'Sign Up →' }} />
  </>
);

13. Rendering

13. 渲染

bash
undefined
bash
undefined

Default MP4

默认MP4格式

npx remotion render src/index.ts SpringTitleCard --output out/title.mp4
npx remotion render src/index.ts SpringTitleCard --output out/title.mp4

High quality

高质量渲染

npx remotion render src/index.ts SpringTitleCard --codec h264 --crf 15
npx remotion render src/index.ts SpringTitleCard --codec h264 --crf 15

GIF

GIF格式

npx remotion render src/index.ts SpringTitleCard --codec gif --every-nth-frame 2
npx remotion render src/index.ts SpringTitleCard --codec gif --every-nth-frame 2

ProRes for editing

用于编辑的ProRes格式

npx remotion render src/index.ts SpringTitleCard --codec prores --prores-profile 4444
undefined
npx remotion render src/index.ts SpringTitleCard --codec prores --prores-profile 4444
undefined