diff --git a/src/spaces/index-fn.js b/src/spaces/index-fn.js index 68a9a7b44..7a3d3ddb7 100644 --- a/src/spaces/index-fn.js +++ b/src/spaces/index-fn.js @@ -19,6 +19,8 @@ export {default as REC_2020_Linear} from "./rec2020-linear.js"; export {default as REC_2020} from "./rec2020.js"; export {default as OKLab} from "./oklab.js"; export {default as OKLCH} from "./oklch.js"; +export {default as OKLrab} from "./oklrab.js"; +export {default as OKLrCH} from "./oklrch.js"; export {default as Okhsl} from "./okhsl.js"; export {default as Okhsv} from "./okhsv.js"; export {default as CAM16_JMh} from "./cam16.js"; diff --git a/src/spaces/oklrab.js b/src/spaces/oklrab.js new file mode 100644 index 000000000..c6f4598b0 --- /dev/null +++ b/src/spaces/oklrab.js @@ -0,0 +1,36 @@ +import ColorSpace from "../space.js"; +import OKLab from "./oklab.js"; +import {toe, toeInv} from "./okhsl.js"; + +export default new ColorSpace({ + id: "oklrab", + name: "Oklrab", + coords: { + l: { + refRange: [0, 1], + name: "Lightness", + }, + a: { + refRange: [-0.4, 0.4], + }, + b: { + refRange: [-0.4, 0.4], + }, + }, + + // Note that XYZ is relative to D65 + white: "D65", + base: OKLab, + fromBase (oklab) { + return [toe(oklab[0]), oklab[1], oklab[2]]; + }, + toBase (oklrab) { + return [toeInv(oklrab[0]), oklrab[1], oklrab[2]]; + }, + + formats: { + "color": { + coords: [" | ", " | [-1,1]", " | [-1,1]"], + }, + }, +}); diff --git a/src/spaces/oklrch.js b/src/spaces/oklrch.js new file mode 100644 index 000000000..8885d1f90 --- /dev/null +++ b/src/spaces/oklrch.js @@ -0,0 +1,70 @@ +import ColorSpace from "../space.js"; +import OKLrab from "./oklrab.js"; +import {toe, toeInv} from "./okhsl.js"; +import {constrain as constrainAngle} from "../angles.js"; +import {isNone} from "../util.js"; + +export default new ColorSpace({ + id: "oklrch", + name: "Oklrch", + coords: { + l: { + refRange: [0, 1], + name: "Lightness", + }, + c: { + refRange: [0, 0.4], + name: "Chroma", + }, + h: { + refRange: [0, 360], + type: "angle", + name: "Hue", + }, + }, + white: "D65", + + base: OKLrab, + fromBase (oklab) { + // Convert to polar form + let [L, a, b] = oklab; + let h; + const ε = 0.0002; // chromatic components much smaller than a,b + + if (Math.abs(a) < ε && Math.abs(b) < ε) { + h = NaN; + } + else { + h = Math.atan2(b, a) * 180 / Math.PI; + } + + return [ + L, // OKLab L is still L + Math.sqrt(a ** 2 + b ** 2), // Chroma + constrainAngle(h), // Hue, in degrees [0 to 360) + ]; + }, + // Convert from polar form + toBase (oklch) { + let [L, C, h] = oklch; + let a, b; + + // check for NaN hue + if (isNone(h)) { + a = 0; + b = 0; + } + else { + a = C * Math.cos(h * Math.PI / 180); + b = C * Math.sin(h * Math.PI / 180); + } + + return [ L, a, b ]; + }, + + formats: { + "color": { + coords: [" | ", " | [0,1]", " | "], + }, + }, +}); diff --git a/test/conversions.js b/test/conversions.js index e26cba81f..ba2584f44 100644 --- a/test/conversions.js +++ b/test/conversions.js @@ -434,22 +434,78 @@ const tests = { expect: [1.0, 0.0, NaN], }, { - name: "sRGB red (D65) to OKlab", + name: "sRGB red (D65) to OKlch", args: "red", expect: [0.6279553639214311, 0.2576833038053608, 29.23388027962784], }, { - name: "sRGB lime (D65) to OKlab", + name: "sRGB lime (D65) to OKlch", args: "lime", expect: [0.8664396175234368, 0.2948272245426958, 142.4953450414439], }, { - name: "sRGB blue (D65) to OKlab", + name: "sRGB blue (D65) to OKlch", args: "blue", expect: [0.45201371817442365, 0.3132143886344849, 264.0520226163699], }, ], }, + { + name: "OKLrab", + data: { + toSpace: "oklrab", + }, + tests: [ + { + name: "sRGB white (D65) to OKlrab", + args: "white", + expect: [ 1.0000000000000002, -4.996003610813204e-16, 0 ], + }, + { + name: "sRGB red (D65) to OKlrab", + args: "red", + expect: [ 0.5680846563197034, 0.2248630684262744, 0.125846277330585 ], + }, + { + name: "sRGB lime (D65) to OKlrab", + args: "lime", + expect: [ 0.8445289714936317, -0.23388758093655815, 0.1794984451609376 ], + }, + { + name: "sRGB blue (D65) to OKlrab", + args: "blue", + expect: [ 0.3665653391870817, -0.03245697517079771, -0.3115281656775778 ], + }, + ], + }, + { + name: "OKLrCh", + data: { + toSpace: "oklrch", + }, + tests: [ + { + name: "sRGB white (D65) to OKlrch", + args: "white", + expect: [1.0, 0.0, NaN], + }, + { + name: "sRGB red (D65) to OKlrch", + args: "red", + expect: [0.5680846563197034, 0.2576833038053608, 29.23388027962784], + }, + { + name: "sRGB lime (D65) to OKlrch", + args: "lime", + expect: [0.8445289714936317, 0.2948272245426958, 142.4953450414439], + }, + { + name: "sRGB blue (D65) to OKlrch", + args: "blue", + expect: [0.3665653391870817, 0.3132143886344849, 264.0520226163699], + }, + ], + }, { name: "Linear-light sRGB", data: { diff --git a/types/src/space-coord-accessors.d.ts b/types/src/space-coord-accessors.d.ts index 87e633337..7339767b4 100644 --- a/types/src/space-coord-accessors.d.ts +++ b/types/src/space-coord-accessors.d.ts @@ -27,6 +27,8 @@ declare class SpaceAccessors { okhsv: SpaceAccessor; oklab: SpaceAccessor; oklch: SpaceAccessor; + oklrab: SpaceAccessor; + oklrch: SpaceAccessor; p3: SpaceAccessor; p3_linear: SpaceAccessor; prophoto: SpaceAccessor; diff --git a/types/src/spaces/oklrab.d.ts b/types/src/spaces/oklrab.d.ts new file mode 100644 index 000000000..47ffb5172 --- /dev/null +++ b/types/src/spaces/oklrab.d.ts @@ -0,0 +1,3 @@ +import ColorSpace from "../space.js"; +declare const _default: ColorSpace; +export default _default; diff --git a/types/src/spaces/oklrch.d.ts b/types/src/spaces/oklrch.d.ts new file mode 100644 index 000000000..47ffb5172 --- /dev/null +++ b/types/src/spaces/oklrch.d.ts @@ -0,0 +1,3 @@ +import ColorSpace from "../space.js"; +declare const _default: ColorSpace; +export default _default;