UNPKG

@patreon/studio

Version:

Patreon Studio Design System

272 lines 12.2 kB
import Color from 'color'; import { colorSchemeModes } from '../../../utilities/color-scheme'; import { LRUCache } from '../../../utilities/lru'; import { customCurve } from './bezier'; /** * This represents the minimum contrast ratio between the creator color and a background color. * 3.0 is AA for large (18px+), 4.5 is AA for small text. * * We've decided to use 3.0 for now, but we may want to revisit this. The current implementation * is based on the optical results instead of the technical expectation of contrast for small text * and may need be revisited. */ const CONTRAST_THRESHOLD = 3.0; const SECONDARY_CONTRAST_THRESHOLD = 10.0; // Reference colors for contrast checks when extending a palette const white = Color('white'); const black = Color('black'); /** * Generates a normalized color ramp based on an input color. Take the input color * hue and saturation, then normalizes the lightness to a bezier curve. * * @param inputColor The color to use as the base for the generated colors. * @param stopCount The number of colors to generate. * * @param hueShift The amount the hue is shifted overall * @param hueRange The range of hues to use when generating colors. * @param hueCurve The curve to use when generating hues. * @param saturationBandwidth The bandwidth of saturation. * @param saturationRange The range of saturations to use when generating colors. * @param saturationCurve The curve to use when generating saturations. * @param lightnessCurve The curve to use when generating lightnesses. * * @returns An array of colors. */ export function interpolateTonalColorStops({ inputColor, stopCount, hueShift = 0, hueRange = 0, saturationBandwidth = 1, saturationRange = 0, }) { // get the original hue and saturation const originalHue = inputColor.hue(); const originalSaturation = inputColor.saturationl(); // check if the inputColor has any lightness const hasLightness = inputColor.lightness() > 0; return Array.from({ length: stopCount }).map((_, i) => { const stopProgress = i / (stopCount - 1); // track the hue progress against the hue curve const hueProgress = customCurve(stopProgress); // calculate the hue bandwidth if the color has lightness // 360 here represents the full range of hues const hueCalcRange = hasLightness ? hueRange : 0; const hueBandwidth = 360 * hueCalcRange; const hueBandwidthOffset = 360 * (hueCalcRange / 2); // calculate the hue offset const hueOffset = hueProgress * hueBandwidth - hueBandwidthOffset; // calculate the hue based on the hue shift and offset const hue = hueShift * 360 + (originalHue + hueOffset); // track the saturation progress against the saturation curve const saturationProgress = customCurve(stopProgress); // calculate the saturation bandwidth if the color has lightness const saturationCalcRange = hasLightness ? saturationRange : 0; // calculate the saturation offset so that the input saturation is the center of the bandwidth const saturationOffset = saturationProgress * (100 * saturationCalcRange) - 100 * (saturationCalcRange / 2); // calculate the saturation based on the bandwidth and offset const saturation = originalSaturation === 0 ? 0 : saturationBandwidth * (originalSaturation + saturationOffset); // track the lightness progress against the lightness curve const lightnessProgress = customCurve(stopProgress); // calculate the lightness based on the lightness curve const lightness = lightnessProgress * 100; // return a hex value return inputColor.lightness(lightness).hue(hue).saturationl(saturation).hex(); }); } /** * Generates a color ramp based on an input and mix color. * * @param inputColor The color to use as the base for the generated colors. * @param stopCount The number of colors to generate. * @param mixColor The color to mix with the input color. * * @returns An array of colors. */ export function interpolateAccentColorStops({ inputColor, stopCount, mixColor, }) { // generate the color ramp which is white/black on the left and creator color on the right return Array.from({ length: stopCount }).map((_, i) => inputColor.mix(mixColor, 1 - i / (stopCount - 1)).hex()); } /** * Generates a color ramp based on an input color. The ramp is generated by interpolating * the alpha of the input color from 0% to 100%. * * @param inputColor The color to use as the base for the generated colors. * @param stopCount The number of colors to generate. * * @returns An array of colors. */ export function interpolateAlphaColorStops({ inputColor, stopCount }) { // generate a color ramp using the creator color which is 0% alpha // on the left and 100% alpha on the right return Array.from({ length: stopCount }).map((_, i) => inputColor .alpha(i / (stopCount - 1)) .rgb() .toString()); } /** * Generates a complete color palette based on an input color. * * @param inputColor The color to use as the base for the generated colors. * @param config The configuration used when generating colors. * * @returns A object containing a set of color ramps. */ export function uncachedGenerateColorPalette({ inputColor: userInputColor, }) { const stopCount = 16; const inputColor = Color(userInputColor); // generate the tonal color ramps used for backgrounds and text const support = interpolateTonalColorStops({ inputColor, stopCount, hueShift: 0, hueRange: 0, saturationBandwidth: 1, saturationRange: 1, }); // generate the tonal color ramps used for backgrounds and text const base = interpolateTonalColorStops({ inputColor, stopCount, hueShift: 0, hueRange: 0, saturationBandwidth: 0.3, saturationRange: 1, }); return colorSchemeModes.reduce((acc, mode) => { // get the functions needed to generate the extended palette const extendPalette = getExtendPalette(mode); // get the mix color for the accent ramp const mixColor = getMixColor(mode); // generate the accent color ramps used for interactive elements const accent = interpolateAccentColorStops({ inputColor, stopCount, mixColor }); // get the input color for the alpha ramp const alphaInputColor = getAlphaInputColor(mode, { support, base }); // generate alpha color stops based on the support ramp. const alpha = interpolateAlphaColorStops({ inputColor: alphaInputColor, stopCount }); // extend the palette with the generated color ramps acc[mode] = extendPalette({ accent, alpha, support, base, }); return acc; }, {}); } /** * Generates a complete color palette based on an input color. * * @param inputColor The color to use as the base for the generated colors. * @param config The configuration used when generating colors. * * @returns A object containing a set of color ramps. */ export const generateColorPalette = (() => { // its okay that this is a global cache because the output of // `generateColorPalette` is deterministic const cache = new LRUCache({ size: 100 }); return ({ inputColor }) => { const cachedPalette = cache.get(inputColor); if (cachedPalette) { return cachedPalette; } const palette = uncachedGenerateColorPalette({ inputColor }); cache.set(inputColor, palette); return palette; }; })(); /** * Generates extended palette for the light color scheme. This has a bit of logic to determine the * button action and onAction assignments based on contrast between the creator color and background. * * It will attempt to make creator color the button.action then fallback to button.onAction when * contrast is too low. * * @param palette A color palette to use when generating the token map. * @returns An extended palette with button action and onAction tokens. */ function extendLightModePalette(palette) { const { accent, support } = palette; const accentColor = Color(accent[15]); // default to accent for button action, support for button on action let buttonAction = [accent[15], accent[14], accent[13]]; let buttonOnAction = [support[15], support[15], support[15]]; // invert to support bg / accent text when accent doesn't have // enough contrast with the page background if (accentColor.contrast(white) < CONTRAST_THRESHOLD) { buttonAction = [support[1], support[2], support[3]]; buttonOnAction = [accent[15], accent[14], accent[13]]; if (accentColor.contrast(black) < SECONDARY_CONTRAST_THRESHOLD) { // invert to support bg / support text when accent doesn't have // enough contrast with the support background buttonOnAction = [support[12], support[12], support[12]]; } } return { ...palette, buttonAction, buttonOnAction }; } /** * Generates extended palette for the dark color scheme. This has a bit of logic to determine the * button action and onAction assignments based on contrast between the creator color and background. * * It will attempt to make creator color the button.action, when the contrast between creator color * and black is too low it will invert the button action pair. When the contrast between creator * color and white is too low it will invert only the button.onAction. * * @param palette A color palette to use when generating the token map. * @returns An extended palette with button action and onAction tokens. */ function extendDarkModePalette(palette) { const { accent, support } = palette; const accentColor = Color(accent[15]); // default to accent bg / support.15 text let buttonAction = [accent[15], accent[14], accent[13]]; let buttonOnAction = [support[15], support[14], support[13]]; // invert to support bg / accent text when accent doesn't have // enough contrast with the page background if (accentColor.contrast(black) < CONTRAST_THRESHOLD) { buttonAction = [support[14], support[13], support[12]]; buttonOnAction = [accent[15], accent[15], accent[15]]; if (accentColor.contrast(white) < SECONDARY_CONTRAST_THRESHOLD) { // invert to support bg / support text when accent doesn't have // enough contrast with the support background buttonOnAction = [support[3], support[3], support[3]]; } // invert to support bg / support text when accent doesn't have // enough contrast with the support background } else if (accentColor.contrast(white) < CONTRAST_THRESHOLD) { const halfLightness = Math.floor((accentColor.lightness() * 16) / 100 / 2); const lightnessStop = halfLightness + 6; const index = lightnessStop; buttonAction = [support[index], support[index - 1], support[index - 2]]; buttonOnAction = [support[1], support[1], support[1]]; } return { ...palette, buttonAction, buttonOnAction }; } /** Returns the extended color palette function based on the color mode. */ function getExtendPalette(colorMode) { switch (colorMode) { case 'dark': return extendDarkModePalette; case 'light': default: return extendLightModePalette; } } /** Returns the alpha input color based on the color mode. */ function getAlphaInputColor(colorMode, { support, base }) { // on light mode we base the alpha ramp on a support color // whereas on dark mode we base it on a base color switch (colorMode) { case 'dark': return Color(base[10]); case 'light': default: return Color(support[4]); } } /** Returns the mix color based on the color mode. */ function getMixColor(colorMode) { switch (colorMode) { case 'dark': return black; case 'light': default: return white; } } //# sourceMappingURL=generateColorPalette.js.map