@js-draw/math 1.0.0 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- package/LICENSE +21 -0
- package/dist/cjs/Color4.test.d.ts +1 -0
- package/dist/cjs/Mat33.test.d.ts +1 -0
- package/dist/cjs/Vec2.test.d.ts +1 -0
- package/dist/cjs/Vec3.test.d.ts +1 -0
- package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
- package/dist/cjs/rounding.test.d.ts +1 -0
- package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.test.d.ts +1 -0
- package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
- package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
- package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
- package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
- package/dist/mjs/Color4.test.d.ts +1 -0
- package/dist/mjs/Mat33.test.d.ts +1 -0
- package/dist/mjs/Vec2.test.d.ts +1 -0
- package/dist/mjs/Vec3.test.d.ts +1 -0
- package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
- package/dist/mjs/rounding.test.d.ts +1 -0
- package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.test.d.ts +1 -0
- package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
- package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
- package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
- package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
- package/dist-test/test_imports/package-lock.json +13 -0
- package/dist-test/test_imports/package.json +12 -0
- package/dist-test/test_imports/test-imports.js +15 -0
- package/dist-test/test_imports/test-require.cjs +15 -0
- package/package.json +4 -3
- package/src/Color4.test.ts +52 -0
- package/src/Color4.ts +318 -0
- package/src/Mat33.test.ts +244 -0
- package/src/Mat33.ts +450 -0
- package/src/Vec2.test.ts +30 -0
- package/src/Vec2.ts +49 -0
- package/src/Vec3.test.ts +51 -0
- package/src/Vec3.ts +245 -0
- package/src/lib.ts +42 -0
- package/src/polynomial/solveQuadratic.test.ts +39 -0
- package/src/polynomial/solveQuadratic.ts +43 -0
- package/src/rounding.test.ts +65 -0
- package/src/rounding.ts +167 -0
- package/src/shapes/Abstract2DShape.ts +63 -0
- package/src/shapes/BezierJSWrapper.ts +93 -0
- package/src/shapes/CubicBezier.ts +35 -0
- package/src/shapes/LineSegment2.test.ts +99 -0
- package/src/shapes/LineSegment2.ts +232 -0
- package/src/shapes/Path.fromString.test.ts +223 -0
- package/src/shapes/Path.test.ts +309 -0
- package/src/shapes/Path.toString.test.ts +77 -0
- package/src/shapes/Path.ts +963 -0
- package/src/shapes/PointShape2D.ts +33 -0
- package/src/shapes/QuadraticBezier.test.ts +31 -0
- package/src/shapes/QuadraticBezier.ts +142 -0
- package/src/shapes/Rect2.test.ts +209 -0
- package/src/shapes/Rect2.ts +346 -0
- package/src/shapes/Triangle.test.ts +61 -0
- package/src/shapes/Triangle.ts +139 -0
@@ -0,0 +1,244 @@
|
|
1
|
+
import Mat33 from './Mat33';
|
2
|
+
import { Point2, Vec2 } from './Vec2';
|
3
|
+
import Vec3 from './Vec3';
|
4
|
+
|
5
|
+
|
6
|
+
describe('Mat33 tests', () => {
|
7
|
+
it('equality', () => {
|
8
|
+
expect(Mat33.identity).objEq(Mat33.identity);
|
9
|
+
expect(new Mat33(
|
10
|
+
0.1, 0.2, 0.3,
|
11
|
+
0.4, 0.5, 0.6,
|
12
|
+
0.7, 0.8, -0.9
|
13
|
+
)).objEq(new Mat33(
|
14
|
+
0.2, 0.1, 0.4,
|
15
|
+
0.5, 0.5, 0.7,
|
16
|
+
0.7, 0.8, -0.9
|
17
|
+
), 0.2);
|
18
|
+
});
|
19
|
+
|
20
|
+
it('transposition', () => {
|
21
|
+
expect(Mat33.identity.transposed()).objEq(Mat33.identity);
|
22
|
+
expect(new Mat33(
|
23
|
+
1, 2, 0,
|
24
|
+
0, 0, 0,
|
25
|
+
0, 1, 0
|
26
|
+
).transposed()).objEq(new Mat33(
|
27
|
+
1, 0, 0,
|
28
|
+
2, 0, 1,
|
29
|
+
0, 0, 0
|
30
|
+
));
|
31
|
+
});
|
32
|
+
|
33
|
+
it('multiplication', () => {
|
34
|
+
const M = new Mat33(
|
35
|
+
1, 2, 3,
|
36
|
+
4, 5, 6,
|
37
|
+
7, 8, 9
|
38
|
+
);
|
39
|
+
|
40
|
+
expect(Mat33.identity.rightMul(Mat33.identity)).objEq(Mat33.identity);
|
41
|
+
expect(M.rightMul(Mat33.identity)).objEq(M);
|
42
|
+
expect(M.rightMul(new Mat33(
|
43
|
+
1, 0, 0,
|
44
|
+
0, 2, 0,
|
45
|
+
0, 0, 1
|
46
|
+
))).objEq(new Mat33(
|
47
|
+
1, 4, 3,
|
48
|
+
4, 10, 6,
|
49
|
+
7, 16, 9
|
50
|
+
));
|
51
|
+
expect(M.rightMul(new Mat33(
|
52
|
+
2, 0, 1,
|
53
|
+
0, 1, 0,
|
54
|
+
0, 0, 3
|
55
|
+
))).objEq(new Mat33(
|
56
|
+
2, 2, 10,
|
57
|
+
8, 5, 22,
|
58
|
+
14, 8, 34
|
59
|
+
));
|
60
|
+
});
|
61
|
+
|
62
|
+
it('the inverse of the identity matrix should be the identity matrix', () => {
|
63
|
+
const fuzz = 0.01;
|
64
|
+
expect(Mat33.identity.inverse()).objEq(Mat33.identity, fuzz);
|
65
|
+
|
66
|
+
const M = new Mat33(
|
67
|
+
1, 2, 3,
|
68
|
+
4, 1, 0,
|
69
|
+
2, 3, 0
|
70
|
+
);
|
71
|
+
expect(M.inverse().rightMul(M)).objEq(Mat33.identity, fuzz);
|
72
|
+
});
|
73
|
+
|
74
|
+
it('90 degree z-rotation matricies should rotate 90 degrees counter clockwise', () => {
|
75
|
+
const fuzz = 0.001;
|
76
|
+
|
77
|
+
const M = Mat33.zRotation(Math.PI / 2);
|
78
|
+
const rotated = M.transformVec2(Vec2.unitX);
|
79
|
+
expect(rotated).objEq(Vec2.unitY, fuzz);
|
80
|
+
expect(M.transformVec2(rotated)).objEq(Vec2.unitX.times(-1), fuzz);
|
81
|
+
});
|
82
|
+
|
83
|
+
it('z-rotation matricies should preserve the given origin', () => {
|
84
|
+
const testPairs: Array<[number, Vec2]> = [
|
85
|
+
[ Math.PI / 2, Vec2.zero ],
|
86
|
+
[ -Math.PI / 2, Vec2.zero ],
|
87
|
+
[ -Math.PI / 2, Vec2.of(10, 10) ],
|
88
|
+
];
|
89
|
+
|
90
|
+
for (const [ angle, center ] of testPairs) {
|
91
|
+
expect(Mat33.zRotation(angle, center).transformVec2(center)).objEq(center);
|
92
|
+
}
|
93
|
+
});
|
94
|
+
|
95
|
+
it('translation matricies should translate Vec2s', () => {
|
96
|
+
const fuzz = 0.01;
|
97
|
+
|
98
|
+
const M = Mat33.translation(Vec2.of(1, -4));
|
99
|
+
expect(M.transformVec2(Vec2.of(0, 0))).objEq(Vec2.of(1, -4), fuzz);
|
100
|
+
expect(M.transformVec2(Vec2.of(-1, 3))).objEq(Vec2.of(0, -1), fuzz);
|
101
|
+
});
|
102
|
+
|
103
|
+
it('scaling matricies should scale about the provided center', () => {
|
104
|
+
const fuzz = 0.01;
|
105
|
+
|
106
|
+
const center = Vec2.of(1, -4);
|
107
|
+
const M = Mat33.scaling2D(2, center);
|
108
|
+
expect(M.transformVec2(center)).objEq(center, fuzz);
|
109
|
+
expect(M.transformVec2(Vec2.of(0, 0))).objEq(Vec2.of(-1, 4), fuzz);
|
110
|
+
});
|
111
|
+
|
112
|
+
it('calling inverse on singular matricies should result in the identity matrix', () => {
|
113
|
+
const fuzz = 0.001;
|
114
|
+
const singularMat = Mat33.ofRows(
|
115
|
+
Vec3.of(0, 0, 1),
|
116
|
+
Vec3.of(0, 1, 0),
|
117
|
+
Vec3.of(0, 1, 1)
|
118
|
+
);
|
119
|
+
expect(singularMat.invertable()).toBe(false);
|
120
|
+
expect(singularMat.inverse()).objEq(Mat33.identity, fuzz);
|
121
|
+
});
|
122
|
+
|
123
|
+
it('z-rotation matricies should be invertable', () => {
|
124
|
+
const fuzz = 0.01;
|
125
|
+
const M = Mat33.zRotation(-0.2617993877991494, Vec2.of(481, 329.5));
|
126
|
+
expect(
|
127
|
+
M.inverse().transformVec2(M.transformVec2(Vec2.unitX))
|
128
|
+
).objEq(Vec2.unitX, fuzz);
|
129
|
+
expect(M.invertable());
|
130
|
+
|
131
|
+
const starterTransform = new Mat33(
|
132
|
+
-0.2588190451025205, -0.9659258262890688, 923.7645204565603,
|
133
|
+
0.9659258262890688, -0.2588190451025205, -49.829447083761465,
|
134
|
+
0, 0, 1
|
135
|
+
);
|
136
|
+
expect(starterTransform.invertable()).toBe(true);
|
137
|
+
|
138
|
+
const fullTransform = starterTransform.rightMul(M);
|
139
|
+
const fullTransformInverse = fullTransform.inverse();
|
140
|
+
expect(fullTransform.invertable()).toBe(true);
|
141
|
+
|
142
|
+
expect(
|
143
|
+
fullTransformInverse.rightMul(fullTransform)
|
144
|
+
).objEq(Mat33.identity, fuzz);
|
145
|
+
|
146
|
+
expect(
|
147
|
+
fullTransform.transformVec2(fullTransformInverse.transformVec2(Vec2.unitX))
|
148
|
+
).objEq(Vec2.unitX, fuzz);
|
149
|
+
|
150
|
+
expect(
|
151
|
+
fullTransformInverse.transformVec2(fullTransform.transformVec2(Vec2.unitX))
|
152
|
+
).objEq(Vec2.unitX, fuzz);
|
153
|
+
});
|
154
|
+
|
155
|
+
it('z-rotation matrix inverses should undo the z-rotation', () => {
|
156
|
+
const testCases: Array<[ number, Point2 ]> = [
|
157
|
+
[ Math.PI / 2, Vec2.zero ],
|
158
|
+
[ Math.PI, Vec2.of(1, 1) ],
|
159
|
+
[ -Math.PI, Vec2.of(1, 1) ],
|
160
|
+
[ -Math.PI * 2, Vec2.of(1, 1) ],
|
161
|
+
[ -Math.PI * 2, Vec2.of(123, 456) ],
|
162
|
+
[ -Math.PI / 4, Vec2.of(123, 456) ],
|
163
|
+
[ 0.1, Vec2.of(1, 2) ],
|
164
|
+
];
|
165
|
+
|
166
|
+
const fuzz = 0.00001;
|
167
|
+
for (const [ angle, center ] of testCases) {
|
168
|
+
const mat = Mat33.zRotation(angle, center);
|
169
|
+
expect(mat.inverse().rightMul(mat)).objEq(Mat33.identity, fuzz);
|
170
|
+
expect(mat.rightMul(mat.inverse())).objEq(Mat33.identity, fuzz);
|
171
|
+
}
|
172
|
+
});
|
173
|
+
|
174
|
+
it('z-rotation should preserve given origin', () => {
|
175
|
+
const testCases: Array<[ number, Point2 ]> = [
|
176
|
+
[ 6.205048847547065, Vec2.of(75.16363373235318, 104.29870408043762) ],
|
177
|
+
[ 1.234, Vec2.of(-56, 789) ],
|
178
|
+
[ -Math.PI, Vec2.of(-56, 789) ],
|
179
|
+
[ -Math.PI / 2, Vec2.of(-0.001, 1.0002) ],
|
180
|
+
];
|
181
|
+
|
182
|
+
for (const [angle, rotationOrigin] of testCases) {
|
183
|
+
expect(Mat33.zRotation(angle, rotationOrigin).transformVec2(rotationOrigin)).objEq(rotationOrigin);
|
184
|
+
}
|
185
|
+
});
|
186
|
+
|
187
|
+
it('should correctly apply a mapping to all components', () => {
|
188
|
+
expect(
|
189
|
+
new Mat33(
|
190
|
+
1, 2, 3,
|
191
|
+
4, 5, 6,
|
192
|
+
7, 8, 9,
|
193
|
+
).mapEntries(component => component - 1)
|
194
|
+
).toMatchObject(new Mat33(
|
195
|
+
0, 1, 2,
|
196
|
+
3, 4, 5,
|
197
|
+
6, 7, 8,
|
198
|
+
));
|
199
|
+
});
|
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
|
+
));
|
243
|
+
});
|
244
|
+
});
|
package/src/Mat33.ts
ADDED
@@ -0,0 +1,450 @@
|
|
1
|
+
import { Point2, Vec2 } from './Vec2';
|
2
|
+
import Vec3 from './Vec3';
|
3
|
+
|
4
|
+
export type Mat33Array = [
|
5
|
+
number, number, number,
|
6
|
+
number, number, number,
|
7
|
+
number, number, number,
|
8
|
+
];
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Represents a three dimensional linear transformation or
|
12
|
+
* a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
|
13
|
+
* **and** translates while a linear transformation just scales/rotates/shears).
|
14
|
+
*/
|
15
|
+
export class Mat33 {
|
16
|
+
private readonly rows: Vec3[];
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Creates a matrix from inputs in the form,
|
20
|
+
* $$
|
21
|
+
* \begin{bmatrix}
|
22
|
+
* a1 & a2 & a3 \\
|
23
|
+
* b1 & b2 & b3 \\
|
24
|
+
* c1 & c2 & c3
|
25
|
+
* \end{bmatrix}
|
26
|
+
* $$
|
27
|
+
*/
|
28
|
+
public constructor(
|
29
|
+
public readonly a1: number,
|
30
|
+
public readonly a2: number,
|
31
|
+
public readonly a3: number,
|
32
|
+
|
33
|
+
public readonly b1: number,
|
34
|
+
public readonly b2: number,
|
35
|
+
public readonly b3: number,
|
36
|
+
|
37
|
+
public readonly c1: number,
|
38
|
+
public readonly c2: number,
|
39
|
+
public readonly c3: number
|
40
|
+
) {
|
41
|
+
this.rows = [
|
42
|
+
Vec3.of(a1, a2, a3),
|
43
|
+
Vec3.of(b1, b2, b3),
|
44
|
+
Vec3.of(c1, c2, c3),
|
45
|
+
];
|
46
|
+
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Creates a matrix from the given rows:
|
50
|
+
* $$
|
51
|
+
* \begin{bmatrix}
|
52
|
+
* \texttt{r1.x} & \texttt{r1.y} & \texttt{r1.z}\\
|
53
|
+
* \texttt{r2.x} & \texttt{r2.y} & \texttt{r2.z}\\
|
54
|
+
* \texttt{r3.x} & \texttt{r3.y} & \texttt{r3.z}\\
|
55
|
+
* \end{bmatrix}
|
56
|
+
* $$
|
57
|
+
*/
|
58
|
+
public static ofRows(r1: Vec3, r2: Vec3, r3: Vec3): Mat33 {
|
59
|
+
return new Mat33(
|
60
|
+
r1.x, r1.y, r1.z,
|
61
|
+
r2.x, r2.y, r2.z,
|
62
|
+
r3.x, r3.y, r3.z
|
63
|
+
);
|
64
|
+
}
|
65
|
+
|
66
|
+
public static identity = new Mat33(
|
67
|
+
1, 0, 0,
|
68
|
+
0, 1, 0,
|
69
|
+
0, 0, 1
|
70
|
+
);
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Either returns the inverse of this, or, if this matrix is singular/uninvertable,
|
74
|
+
* returns Mat33.identity.
|
75
|
+
*
|
76
|
+
* This may cache the computed inverse and return the cached version instead of recomputing
|
77
|
+
* it.
|
78
|
+
*/
|
79
|
+
public inverse(): Mat33 {
|
80
|
+
return this.computeInverse() ?? Mat33.identity;
|
81
|
+
}
|
82
|
+
|
83
|
+
public invertable(): boolean {
|
84
|
+
return this.computeInverse() !== null;
|
85
|
+
}
|
86
|
+
|
87
|
+
private cachedInverse: Mat33|undefined|null = undefined;
|
88
|
+
private computeInverse(): Mat33|null {
|
89
|
+
if (this.cachedInverse !== undefined) {
|
90
|
+
return this.cachedInverse;
|
91
|
+
}
|
92
|
+
|
93
|
+
const toIdentity = [
|
94
|
+
this.rows[0],
|
95
|
+
this.rows[1],
|
96
|
+
this.rows[2],
|
97
|
+
];
|
98
|
+
|
99
|
+
const toResult = [
|
100
|
+
Vec3.unitX,
|
101
|
+
Vec3.unitY,
|
102
|
+
Vec3.unitZ,
|
103
|
+
];
|
104
|
+
|
105
|
+
// Convert toIdentity to the identity matrix and
|
106
|
+
// toResult to the inverse through elementary row operations
|
107
|
+
for (let cursor = 0; cursor < 3; cursor++) {
|
108
|
+
// Select the [cursor]th diagonal entry
|
109
|
+
let pivot = toIdentity[cursor].at(cursor);
|
110
|
+
|
111
|
+
// Don't divide by zero (treat very small numbers as zero).
|
112
|
+
const minDivideBy = 1e-10;
|
113
|
+
if (Math.abs(pivot) < minDivideBy) {
|
114
|
+
let swapIndex = -1;
|
115
|
+
// For all other rows,
|
116
|
+
for (let i = 1; i <= 2; i++) {
|
117
|
+
const otherRowIdx = (cursor + i) % 3;
|
118
|
+
|
119
|
+
if (Math.abs(toIdentity[otherRowIdx].at(cursor)) >= minDivideBy) {
|
120
|
+
swapIndex = otherRowIdx;
|
121
|
+
break;
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
// Can't swap with another row?
|
126
|
+
if (swapIndex === -1) {
|
127
|
+
this.cachedInverse = null;
|
128
|
+
return null;
|
129
|
+
}
|
130
|
+
|
131
|
+
const tmpIdentityRow = toIdentity[cursor];
|
132
|
+
const tmpResultRow = toResult[cursor];
|
133
|
+
|
134
|
+
// Swap!
|
135
|
+
toIdentity[cursor] = toIdentity[swapIndex];
|
136
|
+
toResult[cursor] = toResult[swapIndex];
|
137
|
+
toIdentity[swapIndex] = tmpIdentityRow;
|
138
|
+
toResult[swapIndex] = tmpResultRow;
|
139
|
+
|
140
|
+
pivot = toIdentity[cursor].at(cursor);
|
141
|
+
}
|
142
|
+
|
143
|
+
// Make toIdentity[k = cursor] = 1
|
144
|
+
let scale = 1.0 / pivot;
|
145
|
+
toIdentity[cursor] = toIdentity[cursor].times(scale);
|
146
|
+
toResult[cursor] = toResult[cursor].times(scale);
|
147
|
+
|
148
|
+
const cursorToIdentityRow = toIdentity[cursor];
|
149
|
+
const cursorToResultRow = toResult[cursor];
|
150
|
+
|
151
|
+
// Make toIdentity[k ≠ cursor] = 0
|
152
|
+
for (let i = 1; i <= 2; i++) {
|
153
|
+
const otherRowIdx = (cursor + i) % 3;
|
154
|
+
scale = -toIdentity[otherRowIdx].at(cursor);
|
155
|
+
toIdentity[otherRowIdx] = toIdentity[otherRowIdx].plus(
|
156
|
+
cursorToIdentityRow.times(scale)
|
157
|
+
);
|
158
|
+
toResult[otherRowIdx] = toResult[otherRowIdx].plus(
|
159
|
+
cursorToResultRow.times(scale)
|
160
|
+
);
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
const inverse = Mat33.ofRows(
|
165
|
+
toResult[0],
|
166
|
+
toResult[1],
|
167
|
+
toResult[2]
|
168
|
+
);
|
169
|
+
this.cachedInverse = inverse;
|
170
|
+
return inverse;
|
171
|
+
}
|
172
|
+
|
173
|
+
public transposed(): Mat33 {
|
174
|
+
return new Mat33(
|
175
|
+
this.a1, this.b1, this.c1,
|
176
|
+
this.a2, this.b2, this.c2,
|
177
|
+
this.a3, this.b3, this.c3
|
178
|
+
);
|
179
|
+
}
|
180
|
+
|
181
|
+
public rightMul(other: Mat33): Mat33 {
|
182
|
+
other = other.transposed();
|
183
|
+
|
184
|
+
const at = (row: number, col: number): number => {
|
185
|
+
return this.rows[row].dot(other.rows[col]);
|
186
|
+
};
|
187
|
+
|
188
|
+
return new Mat33(
|
189
|
+
at(0, 0), at(0, 1), at(0, 2),
|
190
|
+
at(1, 0), at(1, 1), at(1, 2),
|
191
|
+
at(2, 0), at(2, 1), at(2, 2)
|
192
|
+
);
|
193
|
+
}
|
194
|
+
|
195
|
+
/**
|
196
|
+
* Applies this as an **affine** transformation to the given vector.
|
197
|
+
* Returns a transformed version of `other`.
|
198
|
+
*
|
199
|
+
* Unlike {@link transformVec3}, this **does** translate the given vector.
|
200
|
+
*/
|
201
|
+
public transformVec2(other: Vec2): Vec2 {
|
202
|
+
// When transforming a Vec2, we want to use the z transformation
|
203
|
+
// components of this for translation:
|
204
|
+
// ⎡ . . tX ⎤
|
205
|
+
// ⎢ . . tY ⎥
|
206
|
+
// ⎣ 0 0 1 ⎦
|
207
|
+
// For this, we need other's z component to be 1 (so that tX and tY
|
208
|
+
// are scaled by 1):
|
209
|
+
let intermediate = Vec3.of(other.x, other.y, 1);
|
210
|
+
intermediate = this.transformVec3(intermediate);
|
211
|
+
|
212
|
+
// Drop the z=1 to allow magnitude to work as expected
|
213
|
+
return Vec2.of(intermediate.x, intermediate.y);
|
214
|
+
}
|
215
|
+
|
216
|
+
/**
|
217
|
+
* Applies this as a linear transformation to the given vector (doesn't translate).
|
218
|
+
* This is the standard way of transforming vectors in ℝ³.
|
219
|
+
*/
|
220
|
+
public transformVec3(other: Vec3): Vec3 {
|
221
|
+
return Vec3.of(
|
222
|
+
this.rows[0].dot(other),
|
223
|
+
this.rows[1].dot(other),
|
224
|
+
this.rows[2].dot(other)
|
225
|
+
);
|
226
|
+
}
|
227
|
+
|
228
|
+
/** @returns true iff this is the identity matrix. */
|
229
|
+
public isIdentity(): boolean {
|
230
|
+
if (this === Mat33.identity) {
|
231
|
+
return true;
|
232
|
+
}
|
233
|
+
|
234
|
+
return this.eq(Mat33.identity);
|
235
|
+
}
|
236
|
+
|
237
|
+
/** Returns true iff this = other ± fuzz */
|
238
|
+
public eq(other: Mat33, fuzz: number = 0): boolean {
|
239
|
+
for (let i = 0; i < 3; i++) {
|
240
|
+
if (!this.rows[i].eq(other.rows[i], fuzz)) {
|
241
|
+
return false;
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
return true;
|
246
|
+
}
|
247
|
+
|
248
|
+
public toString(): string {
|
249
|
+
let result = '';
|
250
|
+
const maxColumnLens = [ 0, 0, 0 ];
|
251
|
+
|
252
|
+
// Determine the longest item in each column so we can pad the others to that
|
253
|
+
// length.
|
254
|
+
for (const row of this.rows) {
|
255
|
+
for (let i = 0; i < 3; i++) {
|
256
|
+
maxColumnLens[i] = Math.max(maxColumnLens[0], `${row.at(i)}`.length);
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
for (let i = 0; i < 3; i++) {
|
261
|
+
if (i === 0) {
|
262
|
+
result += '⎡ ';
|
263
|
+
} else if (i === 1) {
|
264
|
+
result += '⎢ ';
|
265
|
+
} else {
|
266
|
+
result += '⎣ ';
|
267
|
+
}
|
268
|
+
|
269
|
+
// Add each component of the ith row (after padding it)
|
270
|
+
for (let j = 0; j < 3; j++) {
|
271
|
+
const val = this.rows[i].at(j).toString();
|
272
|
+
|
273
|
+
let padding = '';
|
274
|
+
for (let i = val.length; i < maxColumnLens[j]; i++) {
|
275
|
+
padding += ' ';
|
276
|
+
}
|
277
|
+
|
278
|
+
result += val + ', ' + padding;
|
279
|
+
}
|
280
|
+
|
281
|
+
if (i === 0) {
|
282
|
+
result += ' ⎤';
|
283
|
+
} else if (i === 1) {
|
284
|
+
result += ' ⎥';
|
285
|
+
} else {
|
286
|
+
result += ' ⎦';
|
287
|
+
}
|
288
|
+
result += '\n';
|
289
|
+
}
|
290
|
+
|
291
|
+
return result.trimEnd();
|
292
|
+
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* ```
|
296
|
+
* result[0] = top left element
|
297
|
+
* result[1] = element at row zero, column 1
|
298
|
+
* ...
|
299
|
+
* ```
|
300
|
+
*/
|
301
|
+
public toArray(): Mat33Array {
|
302
|
+
return [
|
303
|
+
this.a1, this.a2, this.a3,
|
304
|
+
this.b1, this.b2, this.b3,
|
305
|
+
this.c1, this.c2, this.c3,
|
306
|
+
];
|
307
|
+
}
|
308
|
+
|
309
|
+
/**
|
310
|
+
* Returns a new `Mat33` where each entry is the output of the function
|
311
|
+
* `mapping`.
|
312
|
+
*
|
313
|
+
* @example
|
314
|
+
* ```
|
315
|
+
* new Mat33(
|
316
|
+
* 1, 2, 3,
|
317
|
+
* 4, 5, 6,
|
318
|
+
* 7, 8, 9,
|
319
|
+
* ).mapEntries(component => component - 1);
|
320
|
+
* // → ⎡ 0, 1, 2 ⎤
|
321
|
+
* // ⎢ 3, 4, 5 ⎥
|
322
|
+
* // ⎣ 6, 7, 8 ⎦
|
323
|
+
* ```
|
324
|
+
*/
|
325
|
+
public mapEntries(mapping: (component: number, rowcol: [number, number])=>number): Mat33 {
|
326
|
+
return new Mat33(
|
327
|
+
mapping(this.a1, [0, 0]), mapping(this.a2, [0, 1]), mapping(this.a3, [0, 2]),
|
328
|
+
mapping(this.b1, [1, 0]), mapping(this.b2, [1, 1]), mapping(this.b3, [1, 2]),
|
329
|
+
mapping(this.c1, [2, 0]), mapping(this.c2, [2, 1]), mapping(this.c3, [2, 2]),
|
330
|
+
);
|
331
|
+
}
|
332
|
+
|
333
|
+
/** Estimate the scale factor of this matrix (based on the first row). */
|
334
|
+
public getScaleFactor() {
|
335
|
+
return Math.hypot(this.a1, this.a2);
|
336
|
+
}
|
337
|
+
|
338
|
+
/**
|
339
|
+
* Constructs a 3x3 translation matrix (for translating `Vec2`s) using
|
340
|
+
* **transformVec2**.
|
341
|
+
*/
|
342
|
+
public static translation(amount: Vec2): Mat33 {
|
343
|
+
// When transforming Vec2s by a 3x3 matrix, we give the input
|
344
|
+
// Vec2s z = 1. As such,
|
345
|
+
// outVec2.x = inVec2.x * 1 + inVec2.y * 0 + 1 * amount.x
|
346
|
+
// ...
|
347
|
+
return new Mat33(
|
348
|
+
1, 0, amount.x,
|
349
|
+
0, 1, amount.y,
|
350
|
+
0, 0, 1
|
351
|
+
);
|
352
|
+
}
|
353
|
+
|
354
|
+
public static zRotation(radians: number, center: Point2 = Vec2.zero): Mat33 {
|
355
|
+
if (radians === 0) {
|
356
|
+
return Mat33.identity;
|
357
|
+
}
|
358
|
+
|
359
|
+
const cos = Math.cos(radians);
|
360
|
+
const sin = Math.sin(radians);
|
361
|
+
|
362
|
+
// Translate everything so that rotation is about the origin
|
363
|
+
let result = Mat33.translation(center);
|
364
|
+
|
365
|
+
result = result.rightMul(new Mat33(
|
366
|
+
cos, -sin, 0,
|
367
|
+
sin, cos, 0,
|
368
|
+
0, 0, 1
|
369
|
+
));
|
370
|
+
return result.rightMul(Mat33.translation(center.times(-1)));
|
371
|
+
}
|
372
|
+
|
373
|
+
public static scaling2D(amount: number|Vec2, center: Point2 = Vec2.zero): Mat33 {
|
374
|
+
let result = Mat33.translation(center);
|
375
|
+
let xAmount, yAmount;
|
376
|
+
|
377
|
+
if (typeof amount === 'number') {
|
378
|
+
xAmount = amount;
|
379
|
+
yAmount = amount;
|
380
|
+
} else {
|
381
|
+
xAmount = amount.x;
|
382
|
+
yAmount = amount.y;
|
383
|
+
}
|
384
|
+
|
385
|
+
result = result.rightMul(new Mat33(
|
386
|
+
xAmount, 0, 0,
|
387
|
+
0, yAmount, 0,
|
388
|
+
0, 0, 1
|
389
|
+
));
|
390
|
+
|
391
|
+
// Translate such that [center] goes to (0, 0)
|
392
|
+
return result.rightMul(Mat33.translation(center.times(-1)));
|
393
|
+
}
|
394
|
+
|
395
|
+
/** @see {@link fromCSSMatrix} */
|
396
|
+
public toCSSMatrix(): string {
|
397
|
+
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
|
398
|
+
}
|
399
|
+
|
400
|
+
/**
|
401
|
+
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
|
402
|
+
*
|
403
|
+
* Note that such a matrix has the form,
|
404
|
+
* ```
|
405
|
+
* ⎡ a c e ⎤
|
406
|
+
* ⎢ b d f ⎥
|
407
|
+
* ⎣ 0 0 1 ⎦
|
408
|
+
* ```
|
409
|
+
*/
|
410
|
+
public static fromCSSMatrix(cssString: string): Mat33 {
|
411
|
+
if (cssString === '' || cssString === 'none') {
|
412
|
+
return Mat33.identity;
|
413
|
+
}
|
414
|
+
|
415
|
+
const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
|
416
|
+
const numberSepExp = '[, \\t\\n]';
|
417
|
+
const regExpSource = `^\\s*matrix\\s*\\(${
|
418
|
+
[
|
419
|
+
// According to MDN, matrix(a,b,c,d,e,f) has form:
|
420
|
+
// ⎡ a c e ⎤
|
421
|
+
// ⎢ b d f ⎥
|
422
|
+
// ⎣ 0 0 1 ⎦
|
423
|
+
numberExp, numberExp, numberExp, // a, c, e
|
424
|
+
numberExp, numberExp, numberExp, // b, d, f
|
425
|
+
].join(`${numberSepExp}+`)
|
426
|
+
}${numberSepExp}*\\)\\s*$`;
|
427
|
+
const matrixExp = new RegExp(regExpSource, 'i');
|
428
|
+
const match = matrixExp.exec(cssString);
|
429
|
+
|
430
|
+
if (!match) {
|
431
|
+
throw new Error(`Unsupported transformation: ${cssString}`);
|
432
|
+
}
|
433
|
+
|
434
|
+
const matrixData = match.slice(1).map(entry => parseFloat(entry));
|
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;
|
448
|
+
}
|
449
|
+
}
|
450
|
+
export default Mat33;
|
package/src/Vec2.test.ts
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
import { Vec2 } from './Vec2';
|
2
|
+
import Vec3 from './Vec3';
|
3
|
+
|
4
|
+
describe('Vec2', () => {
|
5
|
+
it('Magnitude', () => {
|
6
|
+
expect(Vec2.of(3, 4).magnitude()).toBe(5);
|
7
|
+
});
|
8
|
+
|
9
|
+
it('Addition', () => {
|
10
|
+
expect(Vec2.of(1, 2).plus(Vec2.of(3, 4))).objEq(Vec2.of(4, 6));
|
11
|
+
});
|
12
|
+
|
13
|
+
it('Multiplication', () => {
|
14
|
+
expect(Vec2.of(1, -1).times(22)).objEq(Vec2.of(22, -22));
|
15
|
+
});
|
16
|
+
|
17
|
+
it('More complicated expressions', () => {
|
18
|
+
expect((Vec2.of(1, 2).plus(Vec2.of(3, 4))).times(2)).objEq(Vec2.of(8, 12));
|
19
|
+
});
|
20
|
+
|
21
|
+
it('Angle', () => {
|
22
|
+
expect(Vec2.of(-1, 1).angle()).toBeCloseTo(3 * Math.PI / 4);
|
23
|
+
});
|
24
|
+
|
25
|
+
it('Perpindicular', () => {
|
26
|
+
const fuzz = 0.001;
|
27
|
+
expect(Vec2.unitX.cross(Vec3.unitZ)).objEq(Vec2.unitY.times(-1), fuzz);
|
28
|
+
expect(Vec2.unitX.orthog()).objEq(Vec2.unitY, fuzz);
|
29
|
+
});
|
30
|
+
});
|