Communitygithub.com

ahmadzulfi/video

规范 Remotion 标记以实现交互性

video 是什么?

该技能为使用 Remotion Studio 的开发者提供了编写交互式视频标记的最佳实践指南。它详细说明了如何通过 `Interactive` 组件、`wrapInSequence` 布局和 schema 定义来构建 Remotion 组件,使得 Studio 能够识别代码结构并支持点击选择、拖拽、缩放、旋转、CSS 样式编辑以及关键帧和缓动值编辑等交互功能。技能涵盖了从基础的可交互 HTML 元素到复杂的注册连接组合(connected compositions)的完整工作流,包括 schema 字段类型选择(如 string、number、color、enum、array、asset 等)、内联 CSS 样式编写、使用 `interpolate()` 进行动画处理,以及将时间属性直接放置在组件上的优化方法。其主要价值在于帮助开发者创建 Studio 完全可编辑的视频组件,避免因标记过于复杂而导致交互功能失效,从而提升 Remotion 项目的开发效率和用户体验。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ahmadzulfi/video/tree/HEAD/.agents/skills/remotion-interactivity

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

预览

来自技能 README

video preview

文档

通过以特定方式编写 Remotion 标记,Remotion Studio 能够识别代码结构并使其具有交互性:

  • 允许通过点击选择项目
  • 支持拖放、调整大小和旋转
  • 编辑 CSS 样式
  • 使关键帧和缓动值可编辑

如果标记对于 Studio 来说过于复杂而无法使其交互,则这些值将变为灰色显示。

优先使用带有自身时间线的交互式组件

对于填充其父容器的组件,请使用 Interactive.withSchema({wrapInSequence: true, layout: 'absolute-fill'})。该包装器提供了组件的视觉根节点和时间线项目,并默认裁剪溢出内容,因此内部组件可以返回一个 fragment。将时间控制、预挂载、尺寸和实例级样式放在导出的组件上。

将属于同一视觉效果的组件放在一起,以便它们一起移动和修剪。内部布局容器仍然可以提供对齐、在较小区域内裁剪或动画分组;仅填充场景的容器通常是不必要的。卡片、文本框和具有自身边界的 SVG 可以保留默认的 layout: 'none',并将 style 转发到它们的视觉根节点。

import {Video} from '@remotion/media';
import type React from 'react';
import {Interactive, useVideoConfig, type InteractivitySchema} from 'remotion';

type SceneProps = {
  readonly title: string;
};

const SceneInner: React.FC<SceneProps> = ({title}) => {
  const {fps} = useVideoConfig();

  return (
    <>
      <Video
        name="Background footage"
        src="https://remotion.media/video.mp4"
        premountFor={fps}
        objectFit="cover"
        style={{width: '100%', height: '100%'}}
      />
      <Interactive.Div
        name="Scene title"
        style={{
          position: 'absolute',
          left: 80,
          bottom: 80,
          color: 'white',
          fontFamily: 'Helvetica, Arial, sans-serif',
          fontSize: 80,
        }}
      >
        {title}
      </Interactive.Div>
    </>
  );
};

const sceneSchema = {
  title: {type: 'string', default: '', description: 'Title'},
} as const satisfies InteractivitySchema;

export const Scene = Interactive.withSchema({
  Component: SceneInner,
  componentName: 'Scene',
  schema: sceneSchema,
  wrapInSequence: true,
  layout: 'absolute-fill',
});

选择 Schema 字段

Schema 中的每个键都是一个 prop,Studio 可以在调用点读取和编辑。 不在 Schema 中的 Props 仍然有效,但无法在 Studio 中编辑。 键可以使用点号表示法,例如 style.color。

类型用途可关键帧化
string字符串,例如标题、提示或代码片段否
number数字,可选 min、max、step、integer是
boolean开/关切换是 (保持)
colorCSS 颜色字符串是
enum在 variants 之间选择,每个变体都有自己的嵌套 schema可选 (保持)
array数字、颜色、枚举等的列表否
asset媒体源,可选 assetType否
font-familyCSS 字体族否
font-weight字体粗细是
translate、scale、rotation-css、rotation-degrees、transform-origin变换是
uv-coordinate元素上的归一化 [x, y] 点是
svg-pathSVG 路径数据是
remotion-captionsCaption[] 数据否
hidden保留在控件之外的值—

每个字段都需要一个 default,并且应该有一个 description,Studio 会将其显示为标签。 设置 keyframable: false 以显示静态控件。

公开内容和实例级外观,例如文本和强调色。\n不要为 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。 对于接受 React.ReactNode 子元素的组件,添加 ...Interactive.childrenSchema。 它提供了子元素内容的控件,并且未来可能会演变为支持更丰富的内容。 目前,它使用一个标签为 "Text" 且默认值为 '' 的字符串字段。 如果组件需要仅字符串的子元素,请改用 type: 'string' 声明一个显式的 children 字段。 Interactive.textSchema 控制排版样式;childrenSchema 控制内容。 子元素片段不包含在 baseSchema 中,也不会自动添加到自定义组件中。 嵌套标记和计算出的子元素目前是只读的。 text-content 是 string 字段类型的已弃用别名。 有关所有选项,请参阅 InteractivitySchema。

