Skip to content

[Image] Forward focus coordinates to object-position #21

@Dangoo

Description

@Dangoo

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/images 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 Images 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 while object-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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions