如何在动态尺寸场景下使用Sanity图片热点(Hotspot)
简而言之:我希望在100vw和100dvh的区域中,无论用户如何调整尺寸,Sanity热点始终保持在屏幕可见范围内。
目标
在客户的网站上,我为英雄区背景设置了图片轮播组件。为了最大化展示图片效果,该区域铺满视口宽度和高度。我需要确保无论区域尺寸如何动态变化,热点始终在该空间内可见。
问题
无论我尝试何种方法,热点始终无法正常工作。有些操作毫无效果,有些则会导致图片偏移过度,露出后方空白区域。即使是最接近预期的尝试,也无法确保热点始终处于可见范围内。
代码
图片构建器
这是Sanity常用的图片构建器工具
// /sanity.ts import createImageUrlBuilder from '@sanity/image-url' /** * ### imageUrlFor * - 获取图片的Sanity URL * - 包含用于自定义输出文件的辅助函数 * * @param source SanityImage * @returns url * @example imageUrlFor(image)?.url() * @example importUrlFor(image)?.format('webp').url() */ export const imageUrlFor = (source: SanityImageReference | AltImage) => { if (!config.dataset || !config.projectId) throw new Error('SANITY_DATASET or SANITY_PROJECT_ID is not set correctcly') // @ts-expect-error dataset and projectId have been verified return createImageUrlBuilder(config).image(source) }
图片缩放辅助函数
(非核心逻辑;仅用于下方的sanity.ts)
// /utils/functions.ts /** * ### Compress Width and Height * - 按比例调整宽高,缩放到指定的最大像素值 * @param {number} width * @param {number} height * @param {number} max 最大宽/高(默认值:25) * @returns {{ width: number, height: number }} { width: number, height: number } */ export function compressWidthAndHeight( width: number, height: number, max: number = 25, ): { width: number; height: number } { if (width === 0 || height === 0) throw new Error( 'Cannot divide by zero. Please provide non-zero values for the width and height.', ) if (width > height) return { width: max, height: Math.ceil((max * height) / width), } return { width: Math.ceil((max * width) / height), height: max, } }
为Next.js Image组件准备图片的函数
我希望能在此处添加热点相关逻辑,无论是通过imageUrlFor实现还是其他方式
// /utils/sanity.ts import { SanityImageReference } from '@/typings/sanity' import { getImageDimensions } from '@sanity/asset-utils' import { ImageProps } from 'next/image' import { compressWidthAndHeight } from './functions' import { imageUrlFor } from '@/sanity' /** * ### Prepare Image * @param {SanityImageReference & { alt: string }} image * @param {Partial<{ aspect: number, max: number, sizes: string, quality: number }>} options * @options aspect 宽高比 * @options dpr 像素密度(默认值:2) * @options max 最大宽/高(默认值:2560) * @options sizes * @options quality 图片质量1-100(默认值:75) * @returns {ImageProps} */ export function prepareImage( image: SanityImageReference & { alt: string }, options?: Partial<{ aspect: number dpr: number max: number sizes: string quality: number }>, ): ImageProps { const { alt } = image const aspect = options?.aspect, dpr = options?.dpr || 2, max = options?.max || 2560, sizes = options?.sizes, quality = options?.quality || 75 if (aspect && aspect <= 0) throw new Error('aspect cannot be less than 1.') if (dpr <= 0) throw new Error('dpr cannot be less than 1.') if (dpr >= 4) throw new Error('dpr cannot be greater than 3.') if (max <= 0) throw new Error('max cannot be less than 1.') if (quality <= 0) throw new Error('quality cannot be less than 1.') if (quality >= 101) throw new Error('quality cannot be greater than 100.') let { width, height } = getImageDimensions(image) if (aspect) { const imageIsLandscape = width > height, aspectIsLandscape = aspect > 1, aspectIsSquare = aspect === 1 if (aspectIsSquare) { if (imageIsLandscape) width = height if (!imageIsLandscape) height = width } if (aspectIsLandscape) { height = Math.round(width / aspect) width = Math.round(height * aspect) } else if (!aspectIsSquare) { height = Math.round(width / aspect) width = Math.round(height * aspect) } } // 用于完整图片 const { width: sizedWidth, height: sizedHeight } = compressWidthAndHeight( width, height, max, ) // 用于blurDataUrl const { width: compressedWidth, height: compressedHeight } = compressWidthAndHeight(width, height) const baseSrcUrl = imageUrlFor(image).format('webp').fit('crop') const src = baseSrcUrl .size(sizedWidth, sizedHeight) .quality(quality) .dpr(dpr) .url() const blurDataURL = baseSrcUrl .quality(15) .size(compressedWidth, compressedHeight) .dpr(1) .blur(200) .auto('format') .url() return { id: `${image.asset._ref}`, src, alt, width: sizedWidth, height: sizedHeight, sizes, quality: quality, placeholder: 'blur', blurDataURL, } }
示例
JavaScript代码
const img = document.querySelector('#example-image') const hotspot = { x: 0.804029, y: 0.239403, width: 0.154323, height: 0.154323 } // 这能让热点与容器左上角对齐——大概是这样… // 这不是预期结果,但这是我至少能定位热点的最佳尝试。 img.style.top = `${hotspot.y * 100}` img.style.left = `${hotspot.x * 100}` img.style.transform = `translateX(-${hotspot.x * 100}%) translateY(-${hotspot.y * 100}%) scaleX(${hotspot.width * 100 + 100}%) scaleY(${hotspot.height * 100 + 100}%)`
CSS代码
* { margin: 0; padding: 0; position: relative; min-width: 0; } html { color-scheme: light dark; } body { min-height: 100svh; font-family: ui-sans-serif; } img { font-style: italic; background-repeat: no-repeat; background-size: cover; shape-margin: 0.75rem; width: 100%; font-family: ui-serif; } h1 { font-weight: unset; font-size: unset; } .absolute { position: absolute; } .inset-0 { inset: 0; } .\-z-10 { z-index: -10; } .flex { display: flex; } .size-screen { width: 100vw; height: 100dvh; } .h-full { height: 100% } .items-center { align-items: center; } .justify-center { justify-content: center; } .mb-2 { margin-bottom: 0.5rem; } .ply-4 { padding-block: 1rem; } .plx-8 { padding-inline: 2rem; } .rounded-tl-10xl { border-top-left-radius: 4.5rem; } .rounded-tr-lg { border-top-right-radius: 0.5rem; } .rounded-bl-lg { border-bottom-left-radius: 0.5rem; } .rounded-br-10xl { border-bottom-right-radius: 4.5rem; } .bg-black\/60 { background-color: rgb(0 0 0 / 0.6) } .text-white { color: white; } .text-lg { font-size: 1.125rem; } .font-extralight { font-weight: 200; } .font-black { font-weight: 900; } .text-center { text-align: center; } .shadow-lg { box-shadow: 0 10px 20px 0 rgb(0 0 0 / 0.25); } .ring-2 { --ring-width: 2px; } .ring-inset { box-shadow: inset 0 0 0 var(--ring-width) var(--ring-color); } .ring-blue\/25 { --ring-color: rgb(0 0 255 / 0.25); } .object-cover { object-fit: cover; } .bg-blur-img { background-image: url(https://images.unsplash.com/photo-1711907392527-4a895b190b61?ixlib=rb-4.0.3&q=1&fm=webp&crop=entropy&cs=srgb&width=20&height=20); } .text-shadow { text-shadow: 0 2.5px 5px rgb(0 0 0 / 0.25); } .backdrop-brightness-150 { --backdrop-brightness: 1.5; -webkit-backdrop-filter: blur(var(--backdrop-blur)) brightness(var(--backdrop-brightness)); backdrop-filter: blur(var(--backdrop-blur)) brightness(var(--backdrop-brightness)); } .backdrop-blur-lg { --backdrop-blur: 16px; -webkit-backdrop-filter: blur(var(--backdrop-blur)) brightness(var(--backdrop-brightness)); backdrop-filter: blur(var(--backdrop-blur)) brightness(var(--backdrop-brightness)); }
HTML代码
<section class='flex size-screen items-center justify-center ring-2 ring-inset ring-blue/25'> <div id='text-container' class='ply-4 plx-8 rounded-tl-10xl rounded-tr-lg rounded-bl-lg rounded-br-10xl bg-black/60 text-white text-center text-shadow shadow-lg backdrop-blur-lg backdrop-brightness-150'> <h1 class='text-lg font-black mb-2'>In this example, the sun is the hotspot.</h1> <p class='font-extralight'>Check JavaScript for explanation.</p> </div> <img id='example-image' src='https://images.unsplash.com/photo-1711907392527-4a895b190b61?ixlib=rb-4.0.3&q=75&fm=webp&crop=entropy&cs=srgb&width=2560&height=1707.046875' alt='Sunset, Canary Islands, Spain' width='2560' height='1707.046875' sizes='100vw' class='absolute inset-0 -z-10 h-full object-cover bg-blur-img' loading='eager' /> </section>
内容的提问来源于stack exchange,提问作者andrilla
相关产品推荐
相关产品推荐

