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}