Files
windwiderstand/src/lib/components/RustyImage.svelte
T
Peter Meier a2f2d5bfb0
Deploy / verify (push) Successful in 41s
Deploy / deploy (push) Successful in 1m2s
feat(image): enhance image handling with new properties and responsive support
- Updated PostEntry type to include _rawImageUrl and _imageFocal for improved image management.
- Refactored image handling in various components to utilize the new properties, ensuring better focal point support and responsive image rendering.
- Introduced warmResponsiveImage function for pre-fetching image variants to optimize loading performance.
- Replaced CmsImage component with RustyImage in multiple blocks for consistent image processing.
2026-04-18 08:43:17 +02:00

138 lines
3.9 KiB
Svelte

<script lang="ts">
/**
* Smart CMS-Image-Component.
* - Automatisches AVIF→WebP→JPEG `<picture>`.
* - Retina-srcset per `densities` (Default [1, 2]).
* - CLS=0 via `width`+`height` (oder aus `aspect` abgeleitet).
* - Focal durchgereicht (cover-Crop am Motiv).
*
* `src` = rohe CMS-URL (ohne /cms-images-Wrapper). Die Component baut
* die transformierten /cms-images?…-URLs selbst.
*/
import { getCmsImageUrl, type RustyImageTransformParams } from "$lib/rusty-image";
interface Props {
/** Rohe CMS-URL (z.B. https://cms.pm86.de/api/assets/...). */
src: string;
/** Ziel-Renderbreite in CSS-px (für Attribut + srcset-Basis). */
width: number;
/** Ziel-Renderhöhe; alternativ `aspect`. Mindestens eins muss gegeben sein. */
height?: number;
/** Seitenverhältnis, z.B. "16/9", "3/2". Alternativ zu `height`. */
aspect?: string;
/** Focal-Point aus CMS-Image-Field. */
focal?: { x: number; y: number } | null;
alt?: string;
/** `sizes`-Attribut für Media-Queries; default `${width}px`. */
sizes?: string;
loading?: "lazy" | "eager";
fetchpriority?: "high" | "auto" | "low";
decoding?: "async" | "sync" | "auto";
class?: string;
style?: string;
/** Pixel-Dichten fürs srcset. Default [1, 2] (Retina). */
densities?: number[];
/** Format-Fallback-Kette. Default ['avif','webp','jpeg']. */
formats?: ("avif" | "webp" | "jpeg")[];
/** JPEG/AVIF/WebP-Qualität. Default 75. */
quality?: number;
fit?: "cover" | "contain" | "fill";
}
let {
src,
width,
height,
aspect,
focal = null,
alt = "",
sizes,
loading = "lazy",
fetchpriority,
decoding = "async",
class: className = "",
style,
densities = [1, 2],
formats = ["webp", "jpeg"],
quality = 75,
fit = "cover",
}: Props = $props();
function parseAr(s: string | undefined): number | null {
if (!s) return null;
const m = s.match(/^(\d+(?:\.\d+)?)\s*[/:x]\s*(\d+(?:\.\d+)?)$/);
if (!m) return null;
const w = Number(m[1]);
const h = Number(m[2]);
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return null;
return w / h;
}
const arValue = $derived(parseAr(aspect));
const computedHeight = $derived(
height ?? (arValue ? Math.round(width / arValue) : undefined),
);
const arString = $derived(
aspect ??
(height && width ? `${width}/${height}` : undefined),
);
const sizesAttr = $derived(sizes ?? `${width}px`);
function baseParams(format: "avif" | "webp" | "jpeg", w: number): RustyImageTransformParams {
return {
width: w,
format,
fit,
quality,
...(arString ? { ar: arString } : {}),
...(focal ? { focalX: focal.x, focalY: focal.y } : {}),
...(arString && fit !== "contain"
? { height: Math.round(w / (arValue ?? 1)) }
: {}),
};
}
function buildSrcset(format: "avif" | "webp" | "jpeg"): string {
return densities
.map((d) => {
const w = Math.round(width * d);
return `${getCmsImageUrl(src, baseParams(format, w))} ${d}x`;
})
.join(", ");
}
const fallbackFormat = $derived<"avif" | "webp" | "jpeg">(
formats.includes("jpeg") ? "jpeg" : formats[formats.length - 1] ?? "jpeg",
);
const fallbackUrl = $derived(
getCmsImageUrl(src, baseParams(fallbackFormat, width)),
);
const fallbackSrcset = $derived(buildSrcset(fallbackFormat));
const pictureSources = $derived(
formats.filter((f) => f !== fallbackFormat).map((f) => ({
type: `image/${f}`,
srcset: buildSrcset(f),
})),
);
</script>
<picture>
{#each pictureSources as s (s.type)}
<source type={s.type} srcset={s.srcset} sizes={sizesAttr} />
{/each}
<img
src={fallbackUrl}
srcset={fallbackSrcset}
sizes={sizesAttr}
{alt}
width={width}
height={computedHeight}
class={className}
{style}
{loading}
{decoding}
{fetchpriority}
/>
</picture>