@emasoft/svg-matrix 1.0.5 → 1.0.7

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.
@@ -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
- * All transforms return 3x3 Matrix objects that can be composed via multiplication.
15
- * Transform composition is right-to-left: T.mul(R).mul(S) applies S first, then R, then T.
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
- * @param {number|string|Decimal} tx - Translation in X direction
23
- * @param {number|string|Decimal} ty - Translation in Y direction
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
- * @param {number|string|Decimal} sx - Scale factor in X direction
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
- * Uses standard rotation matrix:
53
- * | cos(θ) -sin(θ) 0 |
54
- * | sin(θ) cos(θ) 0 |
55
- * | 0 0 1 |
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
- * @param {number|string|Decimal} theta - Rotation angle in radians
76
- * @param {number|string|Decimal} px - X coordinate of rotation center
77
- * @param {number|string|Decimal} py - Y coordinate of rotation center
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
- * | 1 ax 0 |
90
- * | ay 1 0 |
91
- * | 0 0 1 |
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
- * @param {number|string|Decimal} ax - Skew factor in X direction (affects X based on Y)
94
- * @param {number|string|Decimal} ay - Skew factor in Y direction (affects Y based on X)
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
- * The axis should be normalized (ux² + uy² = 1), but this is not enforced.
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
- * @param {Matrix} M - 3x3 transformation matrix
136
- * @param {number|string|Decimal} x - X coordinate of point
137
- * @param {number|string|Decimal} y - Y coordinate of point
138
- * @returns {Decimal[]} Transformed point as [x', y'] array of Decimals
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 (flips Y).
150
- * @returns {Matrix} 3x3 reflection matrix
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 (flips X).
162
- * @returns {Matrix} 3x3 reflection matrix
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 (flips both X and Y).
174
- * Equivalent to rotation by π radians.
175
- * @returns {Matrix} 3x3 reflection matrix
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([