storyblok-toolkit
storyblok-toolkit copied to clipboard
[Image] Forward focus coordinates to object-position
Is your feature request related to a problem? Please describe.
At the time of writing the image component uses the focus
parameter only for passing it to the Storyblok API which works well for images with dimensions that are not relative to their container.
So imagine you have lets say a stage module which is width: 100vw;
and height: 80vh;
and the image should fill the whole area (similar to next/image
s layout="fill"
):
<section className="stage" style={{ width: '100vw', height: '80vh' }}>
<Image {...imageProps} fluid={1920} width="100%" height="100%" />
{...children}
</section>
Depending on the aspect ratio of the original image the Storyblok API tries to shift image to the provided focus as good as it can, but then the Image
s CSS applies object-position: center center
which can lead to a scenario where the focus point is not even in the visible area of the component.
Describe the solution you'd like
What would you think about passing the focus
coordinates down to the CSS to make sure the focus point/area is always visible?
Two things are to be considered:
- Storyblok treats
focus
as area described by two pairs of coordinates whileobject-position
expects a point which would make calculating the center of the focus area necessary (e.g. by<coordinateA> + <coordinateB> / 2
) - Shifting the image using
object-position
by a percentage value can lead to moving it too far and create undesired whitespace so the amount of translation has to be clamped:const pictureStyles: CSSProperties = { /* ... */ objectFit: fit, objectPosition: `clamp(0%, ${focusX}, 100%) clamp(0%, ${focusY}, 100%)', };
Describe alternatives you've considered
For now we tried to patch the Image
component from the outside to reflect the desired behavior, but this is not really a sustainable solution…
const getFocusPoint = (focus: string) =>
focus
.split(':')[0]
.split('x')
.map((p) => parseInt(p))
const getFocusPosition = (focus: string, src: string) => {
const [focusPointX, focusPointY] = getFocusPoint(focus)
const { width, height } = getImageProps(src)
const positionX = Math.floor(
Math.min(Math.max((focusPointX / width) * 100, 0), 100)
)
const positionY = Math.floor(
Math.min(Math.max((focusPointY / height) * 100, 0), 100)
)
return [`${positionX}%`, `${positionY}%`]
}
export const PatchedImage: React.FC<ImageProps> = ({ focus, src, ...restProps }) => {
const imageContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!focus || !src || !imageContainerRef?.current) {
return
}
const [positionX, positionY] = getFocusPosition(focus, src)
const images = imageContainerRef.current.querySelectorAll('img')
for (const image of images) {
image.style.objectPosition = `${positionX} ${positionY}`
}
}, [focus, src])
return (
<div ref={imageContainerRef} style={{ display: 'contents' }}>
<Image src={src} focus={focus} {...restProps} />
</div>
)
}
Let me know what you think, happy to discuss the details or helping with the implementation :)
/cc @martinjuhasz
Thanks for the well-written issue! I think this is indeed something that this library needs, actually when implementing focus points I was considering doing it like this but didn't know clamp
was a thing.
Interestingly when testing I found that the focus point that storyblok gives back is actually two pairs of coordinates that are right next to eachother (1px difference).
I'll try to implement the object-fit behaviour you described.
@BJvdA thank you for looking into this! We discovered the same and our assumption is that with the two types of predefined image blocks led to this focus API:
- "Image (old)" has an image editor with predefined aspect ratios where I'd expect
focus
being the selected area - "Media" interface provides only a point where the focus area is 1x1 px in size to approximate point
clamp
is a rather new feature where depending on the targeted browsers a fallback using the min
/ max
equivalent: max(MIN, min(VAL, MAX))
might extend support.