Skip to content

Commit

Permalink
Adds Oklab / Oklch color spaces (#109)
Browse files Browse the repository at this point in the history
* Adds Oklab / Oklch color spaces
* Adds documentation
  • Loading branch information
danburzo authored Dec 24, 2020
1 parent c3480fb commit 955115b
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 3 deletions.
4 changes: 3 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_) |
Expand Down Expand Up @@ -645,7 +647,7 @@ In cylindrical spaces, the hue is factored into the Euclidean distance in a vari

<a name="differenceHueChroma" href="#differenceHueChroma">#</a> culori.**differenceHueChroma**(_colorA_, _colorB_) &middot; [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 J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> (`jch`).
This is the handling of hue in cylindrical forms of CIE-based color spaces (`lch`, `lchuv`, `dlch`, `oklch`) and J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> (`jch`).

<a name="differenceHueSaturation" href="#differenceHueSaturation">#</a> culori.**differenceHueSaturation**(_colorA_, _colorB_) &middot; [Source](https://github.com/evercoder/culori/blob/master/src/difference.js)

Expand Down
24 changes: 24 additions & 0 deletions docs/color-spaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

### J<sub>z</sub>a<sub>z</sub>b<sub>z</sub>

The J<sub>z</sub>a<sub>z</sub>b<sub>z</sub> color space, as defined by:
Expand Down
10 changes: 9 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,6 +52,8 @@ defineMode(rgbDef);
defineMode(xyz65Def);
defineMode(xyzDef);
defineMode(yiqDef);
defineMode(oklabDef);
defineMode(oklchDef);

let a98 = converter('a98');
let cubehelix = converter('cubehelix');
Expand All @@ -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,
Expand Down Expand Up @@ -102,7 +108,9 @@ export {
rgb,
xyz,
xyz65,
yiq
yiq,
oklab,
oklch
};

export { formatter, formatHex, formatHex8, formatRgb } from './formatter';
Expand Down
18 changes: 18 additions & 0 deletions src/oklab/convertLrgbToOklab.js
Original file line number Diff line number Diff line change
@@ -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;
};
18 changes: 18 additions & 0 deletions src/oklab/convertOklabToLrgb.js
Original file line number Diff line number Diff line change
@@ -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;
};
4 changes: 4 additions & 0 deletions src/oklab/convertOklabToRgb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import convertLrgbToRgb from '../lrgb/convertLrgbToRgb';
import convertOklabToLrgb from './convertOklabToLrgb';

export default c => convertLrgbToRgb(convertOklabToLrgb(c));
10 changes: 10 additions & 0 deletions src/oklab/convertRgbToOklab.js
Original file line number Diff line number Diff line change
@@ -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;
};
31 changes: 31 additions & 0 deletions src/oklab/definition.js
Original file line number Diff line number Diff line change
@@ -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: []
};
25 changes: 25 additions & 0 deletions src/oklch/definition.js
Original file line number Diff line number Diff line change
@@ -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]
}
};
30 changes: 30 additions & 0 deletions test/oklab.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
27 changes: 27 additions & 0 deletions test/oklch.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
2 changes: 1 addition & 1 deletion tools/ranges.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ let ranges = (mode, step = 0.01) => {
return res;
};

console.log(ranges('lch65', 0.0025));
console.log(ranges('oklch', 0.0025));

0 comments on commit 955115b

Please sign in to comment.