This project is a palette creator tool that allows users to generate and customize color palettes for their design projects.
1// oxlint-disable max-lines
2// oxlint-disable no-magic-numbers
3
4const MAX_HUE = 360;
5const MAX_SATURATION = 100;
6const MAX_LIGHTNESS = 100;
7const MAX_RGB_VALUE = 255;
8const HEX_RADIX = 16;
9const LOW_SAT_THRESHOLD = 10;
10const ACHROMATIC_DARK_THRESHOLD = 8;
11const ACHROMATIC_LIGHT_THRESHOLD = 95;
12
13/** * Generates a random hue value between 0 and MAX_HUE.
14 * @returns {number} A random hue value in the range [0, MAX_HUE).
15 */
16const randomHue = () => Math.floor(Math.random() * MAX_HUE);
17
18/**
19 * Returns a random integer in [min, max].
20 * @param {number} min - the minimum integer value (inclusive)
21 * @param {number} max - the maximum integer value (inclusive)
22 * @returns {number} a random integer between min and max (inclusive)
23 */
24const randomInRange = (min, max) =>
25 Math.floor(Math.random() * (max - min + 1)) + min;
26
27/**
28 * Helper function for converting hue to RGB values, used in the HSL to RGB conversion process.
29 * @param {number} p - The first parameter for the hue to RGB conversion, representing a temporary value based on lightness and saturation.
30 * @param {number} q - The second parameter for the hue to RGB conversion, representing a temporary value based on lightness and saturation.
31 * @param {number} t - The hue value adjusted for the specific RGB channel being calculated (red, green, or blue).
32 * @returns {number} The calculated RGB value for the specific channel, normalized to the range [0, 1].
33 */
34const hue2rgb = (p, q, t) => {
35 if (t < 0) {
36 // oxlint-disable-next-line no-param-reassign
37 t += 1;
38 }
39 if (t > 1) {
40 // oxlint-disable-next-line no-param-reassign
41 t -= 1;
42 }
43 if (t < 1 / 6) {
44 return p + (q - p) * 6 * t;
45 }
46 if (t < 1 / 2) {
47 return q;
48 }
49 if (t < 2 / 3) {
50 return p + (q - p) * (2 / 3 - t) * 6;
51 }
52 return p;
53};
54
55/**
56 * Clamps a value between 0 and MAX_LIGHTNESS.
57 * @param {number} v - The value to clamp
58 * @returns {number} The clamped value
59 */
60const clamp = (v) => Math.max(0, Math.min(MAX_LIGHTNESS, v));
61
62/**
63 * Extracts the lightness value from an HSL color string.
64 * @param {string} hsl - The HSL color string (e.g., "hsl(120, 50%, 50%)").
65 * @returns {number} The lightness value (0–100).
66 */
67export const getLightness = (hsl) => {
68 const parts = hsl.match(/\d+/g)?.map(Number) ?? [];
69 return parts[2] ?? 50;
70};
71
72/**
73 * Returns true when saturation is so low that hue rotation produces no visible color variation.
74 * @param {number} s - saturation value (0–100)
75 * @returns {boolean} - true if saturation is low enough to be considered near-achromatic, false otherwise
76 */
77export const isLowSaturation = (s) => s <= LOW_SAT_THRESHOLD;
78
79/**
80 * Returns true when both saturation is near-zero AND lightness is at an extreme where
81 * saturation changes have no visible effect (pure black / pure white region).
82 * @param {number} s - saturation value (0–100)
83 * @param {number} l - lightness value (0–100)
84 * @returns {boolean} - true if the color is effectively achromatic, false otherwise
85 */
86export const isAchromatic = (s, l) =>
87 s <= LOW_SAT_THRESHOLD &&
88 (l <= ACHROMATIC_DARK_THRESHOLD || l >= ACHROMATIC_LIGHT_THRESHOLD);
89
90/**
91 * Extracts the saturation value from an HSL color string.
92 * @param {string} hsl - The HSL color string (e.g., "hsl(120, 50%, 50%)").
93 * @returns {number} The saturation value (0–100).
94 */
95export const getSaturation = (hsl) => Number(hsl.match(/\d+/g)?.[1]);
96
97/**
98 * Generates an HSL color string from the provided hue, saturation, and luminosity values.
99 * @param {number} h - the hue value
100 * @param {number} s - the saturation value
101 * @param {number} l - the luminosity value
102 * @returns {string} the HSL color string in the format "hsl(h, s%, l%)"
103 */
104export const toHslString = (h, s, l) => `hsl(${h}, ${s}%, ${l}%)`;
105
106/**
107 * Generates a random HSL color string by randomly selecting hue, saturation, and luminosity values within their respective ranges.
108 * @returns {string} a random HSL color string in the format "hsl(h, s%, l%)"
109 */
110export const generateHsl = () => {
111 const h = Math.floor(Math.random() * MAX_HUE);
112 const s = Math.floor(Math.random() * MAX_SATURATION);
113 const l = Math.floor(Math.random() * MAX_LIGHTNESS);
114 return toHslString(h, s, l);
115};
116
117/**
118 * Converts an HSL color string to an RGB color string.
119 * @param {string} hsl - The HSL color string (e.g., "hsl(120, 50%, 50%)").
120 * @returns {string} The RGB color string (e.g., "rgb(64, 191, 64)").
121 */
122// oxlint-disable-next-line max-statements
123export const hslToRgb = (hsl) => {
124 let [h, s, l] = hsl.match(/\d+/g)?.map(Number) ?? [];
125 if (h === undefined || s === undefined || l === undefined) {
126 return 'rgb(0, 0, 0)';
127 }
128
129 h /= MAX_HUE;
130 s /= MAX_SATURATION;
131 l /= MAX_LIGHTNESS;
132
133 let r;
134 let g;
135 let b;
136
137 if (s === 0) {
138 // oxlint-disable-next-line no-multi-assign
139 r = g = b = l;
140 } else {
141 const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
142 const p = 2 * l - q;
143 r = hue2rgb(p, q, h + 1 / 3);
144 g = hue2rgb(p, q, h);
145 b = hue2rgb(p, q, h - 1 / 3);
146 }
147
148 return `rgb(${Math.round(r * MAX_RGB_VALUE)}, ${Math.round(g * MAX_RGB_VALUE)}, ${Math.round(
149 b * MAX_RGB_VALUE,
150 )})`;
151};
152
153/**
154 * Converts an RGB color string to an HSL color string.
155 * @param {string} rgb - The RGB color string (e.g., "rgb(64, 191, 64)").
156 * @returns {string} The HSL color string (e.g., "hsl(120, 50%, 50%)").
157 */
158// oxlint-disable-next-line max-statements
159export const rgbToHsl = (rgb) => {
160 let [r, g, b] = rgb.match(/\d+/g)?.map(Number) ?? [];
161 if (r === undefined || g === undefined || b === undefined) {
162 return 'hsl(0, 0%, 0%)';
163 }
164
165 r /= MAX_RGB_VALUE;
166 g /= MAX_RGB_VALUE;
167 b /= MAX_RGB_VALUE;
168
169 const max = Math.max(r, g, b);
170 const min = Math.min(r, g, b);
171
172 let h;
173 let s;
174 const l = (max + min) / 2;
175 if (max === min) {
176 h = 0;
177 s = 0;
178 } else {
179 const d = max - min;
180 s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
181 switch (max) {
182 case r: {
183 h = (g - b) / d + (g < b ? 6 : 0);
184 break;
185 }
186 case g: {
187 h = (b - r) / d + 2;
188 break;
189 }
190 case b: {
191 h = (r - g) / d + 4;
192 break;
193 }
194 default: {
195 break;
196 }
197 }
198 h ??= 0;
199 h /= 6;
200 }
201
202 return `hsl(${Math.round(h * MAX_HUE)}, ${Math.round(s * MAX_SATURATION)}%, ${Math.round(
203 l * MAX_LIGHTNESS,
204 )}%)`;
205};
206
207/**
208 * Converts an RGB color string to a HEX color string.
209 * @param {string} rgb - The RGB color string (e.g., "rgb(64, 191, 64)").
210 * @returns {string} The HEX color string (e.g., "#40bf40").
211 */
212export const rgbToHex = (rgb) => {
213 const [r, g, b] = rgb.match(/\d+/g)?.map(Number) ?? [];
214 if (r === undefined || g === undefined || b === undefined) {
215 return '#000000';
216 }
217
218 const hex = [
219 r.toString(HEX_RADIX),
220 g.toString(HEX_RADIX),
221 b.toString(HEX_RADIX),
222 ];
223
224 for (const [index, color] of hex.entries()) {
225 if (color.length === 1) {
226 hex[index] = `0${color}`;
227 }
228 }
229
230 return `#${hex.join('')}`;
231};
232
233/**
234 * Converts a HEX color string to an HSL color string.
235 * @param {string} hex - The HEX color string (e.g., "40bf40" or "#40bf40").
236 * @returns {string} The HSL color string (e.g., "hsl(120, 50%, 50%)").
237 */
238export const hexToHsl = (hex) => {
239 const r = Number.parseInt(hex.slice(0, 2), 16);
240 const g = Number.parseInt(hex.slice(2, 4), 16);
241 const b = Number.parseInt(hex.slice(4, 6), 16);
242 const hsl = rgbToHsl(`rgb(${r},${g},${b})`);
243 return hsl;
244};
245
246/**
247 * Generates an array of complementary HSL color strings based on the input HSL color.
248 * @param {string} hsl - The base HSL color string (e.g., "hsl(120, 50%, 50%)").
249 * @returns {string[]} An array of complementary HSL color strings.
250 */
251// oxlint-disable-next-line max-lines-per-function
252export const generateComplement = (hsl) => {
253 const [h, s, l] = hsl.match(/\d+/g)?.map(Number) ?? [];
254 if (h === undefined || s === undefined || l === undefined) {
255 return [];
256 }
257
258 if (isLowSaturation(s)) {
259 const base = randomHue();
260 return [
261 toHslString(base, randomInRange(50, 80), randomInRange(40, 60)),
262 toHslString(
263 (base + 180) % MAX_HUE,
264 randomInRange(50, 80),
265 randomInRange(40, 60),
266 ),
267 toHslString(
268 (base + 60) % MAX_HUE,
269 randomInRange(40, 70),
270 randomInRange(70, 85),
271 ),
272 toHslString(
273 (base + 240) % MAX_HUE,
274 randomInRange(40, 70),
275 randomInRange(70, 85),
276 ),
277 toHslString(
278 (base + 90) % MAX_HUE,
279 randomInRange(50, 80),
280 randomInRange(25, 40),
281 ),
282 toHslString(
283 (base + 120) % MAX_HUE,
284 randomInRange(50, 80),
285 randomInRange(45, 65),
286 ),
287 toHslString(
288 (base + 270) % MAX_HUE,
289 randomInRange(50, 80),
290 randomInRange(45, 65),
291 ),
292 ];
293 }
294
295 const h2 = (h + 180) % MAX_HUE;
296 const h3 = (((h - 150) % MAX_HUE) + MAX_HUE) % MAX_HUE;
297 const h4 = (h + 150) % MAX_HUE;
298 const l2 = (l - 30 + MAX_LIGHTNESS) % MAX_LIGHTNESS;
299
300 return [
301 toHslString(h2, s, l),
302 toHslString(h, s, l2),
303 toHslString(h, 50, 90),
304 toHslString(h2, s, l2),
305 toHslString(h2, 50, 90),
306 toHslString(h3, s, l),
307 toHslString(h4, s, l),
308 ];
309};
310
311/**
312 * Generates an array of monochromatic HSL color strings based on the input HSL color.
313 * @param {string} hsl - The base HSL color string (e.g., "hsl(120, 50%, 50%)").
314 * @returns {string[]} An array of monochromatic HSL color strings.
315 */
316export const generateMono = (hsl) => {
317 const [h, s] = hsl.match(/\d+/g)?.map(Number) ?? [];
318 if (h === undefined || s === undefined) {
319 return [];
320 }
321
322 return [
323 toHslString(h, s, 8),
324 toHslString(h, s, 20),
325 toHslString(h, s, 32),
326 toHslString(h, s, 45),
327 toHslString(h, s, 58),
328 toHslString(h, s, 72),
329 toHslString(h, s, 85),
330 toHslString(h, s, 95),
331 ];
332};
333
334/**
335 * Generates an array of triad HSL color strings based on the input HSL color.
336 * @param {string} hsl - The base HSL color string (e.g., "hsl(120, 50%, 50%)").
337 * @returns {string[]} An array of triad HSL color strings.
338 */
339export const generateTriad = (hsl) => {
340 const [h, s, l] = hsl.match(/\d+/g)?.map(Number) ?? [];
341 if (h === undefined || s === undefined || l === undefined) {
342 return [];
343 }
344
345 if (isLowSaturation(s)) {
346 const base = randomHue();
347 const sat = randomInRange(50, 80);
348 const h2 = (base + 120) % MAX_HUE;
349 const h3 = (base + 240) % MAX_HUE;
350 return [
351 toHslString(h2, sat, randomInRange(40, 60)),
352 toHslString(h3, sat, randomInRange(40, 60)),
353 toHslString(h2, sat, randomInRange(25, 40)),
354 toHslString(h3, sat, randomInRange(25, 40)),
355 toHslString(h2, sat, randomInRange(65, 80)),
356 toHslString(h3, sat, randomInRange(65, 80)),
357 ];
358 }
359
360 const h2 = (h + 120) % MAX_HUE;
361 const h3 = (h + 240) % MAX_HUE;
362
363 return [
364 toHslString(h2, s, l),
365 toHslString(h3, s, l),
366 toHslString(h2, s, clamp(l - 20)),
367 toHslString(h3, s, clamp(l - 20)),
368 toHslString(h2, s, clamp(l + 20)),
369 toHslString(h3, s, clamp(l + 20)),
370 ];
371};
372
373/**
374 * Generates an array of analogous HSL color strings based on the input HSL color.
375 * @param {string} hsl - The base HSL color string (e.g., "hsl(120, 50%, 50%)").
376 * @returns {string[]} An array of analogous HSL color strings.
377 */
378export const generateAnalogous = (hsl) => {
379 const [h, s, l] = hsl.match(/\d+/g)?.map(Number) ?? [];
380 if (h === undefined || s === undefined || l === undefined) {
381 return [];
382 }
383
384 if (isLowSaturation(s)) {
385 const base = randomHue();
386 const sat = randomInRange(45, 75);
387 const lit = randomInRange(40, 65);
388 return [
389 toHslString(
390 (((base - 60) % MAX_HUE) + MAX_HUE) % MAX_HUE,
391 sat,
392 lit,
393 ),
394 toHslString(
395 (((base - 30) % MAX_HUE) + MAX_HUE) % MAX_HUE,
396 sat,
397 lit,
398 ),
399 toHslString((base + 30) % MAX_HUE, sat, lit),
400 toHslString((base + 60) % MAX_HUE, sat, lit),
401 toHslString(
402 (((base - 90) % MAX_HUE) + MAX_HUE) % MAX_HUE,
403 sat,
404 lit,
405 ),
406 toHslString((base + 90) % MAX_HUE, sat, lit),
407 ];
408 }
409
410 const h2 = (((h - 60) % MAX_HUE) + MAX_HUE) % MAX_HUE;
411 const h3 = (((h - 30) % MAX_HUE) + MAX_HUE) % MAX_HUE;
412 const h4 = (h + 30) % MAX_HUE;
413 const h5 = (h + 60) % MAX_HUE;
414 const h6 = (((h - 90) % MAX_HUE) + MAX_HUE) % MAX_HUE;
415 const h7 = (h + 90) % MAX_HUE;
416
417 return [
418 toHslString(h2, s, l),
419 toHslString(h3, s, l),
420 toHslString(h4, s, l),
421 toHslString(h5, s, l),
422 toHslString(h6, s, l),
423 toHslString(h7, s, l),
424 ];
425};
426
427/**
428 * Generates an array of HSL color strings with varying saturation based on the input HSL color.
429 * @param {string} hsl - The base HSL color string (e.g., "hsl(120, 50%, 50%)").
430 * @returns {string[]} An array of HSL color strings with different saturations.
431 */
432export const generateSaturations = (hsl) => {
433 const [h, s, l] = hsl.match(/\d+/g)?.map(Number) ?? [];
434 if (h === undefined || s === undefined || l === undefined) {
435 return [];
436 }
437
438 if (isAchromatic(s, l)) {
439 const base = randomHue();
440 return [
441 toHslString(base, 15, 25),
442 toHslString(base, 30, 35),
443 toHslString(base, 45, 45),
444 toHslString(base, 55, 55),
445 toHslString(base, 65, 65),
446 toHslString(base, 75, 75),
447 toHslString(base, 85, 85),
448 toHslString(base, 95, 92),
449 ];
450 }
451
452 const s2 = (((s - 10) % MAX_SATURATION) + MAX_SATURATION) % MAX_SATURATION;
453 const s3 = (s + 10) % MAX_SATURATION;
454 const s4 = (((s - 20) % MAX_SATURATION) + MAX_SATURATION) % MAX_SATURATION;
455 const s5 = (s + 20) % MAX_SATURATION;
456 const s6 = (((s - 30) % MAX_SATURATION) + MAX_SATURATION) % MAX_SATURATION;
457 const s7 = (s + 30) % MAX_SATURATION;
458 const s8 = (((s - 40) % MAX_SATURATION) + MAX_SATURATION) % MAX_SATURATION;
459 const s9 = (s + 40) % MAX_SATURATION;
460
461 return [
462 toHslString(h, s2, l),
463 toHslString(h, s3, l),
464 toHslString(h, s4, l),
465 toHslString(h, s5, l),
466 toHslString(h, s6, l),
467 toHslString(h, s7, l),
468 toHslString(h, s8, l),
469 toHslString(h, s9, l),
470 ];
471};