@pixerate/editor 0.237.0 → 0.239.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.
Files changed (94) hide show
  1. package/dist/agent/index.cjs +40 -39
  2. package/dist/agent/index.js +9 -8
  3. package/dist/animation/index.cjs +7 -6
  4. package/dist/animation/index.js +6 -5
  5. package/dist/animation-editor/index.cjs +66 -65
  6. package/dist/animation-editor/index.js +9 -8
  7. package/dist/{brushstrokeRender-WDTKD54B.cjs → brushstrokeRender-MPOM7BFW.cjs} +7 -5
  8. package/dist/{brushstrokeRender-MAHOXL7K.js → brushstrokeRender-WB67GMUP.js} +4 -2
  9. package/dist/canvas/index.cjs +55 -54
  10. package/dist/canvas/index.js +10 -9
  11. package/dist/chart/index.cjs +159 -158
  12. package/dist/chart/index.js +9 -8
  13. package/dist/{chunk-L6Y7TSQ4.js → chunk-36ZTV7WO.js} +1 -1
  14. package/dist/{chunk-JVNGCS26.js → chunk-3E36VDMJ.js} +1 -1
  15. package/dist/{chunk-WRZ6C4TU.cjs → chunk-3R5P3DVY.cjs} +6 -6
  16. package/dist/{chunk-RUONPPMC.cjs → chunk-44BMJJVA.cjs} +8 -8
  17. package/dist/{chunk-YBAPT7NL.js → chunk-6ADHGQ73.js} +4 -2
  18. package/dist/chunk-7AHBW2CQ.cjs +200 -0
  19. package/dist/{chunk-EUIDTZNQ.cjs → chunk-7PT4RJL5.cjs} +113 -111
  20. package/dist/{chunk-CU6ANS5L.js → chunk-7YGBSFC6.js} +2 -2
  21. package/dist/{chunk-ATKKG76G.cjs → chunk-AD7GA3TL.cjs} +23 -23
  22. package/dist/{chunk-FSLKBQU7.js → chunk-AIS56JOO.js} +3 -3
  23. package/dist/{chunk-5VQXSS2S.cjs → chunk-BBUE3UJL.cjs} +86 -86
  24. package/dist/{chunk-7272D3K2.js → chunk-BP6KSGVD.js} +6 -4
  25. package/dist/{chunk-F6R67I3A.js → chunk-CXXXAZ7H.js} +1 -1
  26. package/dist/{chunk-5DQPDXOM.cjs → chunk-D2SGV525.cjs} +76 -76
  27. package/dist/chunk-F67XMVDI.js +200 -0
  28. package/dist/{chunk-JVSX5QTS.js → chunk-FQVVO5YA.js} +3 -3
  29. package/dist/{chunk-UXETV2NC.cjs → chunk-GA4EU4P3.cjs} +4 -4
  30. package/dist/{chunk-UFEDHACQ.js → chunk-GXAK6Q5J.js} +2 -2
  31. package/dist/{chunk-MHLU6GRJ.js → chunk-JEGJP4OW.js} +7 -196
  32. package/dist/chunk-KMPM5JM5.js +936 -0
  33. package/dist/{chunk-GOBOCZZI.js → chunk-LEQKZGQI.js} +1 -1
  34. package/dist/{chunk-M5GLQQWG.cjs → chunk-LJZRHVZV.cjs} +2 -2
  35. package/dist/{chunk-N76AMOKE.cjs → chunk-MOW3JMNG.cjs} +42 -42
  36. package/dist/{chunk-WKMUBATU.js → chunk-N4DFDI73.js} +1 -1
  37. package/dist/{chunk-GNZI3KYK.cjs → chunk-NKTNLJUJ.cjs} +49 -9
  38. package/dist/{chunk-TQOL6CF7.js → chunk-PJYG6FCF.js} +49 -9
  39. package/dist/{chunk-V3L7U63N.cjs → chunk-PP6BXVRL.cjs} +1 -1
  40. package/dist/{chunk-TFF2GTGN.cjs → chunk-PPFJHTW5.cjs} +24 -22
  41. package/dist/{chunk-6ZZKRH7R.js → chunk-QARCM66V.js} +3 -3
  42. package/dist/{chunk-5UEFBM2M.cjs → chunk-RD7RX52G.cjs} +18 -207
  43. package/dist/{chunk-CDP2AKMA.js → chunk-RV5DKZHU.js} +1 -1
  44. package/dist/chunk-S6OT6CTS.cjs +936 -0
  45. package/dist/{chunk-HUAISCUX.cjs → chunk-T7KQEMAE.cjs} +18 -18
  46. package/dist/{chunk-BQE3Y6ZO.cjs → chunk-THXFU3JB.cjs} +5 -5
  47. package/dist/{chunk-4ZC5QJE2.cjs → chunk-UTI5IITZ.cjs} +3 -3
  48. package/dist/{chunk-XSVUNSDJ.js → chunk-UUBO6RB5.js} +1 -1
  49. package/dist/{chunk-CZWUDFR3.cjs → chunk-UZU6DIZA.cjs} +52 -52
  50. package/dist/{chunk-M2TPMYJE.js → chunk-V6DT2CDH.js} +1 -1
  51. package/dist/{chunk-WV5WTOC6.cjs → chunk-W7GRGGBH.cjs} +16 -16
  52. package/dist/{chunk-23E5GI2U.js → chunk-WGN7AUKB.js} +2 -2
  53. package/dist/{chunk-YMREOA67.js → chunk-XK4SST43.js} +2 -2
  54. package/dist/{chunk-SOSBOS5T.cjs → chunk-XWOLPNBW.cjs} +22 -22
  55. package/dist/{chunk-ILP77UD6.js → chunk-YGQVCQTG.js} +1 -1
  56. package/dist/{chunk-GXLZZVR5.cjs → chunk-ZXNO3ZOX.cjs} +37 -37
  57. package/dist/color/index.cjs +6 -4
  58. package/dist/color/index.js +9 -7
  59. package/dist/export/index.cjs +8 -7
  60. package/dist/export/index.js +7 -6
  61. package/dist/extensions/index.cjs +11 -10
  62. package/dist/extensions/index.js +10 -9
  63. package/dist/gradient/index.cjs +104 -813
  64. package/dist/gradient/index.d.cts +1 -0
  65. package/dist/gradient/index.d.ts +1 -0
  66. package/dist/gradient/index.js +87 -796
  67. package/dist/gradient/sample.d.cts +69 -0
  68. package/dist/gradient/sample.d.ts +69 -0
  69. package/dist/image-editor/index.cjs +826 -778
  70. package/dist/image-editor/index.js +57 -9
  71. package/dist/index.cjs +17 -16
  72. package/dist/index.js +11 -10
  73. package/dist/paint-editor/controller.d.cts +31 -1
  74. package/dist/paint-editor/controller.d.ts +31 -1
  75. package/dist/paint-editor/index.cjs +403 -195
  76. package/dist/paint-editor/index.js +231 -23
  77. package/dist/paint-editor/sticker-tools.d.cts +1 -1
  78. package/dist/paint-editor/sticker-tools.d.ts +1 -1
  79. package/dist/paint-editor/stickers.d.cts +10 -3
  80. package/dist/paint-editor/stickers.d.ts +10 -3
  81. package/dist/paint-editor/ui.d.cts +10 -3
  82. package/dist/paint-editor/ui.d.ts +10 -3
  83. package/dist/tokens/index.cjs +4 -3
  84. package/dist/tokens/index.js +3 -2
  85. package/dist/typography/index.cjs +8 -7
  86. package/dist/typography/index.js +3 -2
  87. package/dist/vector/index.cjs +967 -966
  88. package/dist/vector/index.js +9 -8
  89. package/dist/video-editor/index.cjs +7 -6
  90. package/dist/video-editor/index.js +6 -5
  91. package/package.json +1 -1
  92. package/skills/pixerate-color/SKILL.md +3 -0
  93. package/skills/pixerate-image-editor/SKILL.md +1 -0
  94. package/skills/pixerate-paint-editor/SKILL.md +6 -2
