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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 21 additions & 24 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] },
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 @@ -155,48 +157,44 @@ export function toGamutCSS (origin, { space = origin.space }) {
return to(black, space);
}

if (inGamut(origin_OKLCH, space)) {
if (inGamut(origin_OKLCH, space, {epsilon: 0})) {
return to(origin_OKLCH, space);
}

function clip (_color) {
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;
}

while ((max - min) > ε) {
const chroma = (min + max) / 2;
current = clone(origin_OKLCH);
current.coords[1] = chroma;
if (min_inGamut && inGamut(current, space)) {
if (min_inGamut && inGamut(current, space, {epsilon: 0})) {
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