@pixerate/editor 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import {
3
3
  } from "../chunk-QVZZGT4X.js";
4
4
  import {
5
5
  PlanarMap,
6
+ angleToSvgLinearCoords,
6
7
  bezierPathBounds,
7
8
  bezierPathToSvg,
8
9
  closeContour,
@@ -14,7 +15,6 @@ import {
14
15
  fitCubics,
15
16
  flattenChain,
16
17
  formatNumber,
17
- gradientToSvgDef,
18
18
  hatchPath,
19
19
  isStraightCubic,
20
20
  normalizeBezierPath,
@@ -41,17 +41,25 @@ import {
41
41
  windingNumber
42
42
  } from "../chunk-3LHWTGHW.js";
43
43
  import {
44
- mixColorsOklab
44
+ mixColorsOklab,
45
+ oklabToRgb,
46
+ rgbToOklab
45
47
  } from "../chunk-ZRJMJIQU.js";
46
48
  import {
47
49
  evaluateEasing
48
50
  } from "../chunk-UB2OY7FD.js";
49
51
  import {
50
- hexToRgba
52
+ hexToRgba,
53
+ normalizeTokenColor,
54
+ rgbaToHex
51
55
  } from "../chunk-TGD52FEZ.js";
52
56
 
53
57
  // src/vector/types.ts
54
58
  var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
59
+ var VECTOR_GRADIENT_TYPES = ["linear", "radial", "conic", "reflected", "diamond"];
60
+ var VECTOR_GRADIENT_SPREADS = ["pad", "reflect", "repeat"];
61
+ var VECTOR_GRADIENT_INTERPOLATIONS = ["srgb", "linear", "oklab"];
62
+ var VECTOR_STROKE_GRADIENT_MODES = ["within", "along", "across"];
55
63
  var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
56
64
  var VECTOR_BLEND_SPACINGS = ["steps", "distance", "smoothColor"];
57
65
  var VECTOR_TOOLS = [
@@ -70,20 +78,824 @@ var VECTOR_TOOLS = [
70
78
  "width",
71
79
  "shapeBuilder",
72
80
  "paintBucket",
73
- "blend"
81
+ "blend",
82
+ "gradient"
74
83
  ];
75
84
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
76
85
  var VECTOR_PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
77
86
  var VECTOR_EFFECT_TYPES = ["trim", "roundCorners", "puckerBloat", "twist", "zigzag", "wiggle", "offset", "merge", "repeater"];
78
87
  var VECTOR_MERGE_MODES = ["merge", "add", "subtract", "intersect", "exclude"];
79
88
 
80
- // src/vector/corners.ts
81
- var EPS = 1e-9;
82
- var MIN_SINE = 1e-6;
89
+ // src/vector/gradient.ts
90
+ var clamp = (v2, lo, hi) => Math.min(hi, Math.max(lo, v2));
91
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
92
+ var mul = (a, b) => [
93
+ b[0] * a[0] + b[2] * a[1],
94
+ b[1] * a[0] + b[3] * a[1],
95
+ b[0] * a[2] + b[2] * a[3],
96
+ b[1] * a[2] + b[3] * a[3],
97
+ b[0] * a[4] + b[2] * a[5] + b[4],
98
+ b[1] * a[4] + b[3] * a[5] + b[5]
99
+ ];
100
+ var apply = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
101
+ var invert = (m) => {
102
+ const [a, b, c, d, e, f] = m;
103
+ const det = a * d - b * c;
104
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-18) return null;
105
+ return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
106
+ };
107
+ var isIdentity = (m) => m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
83
108
  var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
84
109
  var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
85
- var mul = (a, k) => ({ x: a.x * k, y: a.y * k });
110
+ var scale = (a, k) => ({ x: a.x * k, y: a.y * k });
86
111
  var dot = (a, b) => a.x * b.x + a.y * b.y;
112
+ var perp = (v2) => ({ x: -v2.y, y: v2.x });
113
+ var finitePoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
114
+ var fmt = (n, digits = 4) => {
115
+ const k = 10 ** digits;
116
+ const v2 = Math.round(n * k) / k;
117
+ return Object.is(v2, -0) ? "0" : String(v2);
118
+ };
119
+ var fmtMatrix = (m) => `matrix(${m.map((n) => fmt(n, 6)).join(" ")})`;
120
+ function isVectorGradient(paint) {
121
+ return !!paint && typeof paint === "object" && VECTOR_GRADIENT_TYPES.includes(paint.type) && Array.isArray(paint.stops);
122
+ }
123
+ function parseVectorColor(color) {
124
+ const hex = normalizeTokenColor(color);
125
+ if (hex === null) return null;
126
+ const c = hexToRgba(hex);
127
+ return [c.r, c.g, c.b, c.a];
128
+ }
129
+ var toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
130
+ var toGamma = (c) => {
131
+ const v2 = clamp(c, 0, 1);
132
+ return v2 <= 31308e-7 ? 12.92 * v2 : 1.055 * v2 ** (1 / 2.4) - 0.055;
133
+ };
134
+ function intoSpace(space, [r, g, b]) {
135
+ if (space === "linear") return [toLinear(r), toLinear(g), toLinear(b)];
136
+ if (space === "oklab") {
137
+ const c = rgbToOklab(r, g, b);
138
+ return [c.l, c.a, c.b];
139
+ }
140
+ return [r, g, b];
141
+ }
142
+ function outOfSpace(space, [x, y, z]) {
143
+ if (space === "linear") return [toGamma(x), toGamma(y), toGamma(z)];
144
+ if (space === "oklab") {
145
+ const c = oklabToRgb({ l: x, a: y, b: z, alpha: 1 });
146
+ return [c.r, c.g, c.b];
147
+ }
148
+ return [clamp(x, 0, 1), clamp(y, 0, 1), clamp(z, 0, 1)];
149
+ }
150
+ var MID_MIN = 0.01;
151
+ var MID_MAX = 0.99;
152
+ var VECTOR_GRADIENT_MIN_STOPS = 2;
153
+ function gradientMidpointRemap(u, m) {
154
+ const mid = clamp(m, MID_MIN, MID_MAX);
155
+ if (Math.abs(mid - 0.5) < 1e-6) return u;
156
+ return u <= mid ? 0.5 * u / mid : 0.5 + 0.5 * (u - mid) / (1 - mid);
157
+ }
158
+ var hasMidpoint = (s) => s.midpoint !== void 0 && Number.isFinite(s.midpoint) && Math.abs(s.midpoint - 0.5) > 1e-3;
159
+ function sortedVectorGradientStops(g) {
160
+ return (g.stops ?? []).filter((s) => s && Number.isFinite(s.offset) && typeof s.color === "string").map((s, i) => ({ s: { ...s, offset: clamp(s.offset, 0, 1) }, i })).sort((a, b) => a.s.offset - b.s.offset || a.i - b.i).map((e) => e.s);
161
+ }
162
+ function vectorGradientNeedsExpansion(g) {
163
+ return (g.interpolation ?? "srgb") !== "srgb" || (g.smoothness ?? 0) > 0 || (g.stops ?? []).some(hasMidpoint);
164
+ }
165
+ function vectorGradientRamp(g) {
166
+ const space = g.interpolation && VECTOR_GRADIENT_INTERPOLATIONS.includes(g.interpolation) ? g.interpolation : "srgb";
167
+ const smooth = clamp(Number.isFinite(g.smoothness) ? g.smoothness : 0, 0, 1);
168
+ const pts = [];
169
+ for (const s of sortedVectorGradientStops(g)) {
170
+ const c = parseVectorColor(s.color);
171
+ if (!c) return null;
172
+ pts.push({ t: s.offset, v: intoSpace(space, c), a: c[3] * clamp(s.opacity ?? 1, 0, 1), mid: s.midpoint ?? 0.5 });
173
+ }
174
+ return {
175
+ sample(t) {
176
+ const n = pts.length;
177
+ if (!n) return [0, 0, 0, 0];
178
+ const first = pts[0];
179
+ const last = pts[n - 1];
180
+ const at2 = (p) => [...outOfSpace(space, p.v), p.a];
181
+ if (!(t > first.t) || n < 2) return at2(first);
182
+ if (t >= last.t) return at2(last);
183
+ let i = 0;
184
+ while (i + 2 < n && !(t >= pts[i].t && t <= pts[i + 1].t)) i++;
185
+ const p0 = pts[i];
186
+ const p1 = pts[i + 1];
187
+ const h = Math.max(p1.t - p0.t, 1e-6);
188
+ const u = gradientMidpointRemap((t - p0.t) / h, p0.mid);
189
+ const alpha = p0.a + (p1.a - p0.a) * u;
190
+ if (smooth <= 0) return [...outOfSpace(space, [0, 1, 2].map((c) => p0.v[c] + (p1.v[c] - p0.v[c]) * u)), alpha];
191
+ const tangent2 = (k) => {
192
+ const [a, b] = k === 0 ? [0, 1] : k === n - 1 ? [n - 2, n - 1] : [k - 1, k + 1];
193
+ const d = Math.max(pts[b].t - pts[a].t, 1e-6);
194
+ const half = k === 0 || k === n - 1 ? 0.5 : 1;
195
+ return [0, 1, 2].map((c) => (pts[b].v[c] - pts[a].v[c]) / d * half);
196
+ };
197
+ const m0 = tangent2(i);
198
+ const m1 = tangent2(i + 1);
199
+ const u2 = u * u;
200
+ const u3 = u2 * u;
201
+ const h00 = 2 * u3 - 3 * u2 + 1;
202
+ const h10 = u3 - 2 * u2 + u;
203
+ const h01 = -2 * u3 + 3 * u2;
204
+ const h11 = u3 - u2;
205
+ const v2 = [0, 1, 2].map((c) => {
206
+ const linear = p0.v[c] + (p1.v[c] - p0.v[c]) * u;
207
+ const spline = h00 * p0.v[c] + h10 * h * m0[c] + h01 * p1.v[c] + h11 * h * m1[c];
208
+ const value = linear + (spline - linear) * smooth;
209
+ return clamp(value, Math.min(p0.v[c], p1.v[c]), Math.max(p0.v[c], p1.v[c]));
210
+ });
211
+ return [...outOfSpace(space, v2), alpha];
212
+ }
213
+ };
214
+ }
215
+ function sampleVectorGradient(g, t) {
216
+ const ramp = vectorGradientRamp(g);
217
+ if (ramp) {
218
+ const [r, gg, b, a] = ramp.sample(t);
219
+ return { color: rgbaToHex(r, gg, b), opacity: a };
220
+ }
221
+ const stops = sortedVectorGradientStops(g);
222
+ const s = [...stops].reverse().find((x) => x.offset <= t) ?? stops[0];
223
+ return s ? { color: s.color, opacity: s.opacity ?? 1 } : { color: "#000000", opacity: 0 };
224
+ }
225
+ var stopOf = (offset, c) => {
226
+ const color = rgbaToHex(c[0], c[1], c[2]);
227
+ const opacity = Math.round(c[3] * 1e4) / 1e4;
228
+ return opacity < 1 ? { offset, color, opacity } : { offset, color };
229
+ };
230
+ var maxDiff = (a, b) => Math.max(Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1]), Math.abs(a[2] - b[2]), Math.abs(a[3] - b[3]));
231
+ var lerpRgba = (a, b, u) => [0, 1, 2, 3].map((c) => a[c] + (b[c] - a[c]) * u);
232
+ function expandVectorGradientStops(g, options = {}) {
233
+ const stops = sortedVectorGradientStops(g);
234
+ const plain3 = () => stops.map(({ offset, color, opacity }) => opacity !== void 0 && opacity !== 1 ? { offset, color, opacity } : { offset, color });
235
+ if (!vectorGradientNeedsExpansion(g)) return plain3();
236
+ const ramp = vectorGradientRamp(g);
237
+ if (!ramp || stops.length < 2) return plain3();
238
+ const tol = options.tolerance ?? 0.5 / 255;
239
+ const sRgbOnly = (g.interpolation ?? "srgb") === "srgb" && !((g.smoothness ?? 0) > 0);
240
+ const own = (s) => {
241
+ const c = parseVectorColor(s.color);
242
+ return [c[0], c[1], c[2], c[3] * clamp(s.opacity ?? 1, 0, 1)];
243
+ };
244
+ const out = [];
245
+ const push = (offset, c) => out.push(stopOf(offset, c));
246
+ for (let i = 0; i < stops.length; i++) {
247
+ const a = stops[i];
248
+ const b = stops[i + 1];
249
+ push(a.offset, own(a));
250
+ if (!b || b.offset - a.offset < 1e-9) continue;
251
+ const knots = [a.offset];
252
+ if (hasMidpoint(a)) knots.push(a.offset + (b.offset - a.offset) * clamp(a.midpoint, MID_MIN, MID_MAX));
253
+ knots.push(b.offset);
254
+ const inside = (t) => ramp.sample(clamp(t, a.offset + 1e-9, b.offset - 1e-9));
255
+ for (let k = 0; k + 1 < knots.length; k++) {
256
+ const t0 = knots[k];
257
+ const t1 = knots[k + 1];
258
+ if (k > 0) push(t0, inside(t0));
259
+ if (sRgbOnly) continue;
260
+ const refine = (x0, c0, x1, c1, depth) => {
261
+ const xm = (x0 + x1) / 2;
262
+ const cm = inside(xm);
263
+ const ok = depth >= 8 || [0.03, 0.1, 0.25, 0.5, 0.75, 0.9, 0.97].every((f) => maxDiff(inside(x0 + (x1 - x0) * f), lerpRgba(c0, c1, f)) <= tol);
264
+ if (ok) return;
265
+ refine(x0, c0, xm, cm, depth + 1);
266
+ push(xm, cm);
267
+ refine(xm, cm, x1, c1, depth + 1);
268
+ };
269
+ refine(t0, inside(t0), t1, inside(t1), 0);
270
+ }
271
+ }
272
+ return out;
273
+ }
274
+ function samplePlainStops(stops, t) {
275
+ const parsed = stops.map((s) => {
276
+ const c = parseVectorColor(s.color) ?? [0, 0, 0, 1];
277
+ return { t: s.offset, c: [c[0], c[1], c[2], c[3] * (s.opacity ?? 1)] };
278
+ });
279
+ if (!parsed.length) return [0, 0, 0, 0];
280
+ if (t <= parsed[0].t) return parsed[0].c;
281
+ for (let i = 0; i + 1 < parsed.length; i++) {
282
+ const a = parsed[i];
283
+ const b = parsed[i + 1];
284
+ if (t <= b.t) return b.t - a.t < 1e-12 ? b.c : lerpRgba(a.c, b.c, (t - a.t) / (b.t - a.t));
285
+ }
286
+ return parsed[parsed.length - 1].c;
287
+ }
288
+ function reverseVectorGradient(g) {
289
+ const stops = sortedVectorGradientStops(g).reverse();
290
+ const next = stops.map((s, i) => {
291
+ const following = stops[i + 1];
292
+ const mid = following ? 1 - (following.midpoint ?? 0.5) : 0.5;
293
+ const { midpoint: _m, ...rest } = s;
294
+ return { ...rest, offset: 1 - s.offset, ...Math.abs(mid - 0.5) > 1e-6 ? { midpoint: mid } : {} };
295
+ });
296
+ return { ...g, stops: next };
297
+ }
298
+ function placeStop(stops, stop) {
299
+ let i = stops.findIndex((o) => o.offset > stop.offset);
300
+ if (i < 0) i = stops.length;
301
+ stops.splice(i, 0, stop);
302
+ return i;
303
+ }
304
+ function insertVectorGradientStop(g, offset) {
305
+ const at2 = clamp(offset, 0, 1);
306
+ const { color, opacity } = sampleVectorGradient(g, at2);
307
+ const stops = sortedVectorGradientStops(g);
308
+ const index = placeStop(stops, opacity < 1 ? { offset: at2, color, opacity } : { offset: at2, color });
309
+ return { stops, index };
310
+ }
311
+ function moveVectorGradientStop(stops, index, offset) {
312
+ const next = stops.map((s2) => ({ ...s2 }));
313
+ if (index < 0 || index >= next.length) return { stops: next, index };
314
+ const [s] = next.splice(index, 1);
315
+ return { stops: next, index: placeStop(next, { ...s, offset: clamp(offset, 0, 1) }) };
316
+ }
317
+ function removeVectorGradientStop(stops, index) {
318
+ if (stops.length <= VECTOR_GRADIENT_MIN_STOPS || index < 0 || index >= stops.length) return null;
319
+ return stops.filter((_, i) => i !== index);
320
+ }
321
+ function vectorGradientMidpointPosition(stops, index) {
322
+ const a = stops[index];
323
+ const b = stops[index + 1];
324
+ if (!a || !b) return null;
325
+ return a.offset + (b.offset - a.offset) * (a.midpoint ?? 0.5);
326
+ }
327
+ function vectorGradientMidpointFromPosition(stops, index, pos) {
328
+ const a = stops[index];
329
+ const b = stops[index + 1];
330
+ if (!a || !b) return null;
331
+ const span = Math.max(b.offset - a.offset, 1e-6);
332
+ return clamp((pos - a.offset) / span, 0.05, 0.95);
333
+ }
334
+ function spanVectorGradientStops(g, from, to, expanded = expandVectorGradientStops(g)) {
335
+ if (to < from) {
336
+ const s = spanVectorGradientStops(g, to, from, expanded);
337
+ return s.map((x) => ({ ...x, offset: 1 - x.offset })).reverse();
338
+ }
339
+ const d = to - from;
340
+ const at2 = (offset, t) => stopOf(offset, samplePlainStops(expanded, t));
341
+ const out = [at2(0, from)];
342
+ if (d > 1e-9) {
343
+ for (const s of expanded) if (s.offset > from && s.offset < to) out.push({ ...s, offset: (s.offset - from) / d });
344
+ }
345
+ out.push(at2(1, to));
346
+ return out;
347
+ }
348
+ var hasExplicitGradientGeometry = (g) => finitePoint(g.start) && finitePoint(g.end);
349
+ function shorthandPoints(g) {
350
+ if (g.type === "linear" || g.type === "reflected") {
351
+ const c = angleToSvgLinearCoords(Number.isFinite(g.angle) ? g.angle : 180);
352
+ const start2 = g.type === "linear" ? { x: c.x1, y: c.y1 } : { x: 0.5, y: 0.5 };
353
+ const end = { x: c.x2, y: c.y2 };
354
+ return { start: start2, end, width: add(start2, perp(sub(end, start2))) };
355
+ }
356
+ const start = { x: Number.isFinite(g.cx) ? g.cx : 0.5, y: Number.isFinite(g.cy) ? g.cy : 0.5 };
357
+ const r = Number.isFinite(g.r) ? g.r : 0.5;
358
+ if (g.type === "radial") return { start, end: { x: start.x + r, y: start.y }, width: { x: start.x, y: start.y + r } };
359
+ const a = (Number.isFinite(g.angle) ? g.angle : 90) * Math.PI / 180;
360
+ const dir = { x: Math.sin(a), y: -Math.cos(a) };
361
+ return { start, end: add(start, scale(dir, r)), width: add(start, scale(perp(dir), r)) };
362
+ }
363
+ function vectorGradientPoints(g) {
364
+ const explicit = hasExplicitGradientGeometry(g);
365
+ const base = explicit ? null : shorthandPoints(g);
366
+ const start = explicit ? { x: g.start.x, y: g.start.y } : base.start;
367
+ const end = explicit ? { x: g.end.x, y: g.end.y } : base.end;
368
+ const width = explicit ? finitePoint(g.width) ? { x: g.width.x, y: g.width.y } : add(start, perp(sub(end, start))) : base.width;
369
+ return {
370
+ start,
371
+ end,
372
+ width,
373
+ focal: g.type === "radial" && finitePoint(g.focal) ? { x: g.focal.x, y: g.focal.y } : null,
374
+ focalRadius: g.type === "radial" ? clamp(Number.isFinite(g.focalRadius) ? g.focalRadius : 0, 0, 0.99) : 0,
375
+ units: explicit && g.units === "userSpaceOnUse" ? "userSpaceOnUse" : "objectBoundingBox"
376
+ };
377
+ }
378
+ function vectorBoxMatrix(bounds) {
379
+ if (!bounds) return IDENTITY;
380
+ let { x, y, width: w, height: h } = bounds;
381
+ const fallback = Math.max(w, h, 1e-9) > 1e-9 ? Math.max(w, h) : 1;
382
+ if (!(w > 1e-9)) {
383
+ x -= fallback / 2 - w / 2;
384
+ w = fallback;
385
+ }
386
+ if (!(h > 1e-9)) {
387
+ y -= fallback / 2 - h / 2;
388
+ h = fallback;
389
+ }
390
+ return [w, 0, 0, h, x, y];
391
+ }
392
+ var framePointsMatrix = (p) => [p.end.x - p.start.x, p.end.y - p.start.y, p.width.x - p.start.x, p.width.y - p.start.y, p.start.x, p.start.y];
393
+ function vectorGradientUnitsMatrix(g, bounds) {
394
+ return vectorGradientPoints(g).units === "objectBoundingBox" ? vectorBoxMatrix(bounds) : IDENTITY;
395
+ }
396
+ function vectorGradientMatrix(g, bounds) {
397
+ return mul(framePointsMatrix(vectorGradientPoints(g)), vectorGradientUnitsMatrix(g, bounds));
398
+ }
399
+ var FOCAL_MAX = 0.99;
400
+ function unitFocal(g) {
401
+ const p = vectorGradientPoints(g);
402
+ if (!p.focal && !(p.focalRadius > 0)) return null;
403
+ const inv = invert(framePointsMatrix(p));
404
+ if (!inv) return null;
405
+ let f = p.focal ? apply(inv, p.focal) : { x: 0, y: 0 };
406
+ const reach = FOCAL_MAX * (1 - p.focalRadius);
407
+ const r = Math.hypot(f.x, f.y);
408
+ if (r > reach) f = scale(f, reach / r);
409
+ return { f, fr: p.focalRadius };
410
+ }
411
+ function vectorGradientFocalParam(q, f = { x: 0, y: 0 }, fr = 0) {
412
+ const d = sub(q, f);
413
+ const dr = 1 - fr;
414
+ const a = dot(f, f) - dr * dr;
415
+ const b = dot(d, f) - fr * dr;
416
+ const c = dot(d, d) - fr * fr;
417
+ if (Math.abs(a) < 1e-12) return Math.abs(b) < 1e-12 ? 0 : -c / (2 * b);
418
+ return (-b - Math.sqrt(Math.max(0, b * b - a * c))) / a;
419
+ }
420
+ function vectorGradientParam(type, q, focal) {
421
+ switch (type) {
422
+ case "linear":
423
+ return q.x;
424
+ case "reflected":
425
+ return Math.abs(q.x);
426
+ case "diamond":
427
+ return Math.abs(q.x) + Math.abs(q.y);
428
+ case "conic": {
429
+ const turn = Math.atan2(q.y, q.x) / (2 * Math.PI);
430
+ return turn - Math.floor(turn);
431
+ }
432
+ case "radial":
433
+ return focal ? vectorGradientFocalParam(q, focal.f, focal.fr) : Math.hypot(q.x, q.y);
434
+ }
435
+ }
436
+ function applyVectorGradientSpread(t, spread = "pad") {
437
+ if (!Number.isFinite(t)) return 0;
438
+ if (spread === "repeat") return t - Math.floor(t);
439
+ if (spread === "reflect") {
440
+ const m = (t % 2 + 2) % 2;
441
+ return m <= 1 ? m : 2 - m;
442
+ }
443
+ return clamp(t, 0, 1);
444
+ }
445
+ function vectorGradientTAt(g, bounds, p) {
446
+ const inv = invert(vectorGradientMatrix(g, bounds));
447
+ if (!inv) return 0;
448
+ const raw = vectorGradientParam(g.type, apply(inv, p), g.type === "radial" ? unitFocal(g) : null);
449
+ return g.type === "conic" ? raw : applyVectorGradientSpread(raw, g.spread ?? "pad");
450
+ }
451
+ function sampleVectorGradientAt(g, bounds, p) {
452
+ const t = vectorGradientTAt(g, bounds, p);
453
+ const ramp = vectorGradientRamp(g);
454
+ return ramp ? ramp.sample(t) : samplePlainStops(sortedVectorGradientStops(g), t);
455
+ }
456
+ function vectorGradientAspect(g) {
457
+ const p = vectorGradientPoints(g);
458
+ const u = sub(p.end, p.start);
459
+ const l2 = dot(u, u);
460
+ return l2 > 1e-18 ? dot(sub(p.width, p.start), perp(u)) / l2 : 1;
461
+ }
462
+ function explicitVectorGradient(g) {
463
+ const p = vectorGradientPoints(g);
464
+ const { angle: _a, cx: _cx, cy: _cy, r: _r, ...rest } = g;
465
+ return {
466
+ ...rest,
467
+ start: p.start,
468
+ end: p.end,
469
+ width: p.width,
470
+ ...p.focal ? { focal: p.focal } : {},
471
+ units: p.units
472
+ };
473
+ }
474
+ function pinVectorGradient(g, bounds) {
475
+ const e = explicitVectorGradient(g);
476
+ if (e.units === "userSpaceOnUse") return e;
477
+ const m = vectorBoxMatrix(bounds);
478
+ return { ...e, start: apply(m, e.start), end: apply(m, e.end), width: apply(m, e.width), ...e.focal ? { focal: apply(m, e.focal) } : {}, units: "userSpaceOnUse" };
479
+ }
480
+ function transformVectorGradient(g, m) {
481
+ if (!hasExplicitGradientGeometry(g) || g.units !== "userSpaceOnUse" || isIdentity(m)) return g;
482
+ const p = vectorGradientPoints(g);
483
+ return { ...g, start: apply(m, p.start), end: apply(m, p.end), width: apply(m, p.width), ...p.focal ? { focal: apply(m, p.focal) } : {} };
484
+ }
485
+ function transformVectorPaint(paint, m) {
486
+ return isVectorGradient(paint) ? transformVectorGradient(paint, m) : paint;
487
+ }
488
+ var isCentredVectorGradient = (type) => type === "radial" || type === "conic" || type === "diamond";
489
+ function vectorGradientHandles(g, bounds, toWorld = IDENTITY) {
490
+ const p = vectorGradientPoints(g);
491
+ const m = mul(vectorGradientUnitsMatrix(g, bounds), toWorld);
492
+ const f = g.type === "radial" ? unitFocal(g) : null;
493
+ const frame2 = mul(framePointsMatrix(p), m);
494
+ return {
495
+ start: apply(m, p.start),
496
+ end: apply(m, p.end),
497
+ aspect: isCentredVectorGradient(g.type) ? apply(m, p.width) : null,
498
+ focal: g.type === "radial" ? apply(frame2, f?.f ?? { x: 0, y: 0 }) : null
499
+ };
500
+ }
501
+ function withMainAxis(p, start, end) {
502
+ const before = invert(framePointsMatrix(p));
503
+ const u = sub(p.end, p.start);
504
+ const l2 = dot(u, u);
505
+ const w = sub(p.width, p.start);
506
+ const rel = l2 > 1e-18 ? { a: dot(w, u) / l2, b: dot(w, perp(u)) / l2 } : { a: 0, b: 1 };
507
+ const u2 = sub(end, start);
508
+ const width = add(start, add(scale(u2, rel.a), scale(perp(u2), rel.b)));
509
+ const next = { start, end, width };
510
+ const focal = p.focal && before ? apply(framePointsMatrix(next), apply(before, p.focal)) : p.focal;
511
+ return { ...next, focal };
512
+ }
513
+ function moveVectorGradientHandle(g, bounds, handle, to, from = to) {
514
+ const pinned = pinVectorGradient(g, bounds);
515
+ const p = vectorGradientPoints(pinned);
516
+ const set = (q) => {
517
+ const next = { ...pinned, ...q };
518
+ if (next.focal === null || next.focal === void 0) delete next.focal;
519
+ return next;
520
+ };
521
+ switch (handle) {
522
+ case "start": {
523
+ const d = sub(to, from);
524
+ return set({ start: add(p.start, d), end: add(p.end, d), width: add(p.width, d), focal: p.focal ? add(p.focal, d) : null });
525
+ }
526
+ case "end":
527
+ case "radius": {
528
+ if (Math.hypot(to.x - p.start.x, to.y - p.start.y) < 1e-6) return pinned;
529
+ const n = withMainAxis(p, p.start, to);
530
+ return set({ end: n.end, width: n.width, focal: n.focal });
531
+ }
532
+ case "aspect": {
533
+ const u = sub(p.end, p.start);
534
+ const l2 = dot(u, u);
535
+ if (l2 < 1e-18) return pinned;
536
+ let aspect = dot(sub(to, p.start), perp(u)) / l2;
537
+ if (Math.abs(aspect) < 0.01) aspect = aspect < 0 ? -0.01 : 0.01;
538
+ const before = invert(framePointsMatrix(p));
539
+ const width = add(p.start, scale(perp(u), aspect));
540
+ const focal = p.focal && before ? apply(framePointsMatrix({ start: p.start, end: p.end, width }), apply(before, p.focal)) : null;
541
+ return set({ width, focal });
542
+ }
543
+ case "focal": {
544
+ const frame2 = framePointsMatrix(p);
545
+ const inv = invert(frame2);
546
+ if (!inv) return pinned;
547
+ let q = apply(inv, to);
548
+ const r = Math.hypot(q.x, q.y);
549
+ if (r < 1e-6) return set({ focal: null });
550
+ const reach = FOCAL_MAX * (1 - p.focalRadius);
551
+ if (r > reach) q = scale(q, reach / r);
552
+ return set({ focal: apply(frame2, q) });
553
+ }
554
+ }
555
+ }
556
+ function setVectorGradientVector(g, bounds, start, end) {
557
+ const pinned = pinVectorGradient(g, bounds);
558
+ const p = vectorGradientPoints(pinned);
559
+ const aspect = Math.abs(vectorGradientAspect(pinned)) > 1e-6 ? vectorGradientAspect(pinned) : 1;
560
+ const u = sub(end, start);
561
+ const width = add(start, scale(perp(u), aspect));
562
+ const before = invert(framePointsMatrix(p));
563
+ const focal = p.focal && before ? apply(framePointsMatrix({ start, end, width }), apply(before, p.focal)) : null;
564
+ const next = { ...pinned, start, end, width };
565
+ if (focal) next.focal = focal;
566
+ else delete next.focal;
567
+ return next;
568
+ }
569
+ var VECTOR_GRADIENT_MAX_PERIODS = 64;
570
+ function wholePeriod(stops) {
571
+ const out = stops.map((s) => ({ ...s }));
572
+ if (out.length && out[0].offset > 0) out.unshift({ ...out[0], offset: 0 });
573
+ const last = out[out.length - 1];
574
+ if (last && last.offset < 1) out.push({ ...last, offset: 1 });
575
+ return out;
576
+ }
577
+ function periodicVectorGradientStops(stops, lo, hi, mirrored) {
578
+ const period = wholePeriod(stops);
579
+ const back = reverseVectorGradient({ stops: period }).stops;
580
+ const n = Math.max(1, hi - lo);
581
+ const out = [];
582
+ for (let j = 0; j < n; j++) {
583
+ const one = mirrored(lo + j) ? back : period;
584
+ for (const s of one) out.push({ ...s, offset: (j + s.offset) / n });
585
+ }
586
+ return out;
587
+ }
588
+ function expandVectorGradientSpread(g, bounds, area) {
589
+ const m = vectorGradientMatrix(g, bounds);
590
+ const inv = invert(m);
591
+ const spread = g.spread ?? "pad";
592
+ const reflected = g.type === "reflected";
593
+ if (!inv || !reflected && spread === "pad") return null;
594
+ const odd = (k) => (k % 2 + 2) % 2 === 1;
595
+ const stops = expandVectorGradientStops(g);
596
+ const along2 = (a, b, mirrored2, capped2 = false) => ({
597
+ start: apply(m, { x: a, y: 0 }),
598
+ end: apply(m, { x: b, y: 0 }),
599
+ from: a,
600
+ to: b,
601
+ matrix: m,
602
+ stops: periodicVectorGradientStops(stops, a, b, mirrored2),
603
+ capped: capped2
604
+ });
605
+ if (reflected && spread === "pad") return along2(-1, 1, (k) => k < 0);
606
+ const us = boxCorners(area).map((c) => apply(inv, c).x);
607
+ let lo = Math.floor(Math.min(...us));
608
+ let hi = Math.ceil(Math.max(...us));
609
+ if (reflected) {
610
+ lo = Math.min(lo, -1);
611
+ hi = Math.max(hi, 1);
612
+ }
613
+ hi = Math.max(hi, lo + 1);
614
+ if (!reflected && lo >= 0 && hi <= 1) return null;
615
+ const capped = hi - lo > VECTOR_GRADIENT_MAX_PERIODS;
616
+ if (capped) {
617
+ lo = Math.max(lo, -VECTOR_GRADIENT_MAX_PERIODS / 2);
618
+ hi = lo + VECTOR_GRADIENT_MAX_PERIODS;
619
+ }
620
+ const mirrored = reflected ? (k) => {
621
+ const j = k >= 0 ? k : -k - 1;
622
+ const own = spread === "reflect" && odd(j);
623
+ return k < 0 ? !own : own;
624
+ } : (k) => spread === "reflect" && odd(k);
625
+ return along2(lo, hi, mirrored, capped);
626
+ }
627
+ var boxCorners = (b) => [
628
+ { x: b.x, y: b.y },
629
+ { x: b.x + b.width, y: b.y },
630
+ { x: b.x + b.width, y: b.y + b.height },
631
+ { x: b.x, y: b.y + b.height }
632
+ ];
633
+ var toSvgStops = (stops) => stops.map((s) => ({ offset: `${fmt(s.offset * 100, 3)}%`, stopColor: s.color, ...s.opacity !== void 0 && s.opacity !== 1 ? { stopOpacity: Math.round(s.opacity * 1e4) / 1e4 } : {} }));
634
+ var polygonD = (pts, precision) => `M${pts.map((p) => `${fmt(p.x, precision)} ${fmt(p.y, precision)}`).join("L")}Z`;
635
+ var padBounds = (b, pad) => ({ x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad });
636
+ var vectorGradientArea = (bounds) => padBounds(bounds ?? { x: 0, y: 0, width: 1, height: 1 }, 1);
637
+ var transformAttr = (m) => isIdentity(m) ? {} : { gradientTransform: fmtMatrix(m) };
638
+ function minScale(m) {
639
+ const [a, b, c, d] = m;
640
+ const p = a * a + b * b;
641
+ const q = c * c + d * d;
642
+ const r = a * c + b * d;
643
+ return Math.sqrt(Math.max(0, (p + q - Math.sqrt((p - q) ** 2 + 4 * r * r)) / 2));
644
+ }
645
+ function patternAttrs(area) {
646
+ const [x, y, w, h] = [area.x, area.y, Math.max(area.width, 1e-3), Math.max(area.height, 1e-3)].map((n) => fmt(n, 3));
647
+ return { patternUnits: "userSpaceOnUse", x, y, width: w, height: h, viewBox: `${x} ${y} ${w} ${h}` };
648
+ }
649
+ function patternDef(id, area, gradients, tiles) {
650
+ return {
651
+ id,
652
+ type: "pattern",
653
+ attrs: patternAttrs(area),
654
+ gradients,
655
+ tiles
656
+ };
657
+ }
658
+ function vectorGradientSvgDefs(g, id, options = {}) {
659
+ const precision = options.precision ?? 3;
660
+ const bounds = options.bounds ?? null;
661
+ const area = options.area ?? vectorGradientArea(bounds);
662
+ const spread = g.spread && VECTOR_GRADIENT_SPREADS.includes(g.spread) ? g.spread : "pad";
663
+ const stops = expandVectorGradientStops(g);
664
+ const p = vectorGradientPoints(g);
665
+ const unitsM = vectorGradientUnitsMatrix(g, bounds);
666
+ const frameM = mul(framePointsMatrix(p), unitsM);
667
+ const spreadAttr = (s) => s === "pad" ? {} : { spreadMethod: s };
668
+ const base = { gradientUnits: "userSpaceOnUse" };
669
+ switch (g.type) {
670
+ case "linear":
671
+ case "reflected": {
672
+ if (g.type === "reflected" && spread !== "reflect") {
673
+ const ex = expandVectorGradientSpread(g, bounds, area);
674
+ const [a, b] = ex ? [ex.from, ex.to] : [-1, 1];
675
+ const st = ex?.stops ?? periodicVectorGradientStops(stops, -1, 1, (k) => k < 0);
676
+ return [{ id, type: "linear", attrs: { ...base, x1: a, y1: 0, x2: b, y2: 0, gradientTransform: fmtMatrix(frameM) }, stops: toSvgStops(st) }];
677
+ }
678
+ const u = sub(p.end, p.start);
679
+ const w = sub(p.width, p.start);
680
+ const square = Math.abs(dot(u, w)) <= 1e-9 * Math.max(dot(u, u), dot(w, w), 1e-18);
681
+ const coords = square ? { x1: fmt(p.start.x, 6), y1: fmt(p.start.y, 6), x2: fmt(p.end.x, 6), y2: fmt(p.end.y, 6), ...transformAttr(unitsM) } : { x1: 0, y1: 0, x2: 1, y2: 0, gradientTransform: fmtMatrix(frameM) };
682
+ return [{ id, type: "linear", attrs: { ...base, ...coords, ...spreadAttr(spread) }, stops: toSvgStops(stops) }];
683
+ }
684
+ case "radial": {
685
+ const f = unitFocal(g);
686
+ const attrs = { ...base, cx: 0, cy: 0, r: 1, gradientTransform: fmtMatrix(frameM), ...spreadAttr(spread) };
687
+ if (f && (Math.abs(f.f.x) > 1e-9 || Math.abs(f.f.y) > 1e-9)) {
688
+ attrs.fx = fmt(f.f.x, 6);
689
+ attrs.fy = fmt(f.f.y, 6);
690
+ }
691
+ if (f && f.fr > 0) attrs.fr = fmt(f.fr, 6);
692
+ return [{ id, type: "radial", attrs, stops: toSvgStops(stops) }];
693
+ }
694
+ case "diamond": {
695
+ const inv = invert(frameM);
696
+ if (!inv) return [patternDef(id, area, [], [{ d: polygonD(boxCorners(area), precision), fill: stops[stops.length - 1]?.color ?? "none" }])];
697
+ const reach = 2 * Math.max(...boxCorners(area).map((c) => {
698
+ const q = apply(inv, c);
699
+ return Math.abs(q.x) + Math.abs(q.y);
700
+ })) + 1;
701
+ const delta = Math.min(0.05 * reach, 0.75 / Math.max(minScale(frameM), 1e-9));
702
+ const gradients = [];
703
+ const tiles = [];
704
+ [[1, 1], [-1, 1], [-1, -1], [1, -1]].forEach(([sx, sy], k) => {
705
+ const gid = `${id}-q${k}`;
706
+ gradients.push({ id: gid, type: "linear", attrs: { ...base, x1: 0, y1: 0, x2: sx / 2, y2: sy / 2, gradientTransform: fmtMatrix(frameM), ...spreadAttr(spread) }, stops: toSvgStops(stops) });
707
+ const quad = [{ x: -delta * sx, y: -delta * sy }, { x: reach * sx, y: -delta * sy }, { x: -delta * sx, y: reach * sy }].map((q) => apply(frameM, q));
708
+ tiles.push({ d: polygonD(quad, precision), fill: `url(#${gid})` });
709
+ });
710
+ return [patternDef(id, area, gradients, tiles)];
711
+ }
712
+ case "conic":
713
+ return [conicPattern(id, frameM, stops, area, options.wedges ?? 72, precision)];
714
+ }
715
+ }
716
+ function conicPattern(id, frameM, stops, area, wedges, precision) {
717
+ const inv = invert(frameM);
718
+ const n = Math.max(8, Math.min(720, Math.round(wedges)));
719
+ const half = Math.PI / n;
720
+ if (!inv || !stops.length) return patternDef(id, area, [], [{ d: polygonD(boxCorners(area), precision), fill: stops[0]?.color ?? "none" }]);
721
+ const reach = (Math.max(...boxCorners(area).map((c) => Math.hypot(apply(inv, c).x, apply(inv, c).y))) + 1) / Math.cos(half);
722
+ const overlap = Math.min(half, 2e-3);
723
+ const gradients = [];
724
+ const tiles = [];
725
+ for (let k = 0; k < n; k++) {
726
+ const a0 = 2 * Math.PI * k / n;
727
+ const a1 = 2 * Math.PI * (k + 1) / n;
728
+ const mid = (a0 + a1) / 2;
729
+ const tau = { x: -Math.sin(mid), y: Math.cos(mid) };
730
+ const s = Math.sin(half);
731
+ const at2 = (angle) => (Math.sin(angle - mid) / s + 1) / 2;
732
+ const t0 = k / n;
733
+ const t1 = (k + 1) / n;
734
+ const own = [stopOf(0, samplePlainStops(stops, t0))];
735
+ for (const st of stops) if (st.offset > t0 && st.offset < t1) own.push({ ...st, offset: at2(st.offset * 2 * Math.PI) });
736
+ own.push(stopOf(1, samplePlainStops(stops, t1)));
737
+ const gid = `${id}-w${k}`;
738
+ gradients.push({
739
+ id: gid,
740
+ type: "linear",
741
+ attrs: { gradientUnits: "userSpaceOnUse", x1: fmt(-s * tau.x, 6), y1: fmt(-s * tau.y, 6), x2: fmt(s * tau.x, 6), y2: fmt(s * tau.y, 6), gradientTransform: fmtMatrix(frameM) },
742
+ stops: toSvgStops(own)
743
+ });
744
+ const from = a0 - (k === 0 ? 0 : overlap);
745
+ const pts = [{ x: 0, y: 0 }, { x: reach * Math.cos(from), y: reach * Math.sin(from) }, { x: reach * Math.cos(a1), y: reach * Math.sin(a1) }].map((q) => apply(frameM, q));
746
+ tiles.push({ d: polygonD(pts, precision), fill: `url(#${gid})` });
747
+ }
748
+ return patternDef(id, area, gradients, tiles);
749
+ }
750
+ var num = (v2, fallback = 0) => {
751
+ if (v2 === void 0) return fallback;
752
+ const n = typeof v2 === "number" ? v2 : parseFloat(v2);
753
+ return Number.isFinite(n) ? n : fallback;
754
+ };
755
+ var parseMatrixAttr = (v2) => {
756
+ const m = typeof v2 === "string" ? /^matrix\(([^)]*)\)$/.exec(v2.trim()) : null;
757
+ if (!m) return IDENTITY;
758
+ const n = m[1].split(/[\s,]+/).map(Number);
759
+ return n.length === 6 && n.every(Number.isFinite) ? n : IDENTITY;
760
+ };
761
+ var plainOf = (stops) => stops.map((s) => ({ offset: parseFloat(s.offset) / (s.offset.endsWith("%") ? 100 : 1), color: s.stopColor, ...s.stopOpacity !== void 0 ? { opacity: s.stopOpacity } : {} }));
762
+ function inTile(d, p) {
763
+ const n = d.match(/-?[\d.]+(?:e-?\d+)?/gi)?.map(Number) ?? [];
764
+ let inside = false;
765
+ for (let i = 0, j = n.length - 2; i + 1 < n.length; j = i, i += 2) {
766
+ const [xi, yi, xj, yj] = [n[i], n[i + 1], n[j], n[j + 1]];
767
+ if (yi > p.y !== yj > p.y && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi) inside = !inside;
768
+ }
769
+ return inside;
770
+ }
771
+ function sampleVectorPaintDefAt(defs, id, p) {
772
+ const all = /* @__PURE__ */ new Map();
773
+ for (const d of defs) {
774
+ all.set(d.id, d);
775
+ if (d.type === "pattern") for (const gd of d.gradients) all.set(gd.id, gd);
776
+ }
777
+ const paint = (ref, q) => {
778
+ const url = /^url\(#(.+)\)$/.exec(ref);
779
+ if (!url) return parseVectorColor(ref);
780
+ const def = all.get(url[1]);
781
+ if (!def) return null;
782
+ if (def.type === "pattern") {
783
+ for (let i = def.tiles.length - 1; i >= 0; i--) if (inTile(def.tiles[i].d, q)) return paint(def.tiles[i].fill, q);
784
+ return null;
785
+ }
786
+ const inv = invert(parseMatrixAttr(def.attrs.gradientTransform)) ?? IDENTITY;
787
+ const local = apply(inv, q);
788
+ const spread = def.attrs.spreadMethod ?? "pad";
789
+ let t;
790
+ if (def.type === "radial") {
791
+ const r = num(def.attrs.r, 0.5);
792
+ const c = { x: num(def.attrs.cx), y: num(def.attrs.cy) };
793
+ const f = { x: num(def.attrs.fx, c.x), y: num(def.attrs.fy, c.y) };
794
+ const u = { x: (local.x - c.x) / r, y: (local.y - c.y) / r };
795
+ t = vectorGradientFocalParam(u, { x: (f.x - c.x) / r, y: (f.y - c.y) / r }, num(def.attrs.fr) / r);
796
+ } else {
797
+ const a = { x: num(def.attrs.x1), y: num(def.attrs.y1) };
798
+ const b = { x: num(def.attrs.x2, 1), y: num(def.attrs.y2) };
799
+ const v2 = sub(b, a);
800
+ const l2 = dot(v2, v2);
801
+ t = l2 > 1e-18 ? dot(sub(local, a), v2) / l2 : 0;
802
+ }
803
+ return samplePlainStops(plainOf(def.stops), applyVectorGradientSpread(t, spread));
804
+ };
805
+ return paint(`url(#${id})`, p);
806
+ }
807
+ var readPoint = (v2) => finitePoint(v2) ? { x: v2.x, y: v2.y } : void 0;
808
+ function sanitizeVectorGradient(input) {
809
+ if (!input || typeof input !== "object") return null;
810
+ const r = input;
811
+ if (!VECTOR_GRADIENT_TYPES.includes(r.type) || !Array.isArray(r.stops)) return null;
812
+ const stops = [];
813
+ for (const s of r.stops) {
814
+ if (!s || typeof s !== "object") continue;
815
+ const x = s;
816
+ if (typeof x.color !== "string" || typeof x.offset !== "number" || !Number.isFinite(x.offset)) continue;
817
+ const stop = { offset: clamp(x.offset, 0, 1), color: x.color.slice(0, 100) };
818
+ if (typeof x.opacity === "number" && Number.isFinite(x.opacity) && x.opacity !== 1) stop.opacity = clamp(x.opacity, 0, 1);
819
+ if (typeof x.midpoint === "number" && Number.isFinite(x.midpoint) && Math.abs(x.midpoint - 0.5) > 1e-6) stop.midpoint = clamp(x.midpoint, 0.05, 0.95);
820
+ stops.push(stop);
821
+ }
822
+ if (!stops.length) return null;
823
+ const g = { type: r.type, stops };
824
+ for (const k of ["angle", "cx", "cy", "r"]) if (typeof r[k] === "number" && Number.isFinite(r[k])) g[k] = r[k];
825
+ const start = readPoint(r.start);
826
+ const end = readPoint(r.end);
827
+ if (start && end) {
828
+ g.start = start;
829
+ g.end = end;
830
+ const width = readPoint(r.width);
831
+ if (width) g.width = width;
832
+ if (r.units === "userSpaceOnUse" || r.units === "objectBoundingBox") g.units = r.units;
833
+ }
834
+ const focal = readPoint(r.focal);
835
+ if (focal && g.type === "radial") g.focal = focal;
836
+ if (typeof r.focalRadius === "number" && r.focalRadius > 0 && g.type === "radial") g.focalRadius = clamp(r.focalRadius, 0, 0.99);
837
+ if (VECTOR_GRADIENT_SPREADS.includes(r.spread) && r.spread !== "pad") g.spread = r.spread;
838
+ if (VECTOR_GRADIENT_INTERPOLATIONS.includes(r.interpolation) && r.interpolation !== "srgb") g.interpolation = r.interpolation;
839
+ if (typeof r.smoothness === "number" && r.smoothness > 0) g.smoothness = clamp(r.smoothness, 0, 1);
840
+ if (VECTOR_STROKE_GRADIENT_MODES.includes(r.strokeMode) && r.strokeMode !== "within") g.strokeMode = r.strokeMode;
841
+ return g;
842
+ }
843
+ function convertVectorGradient(g, type) {
844
+ if (g.type === type) return g;
845
+ const next = { ...g, type };
846
+ if (type !== "radial") {
847
+ delete next.focal;
848
+ delete next.focalRadius;
849
+ }
850
+ if (!hasExplicitGradientGeometry(g)) {
851
+ if (isCentredVectorGradient(type) !== isCentredVectorGradient(g.type)) {
852
+ delete next.angle;
853
+ delete next.cx;
854
+ delete next.cy;
855
+ delete next.r;
856
+ }
857
+ }
858
+ return next;
859
+ }
860
+ function defaultVectorGradient(type = "linear", from) {
861
+ if (isVectorGradient(from)) return convertVectorGradient(from, type);
862
+ const colour = typeof from === "string" && parseVectorColor(from) ? from : "#000000";
863
+ return { type, stops: [{ offset: 0, color: colour }, { offset: 1, color: "#ffffff" }] };
864
+ }
865
+ function applyVectorGradientPatch(g, patch) {
866
+ const { aspect, reverse: reverse2, type, ...fields } = patch;
867
+ let next = type && type !== g.type ? convertVectorGradient(g, type) : { ...g };
868
+ const record = next;
869
+ for (const [k, v2] of Object.entries(fields)) {
870
+ if (v2 === void 0) continue;
871
+ if (v2 === null) delete record[k];
872
+ else record[k] = v2;
873
+ }
874
+ if ((fields.start || fields.end) && !hasExplicitGradientGeometry(next)) {
875
+ const p = vectorGradientPoints({ ...g, start: void 0, end: void 0 });
876
+ next.start = next.start ?? p.start;
877
+ next.end = next.end ?? p.end;
878
+ }
879
+ if (aspect !== void 0 && Number.isFinite(aspect)) {
880
+ const e = explicitVectorGradient(next);
881
+ const u = sub(e.end, e.start);
882
+ const a = Math.abs(aspect) < 0.01 ? aspect < 0 ? -0.01 : 0.01 : aspect;
883
+ next = { ...e, width: add(e.start, scale(perp(u), a)) };
884
+ }
885
+ if (reverse2) next = reverseVectorGradient(next);
886
+ const clean2 = sanitizeVectorGradient(next);
887
+ if (!clean2) return null;
888
+ clean2.stops = sortedVectorGradientStops(clean2);
889
+ return clean2;
890
+ }
891
+
892
+ // src/vector/corners.ts
893
+ var EPS = 1e-9;
894
+ var MIN_SINE = 1e-6;
895
+ var sub2 = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
896
+ var add2 = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
897
+ var mul2 = (a, k) => ({ x: a.x * k, y: a.y * k });
898
+ var dot2 = (a, b) => a.x * b.x + a.y * b.y;
87
899
  var cross = (a, b) => a.x * b.y - a.y * b.x;
88
900
  var len = (a) => Math.hypot(a.x, a.y);
89
901
  var pt = (p) => [p.x, p.y];
@@ -102,9 +914,9 @@ var Side = class {
102
914
  direction() {
103
915
  const p0 = vec(this.curve[0]);
104
916
  for (const q of this.curve.slice(1)) {
105
- const d = sub(vec(q), p0);
917
+ const d = sub2(vec(q), p0);
106
918
  const l = len(d);
107
- if (l > EPS) return mul(d, 1 / l);
919
+ if (l > EPS) return mul2(d, 1 / l);
108
920
  }
109
921
  return null;
110
922
  }
@@ -112,12 +924,12 @@ var Side = class {
112
924
  at(s, u) {
113
925
  if (this.straight) {
114
926
  const t2 = this.length > EPS ? Math.min(1, Math.max(0, s / this.length)) : 0;
115
- return { t: t2, p: add(vec(this.curve[0]), mul(u, s)), dir: u };
927
+ return { t: t2, p: add2(vec(this.curve[0]), mul2(u, s)), dir: u };
116
928
  }
117
929
  const t = segmentParamAtLength(this.curve, s, this.length);
118
930
  const d = vec(segmentDerivative(this.curve, t));
119
931
  const l = len(d);
120
- return { t, p: vec(segmentPoint(this.curve, t)), dir: l > EPS ? mul(d, 1 / l) : u };
932
+ return { t, p: vec(segmentPoint(this.curve, t)), dir: l > EPS ? mul2(d, 1 / l) : u };
121
933
  }
122
934
  };
123
935
  function cornerOf(sp, s, i) {
@@ -136,7 +948,7 @@ function cornerOf(sp, s, i) {
136
948
  const lu = sideU.length;
137
949
  const lv = sideV.length;
138
950
  if (!(lu > EPS && lv > EPS) || !Number.isFinite(lu) || !Number.isFinite(lv)) return null;
139
- const cos = Math.max(-1, Math.min(1, dot(u, v2)));
951
+ const cos = Math.max(-1, Math.min(1, dot2(u, v2)));
140
952
  const sin = Math.abs(cross(u, v2));
141
953
  if (sin <= MIN_SINE) return null;
142
954
  return { subpath: s, index: i, at: { x: a.x, y: a.y }, u, v: v2, lu, lv, cos, sin, angle: Math.atan2(sin, cos) * 180 / Math.PI, maxRadius: Math.min(lu, lv) / 2 * sin / (1 + cos), sideU, sideV };
@@ -158,8 +970,8 @@ function vectorCorners(subpaths) {
158
970
  }
159
971
  var cornerSetback = (c, r) => r * (1 + c.cos) / c.sin;
160
972
  var fitCornerRadius = (c, radius) => Number.isFinite(radius) ? Math.min(Math.max(0, radius), c.maxRadius) : 0;
161
- var cornerBisectorPoint = (c, k) => add(c.at, mul(add(c.u, c.v), k));
162
- var cornerRadiusChange = (c, d) => dot(d, add(c.u, c.v)) * c.sin / (2 + 2 * c.cos);
973
+ var cornerBisectorPoint = (c, k) => add2(c.at, mul2(add2(c.u, c.v), k));
974
+ var cornerRadiusChange = (c, d) => dot2(d, add2(c.u, c.v)) * c.sin / (2 + 2 * c.cos);
163
975
  function arcHandle(cos) {
164
976
  const c = Math.max(-1, Math.min(1, cos));
165
977
  const sinHalf = Math.sqrt(Math.max(0, (1 - c) / 2));
@@ -167,9 +979,9 @@ function arcHandle(cos) {
167
979
  return 4 / 3 * sinHalf / (1 + cosHalf);
168
980
  }
169
981
  function squareTo(a, b) {
170
- const w = sub(b, mul(a, dot(a, b)));
982
+ const w = sub2(b, mul2(a, dot2(a, b)));
171
983
  const l = len(w);
172
- return l > EPS ? mul(w, 1 / l) : { x: 0, y: 0 };
984
+ return l > EPS ? mul2(w, 1 / l) : { x: 0, y: 0 };
173
985
  }
174
986
  function cutCorner(c, radius, kind) {
175
987
  const s = Math.min(cornerSetback(c, radius), Math.min(c.lu, c.lv) / 2);
@@ -178,16 +990,16 @@ function cutCorner(c, radius, kind) {
178
990
  const b = c.sideV.at(s, c.v);
179
991
  if (kind === "chamfer") return { tu: a.t, tv: b.t, start: a.p, out: null, in: null, end: b.p };
180
992
  if (kind === "inverted") {
181
- const h = s * arcHandle(dot(a.dir, b.dir));
182
- return { tu: a.t, tv: b.t, start: a.p, out: add(a.p, mul(squareTo(a.dir, b.dir), h)), in: add(b.p, mul(squareTo(b.dir, a.dir), h)), end: b.p };
993
+ const h = s * arcHandle(dot2(a.dir, b.dir));
994
+ return { tu: a.t, tv: b.t, start: a.p, out: add2(a.p, mul2(squareTo(a.dir, b.dir), h)), in: add2(b.p, mul2(squareTo(b.dir, a.dir), h)), end: b.p };
183
995
  }
184
- const d1 = mul(a.dir, -1);
185
- const d2 = mul(b.dir, -1);
186
- const turn = Math.acos(Math.max(-1, Math.min(1, -dot(d1, d2))));
187
- const chord = len(sub(b.p, a.p));
996
+ const d1 = mul2(a.dir, -1);
997
+ const d2 = mul2(b.dir, -1);
998
+ const turn = Math.acos(Math.max(-1, Math.min(1, -dot2(d1, d2))));
999
+ const chord = len(sub2(b.p, a.p));
188
1000
  const k = turn < 1e-9 ? chord / 3 : 4 / 3 * Math.tan(turn / 4) * chord / (2 * Math.sin(turn / 2));
189
1001
  if (!Number.isFinite(k)) return null;
190
- return { tu: a.t, tv: b.t, start: a.p, out: add(a.p, mul(d1, k)), in: add(b.p, mul(d2, k)), end: b.p };
1002
+ return { tu: a.t, tv: b.t, start: a.p, out: add2(a.p, mul2(d1, k)), in: add2(b.p, mul2(d2, k)), end: b.p };
191
1003
  }
192
1004
  var radiusOf = (a) => {
193
1005
  const r = a.corner?.radius;
@@ -499,7 +1311,7 @@ function pressureWidthProfile(samples, options = {}) {
499
1311
  // src/vector/varstroke.ts
500
1312
  var SAMPLES_PER_SUBPATH = 128;
501
1313
  var MAX_DASHES = 5e3;
502
- var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];
1314
+ var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1]];
503
1315
  var along = (p, v2, k) => [p[0] + v2[0] * k, p[1] + v2[1] * k];
504
1316
  var lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
505
1317
  var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
@@ -590,11 +1402,11 @@ function arcPoints(c, a0, sweep, r0, r1, tol) {
590
1402
  return out;
591
1403
  }
592
1404
  function crossing(a0, a1, b0, b1) {
593
- const r = sub2(a1, a0);
594
- const q = sub2(b1, b0);
1405
+ const r = sub3(a1, a0);
1406
+ const q = sub3(b1, b0);
595
1407
  const den = r[0] * q[1] - r[1] * q[0];
596
1408
  if (Math.abs(den) < 1e-12) return null;
597
- const w = sub2(b0, a0);
1409
+ const w = sub3(b0, a0);
598
1410
  const t = (w[0] * q[1] - w[1] * q[0]) / den;
599
1411
  const u = (w[0] * r[1] - w[1] * r[0]) / den;
600
1412
  return t > 1e-9 && t <= 1 && u >= 0 && u < 1 - 1e-9 ? [a0[0] + r[0] * t, a0[1] + r[1] * t] : null;
@@ -638,7 +1450,7 @@ function variableStrokeRings(path, options) {
638
1450
  const closed = whole && poly.closed;
639
1451
  const m = run.length - 1;
640
1452
  const dirs = [];
641
- for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
1453
+ for (let i = 0; i < m; i++) dirs.push(unitOf(sub3(run[i + 1].p, run[i].p)));
642
1454
  const normals = dirs.map(leftNormal);
643
1455
  const w = run.map((q) => sides2(q.s));
644
1456
  const side = (sigma) => {
@@ -657,12 +1469,12 @@ function variableStrokeRings(path, options) {
657
1469
  const d0 = dirs[i];
658
1470
  const d1 = dirs[j];
659
1471
  const cross2 = d0[0] * d1[1] - d0[1] * d1[0];
660
- const dot2 = d0[0] * d1[0] + d0[1] * d1[1];
1472
+ const dot3 = d0[0] * d1[0] + d0[1] * d1[1];
661
1473
  const r0 = w[i + 1][k];
662
1474
  const r1 = w[j][k];
663
1475
  const e = E(i);
664
1476
  const st = S(j);
665
- if (Math.abs(cross2) < 1e-12 && dot2 > 0) {
1477
+ if (Math.abs(cross2) < 1e-12 && dot3 > 0) {
666
1478
  push(out, e);
667
1479
  if (!seam) push(out, st);
668
1480
  continue;
@@ -680,7 +1492,7 @@ function variableStrokeRings(path, options) {
680
1492
  continue;
681
1493
  }
682
1494
  push(out, e);
683
- const theta = uturn ? Math.PI : Math.atan2(cross2, dot2);
1495
+ const theta = uturn ? Math.PI : Math.atan2(cross2, dot3);
684
1496
  const r = (r0 + r1) / 2;
685
1497
  const kind = run[i + 1].corner || seam && poly.corner[0] ? join : "round";
686
1498
  if (r > 1e-9) {
@@ -700,10 +1512,10 @@ function variableStrokeRings(path, options) {
700
1512
  if (closed && out.length > 1 && dist(out[0], out[out.length - 1]) <= 1e-12) out.pop();
701
1513
  return out;
702
1514
  };
703
- const left = side(1);
1515
+ const left2 = side(1);
704
1516
  const right = side(-1);
705
1517
  if (closed) {
706
- rings.push(left, right.reverse());
1518
+ rings.push(left2, right.reverse());
707
1519
  continue;
708
1520
  }
709
1521
  const capPoints = (atStart) => {
@@ -725,7 +1537,7 @@ function variableStrokeRings(path, options) {
725
1537
  return atStart ? [along(R, o, ext), along(L, o, ext)] : [along(L, o, ext), along(R, o, ext)];
726
1538
  };
727
1539
  const ring = [];
728
- for (const q of [...left, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
1540
+ for (const q of [...left2, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
729
1541
  if (ring.length > 2) rings.push(ring);
730
1542
  }
731
1543
  }
@@ -841,8 +1653,8 @@ function vectorStrokeLineArea(parts, style, tolerance) {
841
1653
  }
842
1654
  var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
843
1655
  function arrowheadWeight(style, end) {
844
- const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
845
- const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
1656
+ const scale3 = end === "start" ? style.startArrowScale : style.endArrowScale;
1657
+ const s = typeof scale3 === "number" && scale3 > 0 && Number.isFinite(scale3) ? scale3 : 1;
846
1658
  const width = style.strokeWidth ?? 1;
847
1659
  const profile = strokeWidthProfileOf(style);
848
1660
  const at2 = profile ? widthProfileAt(profile, end === "start" ? 0 : 1) : null;
@@ -1082,7 +1894,7 @@ function tangentAt(s, t) {
1082
1894
  const d = segmentDerivative(s, t);
1083
1895
  return unit2(d[0], d[1]) ?? startTangent(s);
1084
1896
  }
1085
- var perp = (v2) => [-v2[1], v2[0]];
1897
+ var perp2 = (v2) => [-v2[1], v2[0]];
1086
1898
  function effectPathLength(path) {
1087
1899
  let total = 0;
1088
1900
  for (const s of subsOf(path)) for (const seg of s.segs) total += segmentLength(seg);
@@ -1252,9 +2064,9 @@ function twistPaths(paths, angle, center) {
1252
2064
  };
1253
2065
  return paths.map((p) => {
1254
2066
  const out = [];
1255
- for (const sub3 of subsOf(p)) {
2067
+ for (const sub5 of subsOf(p)) {
1256
2068
  const segs = [];
1257
- for (const seg of sub3.segs) {
2069
+ for (const seg of sub5.segs) {
1258
2070
  const cubic = seg.length === 2 ? [seg[0], seg[0], seg[1], seg[1]] : seg;
1259
2071
  const len2 = segmentLength(seg);
1260
2072
  const m = Math.min(64, Math.max(1, Math.ceil(Math.abs(a) * len2 / rmax / 0.15 + len2 / 50)));
@@ -1267,13 +2079,13 @@ function twistPaths(paths, angle, center) {
1267
2079
  segs.push([p0, [p0[0] + h1[0], p0[1] + h1[1]], [p3[0] + h2[0], p3[1] + h2[1]], p3]);
1268
2080
  }
1269
2081
  }
1270
- out.push({ segs, closed: sub3.closed });
2082
+ out.push({ segs, closed: sub5.closed });
1271
2083
  }
1272
2084
  return subsToPath(out);
1273
2085
  });
1274
2086
  }
1275
- function resample(sub3, m) {
1276
- const segs = sub3.segs;
2087
+ function resample(sub5, m) {
2088
+ const segs = sub5.segs;
1277
2089
  const n = segs.length;
1278
2090
  const lens = segs.map((s) => segmentLength(s));
1279
2091
  const out = [];
@@ -1282,7 +2094,7 @@ function resample(sub3, m) {
1282
2094
  for (let k = 0; k < m; k++) {
1283
2095
  if (k === 0) {
1284
2096
  const tout = startTangent(segs[j]);
1285
- const prev = sub3.closed || j > 0 ? (j + n - 1) % n : null;
2097
+ const prev = sub5.closed || j > 0 ? (j + n - 1) % n : null;
1286
2098
  let tan = tout;
1287
2099
  if (prev !== null) {
1288
2100
  const tin = endTangent(segs[prev]);
@@ -1295,7 +2107,7 @@ function resample(sub3, m) {
1295
2107
  }
1296
2108
  }
1297
2109
  }
1298
- if (!sub3.closed) {
2110
+ if (!sub5.closed) {
1299
2111
  const last = segs[n - 1];
1300
2112
  out.push({ p: last[last.length - 1], tan: endTangent(last), lenPrev: lens[n - 1] / m, lenNext: 0 });
1301
2113
  }
@@ -1323,14 +2135,14 @@ function zigzagPaths(paths, size, ridges, smooth = false) {
1323
2135
  const m = Math.min(1e3, Math.max(0, Math.round(Number.isFinite(ridges) ? ridges : 0))) + 1;
1324
2136
  return paths.map((p) => {
1325
2137
  const out = [];
1326
- for (const sub3 of subsOf(p)) {
1327
- const s = resample(sub3, m);
2138
+ for (const sub5 of subsOf(p)) {
2139
+ const s = resample(sub5, m);
1328
2140
  const disp = s.map((x, i) => {
1329
- const n = perp(x.tan);
2141
+ const n = perp2(x.tan);
1330
2142
  const d = i % 2 === 0 ? size : -size;
1331
2143
  return [n[0] * d, n[1] * d];
1332
2144
  });
1333
- const e = emit(s, disp, sub3.closed, smooth, 0.36);
2145
+ const e = emit(s, disp, sub5.closed, smooth, 0.36);
1334
2146
  if (e) out.push(e);
1335
2147
  }
1336
2148
  return subsToPath(out);
@@ -1369,8 +2181,8 @@ function wigglePaths(paths, w) {
1369
2181
  let idx = 0;
1370
2182
  return paths.map((p) => {
1371
2183
  const out = [];
1372
- for (const sub3 of subsOf(p)) {
1373
- const s = resample(sub3, m);
2184
+ for (const sub5 of subsOf(p)) {
2185
+ const s = resample(sub5, m);
1374
2186
  const disp = s.map(() => {
1375
2187
  const i = BigInt(idx);
1376
2188
  const ox = noise(seed, i * 2n, t + idx * 0.37);
@@ -1378,7 +2190,7 @@ function wigglePaths(paths, w) {
1378
2190
  idx++;
1379
2191
  return [(sx * corr + ox * (1 - corr)) * w.size, (sy * corr + oy * (1 - corr)) * w.size];
1380
2192
  });
1381
- const e = emit(s, disp, sub3.closed, w.smooth, 1 / 3);
2193
+ const e = emit(s, disp, sub5.closed, w.smooth, 1 / 3);
1382
2194
  if (e) out.push(e);
1383
2195
  }
1384
2196
  return subsToPath(out);
@@ -1691,7 +2503,7 @@ var compareOrder = (a, b) => {
1691
2503
  for (let i = 0; i < n; i++) if (a[i] !== b[i]) return a[i] - b[i];
1692
2504
  return a.length - b.length;
1693
2505
  };
1694
- var add2 = (a, b) => ({ x: a.x + (b?.x ?? 0), y: a.y + (b?.y ?? 0) });
2506
+ var add3 = (a, b) => ({ x: a.x + (b?.x ?? 0), y: a.y + (b?.y ?? 0) });
1695
2507
  var mapPaths = (shapes, fn) => {
1696
2508
  const out = fn(shapes.map((s) => s.path));
1697
2509
  return shapes.map((s, i) => ({ ...s, path: out[i] ?? [] }));
@@ -1713,7 +2525,7 @@ function applyVectorEffects(shapes, effects) {
1713
2525
  list = mapPaths(list, (p) => puckerBloatPaths(p, e.params.amount));
1714
2526
  break;
1715
2527
  case "twist":
1716
- list = mapPaths(list, (p) => twistPaths(p, e.params.angle, add2(pathsCenter(p), e.params.center)));
2528
+ list = mapPaths(list, (p) => twistPaths(p, e.params.angle, add3(pathsCenter(p), e.params.center)));
1717
2529
  break;
1718
2530
  case "zigzag":
1719
2531
  list = mapPaths(list, (p) => zigzagPaths(p, e.params.size, e.params.ridges, e.params.smooth));
@@ -1736,7 +2548,7 @@ function applyVectorEffects(shapes, effects) {
1736
2548
  const copies = repeatPaths(
1737
2549
  list.map((s) => s.path),
1738
2550
  r,
1739
- add2(pathsCenter(list.map((s) => s.path)), r.anchor)
2551
+ add3(pathsCenter(list.map((s) => s.path)), r.anchor)
1740
2552
  );
1741
2553
  const n = copies.length;
1742
2554
  const next = [];
@@ -1982,8 +2794,8 @@ function inferAnchorType(a) {
1982
2794
  const li = Math.hypot(ix, iy);
1983
2795
  const lo = Math.hypot(ox, oy);
1984
2796
  const cross2 = (ix * oy - iy * ox) / (li * lo);
1985
- const dot2 = (ix * ox + iy * oy) / (li * lo);
1986
- if (Math.abs(cross2) > 1e-3 || dot2 > 0) return "corner";
2797
+ const dot3 = (ix * ox + iy * oy) / (li * lo);
2798
+ if (Math.abs(cross2) > 1e-3 || dot3 > 0) return "corner";
1987
2799
  return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
1988
2800
  }
1989
2801
  function bezierToSubpaths(input) {
@@ -2232,6 +3044,9 @@ function bakeIntoGeometry(node, m) {
2232
3044
  return {
2233
3045
  ...path,
2234
3046
  ...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
3047
+ // Gradients in the node's own space move with its geometry.
3048
+ ...isVectorGradient(path.fill) ? { fill: transformVectorPaint(path.fill, m) } : {},
3049
+ ...isVectorGradient(path.stroke) ? { stroke: transformVectorPaint(path.stroke, m) } : {},
2235
3050
  subpaths: path.subpaths.map((sp) => ({
2236
3051
  closed: sp.closed,
2237
3052
  anchors: sp.anchors.map((a) => mapAnchor(a, m))
@@ -2334,12 +3149,12 @@ function insertAnchor(sp, segment, t) {
2334
3149
  const ia = segment;
2335
3150
  const ib = (segment + 1) % n;
2336
3151
  const seg = subpathSegment(sp, segment);
2337
- const [left, right] = splitSegment(seg, t);
2338
- const mid = left[left.length - 1];
3152
+ const [left2, right] = splitSegment(seg, t);
3153
+ const mid = left2[left2.length - 1];
2339
3154
  const anchor = { x: mid[0], y: mid[1] };
2340
3155
  if (seg.length === 4) {
2341
- anchors[ia].out = { x: left[1][0], y: left[1][1] };
2342
- anchor.in = { x: left[2][0], y: left[2][1] };
3156
+ anchors[ia].out = { x: left2[1][0], y: left2[1][1] };
3157
+ anchor.in = { x: left2[2][0], y: left2[2][1] };
2343
3158
  anchor.out = { x: right[1][0], y: right[1][1] };
2344
3159
  anchor.type = "smooth";
2345
3160
  anchors[ib].in = { x: right[2][0], y: right[2][1] };
@@ -2424,6 +3239,166 @@ function roundSubpaths(subpaths, precision = 3) {
2424
3239
  }));
2425
3240
  }
2426
3241
 
3242
+ // src/vector/gradientStroke.ts
3243
+ var SHARP = 1.25;
3244
+ var TOLERANCE = 0.2;
3245
+ var OVERLAP = 0.5;
3246
+ var fmt2 = (n, digits = 3) => {
3247
+ const k = 10 ** digits;
3248
+ const v2 = Math.round(n * k) / k;
3249
+ return Object.is(v2, -0) ? "0" : String(v2);
3250
+ };
3251
+ var toSvgStops2 = (stops) => stops.map((s) => ({ offset: `${fmt2(s.offset * 100)}%`, stopColor: s.color, ...s.opacity !== void 0 && s.opacity !== 1 ? { stopOpacity: Math.round(s.opacity * 1e4) / 1e4 } : {} }));
3252
+ var polygonD2 = (pts, precision) => `M${pts.map((p) => `${fmt2(p.x, precision)} ${fmt2(p.y, precision)}`).join("L")}Z`;
3253
+ var add4 = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
3254
+ var sub4 = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
3255
+ var scale2 = (a, k) => ({ x: a.x * k, y: a.y * k });
3256
+ var left = (d) => ({ x: d.y, y: -d.x });
3257
+ var unit3 = (v2) => {
3258
+ const l = Math.hypot(v2.x, v2.y);
3259
+ return l > 1e-12 ? { x: v2.x / l, y: v2.y / l } : { x: 1, y: 0 };
3260
+ };
3261
+ function flattenedSubpaths(node) {
3262
+ const out = [];
3263
+ for (const contour of pathToContours(subpathsToBezier(node.subpaths))) {
3264
+ const segs = closeContour(contour);
3265
+ if (!segs.length) continue;
3266
+ const ring = flattenChain(segs, TOLERANCE);
3267
+ const pts = ring.map(([x, y]) => ({ x, y })).filter((p, i, a) => i === 0 || Math.hypot(p.x - a[i - 1].x, p.y - a[i - 1].y) > 1e-9);
3268
+ if (contour.closed && pts.length > 2 && Math.hypot(pts[0].x - pts[pts.length - 1].x, pts[0].y - pts[pts.length - 1].y) < 1e-9) pts.pop();
3269
+ if (pts.length >= 2) out.push({ pts, closed: contour.closed && pts.length > 2 });
3270
+ }
3271
+ return out;
3272
+ }
3273
+ var signedArea = (pts) => {
3274
+ let a = 0;
3275
+ for (let i = 0; i < pts.length; i++) {
3276
+ const p = pts[i];
3277
+ const q = pts[(i + 1) % pts.length];
3278
+ a += p.x * q.y - q.x * p.y;
3279
+ }
3280
+ return a / 2;
3281
+ };
3282
+ function vectorStrokeGradientDefs(g, id, node, options = {}) {
3283
+ const precision = options.precision ?? 3;
3284
+ const mode = g.strokeMode === "across" ? "across" : "along";
3285
+ const expanded = expandVectorGradientStops(g);
3286
+ const reach = vectorStrokeReach(node) + 1;
3287
+ const area = options.area ?? vectorGradientArea(options.bounds && { x: options.bounds.x - reach, y: options.bounds.y - reach, width: options.bounds.width + 2 * reach, height: options.bounds.height + 2 * reach });
3288
+ const width = node.strokeWidth ?? 1;
3289
+ const align = node.strokeAlign ?? "center";
3290
+ const profile = strokeWidthProfileOf(node);
3291
+ const gradients = [];
3292
+ const patches = [];
3293
+ const slices = [];
3294
+ const colourAt = (t) => {
3295
+ const s = spanVectorGradientStops(g, t, t, expanded)[0];
3296
+ return s;
3297
+ };
3298
+ let counter = 0;
3299
+ const solid = (pts, t) => {
3300
+ const s = colourAt(t);
3301
+ const gid = `${id}-p${counter++}`;
3302
+ gradients.push({ id: gid, type: "linear", attrs: { gradientUnits: "userSpaceOnUse", x1: 0, y1: 0, x2: 1, y2: 0 }, stops: toSvgStops2([{ ...s, offset: 0 }]) });
3303
+ patches.push({ d: polygonD2(pts, precision), fill: `url(#${gid})` });
3304
+ };
3305
+ const disc = (c, r) => Array.from({ length: 16 }, (_, k) => add4(c, scale2({ x: Math.cos(k * Math.PI / 8), y: Math.sin(k * Math.PI / 8) }, r)));
3306
+ for (const { pts, closed } of flattenedSubpaths(node)) {
3307
+ const n = pts.length;
3308
+ const segCount = closed ? n : n - 1;
3309
+ const dirs = [];
3310
+ const lengths = [0];
3311
+ for (let i = 0; i < segCount; i++) {
3312
+ const a = pts[i];
3313
+ const b = pts[(i + 1) % n];
3314
+ dirs.push(unit3(sub4(b, a)));
3315
+ lengths.push(lengths[i] + Math.hypot(b.x - a.x, b.y - a.y));
3316
+ }
3317
+ const total = lengths[segCount];
3318
+ if (!(total > 1e-9)) continue;
3319
+ const tAt = (i) => lengths[i] / total;
3320
+ const clockwise = closed && signedArea(pts) > 0;
3321
+ const visibleLeft = align === "center" || !closed ? null : align === "inside" ? !clockwise : clockwise;
3322
+ const ribs = [];
3323
+ for (let i = 0; i < n; i++) {
3324
+ const p = pts[i];
3325
+ const din = i > 0 || closed ? dirs[(i - 1 + segCount) % segCount] : null;
3326
+ const dout = i < segCount ? dirs[i] : null;
3327
+ const square = (d) => ({ l: add4(p, scale2(left(d), reach)), r: sub4(p, scale2(left(d), reach)) });
3328
+ if (!din || !dout) {
3329
+ const rib = square(din ?? dout);
3330
+ ribs.push({ before: rib, after: rib });
3331
+ continue;
3332
+ }
3333
+ const nin = left(din);
3334
+ const nout = left(dout);
3335
+ const den = 1 + nin.x * nout.x + nin.y * nout.y;
3336
+ const stretch = den > 1e-12 ? Math.sqrt(2 / den) : Infinity;
3337
+ if (stretch <= SHARP) {
3338
+ const m = scale2(add4(nin, nout), reach / den);
3339
+ const rib = { l: add4(p, m), r: sub4(p, m) };
3340
+ ribs.push({ before: rib, after: rib });
3341
+ } else {
3342
+ ribs.push({ before: square(din), after: square(dout) });
3343
+ solid(disc(p, reach * 1.5), tAt(i));
3344
+ }
3345
+ }
3346
+ if (!closed) {
3347
+ const d0 = dirs[0];
3348
+ const d1 = dirs[segCount - 1];
3349
+ const a = pts[0];
3350
+ const b = pts[n - 1];
3351
+ const cap = (p, d) => [add4(p, scale2(left(d), reach)), add4(add4(p, scale2(d, reach)), scale2(left(d), reach)), sub4(add4(p, scale2(d, reach)), scale2(left(d), reach)), sub4(p, scale2(left(d), reach))];
3352
+ solid(cap(a, scale2(d0, -1)), 0);
3353
+ solid(cap(b, d1), 1);
3354
+ }
3355
+ for (let i = 0; i < segCount; i++) {
3356
+ const j = (i + 1) % n;
3357
+ const a = pts[i];
3358
+ const b = pts[j];
3359
+ const d = dirs[i];
3360
+ const from = ribs[i].after;
3361
+ const to = ribs[j].before;
3362
+ const ext = scale2(d, OVERLAP);
3363
+ const quad = [from.l, add4(to.l, ext), add4(to.r, ext), from.r];
3364
+ const gid = `${id}-s${counter++}`;
3365
+ const t0 = tAt(i);
3366
+ const t1 = closed && i === segCount - 1 ? 1 : tAt(i + 1);
3367
+ if (mode === "along") {
3368
+ gradients.push({
3369
+ id: gid,
3370
+ type: "linear",
3371
+ attrs: { gradientUnits: "userSpaceOnUse", x1: fmt2(a.x, 4), y1: fmt2(a.y, 4), x2: fmt2(b.x, 4), y2: fmt2(b.y, 4) },
3372
+ stops: toSvgStops2(spanVectorGradientStops(g, t0, t1, expanded))
3373
+ });
3374
+ } else {
3375
+ const mid = scale2(add4(a, b), 0.5);
3376
+ const side = widthProfileAt(profile, (t0 + t1) / 2);
3377
+ const nrm = left(d);
3378
+ const reachL = visibleLeft === null ? width / 2 * side.left : visibleLeft ? width * side.left : 0;
3379
+ const reachR = visibleLeft === null ? width / 2 * side.right : visibleLeft ? 0 : width * side.right;
3380
+ const p0 = add4(mid, scale2(nrm, reachL));
3381
+ const p1 = sub4(mid, scale2(nrm, reachR));
3382
+ gradients.push({
3383
+ id: gid,
3384
+ type: "linear",
3385
+ attrs: { gradientUnits: "userSpaceOnUse", x1: fmt2(p0.x, 4), y1: fmt2(p0.y, 4), x2: fmt2(p1.x, 4), y2: fmt2(p1.y, 4) },
3386
+ stops: toSvgStops2(expanded)
3387
+ });
3388
+ }
3389
+ slices.push({ d: polygonD2(quad, precision), fill: `url(#${gid})` });
3390
+ }
3391
+ }
3392
+ const def = {
3393
+ id,
3394
+ type: "pattern",
3395
+ attrs: patternAttrs(area),
3396
+ gradients,
3397
+ tiles: [...patches, ...slices]
3398
+ };
3399
+ return [def];
3400
+ }
3401
+
2427
3402
  // src/vector/svg.ts
2428
3403
  function vectorCornersAttribute(subpaths, precision = 3) {
2429
3404
  const out = [];
@@ -2448,10 +3423,30 @@ function applyVectorCornersAttribute(subpaths, value) {
2448
3423
  return subpaths;
2449
3424
  }
2450
3425
  var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2451
- function vectorPaintToSvg(paint, gradientId) {
2452
- if (!hasPaint(paint)) return { value: "none", gradient: null };
2453
- if (typeof paint === "string") return { value: paint, gradient: null };
2454
- return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
3426
+ function vectorPathLocalBounds(node) {
3427
+ const b = bezierPathBounds(subpathsToBezier(node.subpaths));
3428
+ return b ? { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY } : null;
3429
+ }
3430
+ var paintCache = /* @__PURE__ */ new WeakMap();
3431
+ function vectorPaintToSvg(paint, gradientId, options = {}) {
3432
+ if (!hasPaint(paint)) return { value: "none", defs: [] };
3433
+ if (typeof paint === "string") return { value: paint, defs: [] };
3434
+ if (!isVectorGradient(paint)) return { value: "none", defs: [] };
3435
+ const role = options.role ?? "fill";
3436
+ const precision = options.precision ?? 3;
3437
+ const key = `${gradientId}|${role}|${precision}`;
3438
+ const owner = options.node ?? paint;
3439
+ const cached = paintCache.get(owner)?.get(key);
3440
+ if (cached && (!options.node || (role === "fill" ? options.node.fill : options.node.stroke) === paint)) return { value: `url(#${gradientId})`, defs: cached };
3441
+ const node = options.node;
3442
+ const bounds = node ? vectorPathLocalBounds(node) : null;
3443
+ const reach = node && role === "stroke" ? vectorStrokeReach(node) + 1 : 1;
3444
+ const area = bounds ? { x: bounds.x - reach, y: bounds.y - reach, width: bounds.width + 2 * reach, height: bounds.height + 2 * reach } : null;
3445
+ const defs = node && role === "stroke" && (paint.strokeMode === "along" || paint.strokeMode === "across") ? vectorStrokeGradientDefs(paint, gradientId, node, { bounds, area, precision }) : vectorGradientSvgDefs(paint, gradientId, { bounds, area, precision });
3446
+ let byKey = paintCache.get(owner);
3447
+ if (!byKey) paintCache.set(owner, byKey = /* @__PURE__ */ new Map());
3448
+ byKey.set(key, defs);
3449
+ return { value: `url(#${gradientId})`, defs };
2455
3450
  }
2456
3451
  var partsCache = /* @__PURE__ */ new WeakMap();
2457
3452
  function vectorStrokeSvgParts(node, clipId, precision = 3) {
@@ -2490,18 +3485,26 @@ function widthProfileFromAttribute(value) {
2490
3485
  if (!value) return [];
2491
3486
  return sanitizeWidthProfile(
2492
3487
  value.split(";").map((part) => {
2493
- const [t, left, right] = part.split(",").map((n) => parseFloat(n));
2494
- return { t, left, right: Number.isFinite(right) ? right : left };
3488
+ const [t, left2, right] = part.split(",").map((n) => parseFloat(n));
3489
+ return { t, left: left2, right: Number.isFinite(right) ? right : left2 };
2495
3490
  })
2496
3491
  );
2497
3492
  }
2498
3493
  var round2 = (n) => Math.round(n * 100) / 100;
2499
- function gradientMarkup(def) {
3494
+ function gradientMarkup(def, extra = "") {
2500
3495
  const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
2501
3496
  const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
2502
3497
  const stops = def.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escapeAttr(s.stopColor)}"${s.stopOpacity !== void 0 ? ` stop-opacity="${s.stopOpacity}"` : ""}/>`).join("");
2503
- return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
3498
+ return `<${tag} id="${escapeAttr(def.id)}"${attrs}${extra}>${stops}</${tag}>`;
3499
+ }
3500
+ function paintDefMarkup(def, gradient) {
3501
+ const extra = gradient ? ` data-vector-gradient="${escapeAttr(JSON.stringify(gradient))}"` : "";
3502
+ if (def.type !== "pattern") return gradientMarkup(def, extra);
3503
+ const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
3504
+ const inner = def.gradients.map((g) => gradientMarkup(g)).join("") + def.tiles.map((t) => `<path d="${t.d}" fill="${escapeAttr(t.fill)}"/>`).join("");
3505
+ return `<pattern id="${escapeAttr(def.id)}"${attrs}${extra}>${inner}</pattern>`;
2504
3506
  }
3507
+ var paintMarkup = (paint, defs) => defs.map((d, i) => paintDefMarkup(d, i === 0 && isVectorGradient(paint) ? paint : void 0));
2505
3508
  function blendFromAttribute(value, m) {
2506
3509
  if (!value) return null;
2507
3510
  let raw;
@@ -2535,12 +3538,12 @@ function vectorDocumentToSvg(doc, options = {}) {
2535
3538
  const pretty = options.pretty !== false;
2536
3539
  const defs = [];
2537
3540
  const lines = [];
2538
- const num = (n) => formatNumber(n, precision);
3541
+ const num2 = (n) => formatNumber(n, precision);
2539
3542
  const indent = (depth) => pretty ? " ".repeat(depth) : "";
2540
3543
  const common = (node) => {
2541
3544
  let out2 = ` id="${escapeAttr(node.id)}"`;
2542
3545
  if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => formatNumber(n, 6)).join(" ")})"`;
2543
- if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
3546
+ if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num2(node.opacity)}"`;
2544
3547
  if (node.visible === false) out2 += ` display="none"`;
2545
3548
  if (node.blendStep) out2 += ` data-vector-blend-step=""`;
2546
3549
  if (node.type === "group" && node.blend) out2 += ` data-vector-blend="${escapeAttr(JSON.stringify(node.blend))}"`;
@@ -2574,19 +3577,18 @@ function vectorDocumentToSvg(doc, options = {}) {
2574
3577
  lines.push(`${indent(depth)}</g>`);
2575
3578
  return;
2576
3579
  }
2577
- const fill = vectorPaintToSvg(node.fill, `${prefix}fill-${node.id}`);
2578
- const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`);
2579
- if (fill.gradient) defs.push(gradientMarkup(fill.gradient));
2580
- if (stroke.gradient) defs.push(gradientMarkup(stroke.gradient));
3580
+ const fill = vectorPaintToSvg(node.fill, `${prefix}fill-${node.id}`, { node, precision });
3581
+ const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`, { node, role: "stroke", precision });
3582
+ defs.push(...paintMarkup(node.fill, fill.defs), ...paintMarkup(node.stroke, stroke.defs));
2581
3583
  const d = vectorPathData(node, precision);
2582
3584
  const corners2 = vectorCornersAttribute(node.subpaths, precision);
2583
3585
  const live = corners2 ? ` data-vector-d="${bezierPathToSvg(subpathsToBezier(node.subpaths, { corners: false }), { precision })}" data-vector-corners="${corners2}"` : "";
2584
3586
  const fillAttrs = ` fill="${escapeAttr(fill.value)}"${node.fillRule === "evenodd" ? ` fill-rule="evenodd"` : ""}`;
2585
3587
  const lineAttrs = (width) => {
2586
- let out2 = ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(width)}"`;
3588
+ let out2 = ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num2(width)}"`;
2587
3589
  if (node.strokeLinecap && node.strokeLinecap !== "butt") out2 += ` stroke-linecap="${node.strokeLinecap}"`;
2588
3590
  if (node.strokeLinejoin && node.strokeLinejoin !== "miter") out2 += ` stroke-linejoin="${node.strokeLinejoin}"`;
2589
- if (node.strokeDasharray?.length) out2 += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
3591
+ if (node.strokeDasharray?.length) out2 += ` stroke-dasharray="${node.strokeDasharray.map(num2).join(" ")}"`;
2590
3592
  return out2;
2591
3593
  };
2592
3594
  const parts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${prefix}clip-${node.id}`, precision) : void 0;
@@ -2594,19 +3596,19 @@ function vectorDocumentToSvg(doc, options = {}) {
2594
3596
  lines.push(`${indent(depth)}<path${common(node)} d="${d}"${fillAttrs}${stroke.value !== "none" ? lineAttrs(node.strokeWidth ?? 1) : ""}${live}/>`);
2595
3597
  return;
2596
3598
  }
2597
- let data = ` data-vector-path="" data-vector-stroke-width="${num(node.strokeWidth ?? 1)}"`;
3599
+ let data = ` data-vector-path="" data-vector-stroke-width="${num2(node.strokeWidth ?? 1)}"`;
2598
3600
  if (parts.align !== "center") data += ` data-vector-stroke-align="${parts.align}"`;
2599
3601
  if (node.startArrow) data += ` data-vector-start-arrow="${node.startArrow}"`;
2600
3602
  if (node.endArrow) data += ` data-vector-end-arrow="${node.endArrow}"`;
2601
3603
  if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
2602
- if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
2603
- if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
3604
+ if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num2(node.startArrowScale)}"`;
3605
+ if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num2(node.endArrowScale)}"`;
2604
3606
  const profile = strokeWidthProfileOf(node);
2605
3607
  if (profile) {
2606
3608
  data += ` data-vector-width-profile="${widthProfileToAttribute(profile)}"`;
2607
3609
  if (node.strokeLinecap && node.strokeLinecap !== "butt") data += ` data-vector-stroke-linecap="${node.strokeLinecap}"`;
2608
3610
  if (node.strokeLinejoin && node.strokeLinejoin !== "miter") data += ` data-vector-stroke-linejoin="${node.strokeLinejoin}"`;
2609
- if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
3611
+ if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num2).join(" ")}"`;
2610
3612
  }
2611
3613
  lines.push(`${indent(depth)}<g${common(node)}${data}>`);
2612
3614
  lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
@@ -2623,7 +3625,7 @@ function vectorDocumentToSvg(doc, options = {}) {
2623
3625
  clip = ` clip-path="url(#${id})"`;
2624
3626
  } else {
2625
3627
  const b = parts.clip.bounds;
2626
- const box = `x="${num(b.x)}" y="${num(b.y)}" width="${num(b.width)}" height="${num(b.height)}"`;
3628
+ const box = `x="${num2(b.x)}" y="${num2(b.y)}" width="${num2(b.width)}" height="${num2(b.height)}"`;
2627
3629
  defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" ${box}><rect ${box} fill="#fff"/><path d="${d}" fill="#000" fill-rule="${rule}"/></mask>`);
2628
3630
  clip = ` mask="url(#${id})"`;
2629
3631
  }
@@ -2635,8 +3637,8 @@ function vectorDocumentToSvg(doc, options = {}) {
2635
3637
  lines.push(`${indent(depth)}</g>`);
2636
3638
  };
2637
3639
  for (const node of doc.nodes) emit2(node, 1);
2638
- const w = num(doc.width);
2639
- const h = num(doc.height);
3640
+ const w = num2(doc.width);
3641
+ const h = num2(doc.height);
2640
3642
  const nl = pretty ? "\n" : "";
2641
3643
  const out = [`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`];
2642
3644
  if (defs.length) out.push(`${indent(1)}<defs>${defs.join("")}</defs>`);
@@ -2766,28 +3768,89 @@ var fraction = (s, fallback) => {
2766
3768
  if (!Number.isFinite(n)) return fallback;
2767
3769
  return s.trim().endsWith("%") ? n / 100 : n;
2768
3770
  };
3771
+ var PAINT_SERVERS = /* @__PURE__ */ new Set(["linearGradient", "radialGradient", "pattern"]);
2769
3772
  function collectGradients(el, raw) {
2770
- if ((el.name === "linearGradient" || el.name === "radialGradient") && el.attrs.id) raw.set(el.attrs.id, el);
3773
+ if (PAINT_SERVERS.has(el.name) && el.attrs.id) raw.set(el.attrs.id, el);
2771
3774
  for (const c of el.children) collectGradients(c, raw);
2772
3775
  }
2773
- function resolveGradient(id, raw, seen = /* @__PURE__ */ new Set()) {
2774
- const el = raw.get(id);
2775
- if (!el || seen.has(id)) return null;
2776
- seen.add(id);
2777
- let stops = readStops(el);
2778
- const href = el.attrs.href ?? el.attrs["xlink:href"];
2779
- if (!stops.length && href?.startsWith("#")) stops = resolveGradient(href.slice(1), raw, seen)?.stops ?? [];
2780
- if (!stops.length) return null;
2781
- if (el.name === "radialGradient") {
2782
- return { type: "radial", cx: fraction(el.attrs.cx, 0.5), cy: fraction(el.attrs.cy, 0.5), r: fraction(el.attrs.r, 0.5), stops };
3776
+ function gradientFromAttribute(value) {
3777
+ if (!value) return null;
3778
+ try {
3779
+ return sanitizeVectorGradient(JSON.parse(value));
3780
+ } catch {
3781
+ return null;
2783
3782
  }
2784
- const x1 = fraction(el.attrs.x1, 0);
2785
- const y1 = fraction(el.attrs.y1, 0);
2786
- const x2 = fraction(el.attrs.x2, 1);
2787
- const y2 = fraction(el.attrs.y2, 0);
2788
- const angle = Math.round((Math.atan2(x2 - x1, -(y2 - y1)) * 180 / Math.PI + 360) % 360);
2789
- return { type: "linear", angle, stops };
2790
3783
  }
3784
+ function resolveGradient(id, raw) {
3785
+ const first = raw.get(id);
3786
+ if (!first) return null;
3787
+ const own = gradientFromAttribute(first.attrs["data-vector-gradient"]);
3788
+ if (own) return own;
3789
+ if (first.name === "pattern") return null;
3790
+ const chain = [];
3791
+ for (let el = first; el && !chain.includes(el) && chain.length < 32; ) {
3792
+ chain.push(el);
3793
+ const href = el.attrs.href ?? el.attrs["xlink:href"];
3794
+ el = href?.startsWith("#") ? raw.get(href.slice(1)) : void 0;
3795
+ }
3796
+ const attr = (name) => chain.find((el) => el.attrs[name] !== void 0)?.attrs[name];
3797
+ const stops = chain.map(readStops).find((s) => s.length) ?? [];
3798
+ if (!stops.length) return null;
3799
+ const user = attr("gradientUnits") === "userSpaceOnUse";
3800
+ const t = parseSvgTransform(attr("gradientTransform"));
3801
+ const plain3 = t === IDENTITY_MATRIX || t[0] === 1 && t[1] === 0 && t[2] === 0 && t[3] === 1 && t[4] === 0 && t[5] === 0;
3802
+ const at2 = (name, fallback) => user ? length(attr(name), fallback) : fraction(attr(name), fallback);
3803
+ const spreadAttr = attr("spreadMethod");
3804
+ const spread = spreadAttr === "reflect" || spreadAttr === "repeat" ? { spread: spreadAttr } : {};
3805
+ const map = (p) => applySvgMatrix(t, p);
3806
+ if (first.name === "radialGradient") {
3807
+ const cx = at2("cx", user ? 0 : 0.5);
3808
+ const cy = at2("cy", user ? 0 : 0.5);
3809
+ const r = at2("r", user ? 0 : 0.5);
3810
+ const fx = attr("fx") !== void 0 ? at2("fx", cx) : cx;
3811
+ const fy = attr("fy") !== void 0 ? at2("fy", cy) : cy;
3812
+ const fr = attr("fr") !== void 0 ? at2("fr", 0) : 0;
3813
+ const focal = Math.abs(fx - cx) > 1e-9 || Math.abs(fy - cy) > 1e-9;
3814
+ if (!user && plain3 && !focal && !(fr > 0)) return { type: "radial", cx, cy, r, stops, ...spread };
3815
+ const g = {
3816
+ type: "radial",
3817
+ stops,
3818
+ start: map({ x: cx, y: cy }),
3819
+ end: map({ x: cx + r, y: cy }),
3820
+ width: map({ x: cx, y: cy + r }),
3821
+ units: user ? "userSpaceOnUse" : "objectBoundingBox",
3822
+ ...spread
3823
+ };
3824
+ if (focal) g.focal = map({ x: fx, y: fy });
3825
+ if (fr > 0 && r > 0) g.focalRadius = Math.min(0.99, fr / r);
3826
+ return g;
3827
+ }
3828
+ const x1 = at2("x1", 0);
3829
+ const y1 = at2("y1", 0);
3830
+ const x2 = attr("x2") !== void 0 ? at2("x2", 1) : user ? 0 : 1;
3831
+ const y2 = at2("y2", 0);
3832
+ if (!user && plain3) {
3833
+ const angle = Math.round((Math.atan2(x2 - x1, -(y2 - y1)) * 180 / Math.PI + 360) % 360);
3834
+ const c = angleToSvgLinearCoords(angle);
3835
+ const len2 = Math.hypot(x2 - x1, y2 - y1);
3836
+ const d = len2 > 0 ? { x: (x2 - x1) / len2, y: (y2 - y1) / len2 } : { x: 0, y: 1 };
3837
+ const along2 = (x, y) => x * d.x + y * d.y;
3838
+ const same = len2 > 0 && Math.abs((c.x2 - c.x1) * d.y - (c.y2 - c.y1) * d.x) < 1e-3 && Math.abs(along2(c.x1, c.y1) - along2(x1, y1)) < 1e-3 && Math.abs(along2(c.x2, c.y2) - along2(x2, y2)) < 1e-3;
3839
+ if (same) return { type: "linear", angle, stops, ...spread };
3840
+ }
3841
+ const start = { x: x1, y: y1 };
3842
+ const end = { x: x2, y: y2 };
3843
+ return {
3844
+ type: "linear",
3845
+ stops,
3846
+ start: map(start),
3847
+ end: map(end),
3848
+ width: map({ x: x1 - (y2 - y1), y: y1 + (x2 - x1) }),
3849
+ units: user ? "userSpaceOnUse" : "objectBoundingBox",
3850
+ ...spread
3851
+ };
3852
+ }
3853
+ var applySvgMatrix = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
2791
3854
  function parseVectorSvg(svg, options = {}) {
2792
3855
  const root = parseXml(svg);
2793
3856
  if (!root) throw new Error("The markup has no <svg> element.");
@@ -2888,7 +3951,7 @@ function parseVectorSvg(svg, options = {}) {
2888
3951
  const ss = strokeEl ? readStyle(strokeEl, style) : null;
2889
3952
  const width2 = length(a["data-vector-stroke-width"], ss ? length(ss.strokeWidth, 1) : 1);
2890
3953
  const arrow = (v2) => VECTOR_ARROWHEADS.includes(v2) ? v2 : void 0;
2891
- const scale = (v2) => {
3954
+ const scale3 = (v2) => {
2892
3955
  const n = v2 === void 0 ? NaN : parseFloat(v2);
2893
3956
  return n > 0 ? n : void 0;
2894
3957
  };
@@ -2912,8 +3975,8 @@ function parseVectorSvg(svg, options = {}) {
2912
3975
  ...arrow(a["data-vector-start-arrow"]) ? { startArrow: arrow(a["data-vector-start-arrow"]) } : {},
2913
3976
  ...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
2914
3977
  ...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
2915
- ...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
2916
- ...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {},
3978
+ ...scale3(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale3(a["data-vector-start-arrow-scale"]) } : {},
3979
+ ...scale3(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale3(a["data-vector-end-arrow-scale"]) } : {},
2917
3980
  ...profile.length ? { strokeWidthProfile: profile } : {},
2918
3981
  ...keptCap ? { strokeLinecap: keptCap } : {},
2919
3982
  ...keptJoin ? { strokeLinejoin: keptJoin } : {},
@@ -3006,6 +4069,72 @@ function parseVectorSvg(svg, options = {}) {
3006
4069
  return { width, height, nodes };
3007
4070
  }
3008
4071
 
4072
+ // src/vector/gradientTool.ts
4073
+ var VECTOR_GRADIENT_STOP_GAP = 12;
4074
+ var VECTOR_GRADIENT_MIDPOINT_GAP = 9;
4075
+ var VECTOR_GRADIENT_OFF_BAR = 24;
4076
+ var STOP_HIT = 6;
4077
+ var MIDPOINT_HIT = 5;
4078
+ var HANDLE_HIT = 6;
4079
+ var BAR_HIT = 5;
4080
+ var fmt3 = (n) => Math.round(n * 100) / 100;
4081
+ var apply2 = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
4082
+ function vectorGradientTargetPath(doc, selection, target) {
4083
+ for (const id of selection) {
4084
+ const node = findVectorNode(doc.nodes, id);
4085
+ if (!node) continue;
4086
+ const path = collectPathNodes(node).find((p) => isVectorGradient(p[target]));
4087
+ if (path) return path;
4088
+ }
4089
+ return null;
4090
+ }
4091
+ function getVectorGradientAnnotator(doc, selection, viewport, target) {
4092
+ const path = vectorGradientTargetPath(doc, selection, target);
4093
+ if (!path) return null;
4094
+ const gradient = path[target];
4095
+ const world = vectorWorldMatrix(doc.nodes, path.id) ?? IDENTITY_MATRIX;
4096
+ const screen = multiplyMatrices(world, [viewport.zoom, 0, 0, viewport.zoom, viewport.x, viewport.y]);
4097
+ const bounds = vectorPathLocalBounds(path);
4098
+ const h = vectorGradientHandles(gradient, bounds, screen);
4099
+ const v2 = { x: h.end.x - h.start.x, y: h.end.y - h.start.y };
4100
+ const len2 = Math.hypot(v2.x, v2.y);
4101
+ const normal = len2 > 1e-9 ? { x: -v2.y / len2, y: v2.x / len2 } : { x: 0, y: 1 };
4102
+ const at2 = (t, gap) => ({ x: h.start.x + v2.x * t + normal.x * gap, y: h.start.y + v2.y * t + normal.y * gap });
4103
+ const sorted = sortedVectorGradientStops(gradient);
4104
+ const stops = sorted.map((s, index) => ({ ...s, index, ...at2(s.offset, VECTOR_GRADIENT_STOP_GAP) }));
4105
+ const midpoints = sorted.slice(0, -1).flatMap((s, index) => {
4106
+ const pos = vectorGradientMidpointPosition(sorted, index);
4107
+ return pos === null || sorted[index + 1].offset - s.offset < 1e-6 ? [] : [{ index, midpoint: s.midpoint ?? 0.5, ...at2(pos, -VECTOR_GRADIENT_MIDPOINT_GAP) }];
4108
+ });
4109
+ let extent = null;
4110
+ if (isCentredVectorGradient(gradient.type)) {
4111
+ const frame2 = multiplyMatrices(vectorGradientMatrix(gradient, bounds), screen);
4112
+ const pts = gradient.type === "diamond" ? [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }] : Array.from({ length: 48 }, (_, k) => ({ x: Math.cos(k * Math.PI / 24), y: Math.sin(k * Math.PI / 24) }));
4113
+ extent = `M${pts.map((p) => apply2(frame2, p)).map((p) => `${fmt3(p.x)} ${fmt3(p.y)}`).join("L")}Z`;
4114
+ }
4115
+ return { nodeId: path.id, target, gradient, bounds, world, start: h.start, end: h.end, aspect: h.aspect, focal: h.focal, normal, stops, midpoints, extent };
4116
+ }
4117
+ function vectorGradientBarPosition(a, p) {
4118
+ const v2 = { x: a.end.x - a.start.x, y: a.end.y - a.start.y };
4119
+ const l2 = v2.x * v2.x + v2.y * v2.y;
4120
+ const d = { x: p.x - a.start.x, y: p.y - a.start.y };
4121
+ if (l2 < 1e-12) return { offset: 0, across: Math.hypot(d.x, d.y) };
4122
+ return { offset: (d.x * v2.x + d.y * v2.y) / l2, across: d.x * a.normal.x + d.y * a.normal.y };
4123
+ }
4124
+ function hitVectorGradientAnnotator(a, p) {
4125
+ const near2 = (q, r) => !!q && Math.hypot(q.x - p.x, q.y - p.y) <= r;
4126
+ for (let i = a.stops.length - 1; i >= 0; i--) if (near2(a.stops[i], STOP_HIT)) return { kind: "stop", index: a.stops[i].index };
4127
+ for (const m of a.midpoints) if (near2(m, MIDPOINT_HIT)) return { kind: "midpoint", index: m.index };
4128
+ const centred = isCentredVectorGradient(a.gradient.type);
4129
+ if (near2(a.end, HANDLE_HIT)) return { kind: "handle", handle: centred ? "radius" : "end" };
4130
+ if (a.focal && near2(a.focal, HANDLE_HIT) && !near2(a.start, 1)) return { kind: "handle", handle: "focal" };
4131
+ if (near2(a.start, HANDLE_HIT)) return { kind: "handle", handle: "start" };
4132
+ if (near2(a.aspect, HANDLE_HIT)) return { kind: "handle", handle: "aspect" };
4133
+ const { offset, across } = vectorGradientBarPosition(a, p);
4134
+ if (offset >= 0 && offset <= 1 && across >= -BAR_HIT && across <= VECTOR_GRADIENT_STOP_GAP + STOP_HIT) return { kind: "bar", offset };
4135
+ return null;
4136
+ }
4137
+
3009
4138
  // src/vector/blend.ts
3010
4139
  var lerpN = (a, b, t) => a + (b - a) * t;
3011
4140
  var lerpP = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
@@ -3097,10 +4226,10 @@ function refineMorphContour(c, segments) {
3097
4226
  const extra = segments - have;
3098
4227
  const want = lens.map((l) => sum > 1e-12 ? l / sum * extra : extra / have);
3099
4228
  const parts = want.map((w) => 1 + Math.floor(w));
3100
- let left = segments - parts.reduce((a, b) => a + b, 0);
4229
+ let left2 = segments - parts.reduce((a, b) => a + b, 0);
3101
4230
  const frac = want.map((w) => w - Math.floor(w));
3102
4231
  const order = parts.map((_, i) => i).sort((a, b) => Math.abs(frac[b] - frac[a]) > 1e-9 ? frac[b] - frac[a] : a - b);
3103
- for (let i = 0; left > 0; i = (i + 1) % order.length, left--) parts[order[i]]++;
4232
+ for (let i = 0; left2 > 0; i = (i + 1) % order.length, left2--) parts[order[i]]++;
3104
4233
  const pts = [c.pts[0]];
3105
4234
  const anchors = [0];
3106
4235
  let at2 = 0;
@@ -3209,19 +4338,19 @@ function bestStart(fixed, free, useShapeContext) {
3209
4338
  const gsc = useShapeContext ? shapeContexts(ga, free.closed) : null;
3210
4339
  const m = Math.max(1, fn.length);
3211
4340
  const cost = (s) => {
3212
- let dot2 = 0;
4341
+ let dot3 = 0;
3213
4342
  let cross2 = 0;
3214
4343
  let sc = 0;
3215
4344
  for (let i = 0; i < fn.length; i++) {
3216
4345
  const j = free.closed ? (i + s) % n : i;
3217
4346
  const p = fn[i];
3218
4347
  const q = gn[j];
3219
- dot2 += p[0] * q[0] + p[1] * q[1];
4348
+ dot3 += p[0] * q[0] + p[1] * q[1];
3220
4349
  cross2 += p[0] * q[1] - p[1] * q[0];
3221
4350
  if (fsc && gsc) sc += chi2(fsc[i], gsc[j]);
3222
4351
  }
3223
- const residual = Math.max(0, 1 - (dot2 * dot2 + cross2 * cross2) / (m * m));
3224
- const turn = 1 - Math.cos(Math.atan2(cross2, dot2));
4352
+ const residual = Math.max(0, 1 - (dot3 * dot3 + cross2 * cross2) / (m * m));
4353
+ const turn = 1 - Math.cos(Math.atan2(cross2, dot3));
3225
4354
  return residual + TURN_WEIGHT * turn + sc / m;
3226
4355
  };
3227
4356
  if (!free.closed) return { start: 0, cost: cost(0) };
@@ -3366,16 +4495,16 @@ function procrustes(a, b) {
3366
4495
  const ca = frame(pa).c;
3367
4496
  const cb = frame(pb).c;
3368
4497
  let ss = 0;
3369
- let dot2 = 0;
4498
+ let dot3 = 0;
3370
4499
  let cross2 = 0;
3371
4500
  for (let i = 0; i < Math.min(pa.length, pb.length); i++) {
3372
4501
  const p = [pa[i][0] - ca[0], pa[i][1] - ca[1]];
3373
4502
  const q = [pb[i][0] - cb[0], pb[i][1] - cb[1]];
3374
4503
  ss += p[0] * p[0] + p[1] * p[1];
3375
- dot2 += p[0] * q[0] + p[1] * q[1];
4504
+ dot3 += p[0] * q[0] + p[1] * q[1];
3376
4505
  cross2 += p[0] * q[1] - p[1] * q[0];
3377
4506
  }
3378
- return { ca, cb, theta: Math.atan2(cross2, dot2), s: ss > 1e-12 ? Math.hypot(dot2, cross2) / ss : 1 };
4507
+ return { ca, cb, theta: Math.atan2(cross2, dot3), s: ss > 1e-12 ? Math.hypot(dot3, cross2) / ss : 1 };
3379
4508
  }
3380
4509
  function interpolateMorphContours(a, b, u, options = {}) {
3381
4510
  const w = options.linear ? 0 : Math.min(1, Math.max(0, options.rigidity ?? 1));
@@ -3429,39 +4558,24 @@ function interpolateVectorColor(a, b, t) {
3429
4558
  if (t >= 1) return b;
3430
4559
  return mixColorsOklab(a, b, t) ?? (t < 0.5 ? a : b);
3431
4560
  }
3432
- function sortedStops(g) {
3433
- return g.stops.slice().sort((x, y) => x.offset - y.offset);
3434
- }
3435
- function sampleGradient(stops, offset) {
3436
- if (!stops.length) return { color: "transparent", opacity: 1 };
3437
- const first = stops[0];
3438
- const last = stops[stops.length - 1];
3439
- if (offset <= first.offset) return { color: first.color, opacity: first.opacity ?? 1 };
3440
- if (offset >= last.offset) return { color: last.color, opacity: last.opacity ?? 1 };
3441
- let i = 0;
3442
- while (i + 1 < stops.length && stops[i + 1].offset < offset) i++;
3443
- const x = stops[i];
3444
- const y = stops[i + 1];
3445
- const u = y.offset - x.offset > 1e-12 ? (offset - x.offset) / (y.offset - x.offset) : 1;
3446
- return { color: interpolateVectorColor(x.color, y.color, u), opacity: lerpN(x.opacity ?? 1, y.opacity ?? 1, u) };
3447
- }
3448
4561
  function resampleGradientStops(g, offsets) {
3449
- const stops = sortedStops(g);
3450
4562
  return offsets.map((offset) => {
3451
- const s = sampleGradient(stops, offset);
3452
- return s.opacity < 1 ? { offset, color: s.color, opacity: s.opacity } : { offset, color: s.color };
4563
+ const s = sampleVectorGradient(g, offset);
4564
+ return s.opacity < 1 ? { offset, color: s.color, opacity: Math.round(s.opacity * 1e4) / 1e4 } : { offset, color: s.color };
3453
4565
  });
3454
4566
  }
3455
4567
  function lerpAngle(a, b, t) {
3456
4568
  const d = ((b - a + 180) % 360 + 360) % 360 - 180;
3457
4569
  return a + d * t;
3458
4570
  }
3459
- function lerpGradient(a, b, t) {
3460
- if (a.type !== b.type) return null;
3461
- let stopsA = sortedStops(a);
3462
- let stopsB = sortedStops(b);
3463
- if (stopsA.length !== stopsB.length) {
3464
- const offs = [...stopsA, ...stopsB].map((s) => s.offset).filter(Number.isFinite).sort((x, y) => x - y);
4571
+ var lerpPoint = (a, b, t) => ({ x: lerpN(a.x, b.x, t), y: lerpN(a.y, b.y, t) });
4572
+ var sameColourModel = (a, b) => (a.interpolation ?? "srgb") === (b.interpolation ?? "srgb") && (a.smoothness ?? 0) === (b.smoothness ?? 0);
4573
+ function lerpStops(a, b, t) {
4574
+ let stopsA = sortedVectorGradientStops(a);
4575
+ let stopsB = sortedVectorGradientStops(b);
4576
+ const resampled = stopsA.length !== stopsB.length || !sameColourModel(a, b);
4577
+ if (resampled) {
4578
+ const offs = [...expandVectorGradientStops(a), ...expandVectorGradientStops(b), ...stopsA, ...stopsB].map((s) => s.offset).filter(Number.isFinite).sort((x, y) => x - y);
3465
4579
  const common = offs.filter((o, i) => i === 0 || o - offs[i - 1] > 1e-4);
3466
4580
  stopsA = resampleGradientStops(a, common);
3467
4581
  stopsB = resampleGradientStops(b, common);
@@ -3471,12 +4585,53 @@ function lerpGradient(a, b, t) {
3471
4585
  const opacity = lerpN(x.opacity ?? 1, y.opacity ?? 1, t);
3472
4586
  const stop = { offset: lerpN(x.offset, y.offset, t), color: interpolateVectorColor(x.color, y.color, t) };
3473
4587
  if (opacity < 1) stop.opacity = opacity;
4588
+ const mid = lerpN(x.midpoint ?? 0.5, y.midpoint ?? 0.5, t);
4589
+ if (Math.abs(mid - 0.5) > 1e-6) stop.midpoint = mid;
3474
4590
  return stop;
3475
4591
  });
3476
- if (a.type === "linear" && b.type === "linear") return { type: "linear", angle: lerpAngle(a.angle ?? 90, b.angle ?? 90, t), stops };
3477
- const ra = a;
3478
- const rb = b;
3479
- return { type: "radial", cx: lerpN(ra.cx ?? 0.5, rb.cx ?? 0.5, t), cy: lerpN(ra.cy ?? 0.5, rb.cy ?? 0.5, t), r: lerpN(ra.r ?? 0.5, rb.r ?? 0.5, t), stops };
4592
+ return { stops, resampled };
4593
+ }
4594
+ function lerpGradient(a, b, t) {
4595
+ if (a.type !== b.type) return null;
4596
+ const { stops, resampled } = lerpStops(a, b, t);
4597
+ const near2 = t < 0.5 ? a : b;
4598
+ const out = { type: a.type, stops };
4599
+ if (!resampled) {
4600
+ if (a.interpolation && a.interpolation !== "srgb") out.interpolation = a.interpolation;
4601
+ if (a.smoothness) out.smoothness = a.smoothness;
4602
+ }
4603
+ if (near2.spread && near2.spread !== "pad") out.spread = near2.spread;
4604
+ if (near2.strokeMode && near2.strokeMode !== "within") out.strokeMode = near2.strokeMode;
4605
+ const ea = hasExplicitGradientGeometry(a);
4606
+ const eb = hasExplicitGradientGeometry(b);
4607
+ if (!ea && !eb) {
4608
+ if (a.angle !== void 0 || b.angle !== void 0 || a.type === "linear" || a.type === "reflected") {
4609
+ const fallback = a.type === "linear" || a.type === "reflected" ? 180 : 90;
4610
+ out.angle = lerpAngle(a.angle ?? fallback, b.angle ?? fallback, t);
4611
+ }
4612
+ if (a.type !== "linear" && a.type !== "reflected") {
4613
+ out.cx = lerpN(a.cx ?? 0.5, b.cx ?? 0.5, t);
4614
+ out.cy = lerpN(a.cy ?? 0.5, b.cy ?? 0.5, t);
4615
+ out.r = lerpN(a.r ?? 0.5, b.r ?? 0.5, t);
4616
+ }
4617
+ return out;
4618
+ }
4619
+ const pa = vectorGradientPoints(a);
4620
+ const pb = vectorGradientPoints(b);
4621
+ if (pa.units !== pb.units) {
4622
+ const src = explicitVectorGradient(near2);
4623
+ return { ...src, stops: out.stops, ...out.interpolation ? { interpolation: out.interpolation } : {}, ...out.smoothness ? { smoothness: out.smoothness } : {} };
4624
+ }
4625
+ out.start = lerpPoint(pa.start, pb.start, t);
4626
+ out.end = lerpPoint(pa.end, pb.end, t);
4627
+ out.width = lerpPoint(pa.width, pb.width, t);
4628
+ out.units = pa.units;
4629
+ if (a.type === "radial") {
4630
+ if (pa.focal || pb.focal) out.focal = lerpPoint(pa.focal ?? pa.start, pb.focal ?? pb.start, t);
4631
+ const fr = lerpN(pa.focalRadius, pb.focalRadius, t);
4632
+ if (fr > 0) out.focalRadius = fr;
4633
+ }
4634
+ return out;
3480
4635
  }
3481
4636
  function solidLike(color, like) {
3482
4637
  return { ...like, stops: like.stops.map((s) => color === null ? { offset: s.offset, color: s.color, opacity: 0 } : { offset: s.offset, color }) };
@@ -3490,8 +4645,8 @@ function interpolateVectorPaint(a, b, t) {
3490
4645
  if (typeof a === "string" && pa && typeof b === "string" && pb) return interpolateVectorColor(a, b, t);
3491
4646
  if (!pa && typeof b === "string") return interpolateVectorColor("transparent", b, t);
3492
4647
  if (!pb && typeof a === "string") return interpolateVectorColor(a, "transparent", t);
3493
- const ga = pa && typeof a === "object" && a ? a : null;
3494
- const gb = pb && typeof b === "object" && b ? b : null;
4648
+ const ga = pa && isVectorGradient(a) ? a : null;
4649
+ const gb = pb && isVectorGradient(b) ? b : null;
3495
4650
  if (ga && gb) return lerpGradient(ga, gb, t) ?? (t < 0.5 ? a : b);
3496
4651
  if (gb) return lerpGradient(solidLike(pa ? a : null, gb), gb, t) ?? b;
3497
4652
  if (ga) return lerpGradient(ga, solidLike(pb ? b : null, ga), t) ?? a;
@@ -3628,15 +4783,15 @@ function createVectorMorph(a, b, options = {}) {
3628
4783
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
3629
4784
  const x = pa[i];
3630
4785
  const y = pb[i];
3631
- const sub3 = { ...options, id: `${id}-${i}`, starts: void 0 };
4786
+ const sub5 = { ...options, id: `${id}-${i}`, starts: void 0 };
3632
4787
  if (x && y) {
3633
- parts.push(preparePathMorph(x.path, y.path, sub3, x.m, y.m));
4788
+ parts.push(preparePathMorph(x.path, y.path, sub5, x.m, y.m));
3634
4789
  opacities.push([x.opacity, y.opacity]);
3635
4790
  } else if (x) {
3636
- parts.push(preparePathMorph(x.path, collapsedPath(x.path, x.m, ba, bb), sub3, x.m, IDENTITY_MATRIX));
4791
+ parts.push(preparePathMorph(x.path, collapsedPath(x.path, x.m, ba, bb), sub5, x.m, IDENTITY_MATRIX));
3637
4792
  opacities.push([x.opacity, x.opacity]);
3638
4793
  } else if (y) {
3639
- parts.push(preparePathMorph(collapsedPath(y.path, y.m, bb, ba), y.path, sub3, IDENTITY_MATRIX, y.m));
4794
+ parts.push(preparePathMorph(collapsedPath(y.path, y.m, bb, ba), y.path, sub5, IDENTITY_MATRIX, y.m));
3640
4795
  opacities.push([y.opacity, y.opacity]);
3641
4796
  }
3642
4797
  }
@@ -4621,16 +5776,16 @@ function solveOnSegment(seg, base, cond) {
4621
5776
  return cond([p[0] - b[0], p[1] - b[1]], segmentDerivative(seg, t));
4622
5777
  };
4623
5778
  const vals = [];
4624
- let scale = 0;
5779
+ let scale3 = 0;
4625
5780
  for (let i = 0; i <= CURVE_SAMPLES; i++) {
4626
5781
  const t = i / CURVE_SAMPLES;
4627
5782
  const p = segmentPoint(seg, t);
4628
5783
  const d = segmentDerivative(seg, t);
4629
5784
  const r = [p[0] - b[0], p[1] - b[1]];
4630
5785
  vals.push(cond(r, d));
4631
- scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
5786
+ scale3 = Math.max(scale3, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
4632
5787
  }
4633
- if (!(scale > 0) || vals.every((v2) => Math.abs(v2) <= scale * 1e-12)) return [];
5788
+ if (!(scale3 > 0) || vals.every((v2) => Math.abs(v2) <= scale3 * 1e-12)) return [];
4634
5789
  const out = [];
4635
5790
  for (let i = 0; i < CURVE_SAMPLES; i++) {
4636
5791
  const ga = vals[i];
@@ -4664,7 +5819,7 @@ function solveOnSegment(seg, base, cond) {
4664
5819
 
4665
5820
  // src/vector/pathEdit.ts
4666
5821
  var EPS4 = 1e-9;
4667
- var unit3 = (x, y) => {
5822
+ var unit4 = (x, y) => {
4668
5823
  const l = Math.hypot(x, y);
4669
5824
  return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
4670
5825
  };
@@ -4677,14 +5832,14 @@ function cubicOf(sp, i) {
4677
5832
  var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
4678
5833
  function startTangent2(c) {
4679
5834
  for (let k = 1; k < 4; k++) {
4680
- const u = unit3(c[k][0] - c[0][0], c[k][1] - c[0][1]);
5835
+ const u = unit4(c[k][0] - c[0][0], c[k][1] - c[0][1]);
4681
5836
  if (u) return u;
4682
5837
  }
4683
5838
  return [1, 0];
4684
5839
  }
4685
5840
  function endTangent2(c) {
4686
5841
  for (let k = 2; k >= 0; k--) {
4687
- const u = unit3(c[3][0] - c[k][0], c[3][1] - c[k][1]);
5842
+ const u = unit4(c[3][0] - c[k][0], c[3][1] - c[k][1]);
4688
5843
  if (u) return u;
4689
5844
  }
4690
5845
  return [1, 0];
@@ -4748,13 +5903,13 @@ function removeAnchorKeepingShape(sp, index) {
4748
5903
  const pi = (index + n - 1) % n;
4749
5904
  const ni = (index + 1) % n;
4750
5905
  if (!(isLine(sp, pi) && isLine(sp, index))) {
4751
- const left = cubicOf(sp, pi);
5906
+ const left2 = cubicOf(sp, pi);
4752
5907
  const right = cubicOf(sp, index);
4753
5908
  const N = 16;
4754
5909
  const pts = [];
4755
- sample(left, N, pts, true);
5910
+ sample(left2, N, pts, true);
4756
5911
  sample(right, N, pts, false);
4757
- const t0 = startTangent2(left);
5912
+ const t0 = startTangent2(left2);
4758
5913
  const t1 = endTangent2(right);
4759
5914
  const { curve } = fitAcross(pts, N, t0, t1);
4760
5915
  if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
@@ -4956,7 +6111,7 @@ function smoothSubpath(sp, amount = 1, only) {
4956
6111
  const prev = orig[(i + n - 1) % n];
4957
6112
  const next = orig[(i + 1) % n];
4958
6113
  const p = orig[i];
4959
- const dir = unit3(next.x - prev.x, next.y - prev.y);
6114
+ const dir = unit4(next.x - prev.x, next.y - prev.y);
4960
6115
  if (!dir) continue;
4961
6116
  const li = dist2(p, prev) / 3;
4962
6117
  const lo = dist2(p, next) / 3;
@@ -5091,9 +6246,9 @@ function facesOf(path, fillRule) {
5091
6246
  function splitIntoFaces(path, fillRule = "nonzero") {
5092
6247
  return facesOf(path, fillRule).map(faceToPath);
5093
6248
  }
5094
- function rootsInUnit(a, b, c, d, scale) {
6249
+ function rootsInUnit(a, b, c, d, scale3) {
5095
6250
  const f = (t) => ((a * t + b) * t + c) * t + d;
5096
- const tol = 1e-12 * Math.max(1, scale);
6251
+ const tol = 1e-12 * Math.max(1, scale3);
5097
6252
  const cuts = [0];
5098
6253
  const qa = 3 * a;
5099
6254
  const qb = 2 * b;
@@ -5150,9 +6305,9 @@ function lineHits(p, q, seg) {
5150
6305
  coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
5151
6306
  }
5152
6307
  if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
5153
- const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
6308
+ const scale3 = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
5154
6309
  const out = [];
5155
- for (const t of rootsInUnit(...coeffs, scale)) {
6310
+ for (const t of rootsInUnit(...coeffs, scale3)) {
5156
6311
  const point = segmentPoint(seg, t);
5157
6312
  const s = ((point[0] - p[0]) * dx + (point[1] - p[1]) * dy) / len2;
5158
6313
  if (s >= -1e-9 && s <= 1 + 1e-9) out.push({ s: Math.min(1, Math.max(0, s)), t, point });
@@ -6098,7 +7253,8 @@ var TOOL_LABELS = {
6098
7253
  width: "Width",
6099
7254
  shapeBuilder: "Shape Builder",
6100
7255
  paintBucket: "Paint Bucket",
6101
- blend: "Blend"
7256
+ blend: "Blend",
7257
+ gradient: "Gradient"
6102
7258
  };
6103
7259
  var TOOL_KEYS = {
6104
7260
  v: "select",
@@ -6116,7 +7272,8 @@ var TOOL_KEYS = {
6116
7272
  w: "width",
6117
7273
  m: "shapeBuilder",
6118
7274
  b: "paintBucket",
6119
- j: "blend"
7275
+ j: "blend",
7276
+ g: "gradient"
6120
7277
  };
6121
7278
  function vectorToolLabel(tool) {
6122
7279
  return TOOL_LABELS[tool];
@@ -6137,7 +7294,8 @@ var VECTOR_TOOL_SHORTCUTS = {
6137
7294
  width: "W",
6138
7295
  shapeBuilder: "M",
6139
7296
  paintBucket: "B",
6140
- blend: "J"
7297
+ blend: "J",
7298
+ gradient: "G"
6141
7299
  };
6142
7300
  var VECTOR_TOOL_GLYPHS = {
6143
7301
  select: "\u2196",
@@ -6155,7 +7313,8 @@ var VECTOR_TOOL_GLYPHS = {
6155
7313
  width: "\u21F9",
6156
7314
  shapeBuilder: "\u2295",
6157
7315
  paintBucket: "\u25CD",
6158
- blend: "\u29D3"
7316
+ blend: "\u29D3",
7317
+ gradient: "\u25D0"
6159
7318
  };
6160
7319
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
6161
7320
  var BOOLEAN_OPS = {
@@ -6252,6 +7411,7 @@ var VectorEditorController = class {
6252
7411
  blendStart = null;
6253
7412
  /** Options new blends get (the toolbars edit them along with the selected blend's). */
6254
7413
  blendDefaults = {};
7414
+ gradientEdit = { target: "fill", stop: null };
6255
7415
  /** What the highlight shows, to skip notifying when the pointer stays over the same face. */
6256
7416
  builderHoverKey = null;
6257
7417
  createId;
@@ -6276,15 +7436,26 @@ var VectorEditorController = class {
6276
7436
  this.createId = options.createId ?? defaultCreateId;
6277
7437
  this.onChange = options.onChange;
6278
7438
  this.state = this.computeState();
7439
+ this.connect();
7440
+ }
7441
+ /** Listens to the history (once; again after `destroy`). */
7442
+ connect() {
7443
+ if (this.cleanups.length) return;
6279
7444
  this.cleanups.push(
6280
7445
  this.history.subscribe((h) => {
6281
- const s = this.state;
6282
- if (this.applying || s.canUndo === h.canUndo && s.canRedo === h.canRedo && s.undoLabel === h.undoName && s.redoLabel === h.redoName) return;
6283
- this.notify();
7446
+ if (!this.applying && this.historyDiffers(h)) this.notify();
6284
7447
  })
6285
7448
  );
7449
+ if (this.historyDiffers(this.history.getState())) this.state = this.computeState();
6286
7450
  }
6287
- /** Stops listening to the history. */
7451
+ historyDiffers(h) {
7452
+ const s = this.state;
7453
+ return s.canUndo !== h.canUndo || s.canRedo !== h.canRedo || s.undoLabel !== h.undoName || s.redoLabel !== h.redoName;
7454
+ }
7455
+ /**
7456
+ * Stops listening to the history and drops all listeners. A later `subscribe` reconnects, so a
7457
+ * controller torn down by React StrictMode's double effect keeps working when it is reused.
7458
+ */
6288
7459
  destroy() {
6289
7460
  this.cleanups.splice(0).forEach((fn) => fn());
6290
7461
  this.listeners.clear();
@@ -6296,6 +7467,7 @@ var VectorEditorController = class {
6296
7467
  getState = () => this.state;
6297
7468
  /** Calls `listener` after every change. Returns an unsubscribe function. */
6298
7469
  subscribe = (listener) => {
7470
+ this.connect();
6299
7471
  this.listeners.add(listener);
6300
7472
  return () => {
6301
7473
  this.listeners.delete(listener);
@@ -6333,6 +7505,7 @@ var VectorEditorController = class {
6333
7505
  builder: this.builderPreview,
6334
7506
  blendStart: this.blendStart,
6335
7507
  blendDefaults: this.blendDefaults,
7508
+ gradientEdit: this.gradientEdit,
6336
7509
  canUndo: h.canUndo,
6337
7510
  canRedo: h.canRedo,
6338
7511
  undoLabel: h.undoName,
@@ -6406,6 +7579,13 @@ var VectorEditorController = class {
6406
7579
  return this.builderPreview ? "cell" : "crosshair";
6407
7580
  case "blend":
6408
7581
  return h?.nodeId ? this.blendStart && this.blendStart.nodeId !== h.nodeId ? "copy" : "pointer" : "crosshair";
7582
+ case "gradient": {
7583
+ const part = this.drag?.kind === "gradient" ? this.drag.part : h?.gradient;
7584
+ if (!part) return "crosshair";
7585
+ if (part.kind === "bar") return this.drag ? "grabbing" : "copy";
7586
+ if (part.kind === "handle" && part.handle === "start") return this.drag ? "grabbing" : "move";
7587
+ return "pointer";
7588
+ }
6409
7589
  default:
6410
7590
  return "crosshair";
6411
7591
  }
@@ -6831,6 +8011,233 @@ var VectorEditorController = class {
6831
8011
  this.widthEdit = null;
6832
8012
  return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
6833
8013
  }
8014
+ // -------------------------------------------------------------------------
8015
+ // Gradients
8016
+ // -------------------------------------------------------------------------
8017
+ /** Which paint (fill or stroke) the gradient tool and editor work on. */
8018
+ setGradientTarget(target) {
8019
+ if (target === this.gradientEdit.target) return;
8020
+ this.gradientEdit = { target, stop: null };
8021
+ this.notify();
8022
+ }
8023
+ /** Selects a stop of the gradient being edited (index into its sorted stops), or none. */
8024
+ selectGradientStop(index) {
8025
+ if (index === this.gradientEdit.stop) return;
8026
+ this.gradientEdit = { ...this.gradientEdit, stop: index };
8027
+ this.notify();
8028
+ }
8029
+ /** The gradient tool's annotator for the selection (screen pixels), or null when its paint is not a gradient. */
8030
+ getGradientAnnotator() {
8031
+ return getVectorGradientAnnotator(this.doc, this.selection, this.viewport, this.gradientEdit.target);
8032
+ }
8033
+ /** Unlocked paths of `ids` (default: the selection). */
8034
+ gradientPaths(ids = this.selection) {
8035
+ const out = /* @__PURE__ */ new Map();
8036
+ for (const id of ids) {
8037
+ const node = this.getNode(id);
8038
+ if (node && !node.locked) {
8039
+ for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);
8040
+ }
8041
+ }
8042
+ return [...out.values()];
8043
+ }
8044
+ /**
8045
+ * Edits the gradient of the `target` paint (default: the editor's) of the selected (or given)
8046
+ * paths with `patch`: a colour or no paint becomes a gradient (`patch.type`, default linear,
8047
+ * from the colour to white). Consecutive edits with the same `merge` key are one undo step.
8048
+ */
8049
+ setGradient(patch, options = {}) {
8050
+ const target = options.target ?? this.gradientEdit.target;
8051
+ const paths = this.gradientPaths(options.ids ?? this.selection);
8052
+ if (!paths.length) return { ok: false, message: "Nothing is selected." };
8053
+ const next = /* @__PURE__ */ new Map();
8054
+ for (const path of paths) {
8055
+ const paint = path[target];
8056
+ const base = isVectorGradient(paint) ? paint : defaultVectorGradient(patch.type ?? "linear", paint);
8057
+ const g = applyVectorGradientPatch(base, patch);
8058
+ if (!g) return { ok: false, message: "That is not a valid gradient: it needs at least one stop with an offset and a colour." };
8059
+ next.set(path.id, { ...path, [target]: g });
8060
+ }
8061
+ this.change(options.label ?? "Gradient", () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), options.merge);
8062
+ const stops = next.values().next().value?.[target]?.stops.length ?? 0;
8063
+ if (this.gradientEdit.stop !== null && this.gradientEdit.stop >= stops) this.gradientEdit = { ...this.gradientEdit, stop: null };
8064
+ return { ok: true };
8065
+ }
8066
+ /** Applies `fn` to every target gradient of the selection (paths whose paint is not a gradient are skipped). */
8067
+ editGradients(label, fn, merge) {
8068
+ const target = this.gradientEdit.target;
8069
+ const paths = this.gradientPaths().filter((p) => isVectorGradient(p[target]));
8070
+ if (!paths.length) return { ok: false, message: "The selection has no gradient." };
8071
+ const next = /* @__PURE__ */ new Map();
8072
+ for (const path of paths) {
8073
+ const g = path[target];
8074
+ const stops = fn(sortedVectorGradientStops(g));
8075
+ if (!stops) return { ok: false, message: "A gradient keeps at least two stops." };
8076
+ next.set(path.id, { ...path, [target]: { ...g, stops } });
8077
+ }
8078
+ this.change(label, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);
8079
+ return { ok: true };
8080
+ }
8081
+ /** Changes stop `index` (colour, opacity, offset or midpoint) of the selection's gradients; selects it. */
8082
+ setGradientStop(index, patch, options = {}) {
8083
+ let moved = index;
8084
+ const result = this.editGradients(
8085
+ "Gradient Stop",
8086
+ (stops) => {
8087
+ if (!stops[index]) return stops;
8088
+ let next = stops.map((s, i) => {
8089
+ if (i !== index) return s;
8090
+ const out = { ...s };
8091
+ if (patch.color !== void 0) out.color = patch.color;
8092
+ if (patch.opacity !== void 0 && Number.isFinite(patch.opacity)) {
8093
+ const o = Math.min(1, Math.max(0, patch.opacity));
8094
+ if (o < 1) out.opacity = o;
8095
+ else delete out.opacity;
8096
+ }
8097
+ if (patch.midpoint !== void 0 && Number.isFinite(patch.midpoint)) {
8098
+ const m = Math.min(0.95, Math.max(0.05, patch.midpoint));
8099
+ if (Math.abs(m - 0.5) > 1e-3) out.midpoint = m;
8100
+ else delete out.midpoint;
8101
+ }
8102
+ return out;
8103
+ });
8104
+ if (patch.offset !== void 0 && Number.isFinite(patch.offset)) ({ stops: next, index: moved } = moveVectorGradientStop(next, index, patch.offset));
8105
+ return next;
8106
+ },
8107
+ options.merge
8108
+ );
8109
+ if (result.ok) this.selectGradientStop(moved);
8110
+ return result;
8111
+ }
8112
+ /** Adds a stop at `offset` (painting what the gradient paints there) to the selection's gradients and selects it. */
8113
+ addGradientStop(offset) {
8114
+ let index = 0;
8115
+ const target = this.gradientEdit.target;
8116
+ const result = this.editGradients("Add Gradient Stop", (stops) => {
8117
+ const path = this.gradientPaths().find((p) => isVectorGradient(p[target]));
8118
+ const r = insertVectorGradientStop({ ...path[target], stops }, offset);
8119
+ index = r.index;
8120
+ return r.stops;
8121
+ });
8122
+ if (result.ok) this.selectGradientStop(index);
8123
+ return result;
8124
+ }
8125
+ /** Removes stop `index` of the selection's gradients (never below two stops). */
8126
+ removeGradientStop(index) {
8127
+ const result = this.editGradients("Delete Gradient Stop", (stops) => removeVectorGradientStop(stops, index));
8128
+ if (result.ok) this.selectGradientStop(null);
8129
+ return result;
8130
+ }
8131
+ /** Reverses the selection's gradients (stops and midpoints). */
8132
+ reverseGradient() {
8133
+ return this.setGradient({ reverse: true }, { label: "Reverse Gradient" });
8134
+ }
8135
+ /** `doc` with path `id`'s `target` paint replaced. */
8136
+ withGradient(doc, id, target, g) {
8137
+ return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === id ? { ...n, [target]: g } : n) };
8138
+ }
8139
+ downGradient(screen, p, input) {
8140
+ const before = this.snapshot();
8141
+ const a = this.getGradientAnnotator();
8142
+ let part = a ? hitVectorGradientAnnotator(a, screen) : null;
8143
+ if (a && part) {
8144
+ if (part.kind === "handle" && part.handle === "start" && input.alt && a.gradient.type === "radial") part = { kind: "handle", handle: "focal" };
8145
+ if (part.kind === "stop") this.gradientEdit = { ...this.gradientEdit, stop: part.index };
8146
+ this.drag = { kind: "gradient", part, nodeId: a.nodeId, targets: [a.nodeId], annotator: a, start: screen, startDoc: p, before, base: this.doc, moved: false, stop: part.kind === "stop" ? part.index : null };
8147
+ this.interaction = { type: "gradient", part: part.kind, ...part.kind === "handle" ? { handle: part.handle } : {} };
8148
+ this.notify();
8149
+ return;
8150
+ }
8151
+ const hit = this.hitPath(p);
8152
+ if (hit && !this.gradientPaths().some((n) => n.id === hit.id)) {
8153
+ this.selection = [hit.id];
8154
+ this.gradientEdit = { ...this.gradientEdit, stop: null };
8155
+ }
8156
+ const targets = this.gradientPaths().map((n) => n.id);
8157
+ if (!targets.length) {
8158
+ if (this.selection.length) this.clearSelection();
8159
+ return;
8160
+ }
8161
+ this.drag = { kind: "gradient", part: null, nodeId: null, targets, annotator: null, start: screen, startDoc: p, before, base: this.doc, moved: false, stop: null };
8162
+ this.interaction = { type: "gradient", part: "create" };
8163
+ this.notify();
8164
+ }
8165
+ moveGradient(drag, screen, p, input) {
8166
+ if (!drag.moved && Math.hypot(screen.x - drag.start.x, screen.y - drag.start.y) < 3) return;
8167
+ drag.moved = true;
8168
+ const target = this.gradientEdit.target;
8169
+ const base = drag.base;
8170
+ const constrained = (from, to) => {
8171
+ if (!input.shift) return to;
8172
+ const ang = Math.round(Math.atan2(to.y - from.y, to.x - from.x) / (Math.PI / 4)) * (Math.PI / 4);
8173
+ const len2 = Math.hypot(to.x - from.x, to.y - from.y);
8174
+ return { x: from.x + Math.cos(ang) * len2, y: from.y + Math.sin(ang) * len2 };
8175
+ };
8176
+ if (!drag.part) {
8177
+ const end = constrained(drag.startDoc, p);
8178
+ let doc = base;
8179
+ for (const id2 of drag.targets) {
8180
+ const node2 = findVectorNode(base.nodes, id2);
8181
+ if (!node2) continue;
8182
+ const paint = node2[target];
8183
+ const g2 = isVectorGradient(paint) ? paint : defaultVectorGradient("linear", paint);
8184
+ doc = this.withGradient(doc, id2, target, setVectorGradientVector(g2, vectorPathLocalBounds(node2), this.toLocal(id2, drag.startDoc, base), this.toLocal(id2, end, base)));
8185
+ }
8186
+ this.doc = doc;
8187
+ this.notify(true);
8188
+ return;
8189
+ }
8190
+ const id = drag.nodeId;
8191
+ const node = findVectorNode(base.nodes, id);
8192
+ const g = node[target];
8193
+ const bounds = vectorPathLocalBounds(node);
8194
+ const local = (q) => this.toLocal(id, q, base);
8195
+ const part = drag.part;
8196
+ let next = g;
8197
+ if (part.kind === "handle") {
8198
+ let to = p;
8199
+ if (part.handle === "end" || part.handle === "radius") to = constrained(this.screenToDocument(drag.annotator.start), p);
8200
+ next = moveVectorGradientHandle(g, bounds, part.handle, local(to), local(drag.startDoc));
8201
+ } else if (part.kind === "bar") {
8202
+ next = moveVectorGradientHandle(g, bounds, "start", local(p), local(drag.startDoc));
8203
+ } else {
8204
+ const a = drag.annotator;
8205
+ const pos = vectorGradientBarPosition(a, screen);
8206
+ const stops = sortedVectorGradientStops(g);
8207
+ if (part.kind === "stop") {
8208
+ const off = Math.abs(pos.across - VECTOR_GRADIENT_STOP_GAP) > VECTOR_GRADIENT_OFF_BAR;
8209
+ const removed = off ? removeVectorGradientStop(stops, part.index) : null;
8210
+ if (removed) {
8211
+ next = { ...g, stops: removed };
8212
+ drag.stop = null;
8213
+ } else {
8214
+ const r = moveVectorGradientStop(stops, part.index, Math.min(1, Math.max(0, pos.offset)));
8215
+ next = { ...g, stops: r.stops };
8216
+ drag.stop = r.index;
8217
+ }
8218
+ this.interaction = { type: "gradient", part: "stop", ...removed ? { removing: true } : {} };
8219
+ this.gradientEdit = { ...this.gradientEdit, stop: drag.stop };
8220
+ } else {
8221
+ const m = vectorGradientMidpointFromPosition(stops, part.index, pos.offset);
8222
+ if (m !== null) next = { ...g, stops: stops.map((s, i) => i !== part.index ? s : Math.abs(m - 0.5) < 1e-3 ? (({ midpoint: _m, ...rest }) => rest)(s) : { ...s, midpoint: m }) };
8223
+ }
8224
+ }
8225
+ this.doc = this.withGradient(base, id, target, next);
8226
+ this.notify(true);
8227
+ }
8228
+ upGradient(drag) {
8229
+ const part = drag.part;
8230
+ if (!drag.moved) {
8231
+ if (part?.kind === "bar") {
8232
+ this.addGradientStop(part.offset);
8233
+ return;
8234
+ }
8235
+ this.notify();
8236
+ return;
8237
+ }
8238
+ const label = !part ? "Gradient" : part.kind === "stop" ? drag.stop === null ? "Delete Gradient Stop" : "Move Gradient Stop" : part.kind === "midpoint" ? "Gradient Midpoint" : part.kind === "bar" || part.kind === "handle" && part.handle === "start" ? "Move Gradient" : "Gradient Handle";
8239
+ return this.commit(label, drag.before);
8240
+ }
6834
8241
  /** Removes one width point of a path's profile. */
6835
8242
  removeWidthPoint(ref) {
6836
8243
  return this.removeWidthPoints([ref]);
@@ -6881,11 +8288,11 @@ var VectorEditorController = class {
6881
8288
  if (node?.type !== "path") return { ok: false, message: "There is no such path." };
6882
8289
  const g = vectorStrokeWidthGeometry(node);
6883
8290
  if (!g || !(g.unit > 0)) return { ok: false, message: "The path has no visible stroke." };
6884
- const num = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
6885
- const t = num(values.t);
6886
- const left = num(values.left);
6887
- const right = num(values.right);
6888
- const scale = (w) => w === void 0 ? void 0 : Math.max(0, w) / g.unit;
8291
+ const num2 = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
8292
+ const t = num2(values.t);
8293
+ const left2 = num2(values.left);
8294
+ const right = num2(values.right);
8295
+ const scale3 = (w) => w === void 0 ? void 0 : Math.max(0, w) / g.unit;
6889
8296
  const profile = sanitizeWidthProfile(node.strokeWidthProfile);
6890
8297
  let points;
6891
8298
  let index;
@@ -6893,11 +8300,11 @@ var VectorEditorController = class {
6893
8300
  if (existing === null) {
6894
8301
  if (t === void 0) return { ok: false, message: "A new width point needs a position t." };
6895
8302
  if (profile.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { ok: false, message: "The profile has too many width points." };
6896
- ({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale(left), right: scale(right) }, { step: true }));
8303
+ ({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale3(left2), right: scale3(right) }, { step: true }));
6897
8304
  } else {
6898
8305
  const p = profile[existing];
6899
8306
  if (!p) return { ok: false, message: "There is no such width point." };
6900
- const sides2 = { left: scale(left) ?? p.left, right: scale(right) ?? p.right };
8307
+ const sides2 = { left: scale3(left2) ?? p.left, right: scale3(right) ?? p.right };
6901
8308
  const tt = t === void 0 ? p.t : Math.min(Math.max(t, 0), 1);
6902
8309
  if (Math.abs(tt - p.t) <= VECTOR_WIDTH_SAME_T) {
6903
8310
  points = setWidthPoint(profile, existing, sides2);
@@ -8714,6 +10121,13 @@ var VectorEditorController = class {
8714
10121
  const spot = this.strokeSpotAt(p);
8715
10122
  return spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;
8716
10123
  }
10124
+ case "gradient": {
10125
+ const a = this.getGradientAnnotator();
10126
+ const part = a ? hitVectorGradientAnnotator(a, screen) : null;
10127
+ if (a && part) return { nodeId: a.nodeId, gradient: part };
10128
+ const path = this.hitPath(p);
10129
+ return path ? { nodeId: path.id } : null;
10130
+ }
8717
10131
  default:
8718
10132
  return null;
8719
10133
  }
@@ -8936,6 +10350,8 @@ var VectorEditorController = class {
8936
10350
  return this.downBuilder(p, input);
8937
10351
  case "blend":
8938
10352
  return this.downBlend(screen, p);
10353
+ case "gradient":
10354
+ return this.downGradient(screen, p, input);
8939
10355
  case "scissors": {
8940
10356
  const seg = this.nearestOutline(p);
8941
10357
  if (seg) this.cutPathAt(seg.nodeId, seg.point);
@@ -9227,6 +10643,8 @@ var VectorEditorController = class {
9227
10643
  this.viewport = { ...drag.viewport, x: drag.viewport.x + screen.x - drag.start.x, y: drag.viewport.y + screen.y - drag.start.y };
9228
10644
  this.notify();
9229
10645
  return;
10646
+ case "gradient":
10647
+ return this.moveGradient(drag, screen, p, input);
9230
10648
  case "move": {
9231
10649
  let dx = p.x - drag.start.x;
9232
10650
  let dy = p.y - drag.start.y;
@@ -9602,6 +11020,8 @@ var VectorEditorController = class {
9602
11020
  }
9603
11021
  case "builder":
9604
11022
  return this.upBuilder(drag);
11023
+ case "gradient":
11024
+ return this.upGradient(drag);
9605
11025
  case "stroke": {
9606
11026
  this.notify();
9607
11027
  if (drag.tool === "knife") this.knife(drag.points);
@@ -9816,6 +11236,7 @@ var VectorEditorController = class {
9816
11236
  case "Delete":
9817
11237
  case "Backspace":
9818
11238
  if (this.tool === "width" && this.widthPoints.length) return this.removeWidthPoints().ok;
11239
+ if (this.tool === "gradient" && this.gradientEdit.stop !== null) return this.removeGradientStop(this.gradientEdit.stop).ok;
9819
11240
  if (this.pen) {
9820
11241
  const node = this.getNode(this.pen.nodeId);
9821
11242
  const sp = node.subpaths[this.pen.subpath];
@@ -9924,25 +11345,24 @@ function pencilPressureProfile(points, pressures) {
9924
11345
  }
9925
11346
 
9926
11347
  // src/vector/render.ts
9927
- var fmt = (n) => Math.round(n * 100) / 100;
9928
- var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
11348
+ var fmt4 = (n) => Math.round(n * 100) / 100;
11349
+ var fmtMatrix2 = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
9929
11350
  var VECTOR_BUILDER_HATCH_GAP = 6;
9930
11351
  function renderNode(stored, defs, idPrefix) {
9931
11352
  if (stored.visible === false) return null;
9932
11353
  const node = expandVectorAppearance(stored);
9933
11354
  if (node.type === "group") {
9934
11355
  const children = node.children.map((c) => renderNode(c, defs, idPrefix)).filter((c) => !!c);
9935
- return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix(node.transform) } : {}, opacity: node.opacity ?? 1, children };
11356
+ return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix2(node.transform) } : {}, opacity: node.opacity ?? 1, children };
9936
11357
  }
9937
- const fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`);
9938
- const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
9939
- if (fill.gradient) defs.push(fill.gradient);
9940
- if (stroke.gradient) defs.push(stroke.gradient);
11358
+ const fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`, { node });
11359
+ const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`, { node, role: "stroke" });
11360
+ defs.push(...fill.defs, ...stroke.defs);
9941
11361
  const strokeParts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : void 0;
9942
11362
  return {
9943
11363
  kind: "path",
9944
11364
  id: node.id,
9945
- ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
11365
+ ...node.transform ? { transform: fmtMatrix2(node.transform) } : {},
9946
11366
  ...strokeParts ? { strokeParts } : {},
9947
11367
  d: vectorPathData(node),
9948
11368
  fill: fill.value,
@@ -9961,7 +11381,7 @@ function getVectorSceneRenderData(state, options = {}) {
9961
11381
  const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
9962
11382
  const v2 = state.viewport;
9963
11383
  return {
9964
- transform: `translate(${fmt(v2.x)} ${fmt(v2.y)}) scale(${v2.zoom})`,
11384
+ transform: `translate(${fmt4(v2.x)} ${fmt4(v2.y)}) scale(${v2.zoom})`,
9965
11385
  width: state.document.width,
9966
11386
  height: state.document.height,
9967
11387
  ...state.document.background ? { background: state.document.background } : {},
@@ -10049,7 +11469,7 @@ function getVectorOverlayRenderData(state) {
10049
11469
  const a = toScreen(state, last);
10050
11470
  const p = toScreen(state, state.pointer);
10051
11471
  const c = last.out ? toScreen(state, last.out) : null;
10052
- penPreview = c ? `M${fmt(a.x)} ${fmt(a.y)}Q${fmt(c.x)} ${fmt(c.y)} ${fmt(p.x)} ${fmt(p.y)}` : `M${fmt(a.x)} ${fmt(a.y)}L${fmt(p.x)} ${fmt(p.y)}`;
11472
+ penPreview = c ? `M${fmt4(a.x)} ${fmt4(a.y)}Q${fmt4(c.x)} ${fmt4(c.y)} ${fmt4(p.x)} ${fmt4(p.y)}` : `M${fmt4(a.x)} ${fmt4(a.y)}L${fmt4(p.x)} ${fmt4(p.y)}`;
10053
11473
  }
10054
11474
  }
10055
11475
  const origin = toScreen(state, { x: 0, y: 0 });
@@ -10057,45 +11477,45 @@ function getVectorOverlayRenderData(state) {
10057
11477
  let status = null;
10058
11478
  const it = state.interaction;
10059
11479
  if (it?.type === "rotate") status = `${Math.round(it.angle)}\xB0`;
10060
- else if (it?.type === "move" || it?.type === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
11480
+ else if (it?.type === "move" || it?.type === "anchors") status = `\u0394 ${fmt4(it.dx)}, ${fmt4(it.dy)}`;
10061
11481
  else if (it?.type === "scale" || it?.type === "draw") {
10062
11482
  const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionFrame;
10063
- if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
11483
+ if (b) status = `W ${fmt4(b.width)} \xD7 H ${fmt4(b.height)}`;
10064
11484
  }
10065
11485
  const guides = { lines: [], labels: [] };
10066
11486
  if (state.guides) {
10067
11487
  for (const l of state.guides.lines) {
10068
11488
  const a = toScreen(state, { x: l.x1, y: l.y1 });
10069
11489
  const b = toScreen(state, { x: l.x2, y: l.y2 });
10070
- guides.lines.push({ kind: l.kind, x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) });
11490
+ guides.lines.push({ kind: l.kind, x1: fmt4(a.x), y1: fmt4(a.y), x2: fmt4(b.x), y2: fmt4(b.y) });
10071
11491
  }
10072
11492
  for (const l of state.guides.labels) {
10073
11493
  const p = toScreen(state, l);
10074
- guides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });
11494
+ guides.labels.push({ kind: l.kind, x: fmt4(p.x), y: fmt4(p.y), text: l.text });
10075
11495
  }
10076
11496
  }
10077
11497
  let toolStroke = null;
10078
11498
  if (it?.type === "stroke") {
10079
11499
  const pts = it.points.map((q) => toScreen(state, q));
10080
- const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt(q.x)} ${fmt(q.y)}`).join("") + (pts.length === 1 ? `L${fmt(pts[0].x)} ${fmt(pts[0].y)}` : "");
10081
- toolStroke = { tool: it.tool, d, width: fmt(it.width * z) };
11500
+ const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt4(q.x)} ${fmt4(q.y)}`).join("") + (pts.length === 1 ? `L${fmt4(pts[0].x)} ${fmt4(pts[0].y)}` : "");
11501
+ toolStroke = { tool: it.tool, d, width: fmt4(it.width * z) };
10082
11502
  }
10083
11503
  let pencilString = null;
10084
11504
  if (it?.type === "draw" && it.string) {
10085
11505
  const a = toScreen(state, it.string[0]);
10086
11506
  const b = toScreen(state, it.string[1]);
10087
- pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
11507
+ pencilString = { x1: fmt4(a.x), y1: fmt4(a.y), x2: fmt4(b.x), y2: fmt4(b.y) };
10088
11508
  }
10089
11509
  const dragged = it?.type === "corner" ? it.ref : null;
10090
11510
  const cornerWidgets = getVectorCornerWidgets(state).map((w) => {
10091
11511
  const s = toScreen(state, w);
10092
- return { ref: w.ref, x: fmt(s.x), y: fmt(s.y), radius: w.radius, kind: w.kind, hovered: sameAnchor(state.hover?.corner, w.ref), active: !!dragged };
11512
+ return { ref: w.ref, x: fmt4(s.x), y: fmt4(s.y), radius: w.radius, kind: w.kind, hovered: sameAnchor(state.hover?.corner, w.ref), active: !!dragged };
10093
11513
  });
10094
- if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
10095
- const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
11514
+ if (it?.type === "corner") status = `R ${fmt4(it.radius)}`;
11515
+ const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt4(x), y: fmt4(y) }))(toScreen(state, state.hover.segment.point)) : null;
10096
11516
  const sc = (p) => {
10097
11517
  const q = toScreen(state, p);
10098
- return { x: fmt(q.x), y: fmt(q.y) };
11518
+ return { x: fmt4(q.x), y: fmt4(q.y) };
10099
11519
  };
10100
11520
  const sameWidth = (a, b) => !!a && a.nodeId === b.nodeId && a.index === b.index;
10101
11521
  const widthPoints = getVectorWidthHandles(state).map((h) => {
@@ -10106,7 +11526,7 @@ function getVectorOverlayRenderData(state) {
10106
11526
  });
10107
11527
  const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
10108
11528
  const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
10109
- if (it?.type === "width") status = it.mode === "move" ? "Move width point" : it.mode === "copy" ? "Copy width point" : `W ${fmt(it.width)}`;
11529
+ if (it?.type === "width") status = it.mode === "move" ? "Move width point" : it.mode === "copy" ? "Copy width point" : `W ${fmt4(it.width)}`;
10110
11530
  let builder = null;
10111
11531
  if (state.builder) {
10112
11532
  const v2 = state.viewport;
@@ -10129,7 +11549,43 @@ function getVectorOverlayRenderData(state) {
10129
11549
  }
10130
11550
  const blend = blendOverlay(state);
10131
11551
  if (state.tool === "blend" && !it) status = state.blendStart ? "Click another shape to blend into" : "Click a shape to blend from";
10132
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend };
11552
+ let gradient = null;
11553
+ if (state.tool === "gradient") {
11554
+ gradient = gradientOverlay(state);
11555
+ if (!gradient && !it) status = "Drag across a shape to set a gradient";
11556
+ else if (it?.type === "gradient" && it.removing) status = "Release to delete the stop";
11557
+ }
11558
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend, gradient };
11559
+ }
11560
+ function gradientOverlay(state) {
11561
+ const a = getVectorGradientAnnotator(state.document, state.selection, state.viewport, state.gradientEdit.target);
11562
+ if (!a) return null;
11563
+ const hover = state.interaction ? null : state.hover?.gradient;
11564
+ const handleHovered = (h) => hover?.kind === "handle" && hover.handle === h;
11565
+ const pt2 = (p) => ({ x: fmt4(p.x), y: fmt4(p.y) });
11566
+ const centred = isCentredVectorGradient(a.gradient.type);
11567
+ const removing = state.interaction?.type === "gradient" && !!state.interaction.removing;
11568
+ return {
11569
+ nodeId: a.nodeId,
11570
+ type: a.gradient.type,
11571
+ start: { ...pt2(a.start), hovered: handleHovered("start") },
11572
+ end: { ...pt2(a.end), hovered: handleHovered(centred ? "radius" : "end"), handle: centred ? "radius" : "end" },
11573
+ aspect: a.aspect ? { ...pt2(a.aspect), hovered: handleHovered("aspect") } : null,
11574
+ focal: a.focal ? { ...pt2(a.focal), hovered: handleHovered("focal") } : null,
11575
+ stops: a.stops.map((s) => ({
11576
+ index: s.index,
11577
+ ...pt2(s),
11578
+ barX: fmt4(s.x - a.normal.x * VECTOR_GRADIENT_STOP_GAP),
11579
+ barY: fmt4(s.y - a.normal.y * VECTOR_GRADIENT_STOP_GAP),
11580
+ color: s.color,
11581
+ opacity: s.opacity ?? 1,
11582
+ selected: state.gradientEdit.stop === s.index,
11583
+ hovered: hover?.kind === "stop" && hover.index === s.index
11584
+ })),
11585
+ midpoints: a.midpoints.map((m) => ({ index: m.index, ...pt2(m), hovered: hover?.kind === "midpoint" && hover.index === m.index })),
11586
+ extent: a.extent,
11587
+ removing
11588
+ };
10133
11589
  }
10134
11590
  function selectedBlends(state) {
10135
11591
  const out = /* @__PURE__ */ new Map();
@@ -10146,7 +11602,7 @@ function blendOverlay(state) {
10146
11602
  const doc = state.document;
10147
11603
  const sc = (p, m) => {
10148
11604
  const q = { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };
10149
- return { x: fmt(q.x), y: fmt(q.y) };
11605
+ return { x: fmt4(q.x), y: fmt4(q.y) };
10150
11606
  };
10151
11607
  const spines = selectedBlends(state).flatMap((g) => {
10152
11608
  const s = vectorBlendSpine(g);
@@ -10224,7 +11680,7 @@ var VECTOR_OVERLAY_THEME = {
10224
11680
  builderHatchOpacity: 0.6
10225
11681
  };
10226
11682
  function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
10227
- return `M${fmt(x)} ${fmt(y - r)}L${fmt(x + r)} ${fmt(y)}L${fmt(x)} ${fmt(y + r)}L${fmt(x - r)} ${fmt(y)}Z`;
11683
+ return `M${fmt4(x)} ${fmt4(y - r)}L${fmt4(x + r)} ${fmt4(y)}L${fmt4(x)} ${fmt4(y + r)}L${fmt4(x - r)} ${fmt4(y)}Z`;
10228
11684
  }
10229
11685
  function toVectorModifiers(event) {
10230
11686
  return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
@@ -10329,6 +11785,45 @@ function vectorPaintToHex(paint) {
10329
11785
  const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
10330
11786
  return `#${hex.toLowerCase()}`;
10331
11787
  }
11788
+ var VECTOR_GRADIENT_TYPE_LABELS = { linear: "Linear", radial: "Radial", conic: "Conic", reflected: "Reflected", diamond: "Diamond" };
11789
+ var VECTOR_GRADIENT_SPREAD_LABELS = { pad: "Pad", reflect: "Reflect", repeat: "Repeat" };
11790
+ var VECTOR_GRADIENT_INTERPOLATION_LABELS = { srgb: "sRGB", linear: "Linear light", oklab: "Oklab" };
11791
+ var VECTOR_STROKE_GRADIENT_MODE_LABELS = { within: "Within stroke", along: "Along stroke", across: "Across stroke" };
11792
+ function getVectorGradientEditor(state) {
11793
+ const paths = [];
11794
+ for (const id of state.selection) {
11795
+ const node = findVectorNode(state.document.nodes, id);
11796
+ if (node) paths.push(...collect(node));
11797
+ }
11798
+ if (!paths.length) return null;
11799
+ const target = state.gradientEdit.target;
11800
+ const paint = paths[0][target];
11801
+ const gradient = isVectorGradient(paint) ? paint : null;
11802
+ const sorted = gradient ? sortedVectorGradientStops(gradient) : [];
11803
+ const stopIndex = state.gradientEdit.stop;
11804
+ const stops = sorted.map((s, index) => ({ ...s, index, selected: index === stopIndex }));
11805
+ const sel = stopIndex !== null && sorted[stopIndex] ? { ...sorted[stopIndex], index: stopIndex, last: stopIndex === sorted.length - 1 } : null;
11806
+ const preview = gradient ? `linear-gradient(to right, ${expandVectorGradientStops(gradient).map((s) => {
11807
+ const c = parseVectorColor(s.color);
11808
+ const a = (c?.[3] ?? 1) * (s.opacity ?? 1);
11809
+ const color = c ? `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${Math.round(a * 1e3) / 1e3})` : s.color;
11810
+ return `${color} ${Math.round(s.offset * 1e4) / 100}%`;
11811
+ }).join(", ")})` : null;
11812
+ return {
11813
+ target,
11814
+ gradient,
11815
+ color: vectorPaintToHex(paint),
11816
+ stops,
11817
+ selected: sel,
11818
+ preview,
11819
+ type: gradient?.type ?? null,
11820
+ spread: gradient?.spread ?? "pad",
11821
+ interpolation: gradient?.interpolation ?? "srgb",
11822
+ smoothness: gradient?.smoothness ?? 0,
11823
+ strokeMode: gradient?.strokeMode ?? "within",
11824
+ mixed: paths.some((p) => JSON.stringify(p[target] ?? null) !== JSON.stringify(paint ?? null))
11825
+ };
11826
+ }
10332
11827
  var VECTOR_PATH_ACTIONS = [
10333
11828
  { id: "join", label: "\u22C8", title: "Join", shortcut: "J", enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },
10334
11829
  { id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
@@ -10416,8 +11911,41 @@ var checkBlendOptions = (params) => {
10416
11911
  if (params?.steps !== void 0 && !(Number.isFinite(params.steps) && params.steps >= 1)) fail("steps must be at least 1.");
10417
11912
  if (params?.distance !== void 0 && !(Number.isFinite(params.distance) && params.distance > 0)) fail("distance must be positive.");
10418
11913
  };
11914
+ var POINT = { type: "object", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] };
11915
+ var GRADIENT_PROPS = {
11916
+ type: { type: "string", enum: VECTOR_GRADIENT_TYPES, description: "linear, radial, conic (an angle sweep), reflected (linear mirrored about the start) or diamond." },
11917
+ stops: {
11918
+ type: "array",
11919
+ minItems: 1,
11920
+ items: {
11921
+ type: "object",
11922
+ properties: {
11923
+ offset: { type: "number", minimum: 0, maximum: 1 },
11924
+ color: { type: "string", description: "A CSS colour." },
11925
+ opacity: { type: "number", minimum: 0, maximum: 1 },
11926
+ midpoint: { type: "number", minimum: 0.05, maximum: 0.95, description: "Where between this stop and the next the colours are half-mixed (default 0.5)." }
11927
+ },
11928
+ required: ["offset", "color"]
11929
+ }
11930
+ },
11931
+ angle: { type: "number", description: "Shorthand direction in degrees (0 up, clockwise): linear / reflected direction across the box, conic start angle, diamond turn." },
11932
+ cx: { type: "number", description: "Shorthand centre x of radial / conic / diamond gradients, a fraction of the box (default 0.5)." },
11933
+ cy: { type: "number" },
11934
+ r: { type: "number", description: "Shorthand radius, a fraction of the box (default 0.5)." },
11935
+ start: { ...POINT, description: "Explicit start (centre of radial / conic / diamond), in `units`. Wins over the shorthand." },
11936
+ end: { ...POINT, description: "Explicit end of the main axis (the radius of radial / conic / diamond)." },
11937
+ width: { ...POINT, description: "End of the secondary axis (aspect and turn of radial / conic / diamond); default square to the main axis, as long." },
11938
+ focal: { ...POINT, description: "Radial focal point (where t = 0), in `units`." },
11939
+ focalRadius: { type: "number", minimum: 0, maximum: 0.99, description: "Radial focal circle radius as a fraction of the radius (SVG fr)." },
11940
+ units: { type: "string", enum: ["objectBoundingBox", "userSpaceOnUse"], description: "Space of start / end / width / focal: fractions of the shape box (default) or the node's own coordinates." },
11941
+ spread: { type: "string", enum: VECTOR_GRADIENT_SPREADS, description: "What paints past the ends: pad (default), reflect or repeat." },
11942
+ interpolation: { type: "string", enum: VECTOR_GRADIENT_INTERPOLATIONS, description: "Colour mixing space: srgb (default), linear (light) or oklab." },
11943
+ smoothness: { type: "number", minimum: 0, maximum: 1, description: "Bends the ramp along a smooth spline through the stops (0 = straight)." },
11944
+ strokeMode: { type: "string", enum: VECTOR_STROKE_GRADIENT_MODES, description: "On strokes: within (default, like a fill), along the path or across it." }
11945
+ };
10419
11946
  var PAINT = {
10420
- description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
11947
+ description: 'A CSS colour, `null` for none, or a gradient object: `{ type: "linear" | "radial" | "conic" | "reflected" | "diamond", stops: [{ offset, color, opacity?, midpoint? }] }` plus the shorthand `angle` / `cx, cy, r` or explicit `start`, `end`, `width`, `focal`, `focalRadius`, `units`, and `spread`, `interpolation`, `smoothness`, `strokeMode` (see vector.setGradient).',
11948
+ anyOf: [{ type: "string" }, { type: "null" }, { type: "object", properties: GRADIENT_PROPS, required: ["type", "stops"] }]
10421
11949
  };
10422
11950
  var STYLE_PROPS = {
10423
11951
  fill: PAINT,
@@ -10438,6 +11966,10 @@ var STYLE_PROPS = {
10438
11966
  var pickStyle = (params) => {
10439
11967
  const out = {};
10440
11968
  for (const key of Object.keys(STYLE_PROPS)) if (params[key] !== void 0) out[key] = params[key];
11969
+ for (const key of ["fill", "stroke"]) {
11970
+ const paint = out[key];
11971
+ if (paint && typeof paint === "object") out[key] = sanitizeVectorGradient(paint) ?? fail(`${key} is not a valid gradient: give a type and at least one stop { offset, color }.`);
11972
+ }
10441
11973
  return out;
10442
11974
  };
10443
11975
  var check = (result) => {
@@ -10578,6 +12110,26 @@ function createVectorCommands(controller, options = {}) {
10578
12110
  return { ok: true };
10579
12111
  }
10580
12112
  },
12113
+ {
12114
+ ...base,
12115
+ id: `${prefix}setGradient`,
12116
+ label: "Gradient\u2026",
12117
+ keywords: ["gradient", "linear", "radial", "conic", "angle", "reflected", "diamond", "stops", "midpoint", "oklab", "spread", "focal", "along stroke", "across stroke", "reverse"],
12118
+ description: "Edits the gradient of the fill (default) or stroke of paths (groups apply to every path inside): any gradient field (missing ones keep their value; a colour becomes a gradient from it to white), `aspect` (secondary axis over main axis), `reverse` (flip the stops and midpoints), `focal` / `width` null to remove them. Midpoints, smoothness and oklab / linear interpolation export to SVG as extra stops; conic and diamond gradients as patterns.",
12119
+ params: { type: "object", properties: { ids: IDS, target: { type: "string", enum: ["fill", "stroke"] }, ...GRADIENT_PROPS, aspect: { type: "number" }, reverse: { type: "boolean" } } },
12120
+ enabled: enabledFor,
12121
+ undoable: true,
12122
+ run: (_ctx, params) => {
12123
+ const { ids: _i, target, ...patch } = params ?? {};
12124
+ if (patch.type !== void 0 && !VECTOR_GRADIENT_TYPES.includes(patch.type)) fail(`Unknown gradient type "${patch.type}".`);
12125
+ if (patch.stops !== void 0 && !(Array.isArray(patch.stops) && patch.stops.length)) fail("stops needs at least one { offset, color }.");
12126
+ check(controller.setGradient(patch, { ids: ids(params), target: target ?? "fill" }));
12127
+ const id = ids(params)[0];
12128
+ const node = id ? controller.getNode(id) : void 0;
12129
+ const path = node?.type === "path" ? node : void 0;
12130
+ return { ok: true, ...path ? { gradient: path[target ?? "fill"] } : {} };
12131
+ }
12132
+ },
10581
12133
  {
10582
12134
  ...base,
10583
12135
  id: `${prefix}setCorners`,
@@ -11269,6 +12821,17 @@ export {
11269
12821
  VECTOR_EFFECT_PARAM_SPECS,
11270
12822
  VECTOR_EFFECT_TYPES,
11271
12823
  VECTOR_FILL_GAP,
12824
+ VECTOR_GRADIENT_INTERPOLATIONS,
12825
+ VECTOR_GRADIENT_INTERPOLATION_LABELS,
12826
+ VECTOR_GRADIENT_MAX_PERIODS,
12827
+ VECTOR_GRADIENT_MIDPOINT_GAP,
12828
+ VECTOR_GRADIENT_MIN_STOPS,
12829
+ VECTOR_GRADIENT_OFF_BAR,
12830
+ VECTOR_GRADIENT_SPREADS,
12831
+ VECTOR_GRADIENT_SPREAD_LABELS,
12832
+ VECTOR_GRADIENT_STOP_GAP,
12833
+ VECTOR_GRADIENT_TYPES,
12834
+ VECTOR_GRADIENT_TYPE_LABELS,
11272
12835
  VECTOR_INK_MAX_CORNERS,
11273
12836
  VECTOR_MERGE_MODES,
11274
12837
  VECTOR_OVERLAY_THEME,
@@ -11280,6 +12843,8 @@ export {
11280
12843
  VECTOR_PRESSURE_NOMINAL,
11281
12844
  VECTOR_ROTATE_HANDLE_OFFSET,
11282
12845
  VECTOR_STROKE_ALIGN_LABELS,
12846
+ VECTOR_STROKE_GRADIENT_MODES,
12847
+ VECTOR_STROKE_GRADIENT_MODE_LABELS,
11283
12848
  VECTOR_STYLE_KEYS,
11284
12849
  VECTOR_TOOLS,
11285
12850
  VECTOR_TOOL_GLYPHS,
@@ -11299,6 +12864,8 @@ export {
11299
12864
  applyVectorBlendOptions,
11300
12865
  applyVectorCornersAttribute,
11301
12866
  applyVectorEffects,
12867
+ applyVectorGradientPatch,
12868
+ applyVectorGradientSpread,
11302
12869
  arrowheadInset,
11303
12870
  arrowheadPath,
11304
12871
  arrowheadWeight,
@@ -11314,6 +12881,7 @@ export {
11314
12881
  collectPathNodes,
11315
12882
  constrainToLeave,
11316
12883
  convertAnchor,
12884
+ convertVectorGradient,
11317
12885
  cornerBisectorPoint,
11318
12886
  cornerRadiusChange,
11319
12887
  cornerSetback,
@@ -11325,6 +12893,7 @@ export {
11325
12893
  cutSubpathAt,
11326
12894
  dashIntervals,
11327
12895
  decomposeRigid,
12896
+ defaultVectorGradient,
11328
12897
  effectPathLength,
11329
12898
  effectPathVertexCount,
11330
12899
  effectiveStrokeAlign,
@@ -11338,6 +12907,9 @@ export {
11338
12907
  expandVectorBlend,
11339
12908
  expandVectorCorners,
11340
12909
  expandVectorCornersOf,
12910
+ expandVectorGradientSpread,
12911
+ expandVectorGradientStops,
12912
+ explicitVectorGradient,
11341
12913
  findVectorNode,
11342
12914
  fitCornerRadius,
11343
12915
  foldVectorBooleanOperands,
@@ -11351,6 +12923,8 @@ export {
11351
12923
  getVectorCornerWidgets,
11352
12924
  getVectorEffectsPanel,
11353
12925
  getVectorEnteredGroups,
12926
+ getVectorGradientAnnotator,
12927
+ getVectorGradientEditor,
11354
12928
  getVectorOverlayRenderData,
11355
12929
  getVectorSceneRenderData,
11356
12930
  getVectorSelectionBoxGeometry,
@@ -11359,6 +12933,8 @@ export {
11359
12933
  getVectorWidthHandles,
11360
12934
  getVisibleAnchors,
11361
12935
  getVisibleHandles,
12936
+ gradientMidpointRemap,
12937
+ hasExplicitGradientGeometry,
11362
12938
  hasOwnVectorAppearance,
11363
12939
  hasPaint,
11364
12940
  hasStrokeExtras,
@@ -11367,8 +12943,10 @@ export {
11367
12943
  hitTestVectorNode,
11368
12944
  hitTestVectorNodes,
11369
12945
  hitTestVectorStroke,
12946
+ hitVectorGradientAnnotator,
11370
12947
  inferAnchorType,
11371
12948
  insertAnchor,
12949
+ insertVectorGradientStop,
11372
12950
  insertWidthPoint,
11373
12951
  interpolateMorphContours,
11374
12952
  interpolateRigidMatrix,
@@ -11378,10 +12956,12 @@ export {
11378
12956
  interpolateVectorStyle,
11379
12957
  interpolateWidthProfiles,
11380
12958
  invertMatrix,
12959
+ isCentredVectorGradient,
11381
12960
  isClosedBezierShape,
11382
12961
  isIdentityMatrix,
11383
12962
  isUniformWidthProfile,
11384
12963
  isVectorBlendGroup,
12964
+ isVectorGradient,
11385
12965
  isVectorLiveBoolean,
11386
12966
  isVectorTextTarget,
11387
12967
  joinSubpaths,
@@ -11394,6 +12974,8 @@ export {
11394
12974
  morphContourToSubpath,
11395
12975
  morphPathsCross,
11396
12976
  moveAnchorHandle,
12977
+ moveVectorGradientHandle,
12978
+ moveVectorGradientStop,
11397
12979
  multiplyMatrices,
11398
12980
  nearestSegment,
11399
12981
  nearestStrokePosition,
@@ -11402,8 +12984,12 @@ export {
11402
12984
  normalizeVectorEffectParams,
11403
12985
  offsetPaths,
11404
12986
  parseSvgTransform,
12987
+ parseVectorColor,
11405
12988
  parseVectorSvg,
11406
12989
  pathsCenter,
12990
+ patternAttrs,
12991
+ periodicVectorGradientStops,
12992
+ pinVectorGradient,
11407
12993
  planShapeBuilder,
11408
12994
  polygonSubpath,
11409
12995
  polylineBounds,
@@ -11418,26 +13004,36 @@ export {
11418
13004
  removeAnchors,
11419
13005
  removeAnchorsKeepingShape,
11420
13006
  removeRedundantPoints,
13007
+ removeVectorGradientStop,
11421
13008
  removeWidthPoints,
11422
13009
  repeatPaths,
11423
13010
  resampleGradientStops,
11424
13011
  resolveVectorBlendOptions,
11425
13012
  reverseMorphContour,
11426
13013
  reverseSubpath,
13014
+ reverseVectorGradient,
11427
13015
  reverseWidthProfile,
11428
13016
  rotateMatrix,
11429
13017
  roundCornerPaths,
11430
13018
  roundSubpaths,
13019
+ samplePlainStops,
11431
13020
  samplePolyline,
13021
+ sampleVectorGradient,
13022
+ sampleVectorGradientAt,
13023
+ sampleVectorPaintDefAt,
11432
13024
  sanitizeVectorEffects,
13025
+ sanitizeVectorGradient,
11433
13026
  sanitizeWidthProfile,
11434
13027
  scaleFor,
11435
13028
  scaleMatrix,
11436
13029
  setAnchorCorner,
11437
13030
  setSubpathCorners,
13031
+ setVectorGradientVector,
11438
13032
  setWidthPoint,
11439
13033
  simplifyPolyline,
11440
13034
  smoothSubpath,
13035
+ sortedVectorGradientStops,
13036
+ spanVectorGradientStops,
11441
13037
  splitIntoFaces,
11442
13038
  splitSubpathAt,
11443
13039
  starSubpath,
@@ -11454,7 +13050,9 @@ export {
11454
13050
  toVectorModifiers,
11455
13051
  toVectorPointerInput,
11456
13052
  transformVectorEffects,
13053
+ transformVectorGradient,
11457
13054
  transformVectorNode,
13055
+ transformVectorPaint,
11458
13056
  translateAnchor,
11459
13057
  translateMatrix,
11460
13058
  trimIntervals,
@@ -11475,6 +13073,7 @@ export {
11475
13073
  vectorBlendRailAt,
11476
13074
  vectorBlendSpine,
11477
13075
  vectorBlendStepCount,
13076
+ vectorBoxMatrix,
11478
13077
  vectorBuilderErasedEdges,
11479
13078
  vectorCornerAt,
11480
13079
  vectorCorners,
@@ -11483,6 +13082,22 @@ export {
11483
13082
  vectorDocumentToSvg,
11484
13083
  vectorEnteredGroups,
11485
13084
  vectorFillMap,
13085
+ vectorGradientArea,
13086
+ vectorGradientAspect,
13087
+ vectorGradientBarPosition,
13088
+ vectorGradientFocalParam,
13089
+ vectorGradientHandles,
13090
+ vectorGradientMatrix,
13091
+ vectorGradientMidpointFromPosition,
13092
+ vectorGradientMidpointPosition,
13093
+ vectorGradientNeedsExpansion,
13094
+ vectorGradientParam,
13095
+ vectorGradientPoints,
13096
+ vectorGradientRamp,
13097
+ vectorGradientSvgDefs,
13098
+ vectorGradientTAt,
13099
+ vectorGradientTargetPath,
13100
+ vectorGradientUnitsMatrix,
11486
13101
  vectorInkShapeName,
11487
13102
  vectorLiveBooleanSteps,
11488
13103
  vectorNodeBounds,
@@ -11494,6 +13109,7 @@ export {
11494
13109
  vectorParentMatrix,
11495
13110
  vectorPathData,
11496
13111
  vectorPathEntries,
13112
+ vectorPathLocalBounds,
11497
13113
  vectorPathWorldBezier,
11498
13114
  vectorPathfinderShape,
11499
13115
  vectorRepeaterCopies,
@@ -11501,6 +13117,7 @@ export {
11501
13117
  vectorResultStyle,
11502
13118
  vectorShapeBuilderHit,
11503
13119
  vectorShapeBuilderMap,
13120
+ vectorStrokeGradientDefs,
11504
13121
  vectorStrokeLineArea,
11505
13122
  vectorStrokeOutline,
11506
13123
  vectorStrokeOutlinePath,