Skip to content

Commit

Permalink
feat(color): add Oklab color space support
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Jan 23, 2021
1 parent 8280d98 commit 57a5bad
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 9 deletions.
3 changes: 2 additions & 1 deletion packages/color/README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -337,7 +338,7 @@ yarn add @thi.ng/color
<script src="https://unpkg.com/@thi.ng/color/lib/index.umd.js" crossorigin></script>
```

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

Expand Down
9 changes: 5 additions & 4 deletions packages/color/src/api.ts
Expand Up @@ -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";

Expand Down
59 changes: 55 additions & 4 deletions packages/color/src/convert.ts
Expand Up @@ -18,13 +18,15 @@ 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";
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";
Expand Down Expand Up @@ -102,6 +104,15 @@ export function asHSVA(col: any, mode?: ColorMode) {
return <Color>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 <Color>convert(col, "oklab", mode);
}

export function asXYZA(col: IColor): Color;
export function asXYZA(
col: string | number | ReadonlyColor,
Expand Down Expand Up @@ -152,6 +163,7 @@ defConversion("rgb", "css", (x: any) => parseCss(x));
"hsl",
"hsv",
"int",
"oklab",
"xyz",
"ycbcr",
]).forEach((id) =>
Expand All @@ -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

Expand All @@ -178,33 +210,50 @@ 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));

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<ReadonlyColor>][]>[
["hcy", rgbaHcya],
["hsi", rgbaHsia],
["hsl", rgbaHsla],
["hsv", rgbaHsva],
["oklab", rgbaOklab],
["xyz", rgbaXyza],
["ycbcr", rgbaYcbcra],
]).forEach(([id, fn]) => defConversion(id, "rgb", (x: any) => fn([], x)));
Expand All @@ -224,6 +273,7 @@ defConversions(
"hsl",
"hsv",
"int",
"oklab",
"ycbcr"
);

Expand All @@ -238,5 +288,6 @@ defConversions(
"hsl",
"hsv",
"int",
"oklab",
"xyz"
);
5 changes: 5 additions & 0 deletions packages/color/src/index.ts
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
21 changes: 21 additions & 0 deletions 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])
);
5 changes: 5 additions & 0 deletions 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));
29 changes: 29 additions & 0 deletions 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,
]);
43 changes: 43 additions & 0 deletions 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<Oklab> implements IVector<Oklab> {
l!: number;
a!: number;
b!: number;
alpha!: number;

get mode() {
return <ColorMode>"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"]);
34 changes: 34 additions & 0 deletions 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
);
19 changes: 19 additions & 0 deletions 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<Oklab>(),
normRange(100, false)
);

writeFileSync(
"export/oklab.svg",
serialize(
svg({ width: 500, height: 50, convert: true }, swatchesH(cols, 5, 50))
)
);
1 change: 1 addition & 0 deletions packages/color/tpl.readme.md
Expand Up @@ -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)
Expand Down

0 comments on commit 57a5bad

Please sign in to comment.