algorithm.ts

  1// Adapted from @k-vyn/coloralgorithm
  2
  3import bezier from "bezier-easing";
  4import chroma, { Scale } from "chroma-js";
  5import { Curve } from "./curves";
  6import { ColorFamily, ColorProps, ColorSet } from "./types";
  7
  8function validColor(color: string) {
  9  if (chroma.valid(color)) {
 10    return color;
 11  } else {
 12    throw new Error(`Invalid color: ${color}`);
 13  }
 14}
 15
 16function assignColor(scale: Scale, steps: number, step: number) {
 17  const color = scale(step / steps);
 18  const lch = color.lch();
 19  const rgbaArray = color.rgba();
 20  const hex = color.hex();
 21
 22  // Roughly  calculate if a color is dark or light
 23  const isLight = lch[0] > 50;
 24
 25  const result = {
 26    step,
 27    hex,
 28    lch,
 29    rgbaArray,
 30    isLight,
 31  };
 32
 33  return result;
 34}
 35
 36/** Outputs 101 colors (0-100) */
 37export function generateColors(props: ColorProps, inverted: boolean) {
 38  const steps = 101;
 39  const colors: ColorSet = [];
 40
 41  const { start, middle, end } = props.color;
 42
 43  const startColor = typeof start === "string" ? validColor(start) : start;
 44  const middleColor = typeof middle === "string" ? validColor(middle) : middle;
 45  const endColor = typeof end === "string" ? validColor(end) : end;
 46
 47  // TODO: Use curve when generating colors
 48
 49  let scale: Scale;
 50
 51  if (inverted) {
 52    scale = chroma.scale([endColor, middleColor, startColor]).mode("lch");
 53  } else {
 54    scale = chroma.scale([startColor, middleColor, endColor]).mode("lch");
 55  }
 56  for (let i = 0; i < steps; i++) {
 57    const color = assignColor(scale, steps, i);
 58    colors.push(color);
 59  }
 60  return colors;
 61}
 62
 63export function generateColorsUsingCurve(
 64  startColor: string,
 65  endColor: string,
 66  curve: number[]
 67) {
 68  const NUM_STEPS = 101;
 69
 70  const easing = bezier(curve[0], curve[1], curve[2], curve[3]);
 71  const curveProgress = [];
 72  for (let i = 0; i <= NUM_STEPS; i++) {
 73    curveProgress.push(easing(i / NUM_STEPS));
 74  }
 75
 76  const colors: chroma.Color[] = [];
 77  for (let i = 0; i < NUM_STEPS; i++) {
 78    // Use HSL as an input as it is easier to construct programatically
 79    // const color = chroma.hsl();
 80    const color = chroma.mix(startColor, endColor, curveProgress[i], "lch");
 81    colors.push(color);
 82  }
 83
 84  return colors;
 85}
 86
 87export function generateColors2(
 88  hue: {
 89    start: number;
 90    end: number;
 91    curve: Curve;
 92  },
 93  saturation: {
 94    start: number;
 95    end: number;
 96    curve: Curve;
 97  },
 98  lightness: {
 99    start: number;
100    end: number;
101    curve: Curve;
102  }
103) {
104  const NUM_STEPS = 9;
105
106  const hueEasing = bezier(
107    hue.curve.value[0],
108    hue.curve.value[1],
109    hue.curve.value[2],
110    hue.curve.value[3]
111  );
112  const saturationEasing = bezier(
113    saturation.curve.value[0],
114    saturation.curve.value[1],
115    saturation.curve.value[2],
116    saturation.curve.value[3]
117  );
118  const lightnessEasing = bezier(
119    lightness.curve.value[0],
120    lightness.curve.value[1],
121    lightness.curve.value[2],
122    lightness.curve.value[3]
123  );
124
125  const colors: chroma.Color[] = [];
126  for (let i = 0; i < NUM_STEPS; i++) {
127    const hueValue =
128      hueEasing(i / NUM_STEPS) * (hue.end - hue.start) + hue.start;
129    const saturationValue =
130      saturationEasing(i / NUM_STEPS) * (saturation.end - saturation.start) +
131      saturation.start;
132    const lightnessValue =
133      lightnessEasing(i / NUM_STEPS) * (lightness.end - lightness.start) +
134      lightness.start;
135
136    const color = chroma.hsl(
137      hueValue,
138      saturationValue / 100,
139      lightnessValue / 100
140    );
141    colors.push(color);
142  }
143
144  const scale = chroma.scale(colors).mode("lch");
145  return scale;
146}
147
148/** Generates two color ramps:
149 * One for for light, and one for dark.
150 * By generating two ramps, rather than two default themes, we can use the same reference palette values for tokens in components.
151 *
152 * Each ramp has 101 colors (0-100)
153 */
154export function generateColorSet(props: ColorProps) {
155  const generatedColors = generateColors(props, false);
156  const generatedInvertedColors = generateColors(props, true);
157
158  const colors = generatedColors.map((color) => color.hex);
159  const invertedColors = generatedInvertedColors.map((color) => color.hex);
160
161  const result: ColorFamily = {
162    name: props.name,
163    colors: colors,
164    invertedColors: invertedColors,
165    colorsMeta: generatedColors,
166    invertedMeta: generatedInvertedColors,
167  };
168
169  return result;
170}