summaryrefslogtreecommitdiff
path: root/apps/web/src/preview
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-22 16:48:37 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-22 16:48:37 +0200
commitcceb835a84c125990ad8e49784e01a7c52636bb3 (patch)
tree8520cce9a203c72c7bcdc72ca138a36316a5a110 /apps/web/src/preview
parentf1b45b4328cff6a016720ce7b38b87ac2992dc2d (diff)
downloadopencut-cceb835a84c125990ad8e49784e01a7c52636bb3.tar.gz
perf: improve rendering performance by ~20x
Diffstat (limited to 'apps/web/src/preview')
-rw-r--r--apps/web/src/preview/components/index.tsx100
1 files changed, 57 insertions, 43 deletions
diff --git a/apps/web/src/preview/components/index.tsx b/apps/web/src/preview/components/index.tsx
index d3cb2a49..e8f5c572 100644
--- a/apps/web/src/preview/components/index.tsx
+++ b/apps/web/src/preview/components/index.tsx
@@ -132,7 +132,7 @@ function PreviewCanvas({
isVisible: boolean;
}) => void;
}) {
- const canvasRef = useRef<HTMLCanvasElement>(null);
+ const canvasMountRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null);
const lastFrameRef = useRef(-1);
const lastSceneRef = useRef<RootNode | null>(null);
@@ -158,35 +158,51 @@ function PreviewCanvas({
});
}, [nativeWidth, nativeHeight, activeProject.settings.fps]);
+ // Mount the compositor's output canvas directly into the preview. wgpu
+ // renders straight into this element, so there is no intermediate copy —
+ // the container div owns positioning/styling, the canvas itself fills it.
+ useEffect(() => {
+ const mount = canvasMountRef.current;
+ if (!mount) return;
+ const outputCanvas = renderer.getOutputCanvas();
+ outputCanvas.style.display = "block";
+ outputCanvas.style.width = "100%";
+ outputCanvas.style.height = "100%";
+ mount.appendChild(outputCanvas);
+ return () => {
+ if (outputCanvas.parentElement === mount) {
+ mount.removeChild(outputCanvas);
+ }
+ };
+ }, [renderer]);
+
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 (!renderTree || renderingRef.current) return;
- 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;
- });
- }
+ 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
+ ) {
+ return;
}
+
+ renderingRef.current = true;
+ lastSceneRef.current = renderTree;
+ lastFrameRef.current = frame;
+ renderer
+ .render({ node: renderTree, time: renderTime })
+ .then(() => {
+ renderingRef.current = false;
+ });
}, [renderer, renderTree, editor.playback, editor.timeline]);
useRafLoop(render);
@@ -286,22 +302,20 @@ function PreviewCanvas({
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,
- }}
- />
+ <div
+ ref={canvasMountRef}
+ 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,
+ }}
+ />
<PreviewOverlayLayer
instances={overlayInstances}
plane="under-interaction"