This project is a palette creator tool that allows users to generate and customize color palettes for their design projects.
at main 471 lines 15 kB view raw
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};