@js-draw/math 1.2.2 → 1.3.1

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.
@@ -12,7 +12,7 @@ import Vec3 from './Vec3';
12
12
  * console.log('To string:', Color4.orange.toHexString());
13
13
  * ```
14
14
  */
15
- export default class Color4 {
15
+ export declare class Color4 {
16
16
  /** Red component. Should be in the range [0, 1]. */
17
17
  readonly r: number;
18
18
  /** Green component. ${\tt g} \in [0, 1]$ */
@@ -120,4 +120,4 @@ export default class Color4 {
120
120
  static gray: Color4;
121
121
  static white: Color4;
122
122
  }
123
- export { Color4 };
123
+ export default Color4;
@@ -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
  *
@@ -53,6 +53,13 @@ export declare class Path {
53
53
  getExactBBox(): Rect2;
54
54
  private cachedGeometry;
55
55
  get geometry(): GeometryArrayType;
56
+ /**
57
+ * Iterates through the start/end points of each component in this path.
58
+ *
59
+ * If a start point is equivalent to the end point of the previous segment,
60
+ * the point is **not** emitted twice.
61
+ */
62
+ startEndPoints(): Generator<import("../Vec3").Vec3, undefined, unknown>;
56
63
  private cachedPolylineApproximation;
57
64
  polylineApproximation(): LineSegment2[];
58
65
  static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2;
@@ -51,6 +51,7 @@ class Path {
51
51
  let startPoint = this.startPoint;
52
52
  const geometry = [];
53
53
  for (const part of this.parts) {
54
+ let exhaustivenessCheck;
54
55
  switch (part.kind) {
55
56
  case PathCommandType.CubicBezierTo:
56
57
  geometry.push(new CubicBezier_1.default(startPoint, part.controlPoint1, part.controlPoint2, part.endPoint));
@@ -68,11 +69,43 @@ class Path {
68
69
  geometry.push(new PointShape2D_1.default(part.point));
69
70
  startPoint = part.point;
70
71
  break;
72
+ default:
73
+ exhaustivenessCheck = part;
74
+ return exhaustivenessCheck;
71
75
  }
72
76
  }
73
77
  this.cachedGeometry = geometry;
74
78
  return this.cachedGeometry;
75
79
  }
80
+ /**
81
+ * Iterates through the start/end points of each component in this path.
82
+ *
83
+ * If a start point is equivalent to the end point of the previous segment,
84
+ * the point is **not** emitted twice.
85
+ */
86
+ *startEndPoints() {
87
+ yield this.startPoint;
88
+ for (const part of this.parts) {
89
+ let exhaustivenessCheck;
90
+ switch (part.kind) {
91
+ case PathCommandType.CubicBezierTo:
92
+ yield part.endPoint;
93
+ break;
94
+ case PathCommandType.QuadraticBezierTo:
95
+ yield part.endPoint;
96
+ break;
97
+ case PathCommandType.LineTo:
98
+ yield part.point;
99
+ break;
100
+ case PathCommandType.MoveTo:
101
+ yield part.point;
102
+ break;
103
+ default:
104
+ exhaustivenessCheck = part;
105
+ return exhaustivenessCheck;
106
+ }
107
+ }
108
+ }
76
109
  // Approximates this path with a group of line segments.
77
110
  polylineApproximation() {
78
111
  if (this.cachedPolylineApproximation) {
@@ -19,7 +19,6 @@ export declare class Rect2 extends Abstract2DShape {
19
19
  readonly h: number;
20
20
  readonly topLeft: Point2;
21
21
  readonly size: Vec2;
22
- readonly bottomRight: Point2;
23
22
  readonly area: number;
24
23
  constructor(x: number, y: number, w: number, h: number);
25
24
  translatedBy(vec: Vec2): Rect2;
@@ -35,6 +34,7 @@ export declare class Rect2 extends Abstract2DShape {
35
34
  getClosestPointOnBoundaryTo(target: Point2): Vec3;
36
35
  get corners(): Point2[];
37
36
  get maxDimension(): number;
37
+ get bottomRight(): Vec3;
38
38
  get topRight(): Vec3;
39
39
  get bottomLeft(): Vec3;
40
40
  get width(): number;
@@ -26,7 +26,6 @@ class Rect2 extends Abstract2DShape_1.default {
26
26
  // Precompute/store vector forms.
27
27
  this.topLeft = Vec2_1.Vec2.of(this.x, this.y);
28
28
  this.size = Vec2_1.Vec2.of(this.w, this.h);
29
- this.bottomRight = this.topLeft.plus(this.size);
30
29
  this.area = this.w * this.h;
31
30
  }
32
31
  translatedBy(vec) {
@@ -42,8 +41,8 @@ class Rect2 extends Abstract2DShape_1.default {
42
41
  }
43
42
  containsRect(other) {
44
43
  return this.x <= other.x && this.y <= other.y
45
- && this.bottomRight.x >= other.bottomRight.x
46
- && this.bottomRight.y >= other.bottomRight.y;
44
+ && this.x + this.w >= other.x + other.w
45
+ && this.y + this.h >= other.y + other.h;
47
46
  }
48
47
  intersects(other) {
49
48
  // Project along x/y axes.
@@ -116,6 +115,12 @@ class Rect2 extends Abstract2DShape_1.default {
116
115
  if (margin === 0) {
117
116
  return this;
118
117
  }
118
+ // Prevent width/height from being negative
119
+ if (margin < 0) {
120
+ const xMargin = -Math.min(-margin, this.w / 2);
121
+ const yMargin = -Math.min(-margin, this.h / 2);
122
+ return new Rect2(this.x - xMargin, this.y - yMargin, this.w + xMargin * 2, this.h + yMargin * 2);
123
+ }
119
124
  return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
120
125
  }
121
126
  getClosestPointOnBoundaryTo(target) {
@@ -144,6 +149,9 @@ class Rect2 extends Abstract2DShape_1.default {
144
149
  get maxDimension() {
145
150
  return Math.max(this.w, this.h);
146
151
  }
152
+ get bottomRight() {
153
+ return this.topLeft.plus(this.size);
154
+ }
147
155
  get topRight() {
148
156
  return this.bottomRight.plus(Vec2_1.Vec2.of(0, -this.h));
149
157
  }
@@ -234,16 +242,16 @@ class Rect2 extends Abstract2DShape_1.default {
234
242
  return Rect2.empty;
235
243
  }
236
244
  const firstRect = rects[0];
237
- let minX = firstRect.topLeft.x;
238
- let minY = firstRect.topLeft.y;
239
- let maxX = firstRect.bottomRight.x;
240
- let maxY = firstRect.bottomRight.y;
245
+ let minX = firstRect.x;
246
+ let minY = firstRect.y;
247
+ let maxX = firstRect.x + firstRect.w;
248
+ let maxY = firstRect.y + firstRect.h;
241
249
  for (let i = 1; i < rects.length; i++) {
242
250
  const rect = rects[i];
243
- minX = Math.min(minX, rect.topLeft.x);
244
- minY = Math.min(minY, rect.topLeft.y);
245
- maxX = Math.max(maxX, rect.bottomRight.x);
246
- maxY = Math.max(maxY, rect.bottomRight.y);
251
+ minX = Math.min(minX, rect.x);
252
+ minY = Math.min(minY, rect.y);
253
+ maxX = Math.max(maxX, rect.x + rect.w);
254
+ maxY = Math.max(maxY, rect.y + rect.h);
247
255
  }
248
256
  return new Rect2(minX, minY, maxX - minX, maxY - minY);
249
257
  }
@@ -12,7 +12,7 @@ import Vec3 from './Vec3';
12
12
  * console.log('To string:', Color4.orange.toHexString());
13
13
  * ```
14
14
  */
15
- export default class Color4 {
15
+ export declare class Color4 {
16
16
  /** Red component. Should be in the range [0, 1]. */
17
17
  readonly r: number;
18
18
  /** Green component. ${\tt g} \in [0, 1]$ */
@@ -120,4 +120,4 @@ export default class Color4 {
120
120
  static gray: Color4;
121
121
  static white: Color4;
122
122
  }
123
- export { Color4 };
123
+ export default Color4;
@@ -12,7 +12,7 @@ import Vec3 from './Vec3.mjs';
12
12
  * console.log('To string:', Color4.orange.toHexString());
13
13
  * ```
14
14
  */
15
- class Color4 {
15
+ export class Color4 {
16
16
  constructor(
17
17
  /** Red component. Should be in the range [0, 1]. */
18
18
  r,
@@ -377,4 +377,3 @@ Color4.black = Color4.ofRGB(0, 0, 0);
377
377
  Color4.gray = Color4.ofRGB(0.5, 0.5, 0.5);
378
378
  Color4.white = Color4.ofRGB(1, 1, 1);
379
379
  export default Color4;
380
- export { Color4 };
@@ -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
  *