Last active
October 3, 2020 00:48
-
-
Save nathansearles/bc37ebee3f9eba0668d9397681c929d1 to your computer and use it in GitHub Desktop.
Revisions
-
nathansearles revised this gist
Oct 3, 2020 . No changes.There are no files selected for viewing
-
nathansearles revised this gist
Oct 3, 2020 . 2 changed files with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes.File renamed without changes. -
nathansearles revised this gist
Oct 3, 2020 . No changes.There are no files selected for viewing
-
nathansearles created this gist
Oct 3, 2020 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,30 @@ 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 hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,318 @@ 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;