feat(canvas): move image pipeline rendering off main thread with worker fallback
This commit is contained in:
@@ -10,31 +10,76 @@ export type PreviewRenderResult = {
|
||||
histogram: HistogramData;
|
||||
};
|
||||
|
||||
type PreviewCanvas = HTMLCanvasElement | OffscreenCanvas;
|
||||
type PreviewContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
||||
|
||||
function createPreviewContext(width: number, height: number): PreviewContext {
|
||||
if (typeof document !== "undefined") {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) {
|
||||
throw new Error("Preview renderer could not create 2D context.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
if (typeof OffscreenCanvas !== "undefined") {
|
||||
const canvas: PreviewCanvas = new OffscreenCanvas(width, height);
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) {
|
||||
throw new Error("Preview renderer could not create offscreen 2D context.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
throw new Error("Preview rendering is not available in this environment.");
|
||||
}
|
||||
|
||||
async function yieldToMainOrWorkerLoop(): Promise<void> {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
requestAnimationFrame(() => resolve());
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => resolve(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
export async function renderPreview(options: {
|
||||
sourceUrl: string;
|
||||
steps: readonly PipelineStep[];
|
||||
previewWidth: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<PreviewRenderResult> {
|
||||
const bitmap = await loadSourceBitmap(options.sourceUrl);
|
||||
const bitmap = await loadSourceBitmap(options.sourceUrl, {
|
||||
signal: options.signal,
|
||||
});
|
||||
const width = Math.max(1, Math.round(options.previewWidth));
|
||||
const height = Math.max(1, Math.round((bitmap.height / bitmap.width) * width));
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) {
|
||||
throw new Error("Preview renderer could not create 2D context.");
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted.", "AbortError");
|
||||
}
|
||||
|
||||
const context = createPreviewContext(width, height);
|
||||
|
||||
context.drawImage(bitmap, 0, 0, width, height);
|
||||
const imageData = context.getImageData(0, 0, width, height);
|
||||
|
||||
for (let index = 0; index < options.steps.length; index += 1) {
|
||||
applyPipelineStep(imageData.data, options.steps[index]!, width, height);
|
||||
await new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
applyPipelineStep(imageData.data, options.steps[index]!, width, height, {
|
||||
shouldAbort: () => Boolean(options.signal?.aborted),
|
||||
});
|
||||
await yieldToMainOrWorkerLoop();
|
||||
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted.", "AbortError");
|
||||
}
|
||||
}
|
||||
|
||||
const histogram = computeHistogram(imageData.data);
|
||||
|
||||
Reference in New Issue
Block a user