将可复用组件注册为连接组合

将重要的场景和具有自身层或动画的可复用组件注册为独立的组合。Studio 将其称为连接组合:它们可以在自己的时间线中打开,同时与父视频共享相同的组件实现。

在组件内部定义所需的样式和加载字体,以便它无需父级即可工作。 请参阅父级独立性。

注册父级渲染的同一导出的组件引用。对于上面的示例,使用 component={Scene},而不是 SceneInner 或内联包装器。 使用 wrapInSequence: true 时,无需额外的 <Sequence> 即可建立连接。

import {Composition} from 'remotion';
import {Scene} from './Scene';

export const RemotionRoot = () => (
  <Composition
    id="Scene"
    component={Scene}
    width={1280}
    height={720}
    fps={30}
    durationInFrames={30}
    defaultProps={{
      title: 'A new perspective',
    }}
  />
);

选择一个唯一的 ID、具有代表性的内联默认值,以及适合预览组件的尺寸、fps 和持续时间。 注册默认值不会自动从父实例复制 props。 有关注册和提取的详细信息,请参阅连接组合。

直接将时间信息放在组件上

避免在已经接受时间 props 的单个组件周围放置一个单独的 <Sequence>。 内置交互式组件和使用 wrapInSequence: true 创建的自定义组件可以直接接收 name、from、durationInFrames、trimBefore、playbackRate 和 premountFor。

<Scene
  name="Opening scene"
  from={30}
  durationInFrames={90}
  premountFor={fps}
  style={{opacity: 0.9}}
  title="A new perspective"
/>

删除冗余序列时,将其时间信息和名称移动到子组件。 保留任何现有的子组件时间信息;不要覆盖或重复应用偏移。 组件内部的 useCurrentFrame() 调用读取其本地帧,而调用点的样式表达式仍然使用调用者的帧。移动动画样式时保留这种区别。

在以下情况下保留 <Sequence>:它为多个兄弟组件提供共享时间信息、包装了一个不处理时间信息的组件,或者提供子组件无法直接接受的尺寸。管理根也接受 width 和 height 用于 useVideoConfig()。对于连续布局,无论有无转场,都保留 <TransitionSeries.Sequence>;直接的 from props 不会取代这些行为。

为每个独立可编辑的项目提供自己的 JSX 节点

Studio 编辑创建项目的 JSX 源节点。如果多个运行时项目来自同一个 JSX 节点,它们将共享一个源编辑目标。

对于每个应该独立编辑的组合注册、剪辑、场景、层或序列,编写一个单独的 JSX 节点,并将其可编辑的 props 保留在该节点上。这适用于 <Composition>、<Still>、内置媒体组件、<Sequence>、<TransitionSeries.Sequence> 和自定义组件。

例如,像这样编写一个可编辑的时间线:

import { TransitionSeries } from "@remotion/transitions";

<TransitionSeries>
  <TransitionSeries.Sequence name="Introduction" durationInFrames={90}>
    <Introduction />
  </TransitionSeries.Sequence>
  <TransitionSeries.Sequence name="Demo" durationInFrames={150}>
    <Demo />
  </TransitionSeries.Sequence>
</TransitionSeries>

.map() 或其他编程循环会从一个 JSX 源节点创建多个运行时项目。这适用于有意作为单一模板控制的重复输出,例如可视化条或粒子。当实例需要独立的 ID 或名称、props、元数据、时间信息、排序、删除或在 Studio 中复制时,这不适用。

使用 Interactive 使 HTML 元素可交互

每个 HTML 和 SVG 元素(<Img> 除外,它已经是可交互的)都可以使用 Interactive 变为可交互:

<Interactive.Div
  name="Greeting card"
  style={{fontSize: 80, padding: 24}}
>
  Hello
</Interactive.Div>

这允许在 Studio 中设置样式和关键帧。请合理使用,如果组件包含许多元素,时间线可能会变得混乱。

优先使用内联文本

如果文本是固定的且仅使用一次,请直接将其写在交互元素内部,而不是将其提取到常量中。

// 👍 固定副本保持可编辑
<Interactive.Div name="Title">
  Remotion Best Practices
</Interactive.Div>

仅当文本是动态的或可复用时,才使用 prop 或变量。

为交互元素提供描述性名称

向元素添加 name prop 以使其易于识别。 避免使用计算出的名称,请硬编码它们。

<>
  <Interactive.Div name="Hero title" style={{fontSize: 80}}>
    Launch day
  </Interactive.Div>
  <Img name="Avatar" src="https://remotion.media/image.jpeg" />
  <Video name="Background" src="https://remotion.media/video.mp4" />
  <Interactive.Div name="Title">
    Launch day
  </Interactive.Div>
</>

将所有的 CSS 样式保持内联

最好的方法是直接将一个普通对象传递给 style——不要引用常量、不要展开对象、不要做数学运算。

<Interactive.Div
  style={{
    fontSize: 80,
    color: 'red',
  }}
>
  Hello World!