@@ -1,755 +1,42 @@
1
+ import {
2
+ GRADIENT_PRESETS,
3
+ angleFromPointer,
4
+ checkGradient,
5
+ createGradient,
6
+ distributeGradientStops,
7
+ fitGradientStops,
8
+ gradientKeyStep,
9
+ gradientMidpointRemap,
10
+ gradientToCss,
11
+ gradientToCssFallback,
12
+ gradientToSvgDef,
13
+ gradientTrackBackground,
14
+ insertGradientStop,
15
+ isDualColorValue,
16
+ isDualGradient,
17
+ normalizeGradient,
18
+ parseCssGradient,
19
+ removeGradientStop,
20
+ resolveGradient,
21
+ reverseGradient,
22
+ sampleGradient,
23
+ sampleGradientFromLine,
24
+ simplifyGradientStops,
25
+ stopFromTrackPosition,
26
+ trackPositionFromStop,
27
+ updateGradientStop
28
+ } from "../chunk-KMPM5JM5.js";
1
29
  import {
2
30
  HistoryManager
3
31
  } from "../chunk-RNSANXPT.js";
32
+ import "../chunk-BP6KSGVD.js";
33
+ import "../chunk-6ADHGQ73.js";
4
34
  import {
5
- formatDualColor,
6
- isDualColor,
7
- mixColors,
8
- normalizeDualColor,
9
- resolveColor
10
- } from "../chunk-7272D3K2.js";
11
- import {
12
- deltaEOK,
13
- inSrgbGamut
14
- } from "../chunk-YBAPT7NL.js";
15
- import {
16
- contrastRatio,
17
- parseColor
18
- } from "../chunk-MHLU6GRJ.js";
35
+ isColor
36
+ } from "../chunk-JEGJP4OW.js";
37
+ import "../chunk-F67XMVDI.js";
19
38
  import "../chunk-6ONMQ6WF.js";
20
39
 
