aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatarLarge Libravatar memdmp <memdmpestrogenzone>2026-03-25 12:22:23 +0100
committerLibravatarLarge Libravatar memdmp <memdmpestrogenzone>2026-03-25 12:22:23 +0100
commita357a83860aaac90b37832de066e5a5a016910ea (patch)
treefc1b3e812556dc0d05294ea8f65473e3628fae42
parenteef6a55c5efdc4c542a1a9011b59d6917b9772b5 (diff)
downloadmem-estrogen-zone-a357a83860aaac90b37832de066e5a5a016910ea.tar.gz
mem-estrogen-zone-a357a83860aaac90b37832de066e5a5a016910ea.tar.bz2
mem-estrogen-zone-a357a83860aaac90b37832de066e5a5a016910ea.tar.lz
mem-estrogen-zone-a357a83860aaac90b37832de066e5a5a016910ea.zip

feat: figured i'd revisit this code i wrote as a shitpost, it seems quite decent actually

-rw-r--r--src/lib/test/canvas/CanvasCopy.svelte148
-rw-r--r--src/lib/test/canvas/CanvasCopyLib.ts113
-rw-r--r--src/lib/test/canvas/CanvasImg.svelte56
-rw-r--r--src/lib/test/canvas/detect-canvas-block.ts46
-rw-r--r--src/routes/playground/canvas/+page.svelte54
5 files changed, 417 insertions, 0 deletions
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 @@
+<!-- Small test for making copy-pasting images harder (without putting any major technical restrictions in there, and keeping the site itself working if it errors) -->
+
+<script lang="ts">
+ import { onDestroy, tick } from 'svelte';
+ import { canvascopy, shuffleInPlace } from './CanvasCopyLib';
+
+ let {
+ src,
+ alt,
+ loading = 'lazy',
+ width,
+ height,
+ artifacts = 'fix',
+ useBg = false,
+ }: {
+ src: string;
+ alt: string;
+ width?: number;
+ height?: number;
+ loading?: 'eager' | 'lazy' | undefined | null;
+ artifacts?: 'fix' | 'keep';
+ useBg?: boolean;
+ } = $props();
+ let blobUrl = $state(undefined as undefined | string);
+ let additionalOverlayedBlobs = $state([] as string[]);
+ let additionalOverlayedBlobsDone = $state(false);
+ // svelte-ignore state_referenced_locally
+ let lastLoadedSrc = $state(src);
+
+ const blobify = (
+ image: HTMLImageElement,
+ artifacts: 'fix' | 'keep' = 'fix',
+ ) => {
+ firstLoad = false;
+ if (image.src.startsWith('blob:') || image.src === blobUrl) {
+ // The image is likely a partial of the image. Tell the browser to reload the original
+ lastLoadedSrc = '';
+ } else {
+ console.debug('[blobify] Entering blobify');
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
+ if (additionalOverlayedBlobs.length) {
+ for (const blobUrl of additionalOverlayedBlobs)
+ URL.revokeObjectURL(blobUrl);
+ additionalOverlayedBlobs.length = 0;
+ }
+ additionalOverlayedBlobsDone = false;
+
+ // Need to update lastLoaded to prevent unneeded refreshes
+ console.debug('[blobify] Set lastload to', src);
+ const lastLoad = lastLoadedSrc;
+ lastLoadedSrc = src;
+
+ let nextPerf = 0;
+ canvascopy(
+ image,
+ artifacts,
+ () => blobUrl,
+ (blob) => (blobUrl = blob),
+ tick,
+ (blob) => additionalOverlayedBlobs.push(blob),
+ () => shuffleInPlace(additionalOverlayedBlobs).shift()!,
+ () => {
+ const p = performance.now();
+ if (p >= nextPerf) {
+ nextPerf = p + 10;
+ return new Promise((rs) => requestAnimationFrame(() => rs(void 0)));
+ }
+ },
+ )
+ .then(() => {
+ additionalOverlayedBlobsDone = true;
+ })
+ .catch((e) => {
+ lastLoadedSrc = lastLoad;
+ throw e;
+ });
+ }
+ };
+
+ let imgloaded = $state(false);
+ let image = $state(null as null | HTMLImageElement);
+
+ $effect(() => {
+ if (lastLoadedSrc !== src && blobUrl) {
+ URL.revokeObjectURL(blobUrl);
+ blobUrl = undefined;
+ imgloaded = false;
+ console.debug('[effect] Revoking blobURL due to src change');
+ }
+ });
+ let firstLoad = true;
+ $effect(() => {
+ if (imgloaded && image) {
+ console.debug('[effect][imgload] Calling blobify. State:', {
+ imgloaded,
+ image,
+ artifacts,
+ });
+ if (firstLoad) {
+ requestIdleCallback(() => blobify(image!, artifacts));
+ firstLoad = false;
+ } else requestAnimationFrame(() => blobify(image!, artifacts));
+ }
+ });
+
+ onDestroy(() => {
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
+ });
+</script>
+
+<div
+ class="relative max-w-max max-h-max block"
+ aria-label={additionalOverlayedBlobsDone ? alt : undefined}
+ role={additionalOverlayedBlobsDone ? 'img' : undefined}
+>
+ <img
+ class="select-none"
+ {loading}
+ src={blobUrl ?? src}
+ alt={additionalOverlayedBlobsDone ? undefined : alt}
+ {width}
+ {height}
+ onload={() => (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]}
+ <img
+ src={blob1}
+ {loading}
+ alt=""
+ class="blob-img"
+ style={blob2 ? `background-image: url(${JSON.stringify(blob2)});}` : ''}
+ />
+ {/each}
+ {/if}
+</div>
+
+<style lang="postcss">
+ @reference "tailwindcss";
+
+ .blob-img {
+ @apply absolute top-0 left-0 h-full w-full bg-contain select-none;
+ }
+</style>
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 = <T extends any[]>(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<void>,
+ newBlob: (blob: string) => void,
+ getAndRemoveRandomBlob: () => string,
+ betweenSegments: undefined | ((ourPromise: Promise<string>) => Promise<void> | 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<Blob>((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<void>[];
+
+ 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<string>((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 @@
+<script lang="ts">
+ import { onDestroy } from 'svelte';
+ import { detect2dCanvasBlockOnCanvas } from './detect-canvas-block';
+
+ let { src, alt }: { src: string; alt: string } = $props();
+ let blobUrl = $state(undefined as undefined | string);
+
+ const blobify = async (image: HTMLImageElement) => {
+ if (!image.src.startsWith('blob:')) {
+ const canvas = document.createElement('canvas');
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
+ const canvasOpts = {
+ desynchronized: true,
+ alpha: true,
+ willReadFrequently: false,
+ // 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);
+ if (e) throw e;
+ try {
+ // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalHeight
+ const cw = (canvas.width = image.naturalWidth);
+ const ch = (canvas.height = image.naturalHeight);
+
+ const ctx = canvas.getContext('2d', canvasOpts)!;
+ ctx.drawImage(image, 0, 0, cw, ch);
+
+ blobUrl = URL.createObjectURL(
+ await new Promise<Blob>((rs, rj) =>
+ canvas.toBlob((value) =>
+ value ? rs(value) : rj(new Error('No blob')),
+ ),
+ ),
+ );
+ } catch (error) {
+ console.error('Failed to get canvas data:', error);
+ }
+ }
+ };
+
+ onDestroy(() => {
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
+ });
+</script>
+
+<img
+ class="pointer-events-none"
+ loading="lazy"
+ src={blobUrl ?? src}
+ {alt}
+ onload={(e) =>
+ 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)
diff --git a/src/routes/playground/canvas/+page.svelte b/src/routes/playground/canvas/+page.svelte
new file mode 100644
index 0000000..04127c1
--- /dev/null
+++ b/src/routes/playground/canvas/+page.svelte
@@ -0,0 +1,54 @@
+<script>
+ import CanvasCopy from '$/lib/test/canvas/CanvasCopy.svelte';
+ import CanvasImg from '$/lib/test/canvas/CanvasImg.svelte';
+ import { resolve } from '$app/paths';
+ let fixPaddingArtifacts = $state(false);
+ let useBg = $state(true);
+ let src = $state(resolve('/') + 'estrogen-half.png');
+ let alt = $state('Favicon');
+</script>
+
+<h1 class="text-3xl">Canvas Playground</h1>
+
+<p>
+ Here's some fucking around with the HTML canvas element, initially mostly to
+ do a funny bit of obscuring images as found in the DOM.
+</p>
+
+<h2 class="text-xl">Plain Img</h2>
+<p>Normal image tag.</p>
+
+<div class="rounded-2xl"><img {src} {alt} /></div>
+
+<h2 class="text-xl">CanvasImg</h2>
+<p>
+ Simple <code>canvas</code>-based copying of the contents and converting to
+ blob. Falls back to source image.
+</p>
+<div class="rounded-2xl"><CanvasImg {src} {alt} /></div>
+
+<h2 class="text-xl">CanvasCopy</h2>
+<p>
+ A more involved technique, copying sections of the canvas source and rendering
+ them in separate image elements, still falling back to source image.
+</p>
+<p>
+ Single-pixel artifacts on high-dpi and non-1:1 scaled images can be solved by
+ creating an overlapping pixel on each edge. This, however, messes up partial
+ transparency.<br />
+ <label
+ ><input type="checkbox" bind:checked={fixPaddingArtifacts} /> Fix Artifacts</label
+ >
+ <label
+ ><input type="checkbox" bind:checked={useBg} /> Use background-image to reduce
+ element count</label
+ >
+</p>
+<div class="rounded-2xl">
+ <CanvasCopy
+ {src}
+ {alt}
+ artifacts={fixPaddingArtifacts ? 'fix' : 'keep'}
+ {useBg}
+ />
+</div>