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.
This commit is contained in:
@@ -1,25 +1,137 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Dünner img-Wrapper: `src` ist typischerweise von getCmsImageUrl / ensureTransformedImage (/cms-images?…).
|
||||
* 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 {
|
||||
/** Aufgelöster Bildpfad (z. B. ensureTransformedImage). */
|
||||
/** Rohe CMS-URL (z.B. https://cms.pm86.de/api/assets/...). */
|
||||
src: string;
|
||||
width?: number;
|
||||
/** 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;
|
||||
class?: 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, alt = "", class: className = "", loading = "lazy" }: Props = $props();
|
||||
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>
|
||||
|
||||
<img
|
||||
{src}
|
||||
{alt}
|
||||
{width}
|
||||
{height}
|
||||
class={className}
|
||||
{loading}
|
||||
/>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user