summaryrefslogtreecommitdiff
path: root/apps/web/src/services/renderer/resolve.ts
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-08-16 06:16:56 +0200
committercashmere <cashmere@autistici.org>2026-08-16 06:16:56 +0200
commitf1043734438c8518a302f698d9ae6583a75f0c14 (patch)
treed3e636a96e6349c1ad17afa7b28c9ffe5c882e37 /apps/web/src/services/renderer/resolve.ts
parent37d22b1e390bd6cea04bf8e15069281ee125cf10 (diff)
downloadopencut-f1043734438c8518a302f698d9ae6583a75f0c14.tar.gz
feat: add transitions, track mixing, and voiceover controls
Diffstat (limited to 'apps/web/src/services/renderer/resolve.ts')
-rw-r--r--apps/web/src/services/renderer/resolve.ts67
1 files changed, 46 insertions, 21 deletions
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,