diff --git a/src/components/image.client.tsx b/src/components/image.client.tsx index c41cfd8..76d6912 100644 --- a/src/components/image.client.tsx +++ b/src/components/image.client.tsx @@ -1,8 +1,15 @@ "use client"; -import React, { useCallback, useRef, forwardRef } from "react"; +import React, { + useCallback, + useRef, + forwardRef, + type CSSProperties, +} from "react"; import type { StoredObject } from "ronin/types"; +const supportedFitValues = ["fill", "contain", "cover"]; + export interface ImageProps { /** * Defines text that can replace the image in the page. @@ -35,6 +42,14 @@ export interface ImageProps { * a unit. */ height?: number; + /** + * Specifies how the image should be resized to fit its container. + */ + fit?: CSSProperties["objectFit"]; + /** + * The aspect ratio of the image. Can be "square", "video", or a custom string. + */ + aspect?: "square" | "video" | string; /** * Indicates how the browser should load the image. * @@ -54,7 +69,7 @@ export interface ImageProps { /** * The inline style for the image container (not the image itself). */ - style?: React.CSSProperties; + style?: CSSProperties; } const Image = forwardRef( @@ -65,6 +80,8 @@ const Image = forwardRef( size: defaultSize, width: defaultWidth, height: defaultHeight, + fit = "cover", + aspect, quality, loading, style, @@ -79,6 +96,25 @@ const Image = forwardRef( const width = defaultSize || defaultWidth; const height = defaultSize || defaultHeight; + const onLoad = useCallback(() => { + const duration = Date.now() - renderTime.current; + const threshold = 150; + + // Fade in and gradually reduce blur of the real image if loading takes + // longer than the specified threshold. + if (duration > threshold) { + imageElement.current?.animate( + [ + { filter: "blur(4px)", opacity: 0 }, + { filter: "blur(0px)", opacity: 1 }, + ], + { + duration: 200, + }, + ); + } + }, []); + if (!height && !width) throw new Error( "Either `width`, `height`, or `size` must be defined for `Image`.", @@ -93,12 +129,14 @@ const Image = forwardRef( const optimizationParams = new URLSearchParams({ ...(width ? { w: width.toString() } : {}), ...(height ? { h: height.toString() } : {}), + fit: supportedFitValues.includes(fit) ? fit : "cover", q: quality ? quality.toString() : "100", }); const responsiveOptimizationParams = new URLSearchParams({ ...(width ? { h: (width * 2).toString() } : {}), ...(height ? { h: (height * 2).toString() } : {}), + fit: supportedFitValues.includes(fit) ? fit : "cover", q: quality ? quality.toString() : "100", }); @@ -112,25 +150,6 @@ const Image = forwardRef( const placeholder = input && typeof input !== "string" ? input.placeholder?.base64 : null; - const onLoad = useCallback(() => { - const duration = Date.now() - renderTime.current; - const threshold = 150; - - // Fade in and gradually reduce blur of the real image if loading takes - // longer than the specified threshold. - if (duration > threshold) { - imageElement.current?.animate( - [ - { filter: "blur(4px)", opacity: 0 }, - { filter: "blur(0px)", opacity: 1 }, - ], - { - duration: 200, - }, - ); - } - }, []); - return (
( flexShrink: 0, width: width || "100%", height: height || "100%", + aspectRatio: + aspect === "video" ? "16/9" : aspect === "square" ? "1/1" : "auto", ...style, }} > {/* Blurred preview being displayed until the actual image is loaded. */} {placeholder && ( {alt} @@ -160,7 +186,7 @@ const Image = forwardRef( position: "absolute", width: "100%", height: "100%", - objectFit: "cover", + objectFit: fit, }} decoding="async" onLoad={onLoad}