Open iliquifysnacks opened 5 years ago
Yes, that would strengthen the sanity/gatsby combination. Not sure if I could support coding, but would join testing and documenting.
Any update on this subject?
This would be really valuable. My GraphQL site is super-dependent on imagery and am struggling a bit without the crop/hotspot.
I'm dying to get this to work.. doesn't gatsby image take a styles prop, so you could feed it some x,y values related to the hotspot you get from Sanity?
Adding my vote to this too. Would be very useful.
There's a way to work around this by passing in a style object that contains an object-position/objectPosition key!
I did it (for a background-image but the same process applies) like this:
const positionStyles = {
backgroundPositionX: `${data.sanitySiteSettings.image.hotspot.x * 100}%`,
backgroundPositionY: `${data.sanitySiteSettings.image.hotspot.y * 100}%`,
}
Does that also work for crop?
Does that also work for crop?
I haven't tried personally, but I reckon you should be able to by playing around with object-fit and object-position! It depends on your use case and output, I think.
Here's how I'm managing this right now:
The Sanity image tool preview is based entirely on CSS, which is puzzling as the docs recommend dealing with hotspot and crop through @sanity/image-url
. However, this is great for us, as we don't have to re-do how gatsby-image-sanity
creates srcsets and the likes, we just have to copy the code from [imagetool/src/calculateStyles.js
] and apply it to our image components, like so:
export const SanityFluidImage = ({
assetId,
fluidOptions,
hotspot,
crop,
className,
alt,
}) => {
if (!assetId || !fluidOptions) {
return null;
}
// If you already have the fluid props from GraphQL, skip this step
const imgFluidProps = getFluidProps(assetId, fluidOptions);
// If no hotspot or crop, we're good to go with regular GatsbyImage
if (!hotspot && !crop) {
return (
<GatsbyImage alt={alt} fluid={imgFluidProps} className={className} />
);
}
// If we do, however, let's get each element's styles to replicate the hotspot and crop
const targetStyles = calculateStyles({
container: {
aspectRatio: imgFluidProps.aspectRatio,
},
image: {
aspectRatio: imgFluidProps.aspectRatio,
},
hotspot,
crop,
});
// Unfortunately, as we need an extra wrapper for the image to apply the crop styles, we recreate a padding div and add it all to a new container div
return (
<div style={targetStyles.container}>
<div aria-hidden="true" style={targetStyles.padding}></div>
<GatsbyImage
fluid={imgFluidProps}
alt={alt}
className={className}
// The GatsbyImage wrapper will have the crop styles
style={targetStyles.crop}
imgStyle={targetStyles.image}
/>
</div>
);
};
This works for most cases, with a big caveat: if the original image is cropped to a width that is lower than the width it should appear on the front-end, the result will be really weird. There are also some cases where the hotspot doesn't seem to work really well, but that's a small price to pay considering the flexibility we're giving editors 😊
The last time calculatedStyles.js
was touched was on July 2018 and that code is for the specific purpose of displaying inside of the cropping dialog inside of Sanity. I think we can probably get to a better, more resilient way of taking crop and hotspot into consideration, but I haven't had the time to dive deep on this.
Hope this can help someone, let me know if I can help o/
EDIT: this getFluidProps
is my own version of getFluidGatsbyImage
that already contains the instantiated @sanity/client
;)
As for fixed images, we must crop them properly. Here's the code I came to for doing that (it ignores hotspot as it doesn't make sense in most fixed contexts):
function getFixedWithCrop({ assetId, fixed, crop }) {
let newFixed = { ...fixed };
const [, , dimensions] = assetId.split('-');
// Get the original width and height
const [width, height] = dimensions.split('x');
// Let's calculate the rect query string to crop the image
const { left, top, right, bottom } = crop;
const effectiveWidth = Math.ceil((1 - left - right) * width);
const effectiveHeight = Math.ceil((1 - top - bottom) * height);
// rect=x,y,width,height
// (each is in absolute PX, that's why we refer to width and height)
const cropQueryStr = `&rect=${Math.floor(left * width)},${Math.floor(
top * height,
)},${effectiveWidth},${effectiveHeight}`;
/*
cdn.sanity.io/...?w=100&h=94&fit=crop 1x,
cdn.sanity.io/...?w=150&h=94&fit=crop 1.5x,
*/
function addToSrcset(srcSet) {
return (
srcSet
.split(',')
// Map over each individual declaration (divided by ,)
.map((declaration) => {
// And get their URLs for further modification
const [url, multiplier] = declaration.split(' ');
return `${url}${cropQueryStr} ${multiplier}`;
})
// and finally turn this back into a string
.join(',')
);
}
// Add the rect query string we created to all src declarations
newFixed.src = fixed.src + cropQueryStr;
newFixed.srcWebp = fixed.srcWebp + cropQueryStr;
newFixed.srcSet = addToSrcset(fixed.srcSet);
newFixed.srcSetWebp = addToSrcset(fixed.srcSetWebp);
console.log({ fixed, newFixed });
return newFixed;
}
export const getFixedProps = ({ assetId, crop }, options) => {
let fixed = getFixedGatsbyImage(assetId, options, sanityConfig);
// If we have a crop, let's add it to every URL in the fixed object
if (crop && crop.top) {
return getFixedWithCrop({ assetId, fixed, crop });
}
return fixed;
};
As I come to think of it, probably combining @jenkshields object-position
trick for hotspot with this URL-level crop is the best alternative both for fluid and fixed images, as we're saving some bandwidth from the cropped part of the image that doesn't have to be loaded. Plus, we're actually getting the image size we are looking for, as otherwise when cropping with CSS we're scaling the divs
to compensate for the cropping.
Will update this if I come across a better solution 🙌
Haha – this is gold @hcavalieri. It would have been cool if we could've made this a bit easier for you, but cool that we know have a way to go about it!
Great work @hcavalieri - good thinking out of the box!
I do something similar where I grab both the rawImage and the fluidImage from GraphQL, and then I use the rawImage to get a url that support crop and hotspot using @sanity/image-url
.
Then I pull out the rect
part of that url and add it to the fluidImage's srcSet's.
Not the prettiest solution, but until Sanity supports this out of the box, it'll have to do.
The biggest problem I've found with this approach is that you can't use the base64 encoded baseimage because its not cropped.
I can probably clean up my code a bit after looking at your solution @hcavalieri - but just for reference here it is in its current form:
import imageUrlBuilder from "@sanity/image-url"
import { dataset, projectId } from "./sanityConfig"
import { isNil, join, map, pipe, split } from "ramda"
const builder = imageUrlBuilder({ projectId, dataset })
export const sanityImageSrc = (source: any) => builder.image(source)
export const getFluidImage = (
rawImage: any,
fluidImage: any,
width: number,
height: number
) => {
const url = sanityImageSrc(rawImage).width(width).height(height).url()!
const rect = new URL(url).searchParams.get("rect")
const addRectToUrl = (rect: string | null) => (incomingUrl: string) => {
if (isNil(rect)) return incomingUrl
const [url, size] = split(" ")(incomingUrl)
return `${url}&rect=${rect} ${size}`
}
const convertUrl = addRectToUrl(rect)
const addRectToUrlSet = (rect: string | null) => (incomingUrl: string) =>
isNil(rect)
? incomingUrl
: pipe(split(","), map(convertUrl), join(","))(incomingUrl)
const convertUrlSet = addRectToUrlSet(rect)
return {
...fluidImage,
src: convertUrl(fluidImage.src),
srcSet: convertUrlSet(fluidImage.srcSet),
srcSetWebp: convertUrlSet(fluidImage.srcSetWebp),
srcWebp: convertUrl(fluidImage.srcWebp),
}
}
@hcavalieri Where am I putting this code for the SanityFluidImage component. Somewhere in sanity? Just not sure where?
@mellson can you show how this is being used with a component? and a graphql query?
@viperfx sure. The graphql query looks something like this:
query {
rawImage: sanityDocument {
_rawMainImage
mainImage {
asset {
fluid(maxWidth: 1152, maxHeight: 420) {
...GatsbySanityImageFluid_noBase64
}
}
}
}
}
and I use it in an image like this:
<Img
backgroundColor
fluid={getFluidImage(
rawImage._rawMainImage,
rawImage.mainImage.asset.fluid,
1152,
420
)}
/>
Here is my workaround. I use hotspot only in one component, so I just created a custom image component instead of using gatsby-image. I basically made srcSet on my own and copied&pasted the output of gatsby-image. If you want to use this kind of workaround with gatsby-image, I guess you could use patch-package.
Plus, the code below is a little specific to my needs.
I have aspectRatio
which can be 1, 2/3, 3/2, ...
const originalUrl = imageUrlFor(buildImageObj(imageNode))
.width(1440)
.height(1440 * aspectRatio) // aspectRatio: 1, 2/3, 3/2, ...
.url();
const widths = [360, 720, 1440];
const srcSetWebp = widths.map(width => {
const url = `${imageUrlFor(buildImageObj(imageNode))
.width(width)
.height(width * aspectRatio)
.url()}&fm=webp`;
return `${url} ${width}w`;
});
const srcSet = widths.map(width => {
const url = imageUrlFor(buildImageObj(imageNode))
.width(width)
.height(width * aspectRatio)
.url();
return `${url} ${width}w`;
});
const sizes = `(max-width: 1440px) 100vw, 1440px`;
return (
<figure className={className}>
<div
sx={{
position: 'relative',
overflow: 'hidden',
}}
>
<div
aria-hidden={true}
sx={{
width: '100%',
paddingBottom: `${100 * aspectRatio}%`,
}}
></div>
<picture>
<source type="image/webp" srcSet={srcSetWebp} sizes={sizes} />
<source srcSet={srcSet} sizes={sizes} />
<img
sizes={sizes}
srcSet={srcSet}
src={originalUrl}
alt={imageNode.alt}
loading="lazy"
sx={{
position: 'absolute',
top: '0px',
left: '0px',
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center center',
opacity: 1,
transition: 'none 0s ease 0s',
}}
/>
</picture>
</div>
</figure>
);
maybe this funciton
const fluidProps = getFluidGatsbyImage(
props.node.photo.photo,
{},
sanity
)
at this class getGatsbyImageProps.ts could use the crop and hotspot data came from the rawdata and use it, instead of using the arguments of the getFluidGatsbyImage and the default values that comes from them. not sure why the sanity team did that implementation, but I think it can be added the crop and hostpot support here.
What do you think?
@hdoro I would like to integrate your fluid option but I am lost where do you find the calculatedstyles.js?
@holly-searchengineunity it's in Sanity : node-modules/@sanity/imagetool that said, I'd really like to see a real implementation of this :) any codesandbox somewhere ?
@jenkshields sorry, your solution is a bit confusing for me, where do you this code ? in your Sanity schema for the image ?
@jenkshields sorry, your solution is a bit confusing for me, where do you this code ? in your Sanity schema for the image ?
No, in the front-end - in my case in particular I pulled the x and y information from sanity and used it to position a background image. You can do this by using the x and way to set object-position in your css/style object.
@jenkshields thanks a lot !
Two months later... Okay, so if dumb guys like me need to be taken by the hand on this, here's how I finally managed to get it working based on @jenkshields solution:
First, in my grapql query, I grab the hotspot from Sanity:
mainImage {
hotspot {
x
y
}
asset {
fluid(
maxWidth: 1200
) {
...GatsbySanityImageFluid_noBase64
}
}
}
Second, I get it in my component:
export default function BlogPostPreviewList({ mainImage }) {
const objectPosition = {
x: `${mainImage.hotspot.x * 100}%`,
y: `${mainImage.hotspot.y * 100}%`,
};
return (
<FirstNodeStyles className="firstPost" objectPosition={objectPosition}>
{mainImage?.asset && (
<Img
fluid={mainImage.asset.fluid}
alt={mainImage.alt}
/>
)}
</FirstNodeStyles>
);
}
Third, in my styled component :
const FirstNodeStyles = styled.div`
.gatsby-image-wrapper {
--x: ${(props) => props.objectPosition.x};
--y: ${(props) => props.objectPosition.y};
div[aria-hidden='true'] {
padding-bottom: 41% !important;
}
img {
object-position: var(--x) var(--y) !important;
}
}
`;
For people that might want the same: If you use the new Gatsby Image handling and would like hotspots, you can simply extend the Type that contains the asset.
getGatsbyImageData
already accepts an Object that has Keys asset
, hotspot
and crop
, you can simply throw your wrapping type in there.
To make things easier (and make gatsby-node handle things) extend your Type that contains the asset (in my case SanityImage
) with the field gatsbyImageData
.
Would look something like this:
const { getGatsbyImageData } = require('gatsby-source-sanity')
const { getGatsbyImageFieldConfig } = require('gatsby-plugin-image/graphql-utils')
exports.createResolvers = ({ createResolvers }) => {
createResolvers({
SanityImage: {
// pretty much copy and paste from here:
// https://github.com/sanity-io/gatsby-source-sanity/blob/bbe8565c0c639797e25b742df4e1dc120c465108/src/images/extendImageNode.ts#L47
gatsbyImageData: getGatsbyImageFieldConfig(
(image, args) => getGatsbyImageData(image, args, sanityConfig),
{
placeholder: {
type: 'SanityGatsbyImagePlaceholder',
defaultValue: `dominantColor`,
// Also copy the description from this line if you want that comment in your schema
// https://github.com/sanity-io/gatsby-source-sanity/blob/bbe8565c0c639797e25b742df4e1dc120c465108/src/images/extendImageNode.ts#L53
description: "..."
},
fit: {
type: 'SanityImageFit',
defaultValue: 'fill',
},
},
),
},
})
}
You can then query like this (works the same as the asset
field):
fragment ImageWithCropAndHotspot on SanityImage {
gatsbyImageData(
layout: FULL_WIDTH
fit: FILL
height: 600
)
}
Only "downside" I noticed so far: GraphQLCodegen exports the type of that field as any
- but as long as it works...
Hey guys, I found small plugin which handles crops and hotspots
It would be great if there was a way to use the crop and hotspot defined in the sanity backend with the gatsby-image component.