@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.
- package/bin/svg-matrix.js +319 -123
- package/package.json +1 -1
- package/src/clip-path-resolver.js +25 -3
- package/src/flatten-pipeline.js +1158 -0
- package/src/geometry-to-path.js +136 -0
- package/src/index.js +9 -2
- package/src/svg-parser.js +730 -0
- package/src/verification.js +1242 -0
package/src/geometry-to-path.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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,
|