import webp from 'check-for-webp';
import convertToWebP from './imageTypeUtils';
/**
* Utilities for calculating the optimal image sizes to request from the backend.
* @module
* @category templating
*/
/**
* Array of popular browser sizes. This array will be used as baseline to calculate
* the image sizes to request. This array should include mobile browser sizes corrected
* for device pixel ratio. So for this array, a phone with a css width of 320px and a
* device-pixel-ratio of 2 will be considered size 640.
*/
export const SCREEN_WIDTHS = [480, 640, 800, 1080, 1280, 1440, 1920];
/**
* The minimum width difference between image variants. If variants are closer together
* than this amount, the filterSimilarVariants() util will remove them
* @type {number}
*/
export const VARIANT_MIN_DIFF = 20;
/**
* Get the width variants based on a scale of the full screen size.
* @param {number} [scale=1] The scale of the image relative to the full viewport width
* @returns {Array<number>} An array of width variants
* @example // returns image variants for an image that shows at 50% of screen size
* const variants = getViewportRelativeVariants(0.5);
*/
export const getViewportRelativeVariants = (scale = 1) => SCREEN_WIDTHS.map(x => x * scale);
/**
* Returns a new array of width with the widths that are too close together
* filtered out. The amount that widths have to differ is defined in VARIANT_MIN_DIFF
* @param {Array<number>} widths An array of image widths
* @returns {Array<number>} The filtered image widths
*/
export const filterSimilarVariants = widths =>
[...widths]
.sort((a, b) => a - b)
.reduce((newVariants, variant) => {
if (!newVariants.length || variant - newVariants[newVariants.length - 1] > VARIANT_MIN_DIFF) {
newVariants.push(variant);
}
return newVariants;
}, []);
/**
* Picks the variant from an array of widths that should be used as fallback when
* a browser does not support srcset attributes.
* @param {Array<number>} widths An array of image widths
* @returns {number} The fallback variant
*/
export const getFallbackVariant = widths => {
const average = widths.reduce((sum, variant) => sum + variant) / widths.length;
return [...widths].sort((a, b) => Math.abs(average - a) - Math.abs(average - b))[0];
};
/**
* Gets the cropping url for a given image width
* @param {string} url The url of the image resource
* @param {number} width The width of the image to request
* @param {number} [ratio] If set, will also include height in the image request.
* This number should be equal to width divided by height.
* @returns {string} An url to the cropped image resource
* @example const src = getVariantUrl('http://some/image.jpg', 800, 16/9);
*/
export const getVariantUrl = ({ url, width, ratio, useWebp = true }) => {
if (!url) {
return '';
}
return `${webp && useWebp ? convertToWebP(url) : url}${
url.indexOf('?') === -1 ? '?' : '&'
}width=${width}${ratio ? `&height=${parseInt(width / ratio, 10)}` : ''}`;
};
/**
* Generate a image src url based off a cloudinary id
* @param {string} url The url of the cloudinary image
* @param {number} width The width of the image to request
* @param {number} ratio If set, will also include height in the image
* request.
* This number should be equal to width divided by height.
* @param {string} cropType Will crop the image depending on how passed in
* @param {bool} useWebp If true will return a webp url
* @returns {string} An url to the cropped image resource
* @example const src = getCloudinaryVariants({
* url: cloudinarySrc,
useWebp: hasWebpSupport,
width: px,
ratio,
cropTye: cloudinaryCropType,
});
*/
export const getCloudinaryVariants = ({ url, width, cropType = 'fill', ratio, useWebp = true }) => {
const [cloudName, imageUrl] = url && url.split('/image/upload/');
const widthSrc = width ? `w_${width}` : '';
const heightSrc = width && ratio ? `h_${parseInt(width / ratio, 10)}` : '';
const imageRequestCrop = `c_${cropType},${heightSrc},${widthSrc},f_auto/`;
// Needs to pull res.cloudinary.com/ from config
const cropSrc = `https://res.cloudinary.com${cloudName}/image/upload/${imageRequestCrop}${imageUrl}`;
return webp && useWebp ? convertToWebP(cropSrc) : cropSrc;
};
/** 1920/
* Get the srcset to use for the given image widths
* @param {string} url The url of the image resource
* @param {Array<number>} widths The widths of the images to request
* @param {number} [ratio] If set, will also include height in the image requests.
* This number should be equal to width divided by height.
* @returns {object} An object containing the following properties:
* - fallback A string for a fallback url to request when there is no srcset support
* - srcSet A string that can be used as srcset argument
*/
export const getSrcSetFromVariants = (url, widths, ratio, cloudinarySrc = false) => {
const imageVariants = params =>
cloudinarySrc ? getCloudinaryVariants(params) : getVariantUrl(params);
return {
fallback: getVariantUrl({ url, widths: getFallbackVariant(widths), ratio }),
srcSet: widths
.map(variant => `${imageVariants({ url, width: variant, ratio })} ${variant}w`)
.join(','),
srcSetWebp: widths
.map(variant => `${imageVariants({ url, width: variant, ratio, useWebp: true })} ${variant}w`)
.join(','),
};
};