Skip to content

Instantly share code, notes, and snippets.

@nathansearles
Last active October 3, 2020 00:48
Show Gist options
  • Save nathansearles/bc37ebee3f9eba0668d9397681c929d1 to your computer and use it in GitHub Desktop.
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
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;*/
}
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