@luntta/swatch 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/CONTRIBUTING.md +89 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +189 -0
- package/README.md +463 -0
- package/package.json +57 -0
- package/scripts/pack-check.mjs +18 -0
- package/src/bootstrap.js +159 -0
- package/src/core/registry.js +81 -0
- package/src/core/state.js +36 -0
- package/src/core/swatch-class.js +524 -0
- package/src/data/cvd-matrices.js +179 -0
- package/src/data/named-colors.js +157 -0
- package/src/format/css.js +256 -0
- package/src/operations/accessibility.js +103 -0
- package/src/operations/apca.js +80 -0
- package/src/operations/blend.js +72 -0
- package/src/operations/channels.js +123 -0
- package/src/operations/cvd.js +119 -0
- package/src/operations/deltaE.js +207 -0
- package/src/operations/gamut.js +206 -0
- package/src/operations/image.js +192 -0
- package/src/operations/manipulation.js +100 -0
- package/src/operations/mix.js +129 -0
- package/src/operations/naming.js +158 -0
- package/src/operations/palette.js +133 -0
- package/src/operations/random.js +75 -0
- package/src/operations/temperature.js +126 -0
- package/src/operations/tint-shade.js +42 -0
- package/src/palettes/colorbrewer.js +232 -0
- package/src/palettes/index.js +58 -0
- package/src/palettes/viridis.js +59 -0
- package/src/parse/css.js +241 -0
- package/src/parse/hex.js +38 -0
- package/src/parse/index.js +43 -0
- package/src/parse/legacy.js +88 -0
- package/src/parse/named.js +11 -0
- package/src/parse/objects.js +125 -0
- package/src/scale/index.js +382 -0
- package/src/scale/interpolators.js +83 -0
- package/src/spaces/a98.js +55 -0
- package/src/spaces/cmyk.js +75 -0
- package/src/spaces/display-p3.js +50 -0
- package/src/spaces/hsl.js +93 -0
- package/src/spaces/hsluv.js +211 -0
- package/src/spaces/hsv.js +78 -0
- package/src/spaces/hwb.js +48 -0
- package/src/spaces/lab.js +70 -0
- package/src/spaces/lch.js +65 -0
- package/src/spaces/oklab.js +79 -0
- package/src/spaces/oklch.js +53 -0
- package/src/spaces/prophoto.js +72 -0
- package/src/spaces/rec2020.js +65 -0
- package/src/spaces/srgb.js +85 -0
- package/src/spaces/xyz.js +71 -0
- package/src/swatch.js +57 -0
- package/src/util/math.js +53 -0
- package/src/util/matrix.js +92 -0
- package/src/util/suggest.js +66 -0
- package/types/swatch.d.ts +664 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Color space registry and conversion router.
|
|
2
|
+
//
|
|
3
|
+
// Spaces register themselves with `registerSpace`. Each space provides:
|
|
4
|
+
//
|
|
5
|
+
// id — string identifier (e.g. "srgb", "oklab", "display-p3")
|
|
6
|
+
// channels — names of the 3 channels (e.g. ["r", "g", "b"])
|
|
7
|
+
// ranges — natural ranges for each channel, used by clamping/normalization
|
|
8
|
+
// white — D65 (default) or D50 — used to know whether a Bradford CAT
|
|
9
|
+
// needs to run on the way to/from the canonical XYZ-D65 hub
|
|
10
|
+
// toXYZ — convert this space's coords → CIE XYZ D65
|
|
11
|
+
// fromXYZ — convert CIE XYZ D65 → this space's coords
|
|
12
|
+
// shortcuts — optional direct converters keyed by destination space id, used
|
|
13
|
+
// to bypass the XYZ hub for accuracy/speed (e.g. linear sRGB ↔
|
|
14
|
+
// OKLab is a single matrix multiply that loses precision if you
|
|
15
|
+
// round-trip via XYZ).
|
|
16
|
+
//
|
|
17
|
+
// Conversion lookup: A → B is attempted in this order:
|
|
18
|
+
// 1. A === B → identity
|
|
19
|
+
// 2. A.shortcuts[B] if defined
|
|
20
|
+
// 3. B.fromXYZ(A.toXYZ(coords))
|
|
21
|
+
|
|
22
|
+
import { appendSuggestion } from "../util/suggest.js";
|
|
23
|
+
|
|
24
|
+
const spaces = new Map();
|
|
25
|
+
|
|
26
|
+
export function registerSpace(space) {
|
|
27
|
+
if (!space || typeof space.id !== "string") {
|
|
28
|
+
throw new Error("registerSpace: missing id");
|
|
29
|
+
}
|
|
30
|
+
if (typeof space.toXYZ !== "function" || typeof space.fromXYZ !== "function") {
|
|
31
|
+
throw new Error(`registerSpace[${space.id}]: missing toXYZ/fromXYZ`);
|
|
32
|
+
}
|
|
33
|
+
const entry = {
|
|
34
|
+
id: space.id,
|
|
35
|
+
channels: space.channels || ["c1", "c2", "c3"],
|
|
36
|
+
ranges: space.ranges || null,
|
|
37
|
+
white: space.white || "D65",
|
|
38
|
+
toXYZ: space.toXYZ,
|
|
39
|
+
fromXYZ: space.fromXYZ,
|
|
40
|
+
shortcuts: space.shortcuts || {}
|
|
41
|
+
};
|
|
42
|
+
spaces.set(space.id, entry);
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getSpace(id) {
|
|
47
|
+
const entry = spaces.get(id);
|
|
48
|
+
if (!entry) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
appendSuggestion(`Unknown color space: ${id}`, id, listSpaces())
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function hasSpace(id) {
|
|
57
|
+
return spaces.has(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function listSpaces() {
|
|
61
|
+
return Array.from(spaces.keys());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Convert coords from one space to another. Coords is a length-3 array.
|
|
65
|
+
// Returns a new length-3 array (does not mutate input).
|
|
66
|
+
export function convert(coords, fromId, toId) {
|
|
67
|
+
if (fromId === toId) {
|
|
68
|
+
return [coords[0], coords[1], coords[2]];
|
|
69
|
+
}
|
|
70
|
+
const from = getSpace(fromId);
|
|
71
|
+
const to = getSpace(toId);
|
|
72
|
+
|
|
73
|
+
// Direct shortcut?
|
|
74
|
+
if (from.shortcuts[toId]) {
|
|
75
|
+
return from.shortcuts[toId](coords);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Hub route via XYZ D65.
|
|
79
|
+
const xyz = from.toXYZ(coords);
|
|
80
|
+
return to.fromXYZ(xyz);
|
|
81
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Canonical Swatch state shape.
|
|
2
|
+
//
|
|
3
|
+
// Every Swatch instance holds:
|
|
4
|
+
// space — string id of the source color space (e.g. "srgb", "oklab")
|
|
5
|
+
// coords — length-3 array of numbers in the source space's natural units
|
|
6
|
+
// alpha — number in [0, 1]
|
|
7
|
+
//
|
|
8
|
+
// This is the colorjs.io / culori representation: the input space is preserved
|
|
9
|
+
// losslessly and conversions to other spaces are computed lazily on the
|
|
10
|
+
// instance and memoized. Wide-gamut inputs (color(display-p3 1 0 0)) keep
|
|
11
|
+
// their full chromaticity instead of being clamped to sRGB.
|
|
12
|
+
|
|
13
|
+
export function makeState(space, coords, alpha) {
|
|
14
|
+
return {
|
|
15
|
+
space,
|
|
16
|
+
coords: [coords[0], coords[1], coords[2]],
|
|
17
|
+
alpha: alpha == null ? 1 : alpha
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cloneState(state) {
|
|
22
|
+
return {
|
|
23
|
+
space: state.space,
|
|
24
|
+
coords: [state.coords[0], state.coords[1], state.coords[2]],
|
|
25
|
+
alpha: state.alpha
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function statesEqual(a, b, epsilon = 0) {
|
|
30
|
+
if (a.space !== b.space) return false;
|
|
31
|
+
if (Math.abs(a.alpha - b.alpha) > epsilon) return false;
|
|
32
|
+
for (let i = 0; i < 3; i++) {
|
|
33
|
+
if (Math.abs(a.coords[i] - b.coords[i]) > epsilon) return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
// Swatch — the v3 color class.
|
|
2
|
+
//
|
|
3
|
+
// Each instance holds an immutable canonical `state` object
|
|
4
|
+
// ({ space, coords, alpha }) and exposes lazy memoized getters for every
|
|
5
|
+
// registered color space. Conversions are routed through the space registry
|
|
6
|
+
// (see src/core/registry.js), which uses CIE XYZ D65 as the hub.
|
|
7
|
+
//
|
|
8
|
+
// For Phase 1 the public surface is minimal: the canonical state, the
|
|
9
|
+
// `to(space)` converter, and the foundation-space getters (`srgb`,
|
|
10
|
+
// `linearSrgb`, `xyz`). Later phases register more spaces and each one
|
|
11
|
+
// automatically becomes accessible via `swatch.to('<space>')` without
|
|
12
|
+
// touching this file.
|
|
13
|
+
|
|
14
|
+
import { makeState, cloneState } from "./state.js";
|
|
15
|
+
import { convert, getSpace, listSpaces } from "./registry.js";
|
|
16
|
+
import { formatCss } from "../format/css.js";
|
|
17
|
+
|
|
18
|
+
function byteFromUnit(value) {
|
|
19
|
+
return Math.round(Math.max(0, Math.min(1, value)) * 255);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseEqualsOptions(options) {
|
|
23
|
+
if (typeof options === "number") return { epsilon: options };
|
|
24
|
+
if (options == null) return { epsilon: 1e-6 };
|
|
25
|
+
return {
|
|
26
|
+
epsilon: options.epsilon ?? options.tolerance ?? 1e-6,
|
|
27
|
+
space: options.space
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class Swatch {
|
|
32
|
+
constructor(state) {
|
|
33
|
+
if (!state || typeof state.space !== "string") {
|
|
34
|
+
throw new Error("Swatch: state must be { space, coords, alpha }");
|
|
35
|
+
}
|
|
36
|
+
// Validate the space up-front so invalid state objects fail at the
|
|
37
|
+
// boundary instead of producing a Swatch that explodes later during
|
|
38
|
+
// conversion or serialization.
|
|
39
|
+
getSpace(state.space);
|
|
40
|
+
if (!Array.isArray(state.coords) || state.coords.length !== 3) {
|
|
41
|
+
throw new Error("Swatch: coords must be a length-3 array");
|
|
42
|
+
}
|
|
43
|
+
const coords = state.coords.map(Number);
|
|
44
|
+
const alpha = state.alpha == null ? 1 : Number(state.alpha);
|
|
45
|
+
if (coords.some((value) => !Number.isFinite(value))) {
|
|
46
|
+
throw new Error("Swatch: coords must be finite numbers");
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isFinite(alpha)) {
|
|
49
|
+
throw new Error("Swatch: alpha must be a finite number");
|
|
50
|
+
}
|
|
51
|
+
this._state = makeState(state.space, coords, alpha);
|
|
52
|
+
this._cache = new Map();
|
|
53
|
+
// Expose foundation fields directly for ergonomics.
|
|
54
|
+
this.space = this._state.space;
|
|
55
|
+
this.alpha = this._state.alpha;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Raw coords in the source space (no conversion).
|
|
59
|
+
get coords() {
|
|
60
|
+
return [
|
|
61
|
+
this._state.coords[0],
|
|
62
|
+
this._state.coords[1],
|
|
63
|
+
this._state.coords[2]
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Convert to another registered space. Returns a new Swatch.
|
|
68
|
+
to(spaceId) {
|
|
69
|
+
if (spaceId === this._state.space) return this;
|
|
70
|
+
const coords = this._getCoordsIn(spaceId);
|
|
71
|
+
return new Swatch({
|
|
72
|
+
space: spaceId,
|
|
73
|
+
coords,
|
|
74
|
+
alpha: this._state.alpha
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Memoized raw coords in a given space, as a length-3 array. Internal use;
|
|
79
|
+
// most callers should use the named getters below or `.to(space).coords`.
|
|
80
|
+
_getCoordsIn(spaceId) {
|
|
81
|
+
if (spaceId === this._state.space) {
|
|
82
|
+
return [
|
|
83
|
+
this._state.coords[0],
|
|
84
|
+
this._state.coords[1],
|
|
85
|
+
this._state.coords[2]
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
if (this._cache.has(spaceId)) {
|
|
89
|
+
const cached = this._cache.get(spaceId);
|
|
90
|
+
return [cached[0], cached[1], cached[2]];
|
|
91
|
+
}
|
|
92
|
+
const result = convert(this._state.coords, this._state.space, spaceId);
|
|
93
|
+
this._cache.set(spaceId, result);
|
|
94
|
+
return [result[0], result[1], result[2]];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Return a plain object {c1, c2, c3} keyed by the space's channel names.
|
|
98
|
+
_asObjectIn(spaceId) {
|
|
99
|
+
const coords = this._getCoordsIn(spaceId);
|
|
100
|
+
const space = getSpace(spaceId);
|
|
101
|
+
const [k1, k2, k3] = space.channels;
|
|
102
|
+
return { [k1]: coords[0], [k2]: coords[1], [k3]: coords[2] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Clone (independent copy). The cache is not copied, which is fine because
|
|
106
|
+
// lookups are deterministic.
|
|
107
|
+
clone() {
|
|
108
|
+
return new Swatch(cloneState(this._state));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Structural equality with tolerance. Compares the same-space view of
|
|
112
|
+
// the other swatch against this swatch's native coords. Different source
|
|
113
|
+
// spaces will round-trip through conversion so visually-equivalent
|
|
114
|
+
// colors compare equal.
|
|
115
|
+
equals(other, options = 1e-6) {
|
|
116
|
+
let rhs;
|
|
117
|
+
try {
|
|
118
|
+
rhs = other instanceof Swatch ? other : swatch(other);
|
|
119
|
+
} catch (_err) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const { epsilon, space } = parseEqualsOptions(options);
|
|
123
|
+
const spaceId = space || this._state.space;
|
|
124
|
+
const lhsCoords = space ? this._getCoordsIn(spaceId) : this._state.coords;
|
|
125
|
+
const rhsCoords = rhs._getCoordsIn(spaceId);
|
|
126
|
+
if (Math.abs(rhsCoords[0] - lhsCoords[0]) > epsilon) return false;
|
|
127
|
+
if (Math.abs(rhsCoords[1] - lhsCoords[1]) > epsilon) return false;
|
|
128
|
+
if (Math.abs(rhsCoords[2] - lhsCoords[2]) > epsilon) return false;
|
|
129
|
+
if (Math.abs(rhs.alpha - this._state.alpha) > epsilon) return false;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
toJSON() {
|
|
134
|
+
return {
|
|
135
|
+
space: this._state.space,
|
|
136
|
+
coords: this.coords,
|
|
137
|
+
alpha: this._state.alpha
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Foundation-space getters ──────────────────────────────────────
|
|
142
|
+
//
|
|
143
|
+
// These return plain objects in the natural channel layout of each space.
|
|
144
|
+
// Later phases register the other spaces in the registry and add matching
|
|
145
|
+
// getters here.
|
|
146
|
+
|
|
147
|
+
get srgb() {
|
|
148
|
+
return this._asObjectIn("srgb");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
get linearSrgb() {
|
|
152
|
+
return this._asObjectIn("srgb-linear");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get xyz() {
|
|
156
|
+
return this._asObjectIn("xyz");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get lab() {
|
|
160
|
+
return this._asObjectIn("lab");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get lch() {
|
|
164
|
+
return this._asObjectIn("lch");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get oklab() {
|
|
168
|
+
return this._asObjectIn("oklab");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get oklch() {
|
|
172
|
+
return this._asObjectIn("oklch");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get hsl() {
|
|
176
|
+
return this._asObjectIn("hsl");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
get displayP3() {
|
|
180
|
+
return this._asObjectIn("display-p3");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
get rec2020() {
|
|
184
|
+
return this._asObjectIn("rec2020");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
get a98() {
|
|
188
|
+
return this._asObjectIn("a98");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get prophoto() {
|
|
192
|
+
return this._asObjectIn("prophoto");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get hsv() {
|
|
196
|
+
return this._asObjectIn("hsv");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get hwb() {
|
|
200
|
+
return this._asObjectIn("hwb");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
get cmyk() {
|
|
204
|
+
// CMYK is special: we derive K from max(R,G,B) for presentation.
|
|
205
|
+
const { r, g, b } = this.srgb;
|
|
206
|
+
const k = 1 - Math.max(r, g, b);
|
|
207
|
+
if (k >= 1 - 1e-12) return { c: 0, m: 0, y: 0, k: 1 };
|
|
208
|
+
const c = (1 - r - k) / (1 - k);
|
|
209
|
+
const m = (1 - g - k) / (1 - k);
|
|
210
|
+
const y = (1 - b - k) / (1 - k);
|
|
211
|
+
return { c, m, y, k };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
get hsluv() {
|
|
215
|
+
return this._asObjectIn("hsluv");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get luv() {
|
|
219
|
+
return this._asObjectIn("luv");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── CSS serialization ─────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
toString(opts) {
|
|
225
|
+
return formatCss(this, opts);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
toCss(opts) {
|
|
229
|
+
return formatCss(this, opts);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
css(opts) {
|
|
233
|
+
return formatCss(this, opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─── Debug representation ──────────────────────────────────────────
|
|
237
|
+
//
|
|
238
|
+
// Without these, `console.log(swatch("#f00"))` dumps `_state`, `_cache`,
|
|
239
|
+
// and the bound internals. Surface the canonical color instead. The
|
|
240
|
+
// `nodejs.util.inspect.custom` symbol is registered cross-realm, so this
|
|
241
|
+
// is a no-op in browsers (which simply ignore the unknown symbol key).
|
|
242
|
+
|
|
243
|
+
get [Symbol.toStringTag]() {
|
|
244
|
+
return "Swatch";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
248
|
+
return `Swatch <${this.css()}>`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// sRGB view for the lossy display helpers (`hex`, `rgb`). Wide-gamut
|
|
252
|
+
// sources are perceptually mapped into sRGB (CSS Color 4 chroma
|
|
253
|
+
// reduction) by default so `.hex()` / `.rgb()` never silently clip
|
|
254
|
+
// chromaticity. Pass `gamut: false` to fall back to a raw clamp.
|
|
255
|
+
_displaySrgb(gamutMap = true) {
|
|
256
|
+
if (!gamutMap || this.inGamut("srgb")) return this;
|
|
257
|
+
return this.toGamut({ space: "srgb", method: "css4" });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
hex(opts = {}) {
|
|
261
|
+
const includeAlpha =
|
|
262
|
+
typeof opts === "boolean" ? opts : opts?.alpha === true;
|
|
263
|
+
const gamutMap = typeof opts === "boolean" ? true : opts?.gamut !== false;
|
|
264
|
+
return formatCss(this._displaySrgb(gamutMap), {
|
|
265
|
+
format: includeAlpha ? "hex-alpha" : "hex"
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
rgb(opts = {}) {
|
|
270
|
+
const alphaMode =
|
|
271
|
+
typeof opts === "boolean" ? opts : opts?.alpha ?? "auto";
|
|
272
|
+
const gamutMap = typeof opts === "boolean" ? true : opts?.gamut !== false;
|
|
273
|
+
const { r, g, b } = this._displaySrgb(gamutMap).srgb;
|
|
274
|
+
const out = {
|
|
275
|
+
r: byteFromUnit(r),
|
|
276
|
+
g: byteFromUnit(g),
|
|
277
|
+
b: byteFromUnit(b)
|
|
278
|
+
};
|
|
279
|
+
if (
|
|
280
|
+
alphaMode === true ||
|
|
281
|
+
alphaMode === "always" ||
|
|
282
|
+
(alphaMode === "auto" && this.alpha < 1)
|
|
283
|
+
) {
|
|
284
|
+
out.a = this.alpha;
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── Channel get/set ───────────────────────────────────────────────
|
|
290
|
+
//
|
|
291
|
+
// Late-bound to avoid circulars with src/operations/channels.js.
|
|
292
|
+
|
|
293
|
+
get(path) {
|
|
294
|
+
return _getChannel(this, path);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
set(path, value) {
|
|
298
|
+
return _setChannel(this, path, value);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ─── Gamut ─────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
// The smallest standard RGB gamut that contains this color, walking the
|
|
304
|
+
// nested ladder srgb ⊂ display-p3 ⊂ rec2020 ⊂ prophoto. Returns null for
|
|
305
|
+
// imaginary colors that fall outside even ProPhoto. Useful for noticing
|
|
306
|
+
// out-of-sRGB colors before a `.hex()` / `.rgb()` round-trip maps them in.
|
|
307
|
+
get gamut() {
|
|
308
|
+
const ladder = ["srgb", "display-p3", "rec2020", "prophoto"];
|
|
309
|
+
for (const id of ladder) {
|
|
310
|
+
if (this.inGamut(id)) return id;
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
inGamut(spaceId, opts) {
|
|
316
|
+
return _inGamut(this, spaceId, opts);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
toGamut(opts) {
|
|
320
|
+
return _toGamut(this, opts);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ─── Manipulation (OKLCh-based, late-bound) ────────────────────────
|
|
324
|
+
|
|
325
|
+
lighten(amount, opts) {
|
|
326
|
+
return _manip.lighten(this, amount, opts);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
darken(amount, opts) {
|
|
330
|
+
return _manip.darken(this, amount, opts);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
saturate(amount, opts) {
|
|
334
|
+
return _manip.saturate(this, amount, opts);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
desaturate(amount, opts) {
|
|
338
|
+
return _manip.desaturate(this, amount, opts);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
spin(degrees, opts) {
|
|
342
|
+
return _manip.spin(this, degrees, opts);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
greyscale(opts) {
|
|
346
|
+
return _manip.greyscale(this, opts);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
complement(opts) {
|
|
350
|
+
return _manip.complement(this, opts);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
invert() {
|
|
354
|
+
return _manip.invert(this);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
tint(amount) {
|
|
358
|
+
return _tintShade.tint(this, amount);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
shade(amount) {
|
|
362
|
+
return _tintShade.shade(this, amount);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
tone(amount) {
|
|
366
|
+
return _tintShade.tone(this, amount);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
mix(other, amount, opts) {
|
|
370
|
+
return _mix.mix(this, other, amount, opts);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
blend(other, mode) {
|
|
374
|
+
return _mix.blend(this, other, mode);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── ΔE ────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
deltaE(other, mode, opts) {
|
|
380
|
+
return _deltaE.deltaE(this, other, mode, opts);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ─── Naming ────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
name(opts) {
|
|
386
|
+
return _naming.name(this, opts);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
toName() {
|
|
390
|
+
return _naming.toName(this);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ─── Temperature ───────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
temperature() {
|
|
396
|
+
return _temperature.kelvin(this);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Accessibility ─────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
luminance() {
|
|
402
|
+
return _accessibility.luminance(this);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
contrast(other) {
|
|
406
|
+
return _accessibility.contrast(this, other);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
isReadable(other, opts) {
|
|
410
|
+
return _accessibility.isReadable(this, other, opts);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
ensureContrast(other, opts) {
|
|
414
|
+
return _accessibility.ensureContrast(this, other, opts);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
apcaContrast(other) {
|
|
418
|
+
// Receiver is the text; `other` is the background.
|
|
419
|
+
return _apca.apcaContrast(this, other);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ─── CVD ───────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
simulate(type, opts) {
|
|
425
|
+
return _cvd.simulate(this, type, opts);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
daltonize(type, opts) {
|
|
429
|
+
return _cvd.daltonize(this, type, opts);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let _getChannel = null;
|
|
434
|
+
let _setChannel = null;
|
|
435
|
+
export function _bindChannels(getFn, setFn) {
|
|
436
|
+
_getChannel = getFn;
|
|
437
|
+
_setChannel = setFn;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let _inGamut = null;
|
|
441
|
+
let _toGamut = null;
|
|
442
|
+
export function _bindGamut(inFn, toFn) {
|
|
443
|
+
_inGamut = inFn;
|
|
444
|
+
_toGamut = toFn;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let _manip = {};
|
|
448
|
+
export function _bindManipulation(fns) {
|
|
449
|
+
_manip = fns;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
let _tintShade = {};
|
|
453
|
+
export function _bindTintShade(fns) {
|
|
454
|
+
_tintShade = fns;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let _mix = {};
|
|
458
|
+
export function _bindMix(fns) {
|
|
459
|
+
_mix = fns;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let _deltaE = {};
|
|
463
|
+
export function _bindDeltaE(fns) {
|
|
464
|
+
_deltaE = fns;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let _naming = {};
|
|
468
|
+
export function _bindNaming(fns) {
|
|
469
|
+
_naming = fns;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let _temperature = {};
|
|
473
|
+
export function _bindTemperature(fns) {
|
|
474
|
+
_temperature = fns;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let _accessibility = {};
|
|
478
|
+
export function _bindAccessibility(fns) {
|
|
479
|
+
_accessibility = fns;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let _apca = {};
|
|
483
|
+
export function _bindApca(fns) {
|
|
484
|
+
_apca = fns;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
let _cvd = {};
|
|
488
|
+
export function _bindCvd(fns) {
|
|
489
|
+
_cvd = fns;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Factory / invocation without `new`.
|
|
493
|
+
export function swatchFromState(state) {
|
|
494
|
+
return new Swatch(state);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Build a Swatch from any recognized input form (string, object literal,
|
|
498
|
+
// or existing Swatch). Throws on unrecognized input. Lazily imported to
|
|
499
|
+
// avoid a circular dep between Swatch and the parser dispatcher.
|
|
500
|
+
let _parseInput = null;
|
|
501
|
+
export function _bindParseInput(fn) {
|
|
502
|
+
_parseInput = fn;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function swatch(input) {
|
|
506
|
+
if (input instanceof Swatch) return input;
|
|
507
|
+
if (!_parseInput) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
"swatch(): parser not initialized. Import swatch from '@luntta/swatch' (the package entry point) rather than reaching into core/swatch-class.js directly."
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const state = _parseInput(input);
|
|
513
|
+
if (!state) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
"swatch(): could not parse input: " + JSON.stringify(input)
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return new Swatch(state);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Helper for tests: is a given id registered?
|
|
522
|
+
export function knownSpaces() {
|
|
523
|
+
return listSpaces();
|
|
524
|
+
}
|