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}