時間軸模板
使用 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包裝每個片段元件,避免無關更新