From a357a83860aaac90b37832de066e5a5a016910ea Mon Sep 17 00:00:00 2001 From: memdmp Date: Wed, 25 Mar 2026 12:22:23 +0100 Subject: feat: figured i'd revisit this code i wrote as a shitpost, it seems quite decent actually --- src/lib/test/canvas/CanvasCopy.svelte | 148 +++++++++++++++++++++++++++++ src/lib/test/canvas/CanvasCopyLib.ts | 113 ++++++++++++++++++++++ src/lib/test/canvas/CanvasImg.svelte | 56 +++++++++++ src/lib/test/canvas/detect-canvas-block.ts | 46 +++++++++ 4 files changed, 363 insertions(+) create mode 100644 src/lib/test/canvas/CanvasCopy.svelte create mode 100644 src/lib/test/canvas/CanvasCopyLib.ts create mode 100644 src/lib/test/canvas/CanvasImg.svelte create mode 100644 src/lib/test/canvas/detect-canvas-block.ts (limited to 'src/lib') diff --git a/src/lib/test/canvas/CanvasCopy.svelte b/src/lib/test/canvas/CanvasCopy.svelte new file mode 100644 index 0000000..7795116 --- /dev/null +++ b/src/lib/test/canvas/CanvasCopy.svelte @@ -0,0 +1,148 @@ + + + + +
+ {additionalOverlayedBlobsDone (blobUrl !== undefined ? void 0 : (imgloaded = true))} + onloadstart={() => (blobUrl !== undefined ? void 0 : (imgloaded = false))} + bind:this={image} + /> + {#if additionalOverlayedBlobsDone} + {#each additionalOverlayedBlobs + .map( (v, i, a) => (useBg ? (i % 2 === 0 ? ([v, a[i + 1]] as [string, string | undefined]) : undefined!) : ([v] as const)), ) + .filter((v) => v !== undefined) as [blob1, blob2]} + + {/each} + {/if} +
+ + diff --git a/src/lib/test/canvas/CanvasCopyLib.ts b/src/lib/test/canvas/CanvasCopyLib.ts new file mode 100644 index 0000000..96d4e3d --- /dev/null +++ b/src/lib/test/canvas/CanvasCopyLib.ts @@ -0,0 +1,113 @@ +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); + } +}; diff --git a/src/lib/test/canvas/CanvasImg.svelte b/src/lib/test/canvas/CanvasImg.svelte new file mode 100644 index 0000000..ef9c870 --- /dev/null +++ b/src/lib/test/canvas/CanvasImg.svelte @@ -0,0 +1,56 @@ + + + + blobify((e.currentTarget ?? e.target) as unknown as HTMLImageElement)} +/> diff --git a/src/lib/test/canvas/detect-canvas-block.ts b/src/lib/test/canvas/detect-canvas-block.ts new file mode 100644 index 0000000..4557213 --- /dev/null +++ b/src/lib/test/canvas/detect-canvas-block.ts @@ -0,0 +1,46 @@ +export const detect2dCanvasBlockOnCanvas = (canvas: HTMLCanvasElement, resetDimensions = true, canvasOpts: CanvasRenderingContext2D | CanvasRenderingContext2DSettings = { + desynchronized: true, + alpha: true, + willReadFrequently: false, + // @ts-ignore type is incomplete + colorType: 'float16' as const, + colorSpace: 'display-p3' as const, +}): null | Error => { + const dimensions = resetDimensions ? [canvas.width, canvas.height] as const : [0, 0] as const; + const [w, h] = [255, 255]; + const checks = 32; + const ctx = canvasOpts instanceof CanvasRenderingContext2D && canvasOpts && canvasOpts.canvas.width >= 256 && canvasOpts.canvas.height >= 256 ? canvasOpts : (() => { + [canvas.width, canvas.height] = [w, h]; + return canvas.getContext('2d', canvasOpts) as CanvasRenderingContext2D | null + })(); + if (ctx) { + try { + // make sure we have canvas support + ctx.fillStyle = '#0099ff'; + ctx.fillRect(0, 0, w, h); + for (let i = 0; i < checks; i++) { + const data = ctx.getImageData(i % w, i % h, 1, 1).data; + if ( + data[0] !== 0 || + data[1] !== 153 || + data[2] !== 255 || + data[3] !== 255 + ) { + if (resetDimensions) [canvas.width, canvas.height] = dimensions; + return new Error("Canvas Data doesn't match written data."); + } + } + ctx.clearRect(0, 0, w, h); + } catch (error) { + if (resetDimensions) [canvas.width, canvas.height] = dimensions; + return new Error('No canvas support', { + cause: error + }); + } + } else { + if (resetDimensions) [canvas.width, canvas.height] = dimensions; + return new Error('No canvas support: ctx is undefined.'); + } + return null; +} +export const checkFor2dCanvasSupport = () => detect2dCanvasBlockOnCanvas(document.createElement('canvas'), false) -- cgit v1.2.3