</Interactive.Div>
const baseStyle = useMemo(() => {
  return {
    fontSize: 12 // ❌ 不支持非内联样式
  }
}, []);

<Interactive.Div
  style={{
    ...baseStyle, // ❌ 不支持展开
    color: RED, // ❌ 不支持引用常量
    scale: frame * 10 // ❌ 不支持数学运算
  }}
>
  Hello World!
</Interactive.Div>

使用 interpolate() 实现动画

在更改的属性上以内联 interpolate() 调用的形式编写动画。
输出范围、缓动、外推和 output 属性应使用硬编码值。

输入范围还可以额外使用直接从 useVideoConfig() 解构的 durationInFrames、fps、width 和 height。支持裸标识符(例如 durationInFrames)、与数字相乘(例如 2 * fps 或 fps * 2),以及减去一个数字(例如 durationInFrames - 1)。

const {fps, durationInFrames} = useVideoConfig();

// 👍 内联值可以被标准化和关键帧化
<Interactive.Div
  name="Product card"
  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, mass: 1}),
        extrapolateLeft: 'clamp',
        extrapolateRight: 'clamp'
      }
    )
  }}
>
  Hello World!
</Interactive.Div>

不要将 interpolate() 用于 opacity

wrapInSequence: true 会在时间线中暴露一个内建的透明度控件。不要手动添加 opacity 插值,因为它会与内置控件冲突。使用透明度关键帧或直接设置 opacity prop。

使用 Spring 和 Easing 函数

Remotion Studio 支持 Easing.bezier()、Easing.spring() 和 Easing.elastic()。

import {Easing, interpolate} from 'remotion';

// 支持的缓动函数
const springValue = interpolate(frame, [0, 30], [0, 1], {
  easing: Easing.spring({damping: 200}),
  output: 'perceptual-scale',
});

const elasticValue = interpolate(frame, [0, 30], [0, 1], {
  easing: Easing.elastic({springiness: 400}),
});

const bezierValue = interpolate(frame, [0, 30], [0, 1], {
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});

内联使用它们以保持 Studio 的可编辑性。

关键帧输出类型

某些输出需要特定的 output 属性:

输出类型用途
perceptual-scaleCSS scale 属性的缩放值
degreesCSS rotate 的角度值
offsetCSS translate 的平移值
opacityCSS opacity 的透明度值

当输出值有单位时,Studio 可以显示滑块而不是文本输入。

避免使用条件渲染

条件渲染会完全删除节点。尽量使用动画(透明度、变换或裁剪)来隐藏元素。这有助于 Studio 保持时间线结构。

{showTitle && <Interactive.Div name="Title">...</Interactive.Div>}
<Interactive.Div
  name="Title"
  style={{
    opacity: showTitle ? 1 : 0,
    // 或使用 interpolate
  }}
>
  ...
</Interactive.Div>

使用 useMemo() 优化计算开销大的操作

对于开销大的计算(解析大型 SVG、生成路径、数据转换),使用 useMemo()。它不会影响 Studio 的交互性。

const parsedPath = useMemo(() => parseSvgPath(complexPath), [complexPath]);

完整示例

这是一个展示最佳实践的完整组件:

import React from 'react';
import {Interactive, interpolate, Easing, useVideoConfig} from 'remotion';

type Props = {
  title: string;
  accentColor: string;
};

const CardInner: React.FC<Props> = ({title, accentColor}) => {
  const {fps} = useVideoConfig();

  return (
    <Interactive.Div
      name="Card"
      style={{
        backgroundColor: 'white',
        borderRadius: 16,
        padding: 32,
        fontFamily: 'Helvetica',
        color: '#333',
        boxShadow: `0 4px 12px ${accentColor}40`,
      }}
    >
      <Interactive.Div
        name="Title"
        style={{
          fontSize: 40,
          fontWeight: 700,
          color: accentColor,
          marginBottom: 16,
        }}
      >
        {title}
      </Interactive.Div>
    </Interactive.Div>
  );
};

const cardSchema = {
  title: {type: 'string', default: 'Card title', description: 'Card title'},
  accentColor: {type: 'color', default: '#3498db', description: 'Accent color'},
} as const;

export const Card = Interactive.withSchema({
  Component: CardInner,
  componentName: 'Card',
  schema: cardSchema,
  wrapInSequence: true,
  layout: 'absolute-fill',
});

故障排除

值变为灰色

如果 Remotion Studio 无法解析标记,则值会变为灰色。常见原因:

  • 使用外部变量或常量
  • 使用对象展开
  • 使用数学运算
  • 使用条件渲染
  • 使用 map 循环生成具有相同 JSX 节点的多个项目

Studio 不识别组件

确保:

  1. 组件使用 Interactive.withSchema() 包装
  2. Schema 中的所有字段都有 default 和 description
  3. 组件已注册为组合
  4. 导出的组件引用与注册的匹配

时间线不匹配

检查:

  1. wrapInSequence: true 是否设置
  2. 时间 props(from、durationInFrames)是否直接在组件上,而不是在周围的 <Sequence> 上
  3. useCurrentFrame() 是否在组件内部使用,而不是在父组件中使用

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相关技能