import { detect2dCanvasBlockOnCanvas } from './detect-canvas-block'; export const shuffleInPlace = (a: T): T => { a.sort(() => Math.random() - 0.5); return a; }; export const canvascopy = async ( image: HTMLImageElement, artifacts: 'fix' | 'keep' = 'fix', getBlobURL: () => string | undefined, setBlobURL: (url: string | undefined) => void, tick: () => Promise, newBlob: (blob: string) => void, getAndRemoveRandomBlob: () => string, betweenSegments: undefined | ((ourPromise: Promise) => Promise | void) ) => { console.debug('[canvascopy] Entering canvascopy'); const canvas = document.createElement('canvas'); const canvasOpts = { desynchronized: true, alpha: true, willReadFrequently: true, // change to unorm8 to use 8-bit colours (will break HDR images) colorType: 'float16' as const, // change to srgb if colours look off colorSpace: 'display-p3' as const, } as const; const e = detect2dCanvasBlockOnCanvas(canvas, false, { ...canvasOpts, willReadFrequently: false, }); if (e) throw e; console.debug('[canvascopy] Canvas OK'); try { // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalHeight const canvasWidth = (canvas.width = image.naturalWidth); const canvasHeight = (canvas.height = image.naturalHeight); const ctx = canvas.getContext('2d', canvasOpts)!; const doIntermittentBlob = true; if (doIntermittentBlob) { ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight); // First, convert to blob, make sure that goes well setBlobURL(URL.createObjectURL( await new Promise((rs, rj) => canvas.toBlob((value) => value ? rs(value) : rj(new Error('No blob')), ), ), )); await tick(); } // TODO: For GNOME Web, we need to check if drawImage is implemented const oldBlob = doIntermittentBlob ? getBlobURL() : void 0; ctx.clearRect(0, 0, canvasWidth, canvasHeight); const segmentPromises = [] as Promise[]; const xSegments = 8, ySegments = 4; const xSegmentWidth = canvasWidth / xSegments, ySegmentHeight = canvasHeight / ySegments; const pad = artifacts === 'fix' ? 4 : 0; for (let x = 0; x < xSegments; x++) for (let y = 0; y < ySegments; y++) { const padLeft = x === 0 ? 0 : pad; const padRight = x === xSegments - 1 ? 0 : pad; const padTop = y === 0 ? 0 : pad; const padBottom = y === ySegments - 1 ? 0 : pad; const xStart = x * xSegmentWidth - padLeft, xWidth = xSegmentWidth + padLeft + padRight, yStart = y * ySegmentHeight - padTop, yHeight = ySegmentHeight + padTop + padBottom; ctx.drawImage( image, xStart, yStart, xWidth, yHeight, xStart, yStart, xWidth, yHeight, ); const segmentPromise = new Promise((rs, rj) => canvas.toBlob((value) => value ? rs(URL.createObjectURL(value)) : rj(new Error('No blob')), ), ); segmentPromises.push(segmentPromise.then(v => newBlob(v))); console.debug('[canvascopy] Push overlayed blob'); if (betweenSegments) await betweenSegments(segmentPromise) ctx.clearRect(xStart, yStart, xWidth, yHeight); } await Promise.all(segmentPromises) if (doIntermittentBlob) await tick(); setBlobURL(getAndRemoveRandomBlob()); if (doIntermittentBlob) URL.revokeObjectURL(oldBlob!); console.debug('[canvascopy] Done!'); } catch (error) { console.error('[canvascopy] Failed to get canvas data:', error); } };