@js-draw/math 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+