diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/components/editor/panels/preview/index.tsx | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-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.tsx | 294 |
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> - ); -} |
