@js-draw/math 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/Color4.d.ts +40 -0
  3. package/dist/cjs/Color4.js +102 -0
  4. package/dist/cjs/Color4.test.d.ts +1 -0
  5. package/dist/cjs/Mat33.test.d.ts +1 -0
  6. package/dist/cjs/Vec2.test.d.ts +1 -0
  7. package/dist/cjs/Vec3.test.d.ts +1 -0
  8. package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
  9. package/dist/cjs/rounding.test.d.ts +1 -0
  10. package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
  11. package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
  12. package/dist/cjs/shapes/Path.test.d.ts +1 -0
  13. package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
  14. package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
  15. package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
  16. package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
  17. package/dist/mjs/Color4.d.ts +40 -0
  18. package/dist/mjs/Color4.mjs +102 -0
  19. package/dist/mjs/Color4.test.d.ts +1 -0
  20. package/dist/mjs/Mat33.test.d.ts +1 -0
  21. package/dist/mjs/Vec2.test.d.ts +1 -0
  22. package/dist/mjs/Vec3.test.d.ts +1 -0
  23. package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
  24. package/dist/mjs/rounding.test.d.ts +1 -0
  25. package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
  26. package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
  27. package/dist/mjs/shapes/Path.test.d.ts +1 -0
  28. package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
  29. package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
  30. package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
  31. package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
  32. package/dist-test/test_imports/package-lock.json +13 -0
  33. package/dist-test/test_imports/package.json +12 -0
  34. package/dist-test/test_imports/test-imports.js +15 -0
  35. package/dist-test/test_imports/test-require.cjs +15 -0
  36. package/package.json +4 -3
  37. package/src/Color4.test.ts +94 -0
  38. package/src/Color4.ts +430 -0
  39. package/src/Mat33.test.ts +244 -0
  40. package/src/Mat33.ts +450 -0
  41. package/src/Vec2.test.ts +30 -0
  42. package/src/Vec2.ts +49 -0
  43. package/src/Vec3.test.ts +51 -0
  44. package/src/Vec3.ts +245 -0
  45. package/src/lib.ts +42 -0
  46. package/src/polynomial/solveQuadratic.test.ts +39 -0
  47. package/src/polynomial/solveQuadratic.ts +43 -0
  48. package/src/rounding.test.ts +65 -0
  49. package/src/rounding.ts +167 -0
  50. package/src/shapes/Abstract2DShape.ts +63 -0
  51. package/src/shapes/BezierJSWrapper.ts +93 -0
  52. package/src/shapes/CubicBezier.ts +35 -0
  53. package/src/shapes/LineSegment2.test.ts +99 -0
  54. package/src/shapes/LineSegment2.ts +232 -0
  55. package/src/shapes/Path.fromString.test.ts +223 -0
  56. package/src/shapes/Path.test.ts +309 -0
  57. package/src/shapes/Path.toString.test.ts +77 -0
  58. package/src/shapes/Path.ts +963 -0
  59. package/src/shapes/PointShape2D.ts +33 -0
  60. package/src/shapes/QuadraticBezier.test.ts +31 -0
  61. package/src/shapes/QuadraticBezier.ts +142 -0
  62. package/src/shapes/Rect2.test.ts +209 -0
  63. package/src/shapes/Rect2.ts +346 -0
  64. package/src/shapes/Triangle.test.ts +61 -0
  65. 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
+ });