spring-animation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhen 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 when:
interpolate()- 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: is NOT compatible with Remotion (it uses requestAnimationFrame internally). This skill uses Remotion's native function which provides the same physics model in a frame-deterministic way.
@react-spring/webspring()当你创建需要弹簧物理效果的Remotion视频合成内容时,可以使用该技能——弹簧物理效果能实现带有弹跳、过冲和弹性稳定的自然有机运动。
适合使用弹簧效果的场景:
- 弹性/弹跳式入场动画(过冲后稳定)
- 自然减速运动(非线性、非缓动——基于物理模型)
- 每个元素都带有弹簧物理的交错轨迹
- 先过冲后稳定的数字计数器
- 带有自然重量和惯性的缩放/旋转动画
- 使用弹簧算法的入场+退场动画()
in - out - 为每个属性配置不同弹簧参数的多属性协同动画
适合使用Remotion原生的场景:
interpolate()- 无弹跳的线性或缓动运动(淡入、滑动、擦除)
- 需要精确时间控制(必须在第N帧精确结束)
- 裁剪路径动画
- 进度条/确定性计数器
适合使用GSAP(gsap-animation技能)的场景:
- 文本拆分(SplitText:按字符/单词/行拆分并添加遮罩)
- SVG描边绘制(DrawSVG)
- SVG变形(MorphSVG)
- 带有标签和位置参数的复杂时间轴编排
- 乱码文本解码效果
- 可复用的注册式特效
注意: 与Remotion不兼容(它内部使用requestAnimationFrame)。本技能使用Remotion原生的函数,该函数以帧确定性的方式提供相同的物理模型。
@react-spring/webspring()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
配置参数
| Parameter | Range | Default | Effect |
|---|---|---|---|
| 1-200 | 10 | Resistance. Low = bouncy, high = smooth |
| 1-200 | 100 | Snap speed. High = fast, low = slow |
| 0.1-5 | 1 | Weight/inertia. High = sluggish, low = light |
| bool | false | Clamp at target (no overshoot) |
| 参数 | 范围 | 默认值 | 作用 |
|---|---|---|---|
| 1-200 | 10 | 阻力:值越低,弹跳越强;值越高,运动越平滑 |
| 1-200 | 100 | 响应速度:值越高,运动越快;值越低,运动越慢 |
| 0.1-5 | 1 | 重量/惯性:值越高,运动越迟缓;值越低,运动越轻盈 |
| 布尔值 | false | 限制在目标值(禁止过冲) |
Additional Options
额外选项
| Option | Type | Effect |
|---|---|---|
| number | Delay start by N frames (returns 0 until delay elapses) |
| number | Force spring to settle within N frames |
| bool | Animate from 1 to 0 |
| number | Starting value (default 0) |
| number | Ending value (default 1) |
| 选项 | 类型 | 作用 |
|---|---|---|
| 数字 | 延迟N帧后开始动画(延迟期间返回0) |
| 数字 | 强制弹簧动画在N帧内完成稳定 |
| 布尔值 | 从1到0反向动画 |
| 数字 | 起始值(默认0) |
| 数字 | 结束值(默认1) |
measureSpring()
measureSpring()
Calculate how many frames a spring config takes to settle. Essential for and composition duration.
<Sequence>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
预设视觉参考
| Preset | Bounce | Speed | Feel | Best For |
|---|---|---|---|---|
| None | Medium | Butter | Background, subtle reveals |
| Minimal | Fast | Crisp | UI elements, buttons |
| Strong | Medium | Playful | Titles, icons, attention |
| Small | Slow | Weighty | Dramatic reveals, large objects |
| Extreme | Medium | Cartoon | Playful, humorous |
| Tiny | Very fast | Mechanical | Data viz, precise motion |
| Minimal | Slow | Dreamy | Luxury, calm, organic |
| Almost none | Very slow | Heavy | Cinematic, suspense |
| Strong | Fast | Punchy | Scale-in, badge, icon pop |
| Extreme | Fast | Elastic | Exaggerated, cartoon, fun |
| 预设 | 弹跳程度 | 速度 | 质感 | 最佳适用场景 |
|---|---|---|---|---|
| 无 | 中等 | 丝滑 | 背景、微妙显示效果 |
| 轻微 | 快速 | 利落 | UI元素、按钮 |
| 强烈 | 中等 | 活泼 | 标题、图标、吸引注意力 |
| 轻微 | 缓慢 | 沉重 | 戏剧性显示效果、大型物体 |
| 极强 | 中等 | 卡通 | 活泼、幽默风格 |
| 极微 | 极快 | 机械感 | 数据可视化、精确运动 |
| 轻微 | 缓慢 | 梦幻 | 高端风格、平静、自然 |
| 几乎无 | 极慢 | 沉重 | 电影感、悬疑风格 |
| 强烈 | 快速 | 有力 | 缩放入场、徽章、图标弹出 |
| 极强 | 快速 | 弹性 | 夸张、卡通、趣味风格 |
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 -- each element enters with a frame delay.
useTrailtsx
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的——每个元素按帧延迟入场。
useTrailtsx
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 -- animations trigger in sequence using for timing.
useChainmeasureSpringtsx
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的——使用计算时间,按顺序触发动画。
useChainmeasureSpringtsx
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:
- counter: smoothly reaches exact target, no overshoot
interpolate() - 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>
);
};| Skill | Best For |
|---|---|
| spring-animation | Bouncy entrances, elastic trails, organic physics, overshoot effects, spring counters |
| gsap-animation | Text splitting (SplitText), SVG drawing (DrawSVG), SVG morphing, complex timeline labels |
| react-animation | Visual 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
undefinedbash
undefinedDefault 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
undefinednpx remotion render src/index.ts SpringTitleCard --codec prores --prores-profile 4444
undefined