diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-25 16:47:22 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-29 15:48:22 +0200 |
| commit | 8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch) | |
| tree | e9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/services/renderer/nodes/blur-background-node.ts | |
| parent | 39ea298a9ccba3104c549c0ca079c231981811f5 (diff) | |
| download | opencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz | |
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/services/renderer/nodes/blur-background-node.ts')
| -rw-r--r-- | apps/web/src/services/renderer/nodes/blur-background-node.ts | 149 |
1 files changed, 149 insertions, 0 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 new file mode 100644 index 00000000..7f4d06fc --- /dev/null +++ b/apps/web/src/services/renderer/nodes/blur-background-node.ts @@ -0,0 +1,149 @@ +import { TIME_EPSILON_SECONDS } from "@/constants/animation-constants"; +import { buildGaussianBlurPasses } from "@/lib/effects/definitions/blur"; +import { getSourceTimeAtClipTime } from "@/lib/retime"; +import { videoCache } from "@/services/video-cache/service"; +import type { RetimeConfig } from "@/lib/timeline"; +import type { CanvasRenderer } from "../canvas-renderer"; +import { createOffscreenCanvas } from "../canvas-utils"; +import { webglEffectRenderer } from "../webgl/webgl-effect-renderer"; +import { BaseNode } from "./base-node"; +import { loadImageSource, type CachedImageSource } from "./image-node"; + +export type BlurBackgroundNodeParams = { + mediaId: string; + url: string; + file: File; + mediaType: "video" | "image"; + duration: number; + timeOffset: number; + trimStart: number; + trimEnd: number; + retime?: RetimeConfig; + blurIntensity: number; +}; + +type BackdropSource = { + source: CanvasImageSource; + width: number; + height: number; +}; + +export class BlurBackgroundNode extends BaseNode<BlurBackgroundNodeParams> { + private cachedImageSource: Promise<CachedImageSource> | null; + + constructor(params: BlurBackgroundNodeParams) { + super(params); + this.cachedImageSource = + params.mediaType === "image" ? loadImageSource(params.url) : null; + } + + private isInRange({ time }: { time: number }): boolean { + const localTime = time - this.params.timeOffset; + return ( + localTime >= -TIME_EPSILON_SECONDS && localTime < this.params.duration + ); + } + + private getSourceLocalTime({ time }: { time: number }): number { + const clipTime = time - this.params.timeOffset; + return ( + this.params.trimStart + + getSourceTimeAtClipTime({ + clipTime, + retime: this.params.retime, + }) + ); + } + + private async getBackdropSource({ + time, + }: { + time: number; + }): Promise<BackdropSource | null> { + if (this.params.mediaType === "video") { + const frame = await videoCache.getFrameAt({ + mediaId: this.params.mediaId, + file: this.params.file, + time: this.getSourceLocalTime({ time }), + }); + + if (!frame) { + return null; + } + + return { + source: frame.canvas, + width: frame.canvas.width, + height: frame.canvas.height, + }; + } + + if (!this.cachedImageSource) { + return null; + } + + const { source, width, height } = await this.cachedImageSource; + return { source, width, height }; + } + + async render({ renderer, time }: { renderer: CanvasRenderer; time: number }) { + await super.render({ renderer, time }); + + if (!this.isInRange({ time })) { + return; + } + + const backdropSource = await this.getBackdropSource({ time }); + if (!backdropSource) { + return; + } + + const offscreen = createOffscreenCanvas({ + width: renderer.width, + height: renderer.height, + }); + const offscreenCtx = offscreen.getContext("2d") as + | CanvasRenderingContext2D + | OffscreenCanvasRenderingContext2D + | null; + if (!offscreenCtx) { + return; + } + + const coverScale = Math.max( + renderer.width / backdropSource.width, + renderer.height / backdropSource.height, + ); + const scaledWidth = backdropSource.width * coverScale; + const scaledHeight = backdropSource.height * coverScale; + const offsetX = (renderer.width - scaledWidth) / 2; + const offsetY = (renderer.height - scaledHeight) / 2; + + offscreenCtx.drawImage( + backdropSource.source, + offsetX, + offsetY, + scaledWidth, + scaledHeight, + ); + + const passes = buildGaussianBlurPasses({ + sigmaX: this.params.blurIntensity * (renderer.width / 1920), + sigmaY: this.params.blurIntensity * (renderer.height / 1080), + }); + const effectResult = webglEffectRenderer.applyEffect({ + source: offscreen as CanvasImageSource, + width: renderer.width, + height: renderer.height, + passes, + }); + + renderer.context.drawImage( + effectResult, + 0, + 0, + renderer.width, + renderer.height, + ); + } +} |
