@emasoft/svg-matrix 1.0.25 → 1.0.27

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.
@@ -0,0 +1,444 @@
1
+ /**
2
+ * SVG 2.0 Polyfill Generator
3
+ *
4
+ * Detects SVG 2.0 features (mesh gradients, hatches) and generates inline
5
+ * JavaScript polyfills for browser compatibility.
6
+ *
7
+ * Uses the existing mesh-gradient.js math for Coons patch evaluation.
8
+ * All polyfills are embedded inline in the SVG for self-contained output.
9
+ *
10
+ * @module svg2-polyfills
11
+ */
12
+
13
+ /**
14
+ * SVG 2.0 features that can be polyfilled
15
+ */
16
+ export const SVG2_FEATURES = {
17
+ MESH_GRADIENT: 'meshGradient',
18
+ HATCH: 'hatch',
19
+ CONTEXT_PAINT: 'context-paint',
20
+ AUTO_START_REVERSE: 'auto-start-reverse'
21
+ };
22
+
23
+ /**
24
+ * Detect SVG 2.0 features that need polyfills in a document.
25
+ *
26
+ * @param {Object} doc - Parsed SVG document
27
+ * @returns {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} Detected features
28
+ */
29
+ export function detectSVG2Features(doc) {
30
+ const features = {
31
+ meshGradients: [],
32
+ hatches: [],
33
+ contextPaint: false,
34
+ autoStartReverse: false
35
+ };
36
+
37
+ const walk = (el) => {
38
+ if (!el) return;
39
+
40
+ // Check tag name for mesh gradient (case-insensitive)
41
+ const tagName = el.tagName?.toLowerCase();
42
+ if (tagName === 'meshgradient') {
43
+ const id = el.getAttribute('id');
44
+ if (id) features.meshGradients.push(id);
45
+ }
46
+
47
+ // Check for hatch element
48
+ if (tagName === 'hatch') {
49
+ const id = el.getAttribute('id');
50
+ if (id) features.hatches.push(id);
51
+ }
52
+
53
+ // Check for context-paint in fill/stroke
54
+ const fill = el.getAttribute('fill');
55
+ const stroke = el.getAttribute('stroke');
56
+ if (fill === 'context-fill' || fill === 'context-stroke' ||
57
+ stroke === 'context-fill' || stroke === 'context-stroke') {
58
+ features.contextPaint = true;
59
+ }
60
+
61
+ // Check for auto-start-reverse in markers
62
+ const orient = el.getAttribute('orient');
63
+ if (orient === 'auto-start-reverse') {
64
+ features.autoStartReverse = true;
65
+ }
66
+
67
+ // Recurse into children
68
+ if (el.children) {
69
+ for (const child of el.children) {
70
+ walk(child);
71
+ }
72
+ }
73
+ };
74
+
75
+ walk(doc);
76
+ return features;
77
+ }
78
+
79
+ /**
80
+ * Check if document needs any SVG 2 polyfills.
81
+ *
82
+ * @param {Object} doc - Parsed SVG document
83
+ * @returns {boolean} True if polyfills are needed
84
+ */
85
+ export function needsPolyfills(doc) {
86
+ const features = detectSVG2Features(doc);
87
+ return features.meshGradients.length > 0 ||
88
+ features.hatches.length > 0 ||
89
+ features.contextPaint ||
90
+ features.autoStartReverse;
91
+ }
92
+
93
+ /**
94
+ * Generate the mesh gradient polyfill code.
95
+ * This polyfill renders mesh gradients to canvas and uses them as image fills.
96
+ *
97
+ * @returns {string} JavaScript polyfill code
98
+ */
99
+ function generateMeshPolyfillCode() {
100
+ return `
101
+ // Mesh Gradient Polyfill - SVG 2.0 to Canvas fallback
102
+ // Generated by svg-matrix
103
+ (function() {
104
+ 'use strict';
105
+
106
+ // Skip if browser supports mesh gradients natively
107
+ if (typeof document.createElementNS('http://www.w3.org/2000/svg', 'meshGradient').x !== 'undefined') {
108
+ return;
109
+ }
110
+
111
+ // Find all mesh gradients
112
+ var meshes = document.querySelectorAll('meshGradient, meshgradient');
113
+ if (!meshes.length) return;
114
+
115
+ // Parse color string to RGBA
116
+ function parseColor(str) {
117
+ if (!str) return {r: 0, g: 0, b: 0, a: 255};
118
+ var m = str.match(/rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)(?:\\s*,\\s*([\\d.]+))?\\s*\\)/);
119
+ if (m) return {r: +m[1], g: +m[2], b: +m[3], a: m[4] ? Math.round(+m[4] * 255) : 255};
120
+ if (str[0] === '#') {
121
+ var hex = str.slice(1);
122
+ if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
123
+ return {r: parseInt(hex.slice(0,2), 16), g: parseInt(hex.slice(2,4), 16), b: parseInt(hex.slice(4,6), 16), a: 255};
124
+ }
125
+ return {r: 0, g: 0, b: 0, a: 255};
126
+ }
127
+
128
+ // Bilinear color interpolation
129
+ function bilinearColor(c00, c10, c01, c11, u, v) {
130
+ var mu = 1 - u, mv = 1 - v;
131
+ return {
132
+ r: Math.round(mu*mv*c00.r + u*mv*c10.r + mu*v*c01.r + u*v*c11.r),
133
+ g: Math.round(mu*mv*c00.g + u*mv*c10.g + mu*v*c01.g + u*v*c11.g),
134
+ b: Math.round(mu*mv*c00.b + u*mv*c10.b + mu*v*c01.b + u*v*c11.b),
135
+ a: Math.round(mu*mv*c00.a + u*mv*c10.a + mu*v*c01.a + u*v*c11.a)
136
+ };
137
+ }
138
+
139
+ // Evaluate cubic Bezier at t
140
+ function evalBezier(p0, p1, p2, p3, t) {
141
+ var mt = 1 - t, mt2 = mt * mt, mt3 = mt2 * mt;
142
+ var t2 = t * t, t3 = t2 * t;
143
+ return {
144
+ x: mt3*p0.x + 3*mt2*t*p1.x + 3*mt*t2*p2.x + t3*p3.x,
145
+ y: mt3*p0.y + 3*mt2*t*p1.y + 3*mt*t2*p2.y + t3*p3.y
146
+ };
147
+ }
148
+
149
+ // Process each mesh gradient
150
+ meshes.forEach(function(mesh) {
151
+ var id = mesh.getAttribute('id');
152
+ if (!id) return;
153
+
154
+ // Create canvas for rasterization
155
+ var canvas = document.createElement('canvas');
156
+ var ctx = canvas.getContext('2d');
157
+
158
+ // Get bounding box from referencing elements
159
+ var refs = document.querySelectorAll('[fill="url(#' + id + ')"], [stroke="url(#' + id + ')"]');
160
+ if (!refs.length) return;
161
+
162
+ var bbox = refs[0].getBBox();
163
+ var size = Math.max(bbox.width, bbox.height, 256);
164
+ canvas.width = canvas.height = size;
165
+
166
+ // Parse mesh patches
167
+ var patches = [];
168
+ var rows = mesh.querySelectorAll('meshrow');
169
+ rows.forEach(function(row) {
170
+ var rowPatches = row.querySelectorAll('meshpatch');
171
+ rowPatches.forEach(function(patch) {
172
+ var stops = patch.querySelectorAll('stop');
173
+ if (stops.length >= 4) {
174
+ patches.push({
175
+ colors: [
176
+ parseColor(stops[0].getAttribute('stop-color') || stops[0].style.stopColor),
177
+ parseColor(stops[1].getAttribute('stop-color') || stops[1].style.stopColor),
178
+ parseColor(stops[2].getAttribute('stop-color') || stops[2].style.stopColor),
179
+ parseColor(stops[3].getAttribute('stop-color') || stops[3].style.stopColor)
180
+ ]
181
+ });
182
+ }
183
+ });
184
+ });
185
+
186
+ // Render patches with bilinear interpolation
187
+ if (patches.length > 0) {
188
+ var imgData = ctx.createImageData(size, size);
189
+ var data = imgData.data;
190
+ var patch = patches[0];
191
+ for (var y = 0; y < size; y++) {
192
+ for (var x = 0; x < size; x++) {
193
+ var u = x / (size - 1);
194
+ var v = y / (size - 1);
195
+ var c = bilinearColor(patch.colors[0], patch.colors[1], patch.colors[2], patch.colors[3], u, v);
196
+ var i = (y * size + x) * 4;
197
+ data[i] = c.r;
198
+ data[i+1] = c.g;
199
+ data[i+2] = c.b;
200
+ data[i+3] = c.a;
201
+ }
202
+ }
203
+ ctx.putImageData(imgData, 0, 0);
204
+ }
205
+
206
+ // Create pattern from canvas
207
+ var dataUrl = canvas.toDataURL('image/png');
208
+ var pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
209
+ pattern.setAttribute('id', id + '_polyfill');
210
+ pattern.setAttribute('patternUnits', 'objectBoundingBox');
211
+ pattern.setAttribute('width', '1');
212
+ pattern.setAttribute('height', '1');
213
+
214
+ var img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
215
+ img.setAttribute('href', dataUrl);
216
+ img.setAttribute('width', '1');
217
+ img.setAttribute('height', '1');
218
+ img.setAttribute('preserveAspectRatio', 'none');
219
+ pattern.appendChild(img);
220
+
221
+ // Add pattern to defs
222
+ var defs = mesh.closest('svg').querySelector('defs') || (function() {
223
+ var d = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
224
+ mesh.closest('svg').insertBefore(d, mesh.closest('svg').firstChild);
225
+ return d;
226
+ })();
227
+ defs.appendChild(pattern);
228
+
229
+ // Update references to use polyfill pattern
230
+ refs.forEach(function(ref) {
231
+ if (ref.getAttribute('fill') === 'url(#' + id + ')') {
232
+ ref.setAttribute('fill', 'url(#' + id + '_polyfill)');
233
+ }
234
+ if (ref.getAttribute('stroke') === 'url(#' + id + ')') {
235
+ ref.setAttribute('stroke', 'url(#' + id + '_polyfill)');
236
+ }
237
+ });
238
+ });
239
+ })();
240
+ `;
241
+ }
242
+
243
+ /**
244
+ * Generate the hatch pattern polyfill code.
245
+ * Converts SVG 2 hatch elements to SVG 1.1 pattern elements.
246
+ *
247
+ * @returns {string} JavaScript polyfill code
248
+ */
249
+ function generateHatchPolyfillCode() {
250
+ return `
251
+ // Hatch Pattern Polyfill - SVG 2.0 to SVG 1.1 pattern conversion
252
+ // Generated by svg-matrix
253
+ (function() {
254
+ 'use strict';
255
+
256
+ // Find all hatch elements
257
+ var hatches = document.querySelectorAll('hatch');
258
+ if (!hatches.length) return;
259
+
260
+ hatches.forEach(function(hatch) {
261
+ var id = hatch.getAttribute('id');
262
+ if (!id) return;
263
+
264
+ // Get hatch properties
265
+ var href = hatch.getAttribute('href') || hatch.getAttribute('xlink:href');
266
+ var hatchUnits = hatch.getAttribute('hatchUnits') || 'objectBoundingBox';
267
+ var hatchContentUnits = hatch.getAttribute('hatchContentUnits') || 'userSpaceOnUse';
268
+ var pitch = parseFloat(hatch.getAttribute('pitch')) || 8;
269
+ var rotate = parseFloat(hatch.getAttribute('rotate')) || 0;
270
+ var transform = hatch.getAttribute('transform') || '';
271
+
272
+ // Create equivalent pattern
273
+ var pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
274
+ pattern.setAttribute('id', id + '_polyfill');
275
+ pattern.setAttribute('patternUnits', hatchUnits);
276
+ pattern.setAttribute('width', pitch);
277
+ pattern.setAttribute('height', pitch);
278
+
279
+ if (transform || rotate) {
280
+ var fullTransform = transform;
281
+ if (rotate) fullTransform += ' rotate(' + rotate + ')';
282
+ pattern.setAttribute('patternTransform', fullTransform.trim());
283
+ }
284
+
285
+ // Copy hatchpath children as lines
286
+ var hatchpaths = hatch.querySelectorAll('hatchpath, hatchPath');
287
+ hatchpaths.forEach(function(hp) {
288
+ var d = hp.getAttribute('d');
289
+ var strokeColor = hp.getAttribute('stroke') || 'black';
290
+ var strokeWidth = hp.getAttribute('stroke-width') || '1';
291
+
292
+ var line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
293
+ line.setAttribute('d', d || 'M0,0 L' + pitch + ',0');
294
+ line.setAttribute('stroke', strokeColor);
295
+ line.setAttribute('stroke-width', strokeWidth);
296
+ line.setAttribute('fill', 'none');
297
+ pattern.appendChild(line);
298
+ });
299
+
300
+ // If no hatchpaths, create default diagonal line
301
+ if (!hatchpaths.length) {
302
+ var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
303
+ line.setAttribute('x1', '0');
304
+ line.setAttribute('y1', '0');
305
+ line.setAttribute('x2', pitch);
306
+ line.setAttribute('y2', pitch);
307
+ line.setAttribute('stroke', 'black');
308
+ line.setAttribute('stroke-width', '1');
309
+ pattern.appendChild(line);
310
+ }
311
+
312
+ // Add pattern to defs
313
+ var defs = hatch.closest('svg').querySelector('defs') || (function() {
314
+ var d = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
315
+ hatch.closest('svg').insertBefore(d, hatch.closest('svg').firstChild);
316
+ return d;
317
+ })();
318
+ defs.appendChild(pattern);
319
+
320
+ // Update references
321
+ var refs = document.querySelectorAll('[fill="url(#' + id + ')"], [stroke="url(#' + id + ')"]');
322
+ refs.forEach(function(ref) {
323
+ if (ref.getAttribute('fill') === 'url(#' + id + ')') {
324
+ ref.setAttribute('fill', 'url(#' + id + '_polyfill)');
325
+ }
326
+ if (ref.getAttribute('stroke') === 'url(#' + id + ')') {
327
+ ref.setAttribute('stroke', 'url(#' + id + '_polyfill)');
328
+ }
329
+ });
330
+ });
331
+ })();
332
+ `;
333
+ }
334
+
335
+ /**
336
+ * Generate complete polyfill script based on detected features.
337
+ *
338
+ * @param {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} features - Detected features
339
+ * @returns {string|null} Complete polyfill script or null if none needed
340
+ */
341
+ export function generatePolyfillScript(features) {
342
+ const parts = [];
343
+
344
+ parts.push('/* SVG 2.0 Polyfills - Generated by svg-matrix */');
345
+
346
+ if (features.meshGradients.length > 0) {
347
+ parts.push(generateMeshPolyfillCode());
348
+ }
349
+
350
+ if (features.hatches.length > 0) {
351
+ parts.push(generateHatchPolyfillCode());
352
+ }
353
+
354
+ if (parts.length === 1) {
355
+ return null; // Only header, no actual polyfills
356
+ }
357
+
358
+ return parts.join('\n');
359
+ }
360
+
361
+ /**
362
+ * Inject polyfill script into SVG document.
363
+ *
364
+ * @param {Object} doc - Parsed SVG document
365
+ * @param {Object} [options] - Options
366
+ * @param {boolean} [options.force=false] - Force injection even if no features detected
367
+ * @returns {Object} The document (modified in place)
368
+ */
369
+ export function injectPolyfills(doc, options = {}) {
370
+ const features = detectSVG2Features(doc);
371
+
372
+ // Check if polyfills are needed
373
+ if (!options.force &&
374
+ features.meshGradients.length === 0 &&
375
+ features.hatches.length === 0) {
376
+ return doc;
377
+ }
378
+
379
+ const script = generatePolyfillScript(features);
380
+ if (!script) return doc;
381
+
382
+ // Find or create the SVG root
383
+ const svg = doc.documentElement || doc;
384
+
385
+ // Create script element
386
+ // Note: We need to handle this based on the parser being used
387
+ const scriptEl = {
388
+ tagName: 'script',
389
+ attributes: new Map([['type', 'text/javascript']]),
390
+ children: [],
391
+ textContent: script,
392
+ getAttribute(name) { return this.attributes.get(name); },
393
+ setAttribute(name, value) { this.attributes.set(name, value); },
394
+ hasAttribute(name) { return this.attributes.has(name); },
395
+ removeAttribute(name) { this.attributes.delete(name); },
396
+ getAttributeNames() { return [...this.attributes.keys()]; },
397
+ appendChild(child) { this.children.push(child); },
398
+ removeChild(child) {
399
+ const idx = this.children.indexOf(child);
400
+ if (idx >= 0) this.children.splice(idx, 1);
401
+ }
402
+ };
403
+
404
+ // Insert script at beginning of SVG (after any xml declaration)
405
+ if (svg.children && svg.children.length > 0) {
406
+ svg.children.unshift(scriptEl);
407
+ } else if (svg.children) {
408
+ svg.children.push(scriptEl);
409
+ }
410
+
411
+ return doc;
412
+ }
413
+
414
+ /**
415
+ * Remove polyfill scripts from SVG document.
416
+ *
417
+ * @param {Object} doc - Parsed SVG document
418
+ * @returns {Object} The document (modified in place)
419
+ */
420
+ export function removePolyfills(doc) {
421
+ const walk = (el) => {
422
+ if (!el || !el.children) return;
423
+
424
+ // Remove script elements that are svg-matrix polyfills
425
+ el.children = el.children.filter(child => {
426
+ if (child.tagName === 'script') {
427
+ const content = child.textContent || '';
428
+ if (content.includes('SVG 2.0 Polyfill') ||
429
+ content.includes('Generated by svg-matrix')) {
430
+ return false;
431
+ }
432
+ }
433
+ return true;
434
+ });
435
+
436
+ // Recurse
437
+ for (const child of el.children) {
438
+ walk(child);
439
+ }
440
+ };
441
+
442
+ walk(doc);
443
+ return doc;
444
+ }