Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add color spaces for Luv and LCHuv #391

Merged
merged 1 commit into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions get/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@
"id": "acescg",
"url": "https://en.wikipedia.org/wiki/Academy_Color_Encoding_System",
"description": "Scene-referred Academy Color Encoding System, using the wide gamut but physically realizable AP1 primaries and linear-light encoding. Used for physical rendering."
},
{
"name": "Luv",
"id": "luv",
"url": "https://en.wikipedia.org/wiki/CIELUV",
"description": "CIELUV is a color space developed in 1976 for perceptually uniform color representation, ideal for practical applications like digital displays. It's derived from the CIE 1931 XYZ space and includes L* for luminance, and u*, v* for chromaticity, capturing green–red and blue–yellow dimensions."
},
{
"name": "LCHuv",
"id": "lchuv",
"dependencies": ["luv"],
"url": "https://en.wikipedia.org/wiki/CIELUV",
"description": "The polar (Hue, Chroma) form of CIE Luv."
}
],
"optional": [
Expand Down
2 changes: 2 additions & 0 deletions src/spaces/index-fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ export {default as OKLab} from "./oklab.js";
export {default as OKLCH} from "./oklch.js";
export {default as CAM16_JMh} from "./cam16.js";
export {default as HCT} from "./hct.js";
export {default as Luv} from "./luv.js";
export {default as LCHuv} from "./lchuv.js";

export * from "./index-fn-hdr.js";
68 changes: 68 additions & 0 deletions src/spaces/lchuv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import ColorSpace from "../space.js";
import Luv from "./luv.js";
import {constrain as constrainAngle} from "../angles.js";

export default new ColorSpace({
id: "lchuv",
name: "LChuv",
coords: {
l: {
refRange: [0, 100],
name: "Lightness"
},
c: {
refRange: [0, 220],
name: "Chroma"
},
h: {
refRange: [0, 360],
type: "angle",
name: "Hue"
}
},

base: Luv,
fromBase (Luv) {
// Convert to polar form
let [L, u, v] = Luv;
let hue;
const ε = 0.02;

if (Math.abs(u) < ε && Math.abs(v) < ε) {
hue = NaN;
}
else {
hue = Math.atan2(v, u) * 180 / Math.PI;
}

return [
L, // L is still L
Math.sqrt(u ** 2 + v ** 2), // Chroma
constrainAngle(hue) // Hue, in degrees [0 to 360)
];
},
toBase (LCH) {
// Convert from polar form
let [Lightness, Chroma, Hue] = LCH;
// Clamp any negative Chroma
if (Chroma < 0) {
Chroma = 0;
}
// Deal with NaN Hue
if (isNaN(Hue)) {
Hue = 0;
}
return [
Lightness, // L is still L
Chroma * Math.cos(Hue * Math.PI / 180), // u
Chroma * Math.sin(Hue * Math.PI / 180) // v
];
},

formats: {
color: {
id: "--lchuv",
coords: ["<number> | <percentage>", "<number> | <percentage>", "<number> | <angle>"],
}
}
});
85 changes: 85 additions & 0 deletions src/spaces/luv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import ColorSpace from "../space.js";
import {WHITES} from "../adapt.js";
import xyz_d65 from "./xyz-d65.js";
import {uv} from "../chromaticity.js";
import {isNone, skipNone} from "../util.js";

let white = WHITES.D65;

const ε = 216 / 24389; // 6^3/29^3 == (24/116)^3
const κ = 24389 / 27; // 29^3/3^3
const [U_PRIME_WHITE, V_PRIME_WHITE] = uv({space: xyz_d65, coords: white});

export default new ColorSpace({
id: "luv",
name: "Luv",
coords: {
l: {
refRange: [0, 100],
name: "L"
jgerigmeyer marked this conversation as resolved.
Show resolved Hide resolved
},
// Reference ranges from https://facelessuser.github.io/coloraide/colors/luv/
u: {
refRange: [-215, 215]
},
v: {
refRange: [-215, 215]
}
},

white: white,
base: xyz_d65,

// Convert D65-adapted XYZ to Luv
// https://en.wikipedia.org/wiki/CIELUV#The_forward_transformation
fromBase (XYZ) {
let xyz = [skipNone(XYZ[0]), skipNone(XYZ[1]), skipNone(XYZ[2])];
let y = xyz[1];

let [up, vp] = uv({space: xyz_d65, coords: xyz});

// Protect against XYZ of [0, 0, 0]
if (!Number.isFinite(up) || !Number.isFinite(vp)) {
return [0, 0, 0];
}

let L = y <= ε ? κ * y : 116 * Math.cbrt(y) - 16;
return [
L,
13 * L * (up - U_PRIME_WHITE),
13 * L * (vp - V_PRIME_WHITE)
];
},

// Convert Luv to D65-adapted XYZ
// https://en.wikipedia.org/wiki/CIELUV#The_reverse_transformation
toBase (Luv) {
let [L, u, v] = Luv;

// Protect against division by zero and NaN Lightness
if (L === 0 || isNone(L)) {
return [0, 0, 0];
}

u = skipNone(u);
v = skipNone(v);

let up = (u / (13 * L)) + U_PRIME_WHITE;
let vp = (v / (13 * L)) + V_PRIME_WHITE;

let y = L <= 8 ? L / κ : Math.pow((L + 16) / 116, 3);

return [
y * ((9 * up) / (4 * vp)),
y,
y * ((12 - 3 * up - 20 * vp) / (4 * vp))
];
},

formats: {
color: {
id: "--luv",
coords: ["<number> | <percentage>", "<number> | <percentage>[-1,1]", "<number> | <percentage>[-1,1]"]
}
},
});
161 changes: 161 additions & 0 deletions test/conversions.js
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,167 @@ const tests = {
}
]
},
{
name: "Luv",
data: {
toSpace: "luv",
},
tests: [
{
name: "sRGB white to Luv",
args: "white",
expect: [100.0, 0, 0]
},
{
name: "sRGB red to Luv",
args: "red",
expect: [53.23711559542937, 175.0098221628849, 37.76509362555986]
},
{
name: "sRGB lime to Luv",
args: "lime",
expect: [87.73551910966002, -83.06711971440055, 107.41811123934258]
},
{
name: "sRGB blue to Luv",
args: "blue",
expect: [32.30087290398018, -9.402407214824064, -130.35108850356178]
},
{
name: "sRGB cyan to Luv",
args: "cyan",
expect: [91.11475231670536, -70.4643799638778, -15.205397466926968]
},
{
name: "sRGB magenta to Luv",
args: "magenta",
expect: [60.322731354551394, 84.05560198975205, -108.69636549176991]
},
{
name: "sRGB yellow to Luv",
args: "yellow",
expect: [97.13855934179699, 7.7042191772699375, 106.80811125089548]
},
{
name: "sRGB black to Luv",
args: "black",
expect: [0.0, 0.0, 0.0]
},
{
name: "XYZ (none x) to Luv",
args: "color(xyz-d65 none 0.4 0.5)",
expect: [69.46953076845696, -178.66105053418175, 10.54825812268007]
},
{
name: "XYZ (none y) to Luv",
args: "color(xyz-d65 0.3 none 0.5)",
expect: [0.0, 0.0, 0.0]
},
{
name: "XYZ (none z) to Luv",
args: "color(xyz-d65 0.3 0.4 none)",
expect: [69.46953076845696, -6.641260059907392, 93.1177575503318]
},
{
name: "LChuv (sRGB white) to Luv",
args: "color(--lchuv 100.0 0 0)",
expect: [100.0, 0.0, 0.0]
},
{
name: "LChuv (sRGB red) to Luv",
args: "color(--lchuv 53.23711559542937 179.038096923620287 12.1770506300617765)",
expect: [53.23711559542937, 175.0098221628849, 37.76509362555986]
},
]
},
{
name: "Luv to sRGB",
data: {
toSpace: "srgb",
},
tests: [
{
name: "Luv (sRGB white) to sRGB",
args: "color(--luv 100 0 0)",
expect: [1.0, 1.0, 1.0]
},
{
name: "Luv (sRGB red) to sRGB",
args: "color(--luv 53.23711559542937 175.0098221628849 37.76509362555986)",
expect: [1.0, 0.0, 0.0]
},
{
name: "Luv (sRGB lime) to sRGB",
args: "color(--luv 87.73551910966002 -83.06711971440055 107.41811123934258)",
expect: [0.0, 1.0, 0.0]
},
{
name: "Luv (sRGB blue) to sRGB",
args: "color(--luv 32.30087290398018 -9.402407214824064 -130.35108850356178)",
expect: [0.0, 0.0, 1.0]
},
{
name: "Luv (sRGB cyan) to sRGB",
args: "color(--luv 91.11475231670536 -70.4643799638778 -15.205397466926968)",
expect: [0.0, 1.0, 1.0]
},
{
name: "Luv (sRGB magenta) to sRGB",
args: "color(--luv 60.322731354551394 84.05560198975205 -108.69636549176991)",
expect: [1.0, 0.0, 1.0]
},
{
name: "Luv (sRGB yellow) to sRGB",
args: "color(--luv 97.13855934179699 7.7042191772699375 106.80811125089548)",
expect: [1.0, 1.0, 0.0]
},
{
name: "Luv (sRGB black) to sRGB",
args: "color(--luv 0 0 0)",
expect: [0.0, 0.0, 0.0]
},
{
name:"Luv (none lightness) to sRGB",
args: "color(--luv none 50 50)",
expect: [0.0, 0.0, 0.0]
},
{
name:"Luv (none u) to sRGB",
args: "color(--luv 100% none 0)",
expect: [1.0, 1.0, 1.0]
},
{
name:"Luv (none v) to sRGB",
args: "color(--luv 100% 0 none)",
expect: [1.0, 1.0, 1.0]
}

]
},
{
name: "sRGB to LCHuv",
data: {
toSpace: "lchuv"
},
tests: [
{
args: "#771199",
expect: [30.933250438121703, 76.27303932913182, 290.5839513811392]
},
{
args: "#ffee77",
expect: [93.33835580058862, 77.48166024357033, 77.51954539527138]
},
{
args: "white",
expect: [100, 0, NaN]
},
{
args: "black",
expect: [0, 0, NaN]
}
]
},
{
name: "Get coordinates",
data: {
Expand Down
3 changes: 3 additions & 0 deletions types/src/space-coord-accessors.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ declare class SpaceAccessors {
lab: SpaceAccessor;
lab_d65: SpaceAccessor;
lch: SpaceAccessor;
lchuv: SpaceAccessor;
luv: SpaceAccessor;
oklab: SpaceAccessor;
oklch: SpaceAccessor;
p3: SpaceAccessor;
Expand Down Expand Up @@ -54,6 +56,7 @@ declare class SpaceAccessors {
r: number;
s: number;
t: number;
u: number;
v: number;
w: number;
x: number;
Expand Down
2 changes: 2 additions & 0 deletions types/src/spaces/index-fn.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ 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 Luv } from "./luv.js";
export { default as LCHuv } from "./lchuv.js";

export * from "./index-fn-hdr.js";
3 changes: 3 additions & 0 deletions types/src/spaces/lchuv.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ColorSpace from "../space.js";
declare const _default: ColorSpace;
export default _default;
3 changes: 3 additions & 0 deletions types/src/spaces/luv.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ColorSpace from "../space.js";
declare const _default: ColorSpace;
export default _default;