summaryrefslogtreecommitdiff
path: root/apps/web/src/services/renderer/gpu-renderer.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/services/renderer/gpu-renderer.ts')
-rw-r--r--apps/web/src/services/renderer/gpu-renderer.ts118
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;
+}