-
Notifications
You must be signed in to change notification settings - Fork 82
/
toGamut.js
106 lines (88 loc) · 3.04 KB
/
toGamut.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import * as util from "./util.js";
import ColorSpace from "./space.js";
import defaults from "./defaults.js";
import deltaE2000 from "./deltaE/deltaE2000.js";
import inGamut from "./inGamut.js";
import to from "./to.js";
import get from "./get.js";
import set from "./set.js";
import clone from "./clone.js";
import getColor from "./getColor.js";
/**
* Force coordinates to be in gamut of a certain color space.
* Mutates the color it is passed.
* @param {Object} options
* @param {string} options.method - How to force into gamut.
* If "clip", coordinates are just clipped to their reference range.
* If in the form [colorSpaceId].[coordName], that coordinate is reduced
* until the color is in gamut. Please note that this may produce nonsensical
* results for certain coordinates (e.g. hue) or infinite loops if reducing the coordinate never brings the color in gamut.
* @param {ColorSpace|string} options.space - The space whose gamut we want to map to
*/
export default function toGamut (color, {method = defaults.gamut_mapping, space = color.space} = {}) {
if (util.isString(arguments[1])) {
space = arguments[1];
}
space = ColorSpace.get(space);
if (inGamut(color, space, {epsilon: 0})) {
return getColor(color);
}
// 3 spaces:
// color.space: current color space
// space: space whose gamut we are mapping to
// mapSpace: space with the coord we're reducing
let spaceColor = to(color, space);
if (method !== "clip" && !inGamut(color, space)) {
let clipped = toGamut(clone(spaceColor), {method: "clip", space});
if (deltaE2000(color, clipped) > 2) {
// Reduce a coordinate of a certain color space until the color is in gamut
let coordMeta = ColorSpace.resolveCoord(method);
let mapSpace = coordMeta.space;
let coordId = coordMeta.id;
let mappedColor = to(spaceColor, mapSpace);
let bounds = coordMeta.range || coordMeta.refRange;
let min = bounds[0];
let ε = .01; // for deltaE
let low = min;
let high = get(mappedColor, coordId);
while (high - low > ε) {
let clipped = clone(mappedColor);
clipped = toGamut(clipped, {space, method: "clip"});
let deltaE = deltaE2000(mappedColor, clipped);
if (deltaE - 2 < ε) {
low = get(mappedColor, coordId);
}
else {
high = get(mappedColor, coordId);
}
set(mappedColor, coordId, (low + high) / 2);
}
spaceColor = to(mappedColor, space);
}
else {
spaceColor = clipped;
}
}
if (method === "clip" // Dumb coord clipping
// finish off smarter gamut mapping with clip to get rid of ε, see #17
|| !inGamut(spaceColor, space, {epsilon: 0})
) {
let bounds = Object.values(space.coords).map(c => c.range || []);
spaceColor.coords = spaceColor.coords.map((c, i) => {
let [min, max] = bounds[i];
if (min !== undefined) {
c = Math.max(min, c);
}
if (max !== undefined) {
c = Math.min(c, max);
}
return c;
});
}
if (space !== color.space) {
spaceColor = to(spaceColor, color.space);
}
color.coords = spaceColor.coords;
return color;
}
toGamut.returns = "color";