diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-13 04:41:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-13 05:08:29 +0200 |
| commit | cad88ee4d1c9e06e7529b88ef5e5d3fdf3f89149 (patch) | |
| tree | 0bfc1fcb18b3da4969afa2836bb65e6a5e25d123 /apps/web/src/services/renderer/scene-builder.ts | |
| parent | 36efdf66e707c818b81009acf8a00630a0309ca1 (diff) | |
| download | opencut-cad88ee4d1c9e06e7529b88ef5e5d3fdf3f89149.tar.gz | |
refactor: rendering pipeline
Made-with: Cursor
Diffstat (limited to 'apps/web/src/services/renderer/scene-builder.ts')
| -rw-r--r-- | apps/web/src/services/renderer/scene-builder.ts | 530 |
1 files changed, 265 insertions, 265 deletions
diff --git a/apps/web/src/services/renderer/scene-builder.ts b/apps/web/src/services/renderer/scene-builder.ts index 22d941be..68ba956b 100644 --- a/apps/web/src/services/renderer/scene-builder.ts +++ b/apps/web/src/services/renderer/scene-builder.ts @@ -1,265 +1,265 @@ -import type { SceneTracks, TimelineTrack } from "@/lib/timeline";
-import type { MediaAsset } from "@/lib/media/types";
-import { RootNode } from "./nodes/root-node";
-import { VideoNode } from "./nodes/video-node";
-import { ImageNode } from "./nodes/image-node";
-import { TextNode } from "./nodes/text-node";
-import { StickerNode } from "./nodes/sticker-node";
-import { GraphicNode } from "./nodes/graphic-node";
-import { ColorNode } from "./nodes/color-node";
-import { BlurBackgroundNode } from "./nodes/blur-background-node";
-import { EffectLayerNode } from "./nodes/effect-layer-node";
-import type { BaseNode } from "./nodes/base-node";
-import type { TBackground, TCanvasSize } from "@/lib/project/types";
-import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/constants";
-
-const PREVIEW_MAX_IMAGE_SIZE = 2048;
-
-function getVisibleSortedElements({ track }: { track: TimelineTrack }) {
- return track.elements
- .filter((element) => !("hidden" in element && element.hidden))
- .slice()
- .sort((a, b) => {
- if (a.startTime !== b.startTime) return a.startTime - b.startTime;
- return a.id.localeCompare(b.id);
- });
-}
-
-function buildTrackNodes({
- tracks,
- mediaMap,
- canvasSize,
- isPreview,
-}: {
- tracks: TimelineTrack[];
- mediaMap: Map<string, MediaAsset>;
- canvasSize: TCanvasSize;
- isPreview?: boolean;
-}): BaseNode[] {
- const nodes: BaseNode[] = [];
-
- for (const track of tracks) {
- const elements = getVisibleSortedElements({ track });
-
- for (const element of elements) {
- if (element.type === "effect") {
- nodes.push(
- new EffectLayerNode({
- effectType: element.effectType,
- effectParams: element.params,
- timeOffset: element.startTime,
- duration: element.duration,
- }),
- );
- continue;
- }
-
- if (element.type === "video" || element.type === "image") {
- const mediaAsset = mediaMap.get(element.mediaId);
- if (!mediaAsset?.file || !mediaAsset?.url) {
- continue;
- }
-
- if (element.type === "video" && mediaAsset.type === "video") {
- nodes.push(
- new VideoNode({
- mediaId: mediaAsset.id,
- url: mediaAsset.url,
- file: mediaAsset.file,
- duration: element.duration,
- timeOffset: element.startTime,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- retime: element.retime,
- transform: element.transform,
- animations: element.animations,
- opacity: element.opacity,
- blendMode: element.blendMode,
- effects: element.effects,
- masks: element.masks,
- }),
- );
- }
- if (element.type === "image" && mediaAsset.type === "image") {
- nodes.push(
- new ImageNode({
- url: mediaAsset.url,
- duration: element.duration,
- timeOffset: element.startTime,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- transform: element.transform,
- animations: element.animations,
- opacity: element.opacity,
- blendMode: element.blendMode,
- effects: element.effects,
- masks: element.masks,
- ...(isPreview && {
- maxSourceSize: PREVIEW_MAX_IMAGE_SIZE,
- }),
- }),
- );
- }
- }
-
- if (element.type === "text") {
- nodes.push(
- new TextNode({
- ...element,
- canvasCenter: { x: canvasSize.width / 2, y: canvasSize.height / 2 },
- canvasHeight: canvasSize.height,
- textBaseline: "middle",
- effects: element.effects,
- }),
- );
- }
-
- if (element.type === "sticker") {
- nodes.push(
- new StickerNode({
- stickerId: element.stickerId,
- intrinsicWidth: element.intrinsicWidth,
- intrinsicHeight: element.intrinsicHeight,
- duration: element.duration,
- timeOffset: element.startTime,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- transform: element.transform,
- animations: element.animations,
- opacity: element.opacity,
- blendMode: element.blendMode,
- effects: element.effects,
- }),
- );
- }
-
- if (element.type === "graphic") {
- nodes.push(
- new GraphicNode({
- definitionId: element.definitionId,
- params: element.params,
- duration: element.duration,
- timeOffset: element.startTime,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- transform: element.transform,
- animations: element.animations,
- opacity: element.opacity,
- blendMode: element.blendMode,
- effects: element.effects,
- masks: element.masks,
- }),
- );
- }
- }
- }
-
- return nodes;
-}
-
-function buildBlurBackgroundNodes({
- track,
- mediaMap,
- blurIntensity,
-}: {
- track: TimelineTrack | undefined;
- mediaMap: Map<string, MediaAsset>;
- blurIntensity: number;
-}): BaseNode[] {
- if (!track) {
- return [];
- }
-
- const nodes: BaseNode[] = [];
- const elements = getVisibleSortedElements({ track });
-
- for (const element of elements) {
- if (element.type !== "video" && element.type !== "image") {
- continue;
- }
-
- const mediaAsset = mediaMap.get(element.mediaId);
- if (
- !mediaAsset?.file ||
- !mediaAsset?.url ||
- (mediaAsset.type !== "video" && mediaAsset.type !== "image")
- ) {
- continue;
- }
-
- nodes.push(
- new BlurBackgroundNode({
- mediaId: mediaAsset.id,
- url: mediaAsset.url,
- file: mediaAsset.file,
- mediaType: mediaAsset.type,
- duration: element.duration,
- timeOffset: element.startTime,
- trimStart: element.trimStart,
- trimEnd: element.trimEnd,
- retime: element.type === "video" ? element.retime : undefined,
- blurIntensity,
- }),
- );
- }
-
- return nodes;
-}
-
-export type BuildSceneParams = {
- canvasSize: TCanvasSize;
- tracks: SceneTracks;
- mediaAssets: MediaAsset[];
- duration: number;
- background: TBackground;
- isPreview?: boolean;
-};
-
-export function buildScene({
- canvasSize,
- tracks,
- mediaAssets,
- duration,
- background,
- isPreview,
-}: BuildSceneParams) {
- const rootNode = new RootNode({ duration });
- const mediaMap = new Map(mediaAssets.map((m) => [m.id, m]));
-
- const visibleTracks = [
- ...tracks.overlay.filter((track) => !("hidden" in track && track.hidden)),
- ...(!tracks.main.hidden ? [tracks.main] : []),
- ];
- const orderedTracksBottomToTop = visibleTracks.slice().reverse();
- const mainTrack = tracks.main.hidden ? undefined : tracks.main;
-
- const allNodes = buildTrackNodes({
- tracks: orderedTracksBottomToTop,
- mediaMap,
- canvasSize,
- isPreview,
- });
-
- if (background.type === "blur") {
- const blurNodes = buildBlurBackgroundNodes({
- track: mainTrack,
- mediaMap,
- blurIntensity:
- background.blurIntensity ?? DEFAULT_BACKGROUND_BLUR_INTENSITY,
- });
- for (const node of blurNodes) {
- rootNode.add(node);
- }
- } else if (
- background.type === "color" &&
- background.color !== "transparent"
- ) {
- rootNode.add(new ColorNode({ color: background.color }));
- }
-
- for (const node of allNodes) {
- rootNode.add(node);
- }
-
- return rootNode;
-}
+import type { SceneTracks, TimelineTrack } from "@/lib/timeline"; +import type { MediaAsset } from "@/lib/media/types"; +import { RootNode } from "./nodes/root-node"; +import { VideoNode } from "./nodes/video-node"; +import { ImageNode } from "./nodes/image-node"; +import { TextNode } from "./nodes/text-node"; +import { StickerNode } from "./nodes/sticker-node"; +import { GraphicNode } from "./nodes/graphic-node"; +import { ColorNode } from "./nodes/color-node"; +import { BlurBackgroundNode } from "./nodes/blur-background-node"; +import { EffectLayerNode } from "./nodes/effect-layer-node"; +import type { AnyBaseNode } from "./nodes/base-node"; +import type { TBackground, TCanvasSize } from "@/lib/project/types"; +import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/lib/background/constants"; + +const PREVIEW_MAX_IMAGE_SIZE = 2048; + +function getVisibleSortedElements({ track }: { track: TimelineTrack }) { + return track.elements + .filter((element) => !("hidden" in element && element.hidden)) + .slice() + .sort((a, b) => { + if (a.startTime !== b.startTime) return a.startTime - b.startTime; + return a.id.localeCompare(b.id); + }); +} + +function buildTrackNodes({ + tracks, + mediaMap, + canvasSize, + isPreview, +}: { + tracks: TimelineTrack[]; + mediaMap: Map<string, MediaAsset>; + canvasSize: TCanvasSize; + isPreview?: boolean; +}): AnyBaseNode[] { + const nodes: AnyBaseNode[] = []; + + for (const track of tracks) { + const elements = getVisibleSortedElements({ track }); + + for (const element of elements) { + if (element.type === "effect") { + nodes.push( + new EffectLayerNode({ + effectType: element.effectType, + effectParams: element.params, + timeOffset: element.startTime, + duration: element.duration, + }), + ); + continue; + } + + if (element.type === "video" || element.type === "image") { + const mediaAsset = mediaMap.get(element.mediaId); + if (!mediaAsset?.file || !mediaAsset?.url) { + continue; + } + + if (element.type === "video" && mediaAsset.type === "video") { + nodes.push( + new VideoNode({ + mediaId: mediaAsset.id, + url: mediaAsset.url, + file: mediaAsset.file, + duration: element.duration, + timeOffset: element.startTime, + trimStart: element.trimStart, + trimEnd: element.trimEnd, + retime: element.retime, + transform: element.transform, + animations: element.animations, + opacity: element.opacity, + blendMode: element.blendMode, + effects: element.effects ?? [], + masks: element.masks ?? [], + }), + ); + } + if (element.type === "image" && mediaAsset.type === "image") { + nodes.push( + new ImageNode({ + url: mediaAsset.url, + duration: element.duration, + timeOffset: element.startTime, + trimStart: element.trimStart, + trimEnd: element.trimEnd, + transform: element.transform, + animations: element.animations, + opacity: element.opacity, + blendMode: element.blendMode, + effects: element.effects ?? [], + masks: element.masks ?? [], + ...(isPreview && { + maxSourceSize: PREVIEW_MAX_IMAGE_SIZE, + }), + }), + ); + } + } + + if (element.type === "text") { + nodes.push( + new TextNode({ + ...element, + canvasCenter: { x: canvasSize.width / 2, y: canvasSize.height / 2 }, + canvasHeight: canvasSize.height, + textBaseline: "middle", + effects: element.effects ?? [], + }), + ); + } + + if (element.type === "sticker") { + nodes.push( + new StickerNode({ + stickerId: element.stickerId, + intrinsicWidth: element.intrinsicWidth, + intrinsicHeight: element.intrinsicHeight, + duration: element.duration, + timeOffset: element.startTime, + trimStart: element.trimStart, + trimEnd: element.trimEnd, + transform: element.transform, + animations: element.animations, + opacity: element.opacity, + blendMode: element.blendMode, + effects: element.effects ?? [], + }), + ); + } + + if (element.type === "graphic") { + nodes.push( + new GraphicNode({ + definitionId: element.definitionId, + params: element.params, + duration: element.duration, + timeOffset: element.startTime, + trimStart: element.trimStart, + trimEnd: element.trimEnd, + transform: element.transform, + animations: element.animations, + opacity: element.opacity, + blendMode: element.blendMode, + effects: element.effects ?? [], + masks: element.masks ?? [], + }), + ); + } + } + } + + return nodes; +} + +function buildBlurBackgroundNodes({ + track, + mediaMap, + blurIntensity, +}: { + track: TimelineTrack | undefined; + mediaMap: Map<string, MediaAsset>; + blurIntensity: number; +}): AnyBaseNode[] { + if (!track) { + return []; + } + + const nodes: AnyBaseNode[] = []; + const elements = getVisibleSortedElements({ track }); + + for (const element of elements) { + if (element.type !== "video" && element.type !== "image") { + continue; + } + + const mediaAsset = mediaMap.get(element.mediaId); + if ( + !mediaAsset?.file || + !mediaAsset?.url || + (mediaAsset.type !== "video" && mediaAsset.type !== "image") + ) { + continue; + } + + nodes.push( + new BlurBackgroundNode({ + mediaId: mediaAsset.id, + url: mediaAsset.url, + file: mediaAsset.file, + mediaType: mediaAsset.type, + duration: element.duration, + timeOffset: element.startTime, + trimStart: element.trimStart, + trimEnd: element.trimEnd, + retime: element.type === "video" ? element.retime : undefined, + blurIntensity, + }), + ); + } + + return nodes; +} + +export type BuildSceneParams = { + canvasSize: TCanvasSize; + tracks: SceneTracks; + mediaAssets: MediaAsset[]; + duration: number; + background: TBackground; + isPreview?: boolean; +}; + +export function buildScene({ + canvasSize, + tracks, + mediaAssets, + duration, + background, + isPreview, +}: BuildSceneParams) { + const rootNode = new RootNode({ duration }); + const mediaMap = new Map(mediaAssets.map((m) => [m.id, m])); + + const visibleTracks = [ + ...tracks.overlay.filter((track) => !("hidden" in track && track.hidden)), + ...(!tracks.main.hidden ? [tracks.main] : []), + ]; + const orderedTracksBottomToTop = visibleTracks.slice().reverse(); + const mainTrack = tracks.main.hidden ? undefined : tracks.main; + + const allNodes = buildTrackNodes({ + tracks: orderedTracksBottomToTop, + mediaMap, + canvasSize, + isPreview, + }); + + if (background.type === "blur") { + const blurNodes = buildBlurBackgroundNodes({ + track: mainTrack, + mediaMap, + blurIntensity: + background.blurIntensity ?? DEFAULT_BACKGROUND_BLUR_INTENSITY, + }); + for (const node of blurNodes) { + rootNode.add(node); + } + } else if ( + background.type === "color" && + background.color !== "transparent" + ) { + rootNode.add(new ColorNode({ color: background.color })); + } + + for (const node of allNodes) { + rootNode.add(node); + } + + return rootNode; +} |
