import type { FrameRate } from "opencut-wasm"; import type { AnyBaseNode } from "./nodes/base-node"; import { buildFrameDescriptor } from "./compositor/frame-descriptor"; import { wasmCompositor } from "./compositor/wasm-compositor"; import { resolveRenderTree } from "./resolve"; import { measureSpanAsync, measureSpanSync, onRenderPerfFrameComplete, } from "@/diagnostics/render-perf"; export type CanvasRendererParams = { width: number; height: number; fps: FrameRate; }; export class CanvasRenderer { canvas: OffscreenCanvas | HTMLCanvasElement; context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D; width: number; height: number; fps: FrameRate; constructor({ width, height, fps }: CanvasRendererParams) { this.width = width; this.height = height; this.fps = fps; try { this.canvas = new OffscreenCanvas(width, height); } catch { this.canvas = document.createElement("canvas"); this.canvas.width = width; this.canvas.height = height; } const context = this.canvas.getContext("2d"); if (!context) { throw new Error("Failed to get canvas context"); } this.context = context as | OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D; } getOutputCanvas(): HTMLCanvasElement { wasmCompositor.ensureInitialized({ width: this.width, height: this.height, }); return wasmCompositor.getCanvas(); } setSize({ width, height }: { width: number; height: number }) { this.width = width; this.height = height; if (this.canvas instanceof OffscreenCanvas) { this.canvas = new OffscreenCanvas(width, height); } else { this.canvas.width = width; this.canvas.height = height; } const context = this.canvas.getContext("2d"); if (!context) { throw new Error("Failed to get canvas context"); } this.context = context as | OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D; } async render({ node, time }: { node: AnyBaseNode; time: number }) { await measureSpanAsync({ name: "resolve", fn: () => resolveRenderTree({ node, renderer: this, time }), }); const { frame, textures } = await measureSpanAsync({ name: "buildFrame", fn: () => buildFrameDescriptor({ node, renderer: this }), }); wasmCompositor.ensureInitialized({ width: this.width, height: this.height, }); measureSpanSync({ name: "syncTextures", fn: () => wasmCompositor.syncTextures(textures), }); measureSpanSync({ name: "renderFrame", fn: () => wasmCompositor.render(frame), }); } async renderToCanvas({ node, time, targetCanvas, }: { node: AnyBaseNode; time: number; targetCanvas: HTMLCanvasElement; }) { await this.render({ node, time }); const ctx = targetCanvas.getContext("2d"); if (!ctx) { throw new Error("Failed to get target canvas context"); } measureSpanSync({ name: "drawImage", fn: () => ctx.drawImage( wasmCompositor.getCanvas(), 0, 0, targetCanvas.width, targetCanvas.height, ), }); onRenderPerfFrameComplete(); } }