color-convert.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. #!/usr/bin/env node
  2. // Color converter - convert between hex, rgb, hsl, oklch, oklab
  3. // Usage: node color-convert.js <color>
  4. // Accepts: #hex, rgb(r,g,b), hsl(h,s%,l%), oklch(l c h), oklab(l a b)
  5. const args = process.argv.slice(2);
  6. if (args.length < 1) {
  7. console.log(`Usage: node color-convert.js <color>
  8. Examples:
  9. node color-convert.js "#3b82f6"
  10. node color-convert.js "rgb(59, 130, 246)"
  11. node color-convert.js "hsl(217, 91%, 60%)"
  12. node color-convert.js "oklch(0.62 0.18 250)"
  13. node color-convert.js "oklab(0.62 -0.05 -0.16)"`);
  14. process.exit(1);
  15. }
  16. const raw = args.join(' ').trim();
  17. // ========== Conversion math ==========
  18. function srgbToLinear(c) {
  19. return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  20. }
  21. function linearToSrgb(c) {
  22. return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
  23. }
  24. function rgbToLinear(r, g, b) {
  25. return [srgbToLinear(r / 255), srgbToLinear(g / 255), srgbToLinear(b / 255)];
  26. }
  27. function linearToRgb(lr, lg, lb) {
  28. const clamp = v => Math.round(Math.min(255, Math.max(0, linearToSrgb(v) * 255)));
  29. return [clamp(lr), clamp(lg), clamp(lb)];
  30. }
  31. function linearRgbToOklab(lr, lg, lb) {
  32. const l_ = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
  33. const m_ = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
  34. const s_ = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
  35. return [
  36. 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
  37. 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
  38. 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
  39. ];
  40. }
  41. function oklabToLinearRgb(L, a, b) {
  42. const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
  43. const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
  44. const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
  45. const l = l_ * l_ * l_;
  46. const m = m_ * m_ * m_;
  47. const s = s_ * s_ * s_;
  48. return [
  49. +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
  50. -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
  51. -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
  52. ];
  53. }
  54. function oklabToOklch(L, a, b) {
  55. const C = Math.sqrt(a * a + b * b);
  56. const H = (Math.atan2(b, a) * 180 / Math.PI + 360) % 360;
  57. return [L, C, H];
  58. }
  59. function oklchToOklab(L, C, H) {
  60. const hRad = H * Math.PI / 180;
  61. return [L, C * Math.cos(hRad), C * Math.sin(hRad)];
  62. }
  63. function rgbToHsl(r, g, b) {
  64. r /= 255; g /= 255; b /= 255;
  65. const max = Math.max(r, g, b);
  66. const min = Math.min(r, g, b);
  67. const d = max - min;
  68. const l = (max + min) / 2;
  69. if (d === 0) return [0, 0, l * 100];
  70. const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
  71. let h;
  72. if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
  73. else if (max === g) h = ((b - r) / d + 2) / 6;
  74. else h = ((r - g) / d + 4) / 6;
  75. return [h * 360, s * 100, l * 100];
  76. }
  77. function hslToRgb(h, s, l) {
  78. h /= 360; s /= 100; l /= 100;
  79. if (s === 0) { const v = Math.round(l * 255); return [v, v, v]; }
  80. const hue2rgb = (p, q, t) => {
  81. if (t < 0) t += 1;
  82. if (t > 1) t -= 1;
  83. if (t < 1/6) return p + (q - p) * 6 * t;
  84. if (t < 1/2) return q;
  85. if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
  86. return p;
  87. };
  88. const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  89. const p = 2 * l - q;
  90. return [
  91. Math.round(hue2rgb(p, q, h + 1/3) * 255),
  92. Math.round(hue2rgb(p, q, h) * 255),
  93. Math.round(hue2rgb(p, q, h - 1/3) * 255),
  94. ];
  95. }
  96. function relativeLuminance(r, g, b) {
  97. const [lr, lg, lb] = rgbToLinear(r, g, b);
  98. return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
  99. }
  100. // ========== Parsing ==========
  101. function parseColor(str) {
  102. str = str.trim();
  103. // Hex
  104. if (str.startsWith('#') || /^[0-9a-f]{3,8}$/i.test(str)) {
  105. let hex = str.replace('#', '');
  106. if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
  107. const n = parseInt(hex, 16);
  108. return { type: 'hex', rgb: [(n >> 16) & 255, (n >> 8) & 255, n & 255] };
  109. }
  110. // rgb()
  111. const rgbMatch = str.match(/rgb\(\s*(\d+)\s*[,\s]\s*(\d+)\s*[,\s]\s*(\d+)\s*\)/i);
  112. if (rgbMatch) return { type: 'rgb', rgb: [+rgbMatch[1], +rgbMatch[2], +rgbMatch[3]] };
  113. // hsl()
  114. const hslMatch = str.match(/hsl\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)%?\s*[,\s]\s*([\d.]+)%?\s*\)/i);
  115. if (hslMatch) {
  116. const rgb = hslToRgb(+hslMatch[1], +hslMatch[2], +hslMatch[3]);
  117. return { type: 'hsl', rgb, hsl: [+hslMatch[1], +hslMatch[2], +hslMatch[3]] };
  118. }
  119. // oklch()
  120. const oklchMatch = str.match(/oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\)/i);
  121. if (oklchMatch) {
  122. const [L, C, H] = [+oklchMatch[1], +oklchMatch[2], +oklchMatch[3]];
  123. const [labL, labA, labB] = oklchToOklab(L, C, H);
  124. const [lr, lg, lb] = oklabToLinearRgb(labL, labA, labB);
  125. const rgb = linearToRgb(lr, lg, lb);
  126. return { type: 'oklch', rgb, oklch: [L, C, H], oklab: [labL, labA, labB] };
  127. }
  128. // oklab()
  129. const oklabMatch = str.match(/oklab\(\s*([\d.e+-]+)\s+([\d.e+-]+)\s+([\d.e+-]+)\s*\)/i);
  130. if (oklabMatch) {
  131. const [L, a, b] = [+oklabMatch[1], +oklabMatch[2], +oklabMatch[3]];
  132. const [lr, lg, lb] = oklabToLinearRgb(L, a, b);
  133. const rgb = linearToRgb(lr, lg, lb);
  134. return { type: 'oklab', rgb, oklab: [L, a, b] };
  135. }
  136. return null;
  137. }
  138. // ========== Main ==========
  139. const parsed = parseColor(raw);
  140. if (!parsed) {
  141. console.error(`Error: Could not parse color "${raw}"`);
  142. console.error('Supported formats: #hex, rgb(r,g,b), hsl(h,s%,l%), oklch(l c h), oklab(l a b)');
  143. process.exit(1);
  144. }
  145. const [r, g, b] = parsed.rgb;
  146. const hex = '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
  147. const [lr, lg, lb] = rgbToLinear(r, g, b);
  148. const [labL, labA, labB] = parsed.oklab || linearRgbToOklab(lr, lg, lb);
  149. const [oklchL, oklchC, oklchH] = parsed.oklch || oklabToOklch(labL, labA, labB);
  150. const [hslH, hslS, hslL] = parsed.hsl || rgbToHsl(r, g, b);
  151. const lum = relativeLuminance(r, g, b);
  152. const inGamut = [lr, lg, lb].every(v => v >= -0.001 && v <= 1.001);
  153. console.log(`
  154. Input: ${raw}
  155. hex ${hex}
  156. rgb rgb(${r}, ${g}, ${b})
  157. hsl hsl(${hslH.toFixed(1)}, ${hslS.toFixed(1)}%, ${hslL.toFixed(1)}%)
  158. oklch oklch(${oklchL.toFixed(4)} ${oklchC.toFixed(4)} ${oklchH.toFixed(1)})
  159. oklab oklab(${labL.toFixed(4)} ${labA.toFixed(4)} ${labB.toFixed(4)})
  160. Luminance ${lum.toFixed(4)}
  161. sRGB ${inGamut ? 'in gamut' : 'OUT OF GAMUT - will be clamped on sRGB displays'}
  162. `.trim());