a2f2d5bfb0
- 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.
138 lines
3.9 KiB
Svelte
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>
|