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

CSS4 toGamut fixes #352

Merged
merged 14 commits into from
Dec 17, 2023
Merged
41 changes: 19 additions & 22 deletions src/toGamut.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import deltaEOK from "./deltaE/deltaEOK.js";
import inGamut from "./inGamut.js";
import to from "./to.js";
import get from "./get.js";
import oklab from "./spaces/oklab.js";
import set from "./set.js";
import clone from "./clone.js";
import getColor from "./getColor.js";
Expand Down Expand Up @@ -117,8 +118,8 @@ toGamut.returns = "color";
// `Oklch` space. These are created in the `Oklab` space, as it is used by the
// DeltaEOK calculation, so it is guaranteed to be imported.
const COLORS = {
WHITE: { space: "oklab", coords: [1, 0, 0] },
BLACK: { space: "oklab", coords: [0, 0, 0] }
WHITE: { space: oklab, coords: [1, 0, 0] },
facelessuser marked this conversation as resolved.
Show resolved Hide resolved
BLACK: { space: oklab, coords: [0, 0, 0] }
};

/**
Expand All @@ -135,12 +136,13 @@ export function toGamutCSS (origin, { space = origin.space }) {
const JND = 0.02;
const ε = 0.0001;
space = ColorSpace.get(space);
const oklchSpace = ColorSpace.get("oklch");

if (space.isUnbounded) {
return to(origin, space);
}

const origin_OKLCH = to(origin, ColorSpace.get("oklch"));
const origin_OKLCH = to(origin, oklchSpace);
let L = origin_OKLCH.coords[0];

// return media white or black, if lightness is out of range
Expand All @@ -163,40 +165,36 @@ export function toGamutCSS (origin, { space = origin.space }) {
const destColor = to(_color, space);
const spaceCoords = Object.values(space.coords);
destColor.coords = destColor.coords.map((coord, index) => {
const spaceCoord = spaceCoords[index];
if (("range" in spaceCoord)) {
if (coord < spaceCoord.range[0]) {
return spaceCoord.range[0];
}
if (coord > spaceCoord.range[1]) {
return spaceCoord.range[1];
}
if ("range" in spaceCoords[index]) {
const [min, max] = spaceCoords[index].range;
return util.clamp(min, coord, max);
}
return coord;
});
return destColor;
}
let min = 0;
let max = origin_OKLCH.coords[1];

let min_inGamut = true;
let current;
let current = clone(origin_OKLCH);
let clipped = clip(current);

let E = deltaEOK(clipped, current);
if (E < JND) {
return clipped;
}
svgeesus marked this conversation as resolved.
Show resolved Hide resolved

while ((max - min) > ε) {
const chroma = (min + max) / 2;
current = clone(origin_OKLCH);
current.coords[1] = chroma;
if (min_inGamut && inGamut(current, space)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that I missed, this should really be using an epsilon of zero. The earlier shortcut out should be based on an epsilon of zero as well (line 160).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this in 907c8f3

The Gamut tests are passing on this branch- are there additional cases I should add for checking what you were looking at in #378 ?

min = chroma;
continue;
}
else if (!inGamut(current, space)) {
const clipped = clip(current);
const E = deltaEOK(clipped, current);
else {
clipped = clip(current);
E = deltaEOK(clipped, current);
if (E < JND) {
if ((JND - E < ε)) {
// match found
current = clipped;
break;
}
else {
Expand All @@ -206,9 +204,8 @@ export function toGamutCSS (origin, { space = origin.space }) {
}
else {
max = chroma;
continue;
}
}
}
return to(current, space);
return clipped;
}
11 changes: 11 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,14 @@ export function parseCoordGrammar (coordGrammars) {
});
});
}

/**
* Clamp value between the minimum and maximum
* @param {number} min minimum value to return
* @param {number} val the value to return if it is between min and max
* @param {number} max maximum value to return
* @returns number
*/
export function clamp (min, val, max){
return Math.max(Math.min(max, val), min);
}
237 changes: 162 additions & 75 deletions test/gamut.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,87 +11,174 @@ export default {
return color2;
},
map (c) {
return new Color(c).coords;
const color = new Color(c);
return this.data.checkAlpha ? [
...color.coords,
color.alpha
] : color.coords;
},
check: check.deep(check.proximity({ epsilon: 0.01 })),
check: check.deep(check.proximity({ epsilon: 0.001 })),
tests: [
{
name: "P3 primaries to sRGB",
data: {toSpace: "srgb"},
name: "P3 primaries to sRGB, CSS algorithm",
data: { toSpace: "srgb" },
tests: [
{
name: "Using chroma reduction",
tests: [
{
args: ["color(display-p3 1 0 0)"],
expect: "rgb(98.20411139286732% 21.834053137266363% 0%)"
},
{
args: ["color(display-p3 0 1 0)"],
expect: "rgb(0% 99.7921930734509% 0%)"
},
{
args: ["color(display-p3 0 0 1)"],
expect: "rgb(0% 0% 100%)"
},
{
args: ["color(display-p3 1 1 0)"],
expect: "rgb(100% 99.45446271521069% 0%)"
},
args: ["color(display-p3 1 0 0)"],
expect: "rgb(100% 4.457% 4.5932%)"
},
{
args: ["color(display-p3 0 1 0)"],
expect: "rgb(0% 98.576% 15.974%)"
},
{
args: ["color(display-p3 0 0 1)"],
expect: "rgb(0% 0% 100%)"
},
{
args: ["color(display-p3 1 1 0)"],
expect: "rgb(99.623% 99.901% 0%)"
},

{
args: ["color(display-p3 0 1 1)"],
expect: "rgb(0% 100% 98.93709142382755%)"
},
{
args: ["color(display-p3 1 0 1)"],
expect: "rgb(100% 8.637212218104592% 98.22133121285436%)"
},
]
},
{
name: "Using HSL saturation reduction",
data: {method: "hsl.s"},
tests: [
{
args: ["color(display-p3 1 0 0)"],
expect: "rgb(100% 0% 0%)"
},
{
args: ["color(display-p3 0 1 0)"],
expect: "rgb(0% 100% 0%)"
},
{
args: ["color(display-p3 0 0 1)"],
expect: "rgb(0% 0% 100%)"
},
{
args: ["color(display-p3 1 1 0)"],
expect: "rgb(100% 100% 0%)"
},
{
args: ["color(display-p3 0 1 1)"],
expect: "rgb(0% 100% 100%)"
},
{
args: ["color(display-p3 1 0 1)"],
expect: "rgb(100% 0% 100%)"
}
]
},
{
name: "Using clipping",
data: {method: "clip"},
tests: [
{
args: ["color(display-p3 1 0 0)"],
expect: "rgb(100% 0% 0%)"
},
{
args: ["color(display-p3 0 1 0)"],
expect: "rgb(0% 100% 0%)"
},
]
{
args: ["color(display-p3 0 1 1)"],
expect: "rgb(0% 99.645% 98.471%)"
},
{
args: ["color(display-p3 1 0 1)"],
expect: "rgb(100% 16.736% 98.264%)"
},
]
},
{
name: "P3 to sRGB whites/blacks CSS algorithm",
data: { toSpace: "srgb" },
tests: [
{
args: ["color(display-p3 1 1 1)"],
expect: "rgb(100% 100% 100%)"
},
{
args: ["color(display-p3 2 0 1)"],
expect: "rgb(100% 100% 100%)"
},
{
args: ["color(display-p3 0 0 0)"],
expect: "rgb(0% 0% 0%)"
},
{
args: ["color(display-p3 -1 0 0)"],
expect: "rgb(0% 0% 0%)"
}
]
},
{
name: "Maintains alpha",
data: { toSpace: "srgb", checkAlpha: true },
tests: [
{
args: ["color(display-p3 1 1 1 / 1)"],
expect: "rgb(100% 100% 100%)"
},
{
args: ["color(display-p3 1 1 1 / 0.5)"],
expect: "rgb(100% 100% 100% / 0.5)"
},
{
args: ["color(display-p3 1 1 1 / 0)"],
expect: "rgb(100% 100% 100% / 0)"
},
{
args: ["color(display-p3 1 0 0 / 1)"],
expect: "rgb(100% 4.457% 4.5932%)"
},
{
args: ["color(display-p3 1 0 0 / 0.5)"],
expect: "rgb(100% 4.457% 4.5932% / 0.5)"
},
{
args: ["color(display-p3 1 0 0 / 0)"],
expect: "rgb(100% 4.457% 4.5932% / 0)"
},
]
},
{
name: "P3 primaries to sRGB, LCH chroma Reduction",
data: { toSpace: "srgb", method: "lch.c" },
tests: [
{
args: ["color(display-p3 1 0 0)"],
expect: "rgb(98.20411139286732% 21.834053137266363% 0%)"
},
{
args: ["color(display-p3 0 1 0)"],
expect: "rgb(0% 99.7921930734509% 0%)"
},
{
args: ["color(display-p3 0 0 1)"],
expect: "rgb(0% 0% 100%)"
},
{
args: ["color(display-p3 1 1 0)"],
expect: "rgb(100% 99.45446271521069% 0%)"
},

{
args: ["color(display-p3 0 1 1)"],
expect: "rgb(0% 100% 98.93709142382755%)"
},
{
args: ["color(display-p3 1 0 1)"],
expect: "rgb(100% 8.637212218104592% 98.22133121285436%)"
}
]
},

{
name: "P3 primaries to sRGB, HSL saturation reduction",
data: { method: "hsl.s", toSpace: "sRGB" },
tests: [
{
args: ["color(display-p3 1 0 0)"],
expect: "rgb(100% 0% 0%)"
},
{
args: ["color(display-p3 0 1 0)"],
expect: "rgb(0% 75.29% 0%)"
},
{
args: ["color(display-p3 0 0 1)"],
expect: "rgb(0% 0% 100%)"
},
{
args: ["color(display-p3 1 1 0)"],
expect: "rgb(84.872% 84.872% 0%)"
},
{
args: ["color(display-p3 0 1 1)"],
expect: "rgb(0% 76.098% 75.455%))"
},
{
args: ["color(display-p3 1 0 1)"],
expect: "rgb(100% 0% 100%)"
}
]
},
{
name: "Using clipping",
data: { method: "clip", toSpace: "sRGB" },
tests: [
{
args: ["color(display-p3 1 0 0)"],
expect: "rgb(100% 0% 0%)"
},
{
args: ["color(display-p3 0 1 0)"],
expect: "rgb(0% 100% 0%)"
},
{
args: ["color(display-p3 0 0 1)"],
expect: "rgb(0% 0% 100%)"
},
]
},
Expand Down