pts 0.12.8 → 1.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/src/Color.ts ADDED
@@ -0,0 +1,1109 @@
1
+ /*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
2
+
3
+ import { Pt, Group } from "./Pt";
4
+ import { Util } from "./Util";
5
+ import { Num, Geom } from "./Num";
6
+ import { type PtLike, type ColorType } from "./Types";
7
+
8
+ /**
9
+ * Color is a subclass of Pt. Since a color in a color space is analogous to a point or vector in a space, you can apply all Pt operations to colors too. The Color class provides support for many color spaces like HSL and LAB.
10
+ * Convert non-RGB colors to RGB before using `.hex`, `.rgb`, or `.rgba` for rendering. These getters format the channels; they don't convert between color spaces.
11
+ * @example
12
+ * ```
13
+ * const color = Color.hsl(268, 0.37, 0.51);
14
+ * form.fill(Color.HSLtoRGB(color).rgb);
15
+ * ```
16
+ */
17
+ export class Color extends Pt {
18
+ // XYZ property for Standard Observer 2deg, Daylight/sRGB illuminant D65
19
+ private static D65: PtLike = new Pt(95.047, 100, 108.883, 1);
20
+
21
+ protected _mode: ColorType = "rgb";
22
+ private _isNorm: boolean = false;
23
+
24
+ /**
25
+ * Value range for each color space
26
+ */
27
+ static ranges: { [name: string]: Group } = {
28
+ rgb: new Group(new Pt(0, 255), new Pt(0, 255), new Pt(0, 255)),
29
+ hsl: new Group(new Pt(0, 360), new Pt(0, 1), new Pt(0, 1)),
30
+ hsb: new Group(new Pt(0, 360), new Pt(0, 1), new Pt(0, 1)),
31
+ lab: new Group(new Pt(0, 100), new Pt(-128, 127), new Pt(-128, 127)),
32
+ lch: new Group(new Pt(0, 100), new Pt(0, 100), new Pt(0, 360)),
33
+ luv: new Group(new Pt(0, 100), new Pt(-134, 220), new Pt(-140, 122)),
34
+ xyz: new Group(new Pt(0, 100), new Pt(0, 100), new Pt(0, 100)),
35
+ oklab: new Group(new Pt(0, 1), new Pt(-0.4, 0.4), new Pt(-0.4, 0.4)),
36
+ oklch: new Group(new Pt(0, 1), new Pt(0, 0.4), new Pt(0, 360)),
37
+ };
38
+
39
+ /**
40
+ * Create a Color. Same as creating a Pt. Optionally you may use [`Color.from`](#link) to create a color.
41
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties
42
+ */
43
+ constructor(...args: any[]) {
44
+ super(...args);
45
+ }
46
+
47
+ /**
48
+ * Create a Color object with 4 default dimensional values (1,1,1,1).
49
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties
50
+ */
51
+ static from(...args: any[]): Color {
52
+ const p = [1, 1, 1, 1];
53
+ const c = Util.getArgs(args);
54
+ for (let i = 0, len = p.length; i < len; i++) {
55
+ if (i < c.length) p[i] = c[i];
56
+ }
57
+ return new Color(p);
58
+ }
59
+
60
+ /**
61
+ * Convert a rgb hex string like `"#FF0000"` or `"#F00"` to a Color object.
62
+ * @param hex a hex string, with optional '#' prefix
63
+ */
64
+ static fromHex(hex: string): Color {
65
+ if (hex[0] == "#") hex = hex.substr(1); // remove '#' if needed
66
+ if (hex.length <= 4) {
67
+ const fn = (i: number) => hex[i] || "F";
68
+ const shortAlpha = hex.length === 4 ? `${fn(3)}${fn(3)}` : ""; // #RGBA
69
+ hex = `${fn(0)}${fn(0)}${fn(1)}${fn(1)}${fn(2)}${fn(2)}${shortAlpha}`;
70
+ }
71
+
72
+ let alpha = 1;
73
+ if (hex.length === 8) {
74
+ alpha = parseInt(hex.substr(6, 2), 16) / 255;
75
+ hex = hex.substring(0, 6);
76
+ }
77
+
78
+ const hexVal = parseInt(hex, 16);
79
+ return new Color(hexVal >> 16, (hexVal >> 8) & 0xff, hexVal & 0xff, alpha);
80
+ }
81
+
82
+ /**
83
+ * Create RGB Color. RGB color ranges are (0...255, 0...255, 0...255) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
84
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
85
+ */
86
+ static rgb(...args: any[]): Color {
87
+ return Color.from(...args).toMode("rgb");
88
+ }
89
+
90
+ /**
91
+ * Create HSL Color. HSL color ranges are (0...360, 0...1, 0...1) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
92
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
93
+ */
94
+ static hsl(...args: any[]): Color {
95
+ return Color.from(...args).toMode("hsl");
96
+ }
97
+
98
+ /**
99
+ * Create HSB Color. HSB color ranges are (0...360, 0...1, 0...1) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
100
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
101
+ */
102
+ static hsb(...args: any[]): Color {
103
+ return Color.from(...args).toMode("hsb");
104
+ }
105
+
106
+ /**
107
+ * Create LAB Color. LAB color ranges are (0...100, -128...127, -128...127) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
108
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
109
+ */
110
+ static lab(...args: any[]): Color {
111
+ return Color.from(...args).toMode("lab");
112
+ }
113
+
114
+ /**
115
+ * Create LCH Color. LCH color ranges are (0...100, 0...100, 0...360) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
116
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
117
+ */
118
+ static lch(...args: any[]): Color {
119
+ return Color.from(...args).toMode("lch");
120
+ }
121
+
122
+ /**
123
+ * Create LUV Color. LUV color ranges are (0...100, -134...220, -140...122) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
124
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
125
+ */
126
+ static luv(...args: any[]): Color {
127
+ return Color.from(...args).toMode("luv");
128
+ }
129
+
130
+ /**
131
+ * Create XYZ Color. XYZ color ranges are (0...100, 0...100, 0...100) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
132
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
133
+ */
134
+ static xyz(...args: any[]): Color {
135
+ return Color.from(...args).toMode("xyz");
136
+ }
137
+
138
+ /**
139
+ * Create OKLAB Color. OKLAB color ranges are (0...1, -0.4...0.4, -0.4...0.4) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
140
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
141
+ */
142
+ static oklab(...args: any[]): Color {
143
+ return Color.from(...args).toMode("oklab");
144
+ }
145
+
146
+ /**
147
+ * Create OKLCH Color. OKLCH color ranges are (0...1, 0...0.4, 0...360) respectively. You may use [`Color.normalize`](#link) to convert the ranges to 0...1.
148
+ * @param args Pt-like parameters which can be a list of numeric parameters, an array of numbers, or an object with {x,y,z,w} properties.
149
+ */
150
+ static oklch(...args: any[]): Color {
151
+ return Color.from(...args).toMode("oklch");
152
+ }
153
+
154
+ /**
155
+ * Get a Color object whose values are the maximum of its mode.
156
+ * @param mode a mode string such as "rgb" or "lab"
157
+ * @example Color.maxValue("rgb") will return a rgb Color object with values (255,255,255)
158
+ */
159
+ static maxValues(mode: string): Pt {
160
+ return Color.ranges[mode].zipSlice(1).$take([0, 1, 2]);
161
+ }
162
+
163
+ /**
164
+ * Get a hex string such as "#FF0000". Same as `toString("hex")`.
165
+ * For non-RGB colors, convert to RGB first.
166
+ */
167
+ public get hex(): string {
168
+ return this.toString("hex");
169
+ }
170
+
171
+ /**
172
+ * Get a rgb string such as "rgb(255,0,0)". Same as `toString("rgb")`.
173
+ * For non-RGB colors, convert to RGB first, eg `Color.HSLtoRGB(color).rgb`.
174
+ */
175
+ public get rgb(): string {
176
+ return this.toString("rgb");
177
+ }
178
+
179
+ /**
180
+ * Get a rgba string such as "rgba(255,0,0,0.5)". Same as `toString("rgba")`.
181
+ * For non-RGB colors, convert to RGB first.
182
+ */
183
+ public get rgba(): string {
184
+ return this.toString("rgba");
185
+ }
186
+
187
+ /**
188
+ * Clone this Color.
189
+ */
190
+ clone(): Color {
191
+ const c = new Color(this);
192
+ c.toMode(this._mode);
193
+ c._isNorm = this._isNorm;
194
+ return c;
195
+ }
196
+
197
+ /**
198
+ * Get a denormalized copy of a color, trusting the caller's flag over the
199
+ * color's own normalized state so that explicit conversion arguments win.
200
+ * Inlines the range math — this sits on the hot path of every conversion
201
+ * that takes normalized input.
202
+ */
203
+ private static _denorm(c: Color): Color {
204
+ const cc = c.clone();
205
+ const ranges = Color.ranges[cc._mode];
206
+ for (let i = 0; i < 3; i++) {
207
+ const r = ranges[i];
208
+ cc[i] = r[0] + cc[i] * (r[1] - r[0]);
209
+ }
210
+ cc._isNorm = false;
211
+ return cc;
212
+ }
213
+
214
+ /**
215
+ * Normalize a freshly computed full-range color in place and flag it.
216
+ * Conversion outputs are always full-range, so no state check is needed.
217
+ */
218
+ private static _normOut(c: Color): Color {
219
+ const ranges = Color.ranges[c._mode];
220
+ for (let i = 0; i < 3; i++) {
221
+ const r = ranges[i];
222
+ c[i] = (c[i] - r[0]) / (r[1] - r[0]);
223
+ }
224
+ c._isNorm = true;
225
+ return c;
226
+ }
227
+
228
+ /**
229
+ * Convert this color from current color space to another color space.
230
+ * @param mode a ColorType string: "rgb" "hsl" "hsb" "lab" "lch" "luv" "xyz";
231
+ * @param convert if `true`, convert this Color to the new color space specified in `mode`. Default is `false`, which only sets the color mode without converting color values.
232
+ */
233
+ toMode(mode: ColorType, convert: boolean = false): this {
234
+ if (convert && mode !== this._mode) {
235
+ const fname = this._mode.toUpperCase() + "to" + mode.toUpperCase();
236
+ if ((Color as any)[fname]) {
237
+ this.to((Color as any)[fname](this, this._isNorm, this._isNorm));
238
+ } else {
239
+ throw new Error("Cannot convert color with " + fname);
240
+ }
241
+ }
242
+
243
+ this._mode = mode;
244
+ return this;
245
+ }
246
+
247
+ /**
248
+ * Get this Color's mode.
249
+ */
250
+ get mode(): ColorType {
251
+ return this._mode;
252
+ }
253
+
254
+ // rgb
255
+ /**
256
+ * the `r` value in RGB color mode. Same as `x`.
257
+ */
258
+ get r(): number {
259
+ return this[0];
260
+ }
261
+ set r(n: number) {
262
+ this[0] = n;
263
+ }
264
+
265
+ /**
266
+ * the `g` value in RGB color mode. Same as `y`.
267
+ */
268
+ get g(): number {
269
+ return this[1];
270
+ }
271
+ set g(n: number) {
272
+ this[1] = n;
273
+ }
274
+
275
+ /**
276
+ * the `b` value in RGB/LAB/HSB color mode. Same as `z`.
277
+ */
278
+ get b(): number {
279
+ return this[2];
280
+ }
281
+ set b(n: number) {
282
+ this[2] = n;
283
+ }
284
+
285
+ // hsl, hsb
286
+ /**
287
+ * the `h` value in HSL/HSB or LCH color mode. Same as either `x` or `z` depending on current color mode.
288
+ */
289
+ get h(): number {
290
+ return this._mode == "lch" || this._mode == "oklch" ? this[2] : this[0];
291
+ }
292
+ set h(n: number) {
293
+ const i = this._mode == "lch" || this._mode == "oklch" ? 2 : 0;
294
+ this[i] = n;
295
+ }
296
+
297
+ /**
298
+ * the `s` value in HSL/HSB color mode. Same as `y`.
299
+ */
300
+ get s(): number {
301
+ return this[1];
302
+ }
303
+ set s(n: number) {
304
+ this[1] = n;
305
+ }
306
+
307
+ /**
308
+ * the `l` value in HSL or LCH/LAB color mode. Same as either `x` or `z` depending on current color mode.
309
+ */
310
+ get l(): number {
311
+ return this._mode == "hsl" ? this[2] : this[0];
312
+ }
313
+ set l(n: number) {
314
+ const i = this._mode == "hsl" ? 2 : 0;
315
+ this[i] = n;
316
+ }
317
+
318
+ // lab, lch, luv
319
+ /**
320
+ * the `a` value in LAB color mode. Same as `y`.
321
+ */
322
+ get a(): number {
323
+ return this[1];
324
+ }
325
+ set a(n: number) {
326
+ this[1] = n;
327
+ }
328
+
329
+ /**
330
+ * the `c` value in LCH color mode. Same as `y`.
331
+ */
332
+ get c(): number {
333
+ return this[1];
334
+ }
335
+ set c(n: number) {
336
+ this[1] = n;
337
+ }
338
+
339
+ /**
340
+ * the `u` value in LUV color mode. Same as `y`.
341
+ */
342
+ get u(): number {
343
+ return this[1];
344
+ }
345
+ set u(n: number) {
346
+ this[1] = n;
347
+ }
348
+
349
+ /**
350
+ * the `v` value in LUV color mode. Same as `z`.
351
+ */
352
+ get v(): number {
353
+ return this[2];
354
+ }
355
+ set v(n: number) {
356
+ this[2] = n;
357
+ }
358
+
359
+ /**
360
+ * Get alpha value
361
+ */
362
+ set alpha(n: number) {
363
+ if (this.length > 3) this[3] = n;
364
+ }
365
+ get alpha(): number {
366
+ return this.length > 3 ? this[3] : 1;
367
+ }
368
+
369
+ /**
370
+ * Check if color values are normalized (between 0 to 1). If conversion is needed, use [`Color.normalize`](#link) function.
371
+ */
372
+ get normalized(): boolean {
373
+ return this._isNorm;
374
+ }
375
+ set normalized(b: boolean) {
376
+ this._isNorm = b;
377
+ }
378
+
379
+ /**
380
+ * Normalize the color values to between 0 to 1, or revert it back to the min/max values in current color mode.
381
+ * @param toNorm a boolean value specifying whether to normalize (`true`) or revert (`false`)
382
+ */
383
+ normalize(toNorm: boolean = true): Color {
384
+ if (this._isNorm == toNorm) return this;
385
+
386
+ const ranges = Color.ranges[this._mode];
387
+
388
+ for (let i = 0; i < 3; i++) {
389
+ this[i] = !toNorm
390
+ ? Num.mapToRange(this[i], 0, 1, ranges[i][0], ranges[i][1])
391
+ : Num.mapToRange(this[i], ranges[i][0], ranges[i][1], 0, 1);
392
+ }
393
+
394
+ this._isNorm = toNorm;
395
+
396
+ return this;
397
+ }
398
+
399
+ /**
400
+ * Like `normalize()` but returns a new Color.
401
+ * @param toNorm a boolean value specifying whether to normalize (`true`) or revert (`false`)
402
+ * @returns new Color
403
+ */
404
+ $normalize(toNorm: boolean = true): Color {
405
+ return this.clone().normalize(toNorm);
406
+ }
407
+
408
+ /**
409
+ * Convert this Color to a string. It can be used to get a hex or rgb string for use in rendering.
410
+ * This formats the current channels without converting color spaces. Convert to RGB before requesting "hex", "rgb", or "rgba". The default "mode" format is for inspecting values, not for CSS rendering.
411
+ * @param format "hex", "rgb", "rgba", or "mode" which means using current color mode label. Default is "mode".
412
+ */
413
+ toString(format: "hex" | "rgb" | "rgba" | "mode" = "mode"): string {
414
+ // CSS formats need full-range values; a normalized color would floor to 0
415
+ const v: Color | this =
416
+ this._isNorm && format !== "mode" ? Color._denorm(this) : this;
417
+ if (format == "hex") {
418
+ const _hex = (n: number) => {
419
+ const s = Math.floor(n).toString(16);
420
+ return s.length < 2 ? "0" + s : s;
421
+ };
422
+ return `#${_hex(v[0])}${_hex(v[1])}${_hex(v[2])}`;
423
+ } else if (format == "rgba") {
424
+ return `rgba(${Math.floor(v[0])},${Math.floor(v[1])},${Math.floor(v[2])},${this.alpha})`;
425
+ } else if (format == "rgb") {
426
+ return `rgb(${Math.floor(v[0])},${Math.floor(v[1])},${Math.floor(v[2])})`;
427
+ } else {
428
+ return `${this._mode}(${this[0]},${this[1]},${this[2]},${this.alpha})`;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * A static function to convert RGB to HSL.
434
+ * @param rgb a RGB Color
435
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
436
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
437
+ * @returns a new HSL Color
438
+ */
439
+ static RGBtoHSL(
440
+ rgb: Color,
441
+ normalizedInput: boolean = false,
442
+ normalizedOutput: boolean = false,
443
+ ): Color {
444
+ const [r, g, b] = !normalizedInput ? rgb.$normalize() : rgb;
445
+
446
+ const max = Math.max(r, g, b);
447
+ const min = Math.min(r, g, b);
448
+ let h = (max + min) / 2;
449
+ let s = h;
450
+ const l = h;
451
+
452
+ if (max == min) {
453
+ h = 0;
454
+ s = 0; // achromatic
455
+ } else {
456
+ const d = max - min;
457
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
458
+
459
+ h = 0;
460
+ if (max === r) {
461
+ h = (g - b) / d + (g < b ? 6 : 0);
462
+ } else if (max === g) {
463
+ h = (b - r) / d + 2;
464
+ } else if (max === b) {
465
+ h = (r - g) / d + 4;
466
+ }
467
+ }
468
+
469
+ const cc = Color.hsl(normalizedOutput ? h / 6 : h * 60, s, l, rgb.alpha);
470
+ if (normalizedOutput) cc.normalized = true;
471
+ return cc;
472
+ }
473
+
474
+ /**
475
+ * A static function to convert HSL to RGB.
476
+ * @param hsl a HSL Color
477
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
478
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
479
+ * @returns a new RGB Color
480
+ */
481
+ static HSLtoRGB(
482
+ hsl: Color,
483
+ normalizedInput: boolean = false,
484
+ normalizedOutput: boolean = false,
485
+ ): Color {
486
+ let [h, s, l] = hsl;
487
+ if (!normalizedInput) h = h / 360;
488
+ h = h - Math.floor(h); // wrap hue into [0, 1)
489
+
490
+ const sc = normalizedOutput ? 1 : 255;
491
+ if (s == 0) {
492
+ const gray = Color.rgb(sc * l, sc * l, sc * l, hsl.alpha);
493
+ if (normalizedOutput) gray.normalized = true;
494
+ return gray;
495
+ }
496
+
497
+ const q = l <= 0.5 ? l * (1 + s) : l + s - l * s;
498
+ const p = 2 * l - q;
499
+
500
+ const convert = (t: number) => {
501
+ t = t < 0 ? t + 1 : t > 1 ? t - 1 : t;
502
+ if (t * 6 < 1) {
503
+ return p + (q - p) * t * 6;
504
+ } else if (t * 2 < 1) {
505
+ return q;
506
+ } else if (t * 3 < 2) {
507
+ return p + (q - p) * (2 / 3 - t) * 6;
508
+ } else {
509
+ return p;
510
+ }
511
+ };
512
+
513
+ const cc = Color.rgb(
514
+ sc * convert(h + 1 / 3),
515
+ sc * convert(h),
516
+ sc * convert(h - 1 / 3),
517
+ hsl.alpha,
518
+ );
519
+ if (normalizedOutput) cc.normalized = true;
520
+ return cc;
521
+ }
522
+
523
+ /**
524
+ * A static function to convert RGB to HSB.
525
+ * @param rgb a RGB Color
526
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
527
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
528
+ * @returns a new HSB Color
529
+ */
530
+ static RGBtoHSB(
531
+ rgb: Color,
532
+ normalizedInput: boolean = false,
533
+ normalizedOutput: boolean = false,
534
+ ): Color {
535
+ const [r, g, b] = !normalizedInput ? rgb.$normalize() : rgb;
536
+
537
+ const max = Math.max(r, g, b);
538
+ const min = Math.min(r, g, b);
539
+ const d = max - min;
540
+ let h = 0;
541
+ const s = max === 0 ? 0 : d / max;
542
+ const v = max;
543
+
544
+ if (max != min) {
545
+ if (max === r) {
546
+ h = (g - b) / d + (g < b ? 6 : 0);
547
+ } else if (max === g) {
548
+ h = (b - r) / d + 2;
549
+ } else if (max === b) {
550
+ h = (r - g) / d + 4;
551
+ }
552
+ }
553
+
554
+ const cc = Color.hsb(normalizedOutput ? h / 6 : h * 60, s, v, rgb.alpha);
555
+ if (normalizedOutput) cc.normalized = true;
556
+ return cc;
557
+ }
558
+
559
+ /**
560
+ * A static function to convert HSB to RGB.
561
+ * @param hsb a HSB Color
562
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
563
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
564
+ * @returns a new RGB Color
565
+ */
566
+ static HSBtoRGB(
567
+ hsb: Color,
568
+ normalizedInput: boolean = false,
569
+ normalizedOutput: boolean = false,
570
+ ): Color {
571
+ let [h, s, v] = hsb;
572
+ if (!normalizedInput) h = h / 360;
573
+ h = h - Math.floor(h); // wrap hue into [0, 1)
574
+
575
+ const i = Math.floor(h * 6);
576
+ const f = h * 6 - i;
577
+ const p = v * (1 - s);
578
+ const q = v * (1 - f * s);
579
+ const t = v * (1 - (1 - f) * s);
580
+
581
+ const pick = [
582
+ [v, t, p],
583
+ [q, v, p],
584
+ [p, v, t],
585
+ [p, q, v],
586
+ [t, p, v],
587
+ [v, p, q],
588
+ ];
589
+ const c = pick[i % 6];
590
+
591
+ const sc = normalizedOutput ? 1 : 255;
592
+
593
+ const cc = Color.rgb(sc * c[0], sc * c[1], sc * c[2], hsb.alpha);
594
+ if (normalizedOutput) cc.normalized = true;
595
+ return cc;
596
+ }
597
+
598
+ /**
599
+ * A static function to convert RGB to LAB.
600
+ * @param rgb a RGB Color
601
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
602
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
603
+ * @returns a new LAB Color
604
+ */
605
+ static RGBtoLAB(
606
+ rgb: Color,
607
+ normalizedInput: boolean = false,
608
+ normalizedOutput: boolean = false,
609
+ ): Color {
610
+ const c = normalizedInput ? Color._denorm(rgb) : rgb;
611
+ return Color.XYZtoLAB(Color.RGBtoXYZ(c), false, normalizedOutput);
612
+ }
613
+
614
+ /**
615
+ * A static function to convert LAB to RGB.
616
+ * @param lab a LAB Color
617
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
618
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
619
+ * @returns a new RGB Color
620
+ */
621
+ static LABtoRGB(
622
+ lab: Color,
623
+ normalizedInput: boolean = false,
624
+ normalizedOutput: boolean = false,
625
+ ): Color {
626
+ const c = normalizedInput ? Color._denorm(lab) : lab;
627
+ return Color.XYZtoRGB(Color.LABtoXYZ(c), false, normalizedOutput);
628
+ }
629
+
630
+ /**
631
+ * A static function to convert RGB to LCH.
632
+ * @param rgb a RGB Color
633
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
634
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
635
+ * @returns a new LCH Color
636
+ */
637
+ static RGBtoLCH(
638
+ rgb: Color,
639
+ normalizedInput: boolean = false,
640
+ normalizedOutput: boolean = false,
641
+ ): Color {
642
+ const c = normalizedInput ? Color._denorm(rgb) : rgb;
643
+ return Color.LABtoLCH(Color.RGBtoLAB(c), false, normalizedOutput);
644
+ }
645
+
646
+ /**
647
+ * A static function to convert LCH to RGB.
648
+ * @param lch a LCH Color
649
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
650
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
651
+ * @returns a new RGB Color
652
+ */
653
+ static LCHtoRGB(
654
+ lch: Color,
655
+ normalizedInput: boolean = false,
656
+ normalizedOutput: boolean = false,
657
+ ): Color {
658
+ const c = normalizedInput ? Color._denorm(lch) : lch;
659
+ return Color.LABtoRGB(Color.LCHtoLAB(c), false, normalizedOutput);
660
+ }
661
+
662
+ /**
663
+ * A static function to convert RGB to LUV.
664
+ * @param rgb a RGB Color
665
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
666
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
667
+ * @returns a new LUV Color
668
+ */
669
+ static RGBtoLUV(
670
+ rgb: Color,
671
+ normalizedInput: boolean = false,
672
+ normalizedOutput: boolean = false,
673
+ ): Color {
674
+ const c = normalizedInput ? Color._denorm(rgb) : rgb;
675
+ return Color.XYZtoLUV(Color.RGBtoXYZ(c), false, normalizedOutput);
676
+ }
677
+
678
+ /**
679
+ * A static function to convert LUV to RGB.
680
+ * @param luv a LUV Color
681
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
682
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
683
+ * @returns a new RGB Color
684
+ */
685
+ static LUVtoRGB(
686
+ luv: Color,
687
+ normalizedInput: boolean = false,
688
+ normalizedOutput: boolean = false,
689
+ ): Color {
690
+ const c = normalizedInput ? Color._denorm(luv) : luv;
691
+ return Color.XYZtoRGB(Color.LUVtoXYZ(c), false, normalizedOutput);
692
+ }
693
+
694
+ /**
695
+ * A static function to convert RGB to XYZ.
696
+ * @param rgb a RGB Color
697
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
698
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
699
+ * @returns a new XYZ Color
700
+ */
701
+ static RGBtoXYZ(
702
+ rgb: Color,
703
+ normalizedInput: boolean = false,
704
+ normalizedOutput: boolean = false,
705
+ ): Color {
706
+ const c = !normalizedInput ? rgb.$normalize() : rgb.clone();
707
+
708
+ for (let i = 0; i < 3; i++) {
709
+ c[i] =
710
+ c[i] > 0.04045 ? Math.pow((c[i] + 0.055) / 1.055, 2.4) : c[i] / 12.92;
711
+ c[i] = c[i] * 100;
712
+ }
713
+
714
+ const cc = Color.xyz(
715
+ c[0] * 0.4124564 + c[1] * 0.3575761 + c[2] * 0.1804375,
716
+ c[0] * 0.2126729 + c[1] * 0.7151522 + c[2] * 0.072175,
717
+ c[0] * 0.0193339 + c[1] * 0.119192 + c[2] * 0.9503041,
718
+ rgb.alpha,
719
+ );
720
+
721
+ return normalizedOutput ? Color._normOut(cc) : cc;
722
+ }
723
+
724
+ /**
725
+ * A static function to convert XYZ to RGB.
726
+ * @param xyz a XYZ Color
727
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
728
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
729
+ * @returns a new RGB Color
730
+ */
731
+ static XYZtoRGB(
732
+ xyz: Color,
733
+ normalizedInput: boolean = false,
734
+ normalizedOutput: boolean = false,
735
+ ): Color {
736
+ const [x, y, z] = !normalizedInput ? xyz.$normalize() : xyz;
737
+
738
+ const rgb = [
739
+ x * 3.2406254773200533 +
740
+ y * -1.5372079722103187 +
741
+ z * -0.4986285986982479,
742
+ x * -0.9689307147293197 +
743
+ y * 1.8757560608852415 +
744
+ z * 0.041517523842953964,
745
+ x * 0.055710120445510616 +
746
+ y * -0.2040210505984867 +
747
+ z * 1.0569959422543882,
748
+ ];
749
+
750
+ // convert xyz to rgb. Note that not all colors are visible in rgb, so here we bound rgb between 0 to 1
751
+ for (let i = 0; i < 3; i++) {
752
+ rgb[i] =
753
+ rgb[i] > 0.0031308
754
+ ? 1.055 * Math.pow(rgb[i], 1 / 2.4) - 0.055
755
+ : 12.92 * rgb[i];
756
+ rgb[i] = Math.max(0, Math.min(1, rgb[i]));
757
+ if (!normalizedOutput) rgb[i] = Math.round(rgb[i] * 255);
758
+ }
759
+
760
+ const cc = Color.rgb(rgb[0], rgb[1], rgb[2], xyz.alpha);
761
+ if (normalizedOutput) cc.normalized = true;
762
+ return cc;
763
+ }
764
+
765
+ /**
766
+ * A static function to convert XYZ to LAB.
767
+ * @param xyz a XYZ Color
768
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
769
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
770
+ * @returns a new LAB Color
771
+ */
772
+ static XYZtoLAB(
773
+ xyz: Color,
774
+ normalizedInput: boolean = false,
775
+ normalizedOutput: boolean = false,
776
+ ): Color {
777
+ const c = normalizedInput ? Color._denorm(xyz) : xyz.clone();
778
+ const eps = 216 / 24389;
779
+ const kap = 24389 / 27;
780
+
781
+ // adjust for D65
782
+ c.divide(Color.D65);
783
+
784
+ const fn = (n: number) => (n > eps ? Math.cbrt(n) : (kap * n + 16) / 116);
785
+ const cy = fn(c[1]);
786
+
787
+ const cc = Color.lab(
788
+ 116 * cy - 16,
789
+ 500 * (fn(c[0]) - cy),
790
+ 200 * (cy - fn(c[2])),
791
+ xyz.alpha,
792
+ );
793
+ return normalizedOutput ? Color._normOut(cc) : cc;
794
+ }
795
+
796
+ /**
797
+ * A static function to convert LAB to XYZ.
798
+ * @param lab a LAB Color
799
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
800
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
801
+ * @returns a new XYZ Color
802
+ */
803
+ static LABtoXYZ(
804
+ lab: Color,
805
+ normalizedInput: boolean = false,
806
+ normalizedOutput: boolean = false,
807
+ ): Color {
808
+ const c = normalizedInput ? Color._denorm(lab) : lab;
809
+ const y = (c[0] + 16) / 116;
810
+ const x = c[1] / 500 + y;
811
+ const z = y - c[2] / 200;
812
+ const eps = 216 / 24389;
813
+ const kap = 24389 / 27;
814
+
815
+ const d = Color.D65;
816
+ const xxx = Math.pow(x, 3);
817
+ const zzz = Math.pow(z, 3);
818
+
819
+ // adjusted
820
+ const cc = Color.xyz(
821
+ d[0] * (xxx > eps ? xxx : (116 * x - 16) / kap),
822
+ d[1] * (c[0] > kap * eps ? Math.pow((c[0] + 16) / 116, 3) : c[0] / kap),
823
+ d[2] * (zzz > eps ? zzz : (116 * z - 16) / kap),
824
+ lab.alpha,
825
+ );
826
+
827
+ return normalizedOutput ? Color._normOut(cc) : cc;
828
+ }
829
+
830
+ /**
831
+ * A static function to convert XYZ to LUV.
832
+ * @param xyz a XYZ Color
833
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
834
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
835
+ * @returns a new LUV Color
836
+ */
837
+ static XYZtoLUV(
838
+ xyz: Color,
839
+ normalizedInput: boolean = false,
840
+ normalizedOutput: boolean = false,
841
+ ): Color {
842
+ let [x, y, z] = normalizedInput ? Color._denorm(xyz) : xyz;
843
+ // black has no chromaticity, so pin u'v' to avoid 0/0
844
+ const den = x + 15 * y + 3 * z;
845
+ const u = den === 0 ? 0 : (4 * x) / den;
846
+ const v = den === 0 ? 0 : (9 * y) / den;
847
+
848
+ const eps = 216 / 24389;
849
+ const kap = 24389 / 27;
850
+ y = y / 100;
851
+ const L = y > eps ? 116 * Math.cbrt(y) - 16 : kap * y;
852
+
853
+ const refU =
854
+ (4 * Color.D65[0]) /
855
+ (Color.D65[0] + 15 * Color.D65[1] + 3 * Color.D65[2]);
856
+ const refV =
857
+ (9 * Color.D65[1]) /
858
+ (Color.D65[0] + 15 * Color.D65[1] + 3 * Color.D65[2]);
859
+
860
+ const cc = Color.luv(
861
+ L,
862
+ 13 * L * (u - refU),
863
+ 13 * L * (v - refV),
864
+ xyz.alpha,
865
+ );
866
+ return normalizedOutput ? Color._normOut(cc) : cc;
867
+ }
868
+
869
+ /**
870
+ * A static function to convert LUV to XYZ.
871
+ * @param luv a LUV Color
872
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
873
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
874
+ * @returns a new XYZ Color
875
+ */
876
+ static LUVtoXYZ(
877
+ luv: Color,
878
+ normalizedInput: boolean = false,
879
+ normalizedOutput: boolean = false,
880
+ ): Color {
881
+ let [l, u, v] = normalizedInput ? Color._denorm(luv) : luv;
882
+
883
+ const eps = 216 / 24389;
884
+ const kap = 24389 / 27;
885
+ // black carries no chromaticity information
886
+ if (l === 0) {
887
+ const black = Color.xyz(0, 0, 0, luv.alpha);
888
+ return normalizedOutput ? Color._normOut(black) : black;
889
+ }
890
+
891
+ const fy = (l + 16) / 116;
892
+ let y = l > kap * eps ? fy * fy * fy : l / kap;
893
+
894
+ const refU =
895
+ (4 * Color.D65[0]) /
896
+ (Color.D65[0] + 15 * Color.D65[1] + 3 * Color.D65[2]);
897
+ const refV =
898
+ (9 * Color.D65[1]) /
899
+ (Color.D65[0] + 15 * Color.D65[1] + 3 * Color.D65[2]);
900
+
901
+ u = u / (13 * l) + refU;
902
+ v = v / (13 * l) + refV;
903
+
904
+ y = y * 100;
905
+ const x = (-1 * (9 * y * u)) / ((u - 4) * v - u * v);
906
+ const z = (9 * y - 15 * v * y - v * x) / (3 * v);
907
+
908
+ const cc = Color.xyz(x, y, z, luv.alpha);
909
+ return normalizedOutput ? Color._normOut(cc) : cc;
910
+ }
911
+
912
+ /**
913
+ * A static function to convert LAB to LCH.
914
+ * @param lab a LAB Color
915
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
916
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
917
+ * @returns a new LCH Color
918
+ */
919
+ static LABtoLCH(
920
+ lab: Color,
921
+ normalizedInput: boolean = false,
922
+ normalizedOutput: boolean = false,
923
+ ): Color {
924
+ const c = normalizedInput ? Color._denorm(lab) : lab;
925
+ const h = Geom.toDegree(Geom.boundRadian(Math.atan2(c[2], c[1]))); // 0 to 360 degrees
926
+ const cc = Color.lch(
927
+ c[0],
928
+ Math.sqrt(c[1] * c[1] + c[2] * c[2]),
929
+ h,
930
+ lab.alpha,
931
+ );
932
+ return normalizedOutput ? Color._normOut(cc) : cc;
933
+ }
934
+
935
+ /**
936
+ * A static function to convert LCH to LAB.
937
+ * @param lch a LCH Color
938
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
939
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
940
+ * @returns a new LAB Color
941
+ */
942
+ static LCHtoLAB(
943
+ lch: Color,
944
+ normalizedInput: boolean = false,
945
+ normalizedOutput: boolean = false,
946
+ ): Color {
947
+ const c = normalizedInput ? Color._denorm(lch) : lch;
948
+ const rad = Geom.toRadian(c[2]);
949
+ const cc = Color.lab(
950
+ c[0],
951
+ Math.cos(rad) * c[1],
952
+ Math.sin(rad) * c[1],
953
+ lch.alpha,
954
+ );
955
+ return normalizedOutput ? Color._normOut(cc) : cc;
956
+ }
957
+
958
+ /**
959
+ * A static function to convert RGB to OKLAB (Ottosson 2020, as specified in CSS Color 4). OKLAB improves on LAB's perceptual uniformity, especially hue stability in blues.
960
+ * @param rgb a RGB Color
961
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
962
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
963
+ * @returns a new OKLAB Color
964
+ */
965
+ static RGBtoOKLAB(
966
+ rgb: Color,
967
+ normalizedInput: boolean = false,
968
+ normalizedOutput: boolean = false,
969
+ ): Color {
970
+ const c = !normalizedInput ? rgb.$normalize() : rgb;
971
+ const lin = (v: number) =>
972
+ v > 0.04045 ? Math.pow((v + 0.055) / 1.055, 2.4) : v / 12.92;
973
+ const r = lin(c[0]);
974
+ const g = lin(c[1]);
975
+ const b = lin(c[2]);
976
+
977
+ // linear sRGB -> LMS cone response, then cube root
978
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
979
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
980
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
981
+
982
+ const cc = Color.oklab(
983
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
984
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
985
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
986
+ rgb.alpha,
987
+ );
988
+ return normalizedOutput ? Color._normOut(cc) : cc;
989
+ }
990
+
991
+ /**
992
+ * A static function to convert OKLAB to RGB. Out-of-gamut results are clamped to the sRGB range.
993
+ * @param oklab an OKLAB Color
994
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
995
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
996
+ * @returns a new RGB Color
997
+ */
998
+ static OKLABtoRGB(
999
+ oklab: Color,
1000
+ normalizedInput: boolean = false,
1001
+ normalizedOutput: boolean = false,
1002
+ ): Color {
1003
+ const c = normalizedInput ? Color._denorm(oklab) : oklab;
1004
+
1005
+ const l_ = c[0] + 0.3963377774 * c[1] + 0.2158037573 * c[2];
1006
+ const m_ = c[0] - 0.1055613458 * c[1] - 0.0638541728 * c[2];
1007
+ const s_ = c[0] - 0.0894841775 * c[1] - 1.291485548 * c[2];
1008
+ const l = l_ * l_ * l_;
1009
+ const m = m_ * m_ * m_;
1010
+ const s = s_ * s_ * s_;
1011
+
1012
+ const rgb = [
1013
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
1014
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
1015
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
1016
+ ];
1017
+
1018
+ for (let i = 0; i < 3; i++) {
1019
+ rgb[i] =
1020
+ rgb[i] > 0.0031308
1021
+ ? 1.055 * Math.pow(rgb[i], 1 / 2.4) - 0.055
1022
+ : 12.92 * rgb[i];
1023
+ rgb[i] = Math.max(0, Math.min(1, rgb[i]));
1024
+ if (!normalizedOutput) rgb[i] = Math.round(rgb[i] * 255);
1025
+ }
1026
+
1027
+ const cc = Color.rgb(rgb[0], rgb[1], rgb[2], oklab.alpha);
1028
+ if (normalizedOutput) cc.normalized = true;
1029
+ return cc;
1030
+ }
1031
+
1032
+ /**
1033
+ * A static function to convert RGB to OKLCH, the cylindrical form of OKLAB as specified in CSS Color 4.
1034
+ * @param rgb a RGB Color
1035
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
1036
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
1037
+ * @returns a new OKLCH Color
1038
+ */
1039
+ static RGBtoOKLCH(
1040
+ rgb: Color,
1041
+ normalizedInput: boolean = false,
1042
+ normalizedOutput: boolean = false,
1043
+ ): Color {
1044
+ const c = normalizedInput ? Color._denorm(rgb) : rgb;
1045
+ return Color.OKLABtoOKLCH(Color.RGBtoOKLAB(c), false, normalizedOutput);
1046
+ }
1047
+
1048
+ /**
1049
+ * A static function to convert OKLCH to RGB. Out-of-gamut results are clamped to the sRGB range.
1050
+ * @param oklch an OKLCH Color
1051
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
1052
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
1053
+ * @returns a new RGB Color
1054
+ */
1055
+ static OKLCHtoRGB(
1056
+ oklch: Color,
1057
+ normalizedInput: boolean = false,
1058
+ normalizedOutput: boolean = false,
1059
+ ): Color {
1060
+ const c = normalizedInput ? Color._denorm(oklch) : oklch;
1061
+ return Color.OKLABtoRGB(Color.OKLCHtoOKLAB(c), false, normalizedOutput);
1062
+ }
1063
+
1064
+ /**
1065
+ * A static function to convert OKLAB to OKLCH.
1066
+ * @param oklab an OKLAB Color
1067
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
1068
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
1069
+ * @returns a new OKLCH Color
1070
+ */
1071
+ static OKLABtoOKLCH(
1072
+ oklab: Color,
1073
+ normalizedInput: boolean = false,
1074
+ normalizedOutput: boolean = false,
1075
+ ): Color {
1076
+ const c = normalizedInput ? Color._denorm(oklab) : oklab;
1077
+ const h = Geom.toDegree(Geom.boundRadian(Math.atan2(c[2], c[1]))); // 0 to 360 degrees
1078
+ const cc = Color.oklch(
1079
+ c[0],
1080
+ Math.sqrt(c[1] * c[1] + c[2] * c[2]),
1081
+ h,
1082
+ oklab.alpha,
1083
+ );
1084
+ return normalizedOutput ? Color._normOut(cc) : cc;
1085
+ }
1086
+
1087
+ /**
1088
+ * A static function to convert OKLCH to OKLAB.
1089
+ * @param oklch an OKLCH Color
1090
+ * @param normalizedInput a boolean specifying whether input color is normalized. Default is not normalized: `false`.
1091
+ * @param normalizedOutput a boolean specifying whether output color shoud be normalized. Default is not normalized: `false`.
1092
+ * @returns a new OKLAB Color
1093
+ */
1094
+ static OKLCHtoOKLAB(
1095
+ oklch: Color,
1096
+ normalizedInput: boolean = false,
1097
+ normalizedOutput: boolean = false,
1098
+ ): Color {
1099
+ const c = normalizedInput ? Color._denorm(oklch) : oklch;
1100
+ const rad = Geom.toRadian(c[2]);
1101
+ const cc = Color.oklab(
1102
+ c[0],
1103
+ Math.cos(rad) * c[1],
1104
+ Math.sin(rad) * c[1],
1105
+ oklch.alpha,
1106
+ );
1107
+ return normalizedOutput ? Color._normOut(cc) : cc;
1108
+ }
1109
+ }