|
| 1 | +// usage: node ./scripts/rgbToOklch.js > output-colors.json |
| 2 | +// taken from https://github.com/ChromeDevTools/devtools-frontend/tree/main/front_end |
| 3 | + |
| 4 | +const { readFileSync } = require('fs'); |
| 5 | +const path = require('path'); |
| 6 | + |
| 7 | +class Vector3 { |
| 8 | + values = [0, 0, 0]; |
| 9 | + constructor(values) { |
| 10 | + if (values) { |
| 11 | + this.values = values; |
| 12 | + } |
| 13 | + } |
| 14 | +} |
| 15 | +class Matrix3x3 { |
| 16 | + values = [ |
| 17 | + [0, 0, 0], |
| 18 | + [0, 0, 0], |
| 19 | + [0, 0, 0], |
| 20 | + ]; |
| 21 | + |
| 22 | + constructor(values) { |
| 23 | + if (values) { |
| 24 | + this.values = values; |
| 25 | + } |
| 26 | + } |
| 27 | + |
| 28 | + multiply(other) { |
| 29 | + const dst = new Vector3(); |
| 30 | + for (let row = 0; row < 3; ++row) { |
| 31 | + dst.values[row] = this.values[row][0] * other.values[0] + this.values[row][1] * other.values[1] + |
| 32 | + this.values[row][2] * other.values[2]; |
| 33 | + } |
| 34 | + return dst; |
| 35 | + } |
| 36 | +} |
| 37 | +class TransferFunction { |
| 38 | + g; |
| 39 | + a; |
| 40 | + b; |
| 41 | + c; |
| 42 | + d; |
| 43 | + e; |
| 44 | + f; |
| 45 | + |
| 46 | + constructor(g, a, b = 0, c = 0, d = 0, e = 0, f = 0) { |
| 47 | + this.g = g; |
| 48 | + this.a = a; |
| 49 | + this.b = b; |
| 50 | + this.c = c; |
| 51 | + this.d = d; |
| 52 | + this.e = e; |
| 53 | + this.f = f; |
| 54 | + } |
| 55 | + |
| 56 | + eval(val) { |
| 57 | + const sign = val < 0 ? -1.0 : 1.0; |
| 58 | + const abs = val * sign; |
| 59 | + |
| 60 | + // 0 <= |encoded| < d path |
| 61 | + if (abs < this.d) { |
| 62 | + return sign * (this.c * abs + this.f); |
| 63 | + } |
| 64 | + |
| 65 | + // d <= |encoded| path |
| 66 | + return sign * (Math.pow(this.a * abs + this.b, this.g) + this.e); |
| 67 | + } |
| 68 | +} |
| 69 | +function applyTransferFns(fn, r, g, b) { |
| 70 | + return [fn.eval(r), fn.eval(g), fn.eval(b)]; |
| 71 | +} |
| 72 | + |
| 73 | +const EPSILON = 0.01; |
| 74 | +const GAMUT_sRGB = new Matrix3x3([ |
| 75 | + [0.436065674, 0.385147095, 0.143066406], |
| 76 | + [0.222488403, 0.716873169, 0.060607910], |
| 77 | + [0.013916016, 0.097076416, 0.714096069], |
| 78 | +]); |
| 79 | +const LMS_TO_OKLAB_MATRIX = new Matrix3x3([ |
| 80 | + [0.2104542553, 0.7936177849999999, -0.0040720468], |
| 81 | + [1.9779984951000003, -2.4285922049999997, 0.4505937099000001], |
| 82 | + [0.025904037099999982, 0.7827717662, -0.8086757660000001], |
| 83 | +]); |
| 84 | +const XYZ_TO_LMS_MATRIX = new Matrix3x3([ |
| 85 | + [0.8190224432164319, 0.3619062562801221, -0.12887378261216414], |
| 86 | + [0.0329836671980271, 0.9292868468965546, 0.03614466816999844], |
| 87 | + [0.048177199566046255, 0.26423952494422764, 0.6335478258136937], |
| 88 | +]); |
| 89 | +const XYZD50_TO_XYZD65_MATRIX = new Matrix3x3([ |
| 90 | + [0.9555366447632887, -0.02306009252137888, 0.06321844147263304], |
| 91 | + [-0.028315378228764922, 1.009951351591575, 0.021026001591792402], |
| 92 | + [0.012308773293784308, -0.02050053471777469, 1.3301947294775631], |
| 93 | +]); |
| 94 | +const LMS_TO_XYZ_MATRIX = new Matrix3x3([ |
| 95 | + [1.226879873374156, -0.5578149965554814, 0.2813910501772159], |
| 96 | + [-0.040575762624313734, 1.1122868293970596, -0.07171106666151703], |
| 97 | + [-0.07637294974672144, -0.4214933239627915, 1.586924024427242], |
| 98 | +]); |
| 99 | +const OKLAB_TO_LMS_MATRIX = new Matrix3x3([ |
| 100 | + [0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339], |
| 101 | + [1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402], |
| 102 | + [1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399], |
| 103 | +]); |
| 104 | +const XYZD65_TO_XYZD50_MATRIX = new Matrix3x3([ |
| 105 | + [1.0478573189120088, 0.022907374491829943, -0.050162247377152525], |
| 106 | + [0.029570500050499514, 0.9904755577034089, -0.017061518194840468], |
| 107 | + [-0.00924047197558879, 0.015052921526981566, 0.7519708530777581], |
| 108 | +]); |
| 109 | +const TRANSFER_sRGB = new TransferFunction(2.4, (1 / 1.055), (0.055 / 1.055), (1 / 12.92), 0.04045, 0.0, 0.0) |
| 110 | + |
| 111 | +function normalizeHue(hue) { |
| 112 | + // Even though it is highly unlikely, hue can be |
| 113 | + // very negative like -400. The initial modulo |
| 114 | + // operation makes sure that the if the number is |
| 115 | + // negative, it is between [-360, 0]. |
| 116 | + return ((hue % 360) + 360) % 360; |
| 117 | +} |
| 118 | +function equals(a, b, accuracy = EPSILON) { |
| 119 | + if (Array.isArray(a) && Array.isArray(b)) { |
| 120 | + if (a.length !== b.length) { |
| 121 | + return false; |
| 122 | + } |
| 123 | + for (const i in a) { |
| 124 | + if (!equals(a[i], b[i])) { |
| 125 | + return false; |
| 126 | + } |
| 127 | + } |
| 128 | + return true; |
| 129 | + } |
| 130 | + if (Array.isArray(a) || Array.isArray(b)) { |
| 131 | + return false; |
| 132 | + } |
| 133 | + if (a === null || b === null) { |
| 134 | + return a === b; |
| 135 | + } |
| 136 | + return Math.abs(a - b) < accuracy; |
| 137 | +} |
| 138 | + |
| 139 | +function radToDeg(rad) { |
| 140 | + return rad * (180 / Math.PI); |
| 141 | +} |
| 142 | +function degToRad(deg) { |
| 143 | + return deg * (Math.PI / 180); |
| 144 | +} |
| 145 | +function round(v) { |
| 146 | + return Math.round(v * 1000) / 1000; |
| 147 | +} |
| 148 | +function clamp(value, { min, max }) { |
| 149 | + if (value === null) { |
| 150 | + return value; |
| 151 | + } |
| 152 | + if (min !== undefined) { |
| 153 | + value = Math.max(value, min); |
| 154 | + } |
| 155 | + if (max !== undefined) { |
| 156 | + value = Math.min(value, max); |
| 157 | + } |
| 158 | + return value; |
| 159 | +} |
| 160 | + |
| 161 | +function hexToRgb(hex) { |
| 162 | + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); |
| 163 | + return result ? { |
| 164 | + r: parseInt(result[1], 16), |
| 165 | + g: parseInt(result[2], 16), |
| 166 | + b: parseInt(result[3], 16) |
| 167 | + } : hex; |
| 168 | +} |
| 169 | + |
| 170 | +function create(r, g, b) { |
| 171 | + let [l, c, h] = xyzd50ToOklch(...toXyzd50(r, g, b)) |
| 172 | + |
| 173 | + l = clamp(l, { min: 0, max: 1 }); |
| 174 | + c = equals(l, 0) || equals(l, 1) ? 0 : c; |
| 175 | + c = clamp(c, { min: 0 }); |
| 176 | + h = equals(c, 0) ? 0 : h; |
| 177 | + h = normalizeHue(h); |
| 178 | + |
| 179 | + return [l, c, h] |
| 180 | +} |
| 181 | + |
| 182 | +function srgbToXyzd50(r, g, b) { |
| 183 | + const [mappedR, mappedG, mappedB] = applyTransferFns(TRANSFER_sRGB, r, g, b); |
| 184 | + const rgbInput = new Vector3([mappedR, mappedG, mappedB]); |
| 185 | + const xyzOutput = GAMUT_sRGB.multiply(rgbInput); |
| 186 | + return xyzOutput.values; |
| 187 | +} |
| 188 | +function toXyzd50(r, g, b) { |
| 189 | + return srgbToXyzd50(r, g, b); |
| 190 | +} |
| 191 | +function labToLch(l, a, b) { |
| 192 | + return [l, Math.sqrt(a * a + b * b), radToDeg(Math.atan2(b, a))]; |
| 193 | +} |
| 194 | +function xyzd65ToOklab(x, y, z) { |
| 195 | + const xyzInput = new Vector3([x, y, z]); |
| 196 | + const lmsIntermediate = XYZ_TO_LMS_MATRIX.multiply(xyzInput); |
| 197 | + |
| 198 | + lmsIntermediate.values[0] = Math.pow(lmsIntermediate.values[0], 1.0 / 3.0); |
| 199 | + lmsIntermediate.values[1] = Math.pow(lmsIntermediate.values[1], 1.0 / 3.0); |
| 200 | + lmsIntermediate.values[2] = Math.pow(lmsIntermediate.values[2], 1.0 / 3.0); |
| 201 | + |
| 202 | + const labOutput = LMS_TO_OKLAB_MATRIX.multiply(lmsIntermediate); |
| 203 | + return [labOutput.values[0], labOutput.values[1], labOutput.values[2]]; |
| 204 | +} |
| 205 | +function xyzd50ToD65(x, y, z) { |
| 206 | + const xyzInput = new Vector3([x, y, z]); |
| 207 | + const xyzOutput = XYZD50_TO_XYZD65_MATRIX.multiply(xyzInput); |
| 208 | + return xyzOutput.values; |
| 209 | +} |
| 210 | +function xyzd50ToOklch(x, y, z) { |
| 211 | + const [x65, y65, z65] = xyzd50ToD65(x, y, z); |
| 212 | + const [l, a, b] = xyzd65ToOklab(x65, y65, z65); |
| 213 | + return labToLch(l, a, b); |
| 214 | +} |
| 215 | +function toXyzd50(l, c, h) { |
| 216 | + return oklchToXyzd50(l, c, h); |
| 217 | +} |
| 218 | +function oklchToXyzd50(lInput, c, h) { |
| 219 | + const [l, a, b] = lchToLab(lInput, c, h); |
| 220 | + const [x65, y65, z65] = oklabToXyzd65(l, a, b); |
| 221 | + return xyzd65ToD50(x65, y65, z65); |
| 222 | +} |
| 223 | +function xyzd65ToD50(x, y, z) { |
| 224 | + const xyzInput = new Vector3([x, y, z]); |
| 225 | + const xyzOutput = XYZD65_TO_XYZD50_MATRIX.multiply(xyzInput); |
| 226 | + return xyzOutput.values; |
| 227 | +} |
| 228 | +function oklabToXyzd65(l, a, b) { |
| 229 | + const labInput = new Vector3([l, a, b]); |
| 230 | + const lmsIntermediate = OKLAB_TO_LMS_MATRIX.multiply(labInput); |
| 231 | + lmsIntermediate.values[0] = lmsIntermediate.values[0] * lmsIntermediate.values[0] * lmsIntermediate.values[0]; |
| 232 | + lmsIntermediate.values[1] = lmsIntermediate.values[1] * lmsIntermediate.values[1] * lmsIntermediate.values[1]; |
| 233 | + lmsIntermediate.values[2] = lmsIntermediate.values[2] * lmsIntermediate.values[2] * lmsIntermediate.values[2]; |
| 234 | + const xyzOutput = LMS_TO_XYZ_MATRIX.multiply(lmsIntermediate); |
| 235 | + return xyzOutput.values; |
| 236 | +} |
| 237 | + |
| 238 | +function lchToLab(l, c, h) { |
| 239 | + if (h === undefined) { |
| 240 | + return [l, 0, 0]; |
| 241 | + } |
| 242 | + |
| 243 | + return [l, c * Math.cos(degToRad(h)), c * Math.sin(degToRad(h))]; |
| 244 | +} |
| 245 | + |
| 246 | +/// |
| 247 | + |
| 248 | +const input = readFileSync(path.join(__dirname, 'rgbColors.txt'), 'utf8'); |
| 249 | +const result = input.split('\n').map(v => v.trim().split(':')).map(([variable, value]) => ({ name: variable.substring(2).toLowerCase(), value: value.trim().slice(0, -1) })).map(({ name, value }) => ({ name, value: value.slice(-2) === 'FF' ? hexToRgb(value.slice(0, -2)) : value })).reduce((p, v) => { |
| 250 | + const [name, value] = v.name.split('-'); |
| 251 | + if (parseInt(value) + '' !== value) return p; |
| 252 | + if (typeof v.value === 'string') { |
| 253 | + return p; |
| 254 | + } |
| 255 | + const { r, g, b } = v.value; |
| 256 | + const [l, c, h] = create(r / 255, g / 255, b / 255); |
| 257 | + p[name] ??= {}; |
| 258 | + if (isNaN(l)) { |
| 259 | + p[name][value] = v.value; |
| 260 | + return p; |
| 261 | + } |
| 262 | + p[name][value] = [round(l), round(c), round(h)].join(' '); |
| 263 | + return p; |
| 264 | +}, {}); |
| 265 | + |
| 266 | +console.log(JSON.stringify(result, null, 2)); |
0 commit comments