From e50a817b97b87baa0cd8fdde0fdb5d607143c1c3 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 8 Feb 2026 22:21:17 +0100 Subject: add transform + few fixes feat: transform feat: batch command fix: slider undo fix: timeline zoom perf refactor: make updateElement support multiple --- .../web/src/services/renderer/nodes/visual-node.ts | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 apps/web/src/services/renderer/nodes/visual-node.ts (limited to 'apps/web/src/services/renderer/nodes/visual-node.ts') diff --git a/apps/web/src/services/renderer/nodes/visual-node.ts b/apps/web/src/services/renderer/nodes/visual-node.ts new file mode 100644 index 00000000..9c6c88ca --- /dev/null +++ b/apps/web/src/services/renderer/nodes/visual-node.ts @@ -0,0 +1,67 @@ +import type { CanvasRenderer } from "../canvas-renderer"; +import { BaseNode } from "./base-node"; +import type { Transform } from "@/types/timeline"; + +const VISUAL_EPSILON = 1 / 1000; + +export interface VisualNodeParams { + duration: number; + timeOffset: number; + trimStart: number; + trimEnd: number; + transform: Transform; + opacity: number; +} + +export abstract class VisualNode< + Params extends VisualNodeParams = VisualNodeParams, +> extends BaseNode { + protected getLocalTime(time: number): number { + return time - this.params.timeOffset + this.params.trimStart; + } + + protected isInRange(time: number): boolean { + const localTime = this.getLocalTime(time); + return ( + localTime >= this.params.trimStart - VISUAL_EPSILON && + localTime < this.params.trimStart + this.params.duration + ); + } + + protected renderVisual({ + renderer, + source, + sourceWidth, + sourceHeight, + }: { + renderer: CanvasRenderer; + source: CanvasImageSource; + sourceWidth: number; + sourceHeight: number; + }): void { + renderer.context.save(); + + const { transform, opacity } = this.params; + const containScale = Math.min( + renderer.width / sourceWidth, + renderer.height / sourceHeight, + ); + const scaledWidth = sourceWidth * containScale * transform.scale; + const scaledHeight = sourceHeight * containScale * transform.scale; + const x = renderer.width / 2 + transform.position.x - scaledWidth / 2; + const y = renderer.height / 2 + transform.position.y - scaledHeight / 2; + + renderer.context.globalAlpha = opacity; + + if (transform.rotate !== 0) { + const centerX = x + scaledWidth / 2; + const centerY = y + scaledHeight / 2; + renderer.context.translate(centerX, centerY); + renderer.context.rotate((transform.rotate * Math.PI) / 180); + renderer.context.translate(-centerX, -centerY); + } + + renderer.context.drawImage(source, x, y, scaledWidth, scaledHeight); + renderer.context.restore(); + } +} -- cgit v1.3.1