@phin-tech/dottie 0.6.2 → 0.7.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.
package/dist/dottie.js CHANGED
@@ -1,19 +1,73 @@
1
- /*! dottie.js 0.6.2 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.7.0 | 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 __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
5
5
 
6
+ // src/state.ts
7
+ var MAX_NAME = 64;
8
+ function isName(name) {
9
+ return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
10
+ }
11
+ function conditionNames(condition) {
12
+ const out = [];
13
+ for (const any of condition.split("||")) {
14
+ for (const all of any.split("&&")) {
15
+ let term = trim(all);
16
+ if (term.startsWith("!")) term = trim(term.slice(1));
17
+ if (!isName(term)) return null;
18
+ out.push(term);
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+ function holds(condition, state) {
24
+ if (conditionNames(condition) === null) return false;
25
+ return condition.split("||").some((any) => any.split("&&").every((all) => {
26
+ let term = trim(all);
27
+ let negated = false;
28
+ if (term.startsWith("!")) {
29
+ negated = true;
30
+ term = trim(term.slice(1));
31
+ }
32
+ return (state[term] ?? false) !== negated;
33
+ }));
34
+ }
35
+ function stateValues(page, choice) {
36
+ if (choice === "all") return null;
37
+ if (choice === "initial") return { ...page.variables ?? {} };
38
+ return { ...page.variables ?? {}, ...choice };
39
+ }
40
+ function stateHidden(page, choice) {
41
+ const values = stateValues(page, choice);
42
+ const out = /* @__PURE__ */ new Set();
43
+ if (!values) return out;
44
+ for (const e of page.elements) if (e.when !== null && !holds(e.when, values)) out.add(e.id);
45
+ return out;
46
+ }
47
+ function changesState(c) {
48
+ return !!c && (Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
49
+ }
50
+ function applyClick(c, state) {
51
+ const out = { ...state };
52
+ for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
53
+ for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
54
+ return out;
55
+ }
56
+ function trim(s) {
57
+ return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
58
+ }
59
+
6
60
  // src/model.ts
7
61
  var SUPPORTED_VERSION = 2;
8
62
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
9
63
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
10
64
  var KNOWN_FIELDS = {
11
65
  file: ["version", "pages", "images"],
12
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
66
+ page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables"],
13
67
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
14
68
  layer: ["id", "name", "visible", "locked"],
15
69
  rgba: ["r", "g", "b", "a"],
16
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset"],
70
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
17
71
  element: [
18
72
  "id",
19
73
  "kind",
@@ -37,6 +91,7 @@ var KNOWN_FIELDS = {
37
91
  "anchor",
38
92
  "click",
39
93
  "hover",
94
+ "when",
40
95
  "symbol",
41
96
  "playback",
42
97
  "offset",
@@ -54,15 +109,16 @@ var KNOWN_FIELDS = {
54
109
  extensionRef: ["id", "shape", "version", "params"],
55
110
  normalizedPoint: ["x", "y"],
56
111
  normalizedRect: ["x", "y", "width", "height"],
57
- connector: ["start", "end", "routing", "startHead", "endHead"],
112
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
113
+ waypoint: ["x", "y"],
58
114
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
59
115
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
60
116
  keyframe: ["t", "x", "y", "width", "height", "rotation", "strokeWidth", "stroke", "fill", "visible", "reveal", "opacity", "ease"],
61
117
  follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
62
118
  repeat: ["start", "end", "count"],
63
119
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
64
- marker: ["t", "name"],
65
- click: ["scene", "url"],
120
+ marker: ["t", "name", "loop"],
121
+ click: ["scene", "url", "set", "toggle"],
66
122
  hover: ["stroke", "fill", "opacity"]
67
123
  };
68
124
  var MAX_DURATION = 3600;
@@ -136,7 +192,8 @@ var Reader = class {
136
192
  font: str(s.font) || null,
137
193
  gridSize: num(s.gridSize) ?? null,
138
194
  opacity: num(s.opacity) ?? null,
139
- dashOffset: num(s.dashOffset) ?? null
195
+ dashOffset: num(s.dashOffset) ?? null,
196
+ textColor: rgba(s.textColor) ?? null
140
197
  };
141
198
  }
142
199
  endpoint(v) {
@@ -164,7 +221,12 @@ var Reader = class {
164
221
  end: this.endpoint(v.end),
165
222
  routing: routing && ["straight", "curved", "elbow"].includes(routing) ? routing : "straight",
166
223
  startHead: head(v.startHead, "none"),
167
- endHead: head(v.endHead, "arrow")
224
+ endHead: head(v.endHead, "arrow"),
225
+ waypoints: Array.isArray(v.waypoints) ? v.waypoints.filter(isObj).map((w) => {
226
+ this.fields(w, "waypoint");
227
+ return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
228
+ }) : null,
229
+ hops: typeof v.hops === "boolean" ? v.hops : null
168
230
  };
169
231
  }
170
232
  image(v, table) {
@@ -215,6 +277,7 @@ var Reader = class {
215
277
  anchor: str(v.anchor) ?? null,
216
278
  click: this.click(v.click),
217
279
  hover: this.hover(v.hover),
280
+ when: str(v.when) ?? null,
218
281
  symbol: uuid(v.symbol) ?? null,
219
282
  playback: this.playback(v.playback),
220
283
  offset: num(v.offset) ?? null,
@@ -250,13 +313,27 @@ var Reader = class {
250
313
  }
251
314
  return { id, shape, version, params };
252
315
  }
316
+ variables(v) {
317
+ if (!isObj(v)) return null;
318
+ const out = {};
319
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
320
+ return Object.keys(out).length > 0 ? out : null;
321
+ }
253
322
  click(v) {
254
323
  if (!isObj(v)) return null;
255
324
  this.fields(v, "click");
256
325
  const url = str(v.url);
257
326
  const safe = url === void 0 ? null : allowedURL(url);
258
327
  if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
259
- return { scene: str(v.scene) ?? null, url: safe };
328
+ let set = null;
329
+ if (isObj(v.set)) {
330
+ set = {};
331
+ for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
332
+ if (Object.keys(set).length === 0) set = null;
333
+ }
334
+ const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
335
+ const toggle = raw ? raw.filter(isName) : null;
336
+ return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null };
260
337
  }
261
338
  playback(v) {
262
339
  const s = str(v);
@@ -357,7 +434,7 @@ var Reader = class {
357
434
  if (camera) stableSortBy(camera, (c) => c.t);
358
435
  const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
359
436
  const t = num(m.t);
360
- return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
437
+ return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
361
438
  }) : null;
362
439
  if (markers) stableSortBy(markers, (m) => m.t);
363
440
  const poster = num(v.poster);
@@ -390,7 +467,8 @@ var Reader = class {
390
467
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
391
468
  theme: str(v.theme) ?? null,
392
469
  timeline: this.timeline(v.timeline),
393
- symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
470
+ symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
471
+ variables: this.variables(v.variables)
394
472
  };
395
473
  }
396
474
  };
@@ -441,8 +519,18 @@ function isAutomaticInk(c) {
441
519
  function cloneElement(e) {
442
520
  return {
443
521
  ...e,
444
- style: { ...e.style, stroke: { ...e.style.stroke }, fill: { ...e.style.fill } },
445
- connector: e.connector ? { ...e.connector, start: { ...e.connector.start }, end: { ...e.connector.end } } : null
522
+ style: {
523
+ ...e.style,
524
+ stroke: { ...e.style.stroke },
525
+ fill: { ...e.style.fill },
526
+ textColor: e.style.textColor ? { ...e.style.textColor } : null
527
+ },
528
+ connector: e.connector ? {
529
+ ...e.connector,
530
+ start: { ...e.connector.start },
531
+ end: { ...e.connector.end },
532
+ waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
533
+ } : null
446
534
  };
447
535
  }
448
536
  function orderedElements(page) {
@@ -872,13 +960,19 @@ function rotatedBounds(e) {
872
960
  return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
873
961
  }
874
962
  function resolvingInk(e, ink) {
875
- if (!isAutomaticInk(e.style.stroke)) return e;
963
+ const text = e.style.textColor !== null && isAutomaticInk(e.style.textColor);
964
+ if (!isAutomaticInk(e.style.stroke) && !text) return e;
876
965
  const copy = cloneElement(e);
877
966
  const covers = e.style.fillStyle === "solid" || e.style.fillStyle === "grid";
878
967
  const onLightFill = covers && e.style.fill.a >= 0.5 && luminance(e.style.fill) > 0.5;
879
- copy.style.stroke = luminance(ink) > 0.5 && onLightFill ? { ...black } : { ...ink };
968
+ const resolved2 = luminance(ink) > 0.5 && onLightFill ? black : ink;
969
+ if (isAutomaticInk(e.style.stroke)) copy.style.stroke = { ...resolved2 };
970
+ if (text) copy.style.textColor = { ...resolved2 };
880
971
  return copy;
881
972
  }
973
+ function labelColor(e) {
974
+ return e.kind === "text" || e.kind === "note" ? e.style.stroke : e.style.textColor ?? e.style.stroke;
975
+ }
882
976
  function shapeContours(e) {
883
977
  if (e.kind !== "path" || !e.subpaths) return [];
884
978
  const out = [];
@@ -972,34 +1066,93 @@ function resolve(ep, other, index) {
972
1066
  function endpoints(e, index) {
973
1067
  const c = e.connector;
974
1068
  if (!c) return [{ x: 0, y: 0 }, { x: 0, y: 0 }];
975
- return [resolve(c.start, reference(c.end, index), index), resolve(c.end, reference(c.start, index), index)];
1069
+ const w = c.waypoints ?? [];
1070
+ return [
1071
+ resolve(c.start, w.length > 0 ? w[0] : reference(c.end, index), index),
1072
+ resolve(c.end, w.length > 0 ? w[w.length - 1] : reference(c.start, index), index)
1073
+ ];
976
1074
  }
977
1075
  function routePoints(r) {
978
1076
  if (r.kind === "polyline") return r.points;
979
- const out = [];
980
- for (let i = 0; i <= 24; i++) {
981
- const t = i / 24, u = 1 - t;
982
- out.push({
983
- 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,
984
- 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
985
- });
1077
+ const out = [], p = r.points;
1078
+ for (let s = 0; s < Math.floor((p.length - 1) / 3); s++) {
1079
+ const a = p[3 * s], b = p[3 * s + 1], c = p[3 * s + 2], d = p[3 * s + 3];
1080
+ for (let i = s === 0 ? 0 : 1; i <= 24; i++) {
1081
+ const t = i / 24, u = 1 - t;
1082
+ out.push({
1083
+ x: u * u * u * a.x + 3 * u * u * t * b.x + 3 * u * t * t * c.x + t * t * t * d.x,
1084
+ y: u * u * u * a.y + 3 * u * u * t * b.y + 3 * u * t * t * c.y + t * t * t * d.y
1085
+ });
1086
+ }
986
1087
  }
987
1088
  return out;
988
1089
  }
1090
+ var vertical = (p) => p === null ? null : p === "top" || p === "bottom";
1091
+ function simplified(pts) {
1092
+ const out = [];
1093
+ for (const q of pts) {
1094
+ const l = out[out.length - 1];
1095
+ if (l && Math.abs(l.x - q.x) < 0.5 && Math.abs(l.y - q.y) < 0.5) continue;
1096
+ if (out.length >= 2) {
1097
+ const p = out[out.length - 2], m = out[out.length - 1];
1098
+ 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) {
1099
+ out[out.length - 1] = q;
1100
+ continue;
1101
+ }
1102
+ }
1103
+ out.push(q);
1104
+ }
1105
+ return out.length >= 2 ? out : pts;
1106
+ }
1107
+ function routeThrough(p, c) {
1108
+ switch (c.routing) {
1109
+ case "straight":
1110
+ return { kind: "polyline", points: p };
1111
+ case "curved": {
1112
+ const out = [p[0]];
1113
+ for (let i = 0; i < p.length - 1; i++) {
1114
+ const p0 = p[Math.max(i - 1, 0)], p1 = p[i], p2 = p[i + 1], p3 = p[Math.min(i + 2, p.length - 1)];
1115
+ out.push({ x: p1.x + (p2.x - p0.x) / 6, y: p1.y + (p2.y - p0.y) / 6 });
1116
+ out.push({ x: p2.x - (p3.x - p1.x) / 6, y: p2.y - (p3.y - p1.y) / 6 });
1117
+ out.push(p2);
1118
+ }
1119
+ return { kind: "curve", points: out };
1120
+ }
1121
+ case "elbow": {
1122
+ const a = p[0], first = p[1];
1123
+ let goVertical = vertical(c.start.port) ?? Math.abs(first.y - a.y) > Math.abs(first.x - a.x);
1124
+ const endV = vertical(c.end.port);
1125
+ const out = [a];
1126
+ for (let i = 1; i < p.length; i++) {
1127
+ const q = p[i], cur = out[out.length - 1];
1128
+ const alignedX = Math.abs(cur.x - q.x) < 0.5, alignedY = Math.abs(cur.y - q.y) < 0.5;
1129
+ if (alignedX || alignedY) {
1130
+ out.push(q);
1131
+ if (alignedX !== alignedY) goVertical = !alignedX;
1132
+ continue;
1133
+ }
1134
+ const up = i === p.length - 1 && endV !== null ? !endV : goVertical;
1135
+ out.push(up ? { x: cur.x, y: q.y } : { x: q.x, y: cur.y }, q);
1136
+ goVertical = up;
1137
+ }
1138
+ return { kind: "polyline", points: simplified(out) };
1139
+ }
1140
+ }
1141
+ }
989
1142
  function route(e, index) {
990
1143
  const c = e.connector;
991
1144
  if (!c) return { kind: "polyline", points: [] };
992
1145
  const [a, b] = endpoints(e, index);
1146
+ if (c.waypoints && c.waypoints.length > 0) return routeThrough([a, ...c.waypoints.map((w) => ({ x: w.x, y: w.y })), b], c);
993
1147
  const dx = b.x - a.x, dy = b.y - a.y;
994
1148
  const horizontal = Math.abs(dx) >= Math.abs(dy);
995
1149
  switch (c.routing) {
996
1150
  case "straight":
997
1151
  return { kind: "polyline", points: [a, b] };
998
1152
  case "curved":
999
- 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 };
1000
- return { kind: "cubic", a, b: { x: a.x, y: a.y + dy / 2 }, c: { x: b.x, y: b.y - dy / 2 }, d: b };
1153
+ if (horizontal) return { kind: "curve", points: [a, { x: a.x + dx / 2, y: a.y }, { x: b.x - dx / 2, y: b.y }, b] };
1154
+ return { kind: "curve", points: [a, { x: a.x, y: a.y + dy / 2 }, { x: b.x, y: b.y - dy / 2 }, b] };
1001
1155
  case "elbow": {
1002
- const vertical = (p) => p === null ? null : p === "top" || p === "bottom";
1003
1156
  const startV = vertical(c.start.port) ?? !horizontal;
1004
1157
  const endV = vertical(c.end.port) ?? !horizontal;
1005
1158
  if (startV && endV) {
@@ -1579,10 +1732,12 @@ function dashPattern(dash, width) {
1579
1732
  return [0.01, Math.max(5, width * 2.6)];
1580
1733
  }
1581
1734
  }
1582
- function connectorParts(e, index) {
1735
+ function connectorParts(e, index, page) {
1583
1736
  const c = e.connector;
1584
1737
  if (!c) return [new Path(), new Path()];
1585
1738
  const [a, b] = endpoints(e, index);
1739
+ const r = route(e, index);
1740
+ const hops = c.hops === true && page ? hopCrossings(e, r, page, index) : [];
1586
1741
  const key = [
1587
1742
  e.seed,
1588
1743
  e.style.roughness,
@@ -1596,28 +1751,166 @@ function connectorParts(e, index) {
1596
1751
  a.x,
1597
1752
  a.y,
1598
1753
  b.x,
1599
- b.y
1754
+ b.y,
1755
+ JSON.stringify(c.waypoints),
1756
+ JSON.stringify(hops)
1600
1757
  ].join("|");
1601
- return connectors.get(key, () => computeConnector(e, index));
1758
+ return connectors.get(key, () => computeConnector(e, r, hops));
1759
+ }
1760
+ function hopRadius(strokeWidth) {
1761
+ return Math.max(8, strokeWidth * 4);
1602
1762
  }
1603
- function computeConnector(e, index) {
1763
+ function hopSamples(r) {
1764
+ const pts = r.points, out = [];
1765
+ if (r.kind === "polyline") {
1766
+ for (let i = 0; i < pts.length - 1; i++) out.push([pts[i], pts[i + 1]]);
1767
+ return out;
1768
+ }
1769
+ for (let s = 0; s < Math.floor((pts.length - 1) / 3); s++) {
1770
+ const seg = pts.slice(3 * s, 3 * s + 4), samples = [];
1771
+ for (let i = 0; i <= 24; i++) samples.push(cubicPoint(seg, i / 24));
1772
+ out.push(samples);
1773
+ }
1774
+ return out;
1775
+ }
1776
+ function runningLengths(samples) {
1777
+ const out = [0];
1778
+ 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));
1779
+ return out;
1780
+ }
1781
+ function cubicPoint(p, t) {
1782
+ const u = 1 - t;
1783
+ return {
1784
+ 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,
1785
+ 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
1786
+ };
1787
+ }
1788
+ function subCubic(p, t0, t1) {
1789
+ const split = (q, t) => {
1790
+ const lerp = (a, b) => ({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
1791
+ const ab = lerp(q[0], q[1]), bc = lerp(q[1], q[2]), cd = lerp(q[2], q[3]);
1792
+ const abc = lerp(ab, bc), bcd = lerp(bc, cd), m = lerp(abc, bcd);
1793
+ return [[q[0], ab, abc, m], [m, bcd, cd, q[3]]];
1794
+ };
1795
+ const left = split(p, t1)[0];
1796
+ return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
1797
+ }
1798
+ function hopCrossings(e, r, page, index) {
1799
+ const segments = hopSamples(r);
1800
+ if (segments.length === 0) return [];
1801
+ const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
1802
+ const beneath = [];
1803
+ for (const o of orderedElements(page)) {
1804
+ if (o.id === e.id) break;
1805
+ if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
1806
+ beneath.push(routePoints(route(o, index)));
1807
+ }
1808
+ const rad = hopRadius(e.style.strokeWidth);
1809
+ const out = [];
1810
+ for (const samples of segments) {
1811
+ const along = runningLengths(samples);
1812
+ const length = along[along.length - 1];
1813
+ const found = [];
1814
+ for (let i = 0; i < samples.length - 1; i++) {
1815
+ const p = samples[i], q = samples[i + 1];
1816
+ const dx = q.x - p.x, dy = q.y - p.y;
1817
+ for (const line2 of beneath) {
1818
+ for (let k = 0; k < line2.length - 1; k++) {
1819
+ const a = line2[k], b = line2[k + 1];
1820
+ const ex = b.x - a.x, ey = b.y - a.y;
1821
+ const det = dx * ey - dy * ex;
1822
+ if (Math.abs(det) < 1e-9) continue;
1823
+ const wx = a.x - p.x, wy = a.y - p.y;
1824
+ const t = (wx * ey - wy * ex) / det, s = (wx * dy - wy * dx) / det;
1825
+ if (t > 0 && t < 1 && s >= 0 && s <= 1) found.push(along[i] + t * (along[i + 1] - along[i]));
1826
+ }
1827
+ }
1828
+ }
1829
+ const kept = [];
1830
+ for (const at of found.sort((x, y) => x - y)) {
1831
+ if (at < rad + 2 || at > length - rad - 2) continue;
1832
+ if (kept.length > 0 && at - kept[kept.length - 1] < 2 * rad + 2) continue;
1833
+ kept.push(at);
1834
+ }
1835
+ out.push(kept);
1836
+ }
1837
+ return out;
1838
+ }
1839
+ function computeConnector(e, r, hops) {
1604
1840
  const c = e.connector;
1605
1841
  const rng = new SeededRNG(e.seed);
1606
1842
  const rough = e.style.roughness, bow = e.style.bowing;
1607
1843
  const path = new Path(), solid = new Path();
1608
- const r = route(e, index);
1844
+ const w = rough * 3;
1845
+ const j = () => rng.signed() * w;
1846
+ const hop = (s, t, u, rad2) => {
1847
+ let n = { x: u.y, y: -u.x };
1848
+ if (n.y > 1e-9 || Math.abs(n.y) <= 1e-9 && n.x > 0) n = { x: -n.x, y: -n.y };
1849
+ const k = rad2 * 0.5523;
1850
+ const m = { x: (s.x + t.x) / 2, y: (s.y + t.y) / 2 };
1851
+ const top = { x: m.x + n.x * rad2, y: m.y + n.y * rad2 };
1852
+ path.moveTo(s);
1853
+ 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();
1854
+ path.curveTo(top, { x: a1x, y: a1y }, { x: a2x, y: a2y });
1855
+ 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();
1856
+ path.curveTo(t, { x: b1x, y: b1y }, { x: b2x, y: b2y });
1857
+ };
1858
+ const rad = hopRadius(e.style.strokeWidth);
1609
1859
  if (r.kind === "polyline") {
1610
1860
  const pts2 = r.points;
1861
+ const straight = (p, q) => {
1862
+ if (rough <= 0) path.moveTo(p).lineTo(q);
1863
+ else line(p, q, rng, rough, bow, true, path);
1864
+ };
1611
1865
  for (let i = 0; i < Math.max(pts2.length - 1, 0); i++) {
1612
- if (rough <= 0) path.moveTo(pts2[i]).lineTo(pts2[i + 1]);
1613
- else line(pts2[i], pts2[i + 1], rng, rough, bow, true, path);
1866
+ const p = pts2[i], q = pts2[i + 1];
1867
+ const length = Math.hypot(q.x - p.x, q.y - p.y);
1868
+ const u = length > 0 ? { x: (q.x - p.x) / length, y: (q.y - p.y) / length } : { x: 0, y: 0 };
1869
+ const at = (d) => ({ x: p.x + u.x * d, y: p.y + u.y * d });
1870
+ let from = p;
1871
+ for (const d of i < hops.length ? hops[i] : []) {
1872
+ const s = at(d - rad), t = at(d + rad);
1873
+ straight(from, s);
1874
+ hop(s, t, u, rad);
1875
+ from = t;
1876
+ }
1877
+ straight(from, q);
1614
1878
  }
1615
1879
  } else {
1616
- path.moveTo(r.a);
1617
- const w = rough * 3;
1618
- const j = () => rng.signed() * w;
1619
- const c1x = r.b.x + j(), c1y = r.b.y + j(), c2x = r.c.x + j(), c2y = r.c.y + j();
1620
- path.curveTo(r.d, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1880
+ const pts2 = r.points;
1881
+ const samples = hopSamples(r);
1882
+ path.moveTo(pts2[0]);
1883
+ for (let s = 0; s < Math.floor((pts2.length - 1) / 3); s++) {
1884
+ const b = pts2[3 * s + 1], c2 = pts2[3 * s + 2], d = pts2[3 * s + 3];
1885
+ const c1x = b.x + j(), c1y = b.y + j(), c2x = c2.x + j(), c2y = c2.y + j();
1886
+ const here = s < hops.length ? hops[s] : [];
1887
+ if (here.length === 0) {
1888
+ path.curveTo(d, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1889
+ continue;
1890
+ }
1891
+ const cubic = [pts2[3 * s], { x: c1x, y: c1y }, { x: c2x, y: c2y }, d];
1892
+ const along = runningLengths(samples[s]);
1893
+ const param = (at) => {
1894
+ let i = 0;
1895
+ while (i < along.length - 2 && along[i + 1] < at) i++;
1896
+ const span = along[i + 1] - along[i];
1897
+ return (i + (span > 0 ? (at - along[i]) / span : 0)) / 24;
1898
+ };
1899
+ let from = 0;
1900
+ for (const at of here) {
1901
+ const t0 = param(at - rad), t1 = param(at + rad);
1902
+ const piece = subCubic(cubic, from, t0);
1903
+ if (s > 0 || from > 0) path.moveTo(piece[0]);
1904
+ path.curveTo(piece[3], piece[1], piece[2]);
1905
+ const a = cubicPoint(cubic, t0), z = cubicPoint(cubic, t1);
1906
+ const width = Math.hypot(z.x - a.x, z.y - a.y);
1907
+ const u = width > 0 ? { x: (z.x - a.x) / width, y: (z.y - a.y) / width } : { x: 0, y: 0 };
1908
+ hop(a, z, u, width / 2);
1909
+ from = t1;
1910
+ }
1911
+ const rest = subCubic(cubic, from, 1);
1912
+ path.moveTo(rest[0]).curveTo(rest[3], rest[1], rest[2]);
1913
+ }
1621
1914
  }
1622
1915
  const pts = routePoints(r);
1623
1916
  const size = Math.max(10, e.style.strokeWidth * 5);
@@ -1639,10 +1932,10 @@ function computeConnector(e, index) {
1639
1932
  solid.moveTo(tip).lineTo(w1).lineTo(w2).close();
1640
1933
  break;
1641
1934
  case "dot": {
1642
- const rad = size * 0.32;
1935
+ const rad2 = size * 0.32;
1643
1936
  const ang = Math.atan2(tip.y - from.y, tip.x - from.x);
1644
- const cx = tip.x - rad * Math.cos(ang), cy = tip.y - rad * Math.sin(ang);
1645
- solid.addEllipse({ x: cx - rad, y: cy - rad, width: 2 * rad, height: 2 * rad });
1937
+ const cx = tip.x - rad2 * Math.cos(ang), cy = tip.y - rad2 * Math.sin(ang);
1938
+ solid.addEllipse({ x: cx - rad2, y: cy - rad2, width: 2 * rad2, height: 2 * rad2 });
1646
1939
  break;
1647
1940
  }
1648
1941
  case "bar": {
@@ -1845,17 +2138,20 @@ function trackMap(tl) {
1845
2138
  for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
1846
2139
  return m;
1847
2140
  }
1848
- function resolved(page, t) {
1849
- return expandingSymbols(posed(page, t), clampTime(t));
2141
+ function resolved(page, t, state = "initial") {
2142
+ return expandingSymbols(posed(page, t, state), clampTime(t));
1850
2143
  }
1851
- function posed(page, t) {
1852
- const timeline = page.timeline;
2144
+ var EMPTY_TIMELINE = { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
2145
+ function posed(page, t, state = "initial") {
2146
+ const off = stateHidden(page, state);
2147
+ const timeline = page.timeline ?? (off.size === 0 ? null : EMPTY_TIMELINE);
1853
2148
  if (!timeline) return page;
1854
2149
  t = clampTime(t);
1855
2150
  const tracks = trackMap(timeline);
1856
2151
  const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
1857
2152
  const hidden = /* @__PURE__ */ new Set();
1858
2153
  for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
2154
+ for (const id of off) hidden.add(id);
1859
2155
  let moving = null;
1860
2156
  if (timeline.settle === true) {
1861
2157
  moving = /* @__PURE__ */ new Set();
@@ -1894,7 +2190,7 @@ function posed(page, t) {
1894
2190
  elements = elements.map((e) => {
1895
2191
  const c = e.connector;
1896
2192
  if (!c || ![c.start.elementID, c.end.elementID].some((id) => id !== null && hidden.has(id))) return e;
1897
- if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false)) {
2193
+ if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false) || [c.start.elementID, c.end.elementID].some((id) => id !== null && off.has(id))) {
1898
2194
  gone.add(e.id);
1899
2195
  return e;
1900
2196
  }
@@ -1910,7 +2206,7 @@ function posterTime(page) {
1910
2206
  return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
1911
2207
  }
1912
2208
  function still(page) {
1913
- return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(page, 0);
2209
+ return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(posed(page, 0), 0);
1914
2210
  }
1915
2211
  function cameraRect(tl, t) {
1916
2212
  const keys = tl.camera ?? [];
@@ -2019,7 +2315,7 @@ function filled(s, args) {
2019
2315
  }) };
2020
2316
  }
2021
2317
  function symbolPage(s) {
2022
- return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null };
2318
+ return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null };
2023
2319
  }
2024
2320
  function hasInstances(page) {
2025
2321
  return page.elements.some((e) => e.kind === "symbol");
@@ -2109,7 +2405,12 @@ function placed(inner, instance, symbol) {
2109
2405
  const q = point(p);
2110
2406
  return { elementID: null, port: p.port, x: q.x, y: q.y };
2111
2407
  };
2112
- e.connector = { ...inner.connector, start: end(inner.connector.start), end: end(inner.connector.end) };
2408
+ e.connector = {
2409
+ ...inner.connector,
2410
+ start: end(inner.connector.start),
2411
+ end: end(inner.connector.end),
2412
+ waypoints: inner.connector.waypoints?.map(point) ?? null
2413
+ };
2113
2414
  }
2114
2415
  return e;
2115
2416
  }
@@ -2143,16 +2444,21 @@ function clickTargets(page, file) {
2143
2444
  let pageOf = null;
2144
2445
  for (const e of page.elements) {
2145
2446
  const c = e.click;
2447
+ const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {} } : null;
2146
2448
  if (c?.scene) {
2147
2449
  const i = sceneNamed(c.scene, markers);
2148
2450
  if (i >= 0) {
2149
- out.set(e.id, { scene: i });
2451
+ out.set(e.id, { scene: i, ...state });
2150
2452
  continue;
2151
2453
  }
2152
2454
  }
2153
2455
  const url = (c?.url ? allowedURL(c.url) : null) ?? webLink(e.url);
2154
2456
  if (url) {
2155
- out.set(e.id, { url });
2457
+ out.set(e.id, { url, ...state });
2458
+ continue;
2459
+ }
2460
+ if (state) {
2461
+ out.set(e.id, { state: true, ...state });
2156
2462
  continue;
2157
2463
  }
2158
2464
  if (e.link && file && file.pages.length > 1) {
@@ -2885,7 +3191,7 @@ function svgParts(page, background, fixed, links = false) {
2885
3191
  if (!visible.has(e.layerID)) continue;
2886
3192
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
2887
3193
  if (alpha <= 0) continue;
2888
- let body = svgElement(e, index, bg);
3194
+ let body = svgElement(e, index, bg, d);
2889
3195
  if (angle(e) !== 0 && e.kind !== "connector") {
2890
3196
  body = `<g transform="rotate(${fmt(angle(e) * 180 / Math.PI)} ${fmt(midX(frame(e)))} ${fmt(midY(frame(e)))})">
2891
3197
  ${body}</g>
@@ -2920,7 +3226,7 @@ function typed(s, progress) {
2920
3226
  function revealProgress(e) {
2921
3227
  return e.reveal !== null && e.reveal < 1 ? Math.max(0, e.reveal) : null;
2922
3228
  }
2923
- function svgElement(e, index, labelBackground) {
3229
+ function svgElement(e, index, labelBackground, page) {
2924
3230
  const stroke = color(e.style.stroke);
2925
3231
  let strokeAttrs = `stroke="${stroke}" stroke-width="${fmt(e.style.strokeWidth)}" stroke-linecap="round" stroke-linejoin="round"`;
2926
3232
  const dash = dashPattern(e.style.dash, e.style.strokeWidth);
@@ -2931,7 +3237,7 @@ function svgElement(e, index, labelBackground) {
2931
3237
  if (r !== null) {
2932
3238
  if (e.kind === "text") text = typed(text ?? "", r);
2933
3239
  else if (e.kind === "connector") {
2934
- return `<path d="${connectorParts(e, index)[0].trimmed(r).data()}" fill="none" ${strokeAttrs}/>
3240
+ return `<path d="${connectorParts(e, index, page)[0].trimmed(r).data()}" fill="none" ${strokeAttrs}/>
2935
3241
  `;
2936
3242
  } else {
2937
3243
  return `<path d="${outline(e).trimmed(r).data()}" fill="none" ${strokeAttrs}/>
@@ -2940,7 +3246,7 @@ function svgElement(e, index, labelBackground) {
2940
3246
  }
2941
3247
  switch (e.kind) {
2942
3248
  case "connector": {
2943
- const [line2, filled2] = connectorParts(e, index);
3249
+ const [line2, filled2] = connectorParts(e, index, page);
2944
3250
  let s = `<path d="${line2.data()}" fill="none" ${strokeAttrs}/>
2945
3251
  `;
2946
3252
  if (!filled2.isEmpty) s += `<path d="${filled2.data()}" fill="${stroke}" stroke="none"/>
@@ -3023,7 +3329,7 @@ function textElement(text, f, e, centered) {
3023
3329
  const total = lineHeight * lines.length;
3024
3330
  const x = centered ? midX(f) : f.x;
3025
3331
  const top = centered ? midY(f) - total / 2 : f.y;
3026
- 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"` : ""}>`;
3332
+ let s = `<text x="${fmt(x)}" font-family="${cssFamily(e.style.font)}" font-size="${fmt(size)}" fill="${color(labelColor(e))}"${centered ? ` text-anchor="middle"` : ""}>`;
3027
3333
  lines.forEach((line2, i) => {
3028
3334
  s += `<tspan x="${fmt(x)}" y="${fmt(top + lineHeight * (i + 0.8))}">${escapeXML(line2)}</tspan>`;
3029
3335
  });
@@ -3066,6 +3372,10 @@ var Playback = class {
3066
3372
  __publicField(this, "plan");
3067
3373
  __publicField(this, "scenes");
3068
3374
  __publicField(this, "sceneNames");
3375
+ /** Whether each scene loops (Marker.loop). */
3376
+ __publicField(this, "sceneLoops");
3377
+ /** The page's variables now: their starting values until clicks change them. */
3378
+ __publicField(this, "state");
3069
3379
  __publicField(this, "background");
3070
3380
  __publicField(this, "ink");
3071
3381
  /** What clicking each clickable element does, by element id. */
@@ -3125,6 +3435,8 @@ var Playback = class {
3125
3435
  };
3126
3436
  this.scenes = (page.timeline?.markers ?? []).map((m) => near(m.t));
3127
3437
  this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
3438
+ this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
3439
+ this.state = { ...page.variables ?? {} };
3128
3440
  const theme = themeNamed(page.theme);
3129
3441
  this.background = theme?.background ?? WHITE;
3130
3442
  this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
@@ -3175,7 +3487,7 @@ var Playback = class {
3175
3487
  const times = /* @__PURE__ */ new Map();
3176
3488
  for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
3177
3489
  const tc = clampTime(t);
3178
- const pose = posed(this.page, tc);
3490
+ const pose = posed(this.page, tc, this.state);
3179
3491
  const frame2 = expandingSymbols(pose, tc, true, times);
3180
3492
  const { parts } = svgParts(frame2, this.background, r);
3181
3493
  const owned = (m, parts2) => {
@@ -3297,6 +3609,20 @@ ${h}</g>
3297
3609
  });
3298
3610
  return s;
3299
3611
  }
3612
+ /** The frame after scene `s`'s last: the next scene's, or the frame count after the last scene. */
3613
+ sceneEnd(s) {
3614
+ return this.scenes.find((f, j) => j > s && f > this.scenes[s]) ?? this.frameCount;
3615
+ }
3616
+ /** Where playing goes from frame `i`, and whether it stops there: on into a looping scene, round to the start of
3617
+ * the looping scene `i` ends, or a stop at a scene or the end (`loops`: whether the timeline loops at its end). */
3618
+ step(i, loops) {
3619
+ const next = i + 1;
3620
+ const s = this.sceneAt(i);
3621
+ if (s >= 0 && this.sceneLoops[s] && next >= this.sceneEnd(s)) return { frame: this.scenes[s], stop: false };
3622
+ if (next >= this.frameCount) return { frame: 0, stop: !loops || this.scenes.length > 0 };
3623
+ const entered = this.scenes.indexOf(next);
3624
+ return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
3625
+ }
3300
3626
  };
3301
3627
  var ElementBase = typeof HTMLElement === "undefined" ? class {
3302
3628
  } : HTMLElement;
@@ -3450,7 +3776,7 @@ var DottiePlayer = class extends ElementBase {
3450
3776
  this.setPlaying(false);
3451
3777
  return;
3452
3778
  }
3453
- this.setPlaying(!this.hasAttribute("no-autoplay") && !p.scenes.includes(0));
3779
+ this.setPlaying(!this.hasAttribute("no-autoplay") && (!p.scenes.includes(0) || p.sceneLoops[p.scenes.indexOf(0)]));
3454
3780
  }
3455
3781
  // MARK: Playback
3456
3782
  get paused() {
@@ -3476,11 +3802,13 @@ var DottiePlayer = class extends ElementBase {
3476
3802
  pause() {
3477
3803
  this.setPlaying(false);
3478
3804
  }
3479
- /** Plays on to the next scene (from the start after the last). */
3805
+ /** Plays on to the next scene (from the start after the last). Out of a looping scene it jumps to the next. */
3480
3806
  nextScene() {
3481
3807
  const p = this.playback;
3482
3808
  if (!p) return;
3483
- if (p.frame >= p.frameCount - 1) p.show(0);
3809
+ const s = p.sceneAt(p.frame);
3810
+ if (s >= 0 && p.sceneLoops[s]) p.show(s + 1 < p.scenes.length ? p.scenes[s + 1] : 0);
3811
+ else if (p.frame >= p.frameCount - 1) p.show(0);
3484
3812
  this.setPlaying(true);
3485
3813
  }
3486
3814
  /** Back to the start of this scene, or the one before if already there. */
@@ -3509,6 +3837,8 @@ var DottiePlayer = class extends ElementBase {
3509
3837
  this.scheduleReplay();
3510
3838
  }
3511
3839
  if (!click) return;
3840
+ if (click.set || click.toggle) this.setState(applyClick({ scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null }, p.state));
3841
+ if ("state" in click) return;
3512
3842
  if ("url" in click) {
3513
3843
  if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
3514
3844
  } else if ("page" in click) {
@@ -3516,13 +3846,27 @@ var DottiePlayer = class extends ElementBase {
3516
3846
  } else if (click.scene >= 0 && click.scene < p.scenes.length) {
3517
3847
  if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
3518
3848
  this.setPlaying(false);
3519
- p.show(click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
3849
+ p.show(p.sceneLoops[click.scene] ? p.scenes[click.scene] : click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
3520
3850
  } else {
3521
3851
  p.show(p.scenes[click.scene]);
3522
3852
  this.setPlaying(true);
3523
3853
  }
3524
3854
  }
3525
3855
  }
3856
+ /** The page's variables now. */
3857
+ get variables() {
3858
+ return { ...this.playback?.state ?? {} };
3859
+ }
3860
+ /** Gives the page's variables these values (the others keep theirs), redraws, and fires `state`. */
3861
+ setState(values) {
3862
+ const p = this.playback;
3863
+ if (!p) return;
3864
+ const next = { ...p.state, ...values };
3865
+ if (Object.keys(next).every((k) => next[k] === p.state[k])) return;
3866
+ p.state = next;
3867
+ p.redraw();
3868
+ this.dispatchEvent(new CustomEvent("state", { detail: { variables: { ...next } } }));
3869
+ }
3526
3870
  /** Redraws, while the page itself stands still, as long as a clicked copy is playing through. */
3527
3871
  scheduleReplay() {
3528
3872
  if (this.replayRaf) return;
@@ -3548,7 +3892,8 @@ var DottiePlayer = class extends ElementBase {
3548
3892
  schedule() {
3549
3893
  if (this.playing && this.visible && !this.raf && this.isConnected) this.raf = requestAnimationFrame((now) => this.tick(now));
3550
3894
  }
3551
- /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end. */
3895
+ /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end, except that a
3896
+ * looping scene plays round (Playback.step). */
3552
3897
  tick(now) {
3553
3898
  this.raf = 0;
3554
3899
  const p = this.playback;
@@ -3559,14 +3904,14 @@ var DottiePlayer = class extends ElementBase {
3559
3904
  let advanced = false;
3560
3905
  while (this.playing && this.acc >= step) {
3561
3906
  this.acc -= step;
3562
- const next = p.frame + 1;
3563
- if (next >= p.frameCount && (p.scenes.length || this.hasAttribute("no-loop"))) {
3907
+ const { frame: frame2, stop } = p.step(p.frame, !this.hasAttribute("no-loop"));
3908
+ if (stop && frame2 === 0 && p.frame + 1 >= p.frameCount) {
3564
3909
  this.setPlaying(false);
3565
3910
  break;
3566
3911
  }
3567
- p.frame = next % p.frameCount;
3912
+ p.frame = frame2;
3568
3913
  advanced = true;
3569
- if (p.scenes.includes(p.frame)) {
3914
+ if (stop) {
3570
3915
  this.setPlaying(false);
3571
3916
  break;
3572
3917
  }
@@ -3672,6 +4017,7 @@ function inspectFrame(page, t) {
3672
4017
  d.strokeWidth = r4(e.style.strokeWidth);
3673
4018
  d.stroke = rgba2(e.style.stroke);
3674
4019
  d.fill = rgba2(e.style.fill);
4020
+ if (e.style.textColor !== null) d.textColor = rgba2(e.style.textColor);
3675
4021
  if (e.kind === "connector") {
3676
4022
  const [a, b] = endpoints(e, index);
3677
4023
  d.start = pt(a);
@@ -3715,7 +4061,7 @@ function render(source, options = {}) {
3715
4061
  function isFile(v) {
3716
4062
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
3717
4063
  }
3718
- var VERSION = "0.6.2";
4064
+ var VERSION = "0.7.0";
3719
4065
  export {
3720
4066
  BOIL_DRAWINGS,
3721
4067
  BUNDLED_FONTS,