diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-01 01:32:09 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-01 01:32:09 +0100 |
| commit | 5e7c27f542e94ffee5d2106ce318b6f43cc7e738 (patch) | |
| tree | b33c3370c01b9b2ee685f1039543bc02f8764f7b /apps/web/src/services/renderer/nodes | |
| parent | 744419469da403be57d30b5b19f6b43246caa57d (diff) | |
| parent | 354f4dec67ef7eb39d34fca941ca34025a68136b (diff) | |
| download | opencut-5e7c27f542e94ffee5d2106ce318b6f43cc7e738.tar.gz | |
Merge branch 'dev' into ripple-editing
Diffstat (limited to 'apps/web/src/services/renderer/nodes')
8 files changed, 409 insertions, 214 deletions
diff --git a/apps/web/src/services/renderer/nodes/blur-background-node.ts b/apps/web/src/services/renderer/nodes/blur-background-node.ts deleted file mode 100644 index 504161d9..00000000 --- a/apps/web/src/services/renderer/nodes/blur-background-node.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { CanvasRenderer } from "../canvas-renderer"; -import { BaseNode } from "./base-node"; - -export type BlurBackgroundNodeParams = { - blurIntensity: number; - contentNodes: BaseNode[]; -}; - -export class BlurBackgroundNode extends BaseNode<BlurBackgroundNodeParams> { - private blurIntensity: number; - private contentNodes: BaseNode[]; - - constructor(params: BlurBackgroundNodeParams) { - super(params); - this.blurIntensity = params.blurIntensity; - this.contentNodes = params.contentNodes; - } - - async render({ - renderer, - time, - }: { - renderer: CanvasRenderer; - time: number; - }): Promise<void> { - let offscreen: OffscreenCanvas | HTMLCanvasElement; - let offscreenCtx: - | OffscreenCanvasRenderingContext2D - | CanvasRenderingContext2D; - - try { - offscreen = new OffscreenCanvas(renderer.width, renderer.height); - const ctx = offscreen.getContext("2d"); - if (!ctx) { - throw new Error("failed to get offscreen canvas context"); - } - offscreenCtx = ctx; - } catch { - offscreen = document.createElement("canvas"); - offscreen.width = renderer.width; - offscreen.height = renderer.height; - const ctx = offscreen.getContext("2d"); - if (!ctx) { - throw new Error("failed to get canvas context"); - } - offscreenCtx = ctx; - } - - const originalContext = renderer.context; - renderer.context = offscreenCtx; - - for (const node of this.contentNodes) { - await node.render({ renderer, time }); - } - - renderer.context = originalContext; - - const zoomScale = 1.4; - const scaledWidth = renderer.width * zoomScale; - const scaledHeight = renderer.height * zoomScale; - const offsetX = (renderer.width - scaledWidth) / 2; - const offsetY = (renderer.height - scaledHeight) / 2; - - renderer.context.save(); - renderer.context.filter = `blur(${this.blurIntensity}px)`; - renderer.context.drawImage( - offscreen as CanvasImageSource, - offsetX, - offsetY, - scaledWidth, - scaledHeight, - ); - renderer.context.restore(); - } -} diff --git a/apps/web/src/services/renderer/nodes/composite-effect-node.ts b/apps/web/src/services/renderer/nodes/composite-effect-node.ts new file mode 100644 index 00000000..3fbfffc1 --- /dev/null +++ b/apps/web/src/services/renderer/nodes/composite-effect-node.ts @@ -0,0 +1,77 @@ +import type { CanvasRenderer } from "../canvas-renderer"; +import { createOffscreenCanvas } from "../canvas-utils"; +import { getEffect } from "@/lib/effects"; +import type { EffectParamValues } from "@/types/effects"; +import { BaseNode } from "./base-node"; +import { webglEffectRenderer } from "../webgl-effect-renderer"; + +export type CompositeEffectNodeParams = { + contentNodes: BaseNode[]; + effectType: string; + effectParams: EffectParamValues; + scale: number; +}; + +export class CompositeEffectNode extends BaseNode<CompositeEffectNodeParams> { + async render({ + renderer, + time, + }: { + renderer: CanvasRenderer; + time: number; + }): Promise<void> { + const offscreen = createOffscreenCanvas({ + width: renderer.width, + height: renderer.height, + }); + const offscreenCtx = offscreen.getContext("2d") as OffscreenCanvasRenderingContext2D | null; + if (!offscreenCtx) { + throw new Error("failed to get offscreen canvas context"); + } + + const originalContext = renderer.context; + renderer.context = offscreenCtx; + + for (const node of this.params.contentNodes) { + await node.render({ renderer, time }); + } + + renderer.context = originalContext; + + const effectDefinition = getEffect({ effectType: this.params.effectType }); + const scale = this.params.scale; + const scaledWidth = renderer.width * scale; + const scaledHeight = renderer.height * scale; + const offsetX = (renderer.width - scaledWidth) / 2; + const offsetY = (renderer.height - scaledHeight) / 2; + + const passes = effectDefinition.renderer.passes.map((pass) => ({ + fragmentShader: pass.fragmentShader, + uniforms: pass.uniforms({ + effectParams: this.params.effectParams, + width: renderer.width, + height: renderer.height, + }), + })); + const effectResult = webglEffectRenderer.applyEffect({ + source: offscreen as CanvasImageSource, + width: renderer.width, + height: renderer.height, + passes, + }); + + renderer.context.save(); + renderer.context.drawImage( + effectResult, + 0, + 0, + renderer.width, + renderer.height, + offsetX, + offsetY, + scaledWidth, + scaledHeight, + ); + renderer.context.restore(); + } +} diff --git a/apps/web/src/services/renderer/nodes/effect-layer-node.ts b/apps/web/src/services/renderer/nodes/effect-layer-node.ts new file mode 100644 index 00000000..3d84be2b --- /dev/null +++ b/apps/web/src/services/renderer/nodes/effect-layer-node.ts @@ -0,0 +1,82 @@ +import type { CanvasRenderer } from "../canvas-renderer"; +import { getEffect } from "@/lib/effects"; +import type { EffectParamValues } from "@/types/effects"; +import { BaseNode } from "./base-node"; +import { webglEffectRenderer } from "../webgl-effect-renderer"; + +const TIME_EPSILON = 1e-6; + +export type EffectLayerNodeParams = { + effectType: string; + effectParams: EffectParamValues; + timeOffset: number; + duration: number; +}; + +function isInRange({ + time, + timeOffset, + duration, +}: { + time: number; + timeOffset: number; + duration: number; +}): boolean { + return ( + time >= timeOffset - TIME_EPSILON && + time < timeOffset + duration + TIME_EPSILON + ); +} + +// snapshots whatever is currently on the canvas, applies the effect, draws it back +export class EffectLayerNode extends BaseNode<EffectLayerNodeParams> { + async render({ + renderer, + time, + }: { + renderer: CanvasRenderer; + time: number; + }): Promise<void> { + if ( + !isInRange({ + time, + timeOffset: this.params.timeOffset, + duration: this.params.duration, + }) + ) { + return; + } + + const source = renderer.context.canvas as CanvasImageSource; + + const effectDefinition = getEffect({ + effectType: this.params.effectType, + }); + + const passes = effectDefinition.renderer.passes.map((pass) => ({ + fragmentShader: pass.fragmentShader, + uniforms: pass.uniforms({ + effectParams: this.params.effectParams, + width: renderer.width, + height: renderer.height, + }), + })); + const effectResult = webglEffectRenderer.applyEffect({ + source, + width: renderer.width, + height: renderer.height, + passes, + }); + + renderer.context.save(); + renderer.context.clearRect(0, 0, renderer.width, renderer.height); + renderer.context.drawImage( + effectResult, + 0, + 0, + renderer.width, + renderer.height, + ); + renderer.context.restore(); + } +} diff --git a/apps/web/src/services/renderer/nodes/image-node.ts b/apps/web/src/services/renderer/nodes/image-node.ts index 49e84ebe..6720a5e5 100644 --- a/apps/web/src/services/renderer/nodes/image-node.ts +++ b/apps/web/src/services/renderer/nodes/image-node.ts @@ -3,49 +3,88 @@ import { VisualNode, type VisualNodeParams } from "./visual-node"; export interface ImageNodeParams extends VisualNodeParams { url: string; + maxSourceSize?: number; } -export class ImageNode extends VisualNode<ImageNodeParams> { - private image?: HTMLImageElement; - private readyPromise: Promise<void>; +interface CachedImageSource { + source: HTMLImageElement | OffscreenCanvas; + width: number; + height: number; +} - constructor(params: ImageNodeParams) { - super(params); - this.readyPromise = this.load(); - } +const imageSourceCache = new Map<string, Promise<CachedImageSource>>(); + +function loadImageSource( + url: string, + maxSourceSize?: number, +): Promise<CachedImageSource> { + const cacheKey = `${url}::${maxSourceSize ?? "full"}`; - private async load() { + const cached = imageSourceCache.get(cacheKey); + if (cached) return cached; + + const promise = (async (): Promise<CachedImageSource> => { const image = new Image(); - this.image = image; await new Promise<void>((resolve, reject) => { image.onload = () => resolve(); image.onerror = () => reject(new Error("Image load failed")); - image.src = this.params.url; + image.src = url; }); - } - async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) { - await super.render({ renderer, time }); + const naturalWidth = image.naturalWidth; + const naturalHeight = image.naturalHeight; + const exceedsLimit = + maxSourceSize && + (naturalWidth > maxSourceSize || naturalHeight > maxSourceSize); - if (!this.isInRange(time)) { - return; + if (exceedsLimit) { + const scale = Math.min( + maxSourceSize / naturalWidth, + maxSourceSize / naturalHeight, + ); + const scaledWidth = Math.round(naturalWidth * scale); + const scaledHeight = Math.round(naturalHeight * scale); + + const offscreen = new OffscreenCanvas(scaledWidth, scaledHeight); + const ctx = offscreen.getContext("2d"); + + if (ctx) { + ctx.drawImage(image, 0, 0, scaledWidth, scaledHeight); + return { source: offscreen, width: scaledWidth, height: scaledHeight }; + } } - await this.readyPromise; + return { source: image, width: naturalWidth, height: naturalHeight }; + })(); + + imageSourceCache.set(cacheKey, promise); + return promise; +} + +export class ImageNode extends VisualNode<ImageNodeParams> { + private cachedSource: Promise<CachedImageSource>; + + constructor(params: ImageNodeParams) { + super(params); + this.cachedSource = loadImageSource(params.url, params.maxSourceSize); + } + + async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) { + await super.render({ renderer, time }); - if (!this.image) { + if (!this.isInRange({ time })) { return; } - const mediaW = this.image.naturalWidth || renderer.width; - const mediaH = this.image.naturalHeight || renderer.height; + const { source, width, height } = await this.cachedSource; this.renderVisual({ renderer, - source: this.image, - sourceWidth: mediaW, - sourceHeight: mediaH, + source, + sourceWidth: width || renderer.width, + sourceHeight: height || renderer.height, + timelineTime: time, }); } } diff --git a/apps/web/src/services/renderer/nodes/sticker-node.ts b/apps/web/src/services/renderer/nodes/sticker-node.ts index a2fb0170..da271685 100644 --- a/apps/web/src/services/renderer/nodes/sticker-node.ts +++ b/apps/web/src/services/renderer/nodes/sticker-node.ts @@ -6,49 +6,63 @@ export interface StickerNodeParams extends VisualNodeParams { stickerId: string; } -export class StickerNode extends VisualNode<StickerNodeParams> { - private image?: HTMLImageElement; - private readyPromise: Promise<void>; +interface CachedStickerSource { + source: HTMLImageElement; + width: number; + height: number; +} - constructor(params: StickerNodeParams) { - super(params); - this.readyPromise = this.load(); - } +const stickerSourceCache = new Map<string, Promise<CachedStickerSource>>(); - private async load() { - const image = new Image(); - this.image = image; +function loadStickerSource(stickerId: string): Promise<CachedStickerSource> { + const cached = stickerSourceCache.get(stickerId); + if (cached) return cached; + + const promise = (async (): Promise<CachedStickerSource> => { const url = resolveStickerId({ - stickerId: this.params.stickerId, + stickerId, options: { width: 200, height: 200 }, }); + const image = new Image(); + await new Promise<void>((resolve, reject) => { image.onload = () => resolve(); image.onerror = () => - reject(new Error(`Failed to load sticker: ${this.params.stickerId}`)); + reject(new Error(`Failed to load sticker: ${stickerId}`)); image.src = url; }); + + return { source: image, width: 200, height: 200 }; + })(); + + stickerSourceCache.set(stickerId, promise); + return promise; +} + +export class StickerNode extends VisualNode<StickerNodeParams> { + private cachedSource: Promise<CachedStickerSource>; + + constructor(params: StickerNodeParams) { + super(params); + this.cachedSource = loadStickerSource(params.stickerId); } async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) { await super.render({ renderer, time }); - if (!this.isInRange(time)) { + if (!this.isInRange({ time })) { return; } - await this.readyPromise; - - if (!this.image) { - return; - } + const { source, width, height } = await this.cachedSource; this.renderVisual({ renderer, - source: this.image, - sourceWidth: 200, - sourceHeight: 200, + source, + sourceWidth: width, + sourceHeight: height, + timelineTime: time, }); } } diff --git a/apps/web/src/services/renderer/nodes/text-node.ts b/apps/web/src/services/renderer/nodes/text-node.ts index b7e36d64..0ddb4eae 100644 --- a/apps/web/src/services/renderer/nodes/text-node.ts +++ b/apps/web/src/services/renderer/nodes/text-node.ts @@ -2,9 +2,21 @@ import type { CanvasRenderer } from "../canvas-renderer"; import { BaseNode } from "./base-node"; import type { TextElement } from "@/types/timeline"; import { + DEFAULT_TEXT_ELEMENT, DEFAULT_LINE_HEIGHT, FONT_SIZE_SCALE_REFERENCE, } from "@/constants/text-constants"; +import { + getMetricAscent, + getMetricDescent, + getTextBackgroundRect, + measureTextBlock, +} from "@/lib/text/layout"; +import { + getElementLocalTime, + resolveOpacityAtTime, + resolveTransformAtTime, +} from "@/lib/animation"; function scaleFontSize({ fontSize, @@ -20,65 +32,6 @@ function quoteFontFamily({ fontFamily }: { fontFamily: string }): string { return `"${fontFamily.replace(/"/g, '\\"')}"`; } -function getMetricAscent({ - metrics, - fallback, -}: { - metrics: TextMetrics; - fallback: number; -}): number { - return metrics.actualBoundingBoxAscent ?? fallback * 0.8; -} - -function getMetricDescent({ - metrics, - fallback, -}: { - metrics: TextMetrics; - fallback: number; -}): number { - return metrics.actualBoundingBoxDescent ?? fallback * 0.2; -} - -interface TextBlockMeasurement { - visualCenterOffset: number; - height: number; - maxWidth: number; -} - -function measureTextBlock({ - lineMetrics, - lineHeightPx, - fallbackFontSize, -}: { - lineMetrics: TextMetrics[]; - lineHeightPx: number; - fallbackFontSize: number; -}): TextBlockMeasurement { - let top = Number.POSITIVE_INFINITY; - let bottom = Number.NEGATIVE_INFINITY; - let maxWidth = 0; - - for (let i = 0; i < lineMetrics.length; i++) { - const metrics = lineMetrics[i]; - const y = i * lineHeightPx; - top = Math.min( - top, - y - getMetricAscent({ metrics, fallback: fallbackFontSize }), - ); - bottom = Math.max( - bottom, - y + getMetricDescent({ metrics, fallback: fallbackFontSize }), - ); - maxWidth = Math.max(maxWidth, metrics.width); - } - - const height = bottom - top; - const visualCenterOffset = (top + bottom) / 2; - - return { visualCenterOffset, height, maxWidth }; -} - function drawTextDecoration({ ctx, textDecoration, @@ -99,8 +52,8 @@ function drawTextDecoration({ if (textDecoration === "none" || !textDecoration) return; const thickness = Math.max(1, scaledFontSize * 0.07); - const ascent = getMetricAscent({ metrics, fallback: scaledFontSize }); - const descent = getMetricDescent({ metrics, fallback: scaledFontSize }); + const ascent = getMetricAscent({ metrics, fallbackFontSize: scaledFontSize }); + const descent = getMetricDescent({ metrics, fallbackFontSize: scaledFontSize }); let xStart = -lineWidth / 2; if (textAlign === "left") xStart = 0; @@ -138,16 +91,28 @@ export class TextNode extends BaseNode<TextNodeParams> { renderer.context.save(); - const x = this.params.transform.position.x + this.params.canvasCenter.x; - const y = this.params.transform.position.y + this.params.canvasCenter.y; + const localTime = getElementLocalTime({ + timelineTime: time, + elementStartTime: this.params.startTime, + elementDuration: this.params.duration, + }); + const transform = resolveTransformAtTime({ + baseTransform: this.params.transform, + animations: this.params.animations, + localTime, + }); + const opacity = resolveOpacityAtTime({ + baseOpacity: this.params.opacity, + animations: this.params.animations, + localTime, + }); + const x = transform.position.x + this.params.canvasCenter.x; + const y = transform.position.y + this.params.canvasCenter.y; renderer.context.translate(x, y); - renderer.context.scale( - this.params.transform.scale, - this.params.transform.scale, - ); - if (this.params.transform.rotate) { - renderer.context.rotate((this.params.transform.rotate * Math.PI) / 180); + renderer.context.scale(transform.scale, transform.scale); + if (transform.rotate) { + renderer.context.rotate((transform.rotate * Math.PI) / 180); } const fontWeight = this.params.fontWeight === "bold" ? "bold" : "normal"; @@ -171,6 +136,7 @@ export class TextNode extends BaseNode<TextNodeParams> { const lines = this.params.content.split("\n"); const lineHeightPx = scaledFontSize * lineHeight; + const fontSizeRatio = this.params.fontSize / DEFAULT_TEXT_ELEMENT.fontSize; const baseline = this.params.textBaseline ?? "middle"; renderer.context.textBaseline = baseline; @@ -189,24 +155,33 @@ export class TextNode extends BaseNode<TextNodeParams> { ? this.params.blendMode : "source-over" ) as GlobalCompositeOperation; - renderer.context.globalAlpha = this.params.opacity; + renderer.context.globalAlpha = opacity; - if (this.params.backgroundColor && lineCount > 0) { - const padX = 8; - const padY = 4; - renderer.context.fillStyle = this.params.backgroundColor; - let bgLeft = -block.maxWidth / 2; - if (renderer.context.textAlign === "left") bgLeft = 0; - if (renderer.context.textAlign === "right") bgLeft = -block.maxWidth; - - renderer.context.fillRect( - bgLeft - padX, - -block.height / 2 - padY, - block.maxWidth + padX * 2, - block.height + padY * 2, - ); - - renderer.context.fillStyle = this.params.color; + if ( + this.params.background.color && + this.params.background.color !== "transparent" && + lineCount > 0 + ) { + const { color, cornerRadius = 0 } = this.params.background; + const backgroundRect = getTextBackgroundRect({ + textAlign: this.params.textAlign, + block, + background: this.params.background, + fontSizeRatio, + }); + if (backgroundRect) { + renderer.context.fillStyle = color; + renderer.context.beginPath(); + renderer.context.roundRect( + backgroundRect.left, + backgroundRect.top, + backgroundRect.width, + backgroundRect.height, + cornerRadius, + ); + renderer.context.fill(); + renderer.context.fillStyle = this.params.color; + } } for (let i = 0; i < lineCount; i++) { diff --git a/apps/web/src/services/renderer/nodes/video-node.ts b/apps/web/src/services/renderer/nodes/video-node.ts index 8e2ddccd..503c9c03 100644 --- a/apps/web/src/services/renderer/nodes/video-node.ts +++ b/apps/web/src/services/renderer/nodes/video-node.ts @@ -12,11 +12,11 @@ export class VideoNode extends VisualNode<VideoNodeParams> { async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) { await super.render({ renderer, time }); - if (!this.isInRange(time)) { + if (!this.isInRange({ time })) { return; } - const videoTime = this.getLocalTime(time); + const videoTime = this.getSourceLocalTime({ time }); const frame = await videoCache.getFrameAt({ mediaId: this.params.mediaId, file: this.params.file, @@ -29,6 +29,7 @@ export class VideoNode extends VisualNode<VideoNodeParams> { source: frame.canvas, sourceWidth: frame.canvas.width, sourceHeight: frame.canvas.height, + timelineTime: time, }); } } 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(); } } |
