summaryrefslogtreecommitdiff
path: root/apps/web/src/services/renderer/nodes/visual-node.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-01 01:32:09 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-01 01:32:09 +0100
commit5e7c27f542e94ffee5d2106ce318b6f43cc7e738 (patch)
treeb33c3370c01b9b2ee685f1039543bc02f8764f7b /apps/web/src/services/renderer/nodes/visual-node.ts
parent744419469da403be57d30b5b19f6b43246caa57d (diff)
parent354f4dec67ef7eb39d34fca941ca34025a68136b (diff)
downloadopencut-5e7c27f542e94ffee5d2106ce318b6f43cc7e738.tar.gz
Merge branch 'dev' into ripple-editing
Diffstat (limited to 'apps/web/src/services/renderer/nodes/visual-node.ts')
-rw-r--r--apps/web/src/services/renderer/nodes/visual-node.ts98
1 files changed, 90 insertions, 8 deletions
diff --git a/apps/web/src/services/renderer/nodes/visual-node.ts b/apps/web/src/services/renderer/nodes/visual-node.ts
index 7179369e..15a7b081 100644
--- a/apps/web/src/services/renderer/nodes/visual-node.ts
+++ b/apps/web/src/services/renderer/nodes/visual-node.ts
@@ -1,9 +1,18 @@
import type { CanvasRenderer } from "../canvas-renderer";
+import { createOffscreenCanvas } from "../canvas-utils";
import { BaseNode } from "./base-node";
+import type { Effect } from "@/types/effects";
import type { BlendMode } from "@/types/rendering";
import type { Transform } from "@/types/timeline";
-
-const VISUAL_EPSILON = 1 / 1000;
+import type { ElementAnimations } from "@/types/animation";
+import {
+ getElementLocalTime,
+ resolveOpacityAtTime,
+ resolveTransformAtTime,
+} from "@/lib/animation";
+import { TIME_EPSILON_SECONDS } from "@/constants/animation-constants";
+import { getEffect } from "@/lib/effects";
+import { webglEffectRenderer } from "../webgl-effect-renderer";
export interface VisualNodeParams {
duration: number;
@@ -11,21 +20,31 @@ export interface VisualNodeParams {
trimStart: number;
trimEnd: number;
transform: Transform;
+ animations?: ElementAnimations;
opacity: number;
blendMode?: BlendMode;
+ effects?: Effect[];
}
export abstract class VisualNode<
Params extends VisualNodeParams = VisualNodeParams,
> extends BaseNode<Params> {
- protected getLocalTime(time: number): number {
+ protected getSourceLocalTime({ time }: { time: number }): number {
return time - this.params.timeOffset + this.params.trimStart;
}
- protected isInRange(time: number): boolean {
- const localTime = this.getLocalTime(time);
+ protected getAnimationLocalTime({ time }: { time: number }): number {
+ return getElementLocalTime({
+ timelineTime: time,
+ elementStartTime: this.params.timeOffset,
+ elementDuration: this.params.duration,
+ });
+ }
+
+ protected isInRange({ time }: { time: number }): boolean {
+ const localTime = this.getSourceLocalTime({ time });
return (
- localTime >= this.params.trimStart - VISUAL_EPSILON &&
+ localTime >= this.params.trimStart - TIME_EPSILON_SECONDS &&
localTime < this.params.trimStart + this.params.duration
);
}
@@ -35,15 +54,27 @@ export abstract class VisualNode<
source,
sourceWidth,
sourceHeight,
+ timelineTime,
}: {
renderer: CanvasRenderer;
source: CanvasImageSource;
sourceWidth: number;
sourceHeight: number;
+ timelineTime: number;
}): void {
renderer.context.save();
- const { transform, opacity } = this.params;
+ const animationLocalTime = this.getAnimationLocalTime({ time: timelineTime });
+ const transform = resolveTransformAtTime({
+ baseTransform: this.params.transform,
+ animations: this.params.animations,
+ localTime: animationLocalTime,
+ });
+ const opacity = resolveOpacityAtTime({
+ baseOpacity: this.params.opacity,
+ animations: this.params.animations,
+ localTime: animationLocalTime,
+ });
const containScale = Math.min(
renderer.width / sourceWidth,
renderer.height / sourceHeight,
@@ -68,7 +99,58 @@ export abstract class VisualNode<
renderer.context.translate(-centerX, -centerY);
}
- renderer.context.drawImage(source, x, y, scaledWidth, scaledHeight);
+ const enabledEffects =
+ this.params.effects?.filter((effect) => effect.enabled) ?? [];
+
+ if (enabledEffects.length === 0) {
+ renderer.context.drawImage(source, x, y, scaledWidth, scaledHeight);
+ renderer.context.restore();
+ return;
+ }
+
+ const elementCanvas = createOffscreenCanvas({
+ width: Math.round(scaledWidth),
+ height: Math.round(scaledHeight),
+ });
+ const elementCtx = elementCanvas.getContext("2d") as
+ | CanvasRenderingContext2D
+ | OffscreenCanvasRenderingContext2D
+ | null;
+ if (!elementCtx) {
+ renderer.context.drawImage(source, x, y, scaledWidth, scaledHeight);
+ renderer.context.restore();
+ return;
+ }
+
+ elementCtx.drawImage(source, 0, 0, scaledWidth, scaledHeight);
+
+ let currentResult: CanvasImageSource = elementCanvas;
+
+ for (const effect of enabledEffects) {
+ const definition = getEffect({ effectType: effect.type });
+ const passes = definition.renderer.passes.map((pass) => ({
+ fragmentShader: pass.fragmentShader,
+ uniforms: pass.uniforms({
+ effectParams: effect.params,
+ width: scaledWidth,
+ height: scaledHeight,
+ }),
+ }));
+ currentResult = webglEffectRenderer.applyEffect({
+ source: currentResult,
+ width: Math.round(scaledWidth),
+ height: Math.round(scaledHeight),
+ passes,
+ });
+ }
+
+ renderer.context.drawImage(
+ currentResult,
+ x,
+ y,
+ scaledWidth,
+ scaledHeight,
+ );
renderer.context.restore();
}
}