feat: initial thumb blur f88f0bc9
Steve · 2026-01-26 21:34 3 file(s) · +102 −21
src/lib/components/ProgressiveImage.svelte (added) +70 −0
1 +
<script lang="ts">
2 +
  let {
3 +
    src,
4 +
    thumb,
5 +
    alt,
6 +
    class: className = "",
7 +
  }: {
8 +
    src: string;
9 +
    thumb: string;
10 +
    alt: string;
11 +
    class?: string;
12 +
  } = $props();
13 +
14 +
  let loaded = $state(false);
15 +
  let thumbAspect = $state(0);
16 +
  let thumbImg: HTMLImageElement;
17 +
18 +
  function onThumbLoad() {
19 +
    if (thumbImg.naturalWidth && thumbImg.naturalHeight) {
20 +
      thumbAspect = thumbImg.naturalWidth / thumbImg.naturalHeight;
21 +
    }
22 +
  }
23 +
24 +
  $effect(() => {
25 +
    loaded = false;
26 +
    const img = new Image();
27 +
    img.onload = () => {
28 +
      loaded = true;
29 +
    };
30 +
    img.src = src;
31 +
32 +
    return () => {
33 +
      img.onload = null;
34 +
    };
35 +
  });
36 +
</script>
37 +
38 +
<div
39 +
  class="progressive-container"
40 +
  style="max-width: 4000px; {thumbAspect ? `aspect-ratio: ${thumbAspect};` : ''}"
41 +
>
42 +
  <img
43 +
    bind:this={thumbImg}
44 +
    src={loaded ? src : thumb}
45 +
    {alt}
46 +
    class="{className} progressive-image"
47 +
    class:progressive-loading={!loaded}
48 +
    onload={onThumbLoad}
49 +
  />
50 +
</div>
51 +
52 +
<style>
53 +
  .progressive-container {
54 +
    width: 100%;
55 +
  }
56 +
57 +
  .progressive-container .progressive-image {
58 +
    width: 100%;
59 +
    height: 100%;
60 +
    object-fit: contain;
61 +
  }
62 +
63 +
  .progressive-image {
64 +
    transition: filter 0.4s ease-out;
65 +
  }
66 +
67 +
  .progressive-loading {
68 +
    filter: blur(20px);
69 +
  }
70 +
</style>
src/routes/+page.svelte +25 −20
1 1
<script lang="ts">
2 -
import type { PageData } from "./$types";
3 -
type ImageItem = PageData["photos"][number];
4 -
let { data }: { data: PageData } = $props();
2 +
  import type { PageData } from "./$types";
3 +
  import ProgressiveImage from "$lib/components/ProgressiveImage.svelte";
4 +
  type ImageItem = PageData["photos"][number];
5 +
  let { data }: { data: PageData } = $props();
5 6
</script>
6 7
7 8
<div class="bg-[#121113] min-h-screen text-white">
10 11
  </div>
11 12
12 13
  {#snippet figure(image: ImageItem)}
13 -
  <div class="flex sm:flex-row flex-col gap-2 sm:px-8 px-4 pt-2">
14 -
    <a href="/photo/{image.slug}" class="flex-2 min-w-0">
15 -
     	<img class="max-w-full h-auto block" src={image.image} alt={image.title} />
16 -
    </a>
17 -
    <div class="flex flex-col gap-1 flex-1 min-w-0 p-4">
18 -
      <h2 class="text-lg">{image.title.toUpperCase()}</h2>
19 -
      <h3 class="text-sm">{image.make} {image.camera}</h3>
20 -
      <div class="flex flex-col gap-2 text-neutral-400 font-thin text-xs mt-4">
21 -
        <p>{image.focalLength}</p>
22 -
        <p>{image.aperture}</p>
23 -
        <p>{image.exposure}</p>
24 -
        <p>ISO {image.iso}</p>
25 -
        <p>-</p>
26 -
        <p class="text-neutral-700 text-xs">{new Date(image.date).toLocaleDateString()}</p>
14 +
    <div class="flex sm:flex-row flex-col gap-2 sm:px-8 px-4 pt-2">
15 +
      <a href="/photo/{image.slug}" class="flex-2 min-w-0">
16 +
        <ProgressiveImage
17 +
          class="max-w-full h-auto block"
18 +
          src={image.image}
19 +
          thumb={image.thumb}
20 +
          alt={image.title}
21 +
        />
22 +
      </a>
23 +
      <div class="flex flex-col gap-1 flex-1 min-w-0 p-4">
24 +
        <h2 class="text-lg">{image.title.toUpperCase()}</h2>
25 +
        <h3 class="text-sm">{image.make} {image.camera}</h3>
26 +
        <div class="flex flex-col gap-2 text-neutral-400 font-thin text-xs mt-4">
27 +
          <p>{image.focalLength}</p>
28 +
          <p>{image.aperture}</p>
29 +
          <p>{image.exposure}</p>
30 +
          <p>ISO {image.iso}</p>
31 +
          <p>-</p>
32 +
          <p class="text-neutral-700 text-xs">{new Date(image.date).toLocaleDateString()}</p>
33 +
        </div>
27 34
      </div>
28 -
29 35
    </div>
30 -
  </div>
31 36
  {/snippet}
32 37
33 38
  <div class="flex flex-col gap-2 pt-12">
34 39
    {#each data.photos as image}
35 -
        {@render figure(image)}
40 +
      {@render figure(image)}
36 41
    {/each}
37 42
  </div>
38 43
</div>
src/routes/photo/[slug]/+page.svelte +7 −1
1 1
<script lang="ts">
2 2
  import type { PageData } from "./$types";
3 +
  import ProgressiveImage from "$lib/components/ProgressiveImage.svelte";
3 4
  let { data }: { data: PageData } = $props();
4 5
</script>
5 6
10 11
11 12
  <div class="flex sm:flex-row flex-col gap-2 sm:px-8 px-4 pt-16">
12 13
    <div class="flex-6 min-w-0">
13 -
      <img class="max-w-full h-auto block" src={data.photo.image} alt={data.photo.title} />
14 +
      <ProgressiveImage
15 +
        class="max-w-full h-auto block"
16 +
        src={data.photo.image}
17 +
        thumb={data.photo.thumb}
18 +
        alt={data.photo.title}
19 +
      />
14 20
    </div>
15 21
    <div class="flex flex-col p-4 flex-1 min-w-0 justify-between">
16 22
      <div class="flex flex-col gap-1 flex-1 min-w-0">