@js-draw/math 1.0.0 → 1.2.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/LICENSE +21 -0
- package/dist/cjs/Color4.d.ts +40 -0
- package/dist/cjs/Color4.js +102 -0
- package/dist/cjs/Color4.test.d.ts +1 -0
- package/dist/cjs/Mat33.test.d.ts +1 -0
- package/dist/cjs/Vec2.test.d.ts +1 -0
- package/dist/cjs/Vec3.test.d.ts +1 -0
- package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
- package/dist/cjs/rounding.test.d.ts +1 -0
- package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
- package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
- package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
- package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
- package/dist/mjs/Color4.d.ts +40 -0
- package/dist/mjs/Color4.mjs +102 -0
- package/dist/mjs/Color4.test.d.ts +1 -0
- package/dist/mjs/Mat33.test.d.ts +1 -0
- package/dist/mjs/Vec2.test.d.ts +1 -0
- package/dist/mjs/Vec3.test.d.ts +1 -0
- package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
- package/dist/mjs/rounding.test.d.ts +1 -0
- package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
- package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
- package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
- package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
- package/dist-test/test_imports/package-lock.json +13 -0
- package/dist-test/test_imports/package.json +12 -0
- package/dist-test/test_imports/test-imports.js +15 -0
- package/dist-test/test_imports/test-require.cjs +15 -0
- package/package.json +4 -3
- package/src/Color4.test.ts +94 -0
- package/src/Color4.ts +430 -0
- package/src/Mat33.test.ts +244 -0
- package/src/Mat33.ts +450 -0
- package/src/Vec2.test.ts +30 -0
- package/src/Vec2.ts +49 -0
- package/src/Vec3.test.ts +51 -0
- package/src/Vec3.ts +245 -0
- package/src/lib.ts +42 -0
- package/src/polynomial/solveQuadratic.test.ts +39 -0
- package/src/polynomial/solveQuadratic.ts +43 -0
- package/src/rounding.test.ts +65 -0
- package/src/rounding.ts +167 -0
- package/src/shapes/Abstract2DShape.ts +63 -0
- package/src/shapes/BezierJSWrapper.ts +93 -0
- package/src/shapes/CubicBezier.ts +35 -0
- package/src/shapes/LineSegment2.test.ts +99 -0
- package/src/shapes/LineSegment2.ts +232 -0
- package/src/shapes/Path.fromString.test.ts +223 -0
- package/src/shapes/Path.test.ts +309 -0
- package/src/shapes/Path.toString.test.ts +77 -0
- package/src/shapes/Path.ts +963 -0
- package/src/shapes/PointShape2D.ts +33 -0
- package/src/shapes/QuadraticBezier.test.ts +31 -0
- package/src/shapes/QuadraticBezier.ts +142 -0
- package/src/shapes/Rect2.test.ts +209 -0
- package/src/shapes/Rect2.ts +346 -0
- package/src/shapes/Triangle.test.ts +61 -0
- package/src/shapes/Triangle.ts +139 -0
package/src/Color4.ts
ADDED
@@ -0,0 +1,430 @@
|
|
1
|
+
import Vec3 from './Vec3';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Represents a color.
|
5
|
+
*
|
6
|
+
* @example
|
7
|
+
* ```ts,runnable,console
|
8
|
+
* import { Color4 } from '@js-draw/math';
|
9
|
+
*
|
10
|
+
* console.log('Red:', Color4.fromString('#f00'));
|
11
|
+
* console.log('Also red:', Color4.ofRGB(1, 0, 0), Color4.red);
|
12
|
+
* console.log('Mixing red and blue:', Color4.red.mix(Color4.blue, 0.5));
|
13
|
+
* console.log('To string:', Color4.orange.toHexString());
|
14
|
+
* ```
|
15
|
+
*/
|
16
|
+
export default class Color4 {
|
17
|
+
private constructor(
|
18
|
+
/** Red component. Should be in the range [0, 1]. */
|
19
|
+
public readonly r: number,
|
20
|
+
|
21
|
+
/** Green component. ${\tt g} \in [0, 1]$ */
|
22
|
+
public readonly g: number,
|
23
|
+
|
24
|
+
/** Blue component. ${\tt b} \in [0, 1]$ */
|
25
|
+
public readonly b: number,
|
26
|
+
|
27
|
+
/** Alpha/transparent component. ${\tt a} \in [0, 1]$. 0 = transparent */
|
28
|
+
public readonly a: number
|
29
|
+
) {
|
30
|
+
}
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Create a color from red, green, blue components. The color is fully opaque (`a = 1.0`).
|
34
|
+
*
|
35
|
+
* Each component should be in the range [0, 1].
|
36
|
+
*/
|
37
|
+
public static ofRGB(red: number, green: number, blue: number): Color4 {
|
38
|
+
return Color4.ofRGBA(red, green, blue, 1.0);
|
39
|
+
}
|
40
|
+
|
41
|
+
public static ofRGBA(red: number, green: number, blue: number, alpha: number): Color4 {
|
42
|
+
red = Math.max(0, Math.min(red, 1));
|
43
|
+
green = Math.max(0, Math.min(green, 1));
|
44
|
+
blue = Math.max(0, Math.min(blue, 1));
|
45
|
+
alpha = Math.max(0, Math.min(alpha, 1));
|
46
|
+
|
47
|
+
return new Color4(red, green, blue, alpha);
|
48
|
+
}
|
49
|
+
|
50
|
+
public static fromHex(hexString: string): Color4 {
|
51
|
+
// Remove starting '#' (if present)
|
52
|
+
hexString = (hexString.match(/^[#]?(.*)$/) ?? [])[1];
|
53
|
+
hexString = hexString.toUpperCase();
|
54
|
+
|
55
|
+
if (!hexString.match(/^[0-9A-F]+$/)) {
|
56
|
+
throw new Error(`${hexString} is not in a valid format.`);
|
57
|
+
}
|
58
|
+
|
59
|
+
// RGBA or RGB
|
60
|
+
if (hexString.length === 3 || hexString.length === 4) {
|
61
|
+
// Each character is a component
|
62
|
+
const components = hexString.split('');
|
63
|
+
|
64
|
+
// Convert to RRGGBBAA or RRGGBB format
|
65
|
+
hexString = components.map(component => `${component}0`).join('');
|
66
|
+
}
|
67
|
+
|
68
|
+
if (hexString.length === 6) {
|
69
|
+
// Alpha component
|
70
|
+
hexString += 'FF';
|
71
|
+
}
|
72
|
+
|
73
|
+
const components: number[] = [];
|
74
|
+
for (let i = 2; i <= hexString.length; i += 2) {
|
75
|
+
const chunk = hexString.substring(i - 2, i);
|
76
|
+
components.push(parseInt(chunk, 16) / 255);
|
77
|
+
}
|
78
|
+
|
79
|
+
if (components.length !== 4) {
|
80
|
+
throw new Error(`Unable to parse ${hexString}: Wrong number of components.`);
|
81
|
+
}
|
82
|
+
|
83
|
+
return Color4.ofRGBA(components[0], components[1], components[2], components[3]);
|
84
|
+
}
|
85
|
+
|
86
|
+
/** Like fromHex, but can handle additional colors if an `HTMLCanvasElement` is available. */
|
87
|
+
public static fromString(text: string): Color4 {
|
88
|
+
if (text.startsWith('#')) {
|
89
|
+
return Color4.fromHex(text);
|
90
|
+
}
|
91
|
+
|
92
|
+
if (text === 'none' || text === 'transparent') {
|
93
|
+
return Color4.transparent;
|
94
|
+
}
|
95
|
+
|
96
|
+
// rgba?: Match both rgb and rgba strings.
|
97
|
+
// ([,0-9.]+): Match any string of only numeric, '.' and ',' characters.
|
98
|
+
const rgbRegex = /^rgba?\(([,0-9.]+)\)$/i;
|
99
|
+
const rgbMatch = text.replace(/\s*/g, '').match(rgbRegex);
|
100
|
+
|
101
|
+
if (rgbMatch) {
|
102
|
+
const componentsListStr = rgbMatch[1];
|
103
|
+
const componentsList = JSON.parse(`[ ${componentsListStr} ]`);
|
104
|
+
|
105
|
+
if (componentsList.length === 3) {
|
106
|
+
return Color4.ofRGB(
|
107
|
+
componentsList[0] / 255, componentsList[1] / 255, componentsList[2] / 255
|
108
|
+
);
|
109
|
+
} else if (componentsList.length === 4) {
|
110
|
+
return Color4.ofRGBA(
|
111
|
+
componentsList[0] / 255, componentsList[1] / 255, componentsList[2] / 255, componentsList[3]
|
112
|
+
);
|
113
|
+
} else {
|
114
|
+
throw new Error(`RGB string, ${text}, has wrong number of components: ${componentsList.length}`);
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
// Otherwise, try to use an HTMLCanvasElement to determine the color.
|
119
|
+
// Note: We may be unable to create an HTMLCanvasElement if running as a unit test.
|
120
|
+
const canvas = document.createElement('canvas');
|
121
|
+
canvas.width = 1;
|
122
|
+
canvas.height = 1;
|
123
|
+
|
124
|
+
const ctx = canvas.getContext('2d')!;
|
125
|
+
ctx.fillStyle = text;
|
126
|
+
ctx.fillRect(0, 0, 1, 1);
|
127
|
+
|
128
|
+
const data = ctx.getImageData(0, 0, 1, 1);
|
129
|
+
const red = data.data[0] / 255;
|
130
|
+
const green = data.data[1] / 255;
|
131
|
+
const blue = data.data[2] / 255;
|
132
|
+
const alpha = data.data[3] / 255;
|
133
|
+
|
134
|
+
return Color4.ofRGBA(red, green, blue, alpha);
|
135
|
+
}
|
136
|
+
|
137
|
+
/** @returns true if `this` and `other` are approximately equal. */
|
138
|
+
public eq(other: Color4|null|undefined): boolean {
|
139
|
+
if (other == null) {
|
140
|
+
return false;
|
141
|
+
}
|
142
|
+
|
143
|
+
// If both completely transparent,
|
144
|
+
if (this.a === 0 && other.a === 0) {
|
145
|
+
return true;
|
146
|
+
}
|
147
|
+
|
148
|
+
return this.toHexString() === other.toHexString();
|
149
|
+
}
|
150
|
+
|
151
|
+
/**
|
152
|
+
* If `fractionTo` is not in the range $[0, 1]$, it will be clamped to the nearest number
|
153
|
+
* in that range. For example, `a.mix(b, -1)` is equivalent to `a.mix(b, 0)`.
|
154
|
+
*
|
155
|
+
* @returns a color `fractionTo` of the way from this color to `other`.
|
156
|
+
*
|
157
|
+
* @example
|
158
|
+
* ```ts
|
159
|
+
* Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 1, 0), 0.1) // -> Color4(0.9, 0.1, 0)
|
160
|
+
* ```
|
161
|
+
*/
|
162
|
+
public mix(other: Color4, fractionTo: number): Color4 {
|
163
|
+
fractionTo = Math.min(Math.max(fractionTo, 0), 1);
|
164
|
+
const fractionOfThis = 1 - fractionTo;
|
165
|
+
return new Color4(
|
166
|
+
this.r * fractionOfThis + other.r * fractionTo,
|
167
|
+
this.g * fractionOfThis + other.g * fractionTo,
|
168
|
+
this.b * fractionOfThis + other.b * fractionTo,
|
169
|
+
this.a * fractionOfThis + other.a * fractionTo,
|
170
|
+
);
|
171
|
+
}
|
172
|
+
|
173
|
+
/**
|
174
|
+
* Ignoring this color's alpha component, returns a vector with components,
|
175
|
+
* $$
|
176
|
+
* \begin{pmatrix} \colorbox{#F44}{\tt r} \\ \colorbox{#4F4}{\tt g} \\ \colorbox{#44F}{\tt b} \end{pmatrix}
|
177
|
+
* $$
|
178
|
+
*/
|
179
|
+
public get rgb() {
|
180
|
+
return Vec3.of(this.r, this.g, this.b);
|
181
|
+
}
|
182
|
+
|
183
|
+
/**
|
184
|
+
* Returns the [relative luminance](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef)
|
185
|
+
* of this color in the sRGB color space.
|
186
|
+
*
|
187
|
+
* Ignores the alpha component.
|
188
|
+
*/
|
189
|
+
public relativeLuminance(): number {
|
190
|
+
// References:
|
191
|
+
// - https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
192
|
+
// - https://stackoverflow.com/a/9733420
|
193
|
+
|
194
|
+
// Normalize the components, as per above
|
195
|
+
const components = [ this.r, this.g, this.b ].map(component => {
|
196
|
+
if (component < 0.03928) {
|
197
|
+
return component / 12.92;
|
198
|
+
} else {
|
199
|
+
return Math.pow((component + 0.055) / 1.055, 2.4);
|
200
|
+
}
|
201
|
+
});
|
202
|
+
|
203
|
+
// From w3.org,
|
204
|
+
// > For the sRGB colorspace, the relative luminance of a color is
|
205
|
+
// > defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B
|
206
|
+
// where R, G, B are defined in components above.
|
207
|
+
return 0.2126 * components[0] + 0.7152 * components[1] + 0.0722 * components[2];
|
208
|
+
}
|
209
|
+
|
210
|
+
/**
|
211
|
+
* Returns the [contrast ratio](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef)
|
212
|
+
* between `colorA` and `colorB`.
|
213
|
+
*/
|
214
|
+
public static contrastRatio(colorA: Color4, colorB: Color4): number {
|
215
|
+
const L1 = colorA.relativeLuminance();
|
216
|
+
const L2 = colorB.relativeLuminance();
|
217
|
+
|
218
|
+
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
|
219
|
+
}
|
220
|
+
|
221
|
+
/**
|
222
|
+
* @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
|
223
|
+
*/
|
224
|
+
public static average(colors: Color4[]) {
|
225
|
+
let averageA = 0;
|
226
|
+
let averageR = 0;
|
227
|
+
let averageG = 0;
|
228
|
+
let averageB = 0;
|
229
|
+
|
230
|
+
for (const color of colors) {
|
231
|
+
averageA += color.a;
|
232
|
+
averageR += color.r;
|
233
|
+
averageG += color.g;
|
234
|
+
averageB += color.b;
|
235
|
+
}
|
236
|
+
|
237
|
+
if (colors.length > 0) {
|
238
|
+
averageA /= colors.length;
|
239
|
+
averageR /= colors.length;
|
240
|
+
averageG /= colors.length;
|
241
|
+
averageB /= colors.length;
|
242
|
+
}
|
243
|
+
|
244
|
+
return new Color4(averageR, averageG, averageB, averageA);
|
245
|
+
}
|
246
|
+
|
247
|
+
/**
|
248
|
+
* Converts to (hue, saturation, value).
|
249
|
+
* See also https://en.wikipedia.org/wiki/HSL_and_HSV#General_approach
|
250
|
+
*
|
251
|
+
* The resultant hue is represented in radians and is thus in $[0, 2\pi]$.
|
252
|
+
*/
|
253
|
+
public asHSV(): Vec3 {
|
254
|
+
// Ref: https://en.wikipedia.org/wiki/HSL_and_HSV#General_approach
|
255
|
+
//
|
256
|
+
// HUE:
|
257
|
+
// First, consider the unit cube. Rotate it such that one vertex is at the origin
|
258
|
+
// of a plane and its three neighboring vertices are equidistant from that plane:
|
259
|
+
//
|
260
|
+
// /\
|
261
|
+
// / | \
|
262
|
+
// 2 / 3 \ 1
|
263
|
+
// \ | /
|
264
|
+
// \ | /
|
265
|
+
// . \/ .
|
266
|
+
//
|
267
|
+
// .
|
268
|
+
//
|
269
|
+
// Let z be up and (x, y, 0) be in the plane.
|
270
|
+
//
|
271
|
+
// Label vectors 1,2,3 with R, G, and B, respectively. Let R's projection into the plane
|
272
|
+
// lie along the x axis.
|
273
|
+
//
|
274
|
+
// Because R is a unit vector and R, G, B are equidistant from the plane, they must
|
275
|
+
// form 30-60-90 triangles, which have side lengths proportional to (1, √3, 2)
|
276
|
+
//
|
277
|
+
// /|
|
278
|
+
// 1/ | (√3)/2
|
279
|
+
// / |
|
280
|
+
// 1/2
|
281
|
+
//
|
282
|
+
const minComponent = Math.min(this.r, this.g, this.b);
|
283
|
+
const maxComponent = Math.max(this.r, this.g, this.b);
|
284
|
+
const chroma = maxComponent - minComponent;
|
285
|
+
|
286
|
+
let hue;
|
287
|
+
|
288
|
+
// See https://en.wikipedia.org/wiki/HSL_and_HSV#General_approach
|
289
|
+
if (chroma === 0) {
|
290
|
+
hue = 0;
|
291
|
+
} else if (this.r >= this.g && this.r >= this.b) {
|
292
|
+
hue = ((this.g - this.b) / chroma) % 6;
|
293
|
+
} else if (this.g >= this.r && this.g >= this.b) {
|
294
|
+
hue = (this.b - this.r) / chroma + 2;
|
295
|
+
} else {
|
296
|
+
hue = (this.r - this.g) / chroma + 4;
|
297
|
+
}
|
298
|
+
|
299
|
+
// Convert to degree representation, then to radians.
|
300
|
+
hue *= 60;
|
301
|
+
hue *= Math.PI / 180;
|
302
|
+
|
303
|
+
// Ensure positivity.
|
304
|
+
if (hue < 0) {
|
305
|
+
hue += Math.PI * 2;
|
306
|
+
}
|
307
|
+
|
308
|
+
const value = maxComponent;
|
309
|
+
const saturation = value > 0 ? chroma / value : 0;
|
310
|
+
|
311
|
+
return Vec3.of(hue, saturation, value);
|
312
|
+
}
|
313
|
+
|
314
|
+
/**
|
315
|
+
* Creates a new `Color4` from a representation [in $HSV$](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
|
316
|
+
*
|
317
|
+
* [Algorithm](https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB).
|
318
|
+
*
|
319
|
+
* Note that hue must be given **in radians**. While non-standard, this is consistent with
|
320
|
+
* {@link asHSV}.
|
321
|
+
*
|
322
|
+
* `hue` and `value` should range from 0 to 1.
|
323
|
+
*
|
324
|
+
* @param hue $H \in [0, 2\pi]$
|
325
|
+
* @param saturation $S_V \in [0, 1]$
|
326
|
+
* @param value $V \in [0, 1]$
|
327
|
+
*/
|
328
|
+
public static fromHSV(hue: number, saturation: number, value: number) {
|
329
|
+
if (hue < 0) {
|
330
|
+
hue += Math.PI * 2;
|
331
|
+
}
|
332
|
+
hue %= Math.PI * 2;
|
333
|
+
|
334
|
+
// Clamp value and saturation to [0, 1]
|
335
|
+
value = Math.max(0, Math.min(1, value));
|
336
|
+
saturation = Math.max(0, Math.min(1, saturation));
|
337
|
+
|
338
|
+
// Formula from https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
|
339
|
+
|
340
|
+
// Saturation can be thought of as scaled chroma. Unapply the scaling.
|
341
|
+
// See https://en.wikipedia.org/wiki/HSL_and_HSV#Saturation
|
342
|
+
const chroma = value * saturation;
|
343
|
+
|
344
|
+
// Determines which edge of the projected color cube
|
345
|
+
const huePrime = hue / (Math.PI / 3);
|
346
|
+
|
347
|
+
const secondLargestComponent = chroma * (1 - Math.abs((huePrime % 2) - 1));
|
348
|
+
|
349
|
+
let rgb;
|
350
|
+
if (huePrime < 1) {
|
351
|
+
rgb = [ chroma, secondLargestComponent, 0 ];
|
352
|
+
} else if (huePrime < 2) {
|
353
|
+
rgb = [ secondLargestComponent, chroma, 0 ];
|
354
|
+
} else if (huePrime < 3) {
|
355
|
+
rgb = [ 0, chroma, secondLargestComponent ];
|
356
|
+
} else if (huePrime < 4) {
|
357
|
+
rgb = [ 0, secondLargestComponent, chroma ];
|
358
|
+
} else if (huePrime < 5) {
|
359
|
+
rgb = [ secondLargestComponent, 0, chroma ];
|
360
|
+
} else {
|
361
|
+
rgb = [ chroma, 0, secondLargestComponent ];
|
362
|
+
}
|
363
|
+
|
364
|
+
const adjustment = value - chroma;
|
365
|
+
return Color4.ofRGB(rgb[0] + adjustment, rgb[1] + adjustment, rgb[2] + adjustment);
|
366
|
+
}
|
367
|
+
|
368
|
+
|
369
|
+
/**
|
370
|
+
* Equivalent to `ofRGB(rgb.x, rgb.y, rgb.z)`.
|
371
|
+
*
|
372
|
+
* All components should be in the range `[0, 1]` (0 to 1 inclusive).
|
373
|
+
*/
|
374
|
+
public static fromRGBVector(rgb: Vec3, alpha?: number) {
|
375
|
+
return Color4.ofRGBA(rgb.x, rgb.y, rgb.z, alpha ?? 1);
|
376
|
+
}
|
377
|
+
|
378
|
+
private hexString: string|null = null;
|
379
|
+
|
380
|
+
/**
|
381
|
+
* @returns a hexadecimal color string representation of `this`, in the form `#rrggbbaa`.
|
382
|
+
*
|
383
|
+
* @example
|
384
|
+
* ```
|
385
|
+
* Color4.red.toHexString(); // -> #ff0000ff
|
386
|
+
* ```
|
387
|
+
*/
|
388
|
+
public toHexString(): string {
|
389
|
+
if (this.hexString) {
|
390
|
+
return this.hexString;
|
391
|
+
}
|
392
|
+
|
393
|
+
const componentToHex = (component: number): string => {
|
394
|
+
const res = Math.round(255 * component).toString(16);
|
395
|
+
|
396
|
+
if (res.length === 1) {
|
397
|
+
return `0${res}`;
|
398
|
+
}
|
399
|
+
return res;
|
400
|
+
};
|
401
|
+
|
402
|
+
const alpha = componentToHex(this.a);
|
403
|
+
const red = componentToHex(this.r);
|
404
|
+
const green = componentToHex(this.g);
|
405
|
+
const blue = componentToHex(this.b);
|
406
|
+
if (alpha === 'ff') {
|
407
|
+
return `#${red}${green}${blue}`;
|
408
|
+
}
|
409
|
+
this.hexString = `#${red}${green}${blue}${alpha}`;
|
410
|
+
return this.hexString;
|
411
|
+
}
|
412
|
+
|
413
|
+
public toString() {
|
414
|
+
return this.toHexString();
|
415
|
+
}
|
416
|
+
|
417
|
+
public static transparent = Color4.ofRGBA(0, 0, 0, 0);
|
418
|
+
public static red = Color4.ofRGB(1.0, 0.0, 0.0);
|
419
|
+
public static orange = Color4.ofRGB(1.0, 0.65, 0.0);
|
420
|
+
public static green = Color4.ofRGB(0.0, 1.0, 0.0);
|
421
|
+
public static blue = Color4.ofRGB(0.0, 0.0, 1.0);
|
422
|
+
public static purple = Color4.ofRGB(0.5, 0.2, 0.5);
|
423
|
+
public static yellow = Color4.ofRGB(1, 1, 0.1);
|
424
|
+
public static clay = Color4.ofRGB(0.8, 0.4, 0.2);
|
425
|
+
public static black = Color4.ofRGB(0, 0, 0);
|
426
|
+
public static gray = Color4.ofRGB(0.5, 0.5, 0.5);
|
427
|
+
public static white = Color4.ofRGB(1, 1, 1);
|
428
|
+
}
|
429
|
+
|
430
|
+
export { Color4 };
|
@@ -0,0 +1,244 @@
|
|
1
|
+
import Mat33 from './Mat33';
|
2
|
+
import { Point2, Vec2 } from './Vec2';
|
3
|
+
import Vec3 from './Vec3';
|
4
|
+
|
5
|
+
|
6
|
+
describe('Mat33 tests', () => {
|
7
|
+
it('equality', () => {
|
8
|
+
expect(Mat33.identity).objEq(Mat33.identity);
|
9
|
+
expect(new Mat33(
|
10
|
+
0.1, 0.2, 0.3,
|
11
|
+
0.4, 0.5, 0.6,
|
12
|
+
0.7, 0.8, -0.9
|
13
|
+
)).objEq(new Mat33(
|
14
|
+
0.2, 0.1, 0.4,
|
15
|
+
0.5, 0.5, 0.7,
|
16
|
+
0.7, 0.8, -0.9
|
17
|
+
), 0.2);
|
18
|
+
});
|
19
|
+
|
20
|
+
it('transposition', () => {
|
21
|
+
expect(Mat33.identity.transposed()).objEq(Mat33.identity);
|
22
|
+
expect(new Mat33(
|
23
|
+
1, 2, 0,
|
24
|
+
0, 0, 0,
|
25
|
+
0, 1, 0
|
26
|
+
).transposed()).objEq(new Mat33(
|
27
|
+
1, 0, 0,
|
28
|
+
2, 0, 1,
|
29
|
+
0, 0, 0
|
30
|
+
));
|
31
|
+
});
|
32
|
+
|
33
|
+
it('multiplication', () => {
|
34
|
+
const M = new Mat33(
|
35
|
+
1, 2, 3,
|
36
|
+
4, 5, 6,
|
37
|
+
7, 8, 9
|
38
|
+
);
|
39
|
+
|
40
|
+
expect(Mat33.identity.rightMul(Mat33.identity)).objEq(Mat33.identity);
|
41
|
+
expect(M.rightMul(Mat33.identity)).objEq(M);
|
42
|
+
expect(M.rightMul(new Mat33(
|
43
|
+
1, 0, 0,
|
44
|
+
0, 2, 0,
|
45
|
+
0, 0, 1
|
46
|
+
))).objEq(new Mat33(
|
47
|
+
1, 4, 3,
|
48
|
+
4, 10, 6,
|
49
|
+
7, 16, 9
|
50
|
+
));
|
51
|
+
expect(M.rightMul(new Mat33(
|
52
|
+
2, 0, 1,
|
53
|
+
0, 1, 0,
|
54
|
+
0, 0, 3
|
55
|
+
))).objEq(new Mat33(
|
56
|
+
2, 2, 10,
|
57
|
+
8, 5, 22,
|
58
|
+
14, 8, 34
|
59
|
+
));
|
60
|
+
});
|
61
|
+
|
62
|
+
it('the inverse of the identity matrix should be the identity matrix', () => {
|
63
|
+
const fuzz = 0.01;
|
64
|
+
expect(Mat33.identity.inverse()).objEq(Mat33.identity, fuzz);
|
65
|
+
|
66
|
+
const M = new Mat33(
|
67
|
+
1, 2, 3,
|
68
|
+
4, 1, 0,
|
69
|
+
2, 3, 0
|
70
|
+
);
|
71
|
+
expect(M.inverse().rightMul(M)).objEq(Mat33.identity, fuzz);
|
72
|
+
});
|
73
|
+
|
74
|
+
it('90 degree z-rotation matricies should rotate 90 degrees counter clockwise', () => {
|
75
|
+
const fuzz = 0.001;
|
76
|
+
|
77
|
+
const M = Mat33.zRotation(Math.PI / 2);
|
78
|
+
const rotated = M.transformVec2(Vec2.unitX);
|
79
|
+
expect(rotated).objEq(Vec2.unitY, fuzz);
|
80
|
+
expect(M.transformVec2(rotated)).objEq(Vec2.unitX.times(-1), fuzz);
|
81
|
+
});
|
82
|
+
|
83
|
+
it('z-rotation matricies should preserve the given origin', () => {
|
84
|
+
const testPairs: Array<[number, Vec2]> = [
|
85
|
+
[ Math.PI / 2, Vec2.zero ],
|
86
|
+
[ -Math.PI / 2, Vec2.zero ],
|
87
|
+
[ -Math.PI / 2, Vec2.of(10, 10) ],
|
88
|
+
];
|
89
|
+
|
90
|
+
for (const [ angle, center ] of testPairs) {
|
91
|
+
expect(Mat33.zRotation(angle, center).transformVec2(center)).objEq(center);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
|
95
|
+
it('translation matricies should translate Vec2s', () => {
|
96
|
+
const fuzz = 0.01;
|
97
|
+
|
98
|
+
const M = Mat33.translation(Vec2.of(1, -4));
|
99
|
+
expect(M.transformVec2(Vec2.of(0, 0))).objEq(Vec2.of(1, -4), fuzz);
|
100
|
+
expect(M.transformVec2(Vec2.of(-1, 3))).objEq(Vec2.of(0, -1), fuzz);
|
101
|
+
});
|
102
|
+
|
103
|
+
it('scaling matricies should scale about the provided center', () => {
|
104
|
+
const fuzz = 0.01;
|
105
|
+
|
106
|
+
const center = Vec2.of(1, -4);
|
107
|
+
const M = Mat33.scaling2D(2, center);
|
108
|
+
expect(M.transformVec2(center)).objEq(center, fuzz);
|
109
|
+
expect(M.transformVec2(Vec2.of(0, 0))).objEq(Vec2.of(-1, 4), fuzz);
|
110
|
+
});
|
111
|
+
|
112
|
+
it('calling inverse on singular matricies should result in the identity matrix', () => {
|
113
|
+
const fuzz = 0.001;
|
114
|
+
const singularMat = Mat33.ofRows(
|
115
|
+
Vec3.of(0, 0, 1),
|
116
|
+
Vec3.of(0, 1, 0),
|
117
|
+
Vec3.of(0, 1, 1)
|
118
|
+
);
|
119
|
+
expect(singularMat.invertable()).toBe(false);
|
120
|
+
expect(singularMat.inverse()).objEq(Mat33.identity, fuzz);
|
121
|
+
});
|
122
|
+
|
123
|
+
it('z-rotation matricies should be invertable', () => {
|
124
|
+
const fuzz = 0.01;
|
125
|
+
const M = Mat33.zRotation(-0.2617993877991494, Vec2.of(481, 329.5));
|
126
|
+
expect(
|
127
|
+
M.inverse().transformVec2(M.transformVec2(Vec2.unitX))
|
128
|
+
).objEq(Vec2.unitX, fuzz);
|
129
|
+
expect(M.invertable());
|
130
|
+
|
131
|
+
const starterTransform = new Mat33(
|
132
|
+
-0.2588190451025205, -0.9659258262890688, 923.7645204565603,
|
133
|
+
0.9659258262890688, -0.2588190451025205, -49.829447083761465,
|
134
|
+
0, 0, 1
|
135
|
+
);
|
136
|
+
expect(starterTransform.invertable()).toBe(true);
|
137
|
+
|
138
|
+
const fullTransform = starterTransform.rightMul(M);
|
139
|
+
const fullTransformInverse = fullTransform.inverse();
|
140
|
+
expect(fullTransform.invertable()).toBe(true);
|
141
|
+
|
142
|
+
expect(
|
143
|
+
fullTransformInverse.rightMul(fullTransform)
|
144
|
+
).objEq(Mat33.identity, fuzz);
|
145
|
+
|
146
|
+
expect(
|
147
|
+
fullTransform.transformVec2(fullTransformInverse.transformVec2(Vec2.unitX))
|
148
|
+
).objEq(Vec2.unitX, fuzz);
|
149
|
+
|
150
|
+
expect(
|
151
|
+
fullTransformInverse.transformVec2(fullTransform.transformVec2(Vec2.unitX))
|
152
|
+
).objEq(Vec2.unitX, fuzz);
|
153
|
+
});
|
154
|
+
|
155
|
+
it('z-rotation matrix inverses should undo the z-rotation', () => {
|
156
|
+
const testCases: Array<[ number, Point2 ]> = [
|
157
|
+
[ Math.PI / 2, Vec2.zero ],
|
158
|
+
[ Math.PI, Vec2.of(1, 1) ],
|
159
|
+
[ -Math.PI, Vec2.of(1, 1) ],
|
160
|
+
[ -Math.PI * 2, Vec2.of(1, 1) ],
|
161
|
+
[ -Math.PI * 2, Vec2.of(123, 456) ],
|
162
|
+
[ -Math.PI / 4, Vec2.of(123, 456) ],
|
163
|
+
[ 0.1, Vec2.of(1, 2) ],
|
164
|
+
];
|
165
|
+
|
166
|
+
const fuzz = 0.00001;
|
167
|
+
for (const [ angle, center ] of testCases) {
|
168
|
+
const mat = Mat33.zRotation(angle, center);
|
169
|
+
expect(mat.inverse().rightMul(mat)).objEq(Mat33.identity, fuzz);
|
170
|
+
expect(mat.rightMul(mat.inverse())).objEq(Mat33.identity, fuzz);
|
171
|
+
}
|
172
|
+
});
|
173
|
+
|
174
|
+
it('z-rotation should preserve given origin', () => {
|
175
|
+
const testCases: Array<[ number, Point2 ]> = [
|
176
|
+
[ 6.205048847547065, Vec2.of(75.16363373235318, 104.29870408043762) ],
|
177
|
+
[ 1.234, Vec2.of(-56, 789) ],
|
178
|
+
[ -Math.PI, Vec2.of(-56, 789) ],
|
179
|
+
[ -Math.PI / 2, Vec2.of(-0.001, 1.0002) ],
|
180
|
+
];
|
181
|
+
|
182
|
+
for (const [angle, rotationOrigin] of testCases) {
|
183
|
+
expect(Mat33.zRotation(angle, rotationOrigin).transformVec2(rotationOrigin)).objEq(rotationOrigin);
|
184
|
+
}
|
185
|
+
});
|
186
|
+
|
187
|
+
it('should correctly apply a mapping to all components', () => {
|
188
|
+
expect(
|
189
|
+
new Mat33(
|
190
|
+
1, 2, 3,
|
191
|
+
4, 5, 6,
|
192
|
+
7, 8, 9,
|
193
|
+
).mapEntries(component => component - 1)
|
194
|
+
).toMatchObject(new Mat33(
|
195
|
+
0, 1, 2,
|
196
|
+
3, 4, 5,
|
197
|
+
6, 7, 8,
|
198
|
+
));
|
199
|
+
});
|
200
|
+
|
201
|
+
it('should convert CSS matrix(...) strings to matricies', () => {
|
202
|
+
// From MDN:
|
203
|
+
// ⎡ a c e ⎤
|
204
|
+
// ⎢ b d f ⎥ = matrix(a,b,c,d,e,f)
|
205
|
+
// ⎣ 0 0 1 ⎦
|
206
|
+
const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0)');
|
207
|
+
expect(identity).objEq(Mat33.identity);
|
208
|
+
expect(Mat33.fromCSSMatrix('matrix(1, 2, 3, 4, 5, 6)')).objEq(new Mat33(
|
209
|
+
1, 3, 5,
|
210
|
+
2, 4, 6,
|
211
|
+
0, 0, 1,
|
212
|
+
));
|
213
|
+
expect(Mat33.fromCSSMatrix('matrix(1e2, 2, 3, 4, 5, 6)')).objEq(new Mat33(
|
214
|
+
1e2, 3, 5,
|
215
|
+
2, 4, 6,
|
216
|
+
0, 0, 1,
|
217
|
+
));
|
218
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6, 2, .3, 4, 5, 6)')).objEq(new Mat33(
|
219
|
+
1.6, .3, 5,
|
220
|
+
2, 4, 6,
|
221
|
+
0, 0, 1,
|
222
|
+
));
|
223
|
+
expect(Mat33.fromCSSMatrix('matrix(-1, 2, 3.E-2, 4, -5.123, -6.5)')).objEq(new Mat33(
|
224
|
+
-1, 0.03, -5.123,
|
225
|
+
2, 4, -6.5,
|
226
|
+
0, 0, 1,
|
227
|
+
));
|
228
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6,\n\t2, .3, 4, 5, 6)')).objEq(new Mat33(
|
229
|
+
1.6, .3, 5,
|
230
|
+
2, 4, 6,
|
231
|
+
0, 0, 1,
|
232
|
+
));
|
233
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6,2, .3E-2, 4, 5, 6)')).objEq(new Mat33(
|
234
|
+
1.6, 3e-3, 5,
|
235
|
+
2, 4, 6,
|
236
|
+
0, 0, 1,
|
237
|
+
));
|
238
|
+
expect(Mat33.fromCSSMatrix('matrix(-1, 2e6, 3E-2,-5.123, -6.5e-1, 0.01)')).objEq(new Mat33(
|
239
|
+
-1, 3E-2, -6.5e-1,
|
240
|
+
2e6, -5.123, 0.01,
|
241
|
+
0, 0, 1,
|
242
|
+
));
|
243
|
+
});
|
244
|
+
});
|