summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/preview/index.tsx
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-02-23 03:24:02 +0100
committerGitHub <noreply@github.com>2026-02-23 03:24:02 +0100
commit93d1e3383ce47e45157d70f9ba6251239dae6bc2 (patch)
tree1e33a2d7533b65f0e7c8927f03905a1a99937bd1 /apps/web/src/components/editor/panels/preview/index.tsx
parentfca99d6126c31fbb18ed9f1034cee6f940b040e8 (diff)
downloadopencut-93d1e3383ce47e45157d70f9ba6251239dae6bc2.tar.gz
feat: major editor overhaul (assets, properties, timeline, fonts) (#709)
* feat: major editor overhaul (assets, properties, timeline, fonts) Refactor editor core systems to standardize UI architecture and improve performance. Assets & Properties: - Replace monolithic property items with composable `Section` architecture. - Add specialized sections for Transform, Blending, and Text. - Implement `NumberField` with scrubbing and math evaluation. - Add new ColorPicker with EyeDropper and multiple format support. - Standardize asset panels using new `PanelView` layout. Fonts & Stickers: - Implement custom font atlas/sprite system for high-performance previews. - Add virtualized FontPicker with search and favorites. - Refactor stickers to use a provider-based architecture (icons, emoji, flags, shapes). - Standardize sticker IDs to `provider:value` format. Timeline & Interaction: - Convert bookmarks to rich objects with notes, colors, and duration. - Refactor drag-and-drop to use Command pattern (enabling proper undo/redo). - Add Shift modifier to disable snapping during moves/resizes. - Add new overlays for layout guides and text editing. Renderer: - Add support for multi-line text, custom line-height, and letter-spacing. - Implement global composite operation (blend modes). - Update sticker node to resolve dynamic provider IDs. Infrastructure: - Add storage migrations (v3->v6) for text weights, sticker IDs, and bookmarks. - Update global styles and core UI components (Button, Input, Popover). * add ts-nocheck directive to settings-legacy.tsx to suppress TypeScript errors * fix: correct global composite operation assignment in TextNode to ensure proper blend mode handling * deleted shadcn components with errors * formatting * fix linter issues * migrate from next middleware to proxy * add missing component back * add breadcrumb back * chore: add @radix-ui/react-primitive deps * chore: more deps * chore: add missing env vars to bun-ci * next env
Diffstat (limited to 'apps/web/src/components/editor/panels/preview/index.tsx')
-rw-r--r--apps/web/src/components/editor/panels/preview/index.tsx181
1 files changed, 72 insertions, 109 deletions
diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/components/editor/panels/preview/index.tsx
index 902547bd..90cb11a3 100644
--- a/apps/web/src/components/editor/panels/preview/index.tsx
+++ b/apps/web/src/components/editor/panels/preview/index.tsx
@@ -9,18 +9,13 @@ 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 { formatTimeCode, getLastFrameTime } from "@/lib/time";
+import { 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";
+import { BookmarkNoteOverlay } from "./bookmark-note-overlay";
+import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
+import { usePreviewStore } from "@/stores/preview-store";
+import { PreviewContextMenu } from "./context-menu";
+import { PreviewToolbar } from "./toolbar";
function usePreviewSize() {
const editor = useEditor();
@@ -32,6 +27,30 @@ function usePreviewSize() {
};
}
+export function PreviewPanel() {
+ const containerRef = useRef<HTMLDivElement>(null);
+ const { isFullscreen, 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"
+ >
+ <div className="flex min-h-0 min-w-0 flex-1 items-center justify-center p-2 pb-0">
+ <PreviewCanvas
+ onToggleFullscreen={toggleFullscreen}
+ containerRef={containerRef}
+ />
+ <RenderTreeController />
+ </div>
+ <PreviewToolbar
+ isFullscreen={isFullscreen}
+ onToggleFullscreen={toggleFullscreen}
+ />
+ </div>
+ );
+}
+
function RenderTreeController() {
const editor = useEditor();
const tracks = editor.timeline.getTracks();
@@ -58,98 +77,24 @@ function RenderTreeController() {
return null;
}
-export function PreviewPanel() {
- const containerRef = useRef<HTMLDivElement>(null);
- const { isFullscreen, toggleFullscreen } = useFullscreen({ containerRef });
-
- return (
- <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,
+function PreviewCanvas({
onToggleFullscreen,
+ containerRef,
}: {
- isFullscreen: boolean;
onToggleFullscreen: () => void;
+ containerRef: React.RefObject<HTMLElement | null>;
}) {
- 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 outerContainerRef = useRef<HTMLDivElement>(null);
+ const canvasBoundsRef = useRef<HTMLDivElement>(null);
const lastFrameRef = useRef(-1);
const lastSceneRef = useRef<RootNode | null>(null);
const renderingRef = useRef(false);
const { width: nativeWidth, height: nativeHeight } = usePreviewSize();
- const containerSize = useContainerSize({ containerRef });
+ const containerSize = useContainerSize({ containerRef: outerContainerRef });
const editor = useEditor();
const activeProject = editor.project.getActive();
+ const { overlays } = usePreviewStore();
const renderer = useMemo(() => {
return new CanvasRenderer({
@@ -224,24 +169,42 @@ function PreviewCanvas() {
return (
<div
- ref={containerRef}
- className="relative flex h-full w-full items-center justify-center"
+ ref={outerContainerRef}
+ className="relative flex size-full items-center justify-center"
>
- <canvas
- ref={canvasRef}
- width={nativeWidth}
- height={nativeHeight}
- className="block border"
- style={{
- width: displaySize.width,
- height: displaySize.height,
- background:
- activeProject.settings.background.type === "blur"
- ? "transparent"
- : activeProject?.settings.background.color,
- }}
- />
- <PreviewInteractionOverlay canvasRef={canvasRef} />
+ <ContextMenu>
+ <ContextMenuTrigger asChild>
+ <div
+ ref={canvasBoundsRef}
+ className="relative"
+ style={{ width: displaySize.width, height: displaySize.height }}
+ >
+ <canvas
+ ref={canvasRef}
+ width={nativeWidth}
+ height={nativeHeight}
+ className="block border"
+ style={{
+ width: displaySize.width,
+ height: displaySize.height,
+ background:
+ activeProject.settings.background.type === "blur"
+ ? "transparent"
+ : activeProject?.settings.background.color,
+ }}
+ />
+ <PreviewInteractionOverlay
+ canvasRef={canvasRef}
+ containerRef={canvasBoundsRef}
+ />
+ {overlays.bookmarks && <BookmarkNoteOverlay />}
+ </div>
+ </ContextMenuTrigger>
+ <PreviewContextMenu
+ onToggleFullscreen={onToggleFullscreen}
+ containerRef={containerRef}
+ />
+ </ContextMenu>
</div>
);
}