diff options
Diffstat (limited to 'apps/web/src/services/renderer/gpu-renderer.ts')
| -rw-r--r-- | apps/web/src/services/renderer/gpu-renderer.ts | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/apps/web/src/services/renderer/gpu-renderer.ts b/apps/web/src/services/renderer/gpu-renderer.ts new file mode 100644 index 00000000..91b03603 --- /dev/null +++ b/apps/web/src/services/renderer/gpu-renderer.ts @@ -0,0 +1,118 @@ +import { + applyEffectPasses, + applyMaskFeather as applyMaskFeatherWasm, + initializeGpu, +} from "opencut-wasm"; +import type { EffectPass, EffectUniformValue } from "@/lib/effects/types"; + +let initializeGpuRendererPromise: Promise<void> | null = null; + +export function initializeGpuRenderer(): Promise<void> { + if (!initializeGpuRendererPromise) { + initializeGpuRendererPromise = initializeGpu(); + } + const promise = initializeGpuRendererPromise; + if (!promise) { + throw new Error("GPU renderer initialization promise was not created"); + } + return promise; +} + +export const gpuRenderer = { + applyEffect({ + source, + width, + height, + passes, + }: { + source: CanvasImageSource; + width: number; + height: number; + passes: EffectPass[]; + }): CanvasImageSource { + if (passes.length === 0) { + return source; + } + + const sourceCanvas = ensureOffscreenCanvas({ + source, + width, + height, + label: "effect source", + }); + return applyEffectPasses({ + source: sourceCanvas, + width, + height, + passes: serializeEffectPasses(passes), + }); + }, + + applyMaskFeather({ + maskCanvas, + width, + height, + feather, + }: { + maskCanvas: CanvasImageSource; + width: number; + height: number; + feather: number; + }): CanvasImageSource { + const sourceCanvas = ensureOffscreenCanvas({ + source: maskCanvas, + width, + height, + label: "mask source", + }); + return applyMaskFeatherWasm({ + mask: sourceCanvas, + width, + height, + feather, + }); + }, +}; + +function ensureOffscreenCanvas({ + source, + width, + height, + label, +}: { + source: CanvasImageSource; + width: number; + height: number; + label: string; +}): OffscreenCanvas { + if (source instanceof OffscreenCanvas) { + return source; + } + + if (typeof OffscreenCanvas === "undefined") { + throw new Error(`OffscreenCanvas is required for the GPU ${label}`); + } + + const canvas = new OffscreenCanvas(width, height); + const context = canvas.getContext("2d"); + if (!context) { + throw new Error(`Failed to get 2d context for the GPU ${label}`); + } + context.clearRect(0, 0, width, height); + context.drawImage(source, 0, 0, width, height); + return canvas; +} + +function serializeEffectPasses(passes: EffectPass[]) { + return passes.map((pass) => ({ + shader: pass.shader, + uniforms: Object.entries(pass.uniforms).map(([name, value]) => ({ + name, + value: normalizeUniformValue(value), + })), + })); +} + +function normalizeUniformValue(value: EffectUniformValue): number[] { + return typeof value === "number" ? [value] : value; +} |
