From f1043734438c8518a302f698d9ae6583a75f0c14 Mon Sep 17 00:00:00 2001 From: cashmere Date: Sun, 16 Aug 2026 06:16:56 +0200 Subject: feat: add transitions, track mixing, and voiceover controls --- apps/web/src/services/renderer/resolve.ts | 67 +++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 21 deletions(-) (limited to 'apps/web/src/services/renderer/resolve.ts') diff --git a/apps/web/src/services/renderer/resolve.ts b/apps/web/src/services/renderer/resolve.ts index 12f57e30..54a6eca4 100644 --- a/apps/web/src/services/renderer/resolve.ts +++ b/apps/web/src/services/renderer/resolve.ts @@ -22,6 +22,7 @@ import { } from "@/text/measure-element"; import { resolveColorAtTime, resolveOpacityAtTime } from "@/animation/values"; import { resolveTransformAtTime } from "@/rendering/animation-values"; +import { applyVisualTransition } from "@/rendering/transition"; import { videoCache } from "@/services/video-cache/service"; import type { CanvasRenderer } from "./canvas-renderer"; import type { AnyBaseNode } from "./nodes/base-node"; @@ -148,12 +149,13 @@ function resolveVisualState({ return null; } + const contentDuration = params.contentDuration ?? params.duration; const localTime = getElementLocalTime({ timelineTime: context.time, elementStartTime: params.timeOffset, - elementDuration: params.duration, + elementDuration: contentDuration, }); - const transform = applyMotionEffects({ + const baseTransform = applyMotionEffects({ baseTransform: resolveTransformAtTime({ baseTransform: params.transform, animations: params.animations, @@ -162,15 +164,24 @@ function resolveVisualState({ effects: params.effects, animations: params.animations, localTime, - duration: params.duration, + duration: contentDuration, canvasWidth: context.renderer.width, canvasHeight: context.renderer.height, }); - const opacity = resolveOpacityAtTime({ + const baseOpacity = resolveOpacityAtTime({ baseOpacity: params.opacity, animations: params.animations, localTime, }); + const transitionState = applyVisualTransition({ + baseTransform, + transition: params.transition, + localTime, + duration: contentDuration, + canvasWidth: context.renderer.width, + }); + const transform = transitionState.transform; + const opacity = baseOpacity * transitionState.opacityMultiplier; const containScale = Math.min( context.renderer.width / sourceWidth, context.renderer.height / sourceHeight, @@ -207,17 +218,21 @@ async function resolveVideoNode({ if (clipTime < 0 || clipTime >= node.params.duration) { return null; } + const contentDuration = node.params.contentDuration ?? node.params.duration; + const sourceClipTime = Math.min(clipTime, Math.max(0, contentDuration - 1)); const sourceTimeTicks = node.params.trimStart + getSourceTimeAtClipTime({ - clipTime, + clipTime: sourceClipTime, retime: node.params.retime, }); const frame = await videoCache.getFrameAt({ mediaId: node.params.mediaId, file: node.params.file, - time: mediaTimeToSeconds({ time: roundMediaTime({ time: sourceTimeTicks }) }), + time: mediaTimeToSeconds({ + time: roundMediaTime({ time: sourceTimeTicks }), + }), }); if (!frame) { return null; @@ -344,18 +359,27 @@ function resolveTextNode({ elementDuration: node.params.duration, }); const background = buildTextBackgroundFromElement({ element: node.params }); + const baseTransform = resolveTransformAtTime({ + baseTransform: node.params.transform, + animations: node.params.animations, + localTime, + }); + const baseOpacity = resolveOpacityAtTime({ + baseOpacity: node.params.opacity, + animations: node.params.animations, + localTime, + }); + const transitionState = applyVisualTransition({ + baseTransform, + transition: node.params.transition, + localTime, + duration: node.params.duration, + canvasWidth: context.renderer.width, + }); return { - transform: resolveTransformAtTime({ - baseTransform: node.params.transform, - animations: node.params.animations, - localTime, - }), - opacity: resolveOpacityAtTime({ - baseOpacity: node.params.opacity, - animations: node.params.animations, - localTime, - }), + transform: transitionState.transform, + opacity: baseOpacity * transitionState.opacityMultiplier, textColor: resolveColorAtTime({ baseColor: typeof node.params.params.color === "string" @@ -438,7 +462,9 @@ async function resolveBackdropSource({ const frame = await videoCache.getFrameAt({ mediaId: node.params.mediaId, file: node.params.file, - time: mediaTimeToSeconds({ time: roundMediaTime({ time: sourceTimeTicks }) }), + time: mediaTimeToSeconds({ + time: roundMediaTime({ time: sourceTimeTicks }), + }), }); if (!frame) { return null; @@ -485,10 +511,9 @@ function resolveEffectLayerNode({ const motion = definition.motion?.evaluate({ effectParams: node.params.effectParams, progress: sampleEasingBezier({ - bezier: - BUILTIN_PRESETS.find( - (entry) => entry.id === node.params.effectParams.easing, - )?.value ?? [0.25, 0.1, 0.25, 1], + bezier: BUILTIN_PRESETS.find( + (entry) => entry.id === node.params.effectParams.easing, + )?.value ?? [0.25, 0.1, 0.25, 1], x: clamp({ value: (time - node.params.timeOffset) / node.params.duration, min: 0, -- cgit v1.3.1