|
| 1 | +/** |
| 2 | + * Convert OKLCH color to HEX |
| 3 | + * OKLCH -> OKLab -> Linear RGB -> sRGB -> HEX |
| 4 | + */ |
| 5 | + |
| 6 | +// Convert OKLCH to OKLab |
| 7 | +function oklchToOklab(l, c, h) { |
| 8 | + const hRad = (h * Math.PI) / 180; |
| 9 | + const a = c * Math.cos(hRad); |
| 10 | + const b = c * Math.sin(hRad); |
| 11 | + return [l, a, b]; |
| 12 | +} |
| 13 | + |
| 14 | +// Convert OKLab to Linear RGB |
| 15 | +// Using the correct OKLab to LMS to Linear RGB conversion |
| 16 | +function oklabToLinearRgb(l, a, b) { |
| 17 | + // Convert OKLab to LMS (non-linear) |
| 18 | + const l_ = l + 0.3963377774 * a + 0.2158037573 * b; |
| 19 | + const m_ = l - 0.1055613458 * a - 0.0638541728 * b; |
| 20 | + const s_ = l - 0.0894841775 * a - 1.2914855480 * b; |
| 21 | + |
| 22 | + // Apply non-linearity (cube) |
| 23 | + const l_cubed = l_ ** 3; |
| 24 | + const m_cubed = m_ ** 3; |
| 25 | + const s_cubed = s_ ** 3; |
| 26 | + |
| 27 | + // Convert LMS to Linear RGB |
| 28 | + return [ |
| 29 | + +4.0767416621 * l_cubed - 3.3077115913 * m_cubed + 0.2309699292 * s_cubed, |
| 30 | + -1.2684380046 * l_cubed + 2.6097574011 * m_cubed - 0.3413193965 * s_cubed, |
| 31 | + -0.0041960863 * l_cubed - 0.7034186147 * m_cubed + 1.7076147010 * s_cubed, |
| 32 | + ]; |
| 33 | +} |
| 34 | + |
| 35 | +// Convert Linear RGB to sRGB (gamma correction) |
| 36 | +function linearRgbToSrgb(r, g, b) { |
| 37 | + const toSRGB = (c) => { |
| 38 | + if (c <= 0.0031308) { |
| 39 | + return 12.92 * c; |
| 40 | + } |
| 41 | + return 1.055 * Math.pow(c, 1.0 / 2.4) - 0.055; |
| 42 | + }; |
| 43 | + |
| 44 | + return [toSRGB(r), toSRGB(g), toSRGB(b)]; |
| 45 | +} |
| 46 | + |
| 47 | +// Clamp and convert to 0-255 |
| 48 | +function toByte(value) { |
| 49 | + return Math.max(0, Math.min(255, Math.round(value * 255))); |
| 50 | +} |
| 51 | + |
| 52 | +// Convert RGB to HEX |
| 53 | +function rgbToHex(r, g, b) { |
| 54 | + const toHex = (n) => { |
| 55 | + const hex = n.toString(16); |
| 56 | + return hex.length === 1 ? "0" + hex : hex; |
| 57 | + }; |
| 58 | + return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); |
| 59 | +} |
| 60 | + |
| 61 | +// Main conversion function |
| 62 | +function oklchToHex(oklchString) { |
| 63 | + // Parse oklch(62.3% 0.214 259.815) |
| 64 | + const match = oklchString.match(/oklch\(([\d.]+)%\s+([\d.]+)\s+([\d.]+)\)/); |
| 65 | + if (!match) { |
| 66 | + throw new Error(`Invalid OKLCH format: ${oklchString}`); |
| 67 | + } |
| 68 | + |
| 69 | + const l = parseFloat(match[1]) / 100; // Convert percentage to 0-1 |
| 70 | + const c = parseFloat(match[2]); |
| 71 | + const h = parseFloat(match[3]); |
| 72 | + |
| 73 | + // Convert OKLCH to OKLab |
| 74 | + const [labL, labA, labB] = oklchToOklab(l, c, h); |
| 75 | + |
| 76 | + // Convert OKLab to Linear RGB |
| 77 | + const [linearR, linearG, linearB] = oklabToLinearRgb(labL, labA, labB); |
| 78 | + |
| 79 | + // Convert Linear RGB to sRGB |
| 80 | + const [srgbR, srgbG, srgbB] = linearRgbToSrgb(linearR, linearG, linearB); |
| 81 | + |
| 82 | + // Clamp sRGB values to valid range [0, 1] |
| 83 | + const clampedR = Math.max(0, Math.min(1, srgbR)); |
| 84 | + const clampedG = Math.max(0, Math.min(1, srgbG)); |
| 85 | + const clampedB = Math.max(0, Math.min(1, srgbB)); |
| 86 | + |
| 87 | + // Convert to bytes |
| 88 | + const r = toByte(clampedR); |
| 89 | + const g = toByte(clampedG); |
| 90 | + const b = toByte(clampedB); |
| 91 | + |
| 92 | + // Convert to HEX |
| 93 | + return rgbToHex(r, g, b); |
| 94 | +} |
| 95 | + |
| 96 | +// Test the conversion with debugging |
| 97 | +const color1 = "oklch(62.3% 0.214 259.815)"; |
| 98 | +const color2 = "oklch(65.6% 0.241 354.308)"; |
| 99 | + |
| 100 | +console.log(`Converting: ${color1}`); |
| 101 | +const hex1 = oklchToHex(color1); |
| 102 | +console.log(`Result: ${hex1}`); |
| 103 | + |
| 104 | +// Parse and show intermediate values for debugging |
| 105 | +const match1 = color1.match(/oklch\(([\d.]+)%\s+([\d.]+)\s+([\d.]+)\)/); |
| 106 | +if (match1) { |
| 107 | + const l = parseFloat(match1[1]) / 100; |
| 108 | + const c = parseFloat(match1[2]); |
| 109 | + const h = parseFloat(match1[3]); |
| 110 | + const [labL, labA, labB] = oklchToOklab(l, c, h); |
| 111 | + const [linearR, linearG, linearB] = oklabToLinearRgb(labL, labA, labB); |
| 112 | + const [srgbR, srgbG, srgbB] = linearRgbToSrgb(linearR, linearG, linearB); |
| 113 | + console.log(` OKLab: L=${labL.toFixed(4)}, a=${labA.toFixed(4)}, b=${labB.toFixed(4)}`); |
| 114 | + console.log(` Linear RGB: R=${linearR.toFixed(4)}, G=${linearG.toFixed(4)}, B=${linearB.toFixed(4)}`); |
| 115 | + console.log(` sRGB: R=${srgbR.toFixed(4)}, G=${srgbG.toFixed(4)}, B=${srgbB.toFixed(4)}`); |
| 116 | +} |
| 117 | + |
| 118 | +console.log(`\nConverting: ${color2}`); |
| 119 | +const hex2 = oklchToHex(color2); |
| 120 | +console.log(`Result: ${hex2}`); |
| 121 | + |
| 122 | +// Parse and show intermediate values for debugging |
| 123 | +const match2 = color2.match(/oklch\(([\d.]+)%\s+([\d.]+)\s+([\d.]+)\)/); |
| 124 | +if (match2) { |
| 125 | + const l = parseFloat(match2[1]) / 100; |
| 126 | + const c = parseFloat(match2[2]); |
| 127 | + const h = parseFloat(match2[3]); |
| 128 | + const [labL, labA, labB] = oklchToOklab(l, c, h); |
| 129 | + const [linearR, linearG, linearB] = oklabToLinearRgb(labL, labA, labB); |
| 130 | + const [srgbR, srgbG, srgbB] = linearRgbToSrgb(linearR, linearG, linearB); |
| 131 | + console.log(` OKLab: L=${labL.toFixed(4)}, a=${labA.toFixed(4)}, b=${labB.toFixed(4)}`); |
| 132 | + console.log(` Linear RGB: R=${linearR.toFixed(4)}, G=${linearG.toFixed(4)}, B=${linearB.toFixed(4)}`); |
| 133 | + console.log(` sRGB: R=${srgbR.toFixed(4)}, G=${srgbG.toFixed(4)}, B=${srgbB.toFixed(4)}`); |
| 134 | +} |
| 135 | + |
| 136 | +console.log(`\nSVG gradient colors:`); |
| 137 | +console.log(` <stop offset="0" stop-color="${hex1}" />`); |
| 138 | +console.log(` <stop offset="1" stop-color="${hex2}" />`); |
| 139 | + |
0 commit comments