From 57a5bad8cff99636b28f9d17124c7c445f36eebb Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Sat, 23 Jan 2021 01:14:08 +0000 Subject: [PATCH] feat(color): add Oklab color space support --- packages/color/README.md | 3 +- packages/color/src/api.ts | 9 ++--- packages/color/src/convert.ts | 59 +++++++++++++++++++++++++++++--- packages/color/src/index.ts | 5 +++ packages/color/src/lab-lch.ts | 21 ++++++++++++ packages/color/src/oklab-css.ts | 5 +++ packages/color/src/oklab-rgba.ts | 29 ++++++++++++++++ packages/color/src/oklab.ts | 43 +++++++++++++++++++++++ packages/color/src/rgba-oklab.ts | 34 ++++++++++++++++++ packages/color/tools/oklab.ts | 19 ++++++++++ packages/color/tpl.readme.md | 1 + 11 files changed, 219 insertions(+), 9 deletions(-) create mode 100644 packages/color/src/lab-lch.ts create mode 100644 packages/color/src/oklab-css.ts create mode 100644 packages/color/src/oklab-rgba.ts create mode 100644 packages/color/src/oklab.ts create mode 100644 packages/color/src/rgba-oklab.ts create mode 100644 packages/color/tools/oklab.ts diff --git a/packages/color/README.md b/packages/color/README.md index 321df95047..c20c75eb23 100644 --- a/packages/color/README.md +++ b/packages/color/README.md @@ -43,6 +43,7 @@ Fast color space conversions (any direction) between: - HSLA (float4) - HSVA (float4) - Int32 (uint32, `0xaarrggbb`) +- [Oklab](https://bottosson.github.io/posts/oklab/) (float4) - RGBA (float4) - XYZA (float4, aka CIE 1931) - YCbCr (float4) @@ -337,7 +338,7 @@ yarn add @thi.ng/color ``` -Package sizes (gzipped, pre-treeshake): ESM: 8.42 KB / CJS: 8.85 KB / UMD: 8.31 KB +Package sizes (gzipped, pre-treeshake): ESM: 8.94 KB / CJS: 9.39 KB / UMD: 8.81 KB ## Dependencies diff --git a/packages/color/src/api.ts b/packages/color/src/api.ts index 360eaef342..4ea70c1050 100644 --- a/packages/color/src/api.ts +++ b/packages/color/src/api.ts @@ -3,13 +3,14 @@ import type { IRandom } from "@thi.ng/random"; import type { ReadonlyVec, Vec } from "@thi.ng/vectors"; export type ColorMode = - | "rgb" + | "css" | "hcy" - | "hsv" - | "hsl" | "hsi" + | "hsl" + | "hsv" | "int" - | "css" + | "oklab" + | "rgb" | "xyz" | "ycbcr"; diff --git a/packages/color/src/convert.ts b/packages/color/src/convert.ts index 9abd2648d7..78e4df283e 100644 --- a/packages/color/src/convert.ts +++ b/packages/color/src/convert.ts @@ -18,6 +18,7 @@ import { hsvaHsla } from "./hsva-hsla"; import { hsvaRgba } from "./hsva-rgba"; import { int32Css } from "./int-css"; import { int32Rgba } from "./int-rgba"; +import { oklabRgba } from "./oklab-rgba"; import { parseCss } from "./parse-css"; import { rgbaCss } from "./rgba-css"; import { rgbaHcya } from "./rgba-hcya"; @@ -25,6 +26,7 @@ import { rgbaHsia } from "./rgba-hsia"; import { rgbaHsla } from "./rgba-hsla"; import { rgbaHsva } from "./rgba-hsva"; import { rgbaInt } from "./rgba-int"; +import { rgbaOklab } from "./rgba-oklab"; import { rgbaXyza } from "./rgba-xyza"; import { rgbaYcbcra } from "./rgba-ycbcra"; import { xyzaRgba } from "./xyza-rgba"; @@ -102,6 +104,15 @@ export function asHSVA(col: any, mode?: ColorMode) { return convert(col, "hsv", mode); } +export function asOklab(col: IColor): Color; +export function asOklab( + col: string | number | ReadonlyColor, + mode: ColorMode +): Color; +export function asOklab(col: any, mode?: ColorMode) { + return convert(col, "oklab", mode); +} + export function asXYZA(col: IColor): Color; export function asXYZA( col: string | number | ReadonlyColor, @@ -152,6 +163,7 @@ defConversion("rgb", "css", (x: any) => parseCss(x)); "hsl", "hsv", "int", + "oklab", "xyz", "ycbcr", ]).forEach((id) => @@ -160,13 +172,33 @@ defConversion("rgb", "css", (x: any) => parseCss(x)); // Int -defConversions("int", int32Rgba, "hcy", "hsi", "hsl", "hsv", "xyz", "ycbcr"); +defConversions( + "int", + int32Rgba, + "hcy", + "hsi", + "hsl", + "hsv", + "oklab", + "xyz", + "ycbcr" +); defConversion("css", "int", (x: any) => int32Css(x)); // HCYA -defConversions("hcy", hcyaRgba, "css", "int", "hsl", "hsv", "xyz", "ycbcr"); +defConversions( + "hcy", + hcyaRgba, + "css", + "int", + "hsl", + "hsv", + "oklab", + "xyz", + "ycbcr" +); // HSIA @@ -178,13 +210,14 @@ defConversions( "hcy", "hsl", "hsv", + "oklab", "xyz", "ycbcr" ); // HSLA -defConversions("hsl", hslaRgba, "hcy", "hsi", "int", "xyz", "ycbcr"); +defConversions("hsl", hslaRgba, "hcy", "hsi", "int", "oklab", "xyz", "ycbcr"); defConversion("css", "hsl", (x: any) => hslaCss(x)); @@ -192,12 +225,27 @@ defConversion("hsv", "hsl", (x: any) => hslaHsva([], x)); // HSVA -defConversions("hsv", hsvaRgba, "hcy", "hsi", "int", "xyz", "ycbcr"); +defConversions("hsv", hsvaRgba, "hcy", "hsi", "int", "oklab", "xyz", "ycbcr"); defConversion("css", "hsv", (x: any) => hsvaCss(x)); defConversion("hsl", "hsv", (x: any) => hsvaHsla([], x)); +// Oklab + +defConversions( + "oklab", + oklabRgba, + "css", + "hcy", + "hsi", + "hsl", + "hsv", + "int", + "xyz", + "ycbcr" +); + // RGBA (<[ColorMode, ColorConversion][]>[ @@ -205,6 +253,7 @@ defConversion("hsl", "hsv", (x: any) => hsvaHsla([], x)); ["hsi", rgbaHsia], ["hsl", rgbaHsla], ["hsv", rgbaHsva], + ["oklab", rgbaOklab], ["xyz", rgbaXyza], ["ycbcr", rgbaYcbcra], ]).forEach(([id, fn]) => defConversion(id, "rgb", (x: any) => fn([], x))); @@ -224,6 +273,7 @@ defConversions( "hsl", "hsv", "int", + "oklab", "ycbcr" ); @@ -238,5 +288,6 @@ defConversions( "hsl", "hsv", "int", + "oklab", "xyz" ); diff --git a/packages/color/src/index.ts b/packages/color/src/index.ts index f08948cf1c..24cefa451b 100644 --- a/packages/color/src/index.ts +++ b/packages/color/src/index.ts @@ -14,6 +14,9 @@ export * from "./hue-rgba"; export * from "./int-css"; export * from "./int-rgba"; export * from "./kelvin-rgba"; +export * from "./oklab"; +export * from "./oklab-css"; +export * from "./oklab-rgba"; export * from "./resolve"; export * from "./rgba-css"; export * from "./rgba-hcva"; @@ -22,6 +25,7 @@ export * from "./rgba-hsia"; export * from "./rgba-hsla"; export * from "./rgba-hsva"; export * from "./rgba-int"; +export * from "./rgba-oklab"; export * from "./rgba-xyza"; export * from "./rgba-ycbcra"; export * from "./srgba"; @@ -50,6 +54,7 @@ export * from "./color-range"; export * from "./cosine-gradients"; export * from "./distance"; export * from "./invert"; +export * from "./lab-lch"; export * from "./luminance"; export * from "./luminance-rgb"; export * from "./mix"; diff --git a/packages/color/src/lab-lch.ts b/packages/color/src/lab-lch.ts new file mode 100644 index 0000000000..5af5a8e71c --- /dev/null +++ b/packages/color/src/lab-lch.ts @@ -0,0 +1,21 @@ +import { setC4 } from "@thi.ng/vectors"; +import { cossin } from "@thi.ng/math"; +import type { ColorOp } from "./api"; +import { ensureAlpha } from "./internal/ensure-alpha"; + +export const labLch: ColorOp = (out, src) => + setC4( + out || src, + src[0], + Math.hypot(src[1], src[2]), + Math.atan2(src[2], src[1]), + ensureAlpha(src[3]) + ); + +export const lchLab: ColorOp = (out, src) => + setC4( + out || src, + src[0], + ...(<[number, number]>cossin(src[2], src[1])), + ensureAlpha(src[3]) + ); diff --git a/packages/color/src/oklab-css.ts b/packages/color/src/oklab-css.ts new file mode 100644 index 0000000000..ab26664fd6 --- /dev/null +++ b/packages/color/src/oklab-css.ts @@ -0,0 +1,5 @@ +import type { ReadonlyColor } from "./api"; +import { oklabRgba } from "./oklab-rgba"; +import { rgbaCss } from "./rgba-css"; + +export const oklabCss = (src: ReadonlyColor) => rgbaCss(oklabRgba([], src)); diff --git a/packages/color/src/oklab-rgba.ts b/packages/color/src/oklab-rgba.ts new file mode 100644 index 0000000000..8bdad8ff8f --- /dev/null +++ b/packages/color/src/oklab-rgba.ts @@ -0,0 +1,29 @@ +import type { ColorOp } from "./api"; +import { mulV33 } from "./internal/matrix-ops"; + +const M = [ + 4.0767245293, + -1.2681437731, + -0.0041119885, + -3.3072168827, + 2.6093323231, + -0.7034763098, + 0.2307590544, + -0.341134429, + 1.7068625689, +]; + +/** + * @remarks + * Reference: https://bottosson.github.io/posts/oklab/ + * + * @param out + * @param src + */ +export const oklabRgba: ColorOp = (out, [l, a, b, alpha]) => + mulV33(out, M, [ + (l + 0.3963377774 * a + 0.2158037573 * b) ** 3, + (l - 0.1055613458 * a - 0.0638541728 * b) ** 3, + (l - 0.0894841775 * a - 1.291485548 * b) ** 3, + alpha, + ]); diff --git a/packages/color/src/oklab.ts b/packages/color/src/oklab.ts new file mode 100644 index 0000000000..d52671ed5d --- /dev/null +++ b/packages/color/src/oklab.ts @@ -0,0 +1,43 @@ +import { declareIndices, IVector } from "@thi.ng/vectors"; +import type { Color, ColorMode } from "./api"; +import { AColor } from "./internal/acolor"; +import { ensureArgs } from "./internal/ensure-args"; + +export function oklab(col: Color, offset?: number, stride?: number): Oklab; +export function oklab( + l?: number, + a?: number, + b?: number, + alpha?: number +): Oklab; +export function oklab(...args: any[]) { + return new Oklab(...ensureArgs(args)); +} +/** + * @remarks + * Reference: https://bottosson.github.io/posts/oklab/ + */ +export class Oklab extends AColor implements IVector { + l!: number; + a!: number; + b!: number; + alpha!: number; + + get mode() { + return "oklab"; + } + + copy() { + return new Oklab(this.deref()); + } + + copyView() { + return new Oklab(this.buf, this.offset, this.stride); + } + + empty() { + return new Oklab(); + } +} + +declareIndices(Oklab.prototype, ["l", "a", "b", "alpha"]); diff --git a/packages/color/src/rgba-oklab.ts b/packages/color/src/rgba-oklab.ts new file mode 100644 index 0000000000..50510a5c93 --- /dev/null +++ b/packages/color/src/rgba-oklab.ts @@ -0,0 +1,34 @@ +import type { ColorOp } from "./api"; +import { mulV33 } from "./internal/matrix-ops"; + +const M = [ + 0.2104542553, + 1.9779984951, + 0.0259040371, + 0.793617785, + -2.428592205, + 0.7827717662, + -0.0040720468, + 0.4505937099, + -0.808675766, +]; + +/** + * @remarks + * Reference: https://bottosson.github.io/posts/oklab/ + * + * @param out + * @param src + */ +export const rgbaOklab: ColorOp = (out, [r, g, b, alpha]) => + mulV33( + out, + M, + [ + Math.cbrt(0.412165612 * r + 0.536275208 * g + 0.0514575653 * b), + Math.cbrt(0.211859107 * r + 0.6807189584 * g + 0.107406579 * b), + Math.cbrt(0.0883097947 * r + 0.2818474174 * g + 0.6302613616 * b), + alpha, + ], + false + ); diff --git a/packages/color/tools/oklab.ts b/packages/color/tools/oklab.ts new file mode 100644 index 0000000000..53d6bc8953 --- /dev/null +++ b/packages/color/tools/oklab.ts @@ -0,0 +1,19 @@ +import { serialize } from "@thi.ng/hiccup"; +import { svg } from "@thi.ng/hiccup-svg"; +import { TAU } from "@thi.ng/math"; +import { map, normRange, push, transduce } from "@thi.ng/transducers"; +import { writeFileSync } from "fs"; +import { lchLab, oklab, Oklab, swatchesH } from "../src"; + +const cols = transduce( + map((t) => oklab(lchLab(null, [0.9, 0.12, t * TAU]))), + push(), + normRange(100, false) +); + +writeFileSync( + "export/oklab.svg", + serialize( + svg({ width: 500, height: 50, convert: true }, swatchesH(cols, 5, 50)) + ) +); diff --git a/packages/color/tpl.readme.md b/packages/color/tpl.readme.md index b1f9e5e260..d5499a30ba 100644 --- a/packages/color/tpl.readme.md +++ b/packages/color/tpl.readme.md @@ -25,6 +25,7 @@ Fast color space conversions (any direction) between: - HSLA (float4) - HSVA (float4) - Int32 (uint32, `0xaarrggbb`) +- [Oklab](https://bottosson.github.io/posts/oklab/) (float4) - RGBA (float4) - XYZA (float4, aka CIE 1931) - YCbCr (float4)