hex2oklch 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-MIT +22 -0
- package/README.md +97 -0
- package/oklch-swatches.png +0 -0
- package/package.json +48 -0
- package/src/index.js +176 -0
package/LICENSE-MIT
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2026 Glenn Cueto
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
|
4
|
+
obtaining a copy of this software and associated documentation
|
|
5
|
+
files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use,
|
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the
|
|
9
|
+
Software is furnished to do so, subject to the following
|
|
10
|
+
conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be
|
|
13
|
+
included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
hex2oklch
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
Converts hex color to OKLCH and calculates appropriate corresponding foreground.
|
|
5
|
+
|
|
6
|
+
HEX2OKLCH is based on my original project, https://github.com/glnster/hex2rgb, and is updated to use OKLCH instead of RGB.
|
|
7
|
+
|
|
8
|
+
## Example
|
|
9
|
+
|
|
10
|
+
For a dark hex color, hex2oklch will give you the OKLCH equivalent (e.g. `oklch(58.87% 0.2323 282.69)`). It will also calculate and return an appropriate contrasting foreground (either 'black' or 'white').
|
|
11
|
+
|
|
12
|
+
Here's hex2oklch in action. Note the black or white text color (foreground) based on the background color.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install hex2oklch
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import hex2oklch from 'hex2oklch';
|
|
26
|
+
|
|
27
|
+
const hex = '0033ff';
|
|
28
|
+
const shorthex = '03f';
|
|
29
|
+
const hashhex = '#0033ff';
|
|
30
|
+
const badhex = '00PS1E';
|
|
31
|
+
|
|
32
|
+
hex2oklch(hex).oklch; // => { L: 0.26..., C: 0.24..., H: 264.05... }
|
|
33
|
+
hex2oklch(shorthex).oklch; // => same L,C,H as 0033ff
|
|
34
|
+
hex2oklch(hashhex).oklch; // => same
|
|
35
|
+
hex2oklch(hex).oklchString; // => 'oklch(26.45% 0.2432 264.05)' (example)
|
|
36
|
+
hex2oklch(hex).yiq; // => 'white'
|
|
37
|
+
|
|
38
|
+
// try with bad input and with options specified
|
|
39
|
+
hex2oklch(badhex, { debug: true, oklchStringDefault: '#e9e9e9' });
|
|
40
|
+
// logs "(hex2oklch) 00PS1E: Expected 3 or 6 HEX-ONLY chars. Returning defaults."
|
|
41
|
+
// Returns oklch { L: 1, C: 0, H: 0 }, oklchString '#e9e9e9'
|
|
42
|
+
// and yiq 'inherit' as fall-backs.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### *hex2oklch( hex {String}, options {Object} )*
|
|
48
|
+
|
|
49
|
+
#### hex
|
|
50
|
+
A hex-only string of 3 or 6 characters. If the string has a # prefix, the # gets trimmed off.
|
|
51
|
+
|
|
52
|
+
#### {debug: true | false}
|
|
53
|
+
|
|
54
|
+
You can pass {debug: true} to enable errors logged to console.
|
|
55
|
+
|
|
56
|
+
#### {oklchStringDefault: "String e.g. transparent | black | #e9e9e9"}
|
|
57
|
+
|
|
58
|
+
You can specify a default string that `.oklchString` will return when hex input is invalid or yet to be calculated.
|
|
59
|
+
|
|
60
|
+
#### {yiqDefault: "String e.g. inherit | gray | #333"}
|
|
61
|
+
|
|
62
|
+
Similar to oklchStringDefault above.
|
|
63
|
+
|
|
64
|
+
#### .oklch
|
|
65
|
+
Returns an object `{ L, C, H }`. L is in [0, 1], C ≥ 0, H in [0, 360] degrees (or 0 when achromatic). If hex input is invalid or yet to be calculated, `{ L: 1, C: 0, H: 0 }` is returned as a fallback.
|
|
66
|
+
|
|
67
|
+
#### .oklchString
|
|
68
|
+
Returns a string in `oklch(L% C H)` format (e.g. `oklch(58.87% 0.2323 282.69)`). If hex input is invalid or yet to be calculated, either `'inherit'` or your specified string value is returned as a fallback.
|
|
69
|
+
|
|
70
|
+
#### .yiq
|
|
71
|
+
Returns a string of either `'white'` or `'black'`. If hex input is invalid or yet to be calculated, either `'inherit'` or your specified string value is returned as a fallback.
|
|
72
|
+
|
|
73
|
+
## Tests
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
# Unit tests (Vitest)
|
|
77
|
+
npm test
|
|
78
|
+
npm run test:coverage
|
|
79
|
+
|
|
80
|
+
# Visual tests (Playwright) — opens a browser with color swatches
|
|
81
|
+
npm run test:visual # headless
|
|
82
|
+
npm run test:visual:ui # interactive UI mode (like QUnit)
|
|
83
|
+
npm run test:visual:debug # headed browser + Inspector, paused
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Visual tests serve an HTML page that renders color swatches using the library, then verify foreground/background colors in a real browser. Use `test:visual:ui` for an interactive test runner or `test:visual:debug` to pause and inspect elements.
|
|
87
|
+
|
|
88
|
+
## Contributing
|
|
89
|
+
|
|
90
|
+
No formal styleguide, but please maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code.
|
|
91
|
+
|
|
92
|
+
## Thanks
|
|
93
|
+
- Brian Suda for his article, [Calculating Color Contrast](http://24ways.org/2010/calculating-color-contrast/), on 24 ways, which inspired the original hex2rgb project.
|
|
94
|
+
|
|
95
|
+
## Release History
|
|
96
|
+
|
|
97
|
+
- 1.0.0 - Initial release. Ported from hex2rgb. Hex to OKLCH conversion; YIQ-derived foreground; `oklchStringDefault` and `yiqDefault` options; ESM-only; Vitest and Playwright visual tests; ESLint flat config; GitHub Actions CI.
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hex2oklch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Converts hex color to OKLCH and calculates appropriate corresponding foreground.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:coverage": "vitest run --coverage",
|
|
17
|
+
"test:visual": "playwright test",
|
|
18
|
+
"test:visual:ui": "playwright test --ui",
|
|
19
|
+
"test:visual:debug": "playwright test --debug",
|
|
20
|
+
"lint": "eslint ."
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/glnster/hex2oklch.git"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"hex",
|
|
28
|
+
"oklch",
|
|
29
|
+
"yiq",
|
|
30
|
+
"convert",
|
|
31
|
+
"hex to oklch",
|
|
32
|
+
"contrast",
|
|
33
|
+
"foreground"
|
|
34
|
+
],
|
|
35
|
+
"author": "Glenn Cueto <glenncueto@gmail.com> (http://gcgrafix.com)",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/glnster/hex2oklch/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/glnster/hex2oklch",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@eslint/js": "^9.0.0",
|
|
43
|
+
"@playwright/test": "^1.58.2",
|
|
44
|
+
"eslint": "^9.0.0",
|
|
45
|
+
"globals": "^15.0.0",
|
|
46
|
+
"vitest": "^3.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hex2oklch
|
|
3
|
+
* https://github.com/glnster/hex2oklch
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2026 Glenn Cueto
|
|
6
|
+
* Licensed under the MIT license.
|
|
7
|
+
*
|
|
8
|
+
* Converts hex color to OKLCH. Calculates corresponding foreground.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} hex - The hex color to be converted. Can be 3 or 6 HEX-ONLY chars.
|
|
11
|
+
* @param {Object} [options] - Optional options object.
|
|
12
|
+
* @param {boolean} [options.debug=false] - Log invalid hex to console when true.
|
|
13
|
+
* @param {string} [options.oklchStringDefault='inherit'] - Default oklchString when hex is invalid.
|
|
14
|
+
* @param {string} [options.yiqDefault='inherit'] - Default yiq when hex is invalid.
|
|
15
|
+
* @return {{ oklch: { L: number, C: number, H: number }, oklchString: string, yiq: string }}
|
|
16
|
+
* oklch - { L, C, H }; L in [0,1], C >= 0, H in [0,360] (or NaN when C ≈ 0). Invalid hex: { L: 1, C: 0, H: 0 }.
|
|
17
|
+
* oklchString - CSS string e.g. 'oklch(58.87% 0.2323 282.69)'. Invalid: options.oklchStringDefault or 'inherit'.
|
|
18
|
+
* yiq - 'black' or 'white' as foreground against the hex. Invalid: options.yiqDefault or 'inherit'.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const hex2oklch = function (hex, options) {
|
|
22
|
+
if (typeof hex !== 'string') {
|
|
23
|
+
throw new TypeError('Expected a string');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
hex = hex.replace(/^#/, '');
|
|
27
|
+
|
|
28
|
+
options = options || {};
|
|
29
|
+
options.debug = (typeof options.debug === 'boolean') ? options.debug : false;
|
|
30
|
+
options.oklchStringDefault = (typeof options.oklchStringDefault === 'string') ? options.oklchStringDefault : 'inherit';
|
|
31
|
+
options.yiqDefault = (typeof options.yiqDefault === 'string') ? options.yiqDefault : 'inherit';
|
|
32
|
+
|
|
33
|
+
const defaultOklch = { L: 1, C: 0, H: 0 };
|
|
34
|
+
let oklch = defaultOklch;
|
|
35
|
+
let oklchString = options.oklchStringDefault;
|
|
36
|
+
let yiqres = options.yiqDefault;
|
|
37
|
+
|
|
38
|
+
if (hex.length === 3) {
|
|
39
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cleanHex = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
43
|
+
|
|
44
|
+
if (cleanHex !== null) {
|
|
45
|
+
const r8 = parseInt(cleanHex[1], 16);
|
|
46
|
+
const g8 = parseInt(cleanHex[2], 16);
|
|
47
|
+
const b8 = parseInt(cleanHex[3], 16);
|
|
48
|
+
const r = r8 / 255;
|
|
49
|
+
const g = g8 / 255;
|
|
50
|
+
const b = b8 / 255;
|
|
51
|
+
|
|
52
|
+
oklch = sRGBToOklch(r, g, b);
|
|
53
|
+
oklchString = formatOklchString(oklch);
|
|
54
|
+
const rgb = oklchToSRGB(oklch);
|
|
55
|
+
const Y = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
|
|
56
|
+
yiqres = (Y >= 128 || Number.isNaN(Y)) ? 'black' : 'white';
|
|
57
|
+
} else if (options.debug === true) {
|
|
58
|
+
console.error('(hex2oklch) ' + hex + ': Expected 3 or 6 HEX-ONLY chars. Returning defaults.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
oklch,
|
|
63
|
+
oklchString,
|
|
64
|
+
yiq: yiqres
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// --- sRGB (0–1) → OKLCH (L 0–1, C ≥ 0, H 0–360 or NaN) ---
|
|
69
|
+
|
|
70
|
+
function linearizeSRGB(v) {
|
|
71
|
+
const abs = Math.abs(v);
|
|
72
|
+
const lin = abs <= 0.04045 ? abs / 12.92 : Math.pow((abs + 0.055) / 1.055, 2.4);
|
|
73
|
+
return v < 0 ? -lin : lin;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// D65 linear sRGB to XYZ (W3C CSS Color 4 rationals)
|
|
77
|
+
const LIN_SRGB_TO_XYZ = [
|
|
78
|
+
[506752 / 1228815, 87881 / 245763, 12673 / 70218],
|
|
79
|
+
[87098 / 409605, 175762 / 245763, 12673 / 175545],
|
|
80
|
+
[7918 / 409605, 87881 / 737289, 1001167 / 1053270]
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// XYZ to linear LMS (W3C OKLAB)
|
|
84
|
+
const XYZ_TO_LMS = [
|
|
85
|
+
[0.8189330101, 0.3618667424, -0.1288597137],
|
|
86
|
+
[0.0329845436, 0.9293118715, 0.0361456387],
|
|
87
|
+
[0.0482003018, 0.2643662691, 0.6338517070]
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// LMS' (after cbrt) to L,a,b (W3C OKLAB)
|
|
91
|
+
const LMS_TO_LAB = [
|
|
92
|
+
[0.2104542553, 0.7936177850, -0.0040720468],
|
|
93
|
+
[1.9779984951, -2.4285922050, 0.4505937099],
|
|
94
|
+
[0.0259040371, 0.7827717662, -0.8086757660]
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
function mul3(M, x, y, z) {
|
|
98
|
+
return [
|
|
99
|
+
M[0][0] * x + M[0][1] * y + M[0][2] * z,
|
|
100
|
+
M[1][0] * x + M[1][1] * y + M[1][2] * z,
|
|
101
|
+
M[2][0] * x + M[2][1] * y + M[2][2] * z
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sRGBToOklch(r, g, b) {
|
|
106
|
+
const lr = linearizeSRGB(r);
|
|
107
|
+
const lg = linearizeSRGB(g);
|
|
108
|
+
const lb = linearizeSRGB(b);
|
|
109
|
+
const [x, y, z] = mul3(LIN_SRGB_TO_XYZ, lr, lg, lb);
|
|
110
|
+
const [l, m, s] = mul3(XYZ_TO_LMS, x, y, z);
|
|
111
|
+
const l_ = Math.cbrt(l);
|
|
112
|
+
const m_ = Math.cbrt(m);
|
|
113
|
+
const s_ = Math.cbrt(s);
|
|
114
|
+
const [L, a, bLab] = mul3(LMS_TO_LAB, l_, m_, s_);
|
|
115
|
+
const C = Math.sqrt(a * a + bLab * bLab);
|
|
116
|
+
const H = C <= 1e-10 ? 0 : (Math.atan2(bLab, a) * 180 / Math.PI + 360) % 360;
|
|
117
|
+
return { L, C, H };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function formatOklchString(oklch) {
|
|
121
|
+
const Lpct = (oklch.L * 100).toFixed(2) + '%';
|
|
122
|
+
const C = oklch.C.toFixed(4);
|
|
123
|
+
const H = (Number.isNaN(oklch.H) ? 0 : oklch.H).toFixed(2);
|
|
124
|
+
return `oklch(${Lpct} ${C} ${H})`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- OKLCH → sRGB (0–255) for YIQ ---
|
|
128
|
+
|
|
129
|
+
// L,a,b to LMS' (inverse of LMS_TO_LAB)
|
|
130
|
+
const LAB_TO_LMS = [
|
|
131
|
+
[1.0, 0.3963377774, 0.2158037573],
|
|
132
|
+
[1.0, -0.1055613458, -0.0638541728],
|
|
133
|
+
[1.0, -0.0894841775, -1.2914855480]
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// LMS to XYZ (inverse of XYZ_TO_LMS)
|
|
137
|
+
const LMS_TO_XYZ = [
|
|
138
|
+
[1.2270138511, -0.5577999803, 0.2812561489],
|
|
139
|
+
[-0.0405801784, 1.1122568696, -0.0716766787],
|
|
140
|
+
[-0.0763812845, -0.4214819784, 1.5861632204]
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// XYZ to linear sRGB (W3C)
|
|
144
|
+
const XYZ_TO_LIN_SRGB = [
|
|
145
|
+
[12831 / 3959, -329 / 214, -1974 / 3959],
|
|
146
|
+
[-851781 / 878410, 1648619 / 878410, 36519 / 878410],
|
|
147
|
+
[705 / 12673, -2585 / 12673, 705 / 667]
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
function oklchToSRGB(oklch) {
|
|
151
|
+
const L = oklch.L;
|
|
152
|
+
const a = oklch.C * Math.cos(oklch.H * Math.PI / 180);
|
|
153
|
+
const b = oklch.C * Math.sin(oklch.H * Math.PI / 180);
|
|
154
|
+
const [l_, m_, s_] = mul3(LAB_TO_LMS, L, a, b);
|
|
155
|
+
const l = l_ * l_ * l_;
|
|
156
|
+
const m = m_ * m_ * m_;
|
|
157
|
+
const s = s_ * s_ * s_;
|
|
158
|
+
const [x, y, z] = mul3(LMS_TO_XYZ, l, m, s);
|
|
159
|
+
const [lr, lg, lb] = mul3(XYZ_TO_LIN_SRGB, x, y, z);
|
|
160
|
+
|
|
161
|
+
function delinearize(v) {
|
|
162
|
+
const abs = Math.abs(v);
|
|
163
|
+
const gam = abs <= 0.0031308 ? 12.92 * abs : (Math.sign(v) || 1) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
|
|
164
|
+
return gam;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let r = delinearize(lr);
|
|
168
|
+
let g = delinearize(lg);
|
|
169
|
+
let bOut = delinearize(lb);
|
|
170
|
+
r = Math.round(Math.max(0, Math.min(1, r)) * 255);
|
|
171
|
+
g = Math.round(Math.max(0, Math.min(1, g)) * 255);
|
|
172
|
+
bOut = Math.round(Math.max(0, Math.min(1, bOut)) * 255);
|
|
173
|
+
return [r, g, bOut];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default hex2oklch;
|