Last active
October 3, 2020 00:48
-
-
Save nathansearles/bc37ebee3f9eba0668d9397681c929d1 to your computer and use it in GitHub Desktop.
Example usage of an Imgix implementation with React using data from headless CMS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
img, | |
picture { | |
width: 100%; | |
height: auto; | |
} | |
.image { | |
display: inline-block; | |
line-height: 0; | |
position: relative; | |
} | |
.image img { | |
position: absolute; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
width: 100%; | |
opacity: 0; | |
transition: opacity 800ms var(--ease-out) 200ms; | |
} | |
.image.image__loaded img { | |
opacity: 1; | |
} | |
.image.image__loading { | |
/*background: lightpink;*/ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect, useRef } from "react"; | |
import PropTypes from "prop-types"; | |
import "./Picture.css"; | |
/* | |
Override usage: | |
<Picture | |
override={ | |
{ | |
xxl: { | |
width: 1600, | |
ratio: 1, | |
fit: 'crop' | |
} | |
} | |
} | |
/>*/ | |
const Picture = (props) => { | |
const ref = useRef(); | |
useEffect(() => { | |
const picture = ref.current; | |
const image = picture.querySelector("img"); | |
handleAspectRatio(); | |
if (image.complete) { | |
picture.classList.add("image__loaded"); | |
} else { | |
picture.classList.add("image__loading"); | |
} | |
}, []); // Pass empty array to only run once on mount | |
const imageLoaded = () => { | |
// Called using onLoad() anytime the img source changes and is loaded | |
const picture = ref.current; | |
// If image has already been loaded, update the aspect ratio | |
picture.classList.contains("image__loaded") && handleAspectRatio(); | |
// Toggle classes | |
picture.classList.remove("image__loading"); | |
picture.classList.add("image__loaded"); | |
}; | |
const handleBreakpointOverride = (size, param, defaultValue) => { | |
// Failsafe check for prop override | |
if ( | |
typeof props.override !== "undefined" && | |
typeof props.override[size] !== "undefined" && | |
props.override[size][param] | |
) { | |
return props.override[size][param]; | |
} else { | |
return defaultValue; | |
} | |
}; | |
// Get the source aspect ratio using image dimensions from CMS | |
const sourceRatio = () => { | |
const width = props.width; | |
const height = props.height; | |
const ratio = height / width; | |
if (!Number.isNaN(ratio)) { | |
return ratio; | |
} | |
}; | |
// Define image breakpoint data | |
// Used defaults or defined data | |
const breakpoints = (key) => { | |
return { | |
width: handleBreakpointOverride( | |
key, | |
"width", | |
props.breakpoints[key].width | |
), | |
height: handleBreakpointOverride( | |
key, | |
"height", | |
props.breakpoints[key].height | |
), | |
ratio: handleBreakpointOverride( | |
key, | |
"ratio", | |
sourceRatio() > 1 ? sourceRatio() : props.breakpoints[key].ratio | |
), | |
fit: handleBreakpointOverride(key, "fit", props.breakpoints[key].fit), | |
}; | |
}; | |
const handleSrcSet = (breakpoint, dpr = 3) => { | |
// dpr = Device Pixel Ratio | |
// Define the pixel density from dpr | |
const density = Array.from(Array(dpr).keys()); | |
// Create empty storage array | |
let set = []; | |
// Creates image srcSet | |
// Uses Imgix: https://docs.imgix.com/apis/url | |
// Loop through each dpr required | |
density.map((item, index) => { | |
const facepad = props.facepad ? `&facepad=${props.facepad}` : ``; | |
const con = props.con ? `&con=${props.con}` : ``; | |
const sat = props.sat ? `&sat=${props.sat}` : ``; | |
const fit = props.preventMobileCropping | |
? "clip" | |
: breakpoints(breakpoint).fit; | |
const src = | |
`${props.src}` + | |
`?auto=format` + | |
`&dpr=${index + 1}` + | |
`&fp-x=${props.focalPoint.x}` + | |
`&fp-y=${props.focalPoint.y}` + | |
`${facepad}` + | |
`${con}` + | |
`${sat}` + | |
`&crop=focalpoint` + | |
`&fit=${fit}` + | |
`&w=${breakpoints(breakpoint).width}` + | |
`&h=${breakpoints(breakpoint).width * breakpoints(breakpoint).ratio}` + | |
` ${index + 1}x`; | |
return set.push(src); | |
}); | |
// Return defined srcSet | |
return set; | |
}; | |
// Get the current breakpoint name | |
const getBreakpointName = () => { | |
const breakpoints = { | |
sm: 768, | |
md: 1024, | |
lg: 1280, | |
xl: 1600, | |
xxl: 2560, | |
}; | |
const windowWidth = window.innerWidth; | |
const breakpointName = Object.keys(breakpoints).find( | |
(key) => breakpoints[key] >= windowWidth | |
); | |
return breakpointName; | |
}; | |
// Get image aspect ratio and set as paddingTop to picture element | |
const handleAspectRatio = () => { | |
const breakpoint = getBreakpointName(); | |
let aspectRatio = null; | |
if (props.override && props.override.hasOwnProperty(breakpoint)) { | |
// Use aspectRatio override | |
aspectRatio = props.override[breakpoint].ratio; | |
} else if (breakpoint === "sm" && props.preventMobileCropping) { | |
// If sm breakpoint and preventMobileCropping has been defined in CMS | |
aspectRatio = sourceRatio(); | |
} else if (breakpoint === "sm") { | |
// If sm or md breakpoint | |
aspectRatio = | |
sourceRatio() > 1 ? sourceRatio() : breakpoints(breakpoint).ratio; | |
} else { | |
// Default | |
aspectRatio = | |
sourceRatio() || | |
handleBreakpointOverride( | |
breakpoint, | |
"ratio", | |
props.breakpoints[breakpoint].ratio | |
); | |
} | |
// Reference current image | |
const picture = ref.current; | |
// Set paddingTop based on aspect ratio | |
// This creates a container for the images to load into | |
if (props.maxheight) { | |
picture.style.paddingTop = `100vh`; | |
} else { | |
picture.style.paddingTop = `${Number.parseFloat(aspectRatio) * 100}%`; | |
} | |
}; | |
return ( | |
<picture ref={ref} className="image"> | |
<source | |
media="(min-width: 1601px)" | |
width={breakpoints("xxl").width} | |
height={ | |
breakpoints("xxl").width * (sourceRatio() || breakpoints("xxl").ratio) | |
} | |
srcSet={handleSrcSet("xxl")} | |
/> | |
<source | |
media="(min-width: 1281px)" | |
width={breakpoints("xl").width} | |
height={ | |
breakpoints("xl").width * (sourceRatio() || breakpoints("xl").ratio) | |
} | |
srcSet={handleSrcSet("xl")} | |
/> | |
<source | |
media="(min-width: 1025px)" | |
width={breakpoints("lg").width} | |
height={ | |
breakpoints("lg").width * (sourceRatio() || breakpoints("lg").ratio) | |
} | |
srcSet={handleSrcSet("lg")} | |
/> | |
<source | |
media="(min-width: 769px)" | |
width={breakpoints("md").width} | |
height={ | |
breakpoints("md").width * (sourceRatio() || breakpoints("md").ratio) | |
} | |
srcSet={handleSrcSet("md")} | |
/> | |
<img | |
alt={props.alt} | |
onLoad={imageLoaded} | |
width={breakpoints("sm").width} | |
height={ | |
breakpoints("sm").width * (sourceRatio() || breakpoints("sm").ratio) | |
} | |
srcSet={handleSrcSet("sm")} | |
src={handleSrcSet("sm", 1)} | |
/> | |
</picture> | |
); | |
}; | |
Picture.propTypes = { | |
src: PropTypes.string.isRequired, | |
alt: PropTypes.string.isRequired, | |
focalPoint: PropTypes.shape({ | |
x: PropTypes.number, | |
y: PropTypes.number, | |
}), | |
width: PropTypes.number.isRequired, | |
height: PropTypes.number.isRequired, | |
breakpoints: PropTypes.shape({ | |
sm: PropTypes.shape({ | |
width: PropTypes.number, | |
height: PropTypes.number, | |
ratio: PropTypes.number, | |
fit: PropTypes.string, | |
}), | |
md: PropTypes.shape({ | |
width: PropTypes.number, | |
height: PropTypes.number, | |
ratio: PropTypes.number, | |
fit: PropTypes.string, | |
}), | |
lg: PropTypes.shape({ | |
width: PropTypes.number, | |
height: PropTypes.number, | |
ratio: PropTypes.number, | |
fit: PropTypes.string, | |
}), | |
xl: PropTypes.shape({ | |
width: PropTypes.number, | |
height: PropTypes.number, | |
ratio: PropTypes.number, | |
fit: PropTypes.string, | |
}), | |
xxl: PropTypes.shape({ | |
width: PropTypes.number, | |
height: PropTypes.number, | |
ratio: PropTypes.number, | |
fit: PropTypes.string, | |
}), | |
}), | |
}; | |
Picture.defaultProps = { | |
focalPoint: { | |
x: 0.5, | |
y: 0.5, | |
}, | |
breakpoints: { | |
sm: { | |
width: 768, | |
height: 768, | |
ratio: 1, | |
fit: "crop", | |
}, | |
md: { | |
width: 1024, | |
height: 1024, | |
ratio: 0.75, | |
fit: "clip", | |
}, | |
lg: { | |
width: 1280, | |
height: 1280, | |
ratio: 0.75, | |
fit: "clip", | |
}, | |
xl: { | |
width: 1600, | |
height: 1600, | |
ratio: 0.75, | |
fit: "clip", | |
}, | |
xxl: { | |
width: 2560, | |
height: 2560, | |
ratio: 0.75, | |
fit: "clip", | |
}, | |
}, | |
}; | |
export default Picture; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment