@js-draw/math 1.3.0 → 1.4.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.
@@ -107,14 +107,31 @@ export declare class Mat33 {
107
107
  mapEntries(mapping: (component: number, rowcol: [number, number]) => number): Mat33;
108
108
  /** Estimate the scale factor of this matrix (based on the first row). */
109
109
  getScaleFactor(): number;
110
+ /** Returns the `idx`-th column (`idx` is 0-indexed). */
111
+ getColumn(idx: number): Vec3;
112
+ /** Returns the magnitude of the entry with the largest entry */
113
+ maximumEntryMagnitude(): number;
110
114
  /**
111
115
  * Constructs a 3x3 translation matrix (for translating `Vec2`s) using
112
116
  * **transformVec2**.
117
+ *
118
+ * Creates a matrix in the form
119
+ * $$
120
+ * \begin{pmatrix}
121
+ * 1 & 0 & {\tt amount.x}\\
122
+ * 0 & 1 & {\tt amount.y}\\
123
+ * 0 & 0 & 1
124
+ * \end{pmatrix}
125
+ * $$
113
126
  */
114
127
  static translation(amount: Vec2): Mat33;
115
128
  static zRotation(radians: number, center?: Point2): Mat33;
116
129
  static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
117
- /** @see {@link fromCSSMatrix} */
130
+ /**
131
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
132
+ *
133
+ * @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
134
+ */
118
135
  toCSSMatrix(): string;
119
136
  /**
120
137
  * Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
@@ -0,0 +1 @@
1
+ export {};
package/dist/cjs/Mat33.js CHANGED
@@ -259,9 +259,30 @@ class Mat33 {
259
259
  getScaleFactor() {
260
260
  return Math.hypot(this.a1, this.a2);
261
261
  }
262
+ /** Returns the `idx`-th column (`idx` is 0-indexed). */
263
+ getColumn(idx) {
264
+ return Vec3_1.default.of(this.rows[0].at(idx), this.rows[1].at(idx), this.rows[2].at(idx));
265
+ }
266
+ /** Returns the magnitude of the entry with the largest entry */
267
+ maximumEntryMagnitude() {
268
+ let greatestSoFar = Math.abs(this.a1);
269
+ for (const entry of this.toArray()) {
270
+ greatestSoFar = Math.max(greatestSoFar, Math.abs(entry));
271
+ }
272
+ return greatestSoFar;
273
+ }
262
274
  /**
263
275
  * Constructs a 3x3 translation matrix (for translating `Vec2`s) using
264
276
  * **transformVec2**.
277
+ *
278
+ * Creates a matrix in the form
279
+ * $$
280
+ * \begin{pmatrix}
281
+ * 1 & 0 & {\tt amount.x}\\
282
+ * 0 & 1 & {\tt amount.y}\\
283
+ * 0 & 0 & 1
284
+ * \end{pmatrix}
285
+ * $$
265
286
  */
266
287
  static translation(amount) {
267
288
  // When transforming Vec2s by a 3x3 matrix, we give the input
@@ -296,7 +317,11 @@ class Mat33 {
296
317
  // Translate such that [center] goes to (0, 0)
297
318
  return result.rightMul(Mat33.translation(center.times(-1)));
298
319
  }
299
- /** @see {@link fromCSSMatrix} */
320
+ /**
321
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
322
+ *
323
+ * @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
324
+ */
300
325
  toCSSMatrix() {
301
326
  return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
302
327
  }
@@ -314,30 +339,95 @@ class Mat33 {
314
339
  if (cssString === '' || cssString === 'none') {
315
340
  return Mat33.identity;
316
341
  }
317
- const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
318
- const numberSepExp = '[, \\t\\n]';
319
- const regExpSource = `^\\s*matrix\\s*\\(${[
320
- // According to MDN, matrix(a,b,c,d,e,f) has form:
321
- // ⎡ a c e ⎤
322
- // ⎢ b d f
323
- // ⎣ 0 0 1 ⎦
324
- numberExp, numberExp, numberExp,
325
- numberExp, numberExp, numberExp, // b, d, f
326
- ].join(`${numberSepExp}+`)}${numberSepExp}*\\)\\s*$`;
327
- const matrixExp = new RegExp(regExpSource, 'i');
328
- const match = matrixExp.exec(cssString);
329
- if (!match) {
330
- throw new Error(`Unsupported transformation: ${cssString}`);
342
+ const parseArguments = (argumentString) => {
343
+ return argumentString.split(/[, \t\n]+/g).map(argString => {
344
+ let isPercentage = false;
345
+ if (argString.endsWith('%')) {
346
+ isPercentage = true;
347
+ argString = argString.substring(0, argString.length - 1);
348
+ }
349
+ // Remove trailing px units.
350
+ argString = argString.replace(/px$/ig, '');
351
+ const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i;
352
+ if (!numberExp.exec(argString)) {
353
+ throw new Error(`All arguments to transform functions must be numeric (state: ${JSON.stringify({
354
+ currentArgument: argString,
355
+ allArguments: argumentString,
356
+ })})`);
357
+ }
358
+ let argNumber = parseFloat(argString);
359
+ if (isPercentage) {
360
+ argNumber /= 100;
361
+ }
362
+ return argNumber;
363
+ });
364
+ };
365
+ const keywordToAction = {
366
+ matrix: (matrixData) => {
367
+ if (matrixData.length !== 6) {
368
+ throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`);
369
+ }
370
+ const a = matrixData[0];
371
+ const b = matrixData[1];
372
+ const c = matrixData[2];
373
+ const d = matrixData[3];
374
+ const e = matrixData[4];
375
+ const f = matrixData[5];
376
+ const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
377
+ return transform;
378
+ },
379
+ scale: (scaleArgs) => {
380
+ let scaleX, scaleY;
381
+ if (scaleArgs.length === 1) {
382
+ scaleX = scaleArgs[0];
383
+ scaleY = scaleArgs[0];
384
+ }
385
+ else if (scaleArgs.length === 2) {
386
+ scaleX = scaleArgs[0];
387
+ scaleY = scaleArgs[1];
388
+ }
389
+ else {
390
+ throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`);
391
+ }
392
+ return Mat33.scaling2D(Vec2_1.Vec2.of(scaleX, scaleY));
393
+ },
394
+ translate: (translateArgs) => {
395
+ let translateX = 0;
396
+ let translateY = 0;
397
+ if (translateArgs.length === 1) {
398
+ // If no y translation is given, assume 0.
399
+ translateX = translateArgs[0];
400
+ }
401
+ else if (translateArgs.length === 2) {
402
+ translateX = translateArgs[0];
403
+ translateY = translateArgs[1];
404
+ }
405
+ else {
406
+ throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`);
407
+ }
408
+ return Mat33.translation(Vec2_1.Vec2.of(translateX, translateY));
409
+ },
410
+ };
411
+ // A command (\w+)
412
+ // followed by a set of arguments ([ \t\n0-9eE.,\-%]+)
413
+ const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig;
414
+ let match;
415
+ let matrix = null;
416
+ while ((match = partRegex.exec(cssString)) !== null) {
417
+ const action = match[1].toLowerCase();
418
+ if (!(action in keywordToAction)) {
419
+ throw new Error(`Unsupported CSS transform action: ${action}`);
420
+ }
421
+ const args = parseArguments(match[2]);
422
+ const currentMatrix = keywordToAction[action](args);
423
+ if (!matrix) {
424
+ matrix = currentMatrix;
425
+ }
426
+ else {
427
+ matrix = matrix.rightMul(currentMatrix);
428
+ }
331
429
  }
332
- const matrixData = match.slice(1).map(entry => parseFloat(entry));
333
- const a = matrixData[0];
334
- const b = matrixData[1];
335
- const c = matrixData[2];
336
- const d = matrixData[3];
337
- const e = matrixData[4];
338
- const f = matrixData[5];
339
- const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
340
- return transform;
430
+ return matrix ?? Mat33.identity;
341
431
  }
342
432
  }
343
433
  exports.Mat33 = Mat33;
@@ -35,6 +35,13 @@ export declare class Vec3 {
35
35
  length(): number;
36
36
  magnitude(): number;
37
37
  magnitudeSquared(): number;
38
+ /**
39
+ * Returns the entry of this with the greatest magnitude.
40
+ *
41
+ * In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
42
+ * all entries of this vector.
43
+ */
44
+ maximumEntryMagnitude(): number;
38
45
  /**
39
46
  * Return this' angle in the XY plane (treats this as a Vec2).
40
47
  *
package/dist/cjs/Vec3.js CHANGED
@@ -58,6 +58,15 @@ class Vec3 {
58
58
  magnitudeSquared() {
59
59
  return this.dot(this);
60
60
  }
61
+ /**
62
+ * Returns the entry of this with the greatest magnitude.
63
+ *
64
+ * In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
65
+ * all entries of this vector.
66
+ */
67
+ maximumEntryMagnitude() {
68
+ return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z)));
69
+ }
61
70
  /**
62
71
  * Return this' angle in the XY plane (treats this as a Vec2).
63
72
  *
@@ -107,14 +107,31 @@ export declare class Mat33 {
107
107
  mapEntries(mapping: (component: number, rowcol: [number, number]) => number): Mat33;
108
108
  /** Estimate the scale factor of this matrix (based on the first row). */
109
109
  getScaleFactor(): number;
110
+ /** Returns the `idx`-th column (`idx` is 0-indexed). */
111
+ getColumn(idx: number): Vec3;
112
+ /** Returns the magnitude of the entry with the largest entry */
113
+ maximumEntryMagnitude(): number;
110
114
  /**
111
115
  * Constructs a 3x3 translation matrix (for translating `Vec2`s) using
112
116
  * **transformVec2**.
117
+ *
118
+ * Creates a matrix in the form
119
+ * $$
120
+ * \begin{pmatrix}
121
+ * 1 & 0 & {\tt amount.x}\\
122
+ * 0 & 1 & {\tt amount.y}\\
123
+ * 0 & 0 & 1
124
+ * \end{pmatrix}
125
+ * $$
113
126
  */
114
127
  static translation(amount: Vec2): Mat33;
115
128
  static zRotation(radians: number, center?: Point2): Mat33;
116
129
  static scaling2D(amount: number | Vec2, center?: Point2): Mat33;
117
- /** @see {@link fromCSSMatrix} */
130
+ /**
131
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
132
+ *
133
+ * @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
134
+ */
118
135
  toCSSMatrix(): string;
119
136
  /**
120
137
  * Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
@@ -0,0 +1 @@
1
+ export {};
@@ -253,9 +253,30 @@ export class Mat33 {
253
253
  getScaleFactor() {
254
254
  return Math.hypot(this.a1, this.a2);
255
255
  }
256
+ /** Returns the `idx`-th column (`idx` is 0-indexed). */
257
+ getColumn(idx) {
258
+ return Vec3.of(this.rows[0].at(idx), this.rows[1].at(idx), this.rows[2].at(idx));
259
+ }
260
+ /** Returns the magnitude of the entry with the largest entry */
261
+ maximumEntryMagnitude() {
262
+ let greatestSoFar = Math.abs(this.a1);
263
+ for (const entry of this.toArray()) {
264
+ greatestSoFar = Math.max(greatestSoFar, Math.abs(entry));
265
+ }
266
+ return greatestSoFar;
267
+ }
256
268
  /**
257
269
  * Constructs a 3x3 translation matrix (for translating `Vec2`s) using
258
270
  * **transformVec2**.
271
+ *
272
+ * Creates a matrix in the form
273
+ * $$
274
+ * \begin{pmatrix}
275
+ * 1 & 0 & {\tt amount.x}\\
276
+ * 0 & 1 & {\tt amount.y}\\
277
+ * 0 & 0 & 1
278
+ * \end{pmatrix}
279
+ * $$
259
280
  */
260
281
  static translation(amount) {
261
282
  // When transforming Vec2s by a 3x3 matrix, we give the input
@@ -290,7 +311,11 @@ export class Mat33 {
290
311
  // Translate such that [center] goes to (0, 0)
291
312
  return result.rightMul(Mat33.translation(center.times(-1)));
292
313
  }
293
- /** @see {@link fromCSSMatrix} */
314
+ /**
315
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
316
+ *
317
+ * @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
318
+ */
294
319
  toCSSMatrix() {
295
320
  return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
296
321
  }
@@ -308,30 +333,95 @@ export class Mat33 {
308
333
  if (cssString === '' || cssString === 'none') {
309
334
  return Mat33.identity;
310
335
  }
311
- const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
312
- const numberSepExp = '[, \\t\\n]';
313
- const regExpSource = `^\\s*matrix\\s*\\(${[
314
- // According to MDN, matrix(a,b,c,d,e,f) has form:
315
- // ⎡ a c e ⎤
316
- // ⎢ b d f
317
- // ⎣ 0 0 1 ⎦
318
- numberExp, numberExp, numberExp,
319
- numberExp, numberExp, numberExp, // b, d, f
320
- ].join(`${numberSepExp}+`)}${numberSepExp}*\\)\\s*$`;
321
- const matrixExp = new RegExp(regExpSource, 'i');
322
- const match = matrixExp.exec(cssString);
323
- if (!match) {
324
- throw new Error(`Unsupported transformation: ${cssString}`);
336
+ const parseArguments = (argumentString) => {
337
+ return argumentString.split(/[, \t\n]+/g).map(argString => {
338
+ let isPercentage = false;
339
+ if (argString.endsWith('%')) {
340
+ isPercentage = true;
341
+ argString = argString.substring(0, argString.length - 1);
342
+ }
343
+ // Remove trailing px units.
344
+ argString = argString.replace(/px$/ig, '');
345
+ const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i;
346
+ if (!numberExp.exec(argString)) {
347
+ throw new Error(`All arguments to transform functions must be numeric (state: ${JSON.stringify({
348
+ currentArgument: argString,
349
+ allArguments: argumentString,
350
+ })})`);
351
+ }
352
+ let argNumber = parseFloat(argString);
353
+ if (isPercentage) {
354
+ argNumber /= 100;
355
+ }
356
+ return argNumber;
357
+ });
358
+ };
359
+ const keywordToAction = {
360
+ matrix: (matrixData) => {
361
+ if (matrixData.length !== 6) {
362
+ throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`);
363
+ }
364
+ const a = matrixData[0];
365
+ const b = matrixData[1];
366
+ const c = matrixData[2];
367
+ const d = matrixData[3];
368
+ const e = matrixData[4];
369
+ const f = matrixData[5];
370
+ const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
371
+ return transform;
372
+ },
373
+ scale: (scaleArgs) => {
374
+ let scaleX, scaleY;
375
+ if (scaleArgs.length === 1) {
376
+ scaleX = scaleArgs[0];
377
+ scaleY = scaleArgs[0];
378
+ }
379
+ else if (scaleArgs.length === 2) {
380
+ scaleX = scaleArgs[0];
381
+ scaleY = scaleArgs[1];
382
+ }
383
+ else {
384
+ throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`);
385
+ }
386
+ return Mat33.scaling2D(Vec2.of(scaleX, scaleY));
387
+ },
388
+ translate: (translateArgs) => {
389
+ let translateX = 0;
390
+ let translateY = 0;
391
+ if (translateArgs.length === 1) {
392
+ // If no y translation is given, assume 0.
393
+ translateX = translateArgs[0];
394
+ }
395
+ else if (translateArgs.length === 2) {
396
+ translateX = translateArgs[0];
397
+ translateY = translateArgs[1];
398
+ }
399
+ else {
400
+ throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`);
401
+ }
402
+ return Mat33.translation(Vec2.of(translateX, translateY));
403
+ },
404
+ };
405
+ // A command (\w+)
406
+ // followed by a set of arguments ([ \t\n0-9eE.,\-%]+)
407
+ const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig;
408
+ let match;
409
+ let matrix = null;
410
+ while ((match = partRegex.exec(cssString)) !== null) {
411
+ const action = match[1].toLowerCase();
412
+ if (!(action in keywordToAction)) {
413
+ throw new Error(`Unsupported CSS transform action: ${action}`);
414
+ }
415
+ const args = parseArguments(match[2]);
416
+ const currentMatrix = keywordToAction[action](args);
417
+ if (!matrix) {
418
+ matrix = currentMatrix;
419
+ }
420
+ else {
421
+ matrix = matrix.rightMul(currentMatrix);
422
+ }
325
423
  }
326
- const matrixData = match.slice(1).map(entry => parseFloat(entry));
327
- const a = matrixData[0];
328
- const b = matrixData[1];
329
- const c = matrixData[2];
330
- const d = matrixData[3];
331
- const e = matrixData[4];
332
- const f = matrixData[5];
333
- const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
334
- return transform;
424
+ return matrix ?? Mat33.identity;
335
425
  }
336
426
  }
337
427
  Mat33.identity = new Mat33(1, 0, 0, 0, 1, 0, 0, 0, 1);
@@ -35,6 +35,13 @@ export declare class Vec3 {
35
35
  length(): number;
36
36
  magnitude(): number;
37
37
  magnitudeSquared(): number;
38
+ /**
39
+ * Returns the entry of this with the greatest magnitude.
40
+ *
41
+ * In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
42
+ * all entries of this vector.
43
+ */
44
+ maximumEntryMagnitude(): number;
38
45
  /**
39
46
  * Return this' angle in the XY plane (treats this as a Vec2).
40
47
  *
package/dist/mjs/Vec3.mjs CHANGED
@@ -55,6 +55,15 @@ export class Vec3 {
55
55
  magnitudeSquared() {
56
56
  return this.dot(this);
57
57
  }
58
+ /**
59
+ * Returns the entry of this with the greatest magnitude.
60
+ *
61
+ * In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
62
+ * all entries of this vector.
63
+ */
64
+ maximumEntryMagnitude() {
65
+ return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z)));
66
+ }
58
67
  /**
59
68
  * Return this' angle in the XY plane (treats this as a Vec2).
60
69
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-draw/math",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "A math library for js-draw. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -22,13 +22,13 @@
22
22
  "dist-test": "npm run build && cd dist-test/test_imports && npm install && npm run test",
23
23
  "dist": "npm run build && npm run dist-test",
24
24
  "build": "rm -rf ./dist && mkdir dist && build-tool build",
25
- "watch": "rm -rf ./dist && mkdir dist && build-tool watch"
25
+ "watch": "rm -rf ./dist/* && mkdir -p dist && build-tool watch"
26
26
  },
27
27
  "dependencies": {
28
28
  "bezier-js": "6.1.3"
29
29
  },
30
30
  "devDependencies": {
31
- "@js-draw/build-tool": "^1.0.2",
31
+ "@js-draw/build-tool": "^1.4.0",
32
32
  "@types/bezier-js": "4.1.0",
33
33
  "@types/jest": "29.5.3",
34
34
  "@types/jsdom": "21.1.1"
@@ -45,5 +45,5 @@
45
45
  "svg",
46
46
  "math"
47
47
  ],
48
- "gitHead": "46b3d8f819f8e083f6e3e1d01e027e4311355456"
48
+ "gitHead": "b520078c16a4d23d9bed4531eafda87bfce3f6b1"
49
49
  }
@@ -0,0 +1,90 @@
1
+ import Mat33 from './Mat33';
2
+ import { Vec2 } from './Vec2';
3
+
4
+ describe('Mat33.fromCSSMatrix', () => {
5
+ it('should convert CSS matrix(...) strings to matricies', () => {
6
+ // From MDN:
7
+ // ⎡ a c e ⎤
8
+ // ⎢ b d f ⎥ = matrix(a,b,c,d,e,f)
9
+ // ⎣ 0 0 1 ⎦
10
+ const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0)');
11
+ expect(identity).objEq(Mat33.identity);
12
+ expect(Mat33.fromCSSMatrix('matrix(1, 2, 3, 4, 5, 6)')).objEq(new Mat33(
13
+ 1, 3, 5,
14
+ 2, 4, 6,
15
+ 0, 0, 1,
16
+ ));
17
+ expect(Mat33.fromCSSMatrix('matrix(1e2, 2, 3, 4, 5, 6)')).objEq(new Mat33(
18
+ 1e2, 3, 5,
19
+ 2, 4, 6,
20
+ 0, 0, 1,
21
+ ));
22
+ expect(Mat33.fromCSSMatrix('matrix(1.6, 2, .3, 4, 5, 6)')).objEq(new Mat33(
23
+ 1.6, .3, 5,
24
+ 2, 4, 6,
25
+ 0, 0, 1,
26
+ ));
27
+ expect(Mat33.fromCSSMatrix('matrix(-1, 2, 3.E-2, 4, -5.123, -6.5)')).objEq(new Mat33(
28
+ -1, 0.03, -5.123,
29
+ 2, 4, -6.5,
30
+ 0, 0, 1,
31
+ ));
32
+ expect(Mat33.fromCSSMatrix('matrix(1.6,\n\t2, .3, 4, 5, 6)')).objEq(new Mat33(
33
+ 1.6, .3, 5,
34
+ 2, 4, 6,
35
+ 0, 0, 1,
36
+ ));
37
+ expect(Mat33.fromCSSMatrix('matrix(1.6,2, .3E-2, 4, 5, 6)')).objEq(new Mat33(
38
+ 1.6, 3e-3, 5,
39
+ 2, 4, 6,
40
+ 0, 0, 1,
41
+ ));
42
+ expect(Mat33.fromCSSMatrix('matrix(-1, 2e6, 3E-2,-5.123, -6.5e-1, 0.01)')).objEq(new Mat33(
43
+ -1, 3E-2, -6.5e-1,
44
+ 2e6, -5.123, 0.01,
45
+ 0, 0, 1,
46
+ ));
47
+ });
48
+
49
+ it('should convert multi-matrix arguments into a single CSS matrix', () => {
50
+ const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0) matrix(1, 0, 0, 1, 0, 0)');
51
+ expect(identity).objEq(Mat33.identity);
52
+
53
+ expect(Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0) matrix(1, 2, 3, 4, 5, 6) matrix(1, 0, 0, 1, 0, 0)')).objEq(new Mat33(
54
+ 1, 3, 5,
55
+ 2, 4, 6,
56
+ 0, 0, 1,
57
+ ));
58
+
59
+ expect(Mat33.fromCSSMatrix('matrix(2,\n\t 0, 0, 2, 0, 0) matrix(1, 2, 3, 4, 5, 6) matrix(1, 0, 0, 1, 0, 0)')).objEq(new Mat33(
60
+ 2, 6, 10,
61
+ 4, 8, 12,
62
+ 0, 0, 1,
63
+ ));
64
+ });
65
+
66
+ it('should convert scale()s with a single argument', () => {
67
+ expect(Mat33.fromCSSMatrix('scale(1)')).objEq(Mat33.identity);
68
+ expect(Mat33.fromCSSMatrix('scale(0.4)')).objEq(Mat33.scaling2D(0.4));
69
+ expect(Mat33.fromCSSMatrix('scale(-0.4 )')).objEq(Mat33.scaling2D(-0.4));
70
+ expect(Mat33.fromCSSMatrix('scale(100%)')).objEq(Mat33.identity);
71
+ expect(Mat33.fromCSSMatrix('scale(20e2%)')).objEq(Mat33.scaling2D(20));
72
+ expect(Mat33.fromCSSMatrix('scale(200%) scale(50%)')).objEq(Mat33.identity);
73
+ });
74
+
75
+ it('should convert scale()s with two arguments', () => {
76
+ expect(Mat33.fromCSSMatrix('scale(1\t 1)')).objEq(Mat33.identity);
77
+ expect(Mat33.fromCSSMatrix('scale(1\t 2)')).objEq(Mat33.scaling2D(Vec2.of(1, 2)));
78
+ expect(Mat33.fromCSSMatrix('scale(1\t 2) scale(1)')).objEq(Mat33.scaling2D(Vec2.of(1, 2)));
79
+ });
80
+
81
+ it('should convert translate()s', () => {
82
+ expect(Mat33.fromCSSMatrix('translate(0)')).objEq(Mat33.identity);
83
+ expect(Mat33.fromCSSMatrix('translate(1, 1)')).objEq(Mat33.translation(Vec2.of(1, 1)));
84
+ expect(Mat33.fromCSSMatrix('translate(1 200%)')).objEq(Mat33.translation(Vec2.of(1, 2)));
85
+ });
86
+
87
+ it('should support px following numbers', () => {
88
+ expect(Mat33.fromCSSMatrix('translate(1px, 2px)')).objEq(Mat33.translation(Vec2.of(1, 2)));
89
+ });
90
+ });
package/src/Mat33.test.ts CHANGED
@@ -198,47 +198,11 @@ describe('Mat33 tests', () => {
198
198
  ));
199
199
  });
200
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
- ));
201
+ it('getColumn should return the given column index', () => {
202
+ expect(Mat33.identity.getColumn(0)).objEq(Vec3.unitX);
203
+ expect(Mat33.identity.getColumn(1)).objEq(Vec3.of(0, 1, 0));
204
+
205
+ // scaling2D only scales the x/y components of vectors it transforms
206
+ expect(Mat33.scaling2D(2).getColumn(2)).objEq(Vec3.of(0, 0, 1));
243
207
  });
244
208
  });
package/src/Mat33.ts CHANGED
@@ -335,9 +335,37 @@ export class Mat33 {
335
335
  return Math.hypot(this.a1, this.a2);
336
336
  }
337
337
 
338
+ /** Returns the `idx`-th column (`idx` is 0-indexed). */
339
+ public getColumn(idx: number) {
340
+ return Vec3.of(
341
+ this.rows[0].at(idx),
342
+ this.rows[1].at(idx),
343
+ this.rows[2].at(idx),
344
+ );
345
+ }
346
+
347
+ /** Returns the magnitude of the entry with the largest entry */
348
+ public maximumEntryMagnitude() {
349
+ let greatestSoFar = Math.abs(this.a1);
350
+ for (const entry of this.toArray()) {
351
+ greatestSoFar = Math.max(greatestSoFar, Math.abs(entry));
352
+ }
353
+
354
+ return greatestSoFar;
355
+ }
356
+
338
357
  /**
339
358
  * Constructs a 3x3 translation matrix (for translating `Vec2`s) using
340
359
  * **transformVec2**.
360
+ *
361
+ * Creates a matrix in the form
362
+ * $$
363
+ * \begin{pmatrix}
364
+ * 1 & 0 & {\tt amount.x}\\
365
+ * 0 & 1 & {\tt amount.y}\\
366
+ * 0 & 0 & 1
367
+ * \end{pmatrix}
368
+ * $$
341
369
  */
342
370
  public static translation(amount: Vec2): Mat33 {
343
371
  // When transforming Vec2s by a 3x3 matrix, we give the input
@@ -392,7 +420,11 @@ export class Mat33 {
392
420
  return result.rightMul(Mat33.translation(center.times(-1)));
393
421
  }
394
422
 
395
- /** @see {@link fromCSSMatrix} */
423
+ /**
424
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
425
+ *
426
+ * @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
427
+ */
396
428
  public toCSSMatrix(): string {
397
429
  return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
398
430
  }
@@ -412,39 +444,118 @@ export class Mat33 {
412
444
  return Mat33.identity;
413
445
  }
414
446
 
415
- const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
416
- const numberSepExp = '[, \\t\\n]';
417
- const regExpSource = `^\\s*matrix\\s*\\(${
418
- [
419
- // According to MDN, matrix(a,b,c,d,e,f) has form:
420
- // ⎡ a c e
421
- // ⎢ b d f ⎥
422
- // ⎣ 0 0 1 ⎦
423
- numberExp, numberExp, numberExp, // a, c, e
424
- numberExp, numberExp, numberExp, // b, d, f
425
- ].join(`${numberSepExp}+`)
426
- }${numberSepExp}*\\)\\s*$`;
427
- const matrixExp = new RegExp(regExpSource, 'i');
428
- const match = matrixExp.exec(cssString);
429
-
430
- if (!match) {
431
- throw new Error(`Unsupported transformation: ${cssString}`);
447
+ const parseArguments = (argumentString: string) => {
448
+ return argumentString.split(/[, \t\n]+/g).map(argString => {
449
+ let isPercentage = false;
450
+ if (argString.endsWith('%')) {
451
+ isPercentage = true;
452
+ argString = argString.substring(0, argString.length - 1);
453
+ }
454
+
455
+ // Remove trailing px units.
456
+ argString = argString.replace(/px$/ig, '');
457
+
458
+ const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i;
459
+
460
+ if (!numberExp.exec(argString)) {
461
+ throw new Error(
462
+ `All arguments to transform functions must be numeric (state: ${
463
+ JSON.stringify({
464
+ currentArgument: argString,
465
+ allArguments: argumentString,
466
+ })
467
+ })`
468
+ );
469
+ }
470
+
471
+ let argNumber = parseFloat(argString);
472
+
473
+ if (isPercentage) {
474
+ argNumber /= 100;
475
+ }
476
+
477
+ return argNumber;
478
+ });
479
+ };
480
+
481
+
482
+ const keywordToAction = {
483
+ matrix: (matrixData: number[]) => {
484
+ if (matrixData.length !== 6) {
485
+ throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`);
486
+ }
487
+
488
+ const a = matrixData[0];
489
+ const b = matrixData[1];
490
+ const c = matrixData[2];
491
+ const d = matrixData[3];
492
+ const e = matrixData[4];
493
+ const f = matrixData[5];
494
+
495
+ const transform = new Mat33(
496
+ a, c, e,
497
+ b, d, f,
498
+ 0, 0, 1
499
+ );
500
+ return transform;
501
+ },
502
+
503
+ scale: (scaleArgs: number[]) => {
504
+ let scaleX, scaleY;
505
+ if (scaleArgs.length === 1) {
506
+ scaleX = scaleArgs[0];
507
+ scaleY = scaleArgs[0];
508
+ } else if (scaleArgs.length === 2) {
509
+ scaleX = scaleArgs[0];
510
+ scaleY = scaleArgs[1];
511
+ } else {
512
+ throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`);
513
+ }
514
+
515
+ return Mat33.scaling2D(Vec2.of(scaleX, scaleY));
516
+ },
517
+
518
+ translate: (translateArgs: number[]) => {
519
+ let translateX = 0;
520
+ let translateY = 0;
521
+
522
+ if (translateArgs.length === 1) {
523
+ // If no y translation is given, assume 0.
524
+ translateX = translateArgs[0];
525
+ } else if (translateArgs.length === 2) {
526
+ translateX = translateArgs[0];
527
+ translateY = translateArgs[1];
528
+ } else {
529
+ throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`);
530
+ }
531
+
532
+ return Mat33.translation(Vec2.of(translateX, translateY));
533
+ },
534
+ };
535
+
536
+ // A command (\w+)
537
+ // followed by a set of arguments ([ \t\n0-9eE.,\-%]+)
538
+ const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig;
539
+ let match;
540
+ let matrix: Mat33|null = null;
541
+
542
+ while ((match = partRegex.exec(cssString)) !== null) {
543
+ const action = match[1].toLowerCase();
544
+ if (!(action in keywordToAction)) {
545
+ throw new Error(`Unsupported CSS transform action: ${action}`);
546
+ }
547
+
548
+ const args = parseArguments(match[2]);
549
+ const currentMatrix = keywordToAction[action as keyof typeof keywordToAction](args);
550
+
551
+ if (!matrix) {
552
+ matrix = currentMatrix;
553
+ } else {
554
+ matrix = matrix.rightMul(currentMatrix);
555
+ }
432
556
  }
433
557
 
434
- const matrixData = match.slice(1).map(entry => parseFloat(entry));
435
- const a = matrixData[0];
436
- const b = matrixData[1];
437
- const c = matrixData[2];
438
- const d = matrixData[3];
439
- const e = matrixData[4];
440
- const f = matrixData[5];
441
-
442
- const transform = new Mat33(
443
- a, c, e,
444
- b, d, f,
445
- 0, 0, 1
446
- );
447
- return transform;
558
+ return matrix ?? Mat33.identity;
448
559
  }
449
560
  }
450
561
  export default Mat33;
package/src/Vec3.ts CHANGED
@@ -63,6 +63,16 @@ export class Vec3 {
63
63
  return this.dot(this);
64
64
  }
65
65
 
66
+ /**
67
+ * Returns the entry of this with the greatest magnitude.
68
+ *
69
+ * In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
70
+ * all entries of this vector.
71
+ */
72
+ public maximumEntryMagnitude(): number {
73
+ return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z)));
74
+ }
75
+
66
76
  /**
67
77
  * Return this' angle in the XY plane (treats this as a Vec2).
68
78
  *
package/src/rounding.ts CHANGED
@@ -165,3 +165,4 @@ export const toStringOfSamePrecision = (num: number, ...references: string[]): s
165
165
  const negativeSign = textMatch[1];
166
166
  return cleanUpNumber(`${negativeSign}${preDecimal}.${postDecimal}`);
167
167
  };
168
+