@js-draw/math 1.0.2 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -46,6 +46,25 @@ export default class Color4 {
46
46
  * ```
47
47
  */
48
48
  mix(other: Color4, fractionTo: number): Color4;
49
+ /**
50
+ * Ignoring this color's alpha component, returns a vector with components,
51
+ * $$
52
+ * \begin{pmatrix} \colorbox{#F44}{\tt r} \\ \colorbox{#4F4}{\tt g} \\ \colorbox{#44F}{\tt b} \end{pmatrix}
53
+ * $$
54
+ */
55
+ get rgb(): Vec3;
56
+ /**
57
+ * Returns the [relative luminance](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef)
58
+ * of this color in the sRGB color space.
59
+ *
60
+ * Ignores the alpha component.
61
+ */
62
+ relativeLuminance(): number;
63
+ /**
64
+ * Returns the [contrast ratio](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef)
65
+ * between `colorA` and `colorB`.
66
+ */
67
+ static contrastRatio(colorA: Color4, colorB: Color4): number;
49
68
  /**
50
69
  * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
51
70
  */
@@ -57,6 +76,27 @@ export default class Color4 {
57
76
  * The resultant hue is represented in radians and is thus in $[0, 2\pi]$.
58
77
  */
59
78
  asHSV(): Vec3;
79
+ /**
80
+ * Creates a new `Color4` from a representation [in $HSV$](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
81
+ *
82
+ * [Algorithm](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
83
+ *
84
+ * Note that hue must be given **in radians**. While non-standard, this is consistent with
85
+ * {@link asHSV}.
86
+ *
87
+ * `hue` and `value` should range from 0 to 1.
88
+ *
89
+ * @param hue $H \in [0, 2\pi]$
90
+ * @param saturation $S_V \in [0, 1]$
91
+ * @param value $V \in [0, 1]$
92
+ */
93
+ static fromHSV(hue: number, saturation: number, value: number): Color4;
94
+ /**
95
+ * Equivalent to `ofRGB(rgb.x, rgb.y, rgb.z)`.
96
+ *
97
+ * All components should be in the range `[0, 1]` (0 to 1 inclusive).
98
+ */
99
+ static fromRGBVector(rgb: Vec3, alpha?: number): Color4;
60
100
  private hexString;
61
101
  /**
62
102
  * @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
@@ -85,6 +85,9 @@ class Color4 {
85
85
  if (text === 'none' || text === 'transparent') {
86
86
  return Color4.transparent;
87
87
  }
88
+ if (text === '') {
89
+ return Color4.black;
90
+ }
88
91
  // rgba?: Match both rgb and rgba strings.
89
92
  // ([,0-9.]+): Match any string of only numeric, '.' and ',' characters.
90
93
  const rgbRegex = /^rgba?\(([,0-9.]+)\)$/i;
@@ -144,6 +147,49 @@ class Color4 {
144
147
  const fractionOfThis = 1 - fractionTo;
145
148
  return new Color4(this.r * fractionOfThis + other.r * fractionTo, this.g * fractionOfThis + other.g * fractionTo, this.b * fractionOfThis + other.b * fractionTo, this.a * fractionOfThis + other.a * fractionTo);
146
149
  }
150
+ /**
151
+ * Ignoring this color's alpha component, returns a vector with components,
152
+ * $$
153
+ * \begin{pmatrix} \colorbox{#F44}{\tt r} \\ \colorbox{#4F4}{\tt g} \\ \colorbox{#44F}{\tt b} \end{pmatrix}
154
+ * $$
155
+ */
156
+ get rgb() {
157
+ return Vec3_1.default.of(this.r, this.g, this.b);
158
+ }
159
+ /**
160
+ * Returns the [relative luminance](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef)
161
+ * of this color in the sRGB color space.
162
+ *
163
+ * Ignores the alpha component.
164
+ */
165
+ relativeLuminance() {
166
+ // References:
167
+ // - https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
168
+ // - https://stackoverflow.com/a/9733420
169
+ // Normalize the components, as per above
170
+ const components = [this.r, this.g, this.b].map(component => {
171
+ if (component < 0.03928) {
172
+ return component / 12.92;
173
+ }
174
+ else {
175
+ return Math.pow((component + 0.055) / 1.055, 2.4);
176
+ }
177
+ });
178
+ // From w3.org,
179
+ // > For the sRGB colorspace, the relative luminance of a color is
180
+ // > defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B
181
+ // where R, G, B are defined in components above.
182
+ return 0.2126 * components[0] + 0.7152 * components[1] + 0.0722 * components[2];
183
+ }
184
+ /**
185
+ * Returns the [contrast ratio](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef)
186
+ * between `colorA` and `colorB`.
187
+ */
188
+ static contrastRatio(colorA, colorB) {
189
+ const L1 = colorA.relativeLuminance();
190
+ const L2 = colorB.relativeLuminance();
191
+ return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
192
+ }
147
193
  /**
148
194
  * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
149
195
  */
@@ -229,6 +275,65 @@ class Color4 {
229
275
  const saturation = value > 0 ? chroma / value : 0;
230
276
  return Vec3_1.default.of(hue, saturation, value);
231
277
  }
278
+ /**
279
+ * Creates a new `Color4` from a representation [in $HSV$](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
280
+ *
281
+ * [Algorithm](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
282
+ *
283
+ * Note that hue must be given **in radians**. While non-standard, this is consistent with
284
+ * {@link asHSV}.
285
+ *
286
+ * `hue` and `value` should range from 0 to 1.
287
+ *
288
+ * @param hue $H \in [0, 2\pi]$
289
+ * @param saturation $S_V \in [0, 1]$
290
+ * @param value $V \in [0, 1]$
291
+ */
292
+ static fromHSV(hue, saturation, value) {
293
+ if (hue < 0) {
294
+ hue += Math.PI * 2;
295
+ }
296
+ hue %= Math.PI * 2;
297
+ // Clamp value and saturation to [0, 1]
298
+ value = Math.max(0, Math.min(1, value));
299
+ saturation = Math.max(0, Math.min(1, saturation));
300
+ // Formula from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
301
+ // Saturation can be thought of as scaled chroma. Unapply the scaling.
302
+ // See https://en.wikipedia.org/wiki/HSL_and_HSV#Saturation
303
+ const chroma = value * saturation;
304
+ // Determines which edge of the projected color cube
305
+ const huePrime = hue / (Math.PI / 3);
306
+ const secondLargestComponent = chroma * (1 - Math.abs((huePrime % 2) - 1));
307
+ let rgb;
308
+ if (huePrime < 1) {
309
+ rgb = [chroma, secondLargestComponent, 0];
310
+ }
311
+ else if (huePrime < 2) {
312
+ rgb = [secondLargestComponent, chroma, 0];
313
+ }
314
+ else if (huePrime < 3) {
315
+ rgb = [0, chroma, secondLargestComponent];
316
+ }
317
+ else if (huePrime < 4) {
318
+ rgb = [0, secondLargestComponent, chroma];
319
+ }
320
+ else if (huePrime < 5) {
321
+ rgb = [secondLargestComponent, 0, chroma];
322
+ }
323
+ else {
324
+ rgb = [chroma, 0, secondLargestComponent];
325
+ }
326
+ const adjustment = value - chroma;
327
+ return Color4.ofRGB(rgb[0] + adjustment, rgb[1] + adjustment, rgb[2] + adjustment);
328
+ }
329
+ /**
330
+ * Equivalent to `ofRGB(rgb.x, rgb.y, rgb.z)`.
331
+ *
332
+ * All components should be in the range `[0, 1]` (0 to 1 inclusive).
333
+ */
334
+ static fromRGBVector(rgb, alpha) {
335
+ return Color4.ofRGBA(rgb.x, rgb.y, rgb.z, alpha ?? 1);
336
+ }
232
337
  /**
233
338
  * @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
234
339
  *
@@ -46,6 +46,25 @@ export default class Color4 {
46
46
  * ```
47
47
  */
48
48
  mix(other: Color4, fractionTo: number): Color4;
49
+ /**
50
+ * Ignoring this color's alpha component, returns a vector with components,
51
+ * $$
52
+ * \begin{pmatrix} \colorbox{#F44}{\tt r} \\ \colorbox{#4F4}{\tt g} \\ \colorbox{#44F}{\tt b} \end{pmatrix}
53
+ * $$
54
+ */
55
+ get rgb(): Vec3;
56
+ /**
57
+ * Returns the [relative luminance](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef)
58
+ * of this color in the sRGB color space.
59
+ *
60
+ * Ignores the alpha component.
61
+ */
62
+ relativeLuminance(): number;
63
+ /**
64
+ * Returns the [contrast ratio](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef)
65
+ * between `colorA` and `colorB`.
66
+ */
67
+ static contrastRatio(colorA: Color4, colorB: Color4): number;
49
68
  /**
50
69
  * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
51
70
  */
@@ -57,6 +76,27 @@ export default class Color4 {
57
76
  * The resultant hue is represented in radians and is thus in $[0, 2\pi]$.
58
77
  */
59
78
  asHSV(): Vec3;
79
+ /**
80
+ * Creates a new `Color4` from a representation [in $HSV$](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
81
+ *
82
+ * [Algorithm](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
83
+ *
84
+ * Note that hue must be given **in radians**. While non-standard, this is consistent with
85
+ * {@link asHSV}.
86
+ *
87
+ * `hue` and `value` should range from 0 to 1.
88
+ *
89
+ * @param hue $H \in [0, 2\pi]$
90
+ * @param saturation $S_V \in [0, 1]$
91
+ * @param value $V \in [0, 1]$
92
+ */
93
+ static fromHSV(hue: number, saturation: number, value: number): Color4;
94
+ /**
95
+ * Equivalent to `ofRGB(rgb.x, rgb.y, rgb.z)`.
96
+ *
97
+ * All components should be in the range `[0, 1]` (0 to 1 inclusive).
98
+ */
99
+ static fromRGBVector(rgb: Vec3, alpha?: number): Color4;
60
100
  private hexString;
61
101
  /**
62
102
  * @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
@@ -79,6 +79,9 @@ class Color4 {
79
79
  if (text === 'none' || text === 'transparent') {
80
80
  return Color4.transparent;
81
81
  }
82
+ if (text === '') {
83
+ return Color4.black;
84
+ }
82
85
  // rgba?: Match both rgb and rgba strings.
83
86
  // ([,0-9.]+): Match any string of only numeric, '.' and ',' characters.
84
87
  const rgbRegex = /^rgba?\(([,0-9.]+)\)$/i;
@@ -138,6 +141,49 @@ class Color4 {
138
141
  const fractionOfThis = 1 - fractionTo;
139
142
  return new Color4(this.r * fractionOfThis + other.r * fractionTo, this.g * fractionOfThis + other.g * fractionTo, this.b * fractionOfThis + other.b * fractionTo, this.a * fractionOfThis + other.a * fractionTo);
140
143
  }
144
+ /**
145
+ * Ignoring this color's alpha component, returns a vector with components,
146
+ * $$
147
+ * \begin{pmatrix} \colorbox{#F44}{\tt r} \\ \colorbox{#4F4}{\tt g} \\ \colorbox{#44F}{\tt b} \end{pmatrix}
148
+ * $$
149
+ */
150
+ get rgb() {
151
+ return Vec3.of(this.r, this.g, this.b);
152
+ }
153
+ /**
154
+ * Returns the [relative luminance](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef)
155
+ * of this color in the sRGB color space.
156
+ *
157
+ * Ignores the alpha component.
158
+ */
159
+ relativeLuminance() {
160
+ // References:
161
+ // - https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
162
+ // - https://stackoverflow.com/a/9733420
163
+ // Normalize the components, as per above
164
+ const components = [this.r, this.g, this.b].map(component => {
165
+ if (component < 0.03928) {
166
+ return component / 12.92;
167
+ }
168
+ else {
169
+ return Math.pow((component + 0.055) / 1.055, 2.4);
170
+ }
171
+ });
172
+ // From w3.org,
173
+ // > For the sRGB colorspace, the relative luminance of a color is
174
+ // > defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B
175
+ // where R, G, B are defined in components above.
176
+ return 0.2126 * components[0] + 0.7152 * components[1] + 0.0722 * components[2];
177
+ }
178
+ /**
179
+ * Returns the [contrast ratio](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef)
180
+ * between `colorA` and `colorB`.
181
+ */
182
+ static contrastRatio(colorA, colorB) {
183
+ const L1 = colorA.relativeLuminance();
184
+ const L2 = colorB.relativeLuminance();
185
+ return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
186
+ }
141
187
  /**
142
188
  * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
143
189
  */
@@ -223,6 +269,65 @@ class Color4 {
223
269
  const saturation = value > 0 ? chroma / value : 0;
224
270
  return Vec3.of(hue, saturation, value);
225
271
  }
272
+ /**
273
+ * Creates a new `Color4` from a representation [in $HSV$](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
274
+ *
275
+ * [Algorithm](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
276
+ *
277
+ * Note that hue must be given **in radians**. While non-standard, this is consistent with
278
+ * {@link asHSV}.
279
+ *
280
+ * `hue` and `value` should range from 0 to 1.
281
+ *
282
+ * @param hue $H \in [0, 2\pi]$
283
+ * @param saturation $S_V \in [0, 1]$
284
+ * @param value $V \in [0, 1]$
285
+ */
286
+ static fromHSV(hue, saturation, value) {
287
+ if (hue < 0) {
288
+ hue += Math.PI * 2;
289
+ }
290
+ hue %= Math.PI * 2;
291
+ // Clamp value and saturation to [0, 1]
292
+ value = Math.max(0, Math.min(1, value));
293
+ saturation = Math.max(0, Math.min(1, saturation));
294
+ // Formula from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
295
+ // Saturation can be thought of as scaled chroma. Unapply the scaling.
296
+ // See https://en.wikipedia.org/wiki/HSL_and_HSV#Saturation
297
+ const chroma = value * saturation;
298
+ // Determines which edge of the projected color cube
299
+ const huePrime = hue / (Math.PI / 3);
300
+ const secondLargestComponent = chroma * (1 - Math.abs((huePrime % 2) - 1));
301
+ let rgb;
302
+ if (huePrime < 1) {
303
+ rgb = [chroma, secondLargestComponent, 0];
304
+ }
305
+ else if (huePrime < 2) {
306
+ rgb = [secondLargestComponent, chroma, 0];
307
+ }
308
+ else if (huePrime < 3) {
309
+ rgb = [0, chroma, secondLargestComponent];
310
+ }
311
+ else if (huePrime < 4) {
312
+ rgb = [0, secondLargestComponent, chroma];
313
+ }
314
+ else if (huePrime < 5) {
315
+ rgb = [secondLargestComponent, 0, chroma];
316
+ }
317
+ else {
318
+ rgb = [chroma, 0, secondLargestComponent];
319
+ }
320
+ const adjustment = value - chroma;
321
+ return Color4.ofRGB(rgb[0] + adjustment, rgb[1] + adjustment, rgb[2] + adjustment);
322
+ }
323
+ /**
324
+ * Equivalent to `ofRGB(rgb.x, rgb.y, rgb.z)`.
325
+ *
326
+ * All components should be in the range `[0, 1]` (0 to 1 inclusive).
327
+ */
328
+ static fromRGBVector(rgb, alpha) {
329
+ return Color4.ofRGBA(rgb.x, rgb.y, rgb.z, alpha ?? 1);
330
+ }
226
331
  /**
227
332
  * @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
228
333
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-draw/math",
3
- "version": "1.0.2",
3
+ "version": "1.2.1",
4
4
  "description": "A math library for js-draw. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -45,5 +45,5 @@
45
45
  "svg",
46
46
  "math"
47
47
  ],
48
- "gitHead": "f5a92284625b49b2ef6541bdafce1bd926c10441"
48
+ "gitHead": "0cb756d3150c8b33dd9a3217faa7d18229688f34"
49
49
  }
@@ -49,4 +49,46 @@ describe('Color4', () => {
49
49
  expect(Color4.ofRGB(0.5, 0.5, 0.5).asHSV()).objEq(Vec3.of(0, 0, 0.5));
50
50
  expect(Color4.ofRGB(0.5, 0.25, 0.5).asHSV()).objEq(Vec3.of(Math.PI * 5 / 3, 0.5, 0.5), 0.1);
51
51
  });
52
+
53
+ it('fromHSV(color.asHSV) should return the original color', () => {
54
+ const testColors = [
55
+ Color4.red, Color4.green, Color4.blue,
56
+ Color4.white, Color4.black,
57
+ ];
58
+
59
+ const testWithColor = (color: Color4) => {
60
+ expect(Color4.fromHSV(...color.asHSV().asArray())).objEq(color);
61
+ };
62
+
63
+ for (const color of testColors) {
64
+ testWithColor(color);
65
+ }
66
+
67
+ for (let i = 0; i <= 6; i++) {
68
+ testWithColor(Color4.fromHSV(i * Math.PI / 7, 0.5, 0.5));
69
+ testWithColor(Color4.fromHSV(i * Math.PI / 6, 0.5, 0.5));
70
+ }
71
+ });
72
+
73
+ it('.rgb should return a 3-component vector', () => {
74
+ expect(Color4.red.rgb).objEq(Vec3.of(1, 0, 0));
75
+ expect(Color4.green.rgb).objEq(Vec3.of(0, 1, 0));
76
+ expect(Color4.blue.rgb).objEq(Vec3.of(0, 0, 1));
77
+ });
78
+
79
+ it('should return correct contrast ratios', () => {
80
+ // Expected values from https://webaim.org/resources/contrastchecker/
81
+ const testCases: [ Color4, Color4, number ][] = [
82
+ [ Color4.white, Color4.black, 21 ],
83
+ [ Color4.fromHex('#FF0000'), Color4.black, 5.25 ],
84
+ [ Color4.fromHex('#FF0000'), Color4.fromHex('#0000FF'), 2.14 ],
85
+ [ Color4.fromHex('#300000'), Color4.fromHex('#003000'), 1.26 ],
86
+ [ Color4.fromHex('#300000'), Color4.fromHex('#003000'), 1.26 ],
87
+ [ Color4.fromHex('#D60000'), Color4.fromHex('#003000'), 2.71 ],
88
+ ];
89
+
90
+ for (const [ colorA, colorB, expectedContrast ] of testCases) {
91
+ expect(Color4.contrastRatio(colorA, colorB)).toBeCloseTo(expectedContrast, 1);
92
+ }
93
+ });
52
94
  });
package/src/Color4.ts CHANGED
@@ -93,6 +93,10 @@ export default class Color4 {
93
93
  return Color4.transparent;
94
94
  }
95
95
 
96
+ if (text === '') {
97
+ return Color4.black;
98
+ }
99
+
96
100
  // rgba?: Match both rgb and rgba strings.
97
101
  // ([,0-9.]+): Match any string of only numeric, '.' and ',' characters.
98
102
  const rgbRegex = /^rgba?\(([,0-9.]+)\)$/i;
@@ -170,6 +174,54 @@ export default class Color4 {
170
174
  );
171
175
  }
172
176
 
177
+ /**
178
+ * Ignoring this color's alpha component, returns a vector with components,
179
+ * $$
180
+ * \begin{pmatrix} \colorbox{#F44}{\tt r} \\ \colorbox{#4F4}{\tt g} \\ \colorbox{#44F}{\tt b} \end{pmatrix}
181
+ * $$
182
+ */
183
+ public get rgb() {
184
+ return Vec3.of(this.r, this.g, this.b);
185
+ }
186
+
187
+ /**
188
+ * Returns the [relative luminance](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef)
189
+ * of this color in the sRGB color space.
190
+ *
191
+ * Ignores the alpha component.
192
+ */
193
+ public relativeLuminance(): number {
194
+ // References:
195
+ // - https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
196
+ // - https://stackoverflow.com/a/9733420
197
+
198
+ // Normalize the components, as per above
199
+ const components = [ this.r, this.g, this.b ].map(component => {
200
+ if (component < 0.03928) {
201
+ return component / 12.92;
202
+ } else {
203
+ return Math.pow((component + 0.055) / 1.055, 2.4);
204
+ }
205
+ });
206
+
207
+ // From w3.org,
208
+ // > For the sRGB colorspace, the relative luminance of a color is
209
+ // > defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B
210
+ // where R, G, B are defined in components above.
211
+ return 0.2126 * components[0] + 0.7152 * components[1] + 0.0722 * components[2];
212
+ }
213
+
214
+ /**
215
+ * Returns the [contrast ratio](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef)
216
+ * between `colorA` and `colorB`.
217
+ */
218
+ public static contrastRatio(colorA: Color4, colorB: Color4): number {
219
+ const L1 = colorA.relativeLuminance();
220
+ const L2 = colorB.relativeLuminance();
221
+
222
+ return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
223
+ }
224
+
173
225
  /**
174
226
  * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
175
227
  */
@@ -263,6 +315,70 @@ export default class Color4 {
263
315
  return Vec3.of(hue, saturation, value);
264
316
  }
265
317
 
318
+ /**
319
+ * Creates a new `Color4` from a representation [in $HSV$](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
320
+ *
321
+ * [Algorithm](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
322
+ *
323
+ * Note that hue must be given **in radians**. While non-standard, this is consistent with
324
+ * {@link asHSV}.
325
+ *
326
+ * `hue` and `value` should range from 0 to 1.
327
+ *
328
+ * @param hue $H \in [0, 2\pi]$
329
+ * @param saturation $S_V \in [0, 1]$
330
+ * @param value $V \in [0, 1]$
331
+ */
332
+ public static fromHSV(hue: number, saturation: number, value: number) {
333
+ if (hue < 0) {
334
+ hue += Math.PI * 2;
335
+ }
336
+ hue %= Math.PI * 2;
337
+
338
+ // Clamp value and saturation to [0, 1]
339
+ value = Math.max(0, Math.min(1, value));
340
+ saturation = Math.max(0, Math.min(1, saturation));
341
+
342
+ // Formula from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
343
+
344
+ // Saturation can be thought of as scaled chroma. Unapply the scaling.
345
+ // See https://en.wikipedia.org/wiki/HSL_and_HSV#Saturation
346
+ const chroma = value * saturation;
347
+
348
+ // Determines which edge of the projected color cube
349
+ const huePrime = hue / (Math.PI / 3);
350
+
351
+ const secondLargestComponent = chroma * (1 - Math.abs((huePrime % 2) - 1));
352
+
353
+ let rgb;
354
+ if (huePrime < 1) {
355
+ rgb = [ chroma, secondLargestComponent, 0 ];
356
+ } else if (huePrime < 2) {
357
+ rgb = [ secondLargestComponent, chroma, 0 ];
358
+ } else if (huePrime < 3) {
359
+ rgb = [ 0, chroma, secondLargestComponent ];
360
+ } else if (huePrime < 4) {
361
+ rgb = [ 0, secondLargestComponent, chroma ];
362
+ } else if (huePrime < 5) {
363
+ rgb = [ secondLargestComponent, 0, chroma ];
364
+ } else {
365
+ rgb = [ chroma, 0, secondLargestComponent ];
366
+ }
367
+
368
+ const adjustment = value - chroma;
369
+ return Color4.ofRGB(rgb[0] + adjustment, rgb[1] + adjustment, rgb[2] + adjustment);
370
+ }
371
+
372
+
373
+ /**
374
+ * Equivalent to `ofRGB(rgb.x, rgb.y, rgb.z)`.
375
+ *
376
+ * All components should be in the range `[0, 1]` (0 to 1 inclusive).
377
+ */
378
+ public static fromRGBVector(rgb: Vec3, alpha?: number) {
379
+ return Color4.ofRGBA(rgb.x, rgb.y, rgb.z, alpha ?? 1);
380
+ }
381
+
266
382
  private hexString: string|null = null;
267
383
 
268
384
  /**