diff options
Diffstat (limited to 'apps/web/src/services/renderer/scene-builder.ts')
| -rw-r--r-- | apps/web/src/services/renderer/scene-builder.ts | 165 |
1 files changed, 115 insertions, 50 deletions
diff --git a/apps/web/src/services/renderer/scene-builder.ts b/apps/web/src/services/renderer/scene-builder.ts index ee7f3d6c..695e077f 100644 --- a/apps/web/src/services/renderer/scene-builder.ts +++ b/apps/web/src/services/renderer/scene-builder.ts @@ -6,48 +6,59 @@ import { ImageNode } from "./nodes/image-node"; import { TextNode } from "./nodes/text-node"; import { StickerNode } from "./nodes/sticker-node"; import { ColorNode } from "./nodes/color-node"; -import { BlurBackgroundNode } from "./nodes/blur-background-node"; +import { CompositeEffectNode } from "./nodes/composite-effect-node"; +import { EffectLayerNode } from "./nodes/effect-layer-node"; +import type { BaseNode } from "./nodes/base-node"; import type { TBackground, TCanvasSize } from "@/types/project"; import { DEFAULT_BLUR_INTENSITY } from "@/constants/project-constants"; import { isMainTrack } from "@/lib/timeline"; -export type BuildSceneParams = { - canvasSize: TCanvasSize; - tracks: TimelineTrack[]; - mediaAssets: MediaAsset[]; - duration: number; - background: TBackground; -}; - -export function buildScene(params: BuildSceneParams) { - const { tracks, mediaAssets, duration, canvasSize, background } = params; - - const rootNode = new RootNode({ duration }); - const mediaMap = new Map(mediaAssets.map((m) => [m.id, m])); - - const visibleTracks = tracks.filter( - (track) => !("hidden" in track && track.hidden), - ); +const PREVIEW_MAX_IMAGE_SIZE = 2048; +const BLUR_BACKGROUND_ZOOM_SCALE = 1.4; - const orderedTracksTopToBottom = [ - ...visibleTracks.filter((track) => !isMainTrack(track)), - ...visibleTracks.filter((track) => isMainTrack(track)), - ]; - - const orderedTracksBottomToTop = orderedTracksTopToBottom.slice().reverse(); +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); + }); +} - const contentNodes = []; +function buildTrackNodes({ + tracks, + mediaMap, + canvasSize, + isPreview, +}: { + tracks: TimelineTrack[]; + mediaMap: Map<string, MediaAsset>; + canvasSize: TCanvasSize; + isPreview?: boolean; +}): BaseNode[] { + const nodes: BaseNode[] = []; - for (const track of orderedTracksBottomToTop) { - const elements = 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); - }); + 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) { @@ -55,7 +66,7 @@ export function buildScene(params: BuildSceneParams) { } if (mediaAsset.type === "video") { - contentNodes.push( + nodes.push( new VideoNode({ mediaId: mediaAsset.id, url: mediaAsset.url, @@ -65,13 +76,15 @@ export function buildScene(params: BuildSceneParams) { trimStart: element.trimStart, trimEnd: element.trimEnd, transform: element.transform, + animations: element.animations, opacity: element.opacity, blendMode: element.blendMode, + effects: element.effects, }), ); } if (mediaAsset.type === "image") { - contentNodes.push( + nodes.push( new ImageNode({ url: mediaAsset.url, duration: element.duration, @@ -79,26 +92,32 @@ export function buildScene(params: BuildSceneParams) { trimStart: element.trimStart, trimEnd: element.trimEnd, transform: element.transform, + animations: element.animations, opacity: element.opacity, blendMode: element.blendMode, + effects: element.effects, + ...(isPreview && { + maxSourceSize: PREVIEW_MAX_IMAGE_SIZE, + }), }), ); } } if (element.type === "text") { - contentNodes.push( + 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") { - contentNodes.push( + nodes.push( new StickerNode({ stickerId: element.stickerId, duration: element.duration, @@ -106,31 +125,77 @@ export function buildScene(params: BuildSceneParams) { trimStart: element.trimStart, trimEnd: element.trimEnd, transform: element.transform, + animations: element.animations, opacity: element.opacity, blendMode: element.blendMode, + effects: element.effects, }), ); } } } + return nodes; +} + +export type BuildSceneParams = { + canvasSize: TCanvasSize; + tracks: TimelineTrack[]; + 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.filter( + (track) => !("hidden" in track && track.hidden), + ); + + const orderedTracksTopToBottom = [ + ...visibleTracks.filter((track) => !isMainTrack(track)), + ...visibleTracks.filter((track) => isMainTrack(track)), + ]; + + const orderedTracksBottomToTop = orderedTracksTopToBottom.slice().reverse(); + + const allNodes = buildTrackNodes({ + tracks: orderedTracksBottomToTop, + mediaMap, + canvasSize, + isPreview, + }); + if (background.type === "blur") { rootNode.add( - new BlurBackgroundNode({ - blurIntensity: background.blurIntensity ?? DEFAULT_BLUR_INTENSITY, - contentNodes, + new CompositeEffectNode({ + contentNodes: allNodes.filter( + (node) => !(node instanceof EffectLayerNode), + ), + effectType: "blur", + effectParams: { + intensity: + background.blurIntensity ?? DEFAULT_BLUR_INTENSITY, + }, + scale: BLUR_BACKGROUND_ZOOM_SCALE, }), ); - for (const node of contentNodes) { - rootNode.add(node); - } - } else { - if (background.type === "color" && background.color !== "transparent") { - rootNode.add(new ColorNode({ color: background.color })); - } - for (const node of contentNodes) { - 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; |
