@phin-tech/dottie 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PARITY.md CHANGED
@@ -24,7 +24,9 @@ fixture, so the gap is visible rather than silent.
24
24
  | Ports and edge-following, rotated shapes, free ends | Supported | [edge/connectors](../fixtures/edge/connectors.dottie) | |
25
25
  | Connectors and labels on outlines (ADR 0001) | Supported | [edge/extensions](../fixtures/edge/extensions.dottie) | Edge-following meets a `.path`'s actual outline, not its box (ray against 16-step flattened curves). Endpoints pinned with `portIndex` attach to the element's `ports`. A path's label goes in its `labelRect`. |
26
26
  | Extension shapes as stored (ADR 0001) | Supported | [edge/extensions](../fixtures/edge/extensions.dottie) | Drawn from the stored path with no extension installed. The `extension` field is kept as metadata and nothing runs. Parameters, commands and handles are Dottie-only. |
27
+ | Waypoints (bends) on every routing, and hops | Supported | [edge/waypoints](../fixtures/edge/waypoints.dottie), [edge/waypoints-symbol](../fixtures/edge/waypoints-symbol.dottie) | Hops need the page, so `connectorParts` takes it; without one a line draws none. |
27
28
  | Arrowheads: arrow, triangle, dot, bar | Supported | [edge/connectors](../fixtures/edge/connectors.dottie) | |
29
+ | Label colors (`textColor`, apart from the stroke) | Supported | [edge/text-colors](../fixtures/edge/text-colors.dottie) | Text boxes and notes ignore it, as in Dottie |
28
30
  | Connector labels | Supported | [edge/connectors](../fixtures/edge/connectors.dottie) | Label boxes sized with CoreText's measurements. The player paints them on the page color, like the app; Dottie's flipbook export leaves them unpainted |
29
31
  | Keys and eases (linear, in, out, in-out, hold, spring, bounce, elastic) | Supported | [edge/timeline](../fixtures/edge/timeline.dottie), [edge/eases](../fixtures/edge/eases.dottie) | Unknown ease names play linear, with a warning |
30
32
  | Visibility, reveal (draw-on, typing), opacity | Supported | [edge/timeline](../fixtures/edge/timeline.dottie) | |
