@js-draw/math 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -107,15 +107,45 @@ 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;
136
+ /**
137
+ * @beta May change or even be removed between minor releases.
138
+ *
139
+ * Converts this matrix into a list of CSS transforms that attempt to preserve
140
+ * this matrix's translation.
141
+ *
142
+ * In Chrome/Firefox, translation attributes only support 6 digits (likely an artifact
143
+ * of using lower-precision floating point numbers). This works around
144
+ * that by expanding this matrix into the product of several CSS transforms.
145
+ *
146
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
147
+ */
148
+ toSafeCSSTransformList(): string;
119
149
  /**
120
150
  * Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
121
151
  *
package/dist/cjs/Mat33.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Mat33 = void 0;
7
7
  const Vec2_1 = require("./Vec2");
8
8
  const Vec3_1 = __importDefault(require("./Vec3"));
9
+ const rounding_1 = require("./rounding");
9
10
  /**
10
11
  * Represents a three dimensional linear transformation or
11
12
  * a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
@@ -259,9 +260,30 @@ class Mat33 {
259
260
  getScaleFactor() {
260
261
  return Math.hypot(this.a1, this.a2);
261
262
  }
263
+ /** Returns the `idx`-th column (`idx` is 0-indexed). */
264
+ getColumn(idx) {
265
+ return Vec3_1.default.of(this.rows[0].at(idx), this.rows[1].at(idx), this.rows[2].at(idx));
266
+ }
267
+ /** Returns the magnitude of the entry with the largest entry */
268
+ maximumEntryMagnitude() {
269
+ let greatestSoFar = Math.abs(this.a1);
270
+ for (const entry of this.toArray()) {
271
+ greatestSoFar = Math.max(greatestSoFar, Math.abs(entry));
272
+ }
273
+ return greatestSoFar;
274
+ }
262
275
  /**
263
276
  * Constructs a 3x3 translation matrix (for translating `Vec2`s) using
264
277
  * **transformVec2**.
278
+ *
279
+ * Creates a matrix in the form
280
+ * $$
281
+ * \begin{pmatrix}
282
+ * 1 & 0 & {\tt amount.x}\\
283
+ * 0 & 1 & {\tt amount.y}\\
284
+ * 0 & 0 & 1
285
+ * \end{pmatrix}
286
+ * $$
265
287
  */
