import { createOffscreenCanvas } from "./canvas-utils"; import { effectsRegistry, resolveEffectPasses } from "@/lib/effects"; import { buildDefaultParamValues } from "@/lib/registry"; import type { ParamValues } from "@/lib/params"; import { gpuRenderer } from "./gpu-renderer"; const PREVIEW_SIZE = 160; const PREVIEW_IMAGE_PATH = "/effects/preview.jpg"; class EffectPreviewService { private testSourceCanvas: OffscreenCanvas | HTMLCanvasElement | null = null; private previewImageElement: HTMLImageElement | null = null; private onReadyCallbacks = new Set<() => void>(); readonly PREVIEW_SIZE = PREVIEW_SIZE; constructor() { this.loadPreviewImage(); } onPreviewImageReady({ callback, }: { callback: () => void; }): () => void { this.onReadyCallbacks.add(callback); return () => this.onReadyCallbacks.delete(callback); } renderPreview({ effectType, params, targetCanvas, uniformDimensions, }: { effectType: string; params: ParamValues; targetCanvas: HTMLCanvasElement; uniformDimensions?: { width: number; height: number }; }): void { const size = PREVIEW_SIZE; const source = this.getTestSource({ width: size, height: size }); if (!source) return; const definition = effectsRegistry.get(effectType); const resolvedParams = Object.keys(params).length > 0 ? params : buildDefaultParamValues(definition.params); const passes = resolveEffectPasses({ definition, effectParams: resolvedParams, width: uniformDimensions?.width ?? size, height: uniformDimensions?.height ?? size, }); const result = this.applyGpuEffect({ source, width: size, height: size, passes, }); const targetCtx = targetCanvas.getContext( "2d", ) as CanvasRenderingContext2D | null; if (targetCtx) { targetCanvas.width = size; targetCanvas.height = size; targetCtx.drawImage(result, 0, 0, size, size); } } private loadPreviewImage(): void { if (typeof window === "undefined") return; const image = new Image(); image.onload = () => { this.testSourceCanvas = null; for (const callback of this.onReadyCallbacks) { callback(); } }; image.src = PREVIEW_IMAGE_PATH; this.previewImageElement = image; } private createTestSource({ width, height, }: { width: number; height: number; }): OffscreenCanvas | HTMLCanvasElement | null { const isImageReady = this.previewImageElement?.complete && (this.previewImageElement.naturalWidth ?? 0) > 0; if (!isImageReady || !this.previewImageElement) { return null; } const canvas = createOffscreenCanvas({ width, height }); const ctx = canvas.getContext("2d") as | CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null; if (!ctx) { throw new Error("failed to get 2d context for test source"); } ctx.drawImage(this.previewImageElement, 0, 0, width, height); return canvas; } private getTestSource({ width, height, }: { width: number; height: number; }): CanvasImageSource | null { if ( !this.testSourceCanvas || this.testSourceCanvas.width !== width || this.testSourceCanvas.height !== height ) { this.testSourceCanvas = this.createTestSource({ width, height }); } return this.testSourceCanvas; } private applyGpuEffect({ source, width, height, passes, }: { source: CanvasImageSource; width: number; height: number; passes: ReturnType; }): OffscreenCanvas | HTMLCanvasElement { return gpuRenderer.applyEffect({ source, width, height, passes, }) as OffscreenCanvas | HTMLCanvasElement; } } export const effectPreviewService = new EffectPreviewService();