diff --git a/packages/keybr-color/lib/color-oklab.ts b/packages/keybr-color/lib/color-oklab.ts index 32930f2f..1f46287f 100644 --- a/packages/keybr-color/lib/color-oklab.ts +++ b/packages/keybr-color/lib/color-oklab.ts @@ -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"; @@ -95,7 +96,7 @@ export class OklabColor extends Color implements Oklab { } override toRgb(clone?: boolean) { - return null as any; + return oklabToRgb(this); } override toHsl() { diff --git a/packages/keybr-color/lib/color-oklch.ts b/packages/keybr-color/lib/color-oklch.ts index ebbd8b4d..cda36381 100644 --- a/packages/keybr-color/lib/color-oklch.ts +++ b/packages/keybr-color/lib/color-oklch.ts @@ -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"; @@ -95,7 +96,7 @@ export class OklchColor extends Color implements Oklch { } override toRgb(clone?: boolean) { - return null as any; + return oklchToRgb(this); } override toHsl() { diff --git a/packages/keybr-color/lib/convert-rgb.ts b/packages/keybr-color/lib/convert-rgb.ts index e0c54395..e250331b 100644 --- a/packages/keybr-color/lib/convert-rgb.ts +++ b/packages/keybr-color/lib/convert-rgb.ts @@ -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); @@ -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(); -} diff --git a/packages/keybr-color/lib/convert-xyz.test.ts b/packages/keybr-color/lib/convert-xyz.test.ts new file mode 100644 index 00000000..fbca9adb --- /dev/null +++ b/packages/keybr-color/lib/convert-xyz.test.ts @@ -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, + }); +}); diff --git a/packages/keybr-color/lib/convert-xyz.ts b/packages/keybr-color/lib/convert-xyz.ts new file mode 100644 index 00000000..e0f4754f --- /dev/null +++ b/packages/keybr-color/lib/convert-xyz.ts @@ -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; +} diff --git a/packages/keybr-color/lib/index.ts b/packages/keybr-color/lib/index.ts index 66293439..7a423140 100644 --- a/packages/keybr-color/lib/index.ts +++ b/packages/keybr-color/lib/index.ts @@ -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"; diff --git a/packages/keybr-color/lib/types.ts b/packages/keybr-color/lib/types.ts index 1fbb4bfc..e95b336d 100644 --- a/packages/keybr-color/lib/types.ts +++ b/packages/keybr-color/lib/types.ts @@ -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;