tl;dr 我想将 Sanity 热点保留在屏幕上 100vw 和 100dvh 部分,无论用户如何调整它的大小。
在我客户的网站上,我有一个用于英雄部分背景的图像滑块。为了最大化地显示图像,该部分填充视口宽度和视口高度。我想确保热点在这个空间中始终可见,无论该部分有多大,因为它非常动态。
无论我尝试什么,热点似乎都没有为我做任何事情。我尝试过的一些操作没有任何效果,而另一些则将图像移得太远,从而显示图像后面的空白。无论如何,即使是我最接近的尝试似乎也不包含热点。
这只是 Sanity 常用的图像生成器助手
// /sanity.ts
import createImageUrlBuilder from '@sanity/image-url'
/**
* ### imageUrlFor
* - Gets the Sanity URL for images
* - Has helper functions for customizing the output file
*
* @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
* - Resize the width and height it's the ratio, to the max width/height in pixels
* @param {number} width
* @param {number} height
* @param {number} max width/height (default: 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,
}
}
这是我希望能够以某种方式添加热点的地方,无论它是否在 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 width/height
* @options dpr pixel density (default: 2)
* @options max width or height (default: 2560)
* @options sizes
* @options quality 1-100 (default: 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)
}
}
// For the full image
const { width: sizedWidth, height: sizedHeight } = compressWidthAndHeight(
width,
height,
max,
)
// For the 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,
}
}
const img = document.querySelector('#example-image')
const hotspot = {
x: 0.804029,
y: 0.239403,
width: 0.154323,
height: 0.154323
}
// This lines up the hotspot with the top left corner of the container—sort of…
// This is not the intended result, but it was my best attempt at least locating the hotspot in some way.
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}%)`
* {
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));
}
<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>
为了确保热点在动态大小的图像中保持清晰可见并正确对齐,特别是在像英雄滑块这样的完整视口宽度和高度部分中,您可以通过专注于图像定位和缩放的 CSS 和 JavaScript 处理来完善您的方法.
function adjustImagePosition(hotspot, imageElement) {
const xPercent = hotspot.x * 100;
const yPercent = hotspot.y * 100;
// Adjust the object-position to focus on the hotspot
imageElement.style.objectPosition = `${xPercent}% ${yPercent}%`;
}
document.addEventListener('DOMContentLoaded', () => {
const img = document.querySelector('#example-image');
const hotspot = {
x: 0.804029, // Hotspot X coordinate as a fraction of width
y: 0.239403 // Hotspot Y coordinate as a fraction of height
};
adjustImagePosition(hotspot, img);
window.addEventListener('resize', () => {
adjustImagePosition(hotspot, img);
});
});
img {
width: 110%; /* Slightly larger to ensure coverage */
height: 110%;
object-fit: cover;
position: relative;
left: -5%; /* Center the scaled image */
top: -5%;
}
<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'
class='absolute inset-0 -z-10 h-full object-cover bg-blur-img'
loading='eager'
/>
</section>
解释👇
HTML 结构:除了确保图像占据整个部分并根据视口动态调整之外,结构基本保持不变。
CSS 更改:图像稍微放大,以确保调整对象位置以聚焦于热点时不会显示边缘周围的任何空白。
JavaScript 更改: adjustmentImagePosition 函数根据热点坐标动态设置对象位置。在页面加载时以及每次调整窗口大小时都会调用此函数,以确保无论屏幕大小如何,热点都保持可见并居中。