@js-draw/math 1.3.0 → 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.
- package/dist/cjs/Mat33.d.ts +31 -1
- package/dist/cjs/Mat33.js +232 -24
- package/dist/cjs/Vec3.d.ts +7 -0
- package/dist/cjs/Vec3.js +9 -0
- package/dist/mjs/Mat33.d.ts +31 -1
- package/dist/mjs/Mat33.mjs +232 -24
- package/dist/mjs/Vec3.d.ts +7 -0
- package/dist/mjs/Vec3.mjs +9 -0
- package/package.json +3 -3
- package/src/Mat33.fromCSSMatrix.test.ts +90 -0
- package/src/Mat33.test.ts +6 -42
- package/src/Mat33.ts +143 -32
- package/src/Vec3.ts +10 -0
- package/src/rounding.ts +1 -0
- /package/dist/cjs/{Mat33.test.d.ts → Mat33.fromCSSMatrix.test.d.ts} +0 -0
- /package/dist/mjs/{Mat33.test.d.ts → Mat33.fromCSSMatrix.test.d.ts} +0 -0
package/dist/cjs/Mat33.d.ts
CHANGED
@@ -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
|
-
/**
|
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
|
-
/**
|
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
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
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
|
-
|
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;
|
package/dist/cjs/Vec3.d.ts
CHANGED
@@ -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
|
*
|
package/dist/mjs/Mat33.d.ts
CHANGED
@@ -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
|
-
/**
|
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/mjs/Mat33.mjs
CHANGED
@@ -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
|
-
/**
|
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
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
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);
|
package/dist/mjs/Vec3.d.ts
CHANGED
@@ -35,6 +35,13 @@ export declare class Vec3 {
|
|
35
35
|
length(): number;
|
36
36
|
magnitude(): number;
|
37
37
|
magnitudeSquared(): number;
|
38
|
+
/**
|
39
|
+
* Returns the entry of this with the greatest magnitude.
|
40
|
+
*
|
41
|
+
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
|
42
|
+
* all entries of this vector.
|
43
|
+
*/
|
44
|
+
maximumEntryMagnitude(): number;
|
38
45
|
/**
|
39
46
|
* Return this' angle in the XY plane (treats this as a Vec2).
|
40
47
|
*
|
package/dist/mjs/Vec3.mjs
CHANGED
@@ -55,6 +55,15 @@ export class Vec3 {
|
|
55
55
|
magnitudeSquared() {
|
56
56
|
return this.dot(this);
|
57
57
|
}
|
58
|
+
/**
|
59
|
+
* Returns the entry of this with the greatest magnitude.
|
60
|
+
*
|
61
|
+
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
|
62
|
+
* all entries of this vector.
|
63
|
+
*/
|
64
|
+
maximumEntryMagnitude() {
|
65
|
+
return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z)));
|
66
|
+
}
|
58
67
|
/**
|
59
68
|
* Return this' angle in the XY plane (treats this as a Vec2).
|
60
69
|
*
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@js-draw/math",
|
3
|
-
"version": "1.3.
|
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": "
|
48
|
+
"gitHead": "65af7ec944f70b69b2a4b07d98e5bb92eeeca029"
|
49
49
|
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import Mat33 from './Mat33';
|
2
|
+
import { Vec2 } from './Vec2';
|
3
|
+
|
4
|
+
describe('Mat33.fromCSSMatrix', () => {
|
5
|
+
it('should convert CSS matrix(...) strings to matricies', () => {
|
6
|
+
// From MDN:
|
7
|
+
// ⎡ a c e ⎤
|
8
|
+
// ⎢ b d f ⎥ = matrix(a,b,c,d,e,f)
|
9
|
+
// ⎣ 0 0 1 ⎦
|
10
|
+
const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0)');
|
11
|
+
expect(identity).objEq(Mat33.identity);
|
12
|
+
expect(Mat33.fromCSSMatrix('matrix(1, 2, 3, 4, 5, 6)')).objEq(new Mat33(
|
13
|
+
1, 3, 5,
|
14
|
+
2, 4, 6,
|
15
|
+
0, 0, 1,
|
16
|
+
));
|
17
|
+
expect(Mat33.fromCSSMatrix('matrix(1e2, 2, 3, 4, 5, 6)')).objEq(new Mat33(
|
18
|
+
1e2, 3, 5,
|
19
|
+
2, 4, 6,
|
20
|
+
0, 0, 1,
|
21
|
+
));
|
22
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6, 2, .3, 4, 5, 6)')).objEq(new Mat33(
|
23
|
+
1.6, .3, 5,
|
24
|
+
2, 4, 6,
|
25
|
+
0, 0, 1,
|
26
|
+
));
|
27
|
+
expect(Mat33.fromCSSMatrix('matrix(-1, 2, 3.E-2, 4, -5.123, -6.5)')).objEq(new Mat33(
|
28
|
+
-1, 0.03, -5.123,
|
29
|
+
2, 4, -6.5,
|
30
|
+
0, 0, 1,
|
31
|
+
));
|
32
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6,\n\t2, .3, 4, 5, 6)')).objEq(new Mat33(
|
33
|
+
1.6, .3, 5,
|
34
|
+
2, 4, 6,
|
35
|
+
0, 0, 1,
|
36
|
+
));
|
37
|
+
expect(Mat33.fromCSSMatrix('matrix(1.6,2, .3E-2, 4, 5, 6)')).objEq(new Mat33(
|
38
|
+
1.6, 3e-3, 5,
|
39
|
+
2, 4, 6,
|
40
|
+
0, 0, 1,
|
41
|
+
));
|
42
|
+
expect(Mat33.fromCSSMatrix('matrix(-1, 2e6, 3E-2,-5.123, -6.5e-1, 0.01)')).objEq(new Mat33(
|
43
|
+
-1, 3E-2, -6.5e-1,
|
44
|
+
2e6, -5.123, 0.01,
|
45
|
+
0, 0, 1,
|
46
|
+
));
|
47
|
+
});
|
48
|
+
|
49
|
+
it('should convert multi-matrix arguments into a single CSS matrix', () => {
|
50
|
+
const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0) matrix(1, 0, 0, 1, 0, 0)');
|
51
|
+
expect(identity).objEq(Mat33.identity);
|
52
|
+
|
53
|
+
expect(Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0) matrix(1, 2, 3, 4, 5, 6) matrix(1, 0, 0, 1, 0, 0)')).objEq(new Mat33(
|
54
|
+
1, 3, 5,
|
55
|
+
2, 4, 6,
|
56
|
+
0, 0, 1,
|
57
|
+
));
|
58
|
+
|
59
|
+
expect(Mat33.fromCSSMatrix('matrix(2,\n\t 0, 0, 2, 0, 0) matrix(1, 2, 3, 4, 5, 6) matrix(1, 0, 0, 1, 0, 0)')).objEq(new Mat33(
|
60
|
+
2, 6, 10,
|
61
|
+
4, 8, 12,
|
62
|
+
0, 0, 1,
|
63
|
+
));
|
64
|
+
});
|
65
|
+
|
66
|
+
it('should convert scale()s with a single argument', () => {
|
67
|
+
expect(Mat33.fromCSSMatrix('scale(1)')).objEq(Mat33.identity);
|
68
|
+
expect(Mat33.fromCSSMatrix('scale(0.4)')).objEq(Mat33.scaling2D(0.4));
|
69
|
+
expect(Mat33.fromCSSMatrix('scale(-0.4 )')).objEq(Mat33.scaling2D(-0.4));
|
70
|
+
expect(Mat33.fromCSSMatrix('scale(100%)')).objEq(Mat33.identity);
|
71
|
+
expect(Mat33.fromCSSMatrix('scale(20e2%)')).objEq(Mat33.scaling2D(20));
|
72
|
+
expect(Mat33.fromCSSMatrix('scale(200%) scale(50%)')).objEq(Mat33.identity);
|
73
|
+
});
|
74
|
+
|
75
|
+
it('should convert scale()s with two arguments', () => {
|
76
|
+
expect(Mat33.fromCSSMatrix('scale(1\t 1)')).objEq(Mat33.identity);
|
77
|
+
expect(Mat33.fromCSSMatrix('scale(1\t 2)')).objEq(Mat33.scaling2D(Vec2.of(1, 2)));
|
78
|
+
expect(Mat33.fromCSSMatrix('scale(1\t 2) scale(1)')).objEq(Mat33.scaling2D(Vec2.of(1, 2)));
|
79
|
+
});
|
80
|
+
|
81
|
+
it('should convert translate()s', () => {
|
82
|
+
expect(Mat33.fromCSSMatrix('translate(0)')).objEq(Mat33.identity);
|
83
|
+
expect(Mat33.fromCSSMatrix('translate(1, 1)')).objEq(Mat33.translation(Vec2.of(1, 1)));
|
84
|
+
expect(Mat33.fromCSSMatrix('translate(1 200%)')).objEq(Mat33.translation(Vec2.of(1, 2)));
|
85
|
+
});
|
86
|
+
|
87
|
+
it('should support px following numbers', () => {
|
88
|
+
expect(Mat33.fromCSSMatrix('translate(1px, 2px)')).objEq(Mat33.translation(Vec2.of(1, 2)));
|
89
|
+
});
|
90
|
+
});
|
package/src/Mat33.test.ts
CHANGED
@@ -198,47 +198,11 @@ describe('Mat33 tests', () => {
|
|
198
198
|
));
|
199
199
|
});
|
200
200
|
|
201
|
-
it('should
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
//
|
206
|
-
|
207
|
-
expect(identity).objEq(Mat33.identity);
|
208
|
-
expect(Mat33.fromCSSMatrix('matrix(1, 2, 3, 4, 5, 6)')).objEq(new Mat33(
|
209
|
-
1, 3, 5,
|
210
|
-
2, 4, 6,
|
211
|
-
0, 0, 1,
|
212
|
-
));
|
213
|
-
expect(Mat33.fromCSSMatrix('matrix(1e2, 2, 3, 4, 5, 6)')).objEq(new Mat33(
|
214
|
-
1e2, 3, 5,
|
215
|
-
2, 4, 6,
|
216
|
-
0, 0, 1,
|
217
|
-
));
|
218
|
-
expect(Mat33.fromCSSMatrix('matrix(1.6, 2, .3, 4, 5, 6)')).objEq(new Mat33(
|
219
|
-
1.6, .3, 5,
|
220
|
-
2, 4, 6,
|
221
|
-
0, 0, 1,
|
222
|
-
));
|
223
|
-
expect(Mat33.fromCSSMatrix('matrix(-1, 2, 3.E-2, 4, -5.123, -6.5)')).objEq(new Mat33(
|
224
|
-
-1, 0.03, -5.123,
|
225
|
-
2, 4, -6.5,
|
226
|
-
0, 0, 1,
|
227
|
-
));
|
228
|
-
expect(Mat33.fromCSSMatrix('matrix(1.6,\n\t2, .3, 4, 5, 6)')).objEq(new Mat33(
|
229
|
-
1.6, .3, 5,
|
230
|
-
2, 4, 6,
|
231
|
-
0, 0, 1,
|
232
|
-
));
|
233
|
-
expect(Mat33.fromCSSMatrix('matrix(1.6,2, .3E-2, 4, 5, 6)')).objEq(new Mat33(
|
234
|
-
1.6, 3e-3, 5,
|
235
|
-
2, 4, 6,
|
236
|
-
0, 0, 1,
|
237
|
-
));
|
238
|
-
expect(Mat33.fromCSSMatrix('matrix(-1, 2e6, 3E-2,-5.123, -6.5e-1, 0.01)')).objEq(new Mat33(
|
239
|
-
-1, 3E-2, -6.5e-1,
|
240
|
-
2e6, -5.123, 0.01,
|
241
|
-
0, 0, 1,
|
242
|
-
));
|
201
|
+
it('getColumn should return the given column index', () => {
|
202
|
+
expect(Mat33.identity.getColumn(0)).objEq(Vec3.unitX);
|
203
|
+
expect(Mat33.identity.getColumn(1)).objEq(Vec3.of(0, 1, 0));
|
204
|
+
|
205
|
+
// scaling2D only scales the x/y components of vectors it transforms
|
206
|
+
expect(Mat33.scaling2D(2).getColumn(2)).objEq(Vec3.of(0, 0, 1));
|
243
207
|
});
|
244
208
|
});
|
package/src/Mat33.ts
CHANGED
@@ -335,9 +335,37 @@ export class Mat33 {
|
|
335
335
|
return Math.hypot(this.a1, this.a2);
|
336
336
|
}
|
337
337
|
|
338
|
+
/** Returns the `idx`-th column (`idx` is 0-indexed). */
|
339
|
+
public getColumn(idx: number) {
|
340
|
+
return Vec3.of(
|
341
|
+
this.rows[0].at(idx),
|
342
|
+
this.rows[1].at(idx),
|
343
|
+
this.rows[2].at(idx),
|
344
|
+
);
|
345
|
+
}
|
346
|
+
|
347
|
+
/** Returns the magnitude of the entry with the largest entry */
|
348
|
+
public maximumEntryMagnitude() {
|
349
|
+
let greatestSoFar = Math.abs(this.a1);
|
350
|
+
for (const entry of this.toArray()) {
|
351
|
+
greatestSoFar = Math.max(greatestSoFar, Math.abs(entry));
|
352
|
+
}
|
353
|
+
|
354
|
+
return greatestSoFar;
|
355
|
+
}
|
356
|
+
|
338
357
|
/**
|
339
358
|
* Constructs a 3x3 translation matrix (for translating `Vec2`s) using
|
340
359
|
* **transformVec2**.
|
360
|
+
*
|
361
|
+
* Creates a matrix in the form
|
362
|
+
* $$
|
363
|
+
* \begin{pmatrix}
|
364
|
+
* 1 & 0 & {\tt amount.x}\\
|
365
|
+
* 0 & 1 & {\tt amount.y}\\
|
366
|
+
* 0 & 0 & 1
|
367
|
+
* \end{pmatrix}
|
368
|
+
* $$
|
341
369
|
*/
|
342
370
|
public static translation(amount: Vec2): Mat33 {
|
343
371
|
// When transforming Vec2s by a 3x3 matrix, we give the input
|
@@ -392,7 +420,11 @@ export class Mat33 {
|
|
392
420
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
393
421
|
}
|
394
422
|
|
395
|
-
/**
|
423
|
+
/**
|
424
|
+
* **Note**: Assumes `this.c1 = this.c2 = 0` and `this.c3 = 1`.
|
425
|
+
*
|
426
|
+
* @see {@link fromCSSMatrix} and {@link toSafeCSSTransformList}
|
427
|
+
*/
|
396
428
|
public toCSSMatrix(): string {
|
397
429
|
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
|
398
430
|
}
|
@@ -412,39 +444,118 @@ export class Mat33 {
|
|
412
444
|
return Mat33.identity;
|
413
445
|
}
|
414
446
|
|
415
|
-
const
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
447
|
+
const parseArguments = (argumentString: string) => {
|
448
|
+
return argumentString.split(/[, \t\n]+/g).map(argString => {
|
449
|
+
let isPercentage = false;
|
450
|
+
if (argString.endsWith('%')) {
|
451
|
+
isPercentage = true;
|
452
|
+
argString = argString.substring(0, argString.length - 1);
|
453
|
+
}
|
454
|
+
|
455
|
+
// Remove trailing px units.
|
456
|
+
argString = argString.replace(/px$/ig, '');
|
457
|
+
|
458
|
+
const numberExp = /^[-]?\d*(?:\.\d*)?(?:[eE][-+]?\d+)?$/i;
|
459
|
+
|
460
|
+
if (!numberExp.exec(argString)) {
|
461
|
+
throw new Error(
|
462
|
+
`All arguments to transform functions must be numeric (state: ${
|
463
|
+
JSON.stringify({
|
464
|
+
currentArgument: argString,
|
465
|
+
allArguments: argumentString,
|
466
|
+
})
|
467
|
+
})`
|
468
|
+
);
|
469
|
+
}
|
470
|
+
|
471
|
+
let argNumber = parseFloat(argString);
|
472
|
+
|
473
|
+
if (isPercentage) {
|
474
|
+
argNumber /= 100;
|
475
|
+
}
|
476
|
+
|
477
|
+
return argNumber;
|
478
|
+
});
|
479
|
+
};
|
480
|
+
|
481
|
+
|
482
|
+
const keywordToAction = {
|
483
|
+
matrix: (matrixData: number[]) => {
|
484
|
+
if (matrixData.length !== 6) {
|
485
|
+
throw new Error(`Invalid matrix argument: ${matrixData}. Must have length 6`);
|
486
|
+
}
|
487
|
+
|
488
|
+
const a = matrixData[0];
|
489
|
+
const b = matrixData[1];
|
490
|
+
const c = matrixData[2];
|
491
|
+
const d = matrixData[3];
|
492
|
+
const e = matrixData[4];
|
493
|
+
const f = matrixData[5];
|
494
|
+
|
495
|
+
const transform = new Mat33(
|
496
|
+
a, c, e,
|
497
|
+
b, d, f,
|
498
|
+
0, 0, 1
|
499
|
+
);
|
500
|
+
return transform;
|
501
|
+
},
|
502
|
+
|
503
|
+
scale: (scaleArgs: number[]) => {
|
504
|
+
let scaleX, scaleY;
|
505
|
+
if (scaleArgs.length === 1) {
|
506
|
+
scaleX = scaleArgs[0];
|
507
|
+
scaleY = scaleArgs[0];
|
508
|
+
} else if (scaleArgs.length === 2) {
|
509
|
+
scaleX = scaleArgs[0];
|
510
|
+
scaleY = scaleArgs[1];
|
511
|
+
} else {
|
512
|
+
throw new Error(`The scale() function only supports two arguments. Given: ${scaleArgs}`);
|
513
|
+
}
|
514
|
+
|
515
|
+
return Mat33.scaling2D(Vec2.of(scaleX, scaleY));
|
516
|
+
},
|
517
|
+
|
518
|
+
translate: (translateArgs: number[]) => {
|
519
|
+
let translateX = 0;
|
520
|
+
let translateY = 0;
|
521
|
+
|
522
|
+
if (translateArgs.length === 1) {
|
523
|
+
// If no y translation is given, assume 0.
|
524
|
+
translateX = translateArgs[0];
|
525
|
+
} else if (translateArgs.length === 2) {
|
526
|
+
translateX = translateArgs[0];
|
527
|
+
translateY = translateArgs[1];
|
528
|
+
} else {
|
529
|
+
throw new Error(`The translate() function requires either 1 or 2 arguments. Given ${translateArgs}`);
|
530
|
+
}
|
531
|
+
|
532
|
+
return Mat33.translation(Vec2.of(translateX, translateY));
|
533
|
+
},
|
534
|
+
};
|
535
|
+
|
536
|
+
// A command (\w+)
|
537
|
+
// followed by a set of arguments ([ \t\n0-9eE.,\-%]+)
|
538
|
+
const partRegex = /\s*(\w+)\s*\(([^)]*)\)/ig;
|
539
|
+
let match;
|
540
|
+
let matrix: Mat33|null = null;
|
541
|
+
|
542
|
+
while ((match = partRegex.exec(cssString)) !== null) {
|
543
|
+
const action = match[1].toLowerCase();
|
544
|
+
if (!(action in keywordToAction)) {
|
545
|
+
throw new Error(`Unsupported CSS transform action: ${action}`);
|
546
|
+
}
|
547
|
+
|
548
|
+
const args = parseArguments(match[2]);
|
549
|
+
const currentMatrix = keywordToAction[action as keyof typeof keywordToAction](args);
|
550
|
+
|
551
|
+
if (!matrix) {
|
552
|
+
matrix = currentMatrix;
|
553
|
+
} else {
|
554
|
+
matrix = matrix.rightMul(currentMatrix);
|
555
|
+
}
|
432
556
|
}
|
433
557
|
|
434
|
-
|
435
|
-
const a = matrixData[0];
|
436
|
-
const b = matrixData[1];
|
437
|
-
const c = matrixData[2];
|
438
|
-
const d = matrixData[3];
|
439
|
-
const e = matrixData[4];
|
440
|
-
const f = matrixData[5];
|
441
|
-
|
442
|
-
const transform = new Mat33(
|
443
|
-
a, c, e,
|
444
|
-
b, d, f,
|
445
|
-
0, 0, 1
|
446
|
-
);
|
447
|
-
return transform;
|
558
|
+
return matrix ?? Mat33.identity;
|
448
559
|
}
|
449
560
|
}
|
450
561
|
export default Mat33;
|
package/src/Vec3.ts
CHANGED
@@ -63,6 +63,16 @@ export class Vec3 {
|
|
63
63
|
return this.dot(this);
|
64
64
|
}
|
65
65
|
|
66
|
+
/**
|
67
|
+
* Returns the entry of this with the greatest magnitude.
|
68
|
+
*
|
69
|
+
* In other words, returns $\max \{ |x| : x \in {\bf v} \}$, where ${\bf v}$ is the set of
|
70
|
+
* all entries of this vector.
|
71
|
+
*/
|
72
|
+
public maximumEntryMagnitude(): number {
|
73
|
+
return Math.max(Math.abs(this.x), Math.max(Math.abs(this.y), Math.abs(this.z)));
|
74
|
+
}
|
75
|
+
|
66
76
|
/**
|
67
77
|
* Return this' angle in the XY plane (treats this as a Vec2).
|
68
78
|
*
|
package/src/rounding.ts
CHANGED
File without changes
|
File without changes
|