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}