21
- // src/gradient/model.ts
22
- function clamp(v, min, max) {
23
- return Math.max(min, Math.min(max, v));
24
- }
25
- function gradientMidpointRemap(u, m) {
26
- const mid = clamp(m, 0.05, 0.95);
27
- if (Math.abs(mid - 0.5) < 1e-6) return u;
28
- return u <= mid ? 0.5 * u / mid : 0.5 + 0.5 * (u - mid) / (1 - mid);
29
- }
30
- function normalizeGradient(input) {
31
- if (typeof input === "string") {
32
- const parsed = parseCssGradient(input);
33
- if (!parsed) {
34
- return { error: `Could not parse CSS gradient: "${input}".` };
35
- }
36
- return { gradient: parsed };
37
- }
38
- if (!input || typeof input !== "object") {
39
- return { error: "Gradient must be an object." };
40
- }
41
- const obj = input;
42
- const type = obj.type === "radial" || obj.type === "conic" ? obj.type : "linear";
43
- if (!Array.isArray(obj.stops)) {
44
- return { error: "Gradient stops must be an array." };
45
- }
46
- if (obj.stops.length < 2) {
47
- return { error: `A gradient needs at least 2 stops; it has ${obj.stops.length}.` };
48
- }
49
- const usedIds = /* @__PURE__ */ new Set();
50
- for (const s of obj.stops) {
51
- if (s && typeof s === "object" && typeof s.id === "string" && s.id.trim()) {
52
- usedIds.add(s.id.trim());
53
- }
54
- }
55
- let nextIdIndex = 1;
56
- const getNextId = () => {
57
- while (usedIds.has(`s${nextIdIndex}`)) {
58
- nextIdIndex++;
59
- }
60
- const id = `s${nextIdIndex}`;
61
- usedIds.add(id);
62
- nextIdIndex++;
63
- return id;
64
- };
65
- const stops = [];
66
- for (let i = 0; i < obj.stops.length; i++) {
67
- const s = obj.stops[i];
68
- if (!s || typeof s !== "object") {
69
- return { error: `Stop at index ${i} must be an object.` };
70
- }
71
- const rawStop = s;
72
- let normColor = null;
73
- if (typeof rawStop.color === "string" || rawStop.color && typeof rawStop.color === "object") {
74
- const dual = normalizeDualColor(rawStop.color);
75
- if (dual) {
76
- normColor = formatDualColor(dual);
77
- }
78
- }
79
- if (!normColor) {
80
- return { error: `Stop at index ${i} has an invalid colour.` };
81
- }
82
- let offset = Number(rawStop.offset);
83
- if (!Number.isFinite(offset)) {
84
- offset = obj.stops.length === 1 ? 0 : i / (obj.stops.length - 1);
85
- }
86
- offset = clamp(offset, 0, 1);
87
- const id = typeof rawStop.id === "string" && rawStop.id.trim() ? rawStop.id.trim() : getNextId();
88
- const stop = {
89
- id,
90
- offset,
91
- color: normColor
92
- };
93
- if (rawStop.midpoint !== void 0 && rawStop.midpoint !== null) {
94
- const m = Number(rawStop.midpoint);
95
- if (Number.isFinite(m)) {
96
- stop.midpoint = clamp(m, 0.05, 0.95);
97
- }
98
- }
99
- stops.push(stop);
100
- }
101
- stops.sort((a, b) => a.offset - b.offset);
102
- const gradient = {
103
- type,
104
- stops
105
- };
106
- if (type === "linear" || type === "conic") {
107
- if (obj.angle !== void 0 && Number.isFinite(Number(obj.angle))) {
108
- gradient.angle = (Number(obj.angle) % 360 + 360) % 360;
109
- } else {
110
- gradient.angle = type === "linear" ? 180 : 0;
111
- }
112
- }
113
- if (type === "radial" || type === "conic") {
114
- if (obj.center && typeof obj.center === "object") {
115
- const cx = Number(obj.center.x);
116
- const cy = Number(obj.center.y);
117
- gradient.center = {
118
- x: Number.isFinite(cx) ? clamp(cx, 0, 1) : 0.5,
119
- y: Number.isFinite(cy) ? clamp(cy, 0, 1) : 0.5
120
- };
121
- } else {
122
- gradient.center = { x: 0.5, y: 0.5 };
123
- }
124
- }
125
- if (type === "radial") {
126
- gradient.shape = obj.shape === "circle" ? "circle" : "ellipse";
127
- }
128
- const allowedInterp = ["srgb", "srgb-linear", "oklab", "oklch"];
129
- if (obj.interpolation && allowedInterp.includes(obj.interpolation)) {
130
- gradient.interpolation = obj.interpolation;
131
- } else {
132
- gradient.interpolation = "oklab";
133
- }
134
- if (obj.repeating) {
135
- gradient.repeating = true;
136
- }
137
- return { gradient };
138
- }
139
- function createGradient(colorsOrOptions, options) {
140
- if (Array.isArray(colorsOrOptions)) {
141
- const colors = colorsOrOptions;
142
- const resolvedColors = colors.length >= 2 ? colors : colors.length === 1 ? [colors[0], colors[0]] : ["#000000", "#ffffff"];
143
- const stops = resolvedColors.map((color, i) => ({
144
- id: `s${i + 1}`,
145
- offset: i / (resolvedColors.length - 1),
146
- color: formatDualColor(normalizeDualColor(color) ?? { light: "#000000", dark: "#000000" })
147
- }));
148
- const res2 = normalizeGradient({
149
- ...options,
150
- stops
151
- });
152
- if ("error" in res2) {
153
- throw new Error(res2.error);
154
- }
155
- return res2.gradient;
156
- }
157
- if (colorsOrOptions && typeof colorsOrOptions === "object" && !Array.isArray(colorsOrOptions)) {
158
- const raw = colorsOrOptions;
159
- let stops = raw.stops;
160
- if (!stops || stops.length === 0) {
161
- stops = [
162
- { id: "s1", offset: 0, color: "#000000" },
163
- { id: "s2", offset: 1, color: "#ffffff" }
164
- ];
165
- } else if (typeof stops[0] === "string") {
166
- const strStops = stops;
167
- stops = strStops.map((color, i) => ({
168
- id: `s${i + 1}`,
169
- offset: i / (strStops.length - 1),
170
- color
171
- }));
172
- }
173
- const res2 = normalizeGradient({
174
- ...raw,
175
- stops
176
- });
177
- if ("error" in res2) {
178
- throw new Error(res2.error);
179
- }
180
- return res2.gradient;
181
- }
182
- const res = normalizeGradient({
183
- ...options,
184
- stops: [
185
- { id: "s1", offset: 0, color: "#000000" },
186
- { id: "s2", offset: 1, color: "#ffffff" }
187
- ]
188
- });
189
- if ("error" in res) {
190
- throw new Error(res.error);
191
- }
192
- return res.gradient;
193
- }
194
- function isDualColorValue(val) {
195
- if (typeof val === "string") {
196
- return val.trim().toLowerCase().startsWith("light-dark(");
197
- }
198
- if (isDualColor(val)) {
199
- return val.light !== val.dark;
200
- }
201
- return false;
202
- }
203
- function isDualGradient(g) {
204
- return g.stops.some((s) => isDualColorValue(s.color));
205
- }
206
- function resolveGradient(g, scheme) {
207
- return {
208
- ...g,
209
- center: g.center ? { ...g.center } : void 0,
210
- stops: g.stops.map((s) => ({
211
- ...s,
212
- color: resolveColor(s.color, scheme) ?? s.color
213
- }))
214
- };
215
- }
216
- function sampleGradient(g, t, scheme = "light") {
217
- const resolved = resolveGradient(g, scheme);
218
- const clampedT = clamp(t, 0, 1);
219
- const stops = resolved.stops;
220
- if (clampedT <= stops[0].offset) {
221
- return stops[0].color;
222
- }
223
- if (clampedT >= stops[stops.length - 1].offset) {
224
- return stops[stops.length - 1].color;
225
- }
226
- let i = 0;
227
- while (i < stops.length - 1 && stops[i + 1].offset < clampedT) {
228
- i++;
229
- }
230
- const s0 = stops[i];
231
- const s1 = stops[i + 1];
232
- const span = s1.offset - s0.offset;
233
- if (span <= 1e-6) {
234
- return s1.color;
235
- }
236
- let u = (clampedT - s0.offset) / span;
237
- if (s0.midpoint !== void 0) {
238
- u = gradientMidpointRemap(u, s0.midpoint);
239
- }
240
- const mixed = mixColors(s0.color, s1.color, u, { space: g.interpolation ?? "oklab" });
241
- return mixed ?? s0.color;
242
- }
243
- function insertGradientStop(g, offset, color) {
244
- const clampedOffset = clamp(offset, 0, 1);
245
- const stopColor = color !== void 0 ? color : sampleGradient(g, clampedOffset, "light");
246
- const used = new Set(g.stops.map((s) => s.id));
247
- let idx = 1;
248
- while (used.has(`s${idx}`)) idx++;
249
- const id = `s${idx}`;
250
- const newStop = {
251
- id,
252
- offset: clampedOffset,
253
- color: stopColor
254
- };
255
- const stops = [...g.stops, newStop];
256
- const res = normalizeGradient({ ...g, stops });
257
- if ("error" in res) throw new Error(res.error);
258
- const added = res.gradient.stops.find((s) => s.id === id);
259
- return { gradient: res.gradient, stop: added };
260
- }
261
- function listValidIds(g) {
262
- return g.stops.map((s) => `"${s.id}"`).join(", ");
263
- }
264
- function updateGradientStop(g, id, patch) {
265
- const index = g.stops.findIndex((s) => s.id === id);
266
- if (index === -1) {
267
- throw new Error(`Stop "${id}" not found. Valid stop IDs are: ${listValidIds(g)}.`);
268
- }
269
- const stops = g.stops.map((s, i) => i === index ? { ...s, ...patch } : s);
270
- const res = normalizeGradient({ ...g, stops });
271
- if ("error" in res) throw new Error(res.error);
272
- return res.gradient;
273
- }
274
- function removeGradientStop(g, id) {
275
- if (g.stops.length <= 2) {
276
- throw new Error("A gradient needs at least 2 stops; add one with add_gradient_stop before removing this one.");
277
- }
278
- const index = g.stops.findIndex((s) => s.id === id);
279
- if (index === -1) {
280
- throw new Error(`Stop "${id}" not found. Valid stop IDs are: ${listValidIds(g)}.`);
281
- }
282
- const stops = g.stops.filter((s) => s.id !== id);
283
- const res = normalizeGradient({ ...g, stops });
284
- if ("error" in res) throw new Error(res.error);
285
- return res.gradient;
286
- }
287
- function reverseGradient(g) {
288
- const reversedStops = g.stops.map((s) => ({
289
- ...s,
290
- offset: Math.round((1 - s.offset) * 1e4) / 1e4,
291
- midpoint: s.midpoint !== void 0 ? Math.round((1 - s.midpoint) * 1e4) / 1e4 : void 0
292
- }));
293
- reversedStops.sort((a, b) => a.offset - b.offset);
294
- return {
295
- ...g,
296
- center: g.center ? { ...g.center } : void 0,
297
- stops: reversedStops
298
- };
299
- }
300
- function distributeGradientStops(g) {
301
- const n = g.stops.length;
302
- const distributed = g.stops.map((s, i) => ({
303
- ...s,
304
- offset: Math.round(i / (n - 1) * 1e4) / 1e4
305
- }));
306
- return {
307
- ...g,
308
- center: g.center ? { ...g.center } : void 0,
309
- stops: distributed
310
- };
311
- }
312
- function formatPercent(fraction) {
313
- const pct = Math.round(fraction * 1e3) / 10;
314
- return `${pct}%`;
315
- }
316
- function gradientToCss(g) {
317
- const prefix = g.repeating ? `repeating-${g.type}-gradient` : `${g.type}-gradient`;
318
- const parts = [];
319
- const interpPrefix = g.interpolation && g.interpolation !== "srgb" ? `in ${g.interpolation} ` : "";
320
- if (g.type === "linear") {
321
- const angle = g.angle !== void 0 ? `${g.angle}deg` : "180deg";
322
- parts.push(`${interpPrefix}${angle}`.trim());
323
- } else if (g.type === "radial") {
324
- const shape = g.shape ?? "ellipse";
325
- const cx = g.center ? formatPercent(g.center.x) : "50%";
326
- const cy = g.center ? formatPercent(g.center.y) : "50%";
327
- parts.push(`${interpPrefix}${shape} at ${cx} ${cy}`.trim());
328
- } else if (g.type === "conic") {
329
- const angle = g.angle !== void 0 ? `from ${g.angle}deg` : "from 0deg";
330
- const cx = g.center ? formatPercent(g.center.x) : "50%";
331
- const cy = g.center ? formatPercent(g.center.y) : "50%";
332
- parts.push(`${interpPrefix}${angle} at ${cx} ${cy}`.trim());
333
- }
334
- for (let i = 0; i < g.stops.length; i++) {
335
- const s = g.stops[i];
336
- parts.push(`${s.color} ${formatPercent(s.offset)}`);
337
- if (s.midpoint !== void 0 && i < g.stops.length - 1) {
338
- const next = g.stops[i + 1];
339
- const midOffset = s.offset + (next.offset - s.offset) * s.midpoint;
340
- parts.push(formatPercent(midOffset));
341
- }
342
- }
343
- return `${prefix}(${parts.join(", ")})`;
344
- }
345
- function gradientToCssFallback(g, steps = 16, scheme = "light") {
346
- const prefix = g.repeating ? `repeating-${g.type}-gradient` : `${g.type}-gradient`;
347
- const parts = [];
348
- if (g.type === "linear") {
349
- const angle = g.angle !== void 0 ? `${g.angle}deg` : "180deg";
350
- parts.push(angle);
351
- } else if (g.type === "radial") {
352
- const shape = g.shape ?? "ellipse";
353
- const cx = g.center ? formatPercent(g.center.x) : "50%";
354
- const cy = g.center ? formatPercent(g.center.y) : "50%";
355
- parts.push(`${shape} at ${cx} ${cy}`);
356
- } else if (g.type === "conic") {
357
- const angle = g.angle !== void 0 ? `from ${g.angle}deg` : "from 0deg";
358
- const cx = g.center ? formatPercent(g.center.x) : "50%";
359
- const cy = g.center ? formatPercent(g.center.y) : "50%";
360
- parts.push(`${angle} at ${cx} ${cy}`);
361
- }
362
- for (let i = 0; i <= steps; i++) {
363
- const t = i / steps;
364
- const color = sampleGradient(g, t, scheme);
365
- parts.push(`${color} ${formatPercent(t)}`);
366
- }
367
- return `${prefix}(${parts.join(", ")})`;
368
- }
369
- function splitCssArgs(str) {
370
- const parts = [];
371
- let depth = 0;
372
- let current = "";
373
- for (let i = 0; i < str.length; i++) {
374
- const char = str[i];
375
- if (char === "(") depth++;
376
- else if (char === ")") depth = Math.max(0, depth - 1);
377
- if (char === "," && depth === 0) {
378
- if (current.trim()) parts.push(current.trim());
379
- current = "";
380
- } else {
381
- current += char;
382
- }
383
- }
384
- if (current.trim()) parts.push(current.trim());
385
- return parts;
386
- }
387
- function parseCssAngle(val) {
388
- const trimmed = val.trim().toLowerCase();
389
- if (trimmed.endsWith("deg")) return parseFloat(trimmed);
390
- if (trimmed.endsWith("turn")) return parseFloat(trimmed) * 360;
391
- if (trimmed.endsWith("rad")) return parseFloat(trimmed) * 180 / Math.PI;
392
- if (trimmed.startsWith("to ")) {
393
- const dir = trimmed.slice(3).trim();
394
- if (dir === "bottom") return 180;
395
- if (dir === "top") return 0;
396
- if (dir === "right") return 90;
397
- if (dir === "left") return 270;
398
- if (dir === "bottom right" || dir === "right bottom") return 135;
399
- if (dir === "bottom left" || dir === "left bottom") return 225;
400
- if (dir === "top right" || dir === "right top") return 45;
401
- if (dir === "top left" || dir === "left top") return 315;
402
- }
403
- const n = parseFloat(trimmed);
404
- return Number.isFinite(n) ? n : 180;
405
- }
406
- function parseCssGradient(css) {
407
- if (typeof css !== "string") return null;
408
- const trimmed = css.trim();
409
- const match = trimmed.match(/^(repeating-)?(linear|radial|conic)-gradient\s*\(([\s\S]*)\)$/i);
410
- if (!match) return null;
411
- const repeating = Boolean(match[1]);
412
- const type = match[2].toLowerCase();
413
- const body = match[3];
414
- const rawArgs = splitCssArgs(body);
415
- if (rawArgs.length === 0) return null;
416
- let angle = type === "linear" ? 180 : 0;
417
- let center = type !== "linear" ? { x: 0.5, y: 0.5 } : void 0;
418
- let shape = type === "radial" ? "ellipse" : void 0;
419
- let interpolation = "srgb";
420
- let stopStartIndex = 0;
421
- let first = rawArgs[0].trim();
422
- const inMatch = first.match(/^in\s+([a-z0-9-]+)\s*(.*)$/i);
423
- if (inMatch) {
424
- const space = inMatch[1].toLowerCase();
425
- if (space === "oklab" || space === "oklch" || space === "srgb" || space === "srgb-linear") {
426
- interpolation = space;
427
- }
428
- first = inMatch[2].trim();
429
- if (!first) {
430
- stopStartIndex = 1;
431
- first = rawArgs[1]?.trim() ?? "";
432
- }
433
- }
434
- if (first) {
435
- if (type === "linear") {
436
- if (first.endsWith("deg") || first.endsWith("turn") || first.endsWith("rad") || first.startsWith("to ")) {
437
- angle = parseCssAngle(first);
438
- stopStartIndex = stopStartIndex === 0 ? 1 : 2;
439
- }
440
- } else if (type === "radial") {
441
- if (first.includes("circle") || first.includes("ellipse") || first.includes("at ")) {
442
- if (first.includes("circle")) shape = "circle";
443
- if (first.includes("ellipse")) shape = "ellipse";
444
- const atIdx = first.indexOf("at ");
445
- if (atIdx !== -1) {
446
- const posParts = first.slice(atIdx + 3).trim().split(/\s+/);
447
- if (posParts[0]) center = { x: parsePos(posParts[0]), y: posParts[1] ? parsePos(posParts[1]) : parsePos(posParts[0]) };
448
- }
449
- stopStartIndex = stopStartIndex === 0 ? 1 : 2;
450
- }
451
- } else if (type === "conic") {
452
- if (first.includes("from ") || first.includes("at ")) {
453
- const fromMatch = first.match(/from\s+([0-9.]+(?:deg|turn|rad)?)/i);
454
- if (fromMatch) angle = parseCssAngle(fromMatch[1]);
455
- const atIdx = first.indexOf("at ");
456
- if (atIdx !== -1) {
457
- const posParts = first.slice(atIdx + 3).trim().split(/\s+/);
458
- if (posParts[0]) center = { x: parsePos(posParts[0]), y: posParts[1] ? parsePos(posParts[1]) : parsePos(posParts[0]) };
459
- }
460
- stopStartIndex = stopStartIndex === 0 ? 1 : 2;
461
- }
462
- }
463
- }
464
- const stopArgs = rawArgs.slice(stopStartIndex);
465
- if (stopArgs.length === 0) return null;
466
- const items = [];
467
- for (const str of stopArgs) {
468
- const s = str.trim();
469
- if (/^([0-9.]+)%$/.test(s)) {
470
- items.push({ kind: "hint", offset: parseFloat(s) / 100 });
471
- continue;
472
- }
473
- const posMatch = s.match(/\s+([0-9.]+)%$/);
474
- if (posMatch) {
475
- const colorStr = s.slice(0, posMatch.index).trim();
476
- const offset = parseFloat(posMatch[1]) / 100;
477
- items.push({ kind: "stop", color: colorStr, offset });
478
- } else {
479
- items.push({ kind: "stop", color: s });
480
- }
481
- }
482
- const rawStops = items.filter((it) => it.kind === "stop");
483
- if (rawStops.length < 2) return null;
484
- for (let i = 0; i < rawStops.length; i++) {
485
- if (rawStops[i].offset === void 0) {
486
- if (i === 0) rawStops[i].offset = 0;
487
- else if (i === rawStops.length - 1) rawStops[i].offset = 1;
488
- else {
489
- let nextIdx = i + 1;
490
- while (nextIdx < rawStops.length && rawStops[nextIdx].offset === void 0) {
491
- nextIdx++;
492
- }
493
- const nextOffset = nextIdx < rawStops.length ? rawStops[nextIdx].offset : 1;
494
- const prevOffset = rawStops[i - 1].offset ?? 0;
495
- const count = nextIdx - (i - 1);
496
- for (let k = i; k < nextIdx; k++) {
497
- rawStops[k].offset = prevOffset + (k - (i - 1)) / count * (nextOffset - prevOffset);
498
- }
499
- i = nextIdx - 1;
500
- }
501
- }
502
- }
503
- const finalStops = [];
504
- let stopIdx = 0;
505
- for (let i = 0; i < items.length; i++) {
506
- const it = items[i];
507
- if (it.kind === "stop") {
508
- const stop = {
509
- id: `s${stopIdx + 1}`,
510
- offset: clamp(it.offset ?? 0, 0, 1),
511
- color: it.color
512
- };
513
- if (i + 1 < items.length && items[i + 1].kind === "hint") {
514
- const hintOffset = items[i + 1].offset;
515
- const nextStopItem = items.slice(i + 2).find((x) => x.kind === "stop");
516
- if (nextStopItem && nextStopItem.offset !== void 0) {
517
- const span = nextStopItem.offset - stop.offset;
518
- if (span > 0) {
519
- stop.midpoint = clamp((hintOffset - stop.offset) / span, 0.05, 0.95);
520
- }
521
- }
522
- }
523
- finalStops.push(stop);
524
- stopIdx++;
525
- }
526
- }
527
- const res = normalizeGradient({
528
- type,
529
- angle,
530
- center,
531
- shape,
532
- interpolation,
533
- repeating,
534
- stops: finalStops
535
- });
536
- return "gradient" in res ? res.gradient : null;
537
- }
538
- function parsePos(str) {
539
- const s = str.trim();
540
- if (s.endsWith("%")) return clamp(parseFloat(s) / 100, 0, 1);
541
- const val = parseFloat(s);
542
- return Number.isFinite(val) ? clamp(val, 0, 1) : 0.5;
543
- }
544
- function gradientToSvgDef(g, id, scheme = "light") {
545
- const steps = 32;
546
- const stopsXml = [];
547
- for (let i = 0; i <= steps; i++) {
548
- const t = i / steps;
549
- const color = sampleGradient(g, t, scheme);
550
- stopsXml.push(` <stop offset="${formatPercent(t)}" stop-color="${color}" />`);
551
- }
552
- if (g.type === "radial") {
553
- const cx = g.center ? formatPercent(g.center.x) : "50%";
554
- const cy = g.center ? formatPercent(g.center.y) : "50%";
555
- const r = g.shape === "circle" ? "50%" : "70.7%";
556
- return `<radialGradient id="${id}" cx="${cx}" cy="${cy}" r="${r}">
557
- ${stopsXml.join("\n")}
558
- </radialGradient>`;
559
- }
560
- if (g.type === "conic") {
561
- return `<!-- Note: SVG does not natively support conic gradients; rendering as linear fallback -->
562
- <linearGradient id="${id}" x1="0%" y1="0%" x2="100%" y2="0%">
563
- ${stopsXml.join("\n")}
564
- </linearGradient>`;
565
- }
566
- const angle = g.angle ?? 180;
567
- const rad = (90 - angle) * Math.PI / 180;
568
- const dx = Math.cos(rad);
569
- const dy = -Math.sin(rad);
570
- const x1 = formatPercent(0.5 - dx * 0.5);
571
- const y1 = formatPercent(0.5 - dy * 0.5);
572
- const x2 = formatPercent(0.5 + dx * 0.5);
573
- const y2 = formatPercent(0.5 + dy * 0.5);
574
- return `<linearGradient id="${id}" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
575
- ${stopsXml.join("\n")}
576
- </linearGradient>`;
577
- }
578
- function checkGradient(g, options = {}) {
579
- const dual = isDualGradient(g) || isDualColor(options.surface) || isDualColor(options.text);
580
- if (dual) {
581
- const lightG = resolveGradient(g, "light");
582
- const darkG = resolveGradient(g, "dark");
583
- const lightText = options.text ? resolveColor(options.text, "light") ?? void 0 : void 0;
584
- const darkText = options.text ? resolveColor(options.text, "dark") ?? void 0 : void 0;
585
- const checkScheme = (grad, txt, schemeName) => {
586
- const issues2 = [];
587
- for (let i = 0; i < grad.stops.length - 1; i++) {
588
- const gap = grad.stops[i + 1].offset - grad.stops[i].offset;
589
- if (gap < 5e-3) {
590
- issues2.push(`[${schemeName}] Stops "${grad.stops[i].id}" and "${grad.stops[i + 1].id}" are too close (gap < 0.005).`);
591
- }
592
- const c1 = parseColor(grad.stops[i].color);
593
- const c2 = parseColor(grad.stops[i + 1].color);
594
- if (c1 && c2 && gap < 0.05) {
595
- const dE = deltaEOK(c1, c2);
596
- if (dE > 0.5) {
597
- issues2.push(
598
- `[${schemeName}] Banding risk between stops "${grad.stops[i].id}" and "${grad.stops[i + 1].id}" (\u0394EOK = ${dE.toFixed(2)} across gap ${gap.toFixed(3)}).`
599
- );
600
- }
601
- }
602
- }
603
- for (const s of grad.stops) {
604
- const c = parseColor(s.color);
605
- if (c && !inSrgbGamut(c)) {
606
- issues2.push(`[${schemeName}] Stop "${s.id}" colour ${s.color} is outside standard sRGB gamut.`);
607
- }
608
- }
609
- let lowest;
610
- if (txt) {
611
- const parsedTxt = parseColor(txt);
612
- if (parsedTxt) {
613
- let minRatio = Infinity;
614
- for (let step = 0; step <= 32; step++) {
615
- const sample = parseColor(sampleGradient(grad, step / 32, schemeName));
616
- if (sample) {
617
- const r = contrastRatio(parsedTxt, sample);
618
- if (r < minRatio) minRatio = r;
619
- }
620
- }
621
- lowest = Math.round(minRatio * 100) / 100;
622
- }
623
- }
624
- return { issues: issues2, lowest };
625
- };
626
- const lRes = checkScheme(lightG, lightText, "light");
627
- const dRes = checkScheme(darkG, darkText, "dark");
628
- return {
629
- issues: [...lRes.issues, ...dRes.issues],
630
- lowestTextContrast: lRes.lowest !== void 0 && dRes.lowest !== void 0 ? { light: lRes.lowest, dark: dRes.lowest } : void 0
631
- };
632
- }
633
- const issues = [];
634
- for (let i = 0; i < g.stops.length - 1; i++) {
635
- const gap = g.stops[i + 1].offset - g.stops[i].offset;
636
- if (gap < 5e-3) {
637
- issues.push(`Stops "${g.stops[i].id}" and "${g.stops[i + 1].id}" are too close (gap < 0.005).`);
638
- }
639
- const c1 = parseColor(g.stops[i].color);
640
- const c2 = parseColor(g.stops[i + 1].color);
641
- if (c1 && c2 && gap < 0.05) {
642
- const dE = deltaEOK(c1, c2);
643
- if (dE > 0.5) {
644
- issues.push(
645
- `Banding risk between stops "${g.stops[i].id}" and "${g.stops[i + 1].id}" (\u0394EOK = ${dE.toFixed(2)} across gap ${gap.toFixed(3)}).`
646
- );
647
- }
648
- }
649
- }
650
- for (const s of g.stops) {
651
- const c = parseColor(s.color);
652
- if (c && !inSrgbGamut(c)) {
653
- issues.push(`Stop "${s.id}" colour ${s.color} is outside standard sRGB gamut.`);
654
- }
655
- }
656
- let lowestTextContrast;
657
- if (options.text) {
658
- const parsedTxt = parseColor(options.text);
659
- if (parsedTxt) {
660
- let minRatio = Infinity;
661
- for (let step = 0; step <= 32; step++) {
662
- const sample = parseColor(sampleGradient(g, step / 32, "light"));
663
- if (sample) {
664
- const r = contrastRatio(parsedTxt, sample);
665
- if (r < minRatio) minRatio = r;
666
- }
667
- }
668
- lowestTextContrast = Math.round(minRatio * 100) / 100;
669
- }
670
- }
671
- return { issues, lowestTextContrast };
672
- }
673
- var GRADIENT_PRESETS = Object.freeze({
674
- sunset: {
675
- type: "linear",
676
- angle: 90,
677
- interpolation: "oklab",
678
- stops: [
679
- { id: "s1", offset: 0, color: "#f97316" },
680
- { id: "s2", offset: 1, color: "#ec4899" }
681
- ]
682
- },
683
- ocean: {
684
- type: "linear",
685
- angle: 180,
686
- interpolation: "oklab",
687
- stops: [
688
- { id: "s1", offset: 0, color: "#06b6d4" },
689
- { id: "s2", offset: 1, color: "#3b82f6" }
690
- ]
691
- },
692
- "mono-fade": {
693
- type: "linear",
694
- angle: 180,
695
- interpolation: "oklab",
696
- stops: [
697
- { id: "s1", offset: 0, color: "#ffffff" },
698
- { id: "s2", offset: 1, color: "#000000" }
699
- ]
700
- },
701
- "transparent-fade": {
702
- type: "linear",
703
- angle: 180,
704
- interpolation: "oklab",
705
- stops: [
706
- { id: "s1", offset: 0, color: "#000000ff" },
707
- { id: "s2", offset: 1, color: "#00000000" }
708
- ]
709
- },
710
- aurora: {
711
- type: "linear",
712
- angle: 135,
713
- interpolation: "oklab",
714
- stops: [
715
- { id: "s1", offset: 0, color: "#10b981" },
716
- { id: "s2", offset: 0.5, color: "#06b6d4" },
717
- { id: "s3", offset: 1, color: "#8b5cf6" }
718
- ]
719
- },
720
- neon: {
721
- type: "linear",
722
- angle: 90,
723
- interpolation: "oklab",
724
- stops: [
725
- { id: "s1", offset: 0, color: "#f43f5e" },
726
- { id: "s2", offset: 1, color: "#8b5cf6" }
727
- ]
728
- },
729
- radial: {
730
- type: "radial",
731
- shape: "ellipse",
732
- center: { x: 0.5, y: 0.5 },
733
- interpolation: "oklab",
734
- stops: [
735
- { id: "s1", offset: 0, color: "#ffffff" },
736
- { id: "s2", offset: 1, color: "#000000" }
737
- ]
738
- },
739
- conic: {
740
- type: "conic",
741
- angle: 0,
742
- center: { x: 0.5, y: 0.5 },
743
- interpolation: "oklab",
744
- stops: [
745
- { id: "s1", offset: 0, color: "#f43f5e" },
746
- { id: "s2", offset: 0.33, color: "#eab308" },
747
- { id: "s3", offset: 0.66, color: "#06b6d4" },
748
- { id: "s4", offset: 1, color: "#f43f5e" }
749
- ]
750
- }
751
- });
752
-
753
40
  // src/gradient/controller.ts
