@emasoft/svg-matrix 1.0.4 → 1.0.6
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 +341 -304
- package/package.json +19 -1
- package/src/browser-verify.js +463 -0
- package/src/clip-path-resolver.js +759 -0
- package/src/geometry-to-path.js +348 -0
- package/src/index.js +413 -6
- package/src/marker-resolver.js +1006 -0
- package/src/mask-resolver.js +1407 -0
- package/src/mesh-gradient.js +1215 -0
- package/src/pattern-resolver.js +844 -0
- package/src/polygon-clip.js +1491 -0
- package/src/svg-flatten.js +1615 -76
- package/src/text-to-path.js +820 -0
- package/src/transforms2d.js +493 -37
- package/src/transforms3d.js +418 -47
- package/src/use-symbol-resolver.js +1126 -0
- package/samples/test.svg +0 -39
|
@@ -0,0 +1,1215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh Gradient Module - SVG 2.0 meshGradient support with Decimal.js precision
|
|
3
|
+
*
|
|
4
|
+
* Implements parsing, evaluation, and rasterization of Coons patch mesh gradients.
|
|
5
|
+
* Supports both bilinear and bicubic (Coons) interpolation types.
|
|
6
|
+
*
|
|
7
|
+
* @module mesh-gradient
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Decimal from 'decimal.js';
|
|
11
|
+
import { Matrix } from './matrix.js';
|
|
12
|
+
import * as Transforms2D from './transforms2d.js';
|
|
13
|
+
import * as PolygonClip from './polygon-clip.js';
|
|
14
|
+
|
|
15
|
+
Decimal.set({ precision: 80 });
|
|
16
|
+
|
|
17
|
+
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
18
|
+
|
|
19
|
+
// Default samples per patch edge for rasterization
|
|
20
|
+
const DEFAULT_PATCH_SAMPLES = 16;
|
|
21
|
+
|
|
22
|
+
// Subdivision threshold for adaptive rendering
|
|
23
|
+
const SUBDIVISION_THRESHOLD = 2;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a point with Decimal coordinates.
|
|
27
|
+
*
|
|
28
|
+
* Points are the fundamental building blocks for Coons patches and Bezier curves,
|
|
29
|
+
* using arbitrary-precision Decimal values for accurate geometric calculations.
|
|
30
|
+
*
|
|
31
|
+
* @param {number|string|Decimal} x - The x-coordinate (converted to Decimal)
|
|
32
|
+
* @param {number|string|Decimal} y - The y-coordinate (converted to Decimal)
|
|
33
|
+
* @returns {{x: Decimal, y: Decimal}} Point object with Decimal coordinates
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Create a point at (10.5, 20.75)
|
|
37
|
+
* const p = point(10.5, 20.75);
|
|
38
|
+
* console.log(p.x.toString()); // "10.5"
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Using high-precision strings
|
|
42
|
+
* const p = point("3.14159265358979323846", "2.71828182845904523536");
|
|
43
|
+
*/
|
|
44
|
+
export function point(x, y) {
|
|
45
|
+
return { x: D(x), y: D(y) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create an RGBA color object with values in the range 0-255.
|
|
50
|
+
*
|
|
51
|
+
* All components are rounded to integers. This represents colors in sRGB color space,
|
|
52
|
+
* which is the standard for SVG and web graphics.
|
|
53
|
+
*
|
|
54
|
+
* @param {number} r - Red component (0-255)
|
|
55
|
+
* @param {number} g - Green component (0-255)
|
|
56
|
+
* @param {number} b - Blue component (0-255)
|
|
57
|
+
* @param {number} [a=255] - Alpha component (0-255, default is fully opaque)
|
|
58
|
+
* @returns {{r: number, g: number, b: number, a: number}} Color object with integer RGBA values
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Create opaque red
|
|
62
|
+
* const red = color(255, 0, 0);
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // Create semi-transparent blue
|
|
66
|
+
* const blue = color(0, 0, 255, 128);
|
|
67
|
+
*/
|
|
68
|
+
export function color(r, g, b, a = 255) {
|
|
69
|
+
return { r: Math.round(r), g: Math.round(g), b: Math.round(b), a: Math.round(a) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse a CSS color string into an RGBA color object.
|
|
74
|
+
*
|
|
75
|
+
* Supports multiple CSS color formats:
|
|
76
|
+
* - rgb(r, g, b) and rgba(r, g, b, a)
|
|
77
|
+
* - Hex colors: #RGB, #RRGGBB, #RRGGBBAA
|
|
78
|
+
* - Named colors: black, white, red, green, blue, yellow, cyan, magenta, transparent
|
|
79
|
+
*
|
|
80
|
+
* The opacity parameter multiplies the alpha channel, useful for applying
|
|
81
|
+
* SVG stop-opacity attributes.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} colorStr - CSS color string (rgb(), rgba(), hex, or named color)
|
|
84
|
+
* @param {number} [opacity=1] - Optional opacity multiplier (0-1)
|
|
85
|
+
* @returns {{r: number, g: number, b: number, a: number}} RGBA color with values 0-255
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Parse hex color
|
|
89
|
+
* const c1 = parseColor("#ff0000");
|
|
90
|
+
* // {r: 255, g: 0, b: 0, a: 255}
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Parse rgba with opacity modifier
|
|
94
|
+
* const c2 = parseColor("rgba(128, 128, 128, 0.5)", 0.8);
|
|
95
|
+
* // Alpha = 0.5 * 0.8 * 255 = 102
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Parse named color
|
|
99
|
+
* const c3 = parseColor("cyan");
|
|
100
|
+
* // {r: 0, g: 255, b: 255, a: 255}
|
|
101
|
+
*/
|
|
102
|
+
export function parseColor(colorStr, opacity = 1) {
|
|
103
|
+
if (!colorStr) return color(0, 0, 0, 255);
|
|
104
|
+
|
|
105
|
+
// Handle rgb() and rgba()
|
|
106
|
+
const rgbMatch = colorStr.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/i);
|
|
107
|
+
if (rgbMatch) {
|
|
108
|
+
const a = rgbMatch[4] !== undefined ? parseFloat(rgbMatch[4]) * 255 : 255;
|
|
109
|
+
return color(
|
|
110
|
+
parseInt(rgbMatch[1]),
|
|
111
|
+
parseInt(rgbMatch[2]),
|
|
112
|
+
parseInt(rgbMatch[3]),
|
|
113
|
+
a * opacity
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle hex colors
|
|
118
|
+
const hexMatch = colorStr.match(/^#([0-9a-f]{3,8})$/i);
|
|
119
|
+
if (hexMatch) {
|
|
120
|
+
const hex = hexMatch[1];
|
|
121
|
+
if (hex.length === 3) {
|
|
122
|
+
return color(
|
|
123
|
+
parseInt(hex[0] + hex[0], 16),
|
|
124
|
+
parseInt(hex[1] + hex[1], 16),
|
|
125
|
+
parseInt(hex[2] + hex[2], 16),
|
|
126
|
+
255 * opacity
|
|
127
|
+
);
|
|
128
|
+
} else if (hex.length === 6) {
|
|
129
|
+
return color(
|
|
130
|
+
parseInt(hex.slice(0, 2), 16),
|
|
131
|
+
parseInt(hex.slice(2, 4), 16),
|
|
132
|
+
parseInt(hex.slice(4, 6), 16),
|
|
133
|
+
255 * opacity
|
|
134
|
+
);
|
|
135
|
+
} else if (hex.length === 8) {
|
|
136
|
+
return color(
|
|
137
|
+
parseInt(hex.slice(0, 2), 16),
|
|
138
|
+
parseInt(hex.slice(2, 4), 16),
|
|
139
|
+
parseInt(hex.slice(4, 6), 16),
|
|
140
|
+
parseInt(hex.slice(6, 8), 16) * opacity
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Named colors (subset for common ones)
|
|
146
|
+
const namedColors = {
|
|
147
|
+
black: [0, 0, 0], white: [255, 255, 255], red: [255, 0, 0],
|
|
148
|
+
green: [0, 128, 0], blue: [0, 0, 255], yellow: [255, 255, 0],
|
|
149
|
+
cyan: [0, 255, 255], magenta: [255, 0, 255], transparent: [0, 0, 0, 0]
|
|
150
|
+
};
|
|
151
|
+
const named = namedColors[colorStr.toLowerCase()];
|
|
152
|
+
if (named) {
|
|
153
|
+
return color(named[0], named[1], named[2], (named[3] ?? 255) * opacity);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return color(0, 0, 0, 255 * opacity);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Linearly interpolate between two colors.
|
|
161
|
+
*
|
|
162
|
+
* Performs component-wise linear interpolation in sRGB color space:
|
|
163
|
+
* result = c1 * (1 - t) + c2 * t
|
|
164
|
+
*
|
|
165
|
+
* For physically accurate color interpolation, colors should ideally be
|
|
166
|
+
* converted to linear RGB space first, but this implementation uses
|
|
167
|
+
* sRGB directly for simplicity and compatibility with SVG standards.
|
|
168
|
+
*
|
|
169
|
+
* @param {{r: number, g: number, b: number, a: number}} c1 - First color (at t=0)
|
|
170
|
+
* @param {{r: number, g: number, b: number, a: number}} c2 - Second color (at t=1)
|
|
171
|
+
* @param {number|Decimal} t - Interpolation factor (0 = c1, 1 = c2)
|
|
172
|
+
* @returns {{r: number, g: number, b: number, a: number}} Interpolated color
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* // Interpolate from red to blue
|
|
176
|
+
* const red = color(255, 0, 0);
|
|
177
|
+
* const blue = color(0, 0, 255);
|
|
178
|
+
* const purple = lerpColor(red, blue, 0.5);
|
|
179
|
+
* // {r: 128, g: 0, b: 128, a: 255}
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* // Fade from opaque to transparent
|
|
183
|
+
* const opaque = color(100, 150, 200, 255);
|
|
184
|
+
* const trans = color(100, 150, 200, 0);
|
|
185
|
+
* const semi = lerpColor(opaque, trans, 0.75);
|
|
186
|
+
* // {r: 100, g: 150, b: 200, a: 64}
|
|
187
|
+
*/
|
|
188
|
+
export function lerpColor(c1, c2, t) {
|
|
189
|
+
const tNum = Number(t);
|
|
190
|
+
const mt = 1 - tNum;
|
|
191
|
+
return color(
|
|
192
|
+
c1.r * mt + c2.r * tNum,
|
|
193
|
+
c1.g * mt + c2.g * tNum,
|
|
194
|
+
c1.b * mt + c2.b * tNum,
|
|
195
|
+
c1.a * mt + c2.a * tNum
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Bilinear color interpolation for a quadrilateral.
|
|
201
|
+
*
|
|
202
|
+
* Interpolates color across a quad defined by four corner colors using
|
|
203
|
+
* bilinear interpolation in parameter space (u, v):
|
|
204
|
+
*
|
|
205
|
+
* color(u,v) = (1-u)(1-v)·c00 + u(1-v)·c10 + (1-u)v·c01 + uv·c11
|
|
206
|
+
*
|
|
207
|
+
* This ensures smooth color transitions across the quad, with the color
|
|
208
|
+
* at each corner matching the specified corner color exactly.
|
|
209
|
+
*
|
|
210
|
+
* @param {{r: number, g: number, b: number, a: number}} c00 - Color at corner (u=0, v=0)
|
|
211
|
+
* @param {{r: number, g: number, b: number, a: number}} c10 - Color at corner (u=1, v=0)
|
|
212
|
+
* @param {{r: number, g: number, b: number, a: number}} c01 - Color at corner (u=0, v=1)
|
|
213
|
+
* @param {{r: number, g: number, b: number, a: number}} c11 - Color at corner (u=1, v=1)
|
|
214
|
+
* @param {number|Decimal} u - U parameter (0 to 1)
|
|
215
|
+
* @param {number|Decimal} v - V parameter (0 to 1)
|
|
216
|
+
* @returns {{r: number, g: number, b: number, a: number}} Interpolated color
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* // Interpolate in center of quad
|
|
220
|
+
* const red = color(255, 0, 0);
|
|
221
|
+
* const green = color(0, 255, 0);
|
|
222
|
+
* const blue = color(0, 0, 255);
|
|
223
|
+
* const yellow = color(255, 255, 0);
|
|
224
|
+
* const center = bilinearColor(red, green, blue, yellow, 0.5, 0.5);
|
|
225
|
+
* // Average of all four corners
|
|
226
|
+
*/
|
|
227
|
+
export function bilinearColor(c00, c10, c01, c11, u, v) {
|
|
228
|
+
const uNum = Number(u);
|
|
229
|
+
const vNum = Number(v);
|
|
230
|
+
const mu = 1 - uNum;
|
|
231
|
+
const mv = 1 - vNum;
|
|
232
|
+
|
|
233
|
+
return color(
|
|
234
|
+
mu * mv * c00.r + uNum * mv * c10.r + mu * vNum * c01.r + uNum * vNum * c11.r,
|
|
235
|
+
mu * mv * c00.g + uNum * mv * c10.g + mu * vNum * c01.g + uNum * vNum * c11.g,
|
|
236
|
+
mu * mv * c00.b + uNum * mv * c10.b + mu * vNum * c01.b + uNum * vNum * c11.b,
|
|
237
|
+
mu * mv * c00.a + uNum * mv * c10.a + mu * vNum * c01.a + uNum * vNum * c11.a
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Bezier Curve Evaluation
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Evaluate a cubic Bezier curve at parameter t using the Bernstein polynomial.
|
|
247
|
+
*
|
|
248
|
+
* The cubic Bezier curve is defined by the parametric equation:
|
|
249
|
+
*
|
|
250
|
+
* B(t) = (1-t)³·p0 + 3(1-t)²t·p1 + 3(1-t)t²·p2 + t³·p3
|
|
251
|
+
*
|
|
252
|
+
* where t ∈ [0,1]. This is the standard cubic Bezier formulation used in
|
|
253
|
+
* SVG path data and meshGradient definitions.
|
|
254
|
+
*
|
|
255
|
+
* @param {{x: Decimal, y: Decimal}} p0 - Start point (at t=0)
|
|
256
|
+
* @param {{x: Decimal, y: Decimal}} p1 - First control point
|
|
257
|
+
* @param {{x: Decimal, y: Decimal}} p2 - Second control point
|
|
258
|
+
* @param {{x: Decimal, y: Decimal}} p3 - End point (at t=1)
|
|
259
|
+
* @param {Decimal} t - Parameter value (0 to 1)
|
|
260
|
+
* @returns {{x: Decimal, y: Decimal}} Point on the curve at parameter t
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* // Evaluate curve at midpoint
|
|
264
|
+
* const p0 = point(0, 0);
|
|
265
|
+
* const p1 = point(50, 100);
|
|
266
|
+
* const p2 = point(150, 100);
|
|
267
|
+
* const p3 = point(200, 0);
|
|
268
|
+
* const mid = evalCubicBezier(p0, p1, p2, p3, D(0.5));
|
|
269
|
+
* // Returns point on smooth S-curve
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* // Sample curve at multiple points
|
|
273
|
+
* for (let i = 0; i <= 10; i++) {
|
|
274
|
+
* const t = D(i).div(10);
|
|
275
|
+
* const pt = evalCubicBezier(p0, p1, p2, p3, t);
|
|
276
|
+
* console.log(`t=${i/10}: (${pt.x}, ${pt.y})`);
|
|
277
|
+
* }
|
|
278
|
+
*/
|
|
279
|
+
export function evalCubicBezier(p0, p1, p2, p3, t) {
|
|
280
|
+
const mt = D(1).minus(t);
|
|
281
|
+
const mt2 = mt.mul(mt);
|
|
282
|
+
const mt3 = mt2.mul(mt);
|
|
283
|
+
const t2 = t.mul(t);
|
|
284
|
+
const t3 = t2.mul(t);
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
x: mt3.mul(p0.x).plus(D(3).mul(mt2).mul(t).mul(p1.x))
|
|
288
|
+
.plus(D(3).mul(mt).mul(t2).mul(p2.x)).plus(t3.mul(p3.x)),
|
|
289
|
+
y: mt3.mul(p0.y).plus(D(3).mul(mt2).mul(t).mul(p1.y))
|
|
290
|
+
.plus(D(3).mul(mt).mul(t2).mul(p2.y)).plus(t3.mul(p3.y))
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Split a cubic Bezier curve at t=0.5 using De Casteljau's algorithm.
|
|
296
|
+
*
|
|
297
|
+
* De Casteljau's algorithm recursively subdivides the curve by computing
|
|
298
|
+
* midpoints at each level:
|
|
299
|
+
*
|
|
300
|
+
* Level 0: p0, p1, p2, p3 (original control points)
|
|
301
|
+
* Level 1: q0 = mid(p0,p1), q1 = mid(p1,p2), q2 = mid(p2,p3)
|
|
302
|
+
* Level 2: r0 = mid(q0,q1), r1 = mid(q1,q2)
|
|
303
|
+
* Level 3: s = mid(r0,r1) (point on curve at t=0.5)
|
|
304
|
+
*
|
|
305
|
+
* The two resulting curves share the point s and maintain C² continuity.
|
|
306
|
+
*
|
|
307
|
+
* @param {Array<{x: Decimal, y: Decimal}>} curve - Array of 4 control points [p0, p1, p2, p3]
|
|
308
|
+
* @returns {Array<Array<{x: Decimal, y: Decimal}>>} Two curves: [[q0,q1,q2,q3], [r0,r1,r2,r3]]
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* // Split a curve for adaptive subdivision
|
|
312
|
+
* const curve = [
|
|
313
|
+
* point(0, 0),
|
|
314
|
+
* point(100, 200),
|
|
315
|
+
* point(200, 200),
|
|
316
|
+
* point(300, 0)
|
|
317
|
+
* ];
|
|
318
|
+
* const [left, right] = splitBezier(curve);
|
|
319
|
+
* // left and right are two Bezier curves that together equal the original
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* // Recursive subdivision for adaptive sampling
|
|
323
|
+
* function subdivideCurve(curve, maxDepth) {
|
|
324
|
+
* if (maxDepth === 0) return [curve];
|
|
325
|
+
* const [left, right] = splitBezier(curve);
|
|
326
|
+
* return [...subdivideCurve(left, maxDepth-1), ...subdivideCurve(right, maxDepth-1)];
|
|
327
|
+
* }
|
|
328
|
+
*/
|
|
329
|
+
export function splitBezier(curve) {
|
|
330
|
+
const [p0, p1, p2, p3] = curve;
|
|
331
|
+
|
|
332
|
+
// De Casteljau subdivision at t=0.5
|
|
333
|
+
const mid = (a, b) => ({
|
|
334
|
+
x: a.x.plus(b.x).div(2),
|
|
335
|
+
y: a.y.plus(b.y).div(2)
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const q0 = p0;
|
|
339
|
+
const q1 = mid(p0, p1);
|
|
340
|
+
const q2 = mid(mid(p0, p1), mid(p1, p2));
|
|
341
|
+
const q3 = mid(mid(mid(p0, p1), mid(p1, p2)), mid(mid(p1, p2), mid(p2, p3)));
|
|
342
|
+
|
|
343
|
+
const r0 = q3;
|
|
344
|
+
const r1 = mid(mid(p1, p2), mid(p2, p3));
|
|
345
|
+
const r2 = mid(p2, p3);
|
|
346
|
+
const r3 = p3;
|
|
347
|
+
|
|
348
|
+
return [[q0, q1, q2, q3], [r0, r1, r2, r3]];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ============================================================================
|
|
352
|
+
// Coons Patch Evaluation
|
|
353
|
+
// ============================================================================
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* A Coons patch representing a bicubic surface defined by four boundary curves.
|
|
357
|
+
*
|
|
358
|
+
* Coons patches are used in SVG 2.0 meshGradient elements to create smooth
|
|
359
|
+
* color gradients across curved surfaces. Each patch is bounded by four
|
|
360
|
+
* cubic Bezier curves and interpolates both geometry and color.
|
|
361
|
+
*
|
|
362
|
+
* The Coons patch formula creates a bicubic surface S(u,v) that interpolates
|
|
363
|
+
* the four boundary curves using bilinearly blended interpolation:
|
|
364
|
+
*
|
|
365
|
+
* S(u,v) = Lc(u,v) + Ld(u,v) - B(u,v)
|
|
366
|
+
*
|
|
367
|
+
* where:
|
|
368
|
+
* - Lc(u,v) = (1-v)·C(u) + v·D(u) (ruled surface in u direction)
|
|
369
|
+
* - Ld(u,v) = (1-u)·A(v) + u·B(v) (ruled surface in v direction)
|
|
370
|
+
* - B(u,v) = bilinear interpolation of corner points
|
|
371
|
+
* - C(u), D(u), A(v), B(v) are the four boundary curves
|
|
372
|
+
*
|
|
373
|
+
* @class
|
|
374
|
+
*/
|
|
375
|
+
export class CoonsPatch {
|
|
376
|
+
/**
|
|
377
|
+
* Create a Coons patch from four boundary curves and corner colors.
|
|
378
|
+
*
|
|
379
|
+
* The boundary curves must form a closed loop:
|
|
380
|
+
* - top[3] should equal right[0] (top-right corner)
|
|
381
|
+
* - right[3] should equal bottom[3] (bottom-right corner)
|
|
382
|
+
* - bottom[0] should equal left[3] (bottom-left corner)
|
|
383
|
+
* - left[0] should equal top[0] (top-left corner)
|
|
384
|
+
*
|
|
385
|
+
* @param {Array<{x: Decimal, y: Decimal}>} top - Top boundary curve [p0, p1, p2, p3], left to right
|
|
386
|
+
* @param {Array<{x: Decimal, y: Decimal}>} right - Right boundary curve [p0, p1, p2, p3], top to bottom
|
|
387
|
+
* @param {Array<{x: Decimal, y: Decimal}>} bottom - Bottom boundary curve [p0, p1, p2, p3], left to right
|
|
388
|
+
* @param {Array<{x: Decimal, y: Decimal}>} left - Left boundary curve [p0, p1, p2, p3], top to bottom
|
|
389
|
+
* @param {Array<Array<{r: number, g: number, b: number, a: number}>>} colors - 2x2 array of corner colors [[c00, c10], [c01, c11]]
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* // Create a simple rectangular patch
|
|
393
|
+
* const patch = new CoonsPatch(
|
|
394
|
+
* [point(0,0), point(33,0), point(67,0), point(100,0)], // top
|
|
395
|
+
* [point(100,0), point(100,33), point(100,67), point(100,100)], // right
|
|
396
|
+
* [point(0,100), point(33,100), point(67,100), point(100,100)], // bottom
|
|
397
|
+
* [point(0,0), point(0,33), point(0,67), point(0,100)], // left
|
|
398
|
+
* [[color(255,0,0), color(0,255,0)], // top-left red, top-right green
|
|
399
|
+
* [color(0,0,255), color(255,255,0)]] // bottom-left blue, bottom-right yellow
|
|
400
|
+
* );
|
|
401
|
+
*/
|
|
402
|
+
constructor(top, right, bottom, left, colors) {
|
|
403
|
+
this.top = top;
|
|
404
|
+
this.right = right;
|
|
405
|
+
this.bottom = bottom;
|
|
406
|
+
this.left = left;
|
|
407
|
+
this.colors = colors;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Evaluate the Coons patch at parametric coordinates (u, v).
|
|
412
|
+
*
|
|
413
|
+
* Computes both the geometric position and interpolated color at the
|
|
414
|
+
* given parameter values using the bilinearly blended Coons formula:
|
|
415
|
+
*
|
|
416
|
+
* Position:
|
|
417
|
+
* S(u,v) = Sc(u,v) + Sd(u,v) - B(u,v)
|
|
418
|
+
*
|
|
419
|
+
* where:
|
|
420
|
+
* - Sc(u,v) = (1-v)·top(u) + v·bottom(u) (interpolate between top/bottom edges)
|
|
421
|
+
* - Sd(u,v) = (1-u)·left(v) + u·right(v) (interpolate between left/right edges)
|
|
422
|
+
* - B(u,v) = (1-u)(1-v)·P00 + u(1-v)·P10 + (1-u)v·P01 + uv·P11 (bilinear blend of corners)
|
|
423
|
+
*
|
|
424
|
+
* The Coons formula ensures that S(u,v) exactly matches the boundary
|
|
425
|
+
* curves when u or v equals 0 or 1.
|
|
426
|
+
*
|
|
427
|
+
* Color is computed using bilinear interpolation of the four corner colors.
|
|
428
|
+
*
|
|
429
|
+
* @param {Decimal} u - U parameter (0 = left edge, 1 = right edge)
|
|
430
|
+
* @param {Decimal} v - V parameter (0 = top edge, 1 = bottom edge)
|
|
431
|
+
* @returns {{point: {x: Decimal, y: Decimal}, color: {r: number, g: number, b: number, a: number}}} Evaluated point and color
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* // Evaluate at center of patch
|
|
435
|
+
* const result = patch.evaluate(D(0.5), D(0.5));
|
|
436
|
+
* console.log(`Center point: (${result.point.x}, ${result.point.y})`);
|
|
437
|
+
* console.log(`Center color: rgb(${result.color.r}, ${result.color.g}, ${result.color.b})`);
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* // Sample a grid of points
|
|
441
|
+
* for (let i = 0; i <= 10; i++) {
|
|
442
|
+
* for (let j = 0; j <= 10; j++) {
|
|
443
|
+
* const u = D(i).div(10);
|
|
444
|
+
* const v = D(j).div(10);
|
|
445
|
+
* const {point, color} = patch.evaluate(u, v);
|
|
446
|
+
* // Use point and color for rendering
|
|
447
|
+
* }
|
|
448
|
+
* }
|
|
449
|
+
*/
|
|
450
|
+
evaluate(u, v) {
|
|
451
|
+
// Boundary curves
|
|
452
|
+
const Lc = evalCubicBezier(...this.top, u); // L_c(u,0)
|
|
453
|
+
const Ld = evalCubicBezier(...this.bottom, u); // L_d(u,1)
|
|
454
|
+
const La = evalCubicBezier(...this.left, v); // L_a(0,v)
|
|
455
|
+
const Lb = evalCubicBezier(...this.right, v); // L_b(1,v)
|
|
456
|
+
|
|
457
|
+
// Corner points
|
|
458
|
+
const P00 = this.top[0];
|
|
459
|
+
const P10 = this.top[3];
|
|
460
|
+
const P01 = this.bottom[0];
|
|
461
|
+
const P11 = this.bottom[3];
|
|
462
|
+
|
|
463
|
+
// Coons patch formula: S(u,v) = Lc(u) + Ld(u) - B(u,v)
|
|
464
|
+
// where B is bilinear interpolation of corners
|
|
465
|
+
const mu = D(1).minus(u);
|
|
466
|
+
const mv = D(1).minus(v);
|
|
467
|
+
|
|
468
|
+
// Ruled surface in u direction
|
|
469
|
+
const Sc_x = mv.mul(Lc.x).plus(v.mul(Ld.x));
|
|
470
|
+
const Sc_y = mv.mul(Lc.y).plus(v.mul(Ld.y));
|
|
471
|
+
|
|
472
|
+
// Ruled surface in v direction
|
|
473
|
+
const Sd_x = mu.mul(La.x).plus(u.mul(Lb.x));
|
|
474
|
+
const Sd_y = mu.mul(La.y).plus(u.mul(Lb.y));
|
|
475
|
+
|
|
476
|
+
// Bilinear interpolation of corners
|
|
477
|
+
const B_x = mu.mul(mv).mul(P00.x)
|
|
478
|
+
.plus(u.mul(mv).mul(P10.x))
|
|
479
|
+
.plus(mu.mul(v).mul(P01.x))
|
|
480
|
+
.plus(u.mul(v).mul(P11.x));
|
|
481
|
+
const B_y = mu.mul(mv).mul(P00.y)
|
|
482
|
+
.plus(u.mul(mv).mul(P10.y))
|
|
483
|
+
.plus(mu.mul(v).mul(P01.y))
|
|
484
|
+
.plus(u.mul(v).mul(P11.y));
|
|
485
|
+
|
|
486
|
+
// Coons formula: Sc + Sd - B
|
|
487
|
+
const pt = {
|
|
488
|
+
x: Sc_x.plus(Sd_x).minus(B_x),
|
|
489
|
+
y: Sc_y.plus(Sd_y).minus(B_y)
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// Color interpolation (bilinear)
|
|
493
|
+
const col = bilinearColor(
|
|
494
|
+
this.colors[0][0], this.colors[0][1],
|
|
495
|
+
this.colors[1][0], this.colors[1][1],
|
|
496
|
+
u, v
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
return { point: pt, color: col };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Subdivide this patch into four sub-patches for adaptive rendering.
|
|
504
|
+
*
|
|
505
|
+
* Splits the patch at u=0.5 and v=0.5, creating four smaller patches that
|
|
506
|
+
* together exactly reproduce the original patch. This is used for adaptive
|
|
507
|
+
* subdivision when rendering curved patches with high accuracy.
|
|
508
|
+
*
|
|
509
|
+
* Each boundary curve is split using De Casteljau's algorithm. The center
|
|
510
|
+
* point and mid-edge points are computed by evaluating the patch at the
|
|
511
|
+
* subdivision parameters. Interior curves connecting these points are
|
|
512
|
+
* approximated as linear (degenerate Bezier curves) for simplicity.
|
|
513
|
+
*
|
|
514
|
+
* The subdivision maintains color continuity by evaluating colors at the
|
|
515
|
+
* subdivision points.
|
|
516
|
+
*
|
|
517
|
+
* @returns {Array<CoonsPatch>} Array of 4 sub-patches: [top-left, top-right, bottom-left, bottom-right]
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* // Adaptive subdivision until patches are flat enough
|
|
521
|
+
* function subdividePatch(patch, maxDepth = 5) {
|
|
522
|
+
* if (patch.isFlat() || maxDepth === 0) {
|
|
523
|
+
* return [patch];
|
|
524
|
+
* }
|
|
525
|
+
* const subPatches = patch.subdivide();
|
|
526
|
+
* return subPatches.flatMap(sp => subdividePatch(sp, maxDepth - 1));
|
|
527
|
+
* }
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* // Subdivide once for finer rendering
|
|
531
|
+
* const patch = new CoonsPatch(top, right, bottom, left, colors);
|
|
532
|
+
* const [tl, tr, bl, br] = patch.subdivide();
|
|
533
|
+
* // Render each sub-patch separately
|
|
534
|
+
*/
|
|
535
|
+
subdivide() {
|
|
536
|
+
// Split each boundary curve
|
|
537
|
+
const [topL, topR] = splitBezier(this.top);
|
|
538
|
+
const [rightT, rightB] = splitBezier(this.right);
|
|
539
|
+
const [bottomL, bottomR] = splitBezier(this.bottom);
|
|
540
|
+
const [leftT, leftB] = splitBezier(this.left);
|
|
541
|
+
|
|
542
|
+
// Compute center point and mid-edge points
|
|
543
|
+
const center = this.evaluate(D(0.5), D(0.5));
|
|
544
|
+
const midTop = this.evaluate(D(0.5), D(0));
|
|
545
|
+
const midBottom = this.evaluate(D(0.5), D(1));
|
|
546
|
+
const midLeft = this.evaluate(D(0), D(0.5));
|
|
547
|
+
const midRight = this.evaluate(D(1), D(0.5));
|
|
548
|
+
|
|
549
|
+
// Interior curves (linear for simplicity - could be improved)
|
|
550
|
+
const midH = [midLeft.point, midLeft.point, center.point, center.point];
|
|
551
|
+
const midH2 = [center.point, center.point, midRight.point, midRight.point];
|
|
552
|
+
const midV = [midTop.point, midTop.point, center.point, center.point];
|
|
553
|
+
const midV2 = [center.point, center.point, midBottom.point, midBottom.point];
|
|
554
|
+
|
|
555
|
+
// Colors at subdivided corners
|
|
556
|
+
const c00 = this.colors[0][0];
|
|
557
|
+
const c10 = this.colors[0][1];
|
|
558
|
+
const c01 = this.colors[1][0];
|
|
559
|
+
const c11 = this.colors[1][1];
|
|
560
|
+
const cMid = center.color;
|
|
561
|
+
const cTop = midTop.color;
|
|
562
|
+
const cBottom = midBottom.color;
|
|
563
|
+
const cLeft = midLeft.color;
|
|
564
|
+
const cRight = midRight.color;
|
|
565
|
+
|
|
566
|
+
return [
|
|
567
|
+
// Top-left
|
|
568
|
+
new CoonsPatch(topL, midV, midH, leftT, [[c00, cTop], [cLeft, cMid]]),
|
|
569
|
+
// Top-right
|
|
570
|
+
new CoonsPatch(topR, rightT, midH2, midV, [[cTop, c10], [cMid, cRight]]),
|
|
571
|
+
// Bottom-left
|
|
572
|
+
new CoonsPatch(midH, midV2, bottomL, leftB, [[cLeft, cMid], [c01, cBottom]]),
|
|
573
|
+
// Bottom-right
|
|
574
|
+
new CoonsPatch(midH2, rightB, bottomR, midV2, [[cMid, cRight], [cBottom, c11]])
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Check if this patch is flat enough for direct rendering without subdivision.
|
|
580
|
+
*
|
|
581
|
+
* A patch is considered flat when all four boundary curves are approximately
|
|
582
|
+
* linear, meaning the control points lie close to the straight line between
|
|
583
|
+
* the curve endpoints.
|
|
584
|
+
*
|
|
585
|
+
* Flatness is tested by computing the perpendicular distance from each
|
|
586
|
+
* control point to the line connecting the curve's start and end points.
|
|
587
|
+
* If these distances are below SUBDIVISION_THRESHOLD (typically 2 pixels),
|
|
588
|
+
* the curve is considered flat.
|
|
589
|
+
*
|
|
590
|
+
* This test is used to determine when adaptive subdivision can stop,
|
|
591
|
+
* allowing flat patches to be rendered directly as simple quads.
|
|
592
|
+
*
|
|
593
|
+
* @returns {boolean} true if all boundary curves are nearly linear
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* // Adaptive rendering with flatness test
|
|
597
|
+
* function renderPatch(patch) {
|
|
598
|
+
* if (patch.isFlat()) {
|
|
599
|
+
* // Render as simple quad
|
|
600
|
+
* renderQuad(patch);
|
|
601
|
+
* } else {
|
|
602
|
+
* // Subdivide further
|
|
603
|
+
* patch.subdivide().forEach(renderPatch);
|
|
604
|
+
* }
|
|
605
|
+
* }
|
|
606
|
+
*/
|
|
607
|
+
isFlat() {
|
|
608
|
+
// Check if all boundary curves are nearly linear
|
|
609
|
+
const curveFlat = (curve) => {
|
|
610
|
+
const [p0, p1, p2, p3] = curve;
|
|
611
|
+
// Distance from control points to line p0-p3
|
|
612
|
+
const dx = p3.x.minus(p0.x);
|
|
613
|
+
const dy = p3.y.minus(p0.y);
|
|
614
|
+
const len2 = dx.mul(dx).plus(dy.mul(dy));
|
|
615
|
+
if (len2.lt(1e-10)) return true;
|
|
616
|
+
|
|
617
|
+
const dist1 = dx.mul(p1.y.minus(p0.y)).minus(dy.mul(p1.x.minus(p0.x))).abs();
|
|
618
|
+
const dist2 = dx.mul(p2.y.minus(p0.y)).minus(dy.mul(p2.x.minus(p0.x))).abs();
|
|
619
|
+
|
|
620
|
+
return dist1.div(len2.sqrt()).lt(SUBDIVISION_THRESHOLD) &&
|
|
621
|
+
dist2.div(len2.sqrt()).lt(SUBDIVISION_THRESHOLD);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
return curveFlat(this.top) && curveFlat(this.right) &&
|
|
625
|
+
curveFlat(this.bottom) && curveFlat(this.left);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Get the axis-aligned bounding box of this patch.
|
|
630
|
+
*
|
|
631
|
+
* Computes the bounding box by examining all control points of the four
|
|
632
|
+
* boundary curves. Note that this may be a conservative estimate since
|
|
633
|
+
* the actual curve may not reach the control points. For tighter bounds,
|
|
634
|
+
* the curves' extrema would need to be computed.
|
|
635
|
+
*
|
|
636
|
+
* @returns {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} Bounding box with Decimal coordinates
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* // Get bounding box for clipping
|
|
640
|
+
* const bbox = patch.getBBox();
|
|
641
|
+
* console.log(`Patch bounds: (${bbox.minX}, ${bbox.minY}) to (${bbox.maxX}, ${bbox.maxY})`);
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* // Check if patch intersects viewport
|
|
645
|
+
* const bbox = patch.getBBox();
|
|
646
|
+
* const visible = bbox.maxX >= 0 && bbox.minX <= viewportWidth &&
|
|
647
|
+
* bbox.maxY >= 0 && bbox.minY <= viewportHeight;
|
|
648
|
+
*/
|
|
649
|
+
getBBox() {
|
|
650
|
+
const allPoints = [...this.top, ...this.right, ...this.bottom, ...this.left];
|
|
651
|
+
let minX = allPoints[0].x, maxX = allPoints[0].x;
|
|
652
|
+
let minY = allPoints[0].y, maxY = allPoints[0].y;
|
|
653
|
+
|
|
654
|
+
for (const p of allPoints) {
|
|
655
|
+
if (p.x.lt(minX)) minX = p.x;
|
|
656
|
+
if (p.x.gt(maxX)) maxX = p.x;
|
|
657
|
+
if (p.y.lt(minY)) minY = p.y;
|
|
658
|
+
if (p.y.gt(maxY)) maxY = p.y;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return { minX, minY, maxX, maxY };
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ============================================================================
|
|
666
|
+
// Mesh Gradient Parsing
|
|
667
|
+
// ============================================================================
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Parse a mesh gradient definition into an array of Coons patches.
|
|
671
|
+
*
|
|
672
|
+
* Converts an SVG 2.0 meshGradient structure (with meshrows, meshpatches, and stops)
|
|
673
|
+
* into a computational representation using CoonsPatch objects. Each meshpatch in
|
|
674
|
+
* the SVG becomes one CoonsPatch with four boundary curves and corner colors.
|
|
675
|
+
*
|
|
676
|
+
* The SVG meshGradient structure:
|
|
677
|
+
* ```xml
|
|
678
|
+
* <meshgradient x="0" y="0" type="bilinear">
|
|
679
|
+
* <meshrow>
|
|
680
|
+
* <meshpatch>
|
|
681
|
+
* <stop path="c 100,0 100,0 100,0" stop-color="red"/>
|
|
682
|
+
* <stop path="c 0,100 0,100 0,100" stop-color="green"/>
|
|
683
|
+
* <stop path="c -100,0 -100,0 -100,0" stop-color="blue"/>
|
|
684
|
+
* <stop path="c 0,-100 0,-100 0,-100" stop-color="yellow"/>
|
|
685
|
+
* </meshpatch>
|
|
686
|
+
* </meshrow>
|
|
687
|
+
* </meshgradient>
|
|
688
|
+
* ```
|
|
689
|
+
*
|
|
690
|
+
* @param {Object} meshGradientDef - Mesh gradient definition object with meshrows, type, etc.
|
|
691
|
+
* @param {number} [meshGradientDef.x=0] - X coordinate of gradient origin
|
|
692
|
+
* @param {number} [meshGradientDef.y=0] - Y coordinate of gradient origin
|
|
693
|
+
* @param {string} [meshGradientDef.type='bilinear'] - Interpolation type ('bilinear' or 'bicubic')
|
|
694
|
+
* @param {string} [meshGradientDef.gradientUnits='userSpaceOnUse'] - Coordinate system
|
|
695
|
+
* @param {string} [meshGradientDef.gradientTransform] - Transform matrix
|
|
696
|
+
* @param {Array} meshGradientDef.meshrows - Array of mesh rows
|
|
697
|
+
* @returns {{patches: Array<CoonsPatch>, type: string, gradientUnits: string, gradientTransform: string|null, x: number, y: number}} Parsed mesh data
|
|
698
|
+
*
|
|
699
|
+
* @example
|
|
700
|
+
* // Parse a simple mesh gradient
|
|
701
|
+
* const meshDef = {
|
|
702
|
+
* x: 0, y: 0,
|
|
703
|
+
* type: 'bilinear',
|
|
704
|
+
* meshrows: [
|
|
705
|
+
* {
|
|
706
|
+
* meshpatches: [
|
|
707
|
+
* {
|
|
708
|
+
* stops: [
|
|
709
|
+
* { path: 'c 100,0 100,0 100,0', color: 'red' },
|
|
710
|
+
* { path: 'c 0,100 0,100 0,100', color: 'green' },
|
|
711
|
+
* { path: 'c -100,0 -100,0 -100,0', color: 'blue' },
|
|
712
|
+
* { path: 'c 0,-100 0,-100 0,-100', color: 'yellow' }
|
|
713
|
+
* ]
|
|
714
|
+
* }
|
|
715
|
+
* ]
|
|
716
|
+
* }
|
|
717
|
+
* ]
|
|
718
|
+
* };
|
|
719
|
+
* const meshData = parseMeshGradient(meshDef);
|
|
720
|
+
* console.log(`Parsed ${meshData.patches.length} patches`);
|
|
721
|
+
*/
|
|
722
|
+
export function parseMeshGradient(meshGradientDef) {
|
|
723
|
+
const x = D(meshGradientDef.x || 0);
|
|
724
|
+
const y = D(meshGradientDef.y || 0);
|
|
725
|
+
const type = meshGradientDef.type || 'bilinear';
|
|
726
|
+
const gradientUnits = meshGradientDef.gradientUnits || 'userSpaceOnUse';
|
|
727
|
+
const gradientTransform = meshGradientDef.gradientTransform || null;
|
|
728
|
+
|
|
729
|
+
const patches = [];
|
|
730
|
+
const meshRows = meshGradientDef.meshrows || [];
|
|
731
|
+
|
|
732
|
+
// Build the mesh grid
|
|
733
|
+
// nodes[row][col] = point, colors[row][col] = color
|
|
734
|
+
const nodes = [[point(x, y)]];
|
|
735
|
+
const colors = [[]];
|
|
736
|
+
|
|
737
|
+
let currentX = x;
|
|
738
|
+
let currentY = y;
|
|
739
|
+
|
|
740
|
+
for (let rowIdx = 0; rowIdx < meshRows.length; rowIdx++) {
|
|
741
|
+
const row = meshRows[rowIdx];
|
|
742
|
+
const meshPatches = row.meshpatches || [];
|
|
743
|
+
|
|
744
|
+
if (rowIdx > 0) {
|
|
745
|
+
nodes.push([]);
|
|
746
|
+
colors.push([]);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
for (let colIdx = 0; colIdx < meshPatches.length; colIdx++) {
|
|
750
|
+
const patch = meshPatches[colIdx];
|
|
751
|
+
const stops = patch.stops || [];
|
|
752
|
+
|
|
753
|
+
// Each patch has up to 4 stops defining edges
|
|
754
|
+
for (let stopIdx = 0; stopIdx < stops.length; stopIdx++) {
|
|
755
|
+
const stop = stops[stopIdx];
|
|
756
|
+
const pathData = stop.path || '';
|
|
757
|
+
const stopColor = stop.color ? parseColor(stop.color, stop.opacity || 1) : null;
|
|
758
|
+
|
|
759
|
+
// Parse path command (c/C/l/L for bezier/line)
|
|
760
|
+
const pathMatch = pathData.match(/^\s*([cClL])\s*(.*)/);
|
|
761
|
+
if (pathMatch) {
|
|
762
|
+
const cmd = pathMatch[1];
|
|
763
|
+
const coords = pathMatch[2].trim().split(/[\s,]+/).map(Number);
|
|
764
|
+
|
|
765
|
+
if (cmd === 'c' || cmd === 'C') {
|
|
766
|
+
// Cubic bezier: c x1,y1 x2,y2 x3,y3 (relative)
|
|
767
|
+
// or C x1,y1 x2,y2 x3,y3 (absolute)
|
|
768
|
+
const isRelative = cmd === 'c';
|
|
769
|
+
// Store bezier control points for patch construction
|
|
770
|
+
} else if (cmd === 'l' || cmd === 'L') {
|
|
771
|
+
// Line: l dx,dy (relative) or L x,y (absolute)
|
|
772
|
+
const isRelative = cmd === 'l';
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Store color at corner
|
|
777
|
+
if (stopColor && (stopIdx === 0 || stopIdx === 3)) {
|
|
778
|
+
// Corner colors
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
patches,
|
|
786
|
+
type,
|
|
787
|
+
gradientUnits,
|
|
788
|
+
gradientTransform,
|
|
789
|
+
x: Number(x),
|
|
790
|
+
y: Number(y)
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Parse an SVG meshGradient DOM element into a mesh gradient definition object.
|
|
796
|
+
*
|
|
797
|
+
* Extracts the meshGradient structure from a DOM element, reading all attributes
|
|
798
|
+
* and child elements (meshrow, meshpatch, stop) to create a data structure
|
|
799
|
+
* suitable for parseMeshGradient().
|
|
800
|
+
*
|
|
801
|
+
* Handles:
|
|
802
|
+
* - Gradient attributes (x, y, type, gradientUnits, gradientTransform)
|
|
803
|
+
* - Nested meshrow/meshpatch/stop hierarchy
|
|
804
|
+
* - Stop colors from style attributes (stop-color, stop-opacity)
|
|
805
|
+
* - Path data for Bezier curves
|
|
806
|
+
*
|
|
807
|
+
* @param {Element} element - SVG <meshgradient> DOM element
|
|
808
|
+
* @returns {Object} Mesh gradient definition suitable for parseMeshGradient()
|
|
809
|
+
*
|
|
810
|
+
* @example
|
|
811
|
+
* // Parse from DOM
|
|
812
|
+
* const meshElement = document.querySelector('#myMeshGradient');
|
|
813
|
+
* const meshDef = parseMeshGradientElement(meshElement);
|
|
814
|
+
* const meshData = parseMeshGradient(meshDef);
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* // Parse and render
|
|
818
|
+
* const meshElement = document.getElementById('gradient1');
|
|
819
|
+
* const meshDef = parseMeshGradientElement(meshElement);
|
|
820
|
+
* const meshData = parseMeshGradient(meshDef);
|
|
821
|
+
* const imageData = rasterizeMeshGradient(meshData, 800, 600);
|
|
822
|
+
*/
|
|
823
|
+
export function parseMeshGradientElement(element) {
|
|
824
|
+
const data = {
|
|
825
|
+
x: element.getAttribute('x') || '0',
|
|
826
|
+
y: element.getAttribute('y') || '0',
|
|
827
|
+
type: element.getAttribute('type') || 'bilinear',
|
|
828
|
+
gradientUnits: element.getAttribute('gradientUnits') || 'userSpaceOnUse',
|
|
829
|
+
gradientTransform: element.getAttribute('gradientTransform'),
|
|
830
|
+
meshrows: []
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const meshRows = element.querySelectorAll('meshrow');
|
|
834
|
+
meshRows.forEach(row => {
|
|
835
|
+
const rowData = { meshpatches: [] };
|
|
836
|
+
const meshPatches = row.querySelectorAll('meshpatch');
|
|
837
|
+
|
|
838
|
+
meshPatches.forEach(patch => {
|
|
839
|
+
const patchData = { stops: [] };
|
|
840
|
+
const stops = patch.querySelectorAll('stop');
|
|
841
|
+
|
|
842
|
+
stops.forEach(stop => {
|
|
843
|
+
const style = stop.getAttribute('style') || '';
|
|
844
|
+
const colorMatch = style.match(/stop-color:\s*([^;]+)/);
|
|
845
|
+
const opacityMatch = style.match(/stop-opacity:\s*([^;]+)/);
|
|
846
|
+
|
|
847
|
+
patchData.stops.push({
|
|
848
|
+
path: stop.getAttribute('path') || '',
|
|
849
|
+
color: colorMatch ? colorMatch[1].trim() : null,
|
|
850
|
+
opacity: opacityMatch ? parseFloat(opacityMatch[1]) : 1
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
rowData.meshpatches.push(patchData);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
data.meshrows.push(rowData);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
return data;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ============================================================================
|
|
864
|
+
// Mesh Gradient Rasterization
|
|
865
|
+
// ============================================================================
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Rasterize a mesh gradient to a pixel buffer using adaptive subdivision.
|
|
869
|
+
*
|
|
870
|
+
* Converts a mesh gradient (array of CoonsPatch objects) into a raster image
|
|
871
|
+
* by adaptively subdividing each patch until it's flat enough to render
|
|
872
|
+
* directly. Uses scanline rendering with bilinear color interpolation and
|
|
873
|
+
* alpha blending.
|
|
874
|
+
*
|
|
875
|
+
* The rasterization process:
|
|
876
|
+
* 1. For each patch, check if it's flat using isFlat()
|
|
877
|
+
* 2. If flat, render directly as a colored quad
|
|
878
|
+
* 3. If curved, subdivide into 4 sub-patches and recurse
|
|
879
|
+
* 4. Apply alpha blending for overlapping patches
|
|
880
|
+
*
|
|
881
|
+
* @param {Object} meshData - Parsed mesh gradient data from parseMeshGradient()
|
|
882
|
+
* @param {Array<CoonsPatch>} meshData.patches - Array of Coons patches to render
|
|
883
|
+
* @param {number} width - Output image width in pixels
|
|
884
|
+
* @param {number} height - Output image height in pixels
|
|
885
|
+
* @param {Object} [options={}] - Rasterization options
|
|
886
|
+
* @param {number} [options.samples=16] - Initial samples per patch edge (for non-adaptive methods)
|
|
887
|
+
* @returns {{data: Uint8ClampedArray, width: number, height: number}} Image data compatible with Canvas ImageData
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* // Rasterize a mesh gradient to 800x600
|
|
891
|
+
* const meshData = parseMeshGradient(meshDef);
|
|
892
|
+
* const imageData = rasterizeMeshGradient(meshData, 800, 600);
|
|
893
|
+
* // Use with Canvas
|
|
894
|
+
* const canvas = document.createElement('canvas');
|
|
895
|
+
* canvas.width = 800;
|
|
896
|
+
* canvas.height = 600;
|
|
897
|
+
* const ctx = canvas.getContext('2d');
|
|
898
|
+
* ctx.putImageData(new ImageData(imageData.data, 800, 600), 0, 0);
|
|
899
|
+
*
|
|
900
|
+
* @example
|
|
901
|
+
* // High-quality rendering with more samples
|
|
902
|
+
* const imageData = rasterizeMeshGradient(meshData, 1920, 1080, { samples: 32 });
|
|
903
|
+
*/
|
|
904
|
+
export function rasterizeMeshGradient(meshData, width, height, options = {}) {
|
|
905
|
+
const { samples = DEFAULT_PATCH_SAMPLES } = options;
|
|
906
|
+
|
|
907
|
+
// Create image data buffer
|
|
908
|
+
const imageData = new Uint8ClampedArray(width * height * 4);
|
|
909
|
+
|
|
910
|
+
// For each patch, rasterize to the buffer
|
|
911
|
+
for (const patch of meshData.patches || []) {
|
|
912
|
+
rasterizePatch(patch, imageData, width, height, samples);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return { data: imageData, width, height };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Rasterize a single Coons patch using adaptive subdivision.
|
|
920
|
+
*
|
|
921
|
+
* Recursively subdivides the patch until each sub-patch is flat enough
|
|
922
|
+
* (determined by isFlat()), then renders it as a simple quad. This adaptive
|
|
923
|
+
* approach ensures smooth rendering of curved patches while avoiding
|
|
924
|
+
* unnecessary subdivision of flat regions.
|
|
925
|
+
*
|
|
926
|
+
* @private
|
|
927
|
+
* @param {CoonsPatch} patch - The patch to rasterize
|
|
928
|
+
* @param {Uint8ClampedArray} imageData - Output pixel buffer (RGBA, 4 bytes per pixel)
|
|
929
|
+
* @param {number} width - Image width in pixels
|
|
930
|
+
* @param {number} height - Image height in pixels
|
|
931
|
+
* @param {number} samples - Subdivision control parameter (unused in adaptive mode)
|
|
932
|
+
*/
|
|
933
|
+
function rasterizePatch(patch, imageData, width, height, samples) {
|
|
934
|
+
// Adaptive subdivision approach
|
|
935
|
+
const stack = [patch];
|
|
936
|
+
|
|
937
|
+
while (stack.length > 0) {
|
|
938
|
+
const currentPatch = stack.pop();
|
|
939
|
+
|
|
940
|
+
if (currentPatch.isFlat() || samples <= 2) {
|
|
941
|
+
// Render as quad
|
|
942
|
+
renderPatchQuad(currentPatch, imageData, width, height);
|
|
943
|
+
} else {
|
|
944
|
+
// Subdivide
|
|
945
|
+
stack.push(...currentPatch.subdivide());
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Render a flat patch as a colored quadrilateral using scanline rasterization.
|
|
952
|
+
*
|
|
953
|
+
* Renders the patch by scanning all pixels in its bounding box and evaluating
|
|
954
|
+
* the patch at each pixel's (u,v) coordinates. Uses bilinear color interpolation
|
|
955
|
+
* from the corner colors.
|
|
956
|
+
*
|
|
957
|
+
* Alpha blending is performed using the Porter-Duff "over" operator:
|
|
958
|
+
* out_α = src_α + dst_α × (1 - src_α)
|
|
959
|
+
* out_rgb = (src_rgb × src_α + dst_rgb × dst_α × (1 - src_α)) / out_α
|
|
960
|
+
*
|
|
961
|
+
* @private
|
|
962
|
+
* @param {CoonsPatch} patch - The flat patch to render
|
|
963
|
+
* @param {Uint8ClampedArray} imageData - Output pixel buffer (RGBA format)
|
|
964
|
+
* @param {number} width - Image width in pixels
|
|
965
|
+
* @param {number} height - Image height in pixels
|
|
966
|
+
*/
|
|
967
|
+
function renderPatchQuad(patch, imageData, width, height) {
|
|
968
|
+
const bbox = patch.getBBox();
|
|
969
|
+
const minX = Math.max(0, Math.floor(Number(bbox.minX)));
|
|
970
|
+
const maxX = Math.min(width - 1, Math.ceil(Number(bbox.maxX)));
|
|
971
|
+
const minY = Math.max(0, Math.floor(Number(bbox.minY)));
|
|
972
|
+
const maxY = Math.min(height - 1, Math.ceil(Number(bbox.maxY)));
|
|
973
|
+
|
|
974
|
+
// Simple scan-line fill with bilinear color interpolation
|
|
975
|
+
for (let y = minY; y <= maxY; y++) {
|
|
976
|
+
for (let x = minX; x <= maxX; x++) {
|
|
977
|
+
// Convert pixel to patch (u,v) coordinates
|
|
978
|
+
const u = D(x).minus(bbox.minX).div(bbox.maxX.minus(bbox.minX) || D(1));
|
|
979
|
+
const v = D(y).minus(bbox.minY).div(bbox.maxY.minus(bbox.minY) || D(1));
|
|
980
|
+
|
|
981
|
+
if (u.gte(0) && u.lte(1) && v.gte(0) && v.lte(1)) {
|
|
982
|
+
const { color } = patch.evaluate(u, v);
|
|
983
|
+
const idx = (y * width + x) * 4;
|
|
984
|
+
|
|
985
|
+
// Alpha blending
|
|
986
|
+
const srcA = color.a / 255;
|
|
987
|
+
const dstA = imageData[idx + 3] / 255;
|
|
988
|
+
const outA = srcA + dstA * (1 - srcA);
|
|
989
|
+
|
|
990
|
+
if (outA > 0) {
|
|
991
|
+
imageData[idx] = (color.r * srcA + imageData[idx] * dstA * (1 - srcA)) / outA;
|
|
992
|
+
imageData[idx + 1] = (color.g * srcA + imageData[idx + 1] * dstA * (1 - srcA)) / outA;
|
|
993
|
+
imageData[idx + 2] = (color.b * srcA + imageData[idx + 2] * dstA * (1 - srcA)) / outA;
|
|
994
|
+
imageData[idx + 3] = outA * 255;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ============================================================================
|
|
1002
|
+
// Mesh Gradient to Path Approximation
|
|
1003
|
+
// ============================================================================
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Approximate a mesh gradient as a collection of filled polygons.
|
|
1007
|
+
*
|
|
1008
|
+
* Converts each Coons patch into a grid of colored quads by sampling the
|
|
1009
|
+
* patch at regular intervals. This is useful for exporting to vector formats
|
|
1010
|
+
* that don't support mesh gradients (PDF, PostScript, etc.) or for
|
|
1011
|
+
* compatibility with older renderers.
|
|
1012
|
+
*
|
|
1013
|
+
* Each patch is subdivided into subdivisions × subdivisions quads, with each
|
|
1014
|
+
* quad assigned an average color from its four corners. The finer the
|
|
1015
|
+
* subdivision, the smoother the gradient approximation, but the more polygons
|
|
1016
|
+
* are generated.
|
|
1017
|
+
*
|
|
1018
|
+
* @param {Object} meshData - Parsed mesh gradient data from parseMeshGradient()
|
|
1019
|
+
* @param {Array<CoonsPatch>} meshData.patches - Array of Coons patches
|
|
1020
|
+
* @param {Object} [options={}] - Approximation options
|
|
1021
|
+
* @param {number} [options.subdivisions=8] - Number of subdivisions per patch edge
|
|
1022
|
+
* @returns {Array<{polygon: Array<{x: Decimal, y: Decimal}>, color: {r: number, g: number, b: number, a: number}}>} Array of colored polygons
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* // Convert mesh to polygons for PDF export
|
|
1026
|
+
* const meshData = parseMeshGradient(meshDef);
|
|
1027
|
+
* const polygons = meshGradientToPolygons(meshData, { subdivisions: 16 });
|
|
1028
|
+
* // Export each polygon to PDF
|
|
1029
|
+
* polygons.forEach(({polygon, color}) => {
|
|
1030
|
+
* pdf.fillColor(color.r, color.g, color.b, color.a / 255);
|
|
1031
|
+
* pdf.polygon(polygon.map(p => [p.x, p.y]));
|
|
1032
|
+
* pdf.fill();
|
|
1033
|
+
* });
|
|
1034
|
+
*
|
|
1035
|
+
* @example
|
|
1036
|
+
* // Low-resolution approximation
|
|
1037
|
+
* const polygons = meshGradientToPolygons(meshData, { subdivisions: 4 });
|
|
1038
|
+
* console.log(`Generated ${polygons.length} polygons`);
|
|
1039
|
+
*/
|
|
1040
|
+
export function meshGradientToPolygons(meshData, options = {}) {
|
|
1041
|
+
const { subdivisions = 8 } = options;
|
|
1042
|
+
const result = [];
|
|
1043
|
+
|
|
1044
|
+
for (const patch of meshData.patches || []) {
|
|
1045
|
+
const polys = patchToPolygons(patch, subdivisions);
|
|
1046
|
+
result.push(...polys);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return result;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Convert a single Coons patch into a grid of filled quadrilaterals.
|
|
1054
|
+
*
|
|
1055
|
+
* Samples the patch at regular intervals to create a grid of quads. Each quad
|
|
1056
|
+
* is assigned a color that's the average of its four corner colors. This
|
|
1057
|
+
* provides a piecewise-constant color approximation of the smooth gradient.
|
|
1058
|
+
*
|
|
1059
|
+
* @private
|
|
1060
|
+
* @param {CoonsPatch} patch - The patch to convert
|
|
1061
|
+
* @param {number} subdivisions - Number of subdivisions per edge (creates subdivisions² quads)
|
|
1062
|
+
* @returns {Array<{polygon: Array, color: Object}>} Array of colored quads
|
|
1063
|
+
*/
|
|
1064
|
+
function patchToPolygons(patch, subdivisions) {
|
|
1065
|
+
const result = [];
|
|
1066
|
+
const step = D(1).div(subdivisions);
|
|
1067
|
+
|
|
1068
|
+
for (let i = 0; i < subdivisions; i++) {
|
|
1069
|
+
for (let j = 0; j < subdivisions; j++) {
|
|
1070
|
+
const u0 = step.mul(i);
|
|
1071
|
+
const u1 = step.mul(i + 1);
|
|
1072
|
+
const v0 = step.mul(j);
|
|
1073
|
+
const v1 = step.mul(j + 1);
|
|
1074
|
+
|
|
1075
|
+
const p00 = patch.evaluate(u0, v0);
|
|
1076
|
+
const p10 = patch.evaluate(u1, v0);
|
|
1077
|
+
const p01 = patch.evaluate(u0, v1);
|
|
1078
|
+
const p11 = patch.evaluate(u1, v1);
|
|
1079
|
+
|
|
1080
|
+
// Average color for this quad
|
|
1081
|
+
const avgColor = {
|
|
1082
|
+
r: (p00.color.r + p10.color.r + p01.color.r + p11.color.r) / 4,
|
|
1083
|
+
g: (p00.color.g + p10.color.g + p01.color.g + p11.color.g) / 4,
|
|
1084
|
+
b: (p00.color.b + p10.color.b + p01.color.b + p11.color.b) / 4,
|
|
1085
|
+
a: (p00.color.a + p10.color.a + p01.color.a + p11.color.a) / 4
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
result.push({
|
|
1089
|
+
polygon: [
|
|
1090
|
+
PolygonClip.point(p00.point.x, p00.point.y),
|
|
1091
|
+
PolygonClip.point(p10.point.x, p10.point.y),
|
|
1092
|
+
PolygonClip.point(p11.point.x, p11.point.y),
|
|
1093
|
+
PolygonClip.point(p01.point.x, p01.point.y)
|
|
1094
|
+
],
|
|
1095
|
+
color: avgColor
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return result;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// ============================================================================
|
|
1104
|
+
// Mesh Gradient Clipping
|
|
1105
|
+
// ============================================================================
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Apply a clipping path to a mesh gradient.
|
|
1109
|
+
*
|
|
1110
|
+
* Clips a mesh gradient against an arbitrary polygon using the Sutherland-Hodgman
|
|
1111
|
+
* polygon clipping algorithm (via polygon-clip module). The mesh is first
|
|
1112
|
+
* approximated as polygons, then each polygon is clipped against the clip path.
|
|
1113
|
+
*
|
|
1114
|
+
* This is useful for implementing SVG clipPath with mesh gradients or for
|
|
1115
|
+
* rendering only the visible portion of a gradient.
|
|
1116
|
+
*
|
|
1117
|
+
* @param {Object} meshData - Parsed mesh gradient data from parseMeshGradient()
|
|
1118
|
+
* @param {Array<CoonsPatch>} meshData.patches - Array of patches to clip
|
|
1119
|
+
* @param {Array<{x: Decimal, y: Decimal}>} clipPolygon - Clipping polygon vertices (must be closed)
|
|
1120
|
+
* @param {Object} [options={}] - Clipping options
|
|
1121
|
+
* @param {number} [options.subdivisions=16] - Mesh subdivision before clipping (higher = more accurate)
|
|
1122
|
+
* @returns {Array<{polygon: Array<{x: Decimal, y: Decimal}>, color: {r: number, g: number, b: number, a: number}}>} Clipped polygons with preserved colors
|
|
1123
|
+
*
|
|
1124
|
+
* @example
|
|
1125
|
+
* // Clip mesh to circular region
|
|
1126
|
+
* const meshData = parseMeshGradient(meshDef);
|
|
1127
|
+
* const circlePolygon = [];
|
|
1128
|
+
* for (let i = 0; i < 32; i++) {
|
|
1129
|
+
* const angle = (i / 32) * 2 * Math.PI;
|
|
1130
|
+
* circlePolygon.push(point(
|
|
1131
|
+
* 100 + 50 * Math.cos(angle),
|
|
1132
|
+
* 100 + 50 * Math.sin(angle)
|
|
1133
|
+
* ));
|
|
1134
|
+
* }
|
|
1135
|
+
* const clipped = clipMeshGradient(meshData, circlePolygon);
|
|
1136
|
+
*
|
|
1137
|
+
* @example
|
|
1138
|
+
* // Clip to viewport rectangle
|
|
1139
|
+
* const viewport = [
|
|
1140
|
+
* point(0, 0), point(800, 0),
|
|
1141
|
+
* point(800, 600), point(0, 600)
|
|
1142
|
+
* ];
|
|
1143
|
+
* const clipped = clipMeshGradient(meshData, viewport, { subdivisions: 32 });
|
|
1144
|
+
*/
|
|
1145
|
+
export function clipMeshGradient(meshData, clipPolygon, options = {}) {
|
|
1146
|
+
const { subdivisions = 16 } = options;
|
|
1147
|
+
|
|
1148
|
+
// First convert mesh to polygons
|
|
1149
|
+
const meshPolygons = meshGradientToPolygons(meshData, { subdivisions });
|
|
1150
|
+
|
|
1151
|
+
// Clip each polygon
|
|
1152
|
+
const clippedPolygons = [];
|
|
1153
|
+
|
|
1154
|
+
for (const { polygon, color } of meshPolygons) {
|
|
1155
|
+
const clipped = PolygonClip.polygonIntersection(polygon, clipPolygon);
|
|
1156
|
+
|
|
1157
|
+
for (const clippedPoly of clipped) {
|
|
1158
|
+
if (clippedPoly.length >= 3) {
|
|
1159
|
+
clippedPolygons.push({ polygon: clippedPoly, color });
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return clippedPolygons;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Generate SVG path elements from clipped mesh gradient polygons.
|
|
1169
|
+
*
|
|
1170
|
+
* Converts the polygon representation back to SVG path data with fill colors,
|
|
1171
|
+
* suitable for embedding in SVG documents. Each polygon becomes a closed path
|
|
1172
|
+
* with an rgba() fill color.
|
|
1173
|
+
*
|
|
1174
|
+
* This is the final step in rendering mesh gradients to SVG when the target
|
|
1175
|
+
* doesn't support native meshGradient elements.
|
|
1176
|
+
*
|
|
1177
|
+
* @param {Array<{polygon: Array<{x: Decimal, y: Decimal}>, color: {r: number, g: number, b: number, a: number}}>} clippedPolygons - Result from clipMeshGradient() or meshGradientToPolygons()
|
|
1178
|
+
* @returns {Array<{pathData: string, fill: string}>} Array of SVG path objects with path data and fill color
|
|
1179
|
+
*
|
|
1180
|
+
* @example
|
|
1181
|
+
* // Convert clipped mesh to SVG paths
|
|
1182
|
+
* const clipped = clipMeshGradient(meshData, clipPolygon);
|
|
1183
|
+
* const svgPaths = clippedMeshToSVG(clipped);
|
|
1184
|
+
* svgPaths.forEach(({pathData, fill}) => {
|
|
1185
|
+
* const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1186
|
+
* path.setAttribute('d', pathData);
|
|
1187
|
+
* path.setAttribute('fill', fill);
|
|
1188
|
+
* svg.appendChild(path);
|
|
1189
|
+
* });
|
|
1190
|
+
*
|
|
1191
|
+
* @example
|
|
1192
|
+
* // Generate SVG string
|
|
1193
|
+
* const svgPaths = clippedMeshToSVG(clipped);
|
|
1194
|
+
* const svgContent = svgPaths.map(({pathData, fill}) =>
|
|
1195
|
+
* `<path d="${pathData}" fill="${fill}"/>`
|
|
1196
|
+
* ).join('\n');
|
|
1197
|
+
*/
|
|
1198
|
+
export function clippedMeshToSVG(clippedPolygons) {
|
|
1199
|
+
return clippedPolygons.map(({ polygon, color }) => {
|
|
1200
|
+
let pathData = '';
|
|
1201
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
1202
|
+
const p = polygon[i];
|
|
1203
|
+
if (i === 0) {
|
|
1204
|
+
pathData += `M ${Number(p.x).toFixed(6)} ${Number(p.y).toFixed(6)}`;
|
|
1205
|
+
} else {
|
|
1206
|
+
pathData += ` L ${Number(p.x).toFixed(6)} ${Number(p.y).toFixed(6)}`;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
pathData += ' Z';
|
|
1210
|
+
|
|
1211
|
+
const fill = `rgba(${Math.round(color.r)},${Math.round(color.g)},${Math.round(color.b)},${(color.a / 255).toFixed(3)})`;
|
|
1212
|
+
|
|
1213
|
+
return { pathData, fill };
|
|
1214
|
+
});
|
|
1215
|
+
}
|