@pixerate/editor 0.280.0 → 0.281.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.
@@ -2058,8 +2058,8 @@ function snapResize(rect2, handle, targets, options = {}) {
2058
2058
  const ratio = rect2.height > 0 ? rect2.width / rect2.height : 0;
2059
2059
  if (options.keepAspectRatio && ratio > 0 && (hitX || hitY)) {
2060
2060
  if (axes.x !== 0 && axes.y !== 0) {
2061
- const dist = (h, horizontal) => h ? Math.abs(h.value - (horizontal ? axes.x < 0 ? x0 : x1 : axes.y < 0 ? y0 : y1)) : Infinity;
2062
- const useX = dist(hitX, true) <= dist(hitY, false);
2061
+ const dist2 = (h, horizontal) => h ? Math.abs(h.value - (horizontal ? axes.x < 0 ? x0 : x1 : axes.y < 0 ? y0 : y1)) : Infinity;
2062
+ const useX = dist2(hitX, true) <= dist2(hitY, false);
2063
2063
  if (useX && hitX) {
2064
2064
  setEdge(true, hitX.value);
2065
2065
  const height = (nx1 - nx0) / ratio;
@@ -5219,7 +5219,7 @@ var defaultLabel = (node) => typeof node.data?.label === "string" && node.data.l
5219
5219
  function getTimelineRows(nodes, ids, options = {}) {
5220
5220
  const byId = new Map(nodes.map((n3) => [n3.id, n3]));
5221
5221
  const selection = options.selection ?? [];
5222
- const labelOf3 = options.labelOf ?? defaultLabel;
5222
+ const labelOf4 = options.labelOf ?? defaultLabel;
5223
5223
  const rows = [];
5224
5224
  for (const id of new Set(ids)) {
5225
5225
  const node = byId.get(id);
@@ -5230,7 +5230,7 @@ function getTimelineRows(nodes, ids, options = {}) {
5230
5230
  rows.push({
5231
5231
  id: `${id}:${property}`,
5232
5232
  nodeId: id,
5233
- nodeLabel: labelOf3(node),
5233
+ nodeLabel: labelOf4(node),
5234
5234
  property,
5235
5235
  label,
5236
5236
  enabled: track.enabled !== false,
@@ -8058,8 +8058,8 @@ function findStrokesAtPoint(point, nodes, radius = 12) {
8058
8058
  }
8059
8059
  const localPoint = [point[0] - rect2.x, point[1] - rect2.y];
8060
8060
  try {
8061
- const dist = distanceToBezierPath(shape.d, localPoint);
8062
- if (dist <= effectiveRadius) {
8061
+ const dist2 = distanceToBezierPath(shape.d, localPoint);
8062
+ if (dist2 <= effectiveRadius) {
8063
8063
  matches2.push(node);
8064
8064
  }
8065
8065
  } catch {
@@ -11314,8 +11314,8 @@ function calculateCursorArrowRotation(cursor, targetCenter, isAgentOrServiceAcco
11314
11314
  }
11315
11315
  const dx = targetCenter.x - cursor.x;
11316
11316
  const dy = targetCenter.y - cursor.y;
11317
- const dist = Math.hypot(dx, dy);
11318
- if (dist < 2) {
11317
+ const dist2 = Math.hypot(dx, dy);
11318
+ if (dist2 < 2) {
11319
11319
  return { shouldPoint: true, rotationDeg: currentRotation };
11320
11320
  }
11321
11321
  const angleDeg = Math.atan2(dy, dx) * (180 / Math.PI);
@@ -12966,6 +12966,665 @@ function createCanvasStickerHost(options) {
12966
12966
  }
12967
12967
  };
12968
12968
  }
12969
+
12970
+ // src/canvas/edgePaths.ts
12971
+ var DIRECTIONS = {
12972
+ left: { x: -1, y: 0 },
12973
+ right: { x: 1, y: 0 },
12974
+ top: { x: 0, y: -1 },
12975
+ bottom: { x: 0, y: 1 }
12976
+ };
12977
+ function controlOffset(distance, curvature) {
12978
+ return distance >= 0 ? 0.5 * distance : curvature * 25 * Math.sqrt(-distance);
12979
+ }
12980
+ function curvatureControl(pos, x1, y1, x2, y2, c) {
12981
+ switch (pos) {
12982
+ case "left":
12983
+ return [x1 - controlOffset(x1 - x2, c), y1];
12984
+ case "right":
12985
+ return [x1 + controlOffset(x2 - x1, c), y1];
12986
+ case "top":
12987
+ return [x1, y1 - controlOffset(y1 - y2, c)];
12988
+ default:
12989
+ return [x1, y1 + controlOffset(y2 - y1, c)];
12990
+ }
12991
+ }
12992
+ function simpleControl(pos, x1, y1, x2, y2) {
12993
+ return pos === "left" || pos === "right" ? [0.5 * (x1 + x2), y1] : [x1, 0.5 * (y1 + y2)];
12994
+ }
12995
+ function cubicPath(p, s, t) {
12996
+ const { sourceX: sx, sourceY: sy, targetX: tx, targetY: ty } = p;
12997
+ return {
12998
+ path: `M${sx},${sy} C${s[0]},${s[1]} ${t[0]},${t[1]} ${tx},${ty}`,
12999
+ labelX: sx * 0.125 + s[0] * 0.375 + t[0] * 0.375 + tx * 0.125,
13000
+ labelY: sy * 0.125 + s[1] * 0.375 + t[1] * 0.375 + ty * 0.125
13001
+ };
13002
+ }
13003
+ function edgeCenter(sx, sy, tx, ty) {
13004
+ const xOffset = Math.abs(tx - sx) / 2;
13005
+ const yOffset = Math.abs(ty - sy) / 2;
13006
+ return [tx < sx ? tx + xOffset : tx - xOffset, ty < sy ? ty + yOffset : ty - yOffset];
13007
+ }
13008
+ var dist = (a, b) => Math.hypot(b.x - a.x, b.y - a.y);
13009
+ function stepPoints(source, sourcePosition, target, targetPosition, offset) {
13010
+ const sourceDir = DIRECTIONS[sourcePosition];
13011
+ const targetDir = DIRECTIONS[targetPosition];
13012
+ const sourceGapped = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset };
13013
+ const targetGapped = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset };
13014
+ const dir = sourcePosition === "left" || sourcePosition === "right" ? { x: sourceGapped.x < targetGapped.x ? 1 : -1, y: 0 } : { x: 0, y: sourceGapped.y < targetGapped.y ? 1 : -1 };
13015
+ const axis = dir.x !== 0 ? "x" : "y";
13016
+ const currDir = dir[axis];
13017
+ let points;
13018
+ let centerX;
13019
+ let centerY;
13020
+ const sourceGapOffset = { x: 0, y: 0 };
13021
+ const targetGapOffset = { x: 0, y: 0 };
13022
+ if (sourceDir[axis] * targetDir[axis] === -1) {
13023
+ centerX = (sourceGapped.x + targetGapped.x) / 2;
13024
+ centerY = (sourceGapped.y + targetGapped.y) / 2;
13025
+ const verticalSplit = [{ x: centerX, y: sourceGapped.y }, { x: centerX, y: targetGapped.y }];
13026
+ const horizontalSplit = [{ x: sourceGapped.x, y: centerY }, { x: targetGapped.x, y: centerY }];
13027
+ points = sourceDir[axis] === currDir ? axis === "x" ? verticalSplit : horizontalSplit : axis === "x" ? horizontalSplit : verticalSplit;
13028
+ } else {
13029
+ const sourceTarget = [{ x: sourceGapped.x, y: targetGapped.y }];
13030
+ const targetSource = [{ x: targetGapped.x, y: sourceGapped.y }];
13031
+ points = axis === "x" ? sourceDir.x === currDir ? targetSource : sourceTarget : sourceDir.y === currDir ? sourceTarget : targetSource;
13032
+ if (sourcePosition === targetPosition) {
13033
+ const diff = Math.abs(source[axis] - target[axis]);
13034
+ if (diff <= offset) {
13035
+ const gapOffset = Math.min(offset - 1, offset - diff);
13036
+ if (sourceDir[axis] === currDir) sourceGapOffset[axis] = (sourceGapped[axis] > source[axis] ? -1 : 1) * gapOffset;
13037
+ else targetGapOffset[axis] = (targetGapped[axis] > target[axis] ? -1 : 1) * gapOffset;
13038
+ }
13039
+ } else {
13040
+ const opposite = axis === "x" ? "y" : "x";
13041
+ const isSameDir = sourceDir[axis] === targetDir[opposite];
13042
+ const sourceGt = sourceGapped[opposite] > targetGapped[opposite];
13043
+ const sourceLt = sourceGapped[opposite] < targetGapped[opposite];
13044
+ const flip = sourceDir[axis] === 1 && (!isSameDir && sourceGt || isSameDir && sourceLt) || sourceDir[axis] !== 1 && (!isSameDir && sourceLt || isSameDir && sourceGt);
13045
+ if (flip) points = axis === "x" ? sourceTarget : targetSource;
13046
+ }
13047
+ const sourceGapPoint = { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y };
13048
+ const targetGapPoint = { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y };
13049
+ const maxX = Math.max(Math.abs(sourceGapPoint.x - points[0].x), Math.abs(targetGapPoint.x - points[0].x));
13050
+ const maxY = Math.max(Math.abs(sourceGapPoint.y - points[0].y), Math.abs(targetGapPoint.y - points[0].y));
13051
+ if (maxX >= maxY) {
13052
+ centerX = (sourceGapPoint.x + targetGapPoint.x) / 2;
13053
+ centerY = points[0].y;
13054
+ } else {
13055
+ centerX = points[0].x;
13056
+ centerY = (sourceGapPoint.y + targetGapPoint.y) / 2;
13057
+ }
13058
+ }
13059
+ const gappedSource = { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y };
13060
+ const gappedTarget = { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y };
13061
+ const first = points[0];
13062
+ const last = points[points.length - 1];
13063
+ const path = [
13064
+ source,
13065
+ ...gappedSource.x !== first.x || gappedSource.y !== first.y ? [gappedSource] : [],
13066
+ ...points,
13067
+ ...gappedTarget.x !== last.x || gappedTarget.y !== last.y ? [gappedTarget] : [],
13068
+ target
13069
+ ];
13070
+ return [path, centerX, centerY];
13071
+ }
13072
+ function bend(a, b, c, size2) {
13073
+ const bendSize = Math.min(dist(a, b) / 2, dist(b, c) / 2, size2);
13074
+ const { x, y } = b;
13075
+ if (a.x === x && x === c.x || a.y === y && y === c.y) return `L${x} ${y}`;
13076
+ if (a.y === y) {
13077
+ const xDir2 = a.x < c.x ? -1 : 1;
13078
+ const yDir2 = a.y < c.y ? 1 : -1;
13079
+ return `L ${x + bendSize * xDir2},${y}Q ${x},${y} ${x},${y + bendSize * yDir2}`;
13080
+ }
13081
+ const xDir = a.x < c.x ? 1 : -1;
13082
+ const yDir = a.y < c.y ? -1 : 1;
13083
+ return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
13084
+ }
13085
+ function getCanvasEdgePath(params) {
13086
+ const { sourceX: sx, sourceY: sy, targetX: tx, targetY: ty } = params;
13087
+ const sourcePosition = params.sourcePosition ?? "bottom";
13088
+ const targetPosition = params.targetPosition ?? "top";
13089
+ switch (params.type ?? "default") {
13090
+ case "straight": {
13091
+ const [labelX, labelY] = edgeCenter(sx, sy, tx, ty);
13092
+ return { path: `M ${sx},${sy}L ${tx},${ty}`, labelX, labelY };
13093
+ }
13094
+ case "simplebezier":
13095
+ return cubicPath(params, simpleControl(sourcePosition, sx, sy, tx, ty), simpleControl(targetPosition, tx, ty, sx, sy));
13096
+ case "step":
13097
+ case "smoothstep": {
13098
+ const radius = params.type === "step" ? 0 : params.borderRadius ?? 5;
13099
+ const [points, labelX, labelY] = stepPoints({ x: sx, y: sy }, sourcePosition, { x: tx, y: ty }, targetPosition, params.offset ?? 20);
13100
+ let path = "";
13101
+ points.forEach((p, i) => {
13102
+ path += i > 0 && i < points.length - 1 ? bend(points[i - 1], p, points[i + 1], radius) : `${i === 0 ? "M" : "L"}${p.x} ${p.y}`;
13103
+ });
13104
+ return { path, labelX, labelY };
13105
+ }
13106
+ default: {
13107
+ const c = params.curvature ?? 0.25;
13108
+ return cubicPath(params, curvatureControl(sourcePosition, sx, sy, tx, ty, c), curvatureControl(targetPosition, tx, ty, sx, sy, c));
13109
+ }
13110
+ }
13111
+ }
13112
+ function measureSvgPath(d, segments = 24) {
13113
+ const pts = [];
13114
+ const tokens = d.match(/[a-zA-Z]|-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi) ?? [];
13115
+ let i = 0;
13116
+ let cmd = "";
13117
+ let cur = { x: 0, y: 0 };
13118
+ let start = { x: 0, y: 0 };
13119
+ let lastCtrl = null;
13120
+ let lastCmd = "";
13121
+ const num4 = () => Number(tokens[i++]);
13122
+ const push2 = (p) => {
13123
+ pts.push(p);
13124
+ cur = p;
13125
+ };
13126
+ const cubic = (c1, c2, end) => {
13127
+ const p0 = cur;
13128
+ for (let k = 1; k <= segments; k++) {
13129
+ const t = k / segments;
13130
+ const u = 1 - t;
13131
+ push2({
13132
+ x: u * u * u * p0.x + 3 * u * u * t * c1.x + 3 * u * t * t * c2.x + t * t * t * end.x,
13133
+ y: u * u * u * p0.y + 3 * u * u * t * c1.y + 3 * u * t * t * c2.y + t * t * t * end.y
13134
+ });
13135
+ }
13136
+ };
13137
+ const quad = (c, end) => {
13138
+ const p0 = cur;
13139
+ for (let k = 1; k <= segments; k++) {
13140
+ const t = k / segments;
13141
+ const u = 1 - t;
13142
+ push2({ x: u * u * p0.x + 2 * u * t * c.x + t * t * end.x, y: u * u * p0.y + 2 * u * t * c.y + t * t * end.y });
13143
+ }
13144
+ };
13145
+ while (i < tokens.length) {
13146
+ if (/^[a-zA-Z]$/.test(tokens[i])) cmd = tokens[i++];
13147
+ else if (!cmd) break;
13148
+ const rel = cmd === cmd.toLowerCase();
13149
+ const at = (x, y) => rel ? { x: cur.x + x, y: cur.y + y } : { x, y };
13150
+ switch (cmd.toUpperCase()) {
13151
+ case "M": {
13152
+ const p = at(num4(), num4());
13153
+ push2(p);
13154
+ start = p;
13155
+ cmd = rel ? "l" : "L";
13156
+ lastCtrl = null;
13157
+ lastCmd = "M";
13158
+ continue;
13159
+ }
13160
+ case "L":
13161
+ push2(at(num4(), num4()));
13162
+ lastCtrl = null;
13163
+ break;
13164
+ case "H": {
13165
+ const x = num4();
13166
+ push2({ x: rel ? cur.x + x : x, y: cur.y });
13167
+ lastCtrl = null;
13168
+ break;
13169
+ }
13170
+ case "V": {
13171
+ const y = num4();
13172
+ push2({ x: cur.x, y: rel ? cur.y + y : y });
13173
+ lastCtrl = null;
13174
+ break;
13175
+ }
13176
+ case "C": {
13177
+ const c1 = at(num4(), num4());
13178
+ const c2 = at(num4(), num4());
13179
+ const end = at(num4(), num4());
13180
+ cubic(c1, c2, end);
13181
+ lastCtrl = c2;
13182
+ break;
13183
+ }
13184
+ case "S": {
13185
+ const c1 = lastCtrl && /[CS]/i.test(lastCmd) ? { x: 2 * cur.x - lastCtrl.x, y: 2 * cur.y - lastCtrl.y } : cur;
13186
+ const c2 = at(num4(), num4());
13187
+ const end = at(num4(), num4());
13188
+ cubic(c1, c2, end);
13189
+ lastCtrl = c2;
13190
+ break;
13191
+ }
13192
+ case "Q": {
13193
+ const c = at(num4(), num4());
13194
+ const end = at(num4(), num4());
13195
+ quad(c, end);
13196
+ lastCtrl = c;
13197
+ break;
13198
+ }
13199
+ case "T": {
13200
+ const c = lastCtrl && /[QT]/i.test(lastCmd) ? { x: 2 * cur.x - lastCtrl.x, y: 2 * cur.y - lastCtrl.y } : cur;
13201
+ const end = at(num4(), num4());
13202
+ quad(c, end);
13203
+ lastCtrl = c;
13204
+ break;
13205
+ }
13206
+ case "A": {
13207
+ for (let skip = 0; skip < 5; skip++) num4();
13208
+ push2(at(num4(), num4()));
13209
+ lastCtrl = null;
13210
+ break;
13211
+ }
13212
+ case "Z":
13213
+ push2(start);
13214
+ lastCtrl = null;
13215
+ break;
13216
+ default:
13217
+ i = tokens.length;
13218
+ }
13219
+ lastCmd = cmd;
13220
+ if (cmd.toUpperCase() === "Z") cmd = "";
13221
+ }
13222
+ const cumulative = [0];
13223
+ for (let k = 1; k < pts.length; k++) cumulative.push(cumulative[k - 1] + dist(pts[k - 1], pts[k]));
13224
+ const length = cumulative[cumulative.length - 1] ?? 0;
13225
+ const angleAt = (k) => {
13226
+ for (let j = Math.max(1, k); j < pts.length; j++) {
13227
+ if (cumulative[j] > cumulative[j - 1]) return Math.atan2(pts[j].y - pts[j - 1].y, pts[j].x - pts[j - 1].x);
13228
+ }
13229
+ for (let j = Math.min(k, pts.length - 1); j >= 1; j--) {
13230
+ if (cumulative[j] > cumulative[j - 1]) return Math.atan2(pts[j].y - pts[j - 1].y, pts[j].x - pts[j - 1].x);
13231
+ }
13232
+ return 0;
13233
+ };
13234
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
13235
+ for (const p of pts) {
13236
+ if (p.x < minX) minX = p.x;
13237
+ if (p.y < minY) minY = p.y;
13238
+ if (p.x > maxX) maxX = p.x;
13239
+ if (p.y > maxY) maxY = p.y;
13240
+ }
13241
+ const bounds = pts.length ? { x: minX, y: minY, width: maxX - minX, height: maxY - minY } : { x: 0, y: 0, width: 0, height: 0 };
13242
+ return {
13243
+ length,
13244
+ bounds,
13245
+ pointAt(distance) {
13246
+ if (pts.length === 0) return { x: 0, y: 0, angle: 0 };
13247
+ if (pts.length === 1 || length === 0) return { ...pts[0], angle: 0 };
13248
+ const s = Math.min(Math.max(distance, 0), length);
13249
+ let lo = 1;
13250
+ let hi = pts.length - 1;
13251
+ while (lo < hi) {
13252
+ const mid = lo + hi >> 1;
13253
+ if (cumulative[mid] < s) lo = mid + 1;
13254
+ else hi = mid;
13255
+ }
13256
+ const piece = cumulative[lo] - cumulative[lo - 1];
13257
+ const t = piece > 0 ? (s - cumulative[lo - 1]) / piece : 0;
13258
+ const a = pts[lo - 1];
13259
+ const b = pts[lo];
13260
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, angle: angleAt(lo) };
13261
+ }
13262
+ };
13263
+ }
13264
+
13265
+ // src/canvas/edgeAnimation.ts
13266
+ var CANVAS_EDGE_ANIMATION_KINDS = [
13267
+ {
13268
+ kind: "dots",
13269
+ title: "Travelling dots",
13270
+ description: "Dots that travel along the edge, evenly spaced. Good for data or messages flowing between nodes.",
13271
+ defaults: { speed: 80, direction: "forward", size: 3, count: 3, length: 0, gap: 0, glow: 0, trackOpacity: 1 }
13272
+ },
13273
+ {
13274
+ kind: "dash",
13275
+ title: "Marching dashes",
13276
+ description: 'The edge drawn as dashes that march along it. A quiet "this is live" signal.',
13277
+ defaults: { speed: 40, direction: "forward", size: 2, count: 0, length: 8, gap: 6, glow: 0, trackOpacity: 0 }
13278
+ },
13279
+ {
13280
+ kind: "beam",
13281
+ title: "Beam",
13282
+ description: "A short glowing streak that shoots along the edge and leaves it before the next one starts. Good for a request or a trigger.",
13283
+ defaults: { speed: 200, direction: "forward", size: 3, count: 1, length: 40, gap: 0, glow: 4, trackOpacity: 0.35 }
13284
+ },
13285
+ {
13286
+ kind: "pulse",
13287
+ title: "Pulse",
13288
+ description: "The whole edge glows brighter and fades again. Good for an active or highlighted connection.",
13289
+ defaults: { speed: 0, direction: "forward", size: 4, count: 0, length: 0, gap: 0, glow: 3, trackOpacity: 1, duration: 1600 }
13290
+ }
13291
+ ];
13292
+ var KINDS = CANVAS_EDGE_ANIMATION_KINDS.map((k) => k.kind);
13293
+ var DIRECTIONS2 = ["forward", "reverse", "alternate"];
13294
+ var kindInfo = (kind) => CANVAS_EDGE_ANIMATION_KINDS.find((k) => k.kind === kind);
13295
+ var FIELDS = {
13296
+ speed: { min: 1, max: 1e4, description: "canvas units per second" },
13297
+ duration: { min: 100, max: 6e5, description: "ms" },
13298
+ size: { min: 0.5, max: 100, description: "canvas units" },
13299
+ count: { min: 1, max: 50, int: true, description: "dots or beams" },
13300
+ length: { min: 1, max: 1e4, description: "canvas units" },
13301
+ gap: { min: 0, max: 1e4, description: "canvas units" },
13302
+ glow: { min: 0, max: 50, description: "canvas units" },
13303
+ trackOpacity: { min: 0, max: 1, description: "0\u20131" }
13304
+ };
13305
+ function normalizeCanvasEdgeAnimation(input) {
13306
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error('An edge animation is an object with a "kind" (dots, dash, beam or pulse).');
13307
+ const raw = input;
13308
+ if (!KINDS.includes(raw.kind)) {
13309
+ throw new Error(`Unknown edge animation kind ${JSON.stringify(raw.kind)}. Use one of: ${KINDS.join(", ")}.`);
13310
+ }
13311
+ const out = { kind: raw.kind };
13312
+ for (const [key, spec] of Object.entries(FIELDS)) {
13313
+ const value = raw[key];
13314
+ if (value === void 0 || value === null) continue;
13315
+ if (typeof value !== "number" || !Number.isFinite(value) || value < spec.min || value > spec.max || spec.int && !Number.isInteger(value)) {
13316
+ throw new Error(`Edge animation "${key}" must be ${spec.int ? "a whole number" : "a number"} from ${spec.min} to ${spec.max} (${spec.description}); got ${JSON.stringify(value)}.`);
13317
+ }
13318
+ out[key] = value;
13319
+ }
13320
+ if (raw.direction !== void 0 && raw.direction !== null) {
13321
+ if (!DIRECTIONS2.includes(raw.direction)) {
13322
+ throw new Error(`Edge animation "direction" must be one of: ${DIRECTIONS2.join(", ")}; got ${JSON.stringify(raw.direction)}.`);
13323
+ }
13324
+ out.direction = raw.direction;
13325
+ }
13326
+ if (raw.color !== void 0 && raw.color !== null) {
13327
+ if (typeof raw.color !== "string" || raw.color.trim() === "" || /[;{}<>]/.test(raw.color)) {
13328
+ throw new Error(`Edge animation "color" must be a CSS colour such as "#38bdf8" or "rgb(56 189 248)"; got ${JSON.stringify(raw.color)}.`);
13329
+ }
13330
+ out.color = raw.color.trim();
13331
+ }
13332
+ if (raw.paused !== void 0 && raw.paused !== null) {
13333
+ if (typeof raw.paused !== "boolean") throw new Error(`Edge animation "paused" must be true or false; got ${JSON.stringify(raw.paused)}.`);
13334
+ out.paused = raw.paused;
13335
+ }
13336
+ return out;
13337
+ }
13338
+ function getCanvasEdgeAnimation(edge) {
13339
+ const value = edge?.data?.animation;
13340
+ if (value === void 0 || value === null) return null;
13341
+ try {
13342
+ return normalizeCanvasEdgeAnimation(value);
13343
+ } catch {
13344
+ return null;
13345
+ }
13346
+ }
13347
+ function setCanvasEdgeAnimation(edge, animation) {
13348
+ const { animation: _old, ...rest } = edge.data ?? {};
13349
+ if (animation === null) return { ...edge, data: rest };
13350
+ return { ...edge, data: { ...rest, animation: normalizeCanvasEdgeAnimation(animation) } };
13351
+ }
13352
+ function resolveCanvasEdgeAnimation(animation) {
13353
+ const d = kindInfo(animation.kind).defaults;
13354
+ return {
13355
+ kind: animation.kind,
13356
+ speed: animation.speed ?? d.speed,
13357
+ direction: animation.direction ?? d.direction,
13358
+ size: animation.size ?? d.size,
13359
+ count: animation.count ?? d.count,
13360
+ length: animation.length ?? d.length,
13361
+ gap: animation.gap ?? d.gap,
13362
+ glow: animation.glow ?? d.glow,
13363
+ trackOpacity: animation.trackOpacity ?? d.trackOpacity,
13364
+ paused: animation.paused ?? false,
13365
+ ...animation.color !== void 0 ? { color: animation.color } : {},
13366
+ ...animation.duration !== void 0 ? { duration: animation.duration } : d.duration !== void 0 ? { duration: d.duration } : {}
13367
+ };
13368
+ }
13369
+ var round4 = (n3) => Math.round(n3 * 1e3) / 1e3;
13370
+ var idSafe = (id) => id.replace(/[^a-zA-Z0-9_-]/g, "_");
13371
+ function cycleSeconds(anim, distance) {
13372
+ if (anim.duration !== void 0) return anim.duration / 1e3;
13373
+ return Math.max(distance / anim.speed, 0.05);
13374
+ }
13375
+ function getCanvasEdgeAnimationRender(animation, options) {
13376
+ const anim = resolveCanvasEdgeAnimation(normalizeCanvasEdgeAnimation(animation));
13377
+ const measured2 = options.measured ?? measureSvgPath(options.path);
13378
+ const length = measured2.length;
13379
+ const color = anim.color ?? options.stroke ?? "currentColor";
13380
+ const moving = !anim.paused && !options.reducedMotion && length > 0;
13381
+ const margin = anim.size * 2 + anim.glow * 3 + 2;
13382
+ const b = measured2.bounds;
13383
+ const glowFilter = anim.glow > 0 ? {
13384
+ id: `pixerate-edge-glow-${idSafe(options.id)}`,
13385
+ stdDeviation: round4(anim.glow / 2),
13386
+ x: round4(b.x - margin),
13387
+ y: round4(b.y - margin),
13388
+ width: round4(b.width + margin * 2),
13389
+ height: round4(b.height + margin * 2)
13390
+ } : void 0;
13391
+ const filter = glowFilter ? `url(#${glowFilter.id})` : void 0;
13392
+ const alternate = anim.direction === "alternate";
13393
+ const reverse = anim.direction === "reverse";
13394
+ const elements = [];
13395
+ if (anim.kind === "dots") {
13396
+ const oneWay = cycleSeconds(anim, length);
13397
+ const dur = alternate ? oneWay * 2 : oneWay;
13398
+ const keyPoints = alternate ? "0;1;0" : reverse ? "1;0" : "0;1";
13399
+ const keyTimes = alternate ? "0;0.5;1" : "0;1";
13400
+ for (let i = 0; i < anim.count; i++) {
13401
+ const phase = i / anim.count;
13402
+ const s = alternate ? phase < 0.5 ? phase * 2 : 2 - phase * 2 : reverse ? 1 - phase : phase;
13403
+ elements.push({
13404
+ type: "dot",
13405
+ key: `dot-${i}`,
13406
+ r: anim.size,
13407
+ fill: color,
13408
+ filter,
13409
+ at: measured2.pointAt(s * length),
13410
+ motion: { dur: round4(dur), begin: round4(-phase * dur), keyPoints, keyTimes }
13411
+ });
13412
+ }
13413
+ } else if (anim.kind === "dash") {
13414
+ const period = anim.length + anim.gap;
13415
+ const dur = cycleSeconds(anim, period);
13416
+ const to = reverse ? period : -period;
13417
+ elements.push({
13418
+ type: "stroke",
13419
+ key: "dash",
13420
+ stroke: color,
13421
+ strokeWidth: anim.size,
13422
+ strokeLinecap: "butt",
13423
+ strokeDasharray: `${round4(anim.length)} ${round4(anim.gap)}`,
13424
+ strokeDashoffset: 0,
13425
+ filter,
13426
+ animate: [{ attributeName: "stroke-dashoffset", values: alternate ? `0;${round4(-period)};0` : `0;${round4(to)}`, dur: round4(alternate ? dur * 2 : dur), begin: 0 }]
13427
+ });
13428
+ } else if (anim.kind === "beam") {
13429
+ const period = (length + anim.length) / anim.count;
13430
+ const beam = Math.min(anim.length, period * 0.9);
13431
+ const oneWay = cycleSeconds(anim, period);
13432
+ const from = beam;
13433
+ const to = beam - period;
13434
+ const values = alternate ? `${round4(from)};${round4(to)};${round4(from)}` : reverse ? `${round4(to)};${round4(from)}` : `${round4(from)};${round4(to)}`;
13435
+ elements.push({
13436
+ type: "stroke",
13437
+ key: "beam",
13438
+ stroke: color,
13439
+ strokeWidth: anim.size,
13440
+ strokeLinecap: "round",
13441
+ strokeDasharray: `${round4(beam)} ${round4(Math.max(period - beam, 1e-3))}`,
13442
+ // Held still, the beam sits in the middle of the edge rather than off its start.
13443
+ strokeDashoffset: round4(moving ? reverse ? to : from : beam / 2 - length / 2),
13444
+ filter,
13445
+ animate: [{ attributeName: "stroke-dashoffset", values, dur: round4(alternate ? oneWay * 2 : oneWay), begin: 0 }]
13446
+ });
13447
+ } else {
13448
+ const dur = (anim.duration ?? 1600) / 1e3;
13449
+ elements.push({
13450
+ type: "stroke",
13451
+ key: "pulse",
13452
+ stroke: color,
13453
+ strokeWidth: anim.size,
13454
+ strokeLinecap: "round",
13455
+ opacity: 0.2,
13456
+ filter,
13457
+ animate: [
13458
+ { attributeName: "stroke-opacity", values: "0.2;1;0.2", dur: round4(dur), begin: 0 },
13459
+ { attributeName: "stroke-width", values: `${round4(anim.size)};${round4(anim.size * 1.75)};${round4(anim.size)}`, dur: round4(dur), begin: 0 }
13460
+ ]
13461
+ });
13462
+ }
13463
+ return { kind: anim.kind, length: round4(length), trackOpacity: anim.trackOpacity, moving, glowFilter, elements };
13464
+ }
13465
+ function valuesAt(values, t) {
13466
+ if (values.length === 1) return values[0];
13467
+ const f = Math.min(Math.max(t, 0), 1) * (values.length - 1);
13468
+ const i = Math.min(Math.floor(f), values.length - 2);
13469
+ return values[i] + (values[i + 1] - values[i]) * (f - i);
13470
+ }
13471
+ function sampleCanvasEdgeAnimation(animation, path, timeMs) {
13472
+ const measured2 = typeof path === "string" ? measureSvgPath(path) : path;
13473
+ const render = getCanvasEdgeAnimationRender(animation, { path: "", id: "sample", measured: measured2 });
13474
+ const anim = resolveCanvasEdgeAnimation(normalizeCanvasEdgeAnimation(animation));
13475
+ const seconds = anim.paused ? 0 : timeMs / 1e3;
13476
+ const progress = (t) => {
13477
+ const p = (seconds - t.begin) / t.dur;
13478
+ return p - Math.floor(p);
13479
+ };
13480
+ const frame = { kind: anim.kind, length: render.length, dots: [], segments: [], opacity: 1, strokeWidth: anim.size };
13481
+ for (const el of render.elements) {
13482
+ if (el.type === "dot") {
13483
+ const points = el.motion.keyPoints.split(";").map(Number);
13484
+ frame.dots.push(measured2.pointAt(valuesAt(points, progress(el.motion)) * measured2.length));
13485
+ continue;
13486
+ }
13487
+ for (const a of el.animate) {
13488
+ const value = anim.paused && a.attributeName === "stroke-dashoffset" ? el.strokeDashoffset ?? 0 : valuesAt(a.values.split(";").map(Number), progress(a));
13489
+ if (a.attributeName === "stroke-opacity") frame.opacity = value;
13490
+ else if (a.attributeName === "stroke-width") frame.strokeWidth = value;
13491
+ else if (el.strokeDasharray) {
13492
+ const [on, off] = el.strokeDasharray.split(" ").map(Number);
13493
+ const period = on + off;
13494
+ const start = Math.floor((0 + value) / period) - 1;
13495
+ for (let k = start; k * period - value < measured2.length; k++) {
13496
+ const from = k * period - value;
13497
+ const to = from + on;
13498
+ const a0 = Math.max(from, 0);
13499
+ const b0 = Math.min(to, measured2.length);
13500
+ if (b0 > a0) frame.segments.push([round4(a0), round4(b0)]);
13501
+ }
13502
+ }
13503
+ }
13504
+ if (anim.kind === "pulse" && frame.segments.length === 0) frame.segments.push([0, render.length]);
13505
+ }
13506
+ return frame;
13507
+ }
13508
+
13509
+ // src/canvas/edge-animation-tools.ts
13510
+ var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
13511
+ var WRITE_IDEMPOTENT2 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
13512
+ var EDGE_IDS = {
13513
+ type: "array",
13514
+ minItems: 1,
13515
+ uniqueItems: true,
13516
+ items: { type: "string", minLength: 1 },
13517
+ description: "Ids of the edges to change, from list_edge_animations. All of them change in one undo step."
13518
+ };
13519
+ var CANVAS_EDGE_ANIMATION_SCHEMA = {
13520
+ type: "object",
13521
+ properties: {
13522
+ kind: {
13523
+ type: "string",
13524
+ enum: CANVAS_EDGE_ANIMATION_KINDS.map((k) => k.kind),
13525
+ description: "What moves: dots (travelling dots), dash (marching dashes), beam (a glowing streak that shoots along), pulse (the edge glows and fades)."
13526
+ },
13527
+ speed: { type: "number", minimum: 1, maximum: 1e4, description: "Travel speed in canvas units per second (dots, dash, beam). Defaults: dots 80, dash 40, beam 200." },
13528
+ duration: { type: "number", minimum: 100, maximum: 6e5, description: "One cycle in ms; overrides speed. The pulse timing (default 1600)." },
13529
+ direction: { type: "string", enum: ["forward", "reverse", "alternate"], description: "forward = source to target (default), reverse = target to source, alternate = back and forth." },
13530
+ color: { type: "string", minLength: 1, description: 'CSS colour, e.g. "#38bdf8". Default: the edge stroke colour.' },
13531
+ size: { type: "number", minimum: 0.5, maximum: 100, description: "Dot radius, or stroke width of dashes, beams and the pulse, in canvas units. Defaults: dots 3, dash 2, beam 3, pulse 4." },
13532
+ count: { type: "integer", minimum: 1, maximum: 50, description: "How many dots or beams are on the edge at once. Defaults: dots 3, beam 1." },
13533
+ length: { type: "number", minimum: 1, maximum: 1e4, description: "Dash or beam length in canvas units. Defaults: dash 8, beam 40." },
13534
+ gap: { type: "number", minimum: 0, maximum: 1e4, description: "Gap between dashes in canvas units (dash). Default 6." },
13535
+ glow: { type: "number", minimum: 0, maximum: 50, description: "Glow (blur) radius in canvas units; 0 is none. Defaults: beam 4, pulse 3, others 0." },
13536
+ trackOpacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity of the edge's own line under the animation, 0-1. Defaults: dash 0 (the dashes are the line), beam 0.35, others 1." },
13537
+ paused: { type: "boolean", description: "Hold the animation still at its first frame. Default false." }
13538
+ },
13539
+ required: ["kind"],
13540
+ additionalProperties: false
13541
+ };
13542
+ var labelOf3 = (edge) => typeof edge.label === "string" && edge.label !== "" ? edge.label : void 0;
13543
+ function createCanvasEdgeAnimationToolProvider(target, options = {}) {
13544
+ const readOnlyReason = () => {
13545
+ const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
13546
+ return ro ? typeof ro === "string" ? ro : "Edges are read-only on this canvas." : null;
13547
+ };
13548
+ const checkIds = (ids) => {
13549
+ const known = new Set(target.getEdges().map((e) => e.id));
13550
+ const missing = ids.filter((id) => !known.has(id));
13551
+ if (missing.length) {
13552
+ throw new Error(`No edge${missing.length > 1 ? "s" : ""} ${missing.map((id) => `"${id}"`).join(", ")} on the canvas. Call list_edge_animations for edge ids.`);
13553
+ }
13554
+ };
13555
+ const apply = (label, next) => {
13556
+ const before = /* @__PURE__ */ new Map();
13557
+ for (const edge of target.getEdges()) if (next.has(edge.id)) before.set(edge.id, getCanvasEdgeAnimation(edge));
13558
+ const write = (values) => () => target.setEdges((current) => current.map((edge) => values.has(edge.id) ? setCanvasEdgeAnimation(edge, values.get(edge.id) ?? null) : edge));
13559
+ if (target.history) target.history.execute({ name: label, execute: write(next), undo: write(before) });
13560
+ else write(next)();
13561
+ };
13562
+ const listTool = {
13563
+ name: "list_edge_animations",
13564
+ title: "List edge animations",
13565
+ description: "Lists the canvas edges (id, source, target, label, animation or null) and the animation kinds with their defaults, plus the readOnly reason if edges cannot change. Call it before set_edge_animation or clear_edge_animation: it is the source of edge ids.",
13566
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
13567
+ annotations: { title: "List edge animations", ...READ3 },
13568
+ handler: () => ({
13569
+ edges: target.getEdges().map((e) => ({
13570
+ id: e.id,
13571
+ source: e.source,
13572
+ target: e.target,
13573
+ ...labelOf3(e) ? { label: labelOf3(e) } : {},
13574
+ animation: getCanvasEdgeAnimation(e)
13575
+ })),
13576
+ kinds: CANVAS_EDGE_ANIMATION_KINDS.map((k) => ({ kind: k.kind, title: k.title, description: k.description, defaults: k.defaults })),
13577
+ readOnly: readOnlyReason()
13578
+ })
13579
+ };
13580
+ const setTool = {
13581
+ name: "set_edge_animation",
13582
+ title: "Animate edges",
13583
+ description: "Sets the animation of one or more edges (replacing any they had): travelling dots, marching dashes, a beam or a pulse, with optional speed, direction, colour, size, count, length, glow and track opacity. Fields left out use the kind's defaults. One undo step. Returns the edge ids and the animation as it will play, with every default filled in.",
13584
+ inputSchema: {
13585
+ type: "object",
13586
+ properties: { edges: EDGE_IDS, animation: { ...CANVAS_EDGE_ANIMATION_SCHEMA, description: "The animation. Only kind is required." } },
13587
+ required: ["edges", "animation"],
13588
+ additionalProperties: false
13589
+ },
13590
+ annotations: { title: "Animate edges", ...WRITE_IDEMPOTENT2 },
13591
+ handler: (args) => {
13592
+ const reason = readOnlyReason();
13593
+ if (reason) throw new Error(reason);
13594
+ const ids = args.edges;
13595
+ checkIds(ids);
13596
+ const animation = normalizeCanvasEdgeAnimation(args.animation);
13597
+ apply(ids.length === 1 ? "Animate edge" : `Animate ${ids.length} edges`, new Map(ids.map((id) => [id, animation])));
13598
+ return { edges: ids, animation, resolved: resolveCanvasEdgeAnimation(animation) };
13599
+ }
13600
+ };
13601
+ const clearTool = {
13602
+ name: "clear_edge_animation",
13603
+ title: "Stop edge animations",
13604
+ description: "Removes the animation from one or more edges, leaving the plain edge. Edges without one are left alone. One undo step. Returns the ids cleared and those that had no animation.",
13605
+ inputSchema: { type: "object", properties: { edges: EDGE_IDS }, required: ["edges"], additionalProperties: false },
13606
+ annotations: { title: "Stop edge animations", ...WRITE_IDEMPOTENT2 },
13607
+ handler: (args) => {
13608
+ const reason = readOnlyReason();
13609
+ if (reason) throw new Error(reason);
13610
+ const ids = args.edges;
13611
+ checkIds(ids);
13612
+ const animated = new Set(target.getEdges().filter((e) => getCanvasEdgeAnimation(e) || e.data?.animation !== void 0).map((e) => e.id));
13613
+ const cleared = ids.filter((id) => animated.has(id));
13614
+ if (cleared.length) apply(cleared.length === 1 ? "Stop edge animation" : `Stop ${cleared.length} edge animations`, new Map(cleared.map((id) => [id, null])));
13615
+ return { cleared, unchanged: ids.filter((id) => !animated.has(id)) };
13616
+ }
13617
+ };
13618
+ return {
13619
+ id: options.id ?? "canvas-edge-animation",
13620
+ namespace: options.namespace,
13621
+ title: "Canvas edge animation",
13622
+ description: "Animate canvas edges with travelling dots, marching dashes, beams or a pulse.",
13623
+ skill: "pixerate-canvas",
13624
+ tools: () => readOnlyReason() ? [listTool] : [listTool, setTool, clearTool],
13625
+ subscribe: options.subscribe
13626
+ };
13627
+ }
12969
13628
  export {
12970
13629
  ANIMATABLE_PROPERTIES,
12971
13630
  ANIMATION_DATA_KEY,
@@ -12980,6 +13639,8 @@ export {
12980
13639
  CANVAS_CAPTURE_OVERLAY_SELECTORS,
12981
13640
  CANVAS_CAPTURE_VIEWPORT_SELECTOR,
12982
13641
  CANVAS_COMPONENT_DRAG_TYPE,
13642
+ CANVAS_EDGE_ANIMATION_KINDS,
13643
+ CANVAS_EDGE_ANIMATION_SCHEMA,
12983
13644
  CANVAS_EFFECT_MISSING_PATTERN,
12984
13645
  CANVAS_EFFECT_PLACES,
12985
13646
  CANVAS_GRADIENT_PRESETS,
@@ -13113,6 +13774,7 @@ export {
13113
13774
  createCanvasComponent,
13114
13775
  createCanvasComponentsController,
13115
13776
  createCanvasDrawController,
13777
+ createCanvasEdgeAnimationToolProvider,
13116
13778
  createCanvasEditorDocument,
13117
13779
  createCanvasEffectsController,
13118
13780
  createCanvasEffectsToolProvider,
@@ -13211,6 +13873,9 @@ export {
13211
13873
  getBindableTokenProperties,
13212
13874
  getBrushedShapeSvg,
13213
13875
  getCanvasComponentDragId,
13876
+ getCanvasEdgeAnimation,
13877
+ getCanvasEdgeAnimationRender,
13878
+ getCanvasEdgePath,
13214
13879
  getCanvasTokenProperty,
13215
13880
  getCenteredNodePosition,
13216
13881
  getChildLayout,
@@ -13312,6 +13977,7 @@ export {
13312
13977
  measureBetween,
13313
13978
  measureLabel,
13314
13979
  measureSelection,
13980
+ measureSvgPath,
13315
13981
  measureToLine,
13316
13982
  mergeNodeUpdates,
13317
13983
  mermaidEdgeDash,
@@ -13338,6 +14004,7 @@ export {
13338
14004
  nearestHandlePositions,
13339
14005
  nodeSizeOf,
13340
14006
  normalizeBezierPath,
14007
+ normalizeCanvasEdgeAnimation,
13341
14008
  normalizeKeyframes,
13342
14009
  offsetPath,
13343
14010
  offsetShapeNodes,
@@ -13406,6 +14073,7 @@ export {
13406
14073
  resetCanvasInstanceOverrides,
13407
14074
  resizeHandleAxes,
13408
14075
  resolveCanvasBrushSpec,
14076
+ resolveCanvasEdgeAnimation,
13409
14077
  resolveCanvasGradient,
13410
14078
  resolveCanvasTokens,
13411
14079
  resolveEasing,
@@ -13419,6 +14087,7 @@ export {
13419
14087
  runCanvasNodeEffect,
13420
14088
  runMultiNodeTransition,
13421
14089
  sampleAnimationFrames,
14090
+ sampleCanvasEdgeAnimation,
13422
14091
  sanitizeCanvasDocument,
13423
14092
  scaleKeyframes,
13424
14093
  sceneColor,
@@ -13435,6 +14104,7 @@ export {
13435
14104
  sequenceScene,
13436
14105
  serializeCanvasNodes,
13437
14106
  setCanvasComponentProperty,
14107
+ setCanvasEdgeAnimation,
13438
14108
  setCanvasInstanceProperties,
13439
14109
  setCanvasTokenMode,
13440
14110
  setCanvasVariantProperties,