Remotion LabRemotion Lab
資源時間軸模板

時間軸模板

使用 Remotion 的時間軸模板建立影片編輯器中的核心時間軸元件,支援拖曳、縮放與多軌道操作。

時間軸模板

Timeline Template 是 Remotion 提供的時間軸 UI 元件模板,專注於實作影片編輯器中最複雜的部分:一個可拖曳、可縮放、支援多軌道的時間軸介面。

時間軸能做什麼

時間軸是影片編輯器的核心操作介面,負責:

  • 視覺化呈現影片的時間結構,顯示每個場景/素材的起始和結束位置
  • 拖曳調整場景的時間位置和長度
  • 多軌道管理:視訊軌、音訊軌、文字軌等分層顯示
  • 縮放控制:放大查看細節,縮小掌握全局
  • 播放頭:顯示目前預覽的時間點,並支援點擊跳轉

快速開始

npx create-video@latest --template timeline
cd my-timeline-editor
npm install
npm run dev

核心元件

<Timeline> 主容器

import { Timeline } from "./components/Timeline";
 
export default function EditorPage() {
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
      <Preview />
      <Timeline
        tracks={tracks}
        durationInFrames={600}
        fps={30}
        currentFrame={currentFrame}
        onFrameChange={setCurrentFrame}
        onTrackChange={handleTrackChange}
      />
    </div>
  );
}

軌道資料結構

interface Track {
  id: string;
  type: "video" | "audio" | "text" | "image";
  label: string;
  clips: Clip[];
}
 
interface Clip {
  id: string;
  from: number;          // 在時間軸上的起始幀
  durationInFrames: number;
  srcFrom?: number;      // 素材本身的起始點(用於修剪)
  label: string;
  color?: string;
  thumbnail?: string;
}

軌道與片段範例

const tracks: Track[] = [
  {
    id: "video-1",
    type: "video",
    label: "視訊軌 1",
    clips: [
      {
        id: "clip-1",
        from: 0,
        durationInFrames: 150,
        label: "開場影片.mp4",
        color: "#3b82f6",
      },
      {
        id: "clip-2",
        from: 150,
        durationInFrames: 200,
        label: "主體內容.mp4",
        color: "#3b82f6",
      },
    ],
  },
  {
    id: "audio-1",
    type: "audio",
    label: "背景音樂",
    clips: [
      {
        id: "audio-clip-1",
        from: 0,
        durationInFrames: 350,
        label: "bgm.mp3",
        color: "#10b981",
      },
    ],
  },
  {
    id: "text-1",
    type: "text",
    label: "字幕軌",
    clips: [
      {
        id: "text-clip-1",
        from: 30,
        durationInFrames: 90,
        label: "歡迎觀看",
        color: "#f59e0b",
      },
    ],
  },
];

實作拖曳功能

模板使用 @dnd-kit/core 實作拖曳互動:

import { DndContext, DragEndEvent } from "@dnd-kit/core";
 
export const TimelineWithDnd: React.FC = () => {
  const { tracks, updateClip } = useEditorStore();
 
  const handleDragEnd = (event: DragEndEvent) => {
    const { active, delta } = event;
    const clipId = active.id as string;
 
    // 將像素位移轉換為幀數
    const frameDelta = Math.round(delta.x / pixelsPerFrame);
 
    updateClip(clipId, (clip) => ({
      ...clip,
      from: Math.max(0, clip.from + frameDelta),
    }));
  };
 
  return (
    <DndContext onDragEnd={handleDragEnd}>
      {tracks.map((track) => (
        <TrackRow key={track.id} track={track} />
      ))}
    </DndContext>
  );
};

縮放控制

// 使用縮放係數控制時間軸顯示密度
const [zoom, setZoom] = useState(1); // 1 = 每幀 4px
 
const pixelsPerFrame = 4 * zoom;
 
// 滾輪縮放
const handleWheel = (e: WheelEvent) => {
  if (e.ctrlKey || e.metaKey) {
    e.preventDefault();
    setZoom((z) => Math.max(0.1, Math.min(10, z - e.deltaY * 0.01)));
  }
};

播放頭同步

時間軸的播放頭需要與 <Player> 的當前幀保持同步:

import { PlayerRef } from "@remotion/player";
 
export const SyncedTimeline: React.FC = () => {
  const playerRef = useRef<PlayerRef>(null);
  const [currentFrame, setCurrentFrame] = useState(0);
 
  // 監聽 Player 的幀變化,更新時間軸播放頭
  useEffect(() => {
    const { current: player } = playerRef;
    if (!player) return;
 
    const onFrameUpdate = () => {
      setCurrentFrame(player.getCurrentFrame());
    };
 
    player.addEventListener("frameupdate", onFrameUpdate);
    return () => player.removeEventListener("frameupdate", onFrameUpdate);
  }, []);
 
  // 點擊時間軸時,控制 Player 跳轉
  const handleTimelineClick = (frame: number) => {
    playerRef.current?.seekTo(frame);
  };
 
  return (
    <>
      <Player ref={playerRef} {...playerProps} />
      <Timeline
        currentFrame={currentFrame}
        onSeek={handleTimelineClick}
        tracks={tracks}
      />
    </>
  );
};

效能優化建議

時間軸在處理大量片段時可能遇到效能問題,以下是常見的優化策略:

  • 虛擬化渲染:使用 @tanstack/react-virtual 只渲染可見區域的片段
  • 防抖處理:拖曳結束後才更新 store,避免拖曳中頻繁觸發重渲染
  • 記憶化元件:用 React.memo 包裝每個片段元件,避免無關更新

延伸閱讀