@nomideusz/svelte-geometrize
v0.6.0
Published
Geometric image placeholders for Svelte 5 — triangles resolve into the real photo as it loads. Build-time shape fitting, tiny runtime.
Downloads
467
Maintainers
Readme
@nomideusz/svelte-geometrize
Geometric image placeholders for Svelte 5 — instead of a blur, triangles resolve into the photo while it loads, geometrize.co.uk-style.
Live demo → svelte-geometrize.vercel.app · In production on szkolyjogi.pl, where 700+ listing heroes paint an instant geometric preview of the photo while it loads (open any school page, e.g. this one — hard-refresh to replay), and on kurcz.pl.
The expensive shape fitting (hill-climbing, via geometrizejs) runs at build time and emits a small ordered shape list (~1–10 KB raw, far less gzipped). Because geometrize is iterative — shape 1 is the dominant region, shape 100 is fine detail — replaying the shapes in fit order makes the placeholder visibly sharpen until the real image crossfades in. The runtime component is tiny and dependency-free (sharp is an optional peer for the Node / Vite / CLI half only).
Usage
pnpm add @nomideusz/svelte-geometrize
# only needed for the Vite plugin, Node API, or CLI:
pnpm add -D sharpRegister the Vite plugin (build-time half):
// vite.config.ts
import { geometrize } from '@nomideusz/svelte-geometrize/vite';
export default defineConfig({
plugins: [geometrize(), sveltekit()]
});Then in a component:
<script lang="ts">
import { GeometrizedImage } from '@nomideusz/svelte-geometrize';
import placeholder from './photo.jpg?geometrize';
import src from './photo.jpg';
</script>
<GeometrizedImage {placeholder} {src} alt="Sunset over mountains" revealMs={850} loading="lazy" />For typed ?geometrize imports, either copy the declarations into app.d.ts or reference the shipped ambient types:
/// <reference types="@nomideusz/svelte-geometrize/client" />Options
Plugin-wide defaults via geometrize({ ... }), per-image overrides via query params:
./photo.jpg?shapes=150&alpha=160&maxSize=160&shapeTypes=triangle,ellipse&seed=1&geometrizeParam order doesn't matter to the plugin, but keeping geometrize last lets the *&geometrize module declaration type these imports.
| Option | Default | Meaning |
| --- | --- | --- |
| shapes | 100 | Max shapes to fit — more detail, bigger payload |
| shapeTypes | ['triangle'] | Any of rectangle, rotated-rectangle, triangle, ellipse, rotated-ellipse, circle, line, quadratic-bezier |
| alpha | 128 | Shape opacity, 0–255 |
| maxSize | 128 | Longest edge the image is downscaled to before fitting (the SVG scales back up losslessly) |
| candidateShapesPerStep | 50 | Fit quality vs. build speed |
| shapeMutationsPerStep | 100 | Fit quality vs. build speed |
| seed | 1 | PRNG seed for reproducible fits (false = non-deterministic) |
| targetScore | — | Stop early once approximation score ≤ this (lower = closer) |
| cacheDir | node_modules/.cache/svelte-geometrize | Persistent disk cache (plugin only); false disables |
Fits are cached on disk by file content hash + resolved options, so clean rebuilds skip already-fitted images. Concurrent loads of the same key coalesce.
Component props beyond placeholder / src / alt: reveal ('fade' | 'pop' | 'scatter', default 'fade'), revealMs (total ms until the last shape starts — preferred over raw stagger), stagger (ms between shapes, default 15), shapeDuration (per-shape fade, default 400), fadeDuration (crossfade to the real image, default 600), objectFit (cover | contain | fill | …, default cover), objectPosition (default center). onload / onerror are forwarded (chained after the internal reveal). All other props go to the <img>. Width/height default from the placeholder. The reveal is pure CSS, plays with SSR before hydration, and respects prefers-reduced-motion.
Node API
import {
generatePlaceholder,
generatePlaceholderFromUrl,
placeholderToSvg,
placeholderToDataUri
} from '@nomideusz/svelte-geometrize/node';
const placeholder = await generatePlaceholder('photo.jpg', { shapes: 80, seed: 1 });
const fromCms = await generatePlaceholderFromUrl('https://cdn.example/hero.jpg');
const svg = placeholderToSvg(placeholder);Requires the optional peer sharp.
Browser / worker fitter
import { fitShapes } from '@nomideusz/svelte-geometrize/fit';
// rgba from canvas getImageData / a worker — no sharp
const placeholder = fitShapes(rgba, width, height, sourceW, sourceH, { shapes: 60 });placeholderToSvg / placeholderToDataUri are also exported from the root entry (no geometrize/sharp) for CSS backgrounds or og-images.
CLI
npx svelte-geometrize photo.jpg -o photo.json
npx svelte-geometrize ./photos --out ./placeholders --shapes 80 --target-score 0.12Dynamic images (runtime srcs, DB-stored placeholders)
The Vite plugin covers images known at build time. For photos that live behind an API — CMS content, user uploads, scraped listings — generate the placeholder once at ingest (or in a backfill script) with the Node API / CLI, store the JSON next to the record, and pass it straight to the component. This is how szkolyjogi.pl does its listing heroes:
import { generatePlaceholder } from '@nomideusz/svelte-geometrize/node';
const bytes = Buffer.from(await (await fetch(photoUrl)).arrayBuffer());
const placeholder = await generatePlaceholder(bytes);
await db.update(listings)
.set({ photoPlaceholder: JSON.stringify(placeholder) })
.where(eq(listings.id, id));<GeometrizedImage
placeholder={listing.photoPlaceholder}
src="/api/photo/{listing.id}"
alt={listing.name}
revealMs={900}
/>A placeholder is ~2–10 KB raw (a few KB gzipped), so inlining it in server-rendered HTML is cheap — the geometric preview paints before the photo's first byte arrives, no layout shift, and sharp stays a server-side ingest dependency, never shipped to the client.
Demo
pnpm install
pnpm dev