feat(frontend): ImageVariant component and WASM encoder workers
Add client-side multi-format image encoding via Web Workers using @jsquash and @discourse/jxl libraries. ImageVariant Svelte component renders <picture> elements with format-aware fallback. Vite configured for WASM asset inclusion and encoder dependency exclusion. Portfolio page updated to use multi-format URLs. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
interface ThumbURLs {
|
||||
avif: string;
|
||||
webp: string;
|
||||
jpg: string;
|
||||
}
|
||||
|
||||
interface FullURLs {
|
||||
avif: string;
|
||||
webp: string;
|
||||
jpg: string;
|
||||
jxl?: string;
|
||||
}
|
||||
|
||||
let { urls, type = "thumb", alt = "", class: className = "", ...imgProps }: {
|
||||
urls: ThumbURLs | FullURLs;
|
||||
type?: "thumb" | "full";
|
||||
alt?: string;
|
||||
class?: string;
|
||||
[key: string]: unknown;
|
||||
} = $props();
|
||||
|
||||
const fallbackSrc = $derived(urls.jpg || urls.webp || urls.avif || "");
|
||||
const hasJxl = $derived(type === "full" && "jxl" in urls && (urls as FullURLs).jxl);
|
||||
</script>
|
||||
|
||||
<picture>
|
||||
{#if urls.avif}
|
||||
<source srcset={urls.avif} type="image/avif" />
|
||||
{/if}
|
||||
{#if urls.webp}
|
||||
<source srcset={urls.webp} type="image/webp" />
|
||||
{/if}
|
||||
{#if urls.jpg}
|
||||
<source srcset={urls.jpg} type="image/jpeg" />
|
||||
{/if}
|
||||
{#if hasJxl}
|
||||
<source srcset={(urls as FullURLs).jxl} type="image/jxl" />
|
||||
{/if}
|
||||
<img src={fallbackSrc} {alt} class={className} {...imgProps} />
|
||||
</picture>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { init, default as encode } from '@jsquash/avif/encode.js';
|
||||
|
||||
self.onmessage = async (e: MessageEvent<{ imageData: ImageData; quality: number }>) => {
|
||||
try {
|
||||
await init({ locateFile: (path: string) => `/${path}` });
|
||||
const encoded = await encode(e.data.imageData, { quality: e.data.quality });
|
||||
self.postMessage({ encoded, format: 'avif' }, { transfer: [encoded as Transferable] });
|
||||
} catch (err) {
|
||||
self.postMessage({ error: (err as Error).message, format: 'avif' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { init, default as encode } from '@jsquash/jpeg/encode.js';
|
||||
|
||||
self.onmessage = async (e: MessageEvent<{ imageData: ImageData; quality: number }>) => {
|
||||
try {
|
||||
await init({ locateFile: (path: string) => `/${path}` });
|
||||
const encoded = await encode(e.data.imageData, { quality: e.data.quality });
|
||||
self.postMessage({ encoded, format: 'jpg' }, { transfer: [encoded as Transferable] });
|
||||
} catch (err) {
|
||||
self.postMessage({ error: (err as Error).message, format: 'jpg' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { init, default as encode } from '@discourse/jxl/encode.js';
|
||||
|
||||
self.onmessage = async (e: MessageEvent<{ imageData: ImageData; quality: number }>) => {
|
||||
try {
|
||||
await init({ locateFile: (path: string) => `/${path}` });
|
||||
const encoded = await encode(e.data.imageData, { effort: 4, quality: e.data.quality });
|
||||
self.postMessage({ encoded, format: 'jxl' }, { transfer: [encoded as Transferable] });
|
||||
} catch (err) {
|
||||
self.postMessage({ error: (err as Error).message, format: 'jxl' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { init, default as encode } from '@jsquash/webp/encode.js';
|
||||
|
||||
self.onmessage = async (e: MessageEvent<{ imageData: ImageData; quality: number }>) => {
|
||||
try {
|
||||
await init({ locateFile: (path: string) => `/${path}` });
|
||||
const encoded = await encode(e.data.imageData, { quality: e.data.quality });
|
||||
self.postMessage({ encoded, format: 'webp' }, { transfer: [encoded as Transferable] });
|
||||
} catch (err) {
|
||||
self.postMessage({ error: (err as Error).message, format: 'webp' });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user