summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/preview-panel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor/preview-panel.tsx')
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx110
1 files changed, 106 insertions, 4 deletions
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
index 1654624c..c53cdafb 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -13,6 +13,7 @@ import { renderTimelineFrame } from "@/lib/timeline-renderer";
import { cn } from "@/lib/utils";
import { formatTimeCode } from "@/lib/time";
import { EditableTimecode } from "@/components/ui/editable-timecode";
+import { useFrameCache } from "@/hooks/use-frame-cache";
import {
DEFAULT_CANVAS_SIZE,
DEFAULT_FPS,
@@ -44,6 +45,8 @@ export function PreviewPanel() {
const { activeProject } = useProjectStore();
const previewRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
+ const { getCachedFrame, cacheFrame, invalidateCache, preRenderNearbyFrames } =
+ useFrameCache();
const lastFrameTimeRef = useRef(0);
const renderSeqRef = useRef(0);
const offscreenCanvasRef = useRef<OffscreenCanvas | HTMLCanvasElement | null>(
@@ -216,6 +219,17 @@ export function PreviewPanel() {
};
}, [dragState, previewDimensions, canvasSize, updateTextElement]);
+ // Invalidate cache when timeline changes
+ useEffect(() => {
+ invalidateCache();
+ }, [
+ tracks,
+ mediaFiles,
+ activeProject?.backgroundColor,
+ activeProject?.backgroundType,
+ invalidateCache,
+ ]);
+
const handleTextMouseDown = (
e: React.MouseEvent<HTMLDivElement>,
element: any,
@@ -449,7 +463,7 @@ export function PreviewPanel() {
};
}, [isPlaying, volume, muted, mediaFiles]);
- // Canvas: draw current frame for visible elements using offscreen compositing
+ // Canvas: draw current frame with caching
useEffect(() => {
const draw = async () => {
const canvas = canvasRef.current;
@@ -475,10 +489,86 @@ export function PreviewPanel() {
lastFrameTimeRef.current = currentTime;
}
- // Invalidate older async renders when user scrubs rapidly
- const mySeq = (renderSeqRef.current += 1);
+ const cachedFrame = getCachedFrame(
+ currentTime,
+ tracks,
+ mediaFiles,
+ activeProject
+ );
+ if (cachedFrame) {
+ mainCtx.putImageData(cachedFrame, 0, 0);
+
+ // Pre-render nearby frames in background
+ if (!isPlaying) {
+ // Only during scrubbing to avoid interfering with playback
+ preRenderNearbyFrames(
+ currentTime,
+ tracks,
+ mediaFiles,
+ activeProject,
+ async (time: number) => {
+ const tempCanvas = document.createElement("canvas");
+ tempCanvas.width = displayWidth;
+ tempCanvas.height = displayHeight;
+ const tempCtx = tempCanvas.getContext("2d");
+ if (!tempCtx)
+ throw new Error("Failed to create temp canvas context");
- // Offscreen buffer to avoid flicker (reuse canvas)
+ await renderTimelineFrame({
+ ctx: tempCtx,
+ time,
+ canvasWidth: displayWidth,
+ canvasHeight: displayHeight,
+ tracks,
+ mediaFiles,
+ backgroundColor:
+ activeProject?.backgroundType === "blur"
+ ? "transparent"
+ : activeProject?.backgroundColor || "#000000",
+ projectCanvasSize: canvasSize,
+ });
+
+ return tempCtx.getImageData(0, 0, displayWidth, displayHeight);
+ }
+ );
+ } else {
+ // Small lookahead while playing
+ preRenderNearbyFrames(
+ currentTime,
+ tracks,
+ mediaFiles,
+ activeProject,
+ async (time: number) => {
+ const tempCanvas = document.createElement("canvas");
+ tempCanvas.width = displayWidth;
+ tempCanvas.height = displayHeight;
+ const tempCtx = tempCanvas.getContext("2d");
+ if (!tempCtx)
+ throw new Error("Failed to create temp canvas context");
+
+ await renderTimelineFrame({
+ ctx: tempCtx,
+ time,
+ canvasWidth: displayWidth,
+ canvasHeight: displayHeight,
+ tracks,
+ mediaFiles,
+ backgroundColor:
+ activeProject?.backgroundType === "blur"
+ ? "transparent"
+ : activeProject?.backgroundColor || "#000000",
+ projectCanvasSize: canvasSize,
+ });
+
+ return tempCtx.getImageData(0, 0, displayWidth, displayHeight);
+ },
+ 1
+ );
+ }
+ return;
+ }
+
+ // Cache miss - render from scratch
if (!offscreenCanvasRef.current) {
const hasOffscreen =
typeof (globalThis as unknown as { OffscreenCanvas?: unknown })
@@ -541,6 +631,14 @@ export function PreviewPanel() {
projectCanvasSize: canvasSize,
});
+ const imageData = (offCtx as CanvasRenderingContext2D).getImageData(
+ 0,
+ 0,
+ displayWidth,
+ displayHeight
+ );
+ cacheFrame(currentTime, imageData, tracks, mediaFiles, activeProject);
+
// Blit offscreen to visible canvas
mainCtx.clearRect(0, 0, displayWidth, displayHeight);
if ((offscreenCanvas as HTMLCanvasElement).getContext) {
@@ -564,6 +662,10 @@ export function PreviewPanel() {
canvasSize.height,
activeProject?.backgroundType,
activeProject?.backgroundColor,
+ getCachedFrame,
+ cacheFrame,
+ preRenderNearbyFrames,
+ isPlaying,
]);
// Get media elements for blur background (video/image only)