通过以特定方式编写 Remotion 标记,Remotion Studio 能够识别代码结构并使其具有交互性:
- 允许通过点击选择项目
- 允许拖放、调整大小和旋转
- 编辑 CSS 样式
- 使关键帧和缓动值可编辑
如果标记过于复杂,Studio 无法使其交互,则这些值会变为灰色。
首选具有独立时间轴的交互式组件
对自定义场景、卡片、标题和其他每个实例的 props 应该可编辑的视觉组件使用 Interactive.withSchema({wrapInSequence: true})。在 InteractivitySchema 中暴露有意义的内容和外观控制。将装饰性实现细节保留在组件内部。
组件必须接受 style 并将其转发给一个视觉根元素。当变换、裁剪或不透明度应该影响其所有层时,保持一个共享的根元素。当冗余内部包装器的样式可以移动到现有元素而无需更改布局或动画时,将其展平。
import type React from 'react';
import {Interactive, type InteractivitySchema} from 'remotion';
type LowerThirdProps = {
readonly children: string;
readonly accentColor: string;
readonly style?: React.CSSProperties;
};
const LowerThirdInner: React.FC<LowerThirdProps> = ({
children,
accentColor,
style,
}) => {
return (
<Interactive.Div
style={{
position: 'absolute',
left: 80,
bottom: 80,
display: 'flex',
alignItems: 'center',
gap: 20,
backgroundColor: 'white',
borderRadius: 16,
padding: '20px 32px',
color: 'black',
fontFamily: 'Helvetica, Arial, sans-serif',
fontSize: 48,
fontWeight: 600,
...style,
}}
>
<div
style={{
width: 8,
alignSelf: 'stretch',
borderRadius: 4,
backgroundColor: accentColor,
}}
/>
{children}
</Interactive.Div>
);
};
const lowerThirdSchema = {
children: {type: 'text-content', default: '', description: '文本'},
accentColor: {
type: 'color',
default: '#0b84f3',
description: '强调色',
},
} as const satisfies InteractivitySchema;
export const LowerThird = Interactive.withSchema({
Component: LowerThirdInner,
componentName: '<LowerThird>',
schema: lowerThirdSchema,
wrapInSequence: true,
});
在组件内部转发注入的 style 是必需的;在组件的调用点保持可编辑的样式为内联。
选择 Schema 字段
Schema 中的每个键都是一个 prop,Studio 可以在调用点读取和编辑该 prop。不在 Schema 中的 Prop 仍然有效,但不能在 Studio 中编辑。键可以使用点表示法,例如 style.color。
| 类型 | 用途 | 可设置关键帧 |
|---|---|---|
text-content | 文本,例如标题 | 否 |
number | 数字,可选的 min、max、step、integer | 是 |
boolean | 开/关开关 | 是 (长按) |
color | CSS 颜色字符串 | 是 |
enum | 在 variants 之间选择,每个变体都有自己的嵌套 schema | 可选(长按) |
array | 数字、颜色、枚举等的列表 | 否 |
asset | 媒体源,可选的 assetType | 否 |
font-family | CSS 字体族 | 否 |
font-weight | 字体粗细 | 是 |
translate、scale、rotation-css、rotation-degrees、transform-origin | 变换 | 是 |
uv-coordinate | 元素上归一化的 [x, y] 点 | 是 |
svg-path | SVG 路径数据 | 是 |
remotion-captions | Caption[] 数据 | 否 |
hidden | 从控件中隐藏的值 | — |
每个字段都需要一个 default,并且应该有一个 description,Studio 将其显示为标签。设置 keyframable: false 以显示静态控件。
暴露内容和每个实例的外观,例如文本和强调色。不要为 style.translate、style.scale、style.rotate、style.transformOrigin 和 style.opacity 添加字段:wrapInSequence: true 已经添加了它们。
要使根的 CSS 属性在每个实例中可编辑,可以展开内置的 schema 片段:
const cardSchema = {
...Interactive.textSchema, // style.color, style.fontSize, style.fontWeight, ...
...Interactive.backgroundSchema, // style.backgroundColor
...Interactive.borderRadiusSchema, // style.borderRadius, ...
} as const satisfies InteractivitySchema;
还有 Interactive.borderSchema 和 Interactive.captionsSchema(用于接受字幕的组件)。有关所有选项,请参阅 InteractivitySchema。
将可复用的组件注册为连接组合
将具有自己图层或动画的主要场景和可复用组件注册为独立的组合。Studio 将这些称为连接组合:它们可以在自己的时间轴中打开,同时与父视频共享相同的组件实现。
在组件内部定义所需的样式和加载字体,以便它在没有父级的情况下工作。请参阅父级独立性。
注册父级渲染的相同导出的组件引用。对于上面的示例,使用 component={LowerThird},而不是 LowerThirdInner 或内联包装器。使用 wrapInSequence: true,连接不需要额外的 <Sequence>。
import {Composition} from 'remotion';
import {LowerThird} from './LowerThird';
export const RemotionRoot = () => (
<Composition
id="LowerThird"
component={LowerThird}
width={1280}
height={720}
fps={30}
durationInFrames={30}
defaultProps={{
children: 'Jane Doe, 产品设计师',
accentColor: '#0b84f3',
}}
/>
);
选择唯一的 ID、代表性的内联默认值以及适合预览组件的尺寸、fps 和时长。注册默认值不会自动复制父实例中的 props。请参阅连接组合以了解注册和提取细节。
将时间直接放在组件上
避免在已经接受 time props 的组件周围放置单独的 <Sequence>。内置交互式组件和使用 wrapInSequence: true 创建的自定义组件可以直接接收 name、from、durationInFrames、trimBefore、playbackRate 和 premountFor。
<LowerThird
name="字幕"
from={30}
durationInFrames={90}
accentColor="#0b84f3"
>
Jane Doe, 产品设计师
</LowerThird>
删除冗余序列时,将其时间和名称移动到子组件。保留任何现有的子组件时间;不要覆盖或重复应用偏移量。组件内部的 useCurrentFrame() 调用读取其局部帧,而调用点的样式表达式仍使用调用者的帧。移动动画样式时保留这种区别。
在以下情况下保留 <Sequence>:为多个兄弟组件提供共享时间、覆盖 useVideoConfig() 的尺寸,或包装一个不处理时间的组件。将 <Series.Sequence> 用于连续布局,将 <TransitionSeries.Sequence> 用于过渡;直接的 from props 不能替代这些行为。
为每个独立可编辑项提供其自己的 JSX 节点
Studio 编辑创建项目的 JSX 源节点。如果多个运行时项来自同一个 JSX 节点,则它们共享一个源编辑目标。
对于每个应该独立可编辑的组合注册、剪辑、场景、层或序列,编写单独的 JSX 节点,并将其可编辑的 props 保留在该节点上。这适用于 <Composition>、<Still>、内置媒体组件、<Sequence>、<Series.Sequence>、<TransitionSeries.Sequence> 和自定义组件。
例如,像这样编写一个可编辑的时间轴:
<Series>
<Series.Sequence name="介绍" durationInFrames={90}>
<Introduction />
</Series.Sequence>
<Series.Sequence name="演示" durationInFrames={150}>
<Demo />
</Series.Sequence>
</Series>
.map() 或其他程序化循环会从一个 JSX 源节点创建多个运行时项。这适用于有意图地作为模板控制的重复输出,例如可视化条形图或粒子。当实例在 Studio 中需要独立的 ID 或名称、props、元数据、时间、排序、删除或复制时,这不适用。
使用 Interactive 使 HTML 元素交互
每个 HTML 和 SVG 元素(<Img> 除外,它已经是交互的),例如 <div>,都可以使用 Interactive 变为交互:
<Interactive.Div
name="问候卡"
style={{fontSize: 80, padding: 24}}
>
你好
</Interactive.Div>
这允许在 Studio 中设置样式和关键帧。要合理使用,如果组件有很多元素,时间轴可能会变得混乱。
首选内联文本
如果文本是固定的且只使用一次,直接将其写在交互元素内部,而不是提取到常量中。
// 👍 固定副本保持可编辑
<Interactive.Div name="标题">
Remotion 最佳实践
</Interactive.Div>
仅在文本是动态或可复用时使用 prop 或变量。
为交互元素提供描述性名称
向元素添加 name prop 以使其易于识别。避免计算名称,将其硬编码。
<>
<Interactive.Div name="英雄标题" style={{fontSize: 80}}>
发布日
</Interactive.Div>
<Img name="头像" src="https://remotion.media/image.jpeg" />
<Video name="背景" src="https://remotion.media/video.mp4" />
<Interactive.Div name="标题">
发布日
</Interactive.Div>
</>
将所有 CSS 样式保持内联
最好的方法是直接向 style 传递一个普通对象——不要引用常量、不要对象展开、不要数学运算。
<Interactive.Div
style={{
fontSize: 80,
color: 'red',
}}
>
你好世界!
</Interactive.Div>
const baseStyle = useMemo(() => {
return {
fontSize: 12 // ❌ 不支持非内联样式
}
}, []);
<Interactive.Div
style={{
...baseStyle, // ❌ 不支持展开
color: RED, // ❌ 不支持引用常量
scale: frame * 10 // ❌ 不支持数学运算
}}
>
你好世界!
</Interactive.Div>
使用 interpolate() 进行动画
在发生更改的属性上以内联 interpolate() 调用的形式编写动画。
输出范围、缓动、外推和 output 属性应使用硬编码值。
输入范围还可以使用直接从 useVideoConfig() 解构的 durationInFrames、fps、width 和 height。支持裸标识符,例如 durationInFrames、与数字相乘,例如 2 * fps 或 fps * 2,以及数字相减,例如 durationInFrames - 1。
const {fps, durationInFrames} = useVideoConfig();
// 👍 内联值可以标准化和设置关键帧
<Interactive.Div
name="产品卡"
style={{
color: 'white',
fontSize: 80,
scale: interpolate(frame, [0, fps], [0, 1], {
easing: Easing.spring({damping: 200}),
output: 'perceptual-scale',
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
}),
rotate: interpolate(frame, [0, 1 * fps], ['0deg', '20deg'], {
easing: Easing.spring({damping: 200}),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
}),
translate: interpolate(
frame,
[durationInFrames - 30, durationInFrames],
['0px 0px', '0px 120px'],
{
easing: Easing.spring({damping: 200}),
output: 'perceptual-scale',
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
}
),
}}
/>
const translateY = interpolate(frame, [0, 30], [0, 120]); // ❌ 数学运算应直接在标记中
<Interactive.Div
name="产品卡"
style={{
translate: translateY, // ❌ 仅支持内联 interpolate() 调用,
rotate: interpolate(frame, [start, start + 10], [0, Math.PI]), // ❌ 不支持使用任意变量进行数学运算,不支持使用常量
scale
}}
/>
