summaryrefslogtreecommitdiff
path: root/apps/web/src/services/renderer/nodes/image-node.ts
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-01-31 00:20:04 +0100
committerGitHub <noreply@github.com>2026-01-31 00:20:04 +0100
commit7bf0984698e43b9aa2dd879f65951318ed410390 (patch)
treeccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/services/renderer/nodes/image-node.ts
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/services/renderer/nodes/image-node.ts')
-rw-r--r--apps/web/src/services/renderer/nodes/image-node.ts93
1 files changed, 93 insertions, 0 deletions
diff --git a/apps/web/src/services/renderer/nodes/image-node.ts b/apps/web/src/services/renderer/nodes/image-node.ts
new file mode 100644
index 00000000..2a684d5f
--- /dev/null
+++ b/apps/web/src/services/renderer/nodes/image-node.ts
@@ -0,0 +1,93 @@
+import type { CanvasRenderer } from "../canvas-renderer";
+import { BaseNode } from "./base-node";
+import type { BaseMediaNodeParams } from "./video-node";
+
+const IMAGE_EPSILON = 1 / 1000;
+
+export type ImageNodeParams = BaseMediaNodeParams;
+
+export class ImageNode extends BaseNode<ImageNodeParams> {
+ private image?: HTMLImageElement;
+ private readyPromise: Promise<void>;
+
+ constructor(params: ImageNodeParams) {
+ super(params);
+ this.readyPromise = this.load();
+ }
+
+ private async load() {
+ const image = new Image();
+ this.image = image;
+ const url = URL.createObjectURL(this.params.file);
+
+ await new Promise<void>((resolve, reject) => {
+ image.onload = () => resolve();
+ image.onerror = () => reject(new Error("Image load failed"));
+ image.src = url;
+ });
+
+ URL.revokeObjectURL(url);
+ }
+
+ private getImageTime(time: number) {
+ return time - this.params.timeOffset + this.params.trimStart;
+ }
+
+ private isInRange(time: number) {
+ const imageTime = this.getImageTime(time);
+ return (
+ imageTime >= this.params.trimStart - IMAGE_EPSILON &&
+ imageTime < this.params.trimStart + this.params.duration
+ );
+ }
+
+ async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) {
+ await super.render({ renderer, time });
+
+ if (!this.isInRange(time)) {
+ return;
+ }
+
+ await this.readyPromise;
+
+ if (!this.image) {
+ return;
+ }
+
+ renderer.context.save();
+
+ if (this.params.opacity !== undefined) {
+ renderer.context.globalAlpha = this.params.opacity;
+ }
+
+ if (
+ this.params.x !== undefined &&
+ this.params.y !== undefined &&
+ this.params.width !== undefined &&
+ this.params.height !== undefined
+ ) {
+ renderer.context.drawImage(
+ this.image,
+ this.params.x,
+ this.params.y,
+ this.params.width,
+ this.params.height,
+ );
+ } else {
+ const mediaW = this.image.naturalWidth || renderer.width;
+ const mediaH = this.image.naturalHeight || renderer.height;
+ const containScale = Math.min(
+ renderer.width / mediaW,
+ renderer.height / mediaH,
+ );
+ const drawW = mediaW * containScale;
+ const drawH = mediaH * containScale;
+ const drawX = (renderer.width - drawW) / 2;
+ const drawY = (renderer.height - drawH) / 2;
+
+ renderer.context.drawImage(this.image, drawX, drawY, drawW, drawH);
+ }
+
+ renderer.context.restore();
+ }
+}