@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.
- package/dist/cjs/Color4.d.ts +40 -0
- package/dist/cjs/Color4.js +105 -0
- package/dist/mjs/Color4.d.ts +40 -0
- package/dist/mjs/Color4.mjs +105 -0
- package/package.json +2 -2
- package/src/Color4.test.ts +42 -0
- package/src/Color4.ts +116 -0
package/dist/cjs/Color4.d.ts
CHANGED
@@ -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`.
|
package/dist/cjs/Color4.js
CHANGED
@@ -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
|
*
|
package/dist/mjs/Color4.d.ts
CHANGED
@@ -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`.
|
package/dist/mjs/Color4.mjs
CHANGED
@@ -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.
|
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": "
|
48
|
+
"gitHead": "0cb756d3150c8b33dd9a3217faa7d18229688f34"
|
49
49
|
}
|
package/src/Color4.test.ts
CHANGED
@@ -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
|
/**
|