Skip to content

Commit

Permalink
feat: implement the OKLab and the OKLch color conversion routines
Browse files Browse the repository at this point in the history
  • Loading branch information
aradzie committed Nov 29, 2024
1 parent 4b4b7bb commit c6fa117
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 20 deletions.
3 changes: 2 additions & 1 deletion packages/keybr-color/lib/color-oklab.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { clamp, isNumber, isObjectLike } from "@keybr/lang";
import { Color } from "./color.ts";
import { hslToHsv, rgbToHsl } from "./convert-rgb.ts";
import { oklabToRgb } from "./convert-xyz.ts";
import { type Oklab } from "./types.ts";
import { round } from "./util.ts";

Expand Down Expand Up @@ -95,7 +96,7 @@ export class OklabColor extends Color implements Oklab {
}

override toRgb(clone?: boolean) {
return null as any;
return oklabToRgb(this);
}

override toHsl() {
Expand Down
3 changes: 2 additions & 1 deletion packages/keybr-color/lib/color-oklch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { clamp, isNumber, isObjectLike } from "@keybr/lang";
import { Color } from "./color.ts";
import { hslToHsv, rgbToHsl } from "./convert-rgb.ts";
import { oklchToRgb } from "./convert-xyz.ts";
import { type Oklch } from "./types.ts";
import { round } from "./util.ts";

Expand Down Expand Up @@ -95,7 +96,7 @@ export class OklchColor extends Color implements Oklch {
}

override toRgb(clone?: boolean) {
return null as any;
return oklchToRgb(this);
}

override toHsl() {
Expand Down
19 changes: 1 addition & 18 deletions packages/keybr-color/lib/convert-rgb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,8 @@ import { clamp } from "@keybr/lang";
import { HslColor } from "./color-hsl.ts";
import { HsvColor } from "./color-hsv.ts";
import { HwbColor } from "./color-hwb.ts";
import { OklabColor } from "./color-oklab.ts";
import { OklchColor } from "./color-oklch.ts";
import { RgbColor } from "./color-rgb.ts";
import {
type Hsl,
type Hsv,
type Hwb,
type Oklab,
type Oklch,
type Rgb,
} from "./types.ts";
import { type Hsl, type Hsv, type Hwb, type Rgb } from "./types.ts";

export function rgbToHsl({ r, g, b, a }: Rgb): HslColor {
r = clamp(r, 0, 1);
Expand Down Expand Up @@ -183,11 +174,3 @@ export function hsvToHwb({ h, s, v, a }: Hsv): HwbColor {
const bb = 1 - v;
return new HwbColor(h, ww, bb, a);
}

export function oklabToOklch(oklab: Oklab): OklchColor {
return new OklchColor();
}

export function oklchToOklab(oklch: Oklch): OklabColor {
return new OklabColor();
}
123 changes: 123 additions & 0 deletions packages/keybr-color/lib/convert-xyz.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { test } from "node:test";
import { deepEqual, like } from "rich-assert";
import { OklchColor } from "./color-oklch.ts";
import { RgbColor } from "./color-rgb.ts";
import {
linearRgbToXyz,
oklchToRgb,
rgbGammaToLinear,
rgbLinearToGamma,
rgbToOklch,
xyzToLinearRgb,
} from "./convert-xyz.ts";

test("gamma rgb / linear rgb", () => {
const a = { r: 0, g: 0, b: 0, a: 0 };
const b = { r: 0, g: 0, b: 0, a: 0 };

a.r = 0;
a.g = 0;
a.b = 0;
a.a = 0.5;
rgbGammaToLinear(a, b);
deepEqual(b, { r: 0, g: 0, b: 0, a: 0.5 });

rgbLinearToGamma(b, a);
deepEqual(a, { r: 0, g: 0, b: 0, a: 0.5 });

a.r = 0.1;
a.g = 0.2;
a.b = 0.3;
a.a = 0.5;
rgbGammaToLinear(a, b);
deepEqual(b, { r: 0.010022825574869039, g: 0.033104766570885055, b: 0.07323895587840543, a: 0.5 });

rgbLinearToGamma(b, a);
deepEqual(a, { r: 0.1, g: 0.2, b: 0.3, a: 0.5 });

a.r = 1;
a.g = 1;
a.b = 1;
a.a = 0.5;
rgbGammaToLinear(a, b);
deepEqual(b, { r: 1, g: 1, b: 1, a: 0.5 });

rgbLinearToGamma(b, a);
deepEqual(a, { r: 0.9999999999999999, g: 0.9999999999999999, b: 0.9999999999999999, a: 0.5 });
});

test("linear rgb / xyz", () => {
const a = { r: 0, g: 0, b: 0, a: 0 };
const b = { x: 0, y: 0, z: 0, a: 0 };

a.r = 0;
a.g = 0;
a.b = 0;
a.a = 0.5;
linearRgbToXyz(a, b);
deepEqual(b, { x: 0, y: 0, z: 0, a: 0.5 });

xyzToLinearRgb(b, a);
deepEqual(a, { r: 0, g: 0, b: 0, a: 0.5 });

a.r = 0.1;
a.g = 0.2;
a.b = 0.3;
a.a = 0.5;
linearRgbToXyz(a, b);
deepEqual(b, { x: 0.16690018432392184, y: 0.18595533094892233, z: 0.31093168350538253, a: 0.5 });

xyzToLinearRgb(b, a);
deepEqual(a, { r: 0.10000000000000006, g: 0.19999999999999998, b: 0.3, a: 0.5 });

a.r = 1;
a.g = 1;
a.b = 1;
a.a = 0.5;
linearRgbToXyz(a, b);
deepEqual(b, { x: 0.9504559270516717, y: 0.9999999999999999, z: 1.0890577507598784, a: 0.5 });

xyzToLinearRgb(b, a);
deepEqual(a, { r: 1, g: 0.9999999999999998, b: 1, a: 0.5 });
});

test("rgb / oklch", () => {
like(rgbToOklch(new RgbColor(0, 0, 0, 0.5)), {
L: 0,
C: 0,
h: 0,
a: 0.5,
});
like(oklchToRgb(new OklchColor(0, 0, 0, 0.5)), {
r: 0,
g: 0,
b: 0,
a: 0.5,
});

like(rgbToOklch(new RgbColor(1, 0, 0, 0.5)), {
L: 0.6279553639214313,
C: 0.25768330380536064,
h: 0.08120522299896633,
a: 0.5,
});
like(oklchToRgb(new OklchColor(0.6279553639214313, 0.25768330380536064, 0.08120522299896633, 0.5)), {
r: 0.9999999999999997,
g: 5.028833252596066e-15,
b: 0,
a: 0.5,
});

like(rgbToOklch(new RgbColor(1, 1, 1, 0.5)), {
L: 1,
C: 4.996003610813204e-16,
h: 0.5,
a: 0.5,
});
like(oklchToRgb(new OklchColor(1, 0, 0.5, 0.5)), {
r: 1,
g: 0.9999999999999997,
b: 0.9999999999999997,
a: 0.5,
});
});
151 changes: 151 additions & 0 deletions packages/keybr-color/lib/convert-xyz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { OklabColor } from "./color-oklab.ts";
import { OklchColor } from "./color-oklch.ts";
import { RgbColor } from "./color-rgb.ts";
import { type Oklab, type Oklch, type Rgb, type Xyz } from "./types.ts";

const tmpRgb: Rgb = { r: 0, g: 0, b: 0, a: 0 };
const tmpXyz: Xyz = { x: 0, y: 0, z: 0, a: 0 };
const tmpOklab: Oklab = { L: 0, A: 0, B: 0, a: 0 };

const toLinear = (channel: number) =>
channel <= 0.04045
? channel / 12.92
: Math.pow((channel + 0.055) / 1.055, 2.4);

const toGamma = (channel: number) =>
channel > 0.0031308
? 1.055 * Math.pow(channel, 1 / 2.4) - 0.055
: 12.92 * channel;

/**
* Converts a gamma corrected sRGB color to a linear light form.
*/
export function rgbGammaToLinear({ r, g, b, a }: Rgb, to: Rgb): void {
to.r = toLinear(r);
to.g = toLinear(g);
to.b = toLinear(b);
to.a = a;
}

/**
* Converts a linear-light sRGB color to a gamma corrected form.
*/
export function rgbLinearToGamma({ r, g, b, a }: Rgb, to: Rgb): void {
to.r = toGamma(r);
to.g = toGamma(g);
to.b = toGamma(b);
to.a = a;
}

/**
* Converts a linear-light sRGB color to a CIE XYZ color using D65
* (no chromatic adaptation).
*/
export function linearRgbToXyz({ r, g, b, a }: Rgb, to: Xyz): void {
to.x =
0.4123907992659595 * r + 0.35758433938387796 * g + 0.1804807884018343 * b;
to.y =
0.21263900587151036 * r + 0.7151686787677559 * g + 0.07219231536073371 * b;
to.z =
0.01933081871559185 * r + 0.11919477979462599 * g + 0.9505321522496606 * b;
to.a = a;
}

/**
* Converts a CIE XYZ color to a linear light sRGB color using D65
* (no chromatic adaptation).
*/
export function xyzToLinearRgb({ x, y, z, a }: Xyz, to: Rgb): void {
to.r =
3.2409699419045213 * x + -1.5373831775700935 * y + -0.4986107602930033 * z;
to.g =
-0.9692436362808798 * x + 1.8759675015077206 * y + 0.04155505740717561 * z;
to.b =
0.05563007969699361 * x + -0.20397695888897657 * y + 1.0569715142428786 * z;
to.a = a;
}

export function xyzToOklab({ x, y, z, a }: Xyz, to: Oklab): void {
const l = Math.cbrt(
0.819022437996703 * x + 0.3619062600528904 * y - 0.1288737815209879 * z,
);
const m = Math.cbrt(
0.0329836539323885 * x + 0.9292868615863434 * y + 0.0361446663506424 * z,
);
const s = Math.cbrt(
0.0481771893596242 * x + 0.2642395317527308 * y + 0.6335478284694309 * z,
);
to.L =
0.210454268309314 * l + 0.7936177747023054 * m - 0.0040720430116193 * s;
to.A =
1.9779985324311684 * l - 2.4285922420485799 * m + 0.450593709617411 * s;
to.B =
0.0259040424655478 * l + 0.7827717124575296 * m - 0.8086757549230774 * s;
to.a = a;
}

export function oklabToXyz({ L, A, B, a }: Oklab, to: Xyz): void {
const l = Math.pow(L + 0.3963377773761749 * A + 0.2158037573099136 * B, 3);
const m = Math.pow(L - 0.1055613458156586 * A - 0.0638541728258133 * B, 3);
const s = Math.pow(L - 0.0894841775298119 * A - 1.2914855480194092 * B, 3);
to.x =
1.2268798758459243 * l - 0.5578149944602171 * m + 0.2813910456659647 * s;
to.y =
-0.0405757452148008 * l + 1.112286803280317 * m - 0.0717110580655164 * s;
to.z =
-0.0763729366746601 * l - 0.4214933324022432 * m + 1.5869240198367816 * s;
to.a = a;
}

export function oklabToOklch({ L, A, B, a }: Oklab, to: Oklch): void {
const C = Math.sqrt(A * A + B * B);
let h = Math.atan2(B, A) / Math.PI / 2;
if (h < 0) {
h = 1 + h;
}
to.L = L;
to.C = C;
to.h = h;
to.a = a;
}

export function oklchToOklab({ L, C, h, a }: Oklch, to: Oklab): void {
to.L = L;
to.A = C * Math.cos(h * Math.PI * 2);
to.B = C * Math.sin(h * Math.PI * 2);
to.a = a;
}

export function rgbToOklab(rgb: Rgb): OklabColor {
const to = new OklabColor();
rgbGammaToLinear(rgb, tmpRgb);
linearRgbToXyz(tmpRgb, tmpXyz);
xyzToOklab(tmpXyz, to);
return to;
}

export function rgbToOklch(rgb: Rgb): OklchColor {
const to = new OklchColor();
rgbGammaToLinear(rgb, tmpRgb);
linearRgbToXyz(tmpRgb, tmpXyz);
xyzToOklab(tmpXyz, tmpOklab);
oklabToOklch(tmpOklab, to);
return to;
}

export function oklabToRgb(oklab: Oklab): RgbColor {
const to = new RgbColor();
oklabToXyz(oklab, tmpXyz);
xyzToLinearRgb(tmpXyz, tmpRgb);
rgbLinearToGamma(tmpRgb, to);
return to;
}

export function oklchToRgb(oklch: Oklch): RgbColor {
const to = new RgbColor();
oklchToOklab(oklch, tmpOklab);
oklabToXyz(tmpOklab, tmpXyz);
xyzToLinearRgb(tmpXyz, tmpRgb);
rgbLinearToGamma(tmpRgb, to);
return to;
}
1 change: 1 addition & 0 deletions packages/keybr-color/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./color-oklab.ts";
export * from "./color-oklch.ts";
export * from "./color-rgb.ts";
export * from "./convert-rgb.ts";
export * from "./convert-xyz.ts";
export * from "./mix.ts";
export * from "./parse.ts";
export * from "./types.ts";
11 changes: 11 additions & 0 deletions packages/keybr-color/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ export type Hsv = {
a: number;
};

export type Xyz = {
/** The `x` component. */
x: number;
/** The `y` component. */
y: number;
/** The `z` component. */
z: number;
/** The alpha component, a number in range [0,1]. */
a: number;
};

export type Oklab = {
/** The `L` component, a number in range [0,1]. */
L: number;
Expand Down

0 comments on commit c6fa117

Please sign in to comment.