@emasoft/svg-matrix 1.0.4 → 1.0.6
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/README.md +341 -304
- package/package.json +19 -1
- package/src/browser-verify.js +463 -0
- package/src/clip-path-resolver.js +759 -0
- package/src/geometry-to-path.js +348 -0
- package/src/index.js +413 -6
- package/src/marker-resolver.js +1006 -0
- package/src/mask-resolver.js +1407 -0
- package/src/mesh-gradient.js +1215 -0
- package/src/pattern-resolver.js +844 -0
- package/src/polygon-clip.js +1491 -0
- package/src/svg-flatten.js +1615 -76
- package/src/text-to-path.js +820 -0
- package/src/transforms2d.js +493 -37
- package/src/transforms3d.js +418 -47
- package/src/use-symbol-resolver.js +1126 -0
- package/samples/test.svg +0 -39
package/src/transforms2d.js
CHANGED
|
@@ -11,17 +11,91 @@ const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
|
11
11
|
/**
|
|
12
12
|
* 2D Affine Transforms using 3x3 homogeneous matrices.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* ## Mathematical Basis
|
|
15
|
+
*
|
|
16
|
+
* 2D affine transformations are represented as 3x3 homogeneous matrices that operate on
|
|
17
|
+
* homogeneous coordinates [x, y, 1]. This allows linear transformations (rotation, scaling,
|
|
18
|
+
* shearing) and translation to be combined in a single matrix multiplication.
|
|
19
|
+
*
|
|
20
|
+
* A point (x, y) is represented in homogeneous coordinates as:
|
|
21
|
+
* ```
|
|
22
|
+
* [x]
|
|
23
|
+
* [y]
|
|
24
|
+
* [1]
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* A general 2D affine transformation matrix has the form:
|
|
28
|
+
* ```
|
|
29
|
+
* [a b tx] [linear transformation | translation]
|
|
30
|
+
* [c d ty] = [---------------------+------------]
|
|
31
|
+
* [0 0 1] [ perspective | scaling ]
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Where:
|
|
35
|
+
* - [a b; c d] is the 2x2 linear transformation matrix (rotation, scale, shear)
|
|
36
|
+
* - [tx, ty] is the translation vector
|
|
37
|
+
* - The bottom row [0 0 1] ensures the transformation is affine (preserves parallel lines)
|
|
38
|
+
*
|
|
39
|
+
* ## Transform Composition
|
|
40
|
+
*
|
|
41
|
+
* Transforms are composed using matrix multiplication. The order matters!
|
|
42
|
+
* Matrix multiplication is applied RIGHT-TO-LEFT:
|
|
43
|
+
*
|
|
44
|
+
* ```javascript
|
|
45
|
+
* // To apply: first scale, then rotate, then translate
|
|
46
|
+
* const composed = T.mul(R).mul(S);
|
|
47
|
+
* // This reads right-to-left: S first, then R, then T
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* When applying to a point:
|
|
51
|
+
* ```
|
|
52
|
+
* point' = T × R × S × point
|
|
53
|
+
* = (T × R × S) × point
|
|
54
|
+
* = composed × point
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* ## Homogeneous Coordinates
|
|
58
|
+
*
|
|
59
|
+
* After transformation, we get homogeneous coordinates [x', y', w']. To convert back to
|
|
60
|
+
* Cartesian coordinates, we perform perspective division: (x'/w', y'/w'). For affine
|
|
61
|
+
* transforms, w' is always 1, but the division is performed for generality.
|
|
16
62
|
*
|
|
17
63
|
* @module Transforms2D
|
|
18
64
|
*/
|
|
19
65
|
|
|
20
66
|
/**
|
|
21
67
|
* Create a 2D translation matrix.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
68
|
+
*
|
|
69
|
+
* Translates points by adding (tx, ty) to their coordinates. Translation is the only
|
|
70
|
+
* affine transformation that affects the position without changing orientation or scale.
|
|
71
|
+
*
|
|
72
|
+
* Matrix form:
|
|
73
|
+
* ```
|
|
74
|
+
* [1 0 tx]
|
|
75
|
+
* [0 1 ty]
|
|
76
|
+
* [0 0 1]
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* Effect on point (x, y):
|
|
80
|
+
* ```
|
|
81
|
+
* x' = x + tx
|
|
82
|
+
* y' = y + ty
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @param {number|string|Decimal} tx - Translation distance in X direction (positive = right)
|
|
86
|
+
* @param {number|string|Decimal} ty - Translation distance in Y direction (positive = down in screen coords, up in math coords)
|
|
24
87
|
* @returns {Matrix} 3x3 translation matrix
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // Move a point 5 units right and 3 units down
|
|
91
|
+
* const T = translation(5, 3);
|
|
92
|
+
* const [x, y] = applyTransform(T, 10, 20);
|
|
93
|
+
* // Result: x = 15, y = 23
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // Combine with rotation: translate then rotate around new position
|
|
97
|
+
* const transform = rotation(Math.PI / 4).mul(translation(10, 0));
|
|
98
|
+
* // This rotates 45° AFTER translating 10 units right
|
|
25
99
|
*/
|
|
26
100
|
export function translation(tx, ty) {
|
|
27
101
|
return Matrix.from([
|
|
@@ -33,9 +107,50 @@ export function translation(tx, ty) {
|
|
|
33
107
|
|
|
34
108
|
/**
|
|
35
109
|
* Create a 2D scaling matrix.
|
|
36
|
-
*
|
|
110
|
+
*
|
|
111
|
+
* Scales points by multiplying their coordinates by scale factors. The scaling is performed
|
|
112
|
+
* relative to the origin (0, 0). To scale around a different point, combine with translation.
|
|
113
|
+
*
|
|
114
|
+
* Matrix form:
|
|
115
|
+
* ```
|
|
116
|
+
* [sx 0 0]
|
|
117
|
+
* [0 sy 0]
|
|
118
|
+
* [0 0 1]
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* Effect on point (x, y):
|
|
122
|
+
* ```
|
|
123
|
+
* x' = x × sx
|
|
124
|
+
* y' = y × sy
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* Special cases:
|
|
128
|
+
* - sx = sy = 1: Identity (no change)
|
|
129
|
+
* - sx = sy: Uniform scaling (preserves shape)
|
|
130
|
+
* - sx ≠ sy: Non-uniform scaling (stretches or compresses)
|
|
131
|
+
* - sx < 0 or sy < 0: Reflection combined with scaling
|
|
132
|
+
*
|
|
133
|
+
* @param {number|string|Decimal} sx - Scale factor in X direction (2.0 = double width, 0.5 = half width)
|
|
37
134
|
* @param {number|string|Decimal} [sy=sx] - Scale factor in Y direction (defaults to sx for uniform scaling)
|
|
38
135
|
* @returns {Matrix} 3x3 scaling matrix
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* // Uniform scaling: double the size
|
|
139
|
+
* const S = scale(2);
|
|
140
|
+
* const [x, y] = applyTransform(S, 10, 5);
|
|
141
|
+
* // Result: x = 20, y = 10
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* // Non-uniform scaling: stretch horizontally, compress vertically
|
|
145
|
+
* const S = scale(2, 0.5);
|
|
146
|
+
* const [x, y] = applyTransform(S, 10, 20);
|
|
147
|
+
* // Result: x = 20, y = 10
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* // Scale around a specific point (px, py)
|
|
151
|
+
* const px = 100, py = 50;
|
|
152
|
+
* const S = translation(px, py).mul(scale(2)).mul(translation(-px, -py));
|
|
153
|
+
* // This scales by 2× around point (100, 50) instead of origin
|
|
39
154
|
*/
|
|
40
155
|
export function scale(sx, sy = null) {
|
|
41
156
|
if (sy === null) sy = sx;
|
|
@@ -49,13 +164,54 @@ export function scale(sx, sy = null) {
|
|
|
49
164
|
/**
|
|
50
165
|
* Create a 2D rotation matrix (counterclockwise around origin).
|
|
51
166
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
167
|
+
* Rotates points counterclockwise (in standard mathematical orientation) by angle θ
|
|
168
|
+
* around the origin (0, 0). In screen coordinates (where Y increases downward),
|
|
169
|
+
* this produces clockwise rotation.
|
|
170
|
+
*
|
|
171
|
+
* Matrix form:
|
|
172
|
+
* ```
|
|
173
|
+
* [cos(θ) -sin(θ) 0]
|
|
174
|
+
* [sin(θ) cos(θ) 0]
|
|
175
|
+
* [ 0 0 1]
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* Effect on point (x, y):
|
|
179
|
+
* ```
|
|
180
|
+
* x' = x⋅cos(θ) - y⋅sin(θ)
|
|
181
|
+
* y' = x⋅sin(θ) + y⋅cos(θ)
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* The rotation preserves:
|
|
185
|
+
* - Distance from origin: ||p'|| = ||p||
|
|
186
|
+
* - Angles between vectors
|
|
187
|
+
* - Areas and shapes (it's an isometry)
|
|
188
|
+
*
|
|
189
|
+
* Common angles:
|
|
190
|
+
* - 0: No rotation
|
|
191
|
+
* - π/4 (45°): Diagonal rotation
|
|
192
|
+
* - π/2 (90°): Quarter turn
|
|
193
|
+
* - π (180°): Half turn (point reflection)
|
|
194
|
+
* - 2π (360°): Full rotation (identity)
|
|
56
195
|
*
|
|
57
|
-
* @param {number|string|Decimal} theta - Rotation angle in radians
|
|
196
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians (positive = counterclockwise in math coords)
|
|
58
197
|
* @returns {Matrix} 3x3 rotation matrix
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* // Rotate point 90° counterclockwise (π/2 radians)
|
|
201
|
+
* const R = rotate(Math.PI / 2);
|
|
202
|
+
* const [x, y] = applyTransform(R, 10, 0);
|
|
203
|
+
* // Result: x ≈ 0, y ≈ 10
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* // Rotate 45° counterclockwise
|
|
207
|
+
* const R = rotate(Math.PI / 4);
|
|
208
|
+
* const [x, y] = applyTransform(R, 1, 0);
|
|
209
|
+
* // Result: x ≈ 0.707, y ≈ 0.707
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* // Rotate around a different point using rotateAroundPoint
|
|
213
|
+
* const R = rotateAroundPoint(Math.PI / 2, 100, 100);
|
|
214
|
+
* // Rotates 90° around point (100, 100)
|
|
59
215
|
*/
|
|
60
216
|
export function rotate(theta) {
|
|
61
217
|
const t = D(theta);
|
|
@@ -70,12 +226,48 @@ export function rotate(theta) {
|
|
|
70
226
|
|
|
71
227
|
/**
|
|
72
228
|
* Create a 2D rotation matrix around a specific point.
|
|
73
|
-
* Equivalent to: translate(px, py) × rotate(theta) × translate(-px, -py)
|
|
74
229
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
230
|
+
* Rotates points counterclockwise by angle θ around an arbitrary center point (px, py)
|
|
231
|
+
* instead of the origin. This is accomplished by:
|
|
232
|
+
* 1. Translating the center point to the origin
|
|
233
|
+
* 2. Performing the rotation
|
|
234
|
+
* 3. Translating back to the original center
|
|
235
|
+
*
|
|
236
|
+
* Matrix composition:
|
|
237
|
+
* ```
|
|
238
|
+
* R_point = T(px, py) × R(θ) × T(-px, -py)
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* This is equivalent to:
|
|
242
|
+
* ```
|
|
243
|
+
* [cos(θ) -sin(θ) px - px⋅cos(θ) + py⋅sin(θ)]
|
|
244
|
+
* [sin(θ) cos(θ) py - px⋅sin(θ) - py⋅cos(θ)]
|
|
245
|
+
* [ 0 0 1 ]
|
|
246
|
+
* ```
|
|
247
|
+
*
|
|
248
|
+
* Use cases:
|
|
249
|
+
* - Rotating UI elements around their centers
|
|
250
|
+
* - Rotating objects around pivot points
|
|
251
|
+
* - Orbital motion around a fixed point
|
|
252
|
+
*
|
|
253
|
+
* @param {number|string|Decimal} theta - Rotation angle in radians (positive = counterclockwise)
|
|
254
|
+
* @param {number|string|Decimal} px - X coordinate of rotation center (pivot point)
|
|
255
|
+
* @param {number|string|Decimal} py - Y coordinate of rotation center (pivot point)
|
|
78
256
|
* @returns {Matrix} 3x3 rotation matrix around point (px, py)
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* // Rotate a square around its center (50, 50) by 45°
|
|
260
|
+
* const R = rotateAroundPoint(Math.PI / 4, 50, 50);
|
|
261
|
+
* // The center point (50, 50) remains fixed after transformation
|
|
262
|
+
* const [cx, cy] = applyTransform(R, 50, 50);
|
|
263
|
+
* // Result: cx = 50, cy = 50 (center doesn't move)
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* // Rotate a point on a circle around center
|
|
267
|
+
* const centerX = 100, centerY = 100;
|
|
268
|
+
* const R = rotateAroundPoint(Math.PI / 6, centerX, centerY); // 30° rotation
|
|
269
|
+
* const [x, y] = applyTransform(R, 110, 100); // Point on circle, 10 units right of center
|
|
270
|
+
* // Result: point moves 30° counterclockwise around the center
|
|
79
271
|
*/
|
|
80
272
|
export function rotateAroundPoint(theta, px, py) {
|
|
81
273
|
const pxD = D(px);
|
|
@@ -86,13 +278,56 @@ export function rotateAroundPoint(theta, px, py) {
|
|
|
86
278
|
/**
|
|
87
279
|
* Create a 2D skew (shear) matrix.
|
|
88
280
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
281
|
+
* Skewing (or shearing) transforms parallel lines into parallel lines but changes angles
|
|
282
|
+
* between lines. It "slants" the coordinate system. Unlike rotation, skew doesn't preserve
|
|
283
|
+
* angles or distances, only parallelism and ratios of distances along parallel lines.
|
|
284
|
+
*
|
|
285
|
+
* Matrix form:
|
|
286
|
+
* ```
|
|
287
|
+
* [1 ax 0]
|
|
288
|
+
* [ay 1 0]
|
|
289
|
+
* [0 0 1]
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* Effect on point (x, y):
|
|
293
|
+
* ```
|
|
294
|
+
* x' = x + ax⋅y (X shifts based on Y coordinate)
|
|
295
|
+
* y' = ay⋅x + y (Y shifts based on X coordinate)
|
|
296
|
+
* ```
|
|
92
297
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
298
|
+
* Visual effects:
|
|
299
|
+
* - ax > 0: Shear right for positive Y (top of shape leans right in screen coords)
|
|
300
|
+
* - ax < 0: Shear left for positive Y
|
|
301
|
+
* - ay > 0: Shear up for positive X (right side of shape leans up in screen coords)
|
|
302
|
+
* - ay < 0: Shear down for positive X
|
|
303
|
+
*
|
|
304
|
+
* Common uses:
|
|
305
|
+
* - Creating italic/slanted text effects
|
|
306
|
+
* - Simulating perspective distortion
|
|
307
|
+
* - Parallelogram transformations
|
|
308
|
+
*
|
|
309
|
+
* Note: Skewing changes area by factor |1 - ax⋅ay| (determinant of linear part)
|
|
310
|
+
*
|
|
311
|
+
* @param {number|string|Decimal} ax - Horizontal skew factor (affects X based on Y). tan(angle) for X-axis skew.
|
|
312
|
+
* @param {number|string|Decimal} ay - Vertical skew factor (affects Y based on X). tan(angle) for Y-axis skew.
|
|
95
313
|
* @returns {Matrix} 3x3 skew matrix
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* // Skew horizontally (like italic text)
|
|
317
|
+
* const S = skew(0.3, 0);
|
|
318
|
+
* const [x, y] = applyTransform(S, 10, 20);
|
|
319
|
+
* // Result: x = 10 + 0.3×20 = 16, y = 20
|
|
320
|
+
* // Point at height 20 shifts 6 units to the right
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* // Skew at 30° angle from vertical
|
|
324
|
+
* const S = skew(Math.tan(30 * Math.PI / 180), 0);
|
|
325
|
+
* // Creates a ~30° slant (like italic text at 30° from vertical)
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* // Bi-directional skew (parallelogram transformation)
|
|
329
|
+
* const S = skew(0.5, 0.3);
|
|
330
|
+
* // Both coordinates affect each other, creating complex shearing
|
|
96
331
|
*/
|
|
97
332
|
export function skew(ax, ay) {
|
|
98
333
|
return Matrix.from([
|
|
@@ -104,14 +339,62 @@ export function skew(ax, ay) {
|
|
|
104
339
|
|
|
105
340
|
/**
|
|
106
341
|
* Create a stretch matrix along a specified axis direction.
|
|
107
|
-
* Stretches by factor k along the unit vector (ux, uy).
|
|
108
342
|
*
|
|
109
|
-
*
|
|
343
|
+
* Performs directional scaling: stretches (or compresses) space along an arbitrary
|
|
344
|
+
* axis defined by the unit vector (ux, uy), while leaving the perpendicular direction
|
|
345
|
+
* unchanged. This is more general than axis-aligned scaling.
|
|
346
|
+
*
|
|
347
|
+
* Mathematical formula:
|
|
348
|
+
* ```
|
|
349
|
+
* M = I + (k - 1)⋅u⋅u^T
|
|
350
|
+
* where u = [ux, uy] is the unit direction vector
|
|
351
|
+
* ```
|
|
352
|
+
*
|
|
353
|
+
* Matrix form:
|
|
354
|
+
* ```
|
|
355
|
+
* [1 + (k-1)⋅ux² (k-1)⋅ux⋅uy 0]
|
|
356
|
+
* [(k-1)⋅ux⋅uy 1 + (k-1)⋅uy² 0]
|
|
357
|
+
* [ 0 0 1]
|
|
358
|
+
* ```
|
|
359
|
+
*
|
|
360
|
+
* The stretch is applied only in the direction of (ux, uy). Perpendicular
|
|
361
|
+
* directions remain unchanged. This is a directional scale transformation.
|
|
362
|
+
*
|
|
363
|
+
* Special cases:
|
|
364
|
+
* - k = 1: Identity (no change)
|
|
365
|
+
* - k = 2: Double length along axis
|
|
366
|
+
* - k = 0.5: Half length along axis
|
|
367
|
+
* - k = 0: Collapse onto perpendicular axis (singular matrix)
|
|
368
|
+
* - (ux, uy) = (1, 0): Horizontal stretch (same as scale(k, 1))
|
|
369
|
+
* - (ux, uy) = (0, 1): Vertical stretch (same as scale(1, k))
|
|
370
|
+
*
|
|
371
|
+
* Note: The axis vector should be normalized (ux² + uy² = 1) for correct behavior,
|
|
372
|
+
* though this is not enforced. Non-unit vectors will produce scaled results.
|
|
110
373
|
*
|
|
111
|
-
* @param {number|string|Decimal} ux - X component of axis direction (unit vector)
|
|
112
|
-
* @param {number|string|Decimal} uy - Y component of axis direction (unit vector)
|
|
113
|
-
* @param {number|string|Decimal} k - Stretch factor along the axis
|
|
114
|
-
* @returns {Matrix} 3x3 stretch matrix
|
|
374
|
+
* @param {number|string|Decimal} ux - X component of stretch axis direction (should be unit vector)
|
|
375
|
+
* @param {number|string|Decimal} uy - Y component of stretch axis direction (should be unit vector)
|
|
376
|
+
* @param {number|string|Decimal} k - Stretch factor along the axis (1 = no change, >1 = stretch, <1 = compress)
|
|
377
|
+
* @returns {Matrix} 3x3 stretch matrix along the specified axis
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* // Stretch along 45° diagonal (axis at π/4)
|
|
381
|
+
* const angle = Math.PI / 4;
|
|
382
|
+
* const ux = Math.cos(angle); // ≈ 0.707
|
|
383
|
+
* const uy = Math.sin(angle); // ≈ 0.707
|
|
384
|
+
* const S = stretchAlongAxis(ux, uy, 2);
|
|
385
|
+
* // Doubles distances along the diagonal, perpendicular direction unchanged
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* // Horizontal stretch (equivalent to scale(2, 1))
|
|
389
|
+
* const S = stretchAlongAxis(1, 0, 2);
|
|
390
|
+
* const [x, y] = applyTransform(S, 10, 5);
|
|
391
|
+
* // Result: x = 20, y = 5
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* // Compress along vertical axis by 50%
|
|
395
|
+
* const S = stretchAlongAxis(0, 1, 0.5);
|
|
396
|
+
* const [x, y] = applyTransform(S, 10, 20);
|
|
397
|
+
* // Result: x = 10, y = 10 (Y compressed to half)
|
|
115
398
|
*/
|
|
116
399
|
export function stretchAlongAxis(ux, uy, k) {
|
|
117
400
|
const uxD = D(ux), uyD = D(uy), kD = D(k);
|
|
@@ -130,12 +413,60 @@ export function stretchAlongAxis(ux, uy, k) {
|
|
|
130
413
|
|
|
131
414
|
/**
|
|
132
415
|
* Apply a 2D transform matrix to a point.
|
|
133
|
-
* Uses homogeneous coordinates with perspective division.
|
|
134
416
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
417
|
+
* Transforms a 2D point (x, y) using a 3x3 transformation matrix by converting the point
|
|
418
|
+
* to homogeneous coordinates [x, y, 1], multiplying by the matrix, and converting back
|
|
419
|
+
* to Cartesian coordinates via perspective division.
|
|
420
|
+
*
|
|
421
|
+
* Mathematical process:
|
|
422
|
+
* ```
|
|
423
|
+
* 1. Convert to homogeneous: P = [x, y, 1]^T
|
|
424
|
+
* 2. Apply transform: P' = M × P = [x', y', w']^T
|
|
425
|
+
* 3. Perspective division: (x'/w', y'/w')
|
|
426
|
+
* ```
|
|
427
|
+
*
|
|
428
|
+
* For affine transformations (bottom row is [0, 0, 1]), w' is always 1, so the
|
|
429
|
+
* division has no effect. However, it's performed for generality to support
|
|
430
|
+
* projective transformations.
|
|
431
|
+
*
|
|
432
|
+
* The transformation is:
|
|
433
|
+
* ```
|
|
434
|
+
* [x'] [m00 m01 m02] [x] [m00⋅x + m01⋅y + m02]
|
|
435
|
+
* [y'] = [m10 m11 m12] × [y] = [m10⋅x + m11⋅y + m12]
|
|
436
|
+
* [w'] [m20 m21 m22] [1] [m20⋅x + m21⋅y + m22]
|
|
437
|
+
*
|
|
438
|
+
* Result: (x'/w', y'/w')
|
|
439
|
+
* ```
|
|
440
|
+
*
|
|
441
|
+
* @param {Matrix} M - 3x3 transformation matrix to apply
|
|
442
|
+
* @param {number|string|Decimal} x - X coordinate of input point
|
|
443
|
+
* @param {number|string|Decimal} y - Y coordinate of input point
|
|
444
|
+
* @returns {Decimal[]} Transformed point as [x', y'] array of Decimal values
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* // Apply a translation
|
|
448
|
+
* const T = translation(5, 10);
|
|
449
|
+
* const [x, y] = applyTransform(T, 3, 4);
|
|
450
|
+
* // Result: x = 8, y = 14
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* // Apply a rotation by 90°
|
|
454
|
+
* const R = rotate(Math.PI / 2);
|
|
455
|
+
* const [x, y] = applyTransform(R, 1, 0);
|
|
456
|
+
* // Result: x ≈ 0, y ≈ 1
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* // Apply composed transformation: scale 2×, then rotate 45°, then translate
|
|
460
|
+
* const composed = translation(10, 20).mul(rotate(Math.PI / 4)).mul(scale(2));
|
|
461
|
+
* const [x, y] = applyTransform(composed, 1, 0);
|
|
462
|
+
* // First scales to (2, 0), then rotates to (√2, √2), then translates
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* // Transform multiple points with same matrix
|
|
466
|
+
* const T = rotate(Math.PI / 6);
|
|
467
|
+
* const points = [[0, 1], [1, 0], [1, 1]];
|
|
468
|
+
* const transformed = points.map(([x, y]) => applyTransform(T, x, y));
|
|
469
|
+
* // Efficiently reuses the same transformation matrix
|
|
139
470
|
*/
|
|
140
471
|
export function applyTransform(M, x, y) {
|
|
141
472
|
const P = Matrix.from([[D(x)], [D(y)], [new Decimal(1)]]);
|
|
@@ -146,8 +477,46 @@ export function applyTransform(M, x, y) {
|
|
|
146
477
|
}
|
|
147
478
|
|
|
148
479
|
/**
|
|
149
|
-
* Create a reflection matrix across the X axis
|
|
150
|
-
*
|
|
480
|
+
* Create a reflection matrix across the X axis.
|
|
481
|
+
*
|
|
482
|
+
* Reflects points across the X axis by negating the Y coordinate. This creates
|
|
483
|
+
* a mirror image where the X axis acts as the mirror line. Points above the X
|
|
484
|
+
* axis move below it, and vice versa.
|
|
485
|
+
*
|
|
486
|
+
* Matrix form:
|
|
487
|
+
* ```
|
|
488
|
+
* [1 0 0]
|
|
489
|
+
* [0 -1 0]
|
|
490
|
+
* [0 0 1]
|
|
491
|
+
* ```
|
|
492
|
+
*
|
|
493
|
+
* Effect on point (x, y):
|
|
494
|
+
* ```
|
|
495
|
+
* x' = x
|
|
496
|
+
* y' = -y
|
|
497
|
+
* ```
|
|
498
|
+
*
|
|
499
|
+
* This is equivalent to scale(1, -1) and is a special case of reflection.
|
|
500
|
+
* The transformation is its own inverse: reflecting twice returns to original.
|
|
501
|
+
*
|
|
502
|
+
* @returns {Matrix} 3x3 reflection matrix that flips Y coordinates
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* // Reflect a point across the X axis
|
|
506
|
+
* const R = reflectX();
|
|
507
|
+
* const [x, y] = applyTransform(R, 5, 10);
|
|
508
|
+
* // Result: x = 5, y = -10
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* // Double reflection returns to original
|
|
512
|
+
* const R = reflectX();
|
|
513
|
+
* const composed = R.mul(R);
|
|
514
|
+
* // composed is the identity matrix (no change)
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* // Flip a shape vertically in screen coordinates
|
|
518
|
+
* const R = reflectX();
|
|
519
|
+
* // In screen coords (Y down), this flips the shape upside down
|
|
151
520
|
*/
|
|
152
521
|
export function reflectX() {
|
|
153
522
|
return Matrix.from([
|
|
@@ -158,8 +527,46 @@ export function reflectX() {
|
|
|
158
527
|
}
|
|
159
528
|
|
|
160
529
|
/**
|
|
161
|
-
* Create a reflection matrix across the Y axis
|
|
162
|
-
*
|
|
530
|
+
* Create a reflection matrix across the Y axis.
|
|
531
|
+
*
|
|
532
|
+
* Reflects points across the Y axis by negating the X coordinate. This creates
|
|
533
|
+
* a mirror image where the Y axis acts as the mirror line. Points to the right
|
|
534
|
+
* of the Y axis move to the left, and vice versa.
|
|
535
|
+
*
|
|
536
|
+
* Matrix form:
|
|
537
|
+
* ```
|
|
538
|
+
* [-1 0 0]
|
|
539
|
+
* [0 1 0]
|
|
540
|
+
* [0 0 1]
|
|
541
|
+
* ```
|
|
542
|
+
*
|
|
543
|
+
* Effect on point (x, y):
|
|
544
|
+
* ```
|
|
545
|
+
* x' = -x
|
|
546
|
+
* y' = y
|
|
547
|
+
* ```
|
|
548
|
+
*
|
|
549
|
+
* This is equivalent to scale(-1, 1) and is a special case of reflection.
|
|
550
|
+
* The transformation is its own inverse: reflecting twice returns to original.
|
|
551
|
+
*
|
|
552
|
+
* @returns {Matrix} 3x3 reflection matrix that flips X coordinates
|
|
553
|
+
*
|
|
554
|
+
* @example
|
|
555
|
+
* // Reflect a point across the Y axis
|
|
556
|
+
* const R = reflectY();
|
|
557
|
+
* const [x, y] = applyTransform(R, 5, 10);
|
|
558
|
+
* // Result: x = -5, y = 10
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* // Create a mirror image of a shape
|
|
562
|
+
* const R = reflectY();
|
|
563
|
+
* // Points on the right side move to the left, creating horizontal flip
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* // Double reflection returns to original
|
|
567
|
+
* const R = reflectY();
|
|
568
|
+
* const composed = R.mul(R);
|
|
569
|
+
* // composed is the identity matrix (no change)
|
|
163
570
|
*/
|
|
164
571
|
export function reflectY() {
|
|
165
572
|
return Matrix.from([
|
|
@@ -170,9 +577,58 @@ export function reflectY() {
|
|
|
170
577
|
}
|
|
171
578
|
|
|
172
579
|
/**
|
|
173
|
-
* Create a reflection matrix across the origin (
|
|
174
|
-
*
|
|
175
|
-
*
|
|
580
|
+
* Create a reflection matrix across the origin (point reflection).
|
|
581
|
+
*
|
|
582
|
+
* Reflects points through the origin by negating both coordinates. This is also
|
|
583
|
+
* known as a point reflection or 180° rotation. Each point (x, y) maps to (-x, -y),
|
|
584
|
+
* which is the point diametrically opposite through the origin.
|
|
585
|
+
*
|
|
586
|
+
* Matrix form:
|
|
587
|
+
* ```
|
|
588
|
+
* [-1 0 0]
|
|
589
|
+
* [0 -1 0]
|
|
590
|
+
* [0 0 1]
|
|
591
|
+
* ```
|
|
592
|
+
*
|
|
593
|
+
* Effect on point (x, y):
|
|
594
|
+
* ```
|
|
595
|
+
* x' = -x
|
|
596
|
+
* y' = -y
|
|
597
|
+
* ```
|
|
598
|
+
*
|
|
599
|
+
* This transformation is equivalent to:
|
|
600
|
+
* - Rotation by π radians (180°): rotate(Math.PI)
|
|
601
|
+
* - Scale by -1 in both directions: scale(-1, -1)
|
|
602
|
+
* - Composition of reflectX() and reflectY()
|
|
603
|
+
*
|
|
604
|
+
* The transformation is its own inverse: reflecting twice returns to original.
|
|
605
|
+
* This is also a central inversion or half-turn rotation.
|
|
606
|
+
*
|
|
607
|
+
* @returns {Matrix} 3x3 reflection matrix that flips both X and Y coordinates
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* // Reflect a point through the origin
|
|
611
|
+
* const R = reflectOrigin();
|
|
612
|
+
* const [x, y] = applyTransform(R, 3, 4);
|
|
613
|
+
* // Result: x = -3, y = -4
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* // Equivalent to 180° rotation
|
|
617
|
+
* const R1 = reflectOrigin();
|
|
618
|
+
* const R2 = rotate(Math.PI);
|
|
619
|
+
* // R1 and R2 produce the same transformation
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* // Double reflection returns to original
|
|
623
|
+
* const R = reflectOrigin();
|
|
624
|
+
* const composed = R.mul(R);
|
|
625
|
+
* // composed is the identity matrix (no change)
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* // Equivalent to composing X and Y reflections
|
|
629
|
+
* const R1 = reflectOrigin();
|
|
630
|
+
* const R2 = reflectX().mul(reflectY());
|
|
631
|
+
* // R1 and R2 produce the same transformation
|
|
176
632
|
*/
|
|
177
633
|
export function reflectOrigin() {
|
|
178
634
|
return Matrix.from([
|