@emasoft/svg-matrix 1.0.11 → 1.0.13

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.
@@ -3,6 +3,11 @@ import { Matrix } from './matrix.js';
3
3
 
4
4
  const D = x => (x instanceof Decimal ? x : new Decimal(x));
5
5
 
6
+ /**
7
+ * Standard kappa for 90° arcs (4 Bezier curves per circle).
8
+ * kappa = 4/3 * (sqrt(2) - 1) ≈ 0.5522847498
9
+ * Maximum radial error: ~0.027%
10
+ */
6
11
  export function getKappa() {
7
12
  const two = new Decimal(2);
8
13
  const three = new Decimal(3);
@@ -10,6 +15,137 @@ export function getKappa() {
10
15
  return four.mul(two.sqrt().minus(1)).div(three);
11
16
  }
12
17
 
18
+ /**
19
+ * Compute the optimal Bezier control point distance for any arc angle.
20
+ * Formula: L = (4/3) * tan(theta/4) where theta is in radians
21
+ *
22
+ * This is the generalization of the kappa constant.
23
+ * For 90° (π/2): L = (4/3) * tan(π/8) ≈ 0.5522847498 (standard kappa)
24
+ *
25
+ * References:
26
+ * - Spencer Mortensen's optimal Bezier circle approximation:
27
+ * https://spencermortensen.com/articles/bezier-circle/
28
+ * Derives the optimal kappa for minimizing radial error in quarter-circle arcs.
29
+ *
30
+ * - Akhil's ellipse approximation with π/4 step angle:
31
+ * https://www.blog.akhil.cc/ellipse
32
+ * Shows that π/4 (45°) is the optimal step angle for Bezier arc approximation,
33
+ * based on Maisonobe's derivation. This allows precomputing the alpha coefficient.
34
+ *
35
+ * - Math Stack Exchange derivation:
36
+ * https://math.stackexchange.com/questions/873224
37
+ * General formula for control point distance for any arc angle.
38
+ *
39
+ * @param {Decimal|number} thetaRadians - Arc angle in radians
40
+ * @returns {Decimal} Control point distance factor (multiply by radius)
41
+ */
42
+ export function getKappaForArc(thetaRadians) {
43
+ const theta = D(thetaRadians);
44
+ const four = new Decimal(4);
45
+ const three = new Decimal(3);
46
+ // L = (4/3) * tan(theta/4)
47
+ return four.div(three).mul(Decimal.tan(theta.div(four)));
48
+ }
49
+
50
+ /**
51
+ * High-precision circle to path using N Bezier arcs.
52
+ * More arcs = better approximation of the true circle.
53
+ *
54
+ * IMPORTANT: Arc count should be a multiple of 4 (for symmetry) and ideally
55
+ * a multiple of 8 (for optimal π/4 step angle as per Maisonobe's derivation).
56
+ * Reference: https://www.blog.akhil.cc/ellipse
57
+ *
58
+ * Error analysis (measured):
59
+ * - 4 arcs (90° = π/2 each): ~0.027% max radial error (standard)
60
+ * - 8 arcs (45° = π/4 each): ~0.0004% max radial error (optimal base)
61
+ * - 16 arcs (22.5° = π/8 each): ~0.000007% max radial error
62
+ * - 32 arcs (11.25° = π/16 each): ~0.0000004% max radial error
63
+ * - 64 arcs (5.625° = π/32 each): ~0.00000001% max radial error
64
+ *
65
+ * @param {number|Decimal} cx - Center X
66
+ * @param {number|Decimal} cy - Center Y
67
+ * @param {number|Decimal} r - Radius
68
+ * @param {number} arcs - Number of Bezier arcs (must be multiple of 4; 8, 16, 32, 64 recommended)
69
+ * @param {number} precision - Decimal precision for output
70
+ * @returns {string} SVG path data
71
+ */
72
+ export function circleToPathDataHP(cx, cy, r, arcs = 8, precision = 6) {
73
+ return ellipseToPathDataHP(cx, cy, r, r, arcs, precision);
74
+ }
75
+
76
+ /**
77
+ * High-precision ellipse to path using N Bezier arcs.
78
+ * More arcs = better approximation of the true ellipse.
79
+ *
80
+ * Arc count must be a multiple of 4 for proper symmetry.
81
+ * Multiples of 8 are optimal (π/4 step angle per Maisonobe).
82
+ *
83
+ * @param {number|Decimal} cx - Center X
84
+ * @param {number|Decimal} cy - Center Y
85
+ * @param {number|Decimal} rx - Radius X
86
+ * @param {number|Decimal} ry - Radius Y
87
+ * @param {number} arcs - Number of Bezier arcs (must be multiple of 4; 8, 16, 32, 64 recommended)
88
+ * @param {number} precision - Decimal precision for output
89
+ * @returns {string} SVG path data
90
+ */
91
+ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
92
+ // Enforce multiple of 4 for symmetry
93
+ if (arcs % 4 !== 0) {
94
+ arcs = Math.ceil(arcs / 4) * 4;
95
+ }
96
+ const cxD = D(cx), cyD = D(cy), rxD = D(rx), ryD = D(ry);
97
+ const f = v => formatNumber(v, precision);
98
+
99
+ // Angle per arc in radians
100
+ const PI = Decimal.acos(-1);
101
+ const TWO_PI = PI.mul(2);
102
+ const arcAngle = TWO_PI.div(arcs);
103
+
104
+ // Control point distance for this arc angle
105
+ const kappa = getKappaForArc(arcAngle);
106
+
107
+ // Generate path
108
+ const commands = [];
109
+
110
+ for (let i = 0; i < arcs; i++) {
111
+ const startAngle = arcAngle.mul(i);
112
+ const endAngle = arcAngle.mul(i + 1);
113
+
114
+ // Start and end points on ellipse
115
+ const cosStart = Decimal.cos(startAngle);
116
+ const sinStart = Decimal.sin(startAngle);
117
+ const cosEnd = Decimal.cos(endAngle);
118
+ const sinEnd = Decimal.sin(endAngle);
119
+
120
+ const x0 = cxD.plus(rxD.mul(cosStart));
121
+ const y0 = cyD.plus(ryD.mul(sinStart));
122
+ const x3 = cxD.plus(rxD.mul(cosEnd));
123
+ const y3 = cyD.plus(ryD.mul(sinEnd));
124
+
125
+ // Tangent vectors at start and end (perpendicular to radius, scaled by kappa)
126
+ // Tangent at angle θ: (-sin(θ), cos(θ))
127
+ // Control point 1: start + kappa * tangent_at_start * radius
128
+ // Control point 2: end - kappa * tangent_at_end * radius
129
+ const tx0 = sinStart.neg(); // tangent x at start
130
+ const ty0 = cosStart; // tangent y at start
131
+ const tx3 = sinEnd.neg(); // tangent x at end
132
+ const ty3 = cosEnd; // tangent y at end
133
+
134
+ const x1 = x0.plus(kappa.mul(rxD).mul(tx0));
135
+ const y1 = y0.plus(kappa.mul(ryD).mul(ty0));
136
+ const x2 = x3.minus(kappa.mul(rxD).mul(tx3));
137
+ const y2 = y3.minus(kappa.mul(ryD).mul(ty3));
138
+
139
+ if (i === 0) {
140
+ commands.push(`M ${f(x0)} ${f(y0)}`);
141
+ }
142
+ commands.push(`C ${f(x1)} ${f(y1)} ${f(x2)} ${f(y2)} ${f(x3)} ${f(y3)}`);
143
+ }
144
+
145
+ commands.push('Z');
146
+ return commands.join(' ');
147
+ }
148
+
13
149
  function formatNumber(value, precision = 6) {
14
150
  // Use toFixed to preserve trailing zeros for consistent output formatting
15
151
  return value.toFixed(precision);
package/src/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * SVG path conversion, and 2D/3D affine transformations using Decimal.js.
6
6
  *
7
7
  * @module @emasoft/svg-matrix
8
- * @version 1.0.11
8
+ * @version 1.0.12
9
9
  * @license MIT
10
10
  *
11
11
  * @example
@@ -44,6 +44,9 @@ import * as UseSymbolResolver from './use-symbol-resolver.js';
44
44
  import * as MarkerResolver from './marker-resolver.js';
45
45
  import * as MeshGradient from './mesh-gradient.js';
46
46
  import * as TextToPath from './text-to-path.js';
47
+ import * as SVGParser from './svg-parser.js';
48
+ import * as FlattenPipeline from './flatten-pipeline.js';
49
+ import * as Verification from './verification.js';
47
50
  import { Logger, LogLevel, setLogLevel, getLogLevel as getLoggerLevel, enableFileLogging, disableFileLogging } from './logger.js';
48
51
 
49
52
  // Set high-precision default (80 significant digits) on module load
@@ -55,7 +58,7 @@ Decimal.set({ precision: 80 });
55
58
  * Library version
56
59
  * @constant {string}
57
60
  */
58
- export const VERSION = '1.0.11';
61
+ export const VERSION = '1.0.12';
59
62
 
60
63
  /**
61
64
  * Default precision for path output (decimal places)
@@ -89,6 +92,7 @@ export { SVGFlatten, BrowserVerify };
89
92
  export { ClipPathResolver, MaskResolver, PatternResolver };
90
93
  export { UseSymbolResolver, MarkerResolver };
91
94
  export { MeshGradient, TextToPath };
95
+ export { SVGParser, FlattenPipeline, Verification };
92
96
 
93
97
  // ============================================================================
94
98
  // LOGGING: Configurable logging control
@@ -421,6 +425,9 @@ export default {
421
425
  MarkerResolver,
422
426
  MeshGradient,
423
427
  TextToPath,
428
+ SVGParser,
429
+ FlattenPipeline,
430
+ Verification,
424
431
 
425
432
  // Logging
426
433
  Logger,