You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在动态尺寸场景下使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 10:22:03