Skip to content

Instantly share code, notes, and snippets.

@nathansearles
Last active October 3, 2020 00:48
Show Gist options
  • Select an option

  • Save nathansearles/bc37ebee3f9eba0668d9397681c929d1 to your computer and use it in GitHub Desktop.

Select an option

Save nathansearles/bc37ebee3f9eba0668d9397681c929d1 to your computer and use it in GitHub Desktop.

Revisions

  1. nathansearles revised this gist Oct 3, 2020. No changes.
  2. nathansearles revised this gist Oct 3, 2020. 2 changed files with 0 additions and 0 deletions.
    File renamed without changes.
    File renamed without changes.
  3. nathansearles revised this gist Oct 3, 2020. No changes.
  4. nathansearles created this gist Oct 3, 2020.
    30 changes: 30 additions & 0 deletions .css
    Original 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;*/
    }
    318 changes: 318 additions & 0 deletions .js
    Original 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;