754
41
  var GRADIENT_TYPES = ["linear", "radial", "conic"];
755
42
  var GRADIENT_INTERPOLATIONS = ["oklab", "oklch", "srgb", "srgb-linear"];
@@ -1431,6 +718,57 @@ function createGradientToolProvider(controller, options = {}) {
1431
718
  };
1432
719
  }
1433
720
  },
721
+ {
722
+ name: "set_gradient_from_samples",
723
+ title: "Set Gradient From Sampled Colours",
724
+ description: "Fits the fewest stops that reproduce colours sampled along a line (from a photo, painting or shape, e.g. image sample_gradient or your own picks) and makes them the gradient's stops, keeping its type, angle and centre and switching interpolation to oklab. Noise is smoothed first; stops are added where the colour turns most until every sample is within tolerance. One undo step. Returns the gradient, its CSS, the stop count and the largest error left (OKLab \u0394E).",
725
+ inputSchema: {
726
+ type: "object",
727
+ properties: {
728
+ samples: {
729
+ type: "array",
730
+ minItems: 2,
731
+ maxItems: 1024,
732
+ description: "Colours in order along the line.",
733
+ items: {
734
+ type: "object",
735
+ properties: {
736
+ offset: { type: "number", minimum: 0, maximum: 1, description: "Position along the line, 0..1." },
737
+ color: { type: "string", description: "Any CSS colour (hex, rgb(), oklch(), named\u2026)." }
738
+ },
739
+ required: ["offset", "color"],
740
+ additionalProperties: false
741
+ }
742
+ },
743
+ tolerance: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "Largest error left, OKLab \u0394E (default 0.06; 0.02 is a just-noticeable step, lower keeps more stops)." },
744
+ minStops: { type: "integer", minimum: 2, maximum: 64, description: "Fewest stops (default 2)." },
745
+ maxStops: { type: "integer", minimum: 2, maximum: 64, description: "Most stops (default 5)." },
746
+ smoothing: { type: "integer", minimum: 0, maximum: 16, description: "Samples on each side averaged into each one to ignore grain and texture (default 2; 0 fits the raw colours)." }
747
+ },
748
+ required: ["samples"],
749
+ additionalProperties: false
750
+ },
751
+ annotations: writeIdempotentTool("Set Gradient From Sampled Colours"),
752
+ handler: async (args) => {
753
+ const unreadable = args.samples.findIndex((x) => !isColor(x.color));
754
+ if (unreadable >= 0) fail(`samples[${unreadable}].color ${JSON.stringify(args.samples[unreadable].color)} is not a CSS colour.`);
755
+ let fit;
756
+ try {
757
+ fit = fitGradientStops(args.samples, {
758
+ tolerance: args.tolerance,
759
+ minStops: args.minStops,
760
+ maxStops: args.maxStops,
761
+ smoothing: args.smoothing
762
+ });
763
+ } catch (e) {
764
+ return fail(e.message);
765
+ }
766
+ const res = controller.setGradient({ ...controller.getGradient(), stops: fit.stops, interpolation: "oklab" }, "Gradient From Samples");
767
+ if (!res.ok) fail(res.message);
768
+ const gradient = controller.getGradient();
769
+ return { gradient, css: gradientToCss(gradient), stopCount: gradient.stops.length, maxError: fit.maxError };
770
+ }
771
+ },
1434
772
  {
1435
773
  name: "apply_gradient_preset",
1436
774
  title: "Apply Gradient Preset",
@@ -1472,56 +810,6 @@ function createGradientToolProvider(controller, options = {}) {
1472
810
  tools: () => tools
1473
811
  };
1474
812
  }
