aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/test/canvas/CanvasCopy.svelte
blob: 77951168ab6c759d59f50385427dbfa3ed703d78 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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>