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));