@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.
@@ -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
  *
@@ -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;
@@ -45,6 +45,7 @@ export class Path {
45
45
  let startPoint = this.startPoint;
46
46
  const geometry = [];
47
47
  for (const part of this.parts) {
48
+ let exhaustivenessCheck;
48
49
  switch (part.kind) {
49
50
  case PathCommandType.CubicBezierTo:
50
51
  geometry.push(new CubicBezier(startPoint, part.controlPoint1, part.controlPoint2, part.endPoint));
@@ -62,11 +63,43 @@ export class Path {
62
63
  geometry.push(new PointShape2D(part.point));
63
64
  startPoint = part.point;
64
65
  break;
66
+ default:
67
+ exhaustivenessCheck = part;
68
+ return exhaustivenessCheck;
65
69
  }
66
70
  }
67
71
  this.cachedGeometry = geometry;
68
72
  return this.cachedGeometry;
69
73
  }
74
+ /**
75
+ * Iterates through the start/end points of each component in this path.
76
+ *
77
+ * If a start point is equivalent to the end point of the previous segment,
78
+ * the point is **not** emitted twice.
79
+ */
80
+ *startEndPoints() {
81
+ yield this.startPoint;
82
+ for (const part of this.parts) {
83
+ let exhaustivenessCheck;
84
+ switch (part.kind) {
85
+ case PathCommandType.CubicBezierTo:
86
+ yield part.endPoint;
87
+ break;
88
+ case PathCommandType.QuadraticBezierTo:
89
+ yield part.endPoint;
90
+ break;
91
+ case PathCommandType.LineTo:
92
+ yield part.point;
93
+ break;
94
+ case PathCommandType.MoveTo:
95
+ yield part.point;
96
+ break;
97
+ default:
98
+ exhaustivenessCheck = part;
99
+ return exhaustivenessCheck;
100
+ }
101
+ }
102
+ }
70
103
  // Approximates this path with a group of line segments.
71
104
  polylineApproximation() {
72
105
  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;
@@ -20,7 +20,6 @@ export class Rect2 extends Abstract2DShape {
20
20
  // Precompute/store vector forms.
21
21
  this.topLeft = Vec2.of(this.x, this.y);
22
22
  this.size = Vec2.of(this.w, this.h);
23
- this.bottomRight = this.topLeft.plus(this.size);
24
23
  this.area = this.w * this.h;
25
24
  }
26
25
  translatedBy(vec) {
@@ -36,8 +35,8 @@ export class Rect2 extends Abstract2DShape {
36
35
  }
37
36
  containsRect(other) {
38
37
  return this.x <= other.x && this.y <= other.y
39
- && this.bottomRight.x >= other.bottomRight.x
40
- && this.bottomRight.y >= other.bottomRight.y;
38
+ && this.x + this.w >= other.x + other.w
39
+ && this.y + this.h >= other.y + other.h;
41
40
  }
42
41
  intersects(other) {
43
42
  // Project along x/y axes.
@@ -110,6 +109,12 @@ export class Rect2 extends Abstract2DShape {
110
109
  if (margin === 0) {
111
110
  return this;
112
111
  }
112
+ // Prevent width/height from being negative
113
+ if (margin < 0) {
114
+ const xMargin = -Math.min(-margin, this.w / 2);
115
+ const yMargin = -Math.min(-margin, this.h / 2);
116
+ return new Rect2(this.x - xMargin, this.y - yMargin, this.w + xMargin * 2, this.h + yMargin * 2);
117
+ }
113
118
  return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
114
119
  }
115
120
  getClosestPointOnBoundaryTo(target) {
@@ -138,6 +143,9 @@ export class Rect2 extends Abstract2DShape {
138
143
  get maxDimension() {
139
144
  return Math.max(this.w, this.h);
140
145
  }
146
+ get bottomRight() {
147
+ return this.topLeft.plus(this.size);
148
+ }
141
149
  get topRight() {
142
150
  return this.bottomRight.plus(Vec2.of(0, -this.h));
143
151
  }
@@ -228,16 +236,16 @@ export class Rect2 extends Abstract2DShape {
228
236
  return Rect2.empty;
229
237
  }
230
238
  const firstRect = rects[0];
231
- let minX = firstRect.topLeft.x;
232
- let minY = firstRect.topLeft.y;
233
- let maxX = firstRect.bottomRight.x;
234
- let maxY = firstRect.bottomRight.y;
239
+ let minX = firstRect.x;
240
+ let minY = firstRect.y;
241
+ let maxX = firstRect.x + firstRect.w;
242
+ let maxY = firstRect.y + firstRect.h;
235
243
  for (let i = 1; i < rects.length; i++) {
236
244
  const rect = rects[i];
237
- minX = Math.min(minX, rect.topLeft.x);
238
- minY = Math.min(minY, rect.topLeft.y);
239
- maxX = Math.max(maxX, rect.bottomRight.x);
240
- maxY = Math.max(maxY, rect.bottomRight.y);
245
+ minX = Math.min(minX, rect.x);
246
+ minY = Math.min(minY, rect.y);
247
+ maxX = Math.max(maxX, rect.x + rect.w);
248
+ maxY = Math.max(maxY, rect.y + rect.h);
241
249
  }
242
250
  return new Rect2(minX, minY, maxX - minX, maxY - minY);
243
251
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-draw/math",
3
- "version": "1.2.2",
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": "ced98ae289f299c44b54515571d0087aa6d32cdc"
48
+ "gitHead": "65af7ec944f70b69b2a4b07d98e5bb92eeeca029"
49
49
  }
package/src/Color4.ts CHANGED
@@ -13,7 +13,7 @@ import Vec3 from './Vec3';
13
13
  * console.log('To string:', Color4.orange.toHexString());
14
14
  * ```
15
15
  */
16
- export default class Color4 {
16
+ export class Color4 {
17
17
  private constructor(
18
18
  /** Red component. Should be in the range [0, 1]. */
19
19
  public readonly r: number,
@@ -437,4 +437,4 @@ export default class Color4 {
437
437
  public static white = Color4.ofRGB(1, 1, 1);
438
438
  }
439
439
 
440
- export { Color4 };
440
+ export default Color4;
@@ -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
  });