summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/preview/index.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/components/editor/panels/preview/index.tsx
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/components/editor/panels/preview/index.tsx')
-rw-r--r--apps/web/src/components/editor/panels/preview/index.tsx294
1 files changed, 0 insertions, 294 deletions
diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/components/editor/panels/preview/index.tsx
deleted file mode 100644
index 369d854c..00000000
--- a/apps/web/src/components/editor/panels/preview/index.tsx
+++ /dev/null
@@ -1,294 +0,0 @@
-"use client";
-
-import { useCallback, useEffect, useMemo, useRef } from "react";
-import useDeepCompareEffect from "use-deep-compare-effect";
-import { useEditor } from "@/hooks/use-editor";
-import { useRafLoop } from "@/hooks/use-raf-loop";
-import { useContainerSize } from "@/hooks/use-container-size";
-import { useFullscreen } from "@/hooks/use-fullscreen";
-import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
-import { TICKS_PER_SECOND } from "@/lib/wasm";
-import type { RootNode } from "@/services/renderer/nodes/root-node";
-import { buildScene } from "@/services/renderer/scene-builder";
-import { PreviewInteractionOverlay } from "./preview-interaction-overlay";
-import { BookmarkNoteOverlay } from "./bookmark-note-overlay";
-import { GuideOverlay } from "./guide-overlay";
-import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
-import { usePreviewStore } from "@/stores/preview-store";
-import { PreviewContextMenu } from "./context-menu";
-import { PreviewToolbar } from "./toolbar";
-import {
- PreviewViewportProvider,
- usePreviewViewportState,
-} from "./preview-viewport";
-
-function usePreviewSize() {
- const canvasSize = useEditor(
- (e) => e.project.getActive()?.settings.canvasSize,
- );
-
- return {
- width: canvasSize?.width,
- height: canvasSize?.height,
- };
-}
-
-function normalizeWheelDelta({
- delta,
- deltaMode,
- pageSize,
-}: {
- delta: number;
- deltaMode: number;
- pageSize: number;
-}): number {
- if (deltaMode === WheelEvent.DOM_DELTA_LINE) {
- return delta * 16;
- }
-
- if (deltaMode === WheelEvent.DOM_DELTA_PAGE) {
- return delta * pageSize;
- }
-
- return delta;
-}
-
-export function PreviewPanel() {
- const containerRef = useRef<HTMLDivElement>(null);
- const { toggleFullscreen } = useFullscreen({ containerRef });
-
- return (
- <div
- ref={containerRef}
- className="panel bg-background relative flex size-full min-h-0 min-w-0 flex-col rounded-sm border"
- >
- <PreviewCanvas
- containerRef={containerRef}
- onToggleFullscreen={toggleFullscreen}
- />
- <RenderTreeController />
- </div>
- );
-}
-
-function RenderTreeController() {
- const editor = useEditor();
- const tracks = useEditor(
- (e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks,
- );
- const mediaAssets = useEditor((e) => e.media.getAssets());
- const activeProject = useEditor((e) => e.project.getActive());
-
- const { width, height } = usePreviewSize();
-
- useDeepCompareEffect(() => {
- if (!activeProject) return;
-
- const duration = editor.timeline.getTotalDuration();
- const renderTree = buildScene({
- tracks,
- mediaAssets,
- duration,
- canvasSize: { width, height },
- background: activeProject.settings.background,
- isPreview: true,
- });
-
- editor.renderer.setRenderTree({ renderTree });
- }, [tracks, mediaAssets, activeProject?.settings.background, width, height]);
-
- return null;
-}
-
-function PreviewCanvas({
- containerRef,
- onToggleFullscreen,
-}: {
- containerRef: React.RefObject<HTMLElement | null>;
- onToggleFullscreen: () => void;
-}) {
- const canvasRef = useRef<HTMLCanvasElement>(null);
- const viewportRef = useRef<HTMLDivElement>(null);
- const lastFrameRef = useRef(-1);
- const lastSceneRef = useRef<RootNode | null>(null);
- const renderingRef = useRef(false);
- const { width: nativeWidth, height: nativeHeight } = usePreviewSize();
- const viewportSize = useContainerSize({ containerRef: viewportRef });
- const editor = useEditor();
- const activeProject = useEditor((e) => e.project.getActive());
- const renderTree = useEditor((e) => e.renderer.getRenderTree());
- const { overlays } = usePreviewStore();
- const viewport = usePreviewViewportState({
- canvasHeight: nativeHeight,
- canvasWidth: nativeWidth,
- viewportHeight: viewportSize.height,
- viewportRef,
- viewportWidth: viewportSize.width,
- });
-
- const renderer = useMemo(() => {
- return new CanvasRenderer({
- width: nativeWidth,
- height: nativeHeight,
- fps: activeProject.settings.fps,
- });
- }, [nativeWidth, nativeHeight, activeProject.settings.fps.numerator, activeProject.settings.fps.denominator]);
-
- const render = useCallback(() => {
- if (canvasRef.current && renderTree && !renderingRef.current) {
- const renderTime = Math.min(
- editor.playback.getCurrentTime(),
- editor.timeline.getLastFrameTime(),
- );
- const ticksPerFrame = Math.round(TICKS_PER_SECOND * renderer.fps.denominator / renderer.fps.numerator);
- const frame = Math.floor(renderTime / ticksPerFrame);
-
- if (
- frame !== lastFrameRef.current ||
- renderTree !== lastSceneRef.current
- ) {
- renderingRef.current = true;
- lastSceneRef.current = renderTree;
- lastFrameRef.current = frame;
- renderer
- .renderToCanvas({
- node: renderTree,
- time: renderTime,
- targetCanvas: canvasRef.current,
- })
- .then(() => {
- renderingRef.current = false;
- });
- }
- }
- }, [renderer, renderTree, editor.playback]);
-
- useRafLoop(render);
-
- useEffect(() => {
- const container = viewportRef.current;
- if (!container) return;
-
- let pendingZoomDelta = 0;
- let pendingPanDeltaX = 0;
- let pendingPanDeltaY = 0;
- let zoomRafId: ReturnType<typeof requestAnimationFrame> | null = null;
- let panRafId: ReturnType<typeof requestAnimationFrame> | null = null;
-
- const onWheel = (event: WheelEvent) => {
- const normalizedDeltaX = normalizeWheelDelta({
- delta: event.deltaX,
- deltaMode: event.deltaMode,
- pageSize: container.clientWidth,
- });
- const normalizedDeltaY = normalizeWheelDelta({
- delta: event.deltaY,
- deltaMode: event.deltaMode,
- pageSize: container.clientHeight,
- });
- const isZoomGesture = event.ctrlKey || event.metaKey;
- if (isZoomGesture) {
- event.preventDefault();
- pendingZoomDelta += normalizedDeltaY;
-
- if (zoomRafId === null) {
- zoomRafId = requestAnimationFrame(() => {
- const cappedDelta =
- Math.sign(pendingZoomDelta) *
- Math.min(Math.abs(pendingZoomDelta), 30);
- const zoomFactor = Math.exp(-cappedDelta / 300);
-
- viewport.scaleZoom({ factor: zoomFactor });
- pendingZoomDelta = 0;
- zoomRafId = null;
- });
- }
-
- return;
- }
-
- if (!viewport.canPan) {
- return;
- }
-
- if (normalizedDeltaX === 0 && normalizedDeltaY === 0) {
- return;
- }
-
- event.preventDefault();
- pendingPanDeltaX += normalizedDeltaX;
- pendingPanDeltaY += normalizedDeltaY;
-
- if (panRafId === null) {
- panRafId = requestAnimationFrame(() => {
- viewport.panByScreenDelta({
- deltaX: pendingPanDeltaX,
- deltaY: pendingPanDeltaY,
- });
- pendingPanDeltaX = 0;
- pendingPanDeltaY = 0;
- panRafId = null;
- });
- }
- };
-
- container.addEventListener("wheel", onWheel, {
- capture: true,
- passive: false,
- });
-
- return () => {
- container.removeEventListener("wheel", onWheel, {
- capture: true,
- });
- if (zoomRafId !== null) {
- cancelAnimationFrame(zoomRafId);
- }
- if (panRafId !== null) {
- cancelAnimationFrame(panRafId);
- }
- };
- }, [viewport.canPan, viewport.panByScreenDelta, viewport.scaleZoom]);
-
- return (
- <PreviewViewportProvider value={viewport}>
- <div className="flex size-full min-h-0 min-w-0 flex-col">
- <div className="flex min-h-0 min-w-0 flex-1 p-2 pb-0">
- <ContextMenu>
- <ContextMenuTrigger asChild>
- <div
- ref={viewportRef}
- className="relative flex size-full min-h-0 min-w-0 items-center justify-center overflow-hidden"
- >
- <canvas
- ref={canvasRef}
- width={nativeWidth}
- height={nativeHeight}
- className="absolute block border"
- style={{
- left: viewport.sceneLeft,
- top: viewport.sceneTop,
- width: viewport.sceneWidth,
- height: viewport.sceneHeight,
- background:
- activeProject.settings.background.type === "blur"
- ? "transparent"
- : activeProject?.settings.background.color,
- }}
- />
- <GuideOverlay />
- <PreviewInteractionOverlay />
- {overlays.bookmarks && <BookmarkNoteOverlay />}
- </div>
- </ContextMenuTrigger>
- <PreviewContextMenu
- onToggleFullscreen={onToggleFullscreen}
- containerRef={containerRef}
- />
- </ContextMenu>
- </div>
- <PreviewToolbar onToggleFullscreen={onToggleFullscreen} />
- </div>
- </PreviewViewportProvider>
- );
-}