Communitygithub.com

Guilhermeslvg/video-portfolio

結構化 Remotion 標記以實現互動性

video-portfolio 是什麼?

這個技能為 Remotion 開發者提供了一套最佳實踐與模式,用以撰寫 Remotion Studio 可識別並實現互動操作的標記結構。它涵蓋了如何讓元件支援點擊選取、拖放、調整大小與旋轉,以及編輯 CSS 樣式和關鍵影格。透過遵循此技能中的指導原則,開發者可以利用內建的 `Interactive` 輔助工具和結構化物件方案 (Schema) 來建立可自訂的場景與可重用元件。此技能適用於所有 Remotion 專案 (包括 Remotion Studio 和 CLI),透過將視覺元素正確標記為互動式,顯著提升編輯工作流程的效率。其核心方法是:優先使用具備獨立時間軸的互動元件、為每個可獨立編輯的項目建立個別的 JSX 節點、為互動元素提供描述性的名稱、將所有 CSS 樣式內聯化,以及使用 `interpolate()` 函數來製作動畫。此外,技能也詳細說明了如何在不同類別的屬性 (例如字串、數字、顏色、列舉等) 中使用物件方案,以及如何將可重用元件註冊為「已連結的合成」(Connected Composition)。

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

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

預覽

來自技能 README

video-portfolio preview

說明文件

透過以特定方式撰寫 Remotion 標記,Remotion Studio 就能夠辨識程式碼的結構並使其具備互動性:

  • 允許透過點擊選取項目
  • 允許拖放、調整大小和旋轉
  • 編輯 CSS 樣式
  • 讓關鍵影格和 easing 值可編輯

如果標記過於複雜,以至於 Studio 無法使其互動,則這些值會變成灰色。

偏好使用具備獨立時間軸的互動元件

對於填滿其父容器的元件,請使用 Interactive.withSchema({wrapInSequence: true, layout: 'absolute-fill'})。這個包裝器提供了它們的視覺根節點和時間軸項目,並預設裁剪溢出的內容,因此內部元件可以回傳一個片段 (fragment)。將時序、預掛載 (premounting)、尺寸和每個實例的樣式放在匯出的元件上。

將屬於同一視覺元素的內容保留在同一個元件中,以便它們可以一起移動和修剪。內部佈局容器仍然可以提供對齊、在較小區域內進行裁剪或一個動畫群組;一個僅填滿場景的容器通常是不必要的。卡片、文字框和具有自身邊界的 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="背景素材"
        src="https://remotion.media/video.mp4"
        premountFor={fps}
        objectFit="cover"
        style={{width: '100%', height: '100%'}}
      />
      <Interactive.Div
        name="場景標題"
        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: '標題'},
} 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 以顯示靜態控制項。

暴露內容和每個實例的外觀,例如文字和強調色。 不要為 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。 它為子元素內容提供控制項,並可能在未來演進以支援更豐富的內容。 目前,它使用一個標籤為「文字」且預設值為 '' 的字串欄位。 如果一個元件僅接受字串類型的子元素,請改用 type: 'string' 的顯式 children 欄位。 Interactive.textSchema 控制排版樣式;childrenSchema 控制內容。 子元素片段不包含在 baseSchema 中,也不會自動添加到自訂元件。 巢狀標記和計算後的子元素目前是唯讀的。 text-content 是 string 欄位類型的一個已棄用的別名。 有關所有選項,請參閱 InteractivitySchema。

將可重用元件註冊為已連結的合成 (Connected Composition)

將重要的場景和可重用元件(具有自己的圖層或動畫)註冊為獨立的合成 (Composition)。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: '一個新的視角',
    }}
  />
);

選擇一個唯一的 ID、代表性的內聯預設值以及適合預覽元件的尺寸、fps 和持續時間。註冊預設值不會自動從父實例複製 props。有關註冊和提取的詳細資訊,請參閱已連結的合成。

直接將時序放在元件上

避免在一個已接受時序 props 的元件周圍使用單獨的 <Sequence>。使用 wrapInSequence: true 建立的內建互動元件和自訂元件可以直接接收 name、from、durationInFrames、trimBefore、playbackRate 和 premountFor。

<Scene
  name="開場場景"
  from={30}
  durationInFrames={90}
  premountFor={fps}
  style={{opacity: 0.9}}
  title="一個新的視角"
/>

移除冗餘的 sequence 時,將其時序和名稱移動到子元件。保留任何現有的子元件時序;不要覆蓋或雙重套用偏移量。元件內部的 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="介紹" durationInFrames={90}>
    <Introduction />
  </TransitionSeries.Sequence>
  <TransitionSeries.Sequence name="示範" durationInFrames={150}>
    <Demo />
  </TransitionSeries.Sequence>
</TransitionSeries>

一個 .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() 呼叫。 輸出範圍、easing、外推法 (extrapolation) 和 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}),
        extrapolateLeft: 'clamp',
        extrapolateRight: 'clamp'
      }
    ),
  }}
>
  哈囉,世界!
</Interactive.Div>

Individual skills in this repo

This repo contains 5 individual skills — each has its own dedicated page.

相關技能