generate.ts

  1import bezier from "bezier-easing"
  2import chroma from "chroma-js"
  3import { Color, ColorFamily, ColorFamilyConfig, ColorScale } from "../types"
  4import { percentageToNormalized } from "./convert"
  5import { curve } from "./curve"
  6
  7// Re-export interface in a more standard format
  8export type EasingFunction = bezier.EasingFunction
  9
 10/**
 11 * Generates a color, outputs it in multiple formats, and returns a variety of useful metadata.
 12 *
 13 * @param {EasingFunction} hueEasing - An easing function for the hue component of the color.
 14 * @param {EasingFunction} saturationEasing - An easing function for the saturation component of the color.
 15 * @param {EasingFunction} lightnessEasing - An easing function for the lightness component of the color.
 16 * @param {ColorFamilyConfig} family - Configuration for the color family.
 17 * @param {number} step - The current step.
 18 * @param {number} steps - The total number of steps in the color scale.
 19 *
 20 * @returns {Color} The generated color, with its calculated contrast against black and white, as well as its LCH values, RGBA array, hexadecimal representation, and a flag indicating if it is light or dark.
 21 */
 22function generateColor(
 23    hueEasing: EasingFunction,
 24    saturationEasing: EasingFunction,
 25    lightnessEasing: EasingFunction,
 26    family: ColorFamilyConfig,
 27    step: number,
 28    steps: number
 29) {
 30    const { hue, saturation, lightness } = family.color
 31
 32    const stepHue = hueEasing(step / steps) * (hue.end - hue.start) + hue.start
 33    const stepSaturation =
 34        saturationEasing(step / steps) * (saturation.end - saturation.start) +
 35        saturation.start
 36    const stepLightness =
 37        lightnessEasing(step / steps) * (lightness.end - lightness.start) +
 38        lightness.start
 39
 40    const color = chroma.hsl(
 41        stepHue,
 42        percentageToNormalized(stepSaturation),
 43        percentageToNormalized(stepLightness)
 44    )
 45
 46    const contrast = {
 47        black: {
 48            value: chroma.contrast(color, "black"),
 49            aaPass: chroma.contrast(color, "black") >= 4.5,
 50            aaaPass: chroma.contrast(color, "black") >= 7,
 51        },
 52        white: {
 53            value: chroma.contrast(color, "white"),
 54            aaPass: chroma.contrast(color, "white") >= 4.5,
 55            aaaPass: chroma.contrast(color, "white") >= 7,
 56        },
 57    }
 58
 59    const lch = color.lch()
 60    const rgba = color.rgba()
 61    const hex = color.hex()
 62
 63    // 55 is a magic number. It's the lightness value at which we consider a color to be "light".
 64    // It was picked by eye with some testing. We might want to use a more scientific approach in the future.
 65    const isLight = lch[0] > 55
 66
 67    const result: Color = {
 68        step,
 69        lch,
 70        hex,
 71        rgba,
 72        contrast,
 73        isLight,
 74    }
 75
 76    return result
 77}
 78
 79/**
 80 * Generates a color scale based on a color family configuration.
 81 *
 82 * @param {ColorFamilyConfig} config - The configuration for the color family.
 83 * @param {Boolean} inverted - Specifies whether the color scale should be inverted or not.
 84 *
 85 * @returns {ColorScale} The generated color scale.
 86 *
 87 * @example
 88 * ```ts
 89 * const colorScale = generateColorScale({
 90 *   name: "blue",
 91 *   color: {
 92 *     hue: {
 93 *       start: 210,
 94 *       end: 240,
 95 *       curve: "easeInOut"
 96 *     },
 97 *     saturation: {
 98 *       start: 100,
 99 *       end: 100,
100 *       curve: "easeInOut"
101 *     },
102 *     lightness: {
103 *       start: 50,
104 *       end: 50,
105 *       curve: "easeInOut"
106 *     }
107 *   }
108 * });
109 * ```
110 */
111
112export function generateColorScale(
113    config: ColorFamilyConfig,
114    inverted: Boolean = false
115) {
116    const { hue, saturation, lightness } = config.color
117
118    // 101 steps means we get values from 0-100
119    const NUM_STEPS = 101
120
121    const hueEasing = curve(hue.curve, inverted)
122    const saturationEasing = curve(saturation.curve, inverted)
123    const lightnessEasing = curve(lightness.curve, inverted)
124
125    let scale: ColorScale = {
126        colors: [],
127        values: [],
128    }
129
130    for (let i = 0; i < NUM_STEPS; i++) {
131        const color = generateColor(
132            hueEasing,
133            saturationEasing,
134            lightnessEasing,
135            config,
136            i,
137            NUM_STEPS
138        )
139
140        scale.colors.push(color)
141        scale.values.push(color.hex)
142    }
143
144    return scale
145}
146
147/** Generates a color family with a scale and an inverted scale. */
148export function generateColorFamily(config: ColorFamilyConfig) {
149    const scale = generateColorScale(config, false)
150    const invertedScale = generateColorScale(config, true)
151
152    const family: ColorFamily = {
153        name: config.name,
154        scale,
155        invertedScale,
156    }
157
158    return family
159}