diff --git a/docs/api.md b/docs/api.md
index 5e5b1601..6ec62f2e 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -85,6 +85,8 @@ The available modes (color spaces) are listed below. For convenience, each color
| `lchuv` | CIELCHuv color space (D50 Illuminant) | culori.**lchuv**(_color_) |
| `lrgb` | Linear-light sRGB color space | culori.**lrgb**(_color_) |
| `luv` | CIELUV color space (D50 Illuminant) | culori.**luv**(_color_) |
+| `oklab` | Oklab color space | culori.**oklab**(_color_) |
+| `oklch` | Oklab color space, cylindrical form | culori.**oklch**(_color_) |
| `p3` | Display P3 color space | culori.**p3**(_color_) |
| `prophoto` | ProPhoto RGB color space | culori.**prophoto**(_color_) |
| `rec2020` | Rec. 2020 RGB color space | culori.**rec2020**(_color_) |
@@ -645,7 +647,7 @@ In cylindrical spaces, the hue is factored into the Euclidean distance in a vari
# culori.**differenceHueChroma**(_colorA_, _colorB_) · [Source](https://github.com/evercoder/culori/blob/master/src/difference.js)
-This is the handling of hue in cylindrical forms of CIE-based color spaces (`lch`, `lchuv`, `dlch`) and Jzazbz (`jch`).
+This is the handling of hue in cylindrical forms of CIE-based color spaces (`lch`, `lchuv`, `dlch`, `oklch`) and Jzazbz (`jch`).
# culori.**differenceHueSaturation**(_colorA_, _colorB_) · [Source](https://github.com/evercoder/culori/blob/master/src/difference.js)
diff --git a/docs/color-spaces.md b/docs/color-spaces.md
index 36db900e..ede093f8 100644
--- a/docs/color-spaces.md
+++ b/docs/color-spaces.md
@@ -198,6 +198,30 @@ The DIN99o color space in cylindrical form.
| `c` | `[0, 51.484]`≈ | Chroma |
| `h` | `[0, 360)` | Hue |
+### Oklab
+
+The [Oklab color space](https://bottosson.github.io/posts/oklab/), in Cartesian (Lab) and cylindrical (LCh) forms. It uses the D65 standard illuminant.
+
+#### `oklab`
+
+The Oklab color space in Cartesian form.
+
+| Channel | Range | Description |
+| ------- | ------------------ | --------------------- |
+| `l` | `[0, 1]` | Lightness |
+| `a` | `[-0.233, 0.276]`≈ | Green–red component |
+| `b` | `[-0.311, 0.198]`≈ | Blue–yellow component |
+
+#### `oklch`
+
+The Oklab color space in cylindrical form.
+
+| Channel | Range | Description |
+| ------- | ----------- | ----------- |
+| `l` | `[0, 1]` | Lightness |
+| `c` | `[0, 322]`≈ | Chroma |
+| `h` | `[0, 360)` | Hue |
+
### Jzazbz
The Jzazbz color space, as defined by:
diff --git a/src/index.js b/src/index.js
index ff8a8e26..229f221b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -22,6 +22,8 @@ import dlchDef from './dlch/definition';
import xyzDef from './xyz/definition';
import xyz65Def from './xyz65/definition';
import yiqDef from './yiq/definition';
+import oklabDef from './oklab/definition';
+import oklchDef from './oklch/definition';
import { defineMode } from './modes';
import converter from './converter';
@@ -50,6 +52,8 @@ defineMode(rgbDef);
defineMode(xyz65Def);
defineMode(xyzDef);
defineMode(yiqDef);
+defineMode(oklabDef);
+defineMode(oklchDef);
let a98 = converter('a98');
let cubehelix = converter('cubehelix');
@@ -75,6 +79,8 @@ let rgb = converter('rgb');
let xyz = converter('xyz');
let xyz65 = converter('xyz65');
let yiq = converter('yiq');
+let oklab = converter('oklab');
+let oklch = converter('oklch');
export {
a98,
@@ -102,7 +108,9 @@ export {
rgb,
xyz,
xyz65,
- yiq
+ yiq,
+ oklab,
+ oklch
};
export { formatter, formatHex, formatHex8, formatRgb } from './formatter';
diff --git a/src/oklab/convertLrgbToOklab.js b/src/oklab/convertLrgbToOklab.js
new file mode 100644
index 00000000..9474a18e
--- /dev/null
+++ b/src/oklab/convertLrgbToOklab.js
@@ -0,0 +1,18 @@
+export default ({ r, g, b, alpha }) => {
+ let L = Math.cbrt(0.412165612 * r + 0.536275208 * g + 0.0514575653 * b);
+ let M = Math.cbrt(0.211859107 * r + 0.6807189584 * g + 0.107406579 * b);
+ let S = Math.cbrt(0.0883097947 * r + 0.2818474174 * g + 0.6302613616 * b);
+
+ let res = {
+ mode: 'oklab',
+ l: 0.2104542553 * L + 0.793617785 * M - 0.0040720468 * S,
+ a: 1.9779984951 * L - 2.428592205 * M + 0.4505937099 * S,
+ b: 0.0259040371 * L + 0.7827717662 * M - 0.808675766 * S
+ };
+
+ if (alpha !== undefined) {
+ res.alpha = alpha;
+ }
+
+ return res;
+};
diff --git a/src/oklab/convertOklabToLrgb.js b/src/oklab/convertOklabToLrgb.js
new file mode 100644
index 00000000..12c690fb
--- /dev/null
+++ b/src/oklab/convertOklabToLrgb.js
@@ -0,0 +1,18 @@
+export default ({ l, a, b, alpha }) => {
+ let L = Math.pow(l + 0.3963377774 * a + 0.2158037573 * b, 3);
+ let M = Math.pow(l - 0.1055613458 * a - 0.0638541728 * b, 3);
+ let S = Math.pow(l - 0.0894841775 * a - 1.291485548 * b, 3);
+
+ let res = {
+ mode: 'lrgb',
+ r: +4.0767245293 * L - 3.3072168827 * M + 0.2307590544 * S,
+ g: -1.2681437731 * L + 2.6093323231 * M - 0.341134429 * S,
+ b: -0.0041119885 * L - 0.7034763098 * M + 1.7068625689 * S
+ };
+
+ if (alpha !== undefined) {
+ res.alpha = alpha;
+ }
+
+ return res;
+};
diff --git a/src/oklab/convertOklabToRgb.js b/src/oklab/convertOklabToRgb.js
new file mode 100644
index 00000000..d1cc17a3
--- /dev/null
+++ b/src/oklab/convertOklabToRgb.js
@@ -0,0 +1,4 @@
+import convertLrgbToRgb from '../lrgb/convertLrgbToRgb';
+import convertOklabToLrgb from './convertOklabToLrgb';
+
+export default c => convertLrgbToRgb(convertOklabToLrgb(c));
diff --git a/src/oklab/convertRgbToOklab.js b/src/oklab/convertRgbToOklab.js
new file mode 100644
index 00000000..d7679149
--- /dev/null
+++ b/src/oklab/convertRgbToOklab.js
@@ -0,0 +1,10 @@
+import convertRgbToLrgb from '../lrgb/convertRgbToLrgb';
+import convertLrgbToOklab from './convertLrgbToOklab';
+
+export default rgb => {
+ let res = convertLrgbToOklab(convertRgbToLrgb(rgb));
+ if (rgb.r === rgb.b && rgb.b === rgb.g) {
+ res.a = res.b = 0;
+ }
+ return res;
+};
diff --git a/src/oklab/definition.js b/src/oklab/definition.js
new file mode 100644
index 00000000..b7c0f893
--- /dev/null
+++ b/src/oklab/definition.js
@@ -0,0 +1,31 @@
+import convertOklabToLrgb from './convertOklabToLrgb';
+import convertLrgbToOklab from './convertLrgbToOklab';
+import convertRgbToOklab from './convertRgbToOklab';
+import convertOklabToRgb from './convertOklabToRgb';
+
+import lab from '../lab/definition';
+
+/*
+ Oklab, a perceptual color space for image processing by Björn Ottosson
+ Reference: https://bottosson.github.io/posts/oklab/
+ */
+
+export default {
+ ...lab,
+ mode: 'oklab',
+ alias: [],
+ output: {
+ lrgb: convertOklabToLrgb,
+ rgb: convertOklabToRgb
+ },
+ input: {
+ lrgb: convertLrgbToOklab,
+ rgb: convertRgbToOklab
+ },
+ ranges: {
+ l: [0, 1],
+ a: [-0.233, 0.276],
+ b: [-0.311, 0.198]
+ },
+ parsers: []
+};
diff --git a/src/oklch/definition.js b/src/oklch/definition.js
new file mode 100644
index 00000000..dd078853
--- /dev/null
+++ b/src/oklch/definition.js
@@ -0,0 +1,25 @@
+import lch from '../lch/definition';
+import convertLabToLch from '../lch/convertLabToLch';
+import convertLchToLab from '../lch/convertLchToLab';
+import convertOklabToRgb from '../oklab/convertOklabToRgb';
+import convertRgbToOklab from '../oklab/convertRgbToOklab';
+
+export default {
+ ...lch,
+ mode: 'oklch',
+ alias: [],
+ output: {
+ oklab: c => convertLchToLab(c, 'oklab'),
+ rgb: c => convertOklabToRgb(convertLchToLab(c, 'oklab'))
+ },
+ input: {
+ rgb: c => convertLabToLch(convertRgbToOklab(c), 'oklch'),
+ oklab: c => convertLabToLch(c, 'oklch')
+ },
+ parsers: [],
+ ranges: {
+ l: [0, 1],
+ c: [0, 0.322],
+ h: [0, 360]
+ }
+};
diff --git a/test/oklab.test.js b/test/oklab.test.js
new file mode 100644
index 00000000..16b07d40
--- /dev/null
+++ b/test/oklab.test.js
@@ -0,0 +1,30 @@
+import tape from 'tape';
+import { oklab } from '../src/index';
+
+tape('oklab', t => {
+ t.deepEqual(
+ oklab('white'),
+ { mode: 'oklab', l: 0.9999882345920056, a: 0, b: 0 },
+ 'white'
+ );
+
+ // Tests that achromatic RGB colors get a = b = 0 in OKLab
+ t.deepEqual(
+ oklab('#111'),
+ { mode: 'oklab', l: 0.17763568309569516, a: 0, b: 0 },
+ '#111'
+ );
+
+ t.deepEqual(oklab('black'), { mode: 'oklab', l: 0, a: 0, b: 0 }, 'black');
+ t.deepEqual(
+ oklab('red'),
+ {
+ mode: 'oklab',
+ l: 0.6279151939969809,
+ a: 0.2249032308661069,
+ b: 0.12580287012451802
+ },
+ 'red'
+ );
+ t.end();
+});
diff --git a/test/oklch.test.js b/test/oklch.test.js
new file mode 100644
index 00000000..1ea74118
--- /dev/null
+++ b/test/oklch.test.js
@@ -0,0 +1,27 @@
+import tape from 'tape';
+import { oklch } from '../src/index';
+
+tape('oklch', t => {
+ t.deepEqual(
+ oklch('white'),
+ { mode: 'oklch', l: 0.9999882345920056, c: 0 },
+ 'white'
+ );
+ t.deepEqual(
+ oklch('#111'),
+ { mode: 'oklch', l: 0.17763568309569516, c: 0 },
+ '#111'
+ );
+ t.deepEqual(oklch('black'), { mode: 'oklch', l: 0, c: 0 }, 'black');
+ t.deepEqual(
+ oklch('red'),
+ {
+ mode: 'oklch',
+ l: 0.6279151939969809,
+ c: 0.2576971582799852,
+ h: 29.22109743427473
+ },
+ 'red'
+ );
+ t.end();
+});
diff --git a/tools/ranges.js b/tools/ranges.js
index 4e728f47..83b8824b 100644
--- a/tools/ranges.js
+++ b/tools/ranges.js
@@ -31,4 +31,4 @@ let ranges = (mode, step = 0.01) => {
return res;
};
-console.log(ranges('lch65', 0.0025));
+console.log(ranges('oklch', 0.0025));