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}