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  const isLight = lch[0] > 50;
 64
 65  const result: Color = {
 66    step,
 67    lch,
 68    hex,
 69    rgba,
 70    contrast,
 71    isLight,
 72  };
 73
 74  return result;
 75}
 76
 77/**
 78 * Generates a color scale based on a color family configuration.
 79 *
 80 * @param {ColorFamilyConfig} config - The configuration for the color family.
 81 * @param {Boolean} inverted - Specifies whether the color scale should be inverted or not.
 82 *
 83 * @returns {ColorScale} The generated color scale.
 84 *
 85 * @example
 86 * ```ts
 87 * const colorScale = generateColorScale({
 88 *   name: "blue",
 89 *   color: {
 90 *     hue: {
 91 *       start: 210,
 92 *       end: 240,
 93 *       curve: "easeInOut"
 94 *     },
 95 *     saturation: {
 96 *       start: 100,
 97 *       end: 100,
 98 *       curve: "easeInOut"
 99 *     },
100 *     lightness: {
101 *       start: 50,
102 *       end: 50,
103 *       curve: "easeInOut"
104 *     }
105 *   }
106 * });
107 * ```
108 */
109
110export function generateColorScale(
111  config: ColorFamilyConfig,
112  inverted: Boolean = false
113) {
114  const { hue, saturation, lightness } = config.color;
115
116  // 101 steps means we get values from 0-100
117  const NUM_STEPS = 101;
118
119  const hueEasing = curve(hue.curve, inverted);
120  const saturationEasing = curve(saturation.curve, inverted);
121  const lightnessEasing = curve(lightness.curve, inverted);
122
123  let scale: ColorScale = {
124    colors: [],
125    values: [],
126  };
127
128  for (let i = 0; i < NUM_STEPS; i++) {
129    const color = generateColor(
130      hueEasing,
131      saturationEasing,
132      lightnessEasing,
133      config,
134      i,
135      NUM_STEPS
136    );
137
138    scale.colors.push(color);
139    scale.values.push(color.hex);
140  }
141
142  return scale;
143}
144
145/** Generates a color family with a scale and an inverted scale. */
146export function generateColorFamily(config: ColorFamilyConfig) {
147  const scale = generateColorScale(config, false);
148  const invertedScale = generateColorScale(config, true);
149
150  const family: ColorFamily = {
151    name: config.name,
152    scale,
153    invertedScale,
154  };
155
156  return family;
157}