@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.
@@ -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
  });