关于 remotion-keyframes
remotion-keyframes 是用于 Remotion 动画的技能,指导创建可按帧确定性重现的关键帧,并通过 Studio 检查动画状态。它强调主体连续性、可见姿态和最终帧验证。
主要功能
- 01依据当前帧计算动画值
- 02使用 spring 与 interpolate 制作关键帧
- 03按帧延迟编排多项动画
- 04通过 Studio 检查全时间线
- 05避免渲染关键路径中的非确定性动画
适用场景
- 01制作入场、渐变及多段动画
- 02实现文字逐词显示或 SVG 描边动画
- 03检查动画首帧、各姿态和最终帧
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ phamthanhnghia/ remotion-agent-skills --skill remotion-keyframes安装后在 Claude Code 中输入/remotion-keyframes确认已加载。
详细介绍
Remotion 关键帧
关键帧是一份姿态契约:状态清晰可见、主体身份连续、帧结果确定、像素经过验证。
需要通用场景方案时,使用 remotion-animation。需要完整命令文档时,使用 remotion-cli。
流程
- 确定动画主体、可见状态、最终状态,以及动画原语(
spring或interpolate)。 - 选择能够证明提示词的最精简机制。
- 编写帧结果确定的关键帧。所有数值均由
useCurrentFrame()得出。 - 使用
npx remotion studio验证——拖动浏览完整时间轴,检查首帧、所有姿态和末帧。 - 如果验证失败,修正源关键帧,并在渲染前重新验证。
契约
- 明确指出移动的主体。
- 明确列出证明预期运动所需的姿态,包括最终状态。
- 为可见通道设置关键帧,而非隐藏的辅助状态。
- 需要保持连续性时,保留对象身份。
- 可读或具有语义的状态应保持足够长的时间,以便看清(文本至少 10 帧,图标至少 6 帧)。
- 末帧是动画的一部分,不是收尾处理。
- 除非有要求,否则不要重置为静止状态。
- 除非有要求,否则不要以黑屏结束。
运行时规则
interpolate() 和 spring():
- 所有数值都必须由
frame同步计算——不要在 interpolate/spring 调用中使用异步逻辑 - 使用
extrapolateLeft: 'clamp'和extrapolateRight: 'clamp',防止数值超出预期范围 spring({frame, fps, config})——frame 必须是 Remotion 的useCurrentFrame()值,而不是由Date.now()推导出的值- 对于入场动画,偏移 spring:
spring({frame: frame - delay, fps, config});使用Math.max(0, frame - delay),避免负帧驱动 spring
渲染关键动画中绝不要使用:
Date.now()或performance.now()- 未设种子的
Math.random()(改用 Remotion 的random(seed)) - CSS 过渡或 CSS 动画(无法拖动时间轴定位)
requestAnimationFrame- 渲染路径中的
setTimeout/setInterval - 用于视觉属性值的
useState/useEffect
Spring 配置参考
import {spring, useCurrentFrame, useVideoConfig} from 'remotion';
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
// Snappy entrance (default feel)
const scale = spring({frame, fps, config: {damping: 200}});
// Bouncy entrance
const bounce = spring({frame, fps, config: {damping: 8, stiffness: 300}});
// Very smooth, slow
const smooth = spring({frame, fps, config: {damping: 30, stiffness: 80, mass: 2}});
// Sharp exit (reverse spring — animate from 1 to 0)
const {durationInFrames} = useVideoConfig();
const exitFrame = durationInFrames - frame;
const exitScale = spring({frame: exitFrame, fps, config: {damping: 200}});
const scaleValue = 1 - (1 - exitScale) ; // goes from 1 down to 0
Interpolate 方案
import {interpolate} from 'remotion';
// Fade in over 20 frames
const opacity = interpolate(frame, [0, 20], [0, 1], {extrapolateRight: 'clamp'});
// Slide in from left
const x = interpolate(frame, [0, 30], [-300, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// Multi-stop color shift
const r = interpolate(frame, [0, 30, 60], [255, 100, 0], {extrapolateRight: 'clamp'});
const g = interpolate(frame, [0, 30, 60], [0, 150, 255], {extrapolateRight: 'clamp'});
// Scale pop
const scale = interpolate(frame, [0, 8, 12, 18], [0, 1.15, 0.95, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
通道
优先使用合成器/视觉通道:transform: translateX/Y/Z、transform: scale/rotate/skew、opacity、borderRadius、CSS 变量、clipPath、SVG strokeDashoffset。
避免使用布局通道:top/left/right/bottom、width/height、margin/padding——它们会触发重排、速度较慢,并且渲染器并行采样帧时会产生错误结果。
交错模式
const ITEM_COUNT = 5;
const STAGGER_FRAMES = 8; // delay between each item
const items = Array.from({length: ITEM_COUNT}, (_, i) => {
const delayedFrame = Math.max(0, frame - i * STAGGER_FRAMES);
const itemSpring = spring({frame: delayedFrame, fps, config: {damping: 200}});
return {opacity: itemSpring, y: (1 - itemSpring) * 40};
});
文本动画模式
const words = 'Hello Remotion World'.split(' ');
const WordReveal = () => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
return (
<div style={{display: 'flex', gap: 12}}>
{words.map((word, i) => {
const delayedFrame = Math.max(0, frame - i * 6);
const progress = spring({frame: delayedFrame, fps, config: {damping: 200}});
return (
<span
key={i}
style={{
opacity: progress,
transform: `translateY(${(1 - progress) * 20}px)`,
display: 'inline-block',
}}
>
{word}
</span>
);
})}
</div>
);
};
SVG 描边绘制
// SVG stroke-dasharray draw-on effect
const pathLength = 500; // measure with path.getTotalLength() in development
const drawProgress = interpolate(frame, [0, 60], [0, 1], {extrapolateRight: 'clamp'});
<path
d="M 0 0 L 300 200"
stroke="white"
strokeWidth={4}
fill="none"
strokeDasharray={pathLength}
strokeDashoffset={pathLength * (1 - drawProgress)}
/>
3D 深度(CSS 透视)
// CSS perspective 3D scene
<div style={{perspective: 800, perspectiveOrigin: '50% 50%'}}>
<div
style={{
transformStyle: 'preserve-3d',
transform: `rotateY(${interpolate(frame, [0, 60], [45, 0], {extrapolateRight: 'clamp'})}deg)`,
}}
>
{/* 3D content */}
</div>
</div>
机制选择
| 需求 | 机制 |
|---|---|
| 自然减速的入场 | spring() |
| 定时数值过渡 | interpolate() |
| 多段序列 | 带多个输入/输出节点的 interpolate() |
| 多个项目按顺序入场 | 交错:在 spring() 中使用索引化的 frame - i * delay |
| 文本逐词显现 | 按单词索引交错使用 spring |
| 描边延伸或绘制 | 通过 interpolate() 设置 strokeDashoffset |
| 形状显现边界 | 通过 interpolate() 设置 clipPath |
| 3D 深度场景 | 通过 interpolate() 设置 CSS perspective + rotateX/Y/Z |
错误处理
| 问题 | 修复方法 |
|---|---|
| 某些帧的数值发生跳变 | 检查传给 spring() 的 frame 是否为负数——使用 Math.max(0, frame - delay) 将其限制为非负数 |
| 动画无法正确拖动定位 | 确认没有使用 useState/CSS 动画驱动数值 |
| 开头或结尾出现黑帧 | 检查入场时是否设置了 extrapolateLeft: 'clamp',以及出场时是否设置了 extrapolateRight: 'clamp' |
| 文本闪烁或显示不一致 | 确保 random() 的种子是稳定的字符串,而不是 Math.random() |
| Spring 超调并移出屏幕 | 增大 damping(200 = 不超调,8 = 大幅弹跳) |
完成
在 Studio 中验证:拖动浏览首帧、所有可见姿态和末帧。确认运动由主体自身驱动、没有调试叠加层,且首帧没有闪烁。
评论
支持 Markdown