summaryrefslogtreecommitdiff
path: root/apps/web/src/services/renderer/nodes/visual-node.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-27 16:33:57 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-27 16:33:57 +0100
commit49426f19cd8eaed7cf6c47999b66fee7ee637a56 (patch)
tree1b37451529cfcb36e6cae79494c61b9c616077df /apps/web/src/services/renderer/nodes/visual-node.ts
parent9b94f89def4f96836c4f42a23a73e97fe647a43c (diff)
downloadopencut-49426f19cd8eaed7cf6c47999b66fee7ee637a56.tar.gz
feat: implement keyframe animation system
Add keyframe support for transform, opacity, and volume properties. Includes animation engine (interpolation, mutations, resolvers), timeline markers with selection/snapping, properties panel toggles, keyframe-aware renderer, and full undo/redo command support. Also refactor element command constructors to object params, extract timeline pixel math to pixel-utils.ts, and update cursor rules.
Diffstat (limited to 'apps/web/src/services/renderer/nodes/visual-node.ts')
-rw-r--r--apps/web/src/services/renderer/nodes/visual-node.ts38
1 files changed, 32 insertions, 6 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..50451b4a 100644
--- a/apps/web/src/services/renderer/nodes/visual-node.ts
+++ b/apps/web/src/services/renderer/nodes/visual-node.ts
@@ -2,8 +2,13 @@ import type { CanvasRenderer } from "../canvas-renderer";
import { BaseNode } from "./base-node";
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";
export interface VisualNodeParams {
duration: number;
@@ -11,6 +16,7 @@ export interface VisualNodeParams {
trimStart: number;
trimEnd: number;
transform: Transform;
+ animations?: ElementAnimations;
opacity: number;
blendMode?: BlendMode;
}
@@ -18,14 +24,22 @@ export interface VisualNodeParams {
export abstract class VisualNode<
Params extends VisualNodeParams = VisualNodeParams,
> extends BaseNode<Params> {
- protected getLocalTime(time: number): number {
+ protected getSourceLocalTime(time: number): number {
return time - this.params.timeOffset + this.params.trimStart;
}
+ protected getAnimationLocalTime(time: number): number {
+ return getElementLocalTime({
+ timelineTime: time,
+ elementStartTime: this.params.timeOffset,
+ elementDuration: this.params.duration,
+ });
+ }
+
protected isInRange(time: number): boolean {
- const localTime = this.getLocalTime(time);
+ 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 +49,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(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,