package/dist/compile.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.6.2 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.6.3 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -54,7 +54,7 @@ var KNOWN_FIELDS = {
54
54
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
55
55
  layer: ["id", "name", "visible", "locked"],
56
56
  rgba: ["r", "g", "b", "a"],
57
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset"],
57
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
58
58
  element: [
59
59
  "id",
60
60
  "kind",
@@ -95,7 +95,8 @@ var KNOWN_FIELDS = {
95
95
  extensionRef: ["id", "shape", "version", "params"],
96
96
  normalizedPoint: ["x", "y"],
97
97
  normalizedRect: ["x", "y", "width", "height"],
98
- connector: ["start", "end", "routing", "startHead", "endHead"],
98
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
99
+ waypoint: ["x", "y"],
99
100
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
100
101
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
101
102
  keyframe: ["t", "x", "y", "width", "height", "rotation", "strokeWidth", "stroke", "fill", "visible", "reveal", "opacity", "ease"],
@@ -172,7 +173,8 @@ var Reader = class {
172
173
  font: str(s.font) || null,
173
174
  gridSize: num(s.gridSize) ?? null,
174
175
  opacity: num(s.opacity) ?? null,
175
- dashOffset: num(s.dashOffset) ?? null
176
+ dashOffset: num(s.dashOffset) ?? null,
177
+ textColor: rgba(s.textColor) ?? null
176
178
  };
177
179
  }
178
180
  endpoint(v) {
@@ -200,7 +202,12 @@ var Reader = class {
200
202
  end: this.endpoint(v.end),
201
203
  routing: routing && ["straight", "curved", "elbow"].includes(routing) ? routing : "straight",
202
204
  startHead: head(v.startHead, "none"),
203
- endHead: head(v.endHead, "arrow")
205
+ endHead: head(v.endHead, "arrow"),
206
+ waypoints: Array.isArray(v.waypoints) ? v.waypoints.filter(isObj).map((w) => {
207
+ this.fields(w, "waypoint");
208
+ return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
209
+ }) : null,
210
+ hops: typeof v.hops === "boolean" ? v.hops : null
204
211
  };
205
212
  }
206
213
  image(v, table) {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /*! dottie.js 0.6.2 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
+ /*! dottie.js 0.6.3 | Apache-2.0 | https://github.com/phin-tech/dottie */
3
3
  var __defProp = Object.defineProperty;
4
4
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
5
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -62,7 +62,7 @@ var KNOWN_FIELDS = {
62
62
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
63
63
  layer: ["id", "name", "visible", "locked"],
64
64
  rgba: ["r", "g", "b", "a"],
65
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset"],
65
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
66
66
  element: [
67
67
  "id",
68
68
  "kind",
@@ -103,7 +103,8 @@ var KNOWN_FIELDS = {
103
103
  extensionRef: ["id", "shape", "version", "params"],
104
104
  normalizedPoint: ["x", "y"],
105
105
  normalizedRect: ["x", "y", "width", "height"],
106
- connector: ["start", "end", "routing", "startHead", "endHead"],
106
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
107
+ waypoint: ["x", "y"],
107
108
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
108
109
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
109
110
  keyframe: ["t", "x", "y", "width", "height", "rotation", "strokeWidth", "stroke", "fill", "visible", "reveal", "opacity", "ease"],
@@ -180,7 +181,8 @@ var Reader = class {
180
181
  font: str(s.font) || null,
181
182
  gridSize: num(s.gridSize) ?? null,
182
183
  opacity: num(s.opacity) ?? null,
183
- dashOffset: num(s.dashOffset) ?? null
184
+ dashOffset: num(s.dashOffset) ?? null,
185
+ textColor: rgba(s.textColor) ?? null
184
186
  };
185
187
  }
186
188
  endpoint(v) {
@@ -208,7 +210,12 @@ var Reader = class {
208
210
  end: this.endpoint(v.end),
209
211
  routing: routing && ["straight", "curved", "elbow"].includes(routing) ? routing : "straight",
210
212
  startHead: head(v.startHead, "none"),
211
- endHead: head(v.endHead, "arrow")
213
+ endHead: head(v.endHead, "arrow"),
214
+ waypoints: Array.isArray(v.waypoints) ? v.waypoints.filter(isObj).map((w) => {
215
+ this.fields(w, "waypoint");
216
+ return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
217
+ }) : null,
218
+ hops: typeof v.hops === "boolean" ? v.hops : null
212
219
  };
213
220
  }
214
221
  image(v, table) {
@@ -581,7 +588,7 @@ function compile(source2, runtime, options2 = {}) {
581
588
  }
582
589
 
583
590
  // src/index.ts
584
- var VERSION = "0.6.2";
591
+ var VERSION = "0.6.3";
585
592
 
586
593
  // src/cli.ts
587
594
  var usage = `dottie-compile ${VERSION}: a Dottie drawing as one script that plays it on any page.
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.6.2 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.6.3 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
4
4
  var __export = (target, all) => {
@@ -76,7 +76,7 @@ var KNOWN_FIELDS = {
76
76
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
77
77
  layer: ["id", "name", "visible", "locked"],
78
78
  rgba: ["r", "g", "b", "a"],
79
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset"],
79
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
80
80
  element: [
81
81
  "id",
82
82
  "kind",
@@ -117,7 +117,8 @@ var KNOWN_FIELDS = {
117
117
  extensionRef: ["id", "shape", "version", "params"],
118
118
  normalizedPoint: ["x", "y"],
119
119
  normalizedRect: ["x", "y", "width", "height"],
120
- connector: ["start", "end", "routing", "startHead", "endHead"],
120
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
121
+ waypoint: ["x", "y"],
121
122
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
122
123
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
123
124
  keyframe: ["t", "x", "y", "width", "height", "rotation", "strokeWidth", "stroke", "fill", "visible", "reveal", "opacity", "ease"],
@@ -199,7 +200,8 @@ var Reader = class {
199
200
  font: str(s.font) || null,
200
201
  gridSize: num(s.gridSize) ?? null,
201
202
  opacity: num(s.opacity) ?? null,
202
- dashOffset: num(s.dashOffset) ?? null
203
+ dashOffset: num(s.dashOffset) ?? null,
204
+ textColor: rgba(s.textColor) ?? null
203
205
  };
204
206
  }
205
207
  endpoint(v) {
@@ -227,7 +229,12 @@ var Reader = class {
227
229
  end: this.endpoint(v.end),
228
230
  routing: routing && ["straight", "curved", "elbow"].includes(routing) ? routing : "straight",
229
231
  startHead: head(v.startHead, "none"),
230
- endHead: head(v.endHead, "arrow")
232
+ endHead: head(v.endHead, "arrow"),
233
+ waypoints: Array.isArray(v.waypoints) ? v.waypoints.filter(isObj).map((w) => {
234
+ this.fields(w, "waypoint");
235
+ return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
236
+ }) : null,
237
+ hops: typeof v.hops === "boolean" ? v.hops : null
231
238
  };
232
239
  }
233
240
  image(v, table) {
@@ -504,8 +511,18 @@ function isAutomaticInk(c) {
504
511
  function cloneElement(e) {
505
512
  return {
506
513
  ...e,
507
- style: { ...e.style, stroke: { ...e.style.stroke }, fill: { ...e.style.fill } },
508
- connector: e.connector ? { ...e.connector, start: { ...e.connector.start }, end: { ...e.connector.end } } : null
514
+ style: {
515
+ ...e.style,
516
+ stroke: { ...e.style.stroke },
517
+ fill: { ...e.style.fill },
518
+ textColor: e.style.textColor ? { ...e.style.textColor } : null
519
+ },
520
+ connector: e.connector ? {
521
+ ...e.connector,
522
+ start: { ...e.connector.start },
523
+ end: { ...e.connector.end },
524
+ waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
525
+ } : null
509
526
  };
510
527
  }
511
528
  function orderedElements(page) {
@@ -935,13 +952,19 @@ function rotatedBounds(e) {
935
952
  return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
936
953
  }
937
954
  function resolvingInk(e, ink) {
938
- if (!isAutomaticInk(e.style.stroke)) return e;
955
+ const text = e.style.textColor !== null && isAutomaticInk(e.style.textColor);
956
+ if (!isAutomaticInk(e.style.stroke) && !text) return e;
939
957
  const copy = cloneElement(e);
940
958
  const covers = e.style.fillStyle === "solid" || e.style.fillStyle === "grid";
941
959
  const onLightFill = covers && e.style.fill.a >= 0.5 && luminance(e.style.fill) > 0.5;
942
- copy.style.stroke = luminance(ink) > 0.5 && onLightFill ? { ...black } : { ...ink };
960
+ const resolved2 = luminance(ink) > 0.5 && onLightFill ? black : ink;
961
+ if (isAutomaticInk(e.style.stroke)) copy.style.stroke = { ...resolved2 };
962
+ if (text) copy.style.textColor = { ...resolved2 };
943
963
  return copy;
944
964
  }
965
+ function labelColor(e) {
966
+ return e.kind === "text" || e.kind === "note" ? e.style.stroke : e.style.textColor ?? e.style.stroke;
967
+ }
945
968
  function shapeContours(e) {
946
969
  if (e.kind !== "path" || !e.subpaths) return [];
947
970
  const out = [];
@@ -1035,34 +1058,93 @@ function resolve(ep, other, index) {
1035
1058
  function endpoints(e, index) {
1036
1059
  const c = e.connector;
1037
1060
  if (!c) return [{ x: 0, y: 0 }, { x: 0, y: 0 }];
1038
- return [resolve(c.start, reference(c.end, index), index), resolve(c.end, reference(c.start, index), index)];
1061
+ const w = c.waypoints ?? [];
1062
+ return [
1063
+ resolve(c.start, w.length > 0 ? w[0] : reference(c.end, index), index),
1064
+ resolve(c.end, w.length > 0 ? w[w.length - 1] : reference(c.start, index), index)
1065
+ ];
1039
1066
  }
1040
1067
  function routePoints(r) {
1041
1068
  if (r.kind === "polyline") return r.points;
1042
- const out = [];
1043
- for (let i = 0; i <= 24; i++) {
1044
- const t = i / 24, u = 1 - t;
1045
- out.push({
1046
- x: u * u * u * r.a.x + 3 * u * u * t * r.b.x + 3 * u * t * t * r.c.x + t * t * t * r.d.x,
1047
- y: u * u * u * r.a.y + 3 * u * u * t * r.b.y + 3 * u * t * t * r.c.y + t * t * t * r.d.y
1048
- });
1069
+ const out = [], p = r.points;
1070
+ for (let s = 0; s < Math.floor((p.length - 1) / 3); s++) {
1071
+ const a = p[3 * s], b = p[3 * s + 1], c = p[3 * s + 2], d = p[3 * s + 3];
1072
+ for (let i = s === 0 ? 0 : 1; i <= 24; i++) {
1073
+ const t = i / 24, u = 1 - t;
1074
+ out.push({
1075
+ x: u * u * u * a.x + 3 * u * u * t * b.x + 3 * u * t * t * c.x + t * t * t * d.x,
1076
+ y: u * u * u * a.y + 3 * u * u * t * b.y + 3 * u * t * t * c.y + t * t * t * d.y
1077
+ });
1078
+ }
1049
1079
  }
1050
1080
  return out;
1051
1081
  }
1082
+ var vertical = (p) => p === null ? null : p === "top" || p === "bottom";
1083
+ function simplified(pts) {
1084
+ const out = [];
1085
+ for (const q of pts) {
1086
+ const l = out[out.length - 1];
1087
+ if (l && Math.abs(l.x - q.x) < 0.5 && Math.abs(l.y - q.y) < 0.5) continue;
1088
+ if (out.length >= 2) {
1089
+ const p = out[out.length - 2], m = out[out.length - 1];
1090
+ if (Math.abs(p.x - m.x) < 0.5 && Math.abs(m.x - q.x) < 0.5 || Math.abs(p.y - m.y) < 0.5 && Math.abs(m.y - q.y) < 0.5) {
1091
+ out[out.length - 1] = q;
1092
+ continue;
1093
+ }
1094
+ }
1095
+ out.push(q);
1096
+ }
1097
+ return out.length >= 2 ? out : pts;
1098
+ }
1099
+ function routeThrough(p, c) {
1100
+ switch (c.routing) {
1101
+ case "straight":
1102
+ return { kind: "polyline", points: p };
1103
+ case "curved": {
1104
+ const out = [p[0]];
1105
+ for (let i = 0; i < p.length - 1; i++) {
1106
+ const p0 = p[Math.max(i - 1, 0)], p1 = p[i], p2 = p[i + 1], p3 = p[Math.min(i + 2, p.length - 1)];
1107
+ out.push({ x: p1.x + (p2.x - p0.x) / 6, y: p1.y + (p2.y - p0.y) / 6 });
1108
+ out.push({ x: p2.x - (p3.x - p1.x) / 6, y: p2.y - (p3.y - p1.y) / 6 });
1109
+ out.push(p2);
1110
+ }
1111
+ return { kind: "curve", points: out };
1112
+ }
1113
+ case "elbow": {
1114
+ const a = p[0], first = p[1];
1115
+ let goVertical = vertical(c.start.port) ?? Math.abs(first.y - a.y) > Math.abs(first.x - a.x);
1116
+ const endV = vertical(c.end.port);
1117
+ const out = [a];
1118
+ for (let i = 1; i < p.length; i++) {
1119
+ const q = p[i], cur = out[out.length - 1];
1120
+ const alignedX = Math.abs(cur.x - q.x) < 0.5, alignedY = Math.abs(cur.y - q.y) < 0.5;
1121
+ if (alignedX || alignedY) {
1122
+ out.push(q);
1123
+ if (alignedX !== alignedY) goVertical = !alignedX;
1124
+ continue;
1125
+ }
1126
+ const up = i === p.length - 1 && endV !== null ? !endV : goVertical;
1127
+ out.push(up ? { x: cur.x, y: q.y } : { x: q.x, y: cur.y }, q);
1128
+ goVertical = up;
1129
+ }
1130
+ return { kind: "polyline", points: simplified(out) };
1131
+ }
1132
+ }
1133
+ }
1052
1134
  function route(e, index) {
1053
1135
  const c = e.connector;
1054
1136
  if (!c) return { kind: "polyline", points: [] };
1055
1137
  const [a, b] = endpoints(e, index);
1138
+ if (c.waypoints && c.waypoints.length > 0) return routeThrough([a, ...c.waypoints.map((w) => ({ x: w.x, y: w.y })), b], c);
1056
1139
  const dx = b.x - a.x, dy = b.y - a.y;
1057
1140
  const horizontal = Math.abs(dx) >= Math.abs(dy);
1058
1141
  switch (c.routing) {
1059
1142
  case "straight":
1060
1143
  return { kind: "polyline", points: [a, b] };
1061
1144
  case "curved":
1062
- if (horizontal) return { kind: "cubic", a, b: { x: a.x + dx / 2, y: a.y }, c: { x: b.x - dx / 2, y: b.y }, d: b };
1063
- return { kind: "cubic", a, b: { x: a.x, y: a.y + dy / 2 }, c: { x: b.x, y: b.y - dy / 2 }, d: b };
1145
+ if (horizontal) return { kind: "curve", points: [a, { x: a.x + dx / 2, y: a.y }, { x: b.x - dx / 2, y: b.y }, b] };
1146
+ return { kind: "curve", points: [a, { x: a.x, y: a.y + dy / 2 }, { x: b.x, y: b.y - dy / 2 }, b] };
1064
1147
  case "elbow": {
1065
- const vertical = (p) => p === null ? null : p === "top" || p === "bottom";
1066
1148
  const startV = vertical(c.start.port) ?? !horizontal;
1067
1149
  const endV = vertical(c.end.port) ?? !horizontal;
1068
1150
  if (startV && endV) {
@@ -1642,10 +1724,12 @@ function dashPattern(dash, width) {
1642
1724
  return [0.01, Math.max(5, width * 2.6)];
1643
1725
  }
1644
1726
  }
1645
- function connectorParts(e, index) {
1727
+ function connectorParts(e, index, page) {
1646
1728
  const c = e.connector;
1647
1729
  if (!c) return [new Path(), new Path()];
1648
1730
  const [a, b] = endpoints(e, index);
1731
+ const r = route(e, index);
1732
+ const hops = c.hops === true && page ? hopCrossings(e, r, page, index) : [];
1649
1733
  const key = [
1650
1734
  e.seed,
1651
1735
  e.style.roughness,
@@ -1659,28 +1743,166 @@ function connectorParts(e, index) {
1659
1743
  a.x,
1660
1744
  a.y,
1661
1745
  b.x,
1662
- b.y
1746
+ b.y,
1747
+ JSON.stringify(c.waypoints),
1748
+ JSON.stringify(hops)
1663
1749
  ].join("|");
1664
- return connectors.get(key, () => computeConnector(e, index));
1750
+ return connectors.get(key, () => computeConnector(e, r, hops));
1751
+ }
1752
+ function hopRadius(strokeWidth) {
1753
+ return Math.max(8, strokeWidth * 4);
1754
+ }
1755
+ function hopSamples(r) {
1756
+ const pts = r.points, out = [];
1757
+ if (r.kind === "polyline") {
1758
+ for (let i = 0; i < pts.length - 1; i++) out.push([pts[i], pts[i + 1]]);
1759
+ return out;
1760
+ }
1761
+ for (let s = 0; s < Math.floor((pts.length - 1) / 3); s++) {
1762
+ const seg = pts.slice(3 * s, 3 * s + 4), samples = [];
1763
+ for (let i = 0; i <= 24; i++) samples.push(cubicPoint(seg, i / 24));
1764
+ out.push(samples);
1765
+ }
1766
+ return out;
1767
+ }
1768
+ function runningLengths(samples) {
1769
+ const out = [0];
1770
+ for (let i = 1; i < samples.length; i++) out.push(out[i - 1] + Math.hypot(samples[i].x - samples[i - 1].x, samples[i].y - samples[i - 1].y));
1771
+ return out;
1772
+ }
1773
+ function cubicPoint(p, t) {
1774
+ const u = 1 - t;
1775
+ return {
1776
+ x: u * u * u * p[0].x + 3 * u * u * t * p[1].x + 3 * u * t * t * p[2].x + t * t * t * p[3].x,
1777
+ y: u * u * u * p[0].y + 3 * u * u * t * p[1].y + 3 * u * t * t * p[2].y + t * t * t * p[3].y
1778
+ };
1779
+ }
1780
+ function subCubic(p, t0, t1) {
1781
+ const split = (q, t) => {
1782
+ const lerp = (a, b) => ({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
1783
+ const ab = lerp(q[0], q[1]), bc = lerp(q[1], q[2]), cd = lerp(q[2], q[3]);
1784
+ const abc = lerp(ab, bc), bcd = lerp(bc, cd), m = lerp(abc, bcd);
1785
+ return [[q[0], ab, abc, m], [m, bcd, cd, q[3]]];
1786
+ };
1787
+ const left = split(p, t1)[0];
1788
+ return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
1789
+ }
1790
+ function hopCrossings(e, r, page, index) {
1791
+ const segments = hopSamples(r);
1792
+ if (segments.length === 0) return [];
1793
+ const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
1794
+ const beneath = [];
1795
+ for (const o of orderedElements(page)) {
1796
+ if (o.id === e.id) break;
1797
+ if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
1798
+ beneath.push(routePoints(route(o, index)));
1799
+ }
1800
+ const rad = hopRadius(e.style.strokeWidth);
1801
+ const out = [];
1802
+ for (const samples of segments) {
1803
+ const along = runningLengths(samples);
1804
+ const length = along[along.length - 1];
1805
+ const found = [];
1806
+ for (let i = 0; i < samples.length - 1; i++) {
1807
+ const p = samples[i], q = samples[i + 1];
1808
+ const dx = q.x - p.x, dy = q.y - p.y;
1809
+ for (const line2 of beneath) {
1810
+ for (let k = 0; k < line2.length - 1; k++) {
1811
+ const a = line2[k], b = line2[k + 1];
1812
+ const ex = b.x - a.x, ey = b.y - a.y;
1813
+ const det = dx * ey - dy * ex;
1814
+ if (Math.abs(det) < 1e-9) continue;
1815
+ const wx = a.x - p.x, wy = a.y - p.y;
1816
+ const t = (wx * ey - wy * ex) / det, s = (wx * dy - wy * dx) / det;
1817
+ if (t > 0 && t < 1 && s >= 0 && s <= 1) found.push(along[i] + t * (along[i + 1] - along[i]));
1818
+ }
1819
+ }
1820
+ }
1821
+ const kept = [];
1822
+ for (const at of found.sort((x, y) => x - y)) {
1823
+ if (at < rad + 2 || at > length - rad - 2) continue;
1824
+ if (kept.length > 0 && at - kept[kept.length - 1] < 2 * rad + 2) continue;
1825
+ kept.push(at);
1826
+ }
1827
+ out.push(kept);
1828
+ }
1829
+ return out;
1665
1830
  }
1666
- function computeConnector(e, index) {
1831
+ function computeConnector(e, r, hops) {
1667
1832
  const c = e.connector;
1668
1833
  const rng = new SeededRNG(e.seed);
1669
1834
  const rough = e.style.roughness, bow = e.style.bowing;
1670
1835
  const path = new Path(), solid = new Path();
1671
- const r = route(e, index);
1836
+ const w = rough * 3;
1837
+ const j = () => rng.signed() * w;
1838
+ const hop = (s, t, u, rad2) => {
1839
+ let n = { x: u.y, y: -u.x };
1840
+ if (n.y > 1e-9 || Math.abs(n.y) <= 1e-9 && n.x > 0) n = { x: -n.x, y: -n.y };
1841
+ const k = rad2 * 0.5523;
1842
+ const m = { x: (s.x + t.x) / 2, y: (s.y + t.y) / 2 };
1843
+ const top = { x: m.x + n.x * rad2, y: m.y + n.y * rad2 };
1844
+ path.moveTo(s);
1845
+ const a1x = s.x + n.x * k + j(), a1y = s.y + n.y * k + j(), a2x = top.x - u.x * k + j(), a2y = top.y - u.y * k + j();
1846
+ path.curveTo(top, { x: a1x, y: a1y }, { x: a2x, y: a2y });
1847
+ const b1x = top.x + u.x * k + j(), b1y = top.y + u.y * k + j(), b2x = t.x + n.x * k + j(), b2y = t.y + n.y * k + j();
1848
+ path.curveTo(t, { x: b1x, y: b1y }, { x: b2x, y: b2y });
1849
+ };
1850
+ const rad = hopRadius(e.style.strokeWidth);
1672
1851
  if (r.kind === "polyline") {
1673
1852
  const pts2 = r.points;
1853
+ const straight = (p, q) => {
1854
+ if (rough <= 0) path.moveTo(p).lineTo(q);
1855
+ else line(p, q, rng, rough, bow, true, path);
1856
+ };
1674
1857
  for (let i = 0; i < Math.max(pts2.length - 1, 0); i++) {
1675
- if (rough <= 0) path.moveTo(pts2[i]).lineTo(pts2[i + 1]);
1676
- else line(pts2[i], pts2[i + 1], rng, rough, bow, true, path);
1858
+ const p = pts2[i], q = pts2[i + 1];
1859
+ const length = Math.hypot(q.x - p.x, q.y - p.y);
1860
+ const u = length > 0 ? { x: (q.x - p.x) / length, y: (q.y - p.y) / length } : { x: 0, y: 0 };
1861
+ const at = (d) => ({ x: p.x + u.x * d, y: p.y + u.y * d });
1862
+ let from = p;
1863
+ for (const d of i < hops.length ? hops[i] : []) {
1864
+ const s = at(d - rad), t = at(d + rad);
1865
+ straight(from, s);
1866
+ hop(s, t, u, rad);
1867
+ from = t;
1868
+ }
1869
+ straight(from, q);
1677
1870
  }
1678
1871
  } else {
1679
- path.moveTo(r.a);
1680
- const w = rough * 3;
1681
- const j = () => rng.signed() * w;
1682
- const c1x = r.b.x + j(), c1y = r.b.y + j(), c2x = r.c.x + j(), c2y = r.c.y + j();
1683
- path.curveTo(r.d, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1872
+ const pts2 = r.points;
1873
+ const samples = hopSamples(r);
1874
+ path.moveTo(pts2[0]);
1875
+ for (let s = 0; s < Math.floor((pts2.length - 1) / 3); s++) {
1876
+ const b = pts2[3 * s + 1], c2 = pts2[3 * s + 2], d = pts2[3 * s + 3];
1877
+ const c1x = b.x + j(), c1y = b.y + j(), c2x = c2.x + j(), c2y = c2.y + j();
1878
+ const here = s < hops.length ? hops[s] : [];
1879
+ if (here.length === 0) {
1880
+ path.curveTo(d, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1881
+ continue;
1882
+ }
1883
+ const cubic = [pts2[3 * s], { x: c1x, y: c1y }, { x: c2x, y: c2y }, d];
1884
+ const along = runningLengths(samples[s]);
1885
+ const param = (at) => {
1886
+ let i = 0;
1887
+ while (i < along.length - 2 && along[i + 1] < at) i++;
1888
+ const span = along[i + 1] - along[i];
1889
+ return (i + (span > 0 ? (at - along[i]) / span : 0)) / 24;
1890
+ };
1891
+ let from = 0;
1892
+ for (const at of here) {
1893
+ const t0 = param(at - rad), t1 = param(at + rad);
1894
+ const piece = subCubic(cubic, from, t0);
1895
+ if (s > 0 || from > 0) path.moveTo(piece[0]);
1896
+ path.curveTo(piece[3], piece[1], piece[2]);
1897
+ const a = cubicPoint(cubic, t0), z = cubicPoint(cubic, t1);
1898
+ const width = Math.hypot(z.x - a.x, z.y - a.y);
1899
+ const u = width > 0 ? { x: (z.x - a.x) / width, y: (z.y - a.y) / width } : { x: 0, y: 0 };
1900
+ hop(a, z, u, width / 2);
1901
+ from = t1;
1902
+ }
1903
+ const rest = subCubic(cubic, from, 1);
1904
+ path.moveTo(rest[0]).curveTo(rest[3], rest[1], rest[2]);
1905
+ }
1684
1906
  }
1685
1907
  const pts = routePoints(r);
1686
1908
  const size = Math.max(10, e.style.strokeWidth * 5);
@@ -1702,10 +1924,10 @@ function computeConnector(e, index) {
1702
1924
  solid.moveTo(tip).lineTo(w1).lineTo(w2).close();
1703
1925
  break;
1704
1926
  case "dot": {
1705
- const rad = size * 0.32;
1927
+ const rad2 = size * 0.32;
1706
1928
  const ang = Math.atan2(tip.y - from.y, tip.x - from.x);
1707
- const cx = tip.x - rad * Math.cos(ang), cy = tip.y - rad * Math.sin(ang);
1708
- solid.addEllipse({ x: cx - rad, y: cy - rad, width: 2 * rad, height: 2 * rad });
1929
+ const cx = tip.x - rad2 * Math.cos(ang), cy = tip.y - rad2 * Math.sin(ang);
1930
+ solid.addEllipse({ x: cx - rad2, y: cy - rad2, width: 2 * rad2, height: 2 * rad2 });
1709
1931
  break;
1710
1932
  }
1711
1933
  case "bar": {
@@ -2172,7 +2394,12 @@ function placed(inner, instance, symbol) {
2172
2394
  const q = point(p);
2173
2395
  return { elementID: null, port: p.port, x: q.x, y: q.y };
2174
2396
  };
2175
- e.connector = { ...inner.connector, start: end(inner.connector.start), end: end(inner.connector.end) };
2397
+ e.connector = {
2398
+ ...inner.connector,
2399
+ start: end(inner.connector.start),
2400
+ end: end(inner.connector.end),
2401
+ waypoints: inner.connector.waypoints?.map(point) ?? null
2402
+ };
2176
2403
  }
2177
2404
  return e;
2178
2405
  }
@@ -2948,7 +3175,7 @@ function svgParts(page, background, fixed, links = false) {
2948
3175
  if (!visible.has(e.layerID)) continue;
2949
3176
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
2950
3177
  if (alpha <= 0) continue;
2951
- let body = svgElement(e, index, bg);
3178
+ let body = svgElement(e, index, bg, d);
2952
3179
  if (angle(e) !== 0 && e.kind !== "connector") {
2953
3180
  body = `<g transform="rotate(${fmt(angle(e) * 180 / Math.PI)} ${fmt(midX(frame(e)))} ${fmt(midY(frame(e)))})">
2954
3181
  ${body}</g>
@@ -2983,7 +3210,7 @@ function typed(s, progress) {
2983
3210
  function revealProgress(e) {
2984
3211
  return e.reveal !== null && e.reveal < 1 ? Math.max(0, e.reveal) : null;
2985
3212
  }
2986
- function svgElement(e, index, labelBackground) {
3213
+ function svgElement(e, index, labelBackground, page) {
2987
3214
  const stroke = color(e.style.stroke);
2988
3215
  let strokeAttrs = `stroke="${stroke}" stroke-width="${fmt(e.style.strokeWidth)}" stroke-linecap="round" stroke-linejoin="round"`;
2989
3216
  const dash = dashPattern(e.style.dash, e.style.strokeWidth);
@@ -2994,7 +3221,7 @@ function svgElement(e, index, labelBackground) {
2994
3221
  if (r !== null) {
2995
3222
  if (e.kind === "text") text = typed(text ?? "", r);
2996
3223
  else if (e.kind === "connector") {
2997
- return `<path d="${connectorParts(e, index)[0].trimmed(r).data()}" fill="none" ${strokeAttrs}/>
3224
+ return `<path d="${connectorParts(e, index, page)[0].trimmed(r).data()}" fill="none" ${strokeAttrs}/>
2998
3225
  `;
2999
3226
  } else {
3000
3227
  return `<path d="${outline(e).trimmed(r).data()}" fill="none" ${strokeAttrs}/>
@@ -3003,7 +3230,7 @@ function svgElement(e, index, labelBackground) {
3003
3230
  }
3004
3231
  switch (e.kind) {
3005
3232
  case "connector": {
3006
- const [line2, filled2] = connectorParts(e, index);
3233
+ const [line2, filled2] = connectorParts(e, index, page);
3007
3234
  let s = `<path d="${line2.data()}" fill="none" ${strokeAttrs}/>
3008
3235
  `;
3009
3236
  if (!filled2.isEmpty) s += `<path d="${filled2.data()}" fill="${stroke}" stroke="none"/>
@@ -3086,7 +3313,7 @@ function textElement(text, f, e, centered) {
3086
3313
  const total = lineHeight * lines.length;
3087
3314
  const x = centered ? midX(f) : f.x;
3088
3315
  const top = centered ? midY(f) - total / 2 : f.y;
3089
- let s = `<text x="${fmt(x)}" font-family="${cssFamily(e.style.font)}" font-size="${fmt(size)}" fill="${color(e.style.stroke)}"${centered ? ` text-anchor="middle"` : ""}>`;
3316
+ let s = `<text x="${fmt(x)}" font-family="${cssFamily(e.style.font)}" font-size="${fmt(size)}" fill="${color(labelColor(e))}"${centered ? ` text-anchor="middle"` : ""}>`;
3090
3317
  lines.forEach((line2, i) => {
3091
3318
  s += `<tspan x="${fmt(x)}" y="${fmt(top + lineHeight * (i + 0.8))}">${escapeXML(line2)}</tspan>`;
3092
3319
  });
@@ -3735,6 +3962,7 @@ function inspectFrame(page, t) {
3735
3962
  d.strokeWidth = r4(e.style.strokeWidth);
3736
3963
  d.stroke = rgba2(e.style.stroke);
3737
3964
  d.fill = rgba2(e.style.fill);
3965
+ if (e.style.textColor !== null) d.textColor = rgba2(e.style.textColor);
3738
3966
  if (e.kind === "connector") {
3739
3967
  const [a, b] = endpoints(e, index);
3740
3968
  d.start = pt(a);
@@ -3778,7 +4006,7 @@ function render(source, options = {}) {
3778
4006
  function isFile(v) {
3779
4007
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
3780
4008
  }
3781
- var VERSION = "0.6.2";
4009
+ var VERSION = "0.6.3";
3782
4010
 
3783
4011
  // src/player-entry.ts
3784
4012
  definePlayer();