summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/preview/index.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-10 16:28:21 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-10 16:28:21 +0100
commitfca99d6126c31fbb18ed9f1034cee6f940b040e8 (patch)
tree11500ff0fcc4a933650e3e91f1ea00fd315c4d0c /apps/web/src/components/editor/panels/preview/index.tsx
parent33a8705f0d967891402c61dd3192e6bad24921ef (diff)
downloadopencut-fca99d6126c31fbb18ed9f1034cee6f940b040e8.tar.gz
refactor
Diffstat (limited to 'apps/web/src/components/editor/panels/preview/index.tsx')
-rw-r--r--apps/web/src/components/editor/panels/preview/index.tsx141
1 files changed, 130 insertions, 11 deletions
diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/components/editor/panels/preview/index.tsx
index c579b15b..902547bd 100644
--- a/apps/web/src/components/editor/panels/preview/index.tsx
+++ b/apps/web/src/components/editor/panels/preview/index.tsx
@@ -4,11 +4,23 @@ import { useCallback, 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 type { RootNode } from "@/services/renderer/nodes/root-node";
import { buildScene } from "@/services/renderer/scene-builder";
-import { getLastFrameTime } from "@/lib/time";
+import { formatTimeCode, getLastFrameTime } from "@/lib/time";
import { PreviewInteractionOverlay } from "./preview-interaction-overlay";
+import { EditableTimecode } from "@/components/editable-timecode";
+import { invokeAction } from "@/lib/actions";
+import { Button } from "@/components/ui/button";
+import {
+ FullScreenIcon,
+ PauseIcon,
+ PlayIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { cn } from "@/utils/ui";
function usePreviewSize() {
const editor = useEditor();
@@ -47,32 +59,134 @@ function RenderTreeController() {
}
export function PreviewPanel() {
+ const containerRef = useRef<HTMLDivElement>(null);
+ const { isFullscreen, toggleFullscreen } = useFullscreen({ containerRef });
+
return (
- <div className="bg-panel relative flex h-full min-h-0 w-full min-w-0 flex-col rounded-sm">
- <div className="flex min-h-0 min-w-0 flex-1 items-center justify-center p-2">
+ <div
+ ref={containerRef}
+ className={cn(
+ "panel bg-background relative flex h-full min-h-0 w-full min-w-0 flex-col rounded-sm border",
+ isFullscreen && "bg-background",
+ )}
+ >
+ <div className="flex min-h-0 min-w-0 flex-1 items-center justify-center p-2 pb-0">
<PreviewCanvas />
<RenderTreeController />
</div>
+ <PreviewToolbar
+ isFullscreen={isFullscreen}
+ onToggleFullscreen={toggleFullscreen}
+ />
+ </div>
+ );
+}
+
+function PreviewToolbar({
+ isFullscreen,
+ onToggleFullscreen,
+}: {
+ isFullscreen: boolean;
+ onToggleFullscreen: () => void;
+}) {
+ const editor = useEditor();
+ const isPlaying = editor.playback.getIsPlaying();
+ const currentTime = editor.playback.getCurrentTime();
+ const totalDuration = editor.timeline.getTotalDuration();
+ const fps = editor.project.getActive().settings.fps;
+
+ return (
+ <div className="grid grid-cols-[1fr_auto_1fr] items-center pb-3 pt-5 px-5">
+ <div className="flex items-center mt-1">
+ <EditableTimecode
+ time={currentTime}
+ duration={totalDuration}
+ format="HH:MM:SS:FF"
+ fps={fps}
+ onTimeChange={({ time }) => editor.playback.seek({ time })}
+ className="text-center"
+ />
+ <span className="text-muted-foreground px-2 font-mono text-xs">/</span>
+ <span className="text-muted-foreground font-mono text-xs">
+ {formatTimeCode({
+ timeInSeconds: totalDuration,
+ format: "HH:MM:SS:FF",
+ fps,
+ })}
+ </span>
+ </div>
+
+ <Button
+ variant="text"
+ size="icon"
+ type="button"
+ onClick={() => invokeAction("toggle-play")}
+ >
+ <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
+ </Button>
+
+ <div className="justify-self-end">
+ <Button
+ variant="text"
+ size="icon"
+ type="button"
+ onClick={onToggleFullscreen}
+ title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
+ >
+ <HugeiconsIcon icon={FullScreenIcon} />
+ </Button>
+ </div>
</div>
);
}
function PreviewCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
+ const containerRef = useRef<HTMLDivElement>(null);
const lastFrameRef = useRef(-1);
const lastSceneRef = useRef<RootNode | null>(null);
const renderingRef = useRef(false);
- const { width, height } = usePreviewSize();
+ const { width: nativeWidth, height: nativeHeight } = usePreviewSize();
+ const containerSize = useContainerSize({ containerRef });
const editor = useEditor();
const activeProject = editor.project.getActive();
const renderer = useMemo(() => {
return new CanvasRenderer({
- width,
- height,
+ width: nativeWidth,
+ height: nativeHeight,
fps: activeProject.settings.fps,
});
- }, [width, height, activeProject.settings.fps]);
+ }, [nativeWidth, nativeHeight, activeProject.settings.fps]);
+
+ const displaySize = useMemo(() => {
+ if (
+ !nativeWidth ||
+ !nativeHeight ||
+ containerSize.width === 0 ||
+ containerSize.height === 0
+ ) {
+ return { width: nativeWidth ?? 0, height: nativeHeight ?? 0 };
+ }
+
+ const paddingBuffer = 4;
+ const availableWidth = containerSize.width - paddingBuffer;
+ const availableHeight = containerSize.height - paddingBuffer;
+
+ const aspectRatio = nativeWidth / nativeHeight;
+ const containerAspect = availableWidth / availableHeight;
+
+ const displayWidth =
+ containerAspect > aspectRatio
+ ? availableHeight * aspectRatio
+ : availableWidth;
+ const displayHeight =
+ containerAspect > aspectRatio
+ ? availableHeight
+ : availableWidth / aspectRatio;
+
+ return { width: displayWidth, height: displayHeight };
+ }, [nativeWidth, nativeHeight, containerSize.width, containerSize.height]);
const renderTree = editor.renderer.getRenderTree();
@@ -109,13 +223,18 @@ function PreviewCanvas() {
useRafLoop(render);
return (
- <div className="relative">
+ <div
+ ref={containerRef}
+ className="relative flex h-full w-full items-center justify-center"
+ >
<canvas
ref={canvasRef}
- width={width}
- height={height}
- className="block max-h-full max-w-full border"
+ width={nativeWidth}
+ height={nativeHeight}
+ className="block border"
style={{
+ width: displaySize.width,
+ height: displaySize.height,
background:
activeProject.settings.background.type === "blur"
? "transparent"