1475
-
1476
- // src/gradient/editor.ts
1477
- function stopFromTrackPosition(g, x) {
1478
- return Math.max(0, Math.min(1, Math.round(x * 1e3) / 1e3));
1479
- }
1480
- function trackPositionFromStop(stop) {
1481
- return Math.max(0, Math.min(1, stop.offset));
1482
- }
1483
- function gradientKeyStep(g, id, key, shift = false) {
1484
- const stop = g.stops.find((s) => s.id === id);
1485
- if (!stop) return null;
1486
- const step = shift ? 0.1 : 0.01;
1487
- switch (key) {
1488
- case "ArrowLeft":
1489
- case "ArrowDown": {
1490
- const offset = Math.max(0, Math.round((stop.offset - step) * 1e3) / 1e3);
1491
- return { stopId: id, offset };
1492
- }
1493
- case "ArrowRight":
1494
- case "ArrowUp": {
1495
- const offset = Math.min(1, Math.round((stop.offset + step) * 1e3) / 1e3);
1496
- return { stopId: id, offset };
1497
- }
1498
- case "Delete":
1499
- case "Backspace": {
1500
- return { stopId: id, remove: true };
1501
- }
1502
- default:
1503
- return null;
1504
- }
1505
- }
1506
- function angleFromPointer(center, point, snapStep) {
1507
- const dx = point.x - center.x;
1508
- const dy = point.y - center.y;
1509
- let deg = Math.atan2(dx, -dy) * 180 / Math.PI;
1510
- if (deg < 0) deg += 360;
1511
- if (snapStep && snapStep > 0) {
1512
- deg = Math.round(deg / snapStep) * snapStep;
1513
- if (deg >= 360) deg -= 360;
1514
- }
1515
- return Math.round(deg);
1516
- }
1517
- function gradientTrackBackground(g) {
1518
- const linearCopy = {
1519
- ...g,
1520
- type: "linear",
1521
- angle: 90
1522
- };
1523
- return gradientToCss(linearCopy);
1524
- }
1525
813
  export {
1526
814
  GRADIENT_INTERPOLATIONS,
1527
815
  GRADIENT_PRESETS,
@@ -1532,6 +820,7 @@ export {
1532
820
  createGradient,
1533
821
  createGradientToolProvider,
1534
822
  distributeGradientStops,
823
+ fitGradientStops,
1535
824
  gradientKeyStep,
1536
825
  gradientMidpointRemap,
1537
826
  gradientToCss,
@@ -1547,6 +836,8 @@ export {
1547
836
  resolveGradient,
1548
837
  reverseGradient,
1549
838
  sampleGradient,
839
+ sampleGradientFromLine,
840
+ simplifyGradientStops,
1550
841
  stopFromTrackPosition,
1551
842
  trackPositionFromStop,
1552
843
  updateGradientStop