@emasoft/svg-matrix 1.0.18 → 1.0.20
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 +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/scripts/postinstall.js +6 -9
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emasoft/svg-matrix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"test:browser": "node test/browser-verify.mjs",
|
|
10
10
|
"test:playwright": "node test/playwright-diagnose.js",
|
|
11
11
|
"ci-test": "npm ci && npm test",
|
|
12
|
+
"lint": "node bin/svglinter.cjs",
|
|
13
|
+
"lint:fix": "node bin/svglinter.cjs --fix",
|
|
12
14
|
"version:sync": "node scripts/version-sync.js",
|
|
13
15
|
"version:check": "node scripts/version-sync.js --check",
|
|
14
16
|
"preversion": "npm run version:sync",
|
|
@@ -52,7 +54,8 @@
|
|
|
52
54
|
"url": "https://github.com/Emasoft/SVG-MATRIX/issues"
|
|
53
55
|
},
|
|
54
56
|
"bin": {
|
|
55
|
-
"svg-matrix": "./bin/svg-matrix.js"
|
|
57
|
+
"svg-matrix": "./bin/svg-matrix.js",
|
|
58
|
+
"svglinter": "./bin/svglinter.cjs"
|
|
56
59
|
},
|
|
57
60
|
"engines": {
|
|
58
61
|
"node": ">=24.0.0"
|
|
@@ -74,5 +77,8 @@
|
|
|
74
77
|
"playwright": {
|
|
75
78
|
"optional": true
|
|
76
79
|
}
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"svgo": "^4.0.0"
|
|
77
83
|
}
|
|
78
84
|
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -114,15 +114,12 @@ ${c.cyan}${B.v}${hr}${B.v}${c.reset}
|
|
|
114
114
|
${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
|
|
115
115
|
${c.cyan}${B.v}${c.reset}${R(` ${c.yellow}CLI Commands:${c.reset}`)}${c.cyan}${B.v}${c.reset}
|
|
116
116
|
${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
|
|
117
|
-
${c.cyan}${B.v}${c.reset}${R(` ${c.green}svg-matrix flatten${c.reset}
|
|
118
|
-
${c.cyan}${B.v}${c.reset}${R(`
|
|
117
|
+
${c.cyan}${B.v}${c.reset}${R(` ${c.green}svg-matrix flatten${c.reset} ${c.dim}Bake transforms into path coordinates${c.reset}`)}${c.cyan}${B.v}${c.reset}
|
|
118
|
+
${c.cyan}${B.v}${c.reset}${R(` ${c.green}svg-matrix convert${c.reset} ${c.dim}Convert shapes to <path> elements${c.reset}`)}${c.cyan}${B.v}${c.reset}
|
|
119
|
+
${c.cyan}${B.v}${c.reset}${R(` ${c.green}svg-matrix normalize${c.reset} ${c.dim}Convert paths to cubic Beziers${c.reset}`)}${c.cyan}${B.v}${c.reset}
|
|
120
|
+
${c.cyan}${B.v}${c.reset}${R(` ${c.green}svg-matrix info${c.reset} ${c.dim}Show SVG file information${c.reset}`)}${c.cyan}${B.v}${c.reset}
|
|
119
121
|
${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
|
|
120
|
-
${c.cyan}${B.v}${c.reset}${R(` ${c.
|
|
121
|
-
${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--precision N${c.reset} Output decimal places (default: 6)`)}${c.cyan}${B.v}${c.reset}
|
|
122
|
-
${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--clip-segments N${c.reset} Polygon sampling for clips (default: 64)`)}${c.cyan}${B.v}${c.reset}
|
|
123
|
-
${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--bezier-arcs N${c.reset} Bezier arcs for curves (default: 8)`)}${c.cyan}${B.v}${c.reset}
|
|
124
|
-
${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--e2e-tolerance N${c.reset} Verification tolerance (default: 1e-10)`)}${c.cyan}${B.v}${c.reset}
|
|
125
|
-
${c.cyan}${B.v}${c.reset}${R(` ${c.dim}--verbose${c.reset} Show processing details`)}${c.cyan}${B.v}${c.reset}
|
|
122
|
+
${c.cyan}${B.v}${c.reset}${R(` ${c.dim}Run${c.reset} svg-matrix --help ${c.dim}or${c.reset} svg-matrix <cmd> --help`)}${c.cyan}${B.v}${c.reset}
|
|
126
123
|
${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
|
|
127
124
|
${c.cyan}${B.v}${hr}${B.v}${c.reset}
|
|
128
125
|
${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
|
|
@@ -137,7 +134,7 @@ ${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot} Transforms3D${c.reset} 3D af
|
|
|
137
134
|
${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
|
|
138
135
|
${c.cyan}${B.v}${hr}${B.v}${c.reset}
|
|
139
136
|
${c.cyan}${B.v}${c.reset}${R('')}${c.cyan}${B.v}${c.reset}
|
|
140
|
-
${c.cyan}${B.v}${c.reset}${R(` ${c.yellow}New in v1.0.
|
|
137
|
+
${c.cyan}${B.v}${c.reset}${R(` ${c.yellow}New in v1.0.19:${c.reset}`)}${c.cyan}${B.v}${c.reset}
|
|
141
138
|
${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot}${c.reset} Enhanced CLI help: svg-matrix <command> --help`)}${c.cyan}${B.v}${c.reset}
|
|
142
139
|
${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot}${c.reset} High-precision Bezier circle/ellipse approximation`)}${c.cyan}${B.v}${c.reset}
|
|
143
140
|
${c.cyan}${B.v}${c.reset}${R(` ${c.green}${B.dot}${c.reset} E2E verification always enabled for precision`)}${c.cyan}${B.v}${c.reset}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Timing Optimization for SVG
|
|
3
|
+
*
|
|
4
|
+
* Optimizes SMIL animation timing attributes without breaking animations:
|
|
5
|
+
*
|
|
6
|
+
* 1. keySplines optimization:
|
|
7
|
+
* - Remove leading zeros (0.4 -> .4)
|
|
8
|
+
* - Remove trailing zeros (0.500 -> .5)
|
|
9
|
+
* - Precision reduction with configurable decimal places
|
|
10
|
+
* - Detect linear splines (0 0 1 1) and simplify calcMode
|
|
11
|
+
*
|
|
12
|
+
* 2. keyTimes optimization:
|
|
13
|
+
* - Remove redundant precision
|
|
14
|
+
* - Normalize separator spacing
|
|
15
|
+
*
|
|
16
|
+
* 3. values optimization:
|
|
17
|
+
* - Numeric precision reduction for numeric values
|
|
18
|
+
* - Preserve ID references (#frame1;#frame2) exactly
|
|
19
|
+
*
|
|
20
|
+
* @module animation-optimization
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import Decimal from 'decimal.js';
|
|
24
|
+
|
|
25
|
+
// Configure Decimal for high precision internally
|
|
26
|
+
Decimal.set({ precision: 20, rounding: Decimal.ROUND_HALF_UP });
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Standard easing curves that can be recognized
|
|
30
|
+
* Format: [x1, y1, x2, y2] control points
|
|
31
|
+
*/
|
|
32
|
+
export const STANDARD_EASINGS = {
|
|
33
|
+
linear: [0, 0, 1, 1],
|
|
34
|
+
ease: [0.25, 0.1, 0.25, 1],
|
|
35
|
+
'ease-in': [0.42, 0, 1, 1],
|
|
36
|
+
'ease-out': [0, 0, 0.58, 1],
|
|
37
|
+
'ease-in-out': [0.42, 0, 0.58, 1],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format a number with optimal precision (no trailing zeros, no leading zero for decimals < 1)
|
|
42
|
+
* @param {number|string} value - Number to format
|
|
43
|
+
* @param {number} precision - Maximum decimal places (default: 3)
|
|
44
|
+
* @returns {string} Optimized number string
|
|
45
|
+
*/
|
|
46
|
+
export function formatSplineValue(value, precision = 3) {
|
|
47
|
+
const num = new Decimal(value);
|
|
48
|
+
|
|
49
|
+
// Round to precision
|
|
50
|
+
const rounded = num.toDecimalPlaces(precision);
|
|
51
|
+
|
|
52
|
+
// Convert to string
|
|
53
|
+
let str = rounded.toString();
|
|
54
|
+
|
|
55
|
+
// Remove trailing zeros after decimal point
|
|
56
|
+
if (str.includes('.')) {
|
|
57
|
+
str = str.replace(/\.?0+$/, '');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Remove leading zero for values between -1 and 1 (exclusive)
|
|
61
|
+
if (str.startsWith('0.')) {
|
|
62
|
+
str = str.substring(1); // "0.5" -> ".5"
|
|
63
|
+
} else if (str.startsWith('-0.')) {
|
|
64
|
+
str = '-' + str.substring(2); // "-0.5" -> "-.5"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle edge case: ".0" should be "0"
|
|
68
|
+
if (str === '' || str === '.') str = '0';
|
|
69
|
+
|
|
70
|
+
return str;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse a keySplines attribute value into array of spline arrays
|
|
75
|
+
* Each spline has 4 values: [x1, y1, x2, y2]
|
|
76
|
+
* @param {string} keySplines - keySplines attribute value
|
|
77
|
+
* @returns {number[][]} Array of [x1, y1, x2, y2] arrays
|
|
78
|
+
*/
|
|
79
|
+
export function parseKeySplines(keySplines) {
|
|
80
|
+
if (!keySplines || typeof keySplines !== 'string') return [];
|
|
81
|
+
|
|
82
|
+
// Split by semicolon to get individual splines
|
|
83
|
+
const splines = keySplines.split(';').map(s => s.trim()).filter(s => s);
|
|
84
|
+
|
|
85
|
+
return splines.map(spline => {
|
|
86
|
+
// Split by whitespace or comma to get control points
|
|
87
|
+
const values = spline.split(/[\s,]+/).map(v => parseFloat(v));
|
|
88
|
+
return values;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Serialize splines array back to keySplines attribute format
|
|
94
|
+
* @param {number[][]} splines - Array of [x1, y1, x2, y2] arrays
|
|
95
|
+
* @param {number} precision - Maximum decimal places
|
|
96
|
+
* @returns {string} keySplines attribute value
|
|
97
|
+
*/
|
|
98
|
+
export function serializeKeySplines(splines, precision = 3) {
|
|
99
|
+
return splines.map(spline => {
|
|
100
|
+
return spline.map(v => formatSplineValue(v, precision)).join(' ');
|
|
101
|
+
}).join('; ');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if a spline is effectively linear (0 0 1 1)
|
|
106
|
+
* @param {number[]} spline - [x1, y1, x2, y2] control points
|
|
107
|
+
* @param {number} tolerance - Comparison tolerance (default: 0.001)
|
|
108
|
+
* @returns {boolean} True if spline is linear
|
|
109
|
+
*/
|
|
110
|
+
export function isLinearSpline(spline, tolerance = 0.001) {
|
|
111
|
+
if (!spline || spline.length !== 4) return false;
|
|
112
|
+
|
|
113
|
+
const [x1, y1, x2, y2] = spline;
|
|
114
|
+
return (
|
|
115
|
+
Math.abs(x1) < tolerance &&
|
|
116
|
+
Math.abs(y1) < tolerance &&
|
|
117
|
+
Math.abs(x2 - 1) < tolerance &&
|
|
118
|
+
Math.abs(y2 - 1) < tolerance
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if all splines in a keySplines value are linear
|
|
124
|
+
* @param {string} keySplines - keySplines attribute value
|
|
125
|
+
* @returns {boolean} True if all splines are linear
|
|
126
|
+
*/
|
|
127
|
+
export function areAllSplinesLinear(keySplines) {
|
|
128
|
+
const splines = parseKeySplines(keySplines);
|
|
129
|
+
if (splines.length === 0) return false;
|
|
130
|
+
// Must wrap in arrow function to avoid .every() passing index as tolerance
|
|
131
|
+
return splines.every(s => isLinearSpline(s));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Identify if a spline matches a standard CSS easing
|
|
136
|
+
* @param {number[]} spline - [x1, y1, x2, y2] control points
|
|
137
|
+
* @param {number} tolerance - Comparison tolerance
|
|
138
|
+
* @returns {string|null} Easing name or null if not standard
|
|
139
|
+
*/
|
|
140
|
+
export function identifyStandardEasing(spline, tolerance = 0.01) {
|
|
141
|
+
if (!spline || spline.length !== 4) return null;
|
|
142
|
+
|
|
143
|
+
for (const [name, standard] of Object.entries(STANDARD_EASINGS)) {
|
|
144
|
+
const matches = spline.every((val, i) => Math.abs(val - standard[i]) < tolerance);
|
|
145
|
+
if (matches) return name;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Optimize a keySplines attribute value
|
|
152
|
+
* @param {string} keySplines - Original keySplines value
|
|
153
|
+
* @param {Object} options - Optimization options
|
|
154
|
+
* @param {number} options.precision - Max decimal places (default: 3)
|
|
155
|
+
* @param {boolean} options.removeLinear - If true and all splines are linear, return null (default: true)
|
|
156
|
+
* @returns {{value: string|null, allLinear: boolean, standardEasings: string[]}}
|
|
157
|
+
*/
|
|
158
|
+
export function optimizeKeySplines(keySplines, options = {}) {
|
|
159
|
+
const precision = options.precision ?? 3;
|
|
160
|
+
const removeLinear = options.removeLinear !== false;
|
|
161
|
+
|
|
162
|
+
const splines = parseKeySplines(keySplines);
|
|
163
|
+
|
|
164
|
+
if (splines.length === 0) {
|
|
165
|
+
return { value: null, allLinear: false, standardEasings: [] };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check for all linear splines (wrap to avoid .every() passing index as tolerance)
|
|
169
|
+
const allLinear = splines.every(s => isLinearSpline(s));
|
|
170
|
+
|
|
171
|
+
// Identify standard easings
|
|
172
|
+
const standardEasings = splines.map(s => identifyStandardEasing(s)).filter(Boolean);
|
|
173
|
+
|
|
174
|
+
// If all linear and removeLinear is true, suggest removing keySplines
|
|
175
|
+
if (allLinear && removeLinear) {
|
|
176
|
+
return { value: null, allLinear: true, standardEasings };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Optimize each spline value with precision
|
|
180
|
+
const optimized = serializeKeySplines(splines, precision);
|
|
181
|
+
|
|
182
|
+
return { value: optimized, allLinear, standardEasings };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Parse keyTimes attribute value
|
|
187
|
+
* @param {string} keyTimes - keyTimes attribute value
|
|
188
|
+
* @returns {number[]} Array of time values
|
|
189
|
+
*/
|
|
190
|
+
export function parseKeyTimes(keyTimes) {
|
|
191
|
+
if (!keyTimes || typeof keyTimes !== 'string') return [];
|
|
192
|
+
return keyTimes.split(';').map(s => parseFloat(s.trim())).filter(v => !isNaN(v));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Serialize keyTimes array back to attribute format
|
|
197
|
+
* @param {number[]} times - Array of time values
|
|
198
|
+
* @param {number} precision - Maximum decimal places
|
|
199
|
+
* @returns {string} keyTimes attribute value
|
|
200
|
+
*/
|
|
201
|
+
export function serializeKeyTimes(times, precision = 3) {
|
|
202
|
+
return times.map(t => formatSplineValue(t, precision)).join('; ');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Optimize keyTimes attribute value
|
|
207
|
+
* @param {string} keyTimes - Original keyTimes value
|
|
208
|
+
* @param {number} precision - Max decimal places (default: 3)
|
|
209
|
+
* @returns {string} Optimized keyTimes value
|
|
210
|
+
*/
|
|
211
|
+
export function optimizeKeyTimes(keyTimes, precision = 3) {
|
|
212
|
+
const times = parseKeyTimes(keyTimes);
|
|
213
|
+
if (times.length === 0) return keyTimes;
|
|
214
|
+
return serializeKeyTimes(times, precision);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Optimize numeric values in animation values attribute
|
|
219
|
+
* Preserves ID references (#id) exactly
|
|
220
|
+
* @param {string} values - values attribute
|
|
221
|
+
* @param {number} precision - Max decimal places for numbers
|
|
222
|
+
* @returns {string} Optimized values
|
|
223
|
+
*/
|
|
224
|
+
export function optimizeAnimationValues(values, precision = 3) {
|
|
225
|
+
if (!values || typeof values !== 'string') return values;
|
|
226
|
+
|
|
227
|
+
// Split by semicolon
|
|
228
|
+
const parts = values.split(';');
|
|
229
|
+
|
|
230
|
+
const optimized = parts.map(part => {
|
|
231
|
+
const trimmed = part.trim();
|
|
232
|
+
|
|
233
|
+
// Preserve ID references exactly
|
|
234
|
+
if (trimmed.startsWith('#') || trimmed.includes('url(')) {
|
|
235
|
+
return trimmed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Try to parse as numbers (could be space-separated like "0 0" for translate)
|
|
239
|
+
const nums = trimmed.split(/[\s,]+/);
|
|
240
|
+
const optimizedNums = nums.map(n => {
|
|
241
|
+
const num = parseFloat(n);
|
|
242
|
+
if (isNaN(num)) return n; // Not a number, preserve as-is
|
|
243
|
+
return formatSplineValue(num, precision);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return optimizedNums.join(' ');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return optimized.join('; ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Optimize all animation timing attributes on an element
|
|
254
|
+
* @param {Element} el - SVG element (animate, animateTransform, etc.)
|
|
255
|
+
* @param {Object} options - Optimization options
|
|
256
|
+
* @returns {{modified: boolean, changes: string[]}}
|
|
257
|
+
*/
|
|
258
|
+
export function optimizeElementTiming(el, options = {}) {
|
|
259
|
+
const precision = options.precision ?? 3;
|
|
260
|
+
const removeLinearSplines = options.removeLinearSplines !== false;
|
|
261
|
+
const optimizeValues = options.optimizeValues !== false;
|
|
262
|
+
|
|
263
|
+
const changes = [];
|
|
264
|
+
let modified = false;
|
|
265
|
+
|
|
266
|
+
// Optimize keySplines
|
|
267
|
+
const keySplines = el.getAttribute('keySplines');
|
|
268
|
+
if (keySplines) {
|
|
269
|
+
const result = optimizeKeySplines(keySplines, { precision, removeLinear: removeLinearSplines });
|
|
270
|
+
|
|
271
|
+
if (result.allLinear && removeLinearSplines) {
|
|
272
|
+
// All splines are linear - can simplify to calcMode="linear"
|
|
273
|
+
const calcMode = el.getAttribute('calcMode');
|
|
274
|
+
if (calcMode === 'spline') {
|
|
275
|
+
el.setAttribute('calcMode', 'linear');
|
|
276
|
+
el.removeAttribute('keySplines');
|
|
277
|
+
changes.push('Converted linear splines to calcMode="linear"');
|
|
278
|
+
modified = true;
|
|
279
|
+
}
|
|
280
|
+
} else if (result.value && result.value !== keySplines) {
|
|
281
|
+
el.setAttribute('keySplines', result.value);
|
|
282
|
+
changes.push(`keySplines: "${keySplines}" -> "${result.value}"`);
|
|
283
|
+
modified = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Optimize keyTimes
|
|
288
|
+
const keyTimes = el.getAttribute('keyTimes');
|
|
289
|
+
if (keyTimes) {
|
|
290
|
+
const optimized = optimizeKeyTimes(keyTimes, precision);
|
|
291
|
+
if (optimized !== keyTimes) {
|
|
292
|
+
el.setAttribute('keyTimes', optimized);
|
|
293
|
+
changes.push(`keyTimes: "${keyTimes}" -> "${optimized}"`);
|
|
294
|
+
modified = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Optimize values (only numeric, preserve ID refs)
|
|
299
|
+
if (optimizeValues) {
|
|
300
|
+
const values = el.getAttribute('values');
|
|
301
|
+
if (values && !values.includes('#')) {
|
|
302
|
+
// Only optimize if no ID references
|
|
303
|
+
const optimized = optimizeAnimationValues(values, precision);
|
|
304
|
+
if (optimized !== values) {
|
|
305
|
+
el.setAttribute('values', optimized);
|
|
306
|
+
changes.push(`values: "${values}" -> "${optimized}"`);
|
|
307
|
+
modified = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Optimize from/to
|
|
313
|
+
for (const attr of ['from', 'to', 'by']) {
|
|
314
|
+
const val = el.getAttribute(attr);
|
|
315
|
+
if (val && !val.includes('#')) {
|
|
316
|
+
const optimized = optimizeAnimationValues(val, precision);
|
|
317
|
+
if (optimized !== val) {
|
|
318
|
+
el.setAttribute(attr, optimized);
|
|
319
|
+
changes.push(`${attr}: "${val}" -> "${optimized}"`);
|
|
320
|
+
modified = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { modified, changes };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Animation elements that can have timing attributes
|
|
330
|
+
* Note: all lowercase to match svg-parser tagName normalization
|
|
331
|
+
*/
|
|
332
|
+
export const ANIMATION_ELEMENTS = ['animate', 'animatetransform', 'animatemotion', 'animatecolor', 'set'];
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Optimize all animation timing in an SVG document
|
|
336
|
+
* @param {Element} root - SVG root element
|
|
337
|
+
* @param {Object} options - Optimization options
|
|
338
|
+
* @returns {{elementsModified: number, totalChanges: number, details: Array}}
|
|
339
|
+
*/
|
|
340
|
+
export function optimizeDocumentAnimationTiming(root, options = {}) {
|
|
341
|
+
let elementsModified = 0;
|
|
342
|
+
let totalChanges = 0;
|
|
343
|
+
const details = [];
|
|
344
|
+
|
|
345
|
+
const processElement = (el) => {
|
|
346
|
+
const tagName = el.tagName?.toLowerCase();
|
|
347
|
+
|
|
348
|
+
if (ANIMATION_ELEMENTS.includes(tagName)) {
|
|
349
|
+
const result = optimizeElementTiming(el, options);
|
|
350
|
+
if (result.modified) {
|
|
351
|
+
elementsModified++;
|
|
352
|
+
totalChanges += result.changes.length;
|
|
353
|
+
details.push({
|
|
354
|
+
element: tagName,
|
|
355
|
+
id: el.getAttribute('id') || null,
|
|
356
|
+
changes: result.changes
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const child of el.children || []) {
|
|
362
|
+
processElement(child);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
processElement(root);
|
|
367
|
+
|
|
368
|
+
return { elementsModified, totalChanges, details };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export default {
|
|
372
|
+
// Core functions
|
|
373
|
+
formatSplineValue,
|
|
374
|
+
parseKeySplines,
|
|
375
|
+
serializeKeySplines,
|
|
376
|
+
parseKeyTimes,
|
|
377
|
+
serializeKeyTimes,
|
|
378
|
+
|
|
379
|
+
// Analysis
|
|
380
|
+
isLinearSpline,
|
|
381
|
+
areAllSplinesLinear,
|
|
382
|
+
identifyStandardEasing,
|
|
383
|
+
STANDARD_EASINGS,
|
|
384
|
+
|
|
385
|
+
// Optimization
|
|
386
|
+
optimizeKeySplines,
|
|
387
|
+
optimizeKeyTimes,
|
|
388
|
+
optimizeAnimationValues,
|
|
389
|
+
optimizeElementTiming,
|
|
390
|
+
optimizeDocumentAnimationTiming,
|
|
391
|
+
|
|
392
|
+
// Constants
|
|
393
|
+
ANIMATION_ELEMENTS,
|
|
394
|
+
};
|