@js-draw/math 1.2.2 → 1.3.1

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