266
288
  static translation(amount) {
267
289
  // When transforming Vec2s by a 3x3 matrix, we give the input
@@ -296,10 +318,131 @@ class Mat33 {
296
318
  // Translate such that [center] goes to (0, 0)
297
319
  return result.rightMul(Mat33.translation(center.times(-1)));
298
320
  }
299
- /** @see {@link fromCSSMatrix} */
321
+ /**
322
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
323
+ *
324
+ * @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
325
+ */
300
326
  toCSSMatrix() {
301
327
  return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
302
328
  }
329
+ /**
330
+ * @beta May change or even be removed between minor releases.
331
+ *
332
+ * Converts this matrix into a list of CSS transforms that attempt to preserve
333
+ * this matrix's translation.
334
+ *
335
+ * In Chrome/Firefox, translation attributes only support 6 digits (likely an artifact
336
+ * of using lower-precision floating point numbers). This works around
337
+ * that by expanding this matrix into the product of several CSS transforms.
338
+ *
339
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
340
+ */
341
+ toSafeCSSTransformList() {
342
+ // Check whether it's safe to return just the CSS matrix
343
+ const translation = Vec2_1.Vec2.of(this.a3, this.b3);
344
+ const translationRoundedX = (0, rounding_1.toRoundedString)(translation.x);
345
+ const translationRoundedY = (0, rounding_1.toRoundedString)(translation.y);
346
+ const nonDigitsRegex = /[^0-9]+/g;
347
+ const translationXDigits = translationRoundedX.replace(nonDigitsRegex, '').length;
348
+ const translationYDigits = translationRoundedY.replace(nonDigitsRegex, '').length;
349
+ // Is it safe to just return the default CSS matrix?
350
+ if (translationXDigits <= 5 && translationYDigits <= 5) {
351
+ return this.toCSSMatrix();
352
+ }
353
+ // Remove the last column (the translation column)
354
+ let transform = new Mat33(this.a1, this.a2, 0, this.b1, this.b2, 0, 0, 0, 1);
355
+ const transforms = [];
356
+ let lastScale = null;
357
+ // Appends a translate() command to the list of `transforms`.
358
+ const addTranslate = (translation) => {
359
+ lastScale = null;
360
+ if (!translation.eq(Vec2_1.Vec2.zero)) {
361
+ transforms.push(`translate(${(0, rounding_1.toRoundedString)(translation.x)}px, ${(0, rounding_1.toRoundedString)(translation.y)}px)`);
362
+ }
363
+ };
364
+ // Appends a scale() command to the list of transforms, possibly merging with
365
+ // the last command, if a scale().
366
+ const addScale = (scale) => {
367
+ // Merge with the last scale
368
+ if (lastScale) {
369
+ const newScale = lastScale.scale(scale);
370
+ // Don't merge if the new scale has very large values
371
+ if (newScale.maximumEntryMagnitude() < 1e7) {
372
+ const previousCommand = transforms.pop();
373
+ console.assert(previousCommand.startsWith('scale'), 'Invalid state: Merging scale commands');
374
+ scale = newScale;
375
+ }
376
+ }
377
+ if (scale.x === scale.y) {
378
+ transforms.push(`scale(${(0, rounding_1.toRoundedString)(scale.x)})`);
379
+ }
380
+ else {
381
+ transforms.push(`scale(${(0, rounding_1.toRoundedString)(scale.x)}, ${(0, rounding_1.toRoundedString)(scale.y)})`);
382
+ }
383
+ lastScale = scale;
384
+ };
385
+ // Returns the number of digits before the `.` in the given number string.
386
+ const digitsPreDecimalCount = (numberString) => {
387
+ let decimalIndex = numberString.indexOf('.');
388
+ if (decimalIndex === -1) {
389
+ decimalIndex = numberString.length;
390
+ }
391
+ return numberString.substring(0, decimalIndex).replace(nonDigitsRegex, '').length;
392
+ };
393
+ // Returns the number of digits (positive for left shift, negative for right shift)
394
+ // required to shift the decimal to the middle of the number.
395
+ const getShift = (numberString) => {
396
+ const preDecimal = digitsPreDecimalCount(numberString);
397
+ const postDecimal = (numberString.match(/[.](\d*)/) ?? ['', ''])[1].length;
398
+ // The shift required to center the decimal point.
399
+ const toCenter = postDecimal - preDecimal;
400
+ // toCenter is positive for a left shift (adding more pre-decimals),
401
+ // so, after applying it,
402
+ const postShiftPreDecimal = preDecimal + toCenter;
403
+ // We want the digits before the decimal to have a length at most 4, however.
404
+ // Thus, right shift until this is the case.
405
+ const shiftForAtMost5DigitsPreDecimal = 4 - Math.max(postShiftPreDecimal, 4);
406
+ return toCenter + shiftForAtMost5DigitsPreDecimal;
407
+ };
408
+ const addShiftedTranslate = (translate, depth = 0) => {
409
+ const xString = (0, rounding_1.toRoundedString)(translate.x);
410
+ const yString = (0, rounding_1.toRoundedString)(translate.y);
411
+ const xShiftDigits = getShift(xString);
412
+ const yShiftDigits = getShift(yString);
413
+ const shift = Vec2_1.Vec2.of(Math.pow(10, xShiftDigits), Math.pow(10, yShiftDigits));
414
+ const invShift = Vec2_1.Vec2.of(Math.pow(10, -xShiftDigits), Math.pow(10, -yShiftDigits));
415
+ addScale(invShift);
416
+ const shiftedTranslate = translate.scale(shift);
417
+ const roundedShiftedTranslate = Vec2_1.Vec2.of(Math.floor(shiftedTranslate.x), Math.floor(shiftedTranslate.y));
418
+ addTranslate(roundedShiftedTranslate);
419
+ // Don't recurse more than 3 times -- the more times we recurse, the more
420
+ // the scaling is influenced by error.
421
+ if (!roundedShiftedTranslate.eq(shiftedTranslate) && depth < 3) {
422
+ addShiftedTranslate(shiftedTranslate.minus(roundedShiftedTranslate), depth + 1);
423
+ }
424
+ addScale(shift);
425
+ return translate;
426
+ };
427
+ const adjustTransformFromScale = () => {
428
+ if (lastScale) {
429
+ const scaledTransform = transform.rightMul(Mat33.scaling2D(lastScale));
430
+ // If adding the scale to the transform leads to large values, avoid
431
+ // doing this.
432
+ if (scaledTransform.maximumEntryMagnitude() < 1e12) {
433
+ transforms.pop();
434
+ transform = transform.rightMul(Mat33.scaling2D(lastScale));
435
+ lastScale = null;
436
+ }
437
+ }
438
+ };
439
+ addShiftedTranslate(translation);
440
+ adjustTransformFromScale();
441
+ if (!transform.eq(Mat33.identity)) {
442
+ transforms.push(transform.toCSSMatrix());
443
+ }
444
+ return transforms.join(' ');
445
+ }
303
446
  /**
304
447
  * Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
305
448
  *
@@ -314,30 +457,95 @@ class Mat33 {
314
457
  if (cssString === '' || cssString === 'none') {
315
458
  return Mat33.identity;
316
459
  }
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}`);
460
+ const parseArguments = (argumentString) => {
461
+ return argumentString.split(/[, \t\n]+/g).map(argString => {
462
+ let isPercentage = false;
463
+ if (argString.endsWith('%')) {
464
+ isPercentage = true;
465
+ argString = argString.substring(0, argString.length - 1);
466
+ }
467
+ // Remove trailing px units.
468
+ argString = argString.replace(/px$/ig, '');
469
+ const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i;
470
+ if (!numberExp.exec(argString)) {
471
+ throw new Error(`All arguments to transform functions must be numeric (state: ${JSON.stringify({
472
+ currentArgument: argString,
473
+ allArguments: argumentString,
474
+ })})`);
475
+ }
476
+ let argNumber = parseFloat(argString);
477
+ if (isPercentage) {
478
+ argNumber /= 100;
479
+ }
480
+ return argNumber;
481
+ });
482
+ };
483
+ const keywordToAction = {
484
+ matrix: (matrixData) => {
485
+ if (matrixData.length !== 6) {
486
+ throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`);
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
+ const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
495
+ return transform;
496
+ },
497
+ scale: (scaleArgs) => {
498
+ let scaleX, scaleY;
499
+ if (scaleArgs.length === 1) {
500
+ scaleX = scaleArgs[0];
501
+ scaleY = scaleArgs[0];
502
+ }
503
+ else if (scaleArgs.length === 2) {
504
+ scaleX = scaleArgs[0];
505
+ scaleY = scaleArgs[1];
506
+ }
507
+ else {
508
+ throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`);
509
+ }
510
+ return Mat33.scaling2D(Vec2_1.Vec2.of(scaleX, scaleY));
511
+ },
512
+ translate: (translateArgs) => {
513
+ let translateX = 0;
514
+ let translateY = 0;
515
+ if (translateArgs.length === 1) {
516
+ // If no y translation is given, assume 0.
517
+ translateX = translateArgs[0];
518
+ }
519
+ else if (translateArgs.length === 2) {
520
+ translateX = translateArgs[0];
521
+ translateY = translateArgs[1];
522
+ }
523
+ else {
524
+ throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`);
525
+ }
526
+ return Mat33.translation(Vec2_1.Vec2.of(translateX, translateY));
527
+ },
528
+ };
529
+ // A command (\w+)
530
+ // followed by a set of arguments ([ \t\n0-9eE.,\-%]+)
531
+ const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig;
532
+ let match;
533
+ let matrix = null;
534
+ while ((match = partRegex.exec(cssString)) !== null) {
535
+ const action = match[1].toLowerCase();
536
+ if (!(action in keywordToAction)) {
537
+ throw new Error(`Unsupported CSS transform action: ${action}`);
538
+ }
539
+ const args = parseArguments(match[2]);
540
+ const currentMatrix = keywordToAction[action](args);
541
+ if (!matrix) {
542
+ matrix = currentMatrix;
543
+ }
544
+ else {
545
+ matrix = matrix.rightMul(currentMatrix);
546
+ }
331
547
  }
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;
548
+ return matrix ?? Mat33.identity;
341
549
  }
342
550
  }
343
551
  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,15 +107,45 @@ 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;
136
+ /**
137
+ * @beta May change or even be removed between minor releases.
138
+ *
139
+ * Converts this matrix into a list of CSS transforms that attempt to preserve
140
+ * this matrix's translation.
141
+ *
142
+ * In Chrome/Firefox, translation attributes only support 6 digits (likely an artifact
143
+ * of using lower-precision floating point numbers). This works around
144
+ * that by expanding this matrix into the product of several CSS transforms.
145
+ *
146
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
147
+ */
148
+ toSafeCSSTransformList(): string;
119
149
  /**
120
150
  * Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
121
151
  *
@@ -1,5 +1,6 @@
1
1
  import { Vec2 } from './Vec2.mjs';
2
2
  import Vec3 from './Vec3.mjs';
3
+ import { toRoundedString } from './rounding.mjs';
3
4
  /**
4
5
  * Represents a three dimensional linear transformation or
5
6
  * a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
@@ -253,9 +254,30 @@ export class Mat33 {
253
254
  getScaleFactor() {
254
255
  return Math.hypot(this.a1, this.a2);
255
256
  }
257
+ /** Returns the `idx`-th column (`idx` is 0-indexed). */
258
+ getColumn(idx) {
259
+ return Vec3.of(this.rows[0].at(idx), this.rows[1].at(idx), this.rows[2].at(idx));
260
+ }
261
+ /** Returns the magnitude of the entry with the largest entry */
262
+ maximumEntryMagnitude() {
263
+ let greatestSoFar = Math.abs(this.a1);
264
+ for (const entry of this.toArray()) {
265
+ greatestSoFar = Math.max(greatestSoFar, Math.abs(entry));
266
+ }
267
+ return greatestSoFar;
268
+ }
256
269
  /**
257
270
  * Constructs a 3x3 translation matrix (for translating `Vec2`s) using
258
271
  * **transformVec2**.
272
+ *
273
+ * Creates a matrix in the form
274
+ * $$
275
+ * \begin{pmatrix}
276
+ * 1 & 0 & {\tt amount.x}\\
277
+ * 0 & 1 & {\tt amount.y}\\
278
+ * 0 & 0 & 1
279
+ * \end{pmatrix}
280
+ * $$
259
281
  */
260
282
  static translation(amount) {
261
283
  // When transforming Vec2s by a 3x3 matrix, we give the input
@@ -290,10 +312,131 @@ export class Mat33 {
290
312
  // Translate such that [center] goes to (0, 0)
291
313
  return result.rightMul(Mat33.translation(center.times(-1)));
292
314
  }
293
- /** @see {@link fromCSSMatrix} */
315
+ /**
316
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
317
+ *
318
+ * @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
319
+ */
294
320
  toCSSMatrix() {
295
321
  return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
296
322
  }
323
+ /**
324
+ * @beta May change or even be removed between minor releases.
325
+ *
326
+ * Converts this matrix into a list of CSS transforms that attempt to preserve
327
+ * this matrix's translation.
328
+ *
329
+ * In Chrome/Firefox, translation attributes only support 6 digits (likely an artifact
330
+ * of using lower-precision floating point numbers). This works around
331
+ * that by expanding this matrix into the product of several CSS transforms.
332
+ *
333
+ * **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
334
+ */
335
+ toSafeCSSTransformList() {
336
+ // Check whether it's safe to return just the CSS matrix
337
+ const translation = Vec2.of(this.a3, this.b3);
338
+ const translationRoundedX = toRoundedString(translation.x);
339
+ const translationRoundedY = toRoundedString(translation.y);
340
+ const nonDigitsRegex = /[^0-9]+/g;
341
+ const translationXDigits = translationRoundedX.replace(nonDigitsRegex, '').length;
342
+ const translationYDigits = translationRoundedY.replace(nonDigitsRegex, '').length;
343
+ // Is it safe to just return the default CSS matrix?
344
+ if (translationXDigits <= 5 && translationYDigits <= 5) {
345
+ return this.toCSSMatrix();
346
+ }
347
+ // Remove the last column (the translation column)
348
+ let transform = new Mat33(this.a1, this.a2, 0, this.b1, this.b2, 0, 0, 0, 1);
349
+ const transforms = [];
350
+ let lastScale = null;
351
+ // Appends a translate() command to the list of `transforms`.
352
+ const addTranslate = (translation) => {
353
+ lastScale = null;
354
+ if (!translation.eq(Vec2.zero)) {
355
+ transforms.push(`translate(${toRoundedString(translation.x)}px, ${toRoundedString(translation.y)}px)`);
356
+ }
357
+ };
358
+ // Appends a scale() command to the list of transforms, possibly merging with
359
+ // the last command, if a scale().
360
+ const addScale = (scale) => {
361
+ // Merge with the last scale
362
+ if (lastScale) {
363
+ const newScale = lastScale.scale(scale);
364
+ // Don't merge if the new scale has very large values
365
+ if (newScale.maximumEntryMagnitude() < 1e7) {
366
+ const previousCommand = transforms.pop();
367
+ console.assert(previousCommand.startsWith('scale'), 'Invalid state: Merging scale commands');
368
+ scale = newScale;
369
+ }
370
+ }
371
+ if (scale.x === scale.y) {
372
+ transforms.push(`scale(${toRoundedString(scale.x)})`);
373
+ }
374
+ else {
375
+ transforms.push(`scale(${toRoundedString(scale.x)}, ${toRoundedString(scale.y)})`);
376
+ }
377
+ lastScale = scale;
378
+ };
379
+ // Returns the number of digits before the `.` in the given number string.
380
+ const digitsPreDecimalCount = (numberString) => {
381
+ let decimalIndex = numberString.indexOf('.');
382
+ if (decimalIndex === -1) {
383
+ decimalIndex = numberString.length;
384
+ }
385
+ return numberString.substring(0, decimalIndex).replace(nonDigitsRegex, '').length;
386
+ };
387
+ // Returns the number of digits (positive for left shift, negative for right shift)
388
+ // required to shift the decimal to the middle of the number.
389
+ const getShift = (numberString) => {
390
+ const preDecimal = digitsPreDecimalCount(numberString);
391
+ const postDecimal = (numberString.match(/[.](\d*)/) ?? ['', ''])[1].length;
392
+ // The shift required to center the decimal point.
393
+ const toCenter = postDecimal - preDecimal;
394
+ // toCenter is positive for a left shift (adding more pre-decimals),
395
+ // so, after applying it,
396
+ const postShiftPreDecimal = preDecimal + toCenter;
397
+ // We want the digits before the decimal to have a length at most 4, however.
398
+ // Thus, right shift until this is the case.
399
+ const shiftForAtMost5DigitsPreDecimal = 4 - Math.max(postShiftPreDecimal, 4);
400
+ return toCenter + shiftForAtMost5DigitsPreDecimal;
401
+ };
402
+ const addShiftedTranslate = (translate, depth = 0) => {
403
+ const xString = toRoundedString(translate.x);
404
+ const yString = toRoundedString(translate.y);
405
+ const xShiftDigits = getShift(xString);
406
+ const yShiftDigits = getShift(yString);
407
+ const shift = Vec2.of(Math.pow(10, xShiftDigits), Math.pow(10, yShiftDigits));
408
+ const invShift = Vec2.of(Math.pow(10, -xShiftDigits), Math.pow(10, -yShiftDigits));
409
+ addScale(invShift);
410
+ const shiftedTranslate = translate.scale(shift);
411
+ const roundedShiftedTranslate = Vec2.of(Math.floor(shiftedTranslate.x), Math.floor(shiftedTranslate.y));
412
+ addTranslate(roundedShiftedTranslate);
413
+ // Don't recurse more than 3 times -- the more times we recurse, the more
414
+ // the scaling is influenced by error.
415
+ if (!roundedShiftedTranslate.eq(shiftedTranslate) && depth < 3) {
416
+ addShiftedTranslate(shiftedTranslate.minus(roundedShiftedTranslate), depth + 1);
417
+ }
418
+ addScale(shift);
419
+ return translate;
420
+ };
421
+ const adjustTransformFromScale = () => {
422
+ if (lastScale) {
423
+ const scaledTransform = transform.rightMul(Mat33.scaling2D(lastScale));
424
+ // If adding the scale to the transform leads to large values, avoid
425
+ // doing this.
426
+ if (scaledTransform.maximumEntryMagnitude() < 1e12) {
427
+ transforms.pop();
428
+ transform = transform.rightMul(Mat33.scaling2D(lastScale));
429
+ lastScale = null;
430
+ }
431
+ }
432
+ };
433
+ addShiftedTranslate(translation);
434
+ adjustTransformFromScale();
435
+ if (!transform.eq(Mat33.identity)) {
436
+ transforms.push(transform.toCSSMatrix());
437
+ }
438
+ return transforms.join(' ');
439
+ }
297
440
  /**
298
441
  * Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
299
442
  *
@@ -308,30 +451,95 @@ export class Mat33 {
308
451
  if (cssString === '' || cssString === 'none') {
309
452
  return Mat33.identity;
310
453
  }
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}`);
454
+ const parseArguments = (argumentString) => {
455
+ return argumentString.split(/[, \t\n]+/g).map(argString => {
456
+ let isPercentage = false;
457
+ if (argString.endsWith('%')) {
458
+ isPercentage = true;
459
+ argString = argString.substring(0, argString.length - 1);
460
+ }
461
+ // Remove trailing px units.
462
+ argString = argString.replace(/px$/ig, '');
463
+ const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i;
464
+ if (!numberExp.exec(argString)) {
465
+ throw new Error(`All arguments to transform functions must be numeric (state: ${JSON.stringify({
466
+ currentArgument: argString,
467
+ allArguments: argumentString,
468
+ })})`);
469
+ }
470
+ let argNumber = parseFloat(argString);
471
+ if (isPercentage) {
472
+ argNumber /= 100;
473
+ }
474
+ return argNumber;
475
+ });
476
+ };
477
+ const keywordToAction = {
478
+ matrix: (matrixData) => {
479
+ if (matrixData.length !== 6) {
480
+ throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`);
481
+ }
482
+ const a = matrixData[0];
483
+ const b = matrixData[1];
484
+ const c = matrixData[2];
485
+ const d = matrixData[3];
486
+ const e = matrixData[4];
487
+ const f = matrixData[5];
488
+ const transform = new Mat33(a, c, e, b, d, f, 0, 0, 1);
489
+ return transform;
490
+ },
491
+ scale: (scaleArgs) => {
492
+ let scaleX, scaleY;
493
+ if (scaleArgs.length === 1) {
494
+ scaleX = scaleArgs[0];
495
+ scaleY = scaleArgs[0];
496
+ }
497
+ else if (scaleArgs.length === 2) {
498
+ scaleX = scaleArgs[0];
499
+ scaleY = scaleArgs[1];
500
+ }
501
+ else {
502
+ throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`);
503
+ }
504
+ return Mat33.scaling2D(Vec2.of(scaleX, scaleY));
505
+ },
506
+ translate: (translateArgs) => {
507
+ let translateX = 0;
508
+ let translateY = 0;
509
+ if (translateArgs.length === 1) {
510
+ // If no y translation is given, assume 0.
511
+ translateX = translateArgs[0];
512
+ }
513
+ else if (translateArgs.length === 2) {
514
+ translateX = translateArgs[0];
515
+ translateY = translateArgs[1];
516
+ }
517
+ else {
518
+ throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`);
519
+ }
520
+ return Mat33.translation(Vec2.of(translateX, translateY));
521
+ },
522
+ };
523
+ // A command (\w+)
524
+ // followed by a set of arguments ([ \t\n0-9eE.,\-%]+)
525
+ const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig;
526
+ let match;
527
+ let matrix = null;
528
+ while ((match = partRegex.exec(cssString)) !== null) {
529
+ const action = match[1].toLowerCase();
530
+ if (!(action in keywordToAction)) {
531
+ throw new Error(`Unsupported CSS transform action: ${action}`);
532
+ }
533
+ const args = parseArguments(match[2]);
534
+ const currentMatrix = keywordToAction[action](args);
535
+ if (!matrix) {
536
+ matrix = currentMatrix;
537
+ }
538
+ else {
539
+ matrix = matrix.rightMul(currentMatrix);
540
+ }
325
541
  }
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;
542
+ return matrix ?? Mat33.identity;
335
543
  }
336
544
  }
337
545
  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.3.1",
4
4
  "description": "A math library for js-draw. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -22,7 +22,7 @@
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"
@@ -45,5 +45,5 @@
45
45
  "svg",
46
46
  "math"
47
47
  ],
48
- "gitHead": "46b3d8f819f8e083f6e3e1d01e027e4311355456"
48
+ "gitHead": "65af7ec944f70b69b2a4b07d98e5bb92eeeca029"
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
+