@phin-tech/dottie 0.9.0 → 0.11.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/PARITY.md CHANGED
@@ -24,6 +24,11 @@ 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
+ | Click logic (`logic`, a click's `run`) | Supported | [edge/logic](../fixtures/edge/logic.dottie) | One engine for both: `logic.ts` (QuickJS in WebAssembly), run by `LogicEngine.swift` in JavaScriptCore. Runs only with `allow-logic` (ADR 0007). |
28
+ | Number variables, expressions in conditions and clicks, `{value}` in labels | Supported | [edge/counters](../fixtures/edge/counters.dottie), [expressions.json](../fixtures/expressions.json) | One grammar in `Expression.swift` and `expression.ts` (ADR 0007). |
29
+ | Containers and swimlanes | Supported | [edge/containers](../fixtures/edge/containers.dottie) | Drawn from the stored frame and title band (`labelRect`); the app keeps them fitted (ADR 0006). |
30
+ | Themes of a page's own, and palette colors by name | Supported | [edge/palette](../fixtures/edge/palette.dottie) | Names are resolved when a file is read, by the same rules in `Theme.swift` and `theme.ts` (ADR 0005). |
31
+ | Elbows that go round shapes (`avoid`) | Supported | [edge/elbow-around](../fixtures/edge/elbow-around.dottie) | The same grid router, point for point (`ElbowRouter.swift`, `elbow.ts`). |
27
32
  | 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. |
28
33
  | Arrowheads: arrow, triangle, dot, bar | Supported | [edge/connectors](../fixtures/edge/connectors.dottie) | |
29
34
  | 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 |
package/README.md CHANGED
@@ -56,6 +56,21 @@ when it is showing. The classic script starts the worker from its own source (a
56
56
  loads `dottie-player.min.js` from beside itself, so host the two together. Where a worker can't start, as under a
57
57
  `worker-src` policy that refuses it, the player works it out in place as before. Small drawings always do.
58
58
 
59
+ **Click logic.** Variables can be numbers, and conditions and clicks are small expressions (`count >= 3`,
60
+ `"count + 1"`, `{count}` in a label). For more, a page can carry `logic`: JavaScript handlers a click `run`s, each
61
+ getting the variables and the click and returning new values. The player runs them only with `allow-logic`:
62
+
63
+ ```html
64
+ <dottie-player src="game.dottie" allow-logic></dottie-player>
65
+ ```
66
+
67
+ Each click's handler runs in a fresh QuickJS sandbox compiled to WebAssembly, never in your page's JavaScript: it has
68
+ no `window`, `document`, network, timers, `Date` or `Math.random`, and a time and memory limit, so the same click on
69
+ the same state gives the same result everywhere. A handler that fails leaves the state as it was and fires
70
+ `logicerror` (with `{element, handler, error}`). Without `allow-logic`, a click does its own `set` and `toggle`
71
+ instead. The engine (`dottie-logic.min.js`, about 850 KB) is loaded from beside the player only when a page with
72
+ logic is clicked, so host it with the player; `dottie-compile --allow-logic` puts it in the compiled script instead.
73
+
59
74
  **Fonts.** Text in Dottie's handwriting fonts (Caveat, Patrick Hand, Architects Daughter; SIL Open Font License)
60
75
  is drawn in them: the player loads the ones a drawing uses from `fonts/` beside its own script, as `dist/fonts` on
61
76
  npm and the CDNs. If you host the player yourself, copy that folder with it, or call `Dottie.setFontBase(url)`
package/dist/compile.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.9.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.11.0 | 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);
@@ -50,19 +50,80 @@ function isName(name) {
50
50
  return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
51
51
  }
52
52
 
53
+ // src/theme.ts
54
+ var rgb = (r, g, b) => ({ r, g, b, a: 1 });
55
+ function hex(h) {
56
+ const n = parseInt(h.slice(1), 16);
57
+ return { r: (n >> 16 & 255) / 255, g: (n >> 8 & 255) / 255, b: (n & 255) / 255, a: 1 };
58
+ }
59
+ var PALETTE_NAMES = ["accent", "muted", "danger", "warning", "success", "info"];
60
+ var palette = (colors) => PALETTE_NAMES.map((name, i) => ({ name, color: hex(colors[i]) }));
61
+ var LIGHT_PALETTE = palette(["#2563eb", "#6b7280", "#dc2626", "#d97706", "#16a34a", "#0891b2"]);
62
+ var DARK_PALETTE = palette(["#93c5fd", "#cbd5e1", "#fca5a5", "#fcd34d", "#86efac", "#67e8f9"]);
63
+ var themes = [
64
+ { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
65
+ { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
66
+ {
67
+ id: "sepia",
68
+ name: "Sepia",
69
+ background: rgb(0.953, 0.914, 0.824),
70
+ ink: rgb(0.23, 0.18, 0.13),
71
+ palette: palette(["#9a3412", "#78716c", "#b91c1c", "#b45309", "#4d7c0f", "#0e7490"])
72
+ },
73
+ { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984), palette: DARK_PALETTE },
74
+ { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918), palette: DARK_PALETTE },
75
+ { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94), palette: DARK_PALETTE }
76
+ ];
77
+ function themeNamed(id) {
78
+ return themes.find((t) => t.id === id) ?? null;
79
+ }
80
+ function pageTheme(page) {
81
+ const c = page.customTheme;
82
+ if (c) return { id: "custom", name: c.name, background: c.background, ink: c.ink, palette: c.palette };
83
+ return themeNamed(page.theme);
84
+ }
85
+ function paletteColor(page, name) {
86
+ const n = name.trim().toLowerCase();
87
+ return (pageTheme(page)?.palette ?? LIGHT_PALETTE).find((p) => p.name.toLowerCase() === n)?.color ?? null;
88
+ }
89
+ function resolvingPalette(page) {
90
+ const named = (e) => !!e.style.named;
91
+ if (!page.elements.some(named) && !(page.symbols ?? []).some((s) => s.elements.some(named))) return page;
92
+ const resolve = (e) => {
93
+ const n = e.style.named;
94
+ if (!n) return e;
95
+ const style = { ...e.style };
96
+ const f = n.fill !== null ? paletteColor(page, n.fill) : null;
97
+ const s = n.stroke !== null ? paletteColor(page, n.stroke) : null;
98
+ const t = n.textColor !== null ? paletteColor(page, n.textColor) : null;
99
+ if (f) style.fill = { ...f };
100
+ if (s) style.stroke = { ...s };
101
+ if (t) style.textColor = { ...t };
102
+ return { ...e, style };
103
+ };
104
+ return {
105
+ ...page,
106
+ elements: page.elements.map(resolve),
107
+ symbols: page.symbols ? page.symbols.map((s) => ({ ...s, elements: s.elements.map(resolve) })) : page.symbols
108
+ };
109
+ }
110
+
53
111
  // src/model.ts
54
- var SUPPORTED_VERSION = 2;
112
+ var SUPPORTED_VERSION = 3;
55
113
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
56
114
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
57
115
  var KNOWN_FIELDS = {
58
116
  file: ["version", "pages", "images"],
59
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
117
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "logic", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
60
118
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
61
119
  flowOverride: ["when", "volume"],
62
120
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
63
121
  layer: ["id", "name", "visible", "locked"],
64
122
  rgba: ["r", "g", "b", "a"],
65
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
123
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor", "named"],
124
+ colorNames: ["fill", "stroke", "textColor"],
125
+ customTheme: ["name", "background", "ink", "palette", "base"],
126
+ paletteColor: ["name", "color"],
66
127
  element: [
67
128
  "id",
68
129
  "kind",
@@ -100,8 +161,11 @@ var KNOWN_FIELDS = {
100
161
  "capacity",
101
162
  "delay",
102
163
  "queue",
103
- "gauge"
164
+ "gauge",
165
+ "containerID",
166
+ "container"
104
167
  ],
168
+ container: ["fixed", "lanes"],
105
169
  gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
106
170
  strokePoint: ["x", "y"],
107
171
  subpath: ["closed", "coords"],
@@ -110,7 +174,7 @@ var KNOWN_FIELDS = {
110
174
  extensionRef: ["id", "shape", "version", "params"],
111
175
  normalizedPoint: ["x", "y"],
112
176
  normalizedRect: ["x", "y", "width", "height"],
113
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
177
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "avoid", "cost"],
114
178
  waypoint: ["x", "y"],
115
179
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
116
180
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -119,7 +183,7 @@ var KNOWN_FIELDS = {
119
183
  repeat: ["start", "end", "count"],
120
184
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
121
185
  marker: ["t", "name", "loop"],
122
- click: ["scene", "url", "set", "toggle", "reset"],
186
+ click: ["scene", "url", "set", "toggle", "reset", "run"],
123
187
  hover: ["stroke", "fill", "opacity"],
124
188
  override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
125
189
  };
@@ -192,9 +256,36 @@ var Reader = class {
192
256
  gridSize: num(s.gridSize) ?? null,
193
257
  opacity: num(s.opacity) ?? null,
194
258
  dashOffset: num(s.dashOffset) ?? null,
195
- textColor: rgba(s.textColor) ?? null
259
+ textColor: rgba(s.textColor) ?? null,
260
+ named: this.colorNames(s.named)
196
261
  };
197
262
  }
263
+ container(v) {
264
+ if (!isObj(v)) return null;
265
+ this.fields(v, "container");
266
+ return { fixed: v.fixed === true ? true : null, lanes: v.lanes === "rows" || v.lanes === "columns" ? v.lanes : null };
267
+ }
268
+ colorNames(v) {
269
+ if (!isObj(v)) return null;
270
+ this.fields(v, "colorNames");
271
+ const n = { fill: str(v.fill) ?? null, stroke: str(v.stroke) ?? null, textColor: str(v.textColor) ?? null };
272
+ return n.fill === null && n.stroke === null && n.textColor === null ? null : n;
273
+ }
274
+ customTheme(v) {
275
+ if (!isObj(v)) return null;
276
+ this.fields(v, "customTheme");
277
+ const background = rgba(v.background), ink = rgba(v.ink);
278
+ if (!background || !ink) {
279
+ this.warn("A custom theme needs a background and an ink; drawn without it");
280
+ return null;
281
+ }
282
+ const palette2 = (Array.isArray(v.palette) ? v.palette : []).filter(isObj).flatMap((p) => {
283
+ this.fields(p, "paletteColor");
284
+ const name = str(p.name), color = rgba(p.color);
285
+ return name && color ? [{ name, color }] : [];
286
+ });
287
+ return { name: str(v.name) ?? "Custom", background, ink, palette: palette2, base: str(v.base) ?? null };
288
+ }
198
289
  endpoint(v) {
199
290
  const e = isObj(v) ? v : {};
200
291
  const port = str(e.port);
@@ -226,6 +317,7 @@ var Reader = class {
226
317
  return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
227
318
  }) : null,
228
319
  hops: typeof v.hops === "boolean" ? v.hops : null,
320
+ avoid: typeof v.avoid === "boolean" ? v.avoid : null,
229
321
  cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
230
322
  };
231
323
  }
@@ -294,7 +386,9 @@ var Reader = class {
294
386
  capacity: num(v.capacity) ?? null,
295
387
  delay: num(v.delay) ?? null,
296
388
  queue: v.queue === true ? true : null,
297
- gauge: this.gauge(v.gauge)
389
+ gauge: this.gauge(v.gauge),
390
+ containerID: uuid(v.containerID) ?? null,
391
+ container: this.container(v.container)
298
392
  };
299
393
  }
300
394
  gauge(v) {
@@ -336,7 +430,7 @@ var Reader = class {
336
430
  variables(v) {
337
431
  if (!isObj(v)) return null;
338
432
  const out = {};
339
- for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
433
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean" || typeof b === "number" && Number.isFinite(b)) out[k] = b;
340
434
  return Object.keys(out).length > 0 ? out : null;
341
435
  }
342
436
  click(v) {
@@ -348,12 +442,22 @@ var Reader = class {
348
442
  let set = null;
349
443
  if (isObj(v.set)) {
350
444
  set = {};
351
- for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
445
+ for (const [k, b] of Object.entries(v.set)) {
446
+ if (isName(k) && (typeof b === "boolean" || typeof b === "string" || typeof b === "number" && Number.isFinite(b))) set[k] = b;
447
+ }
352
448
  if (Object.keys(set).length === 0) set = null;
353
449
  }
354
450
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
355
451
  const toggle = raw ? raw.filter(isName) : null;
356
- return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
452
+ const run = str(v.run);
453
+ return {
454
+ scene: str(v.scene) ?? null,
455
+ url: safe,
456
+ set,
457
+ toggle: toggle && toggle.length > 0 ? toggle : null,
458
+ reset: v.reset === true ? true : null,
459
+ run: run !== void 0 && isName(run) ? run : null
460
+ };
357
461
  }
358
462
  playback(v) {
359
463
  const s = str(v);
@@ -515,6 +619,8 @@ var Reader = class {
515
619
  layers,
516
620
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
517
621
  theme: str(v.theme) ?? null,
622
+ customTheme: this.customTheme(v.customTheme),
623
+ logic: typeof v.logic === "string" && v.logic.length > 0 ? v.logic : null,
518
624
  timeline: this.timeline(v.timeline),
519
625
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
520
626
  variables: this.variables(v.variables),
@@ -578,7 +684,7 @@ function parseDottie(source) {
578
684
  if (isObj(v) && typeof v.data === "string") images[k] = reader.picture(v.data, str(v.format));
579
685
  }
580
686
  }
581
- const pages = json.pages.map((p, i) => reader.page(p, images, i + 1));
687
+ const pages = json.pages.map((p, i) => resolvingPalette(reader.page(p, images, i + 1)));
582
688
  if (new Set(pages.map((p) => p.id)).size !== pages.length) throw new DottieFormatError("Two pages share an id");
583
689
  return { version, pages, warnings: reader.warnings };
584
690
  }
@@ -656,6 +762,7 @@ function playerAttributes(options) {
656
762
  if (options.noAutoplay) out["no-autoplay"] = "";
657
763
  if (options.noLoop) out["no-loop"] = "";
658
764
  if (options.label !== void 0) out.label = options.label;
765
+ if (options.logicEngine) out["allow-logic"] = "";
659
766
  return out;
660
767
  }
661
768
  function compile(source, runtime, options = {}) {
@@ -688,10 +795,11 @@ function compile(source, runtime, options = {}) {
688
795
  }
689
796
  const fill = {
690
797
  DOTTIE_DATA: () => scriptJSON(data),
691
- DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(options)),
798
+ DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(file.pages[i].logic ? options : { ...options, logicEngine: void 0 })),
692
799
  DOTTIE_FONTS: () => scriptJSON(fonts)
693
800
  };
694
- return runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
801
+ const engine = options.logicEngine && file.pages[i].logic ? options.logicEngine.trimEnd() + "\n" : "";
802
+ return engine + runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
695
803
  }
696
804
  export {
697
805
  LOADER,
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /*! dottie.js 0.9.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
+ /*! dottie.js 0.11.0 | 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);
@@ -58,19 +58,80 @@ function isName(name) {
58
58
  return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
59
59
  }
60
60
 
61
+ // src/theme.ts
62
+ var rgb = (r, g, b) => ({ r, g, b, a: 1 });
63
+ function hex(h) {
64
+ const n = parseInt(h.slice(1), 16);
65
+ return { r: (n >> 16 & 255) / 255, g: (n >> 8 & 255) / 255, b: (n & 255) / 255, a: 1 };
66
+ }
67
+ var PALETTE_NAMES = ["accent", "muted", "danger", "warning", "success", "info"];
68
+ var palette = (colors) => PALETTE_NAMES.map((name, i) => ({ name, color: hex(colors[i]) }));
69
+ var LIGHT_PALETTE = palette(["#2563eb", "#6b7280", "#dc2626", "#d97706", "#16a34a", "#0891b2"]);
70
+ var DARK_PALETTE = palette(["#93c5fd", "#cbd5e1", "#fca5a5", "#fcd34d", "#86efac", "#67e8f9"]);
71
+ var themes = [
72
+ { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
73
+ { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
74
+ {
75
+ id: "sepia",
76
+ name: "Sepia",
77
+ background: rgb(0.953, 0.914, 0.824),
78
+ ink: rgb(0.23, 0.18, 0.13),
79
+ palette: palette(["#9a3412", "#78716c", "#b91c1c", "#b45309", "#4d7c0f", "#0e7490"])
80
+ },
81
+ { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984), palette: DARK_PALETTE },
82
+ { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918), palette: DARK_PALETTE },
83
+ { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94), palette: DARK_PALETTE }
84
+ ];
85
+ function themeNamed(id) {
86
+ return themes.find((t) => t.id === id) ?? null;
87
+ }
88
+ function pageTheme(page) {
89
+ const c = page.customTheme;
90
+ if (c) return { id: "custom", name: c.name, background: c.background, ink: c.ink, palette: c.palette };
91
+ return themeNamed(page.theme);
92
+ }
93
+ function paletteColor(page, name) {
94
+ const n = name.trim().toLowerCase();
95
+ return (pageTheme(page)?.palette ?? LIGHT_PALETTE).find((p) => p.name.toLowerCase() === n)?.color ?? null;
96
+ }
97
+ function resolvingPalette(page) {
98
+ const named = (e) => !!e.style.named;
99
+ if (!page.elements.some(named) && !(page.symbols ?? []).some((s) => s.elements.some(named))) return page;
100
+ const resolve = (e) => {
101
+ const n = e.style.named;
102
+ if (!n) return e;
103
+ const style = { ...e.style };
104
+ const f = n.fill !== null ? paletteColor(page, n.fill) : null;
105
+ const s = n.stroke !== null ? paletteColor(page, n.stroke) : null;
106
+ const t = n.textColor !== null ? paletteColor(page, n.textColor) : null;
107
+ if (f) style.fill = { ...f };
108
+ if (s) style.stroke = { ...s };
109
+ if (t) style.textColor = { ...t };
110
+ return { ...e, style };
111
+ };
112
+ return {
113
+ ...page,
114
+ elements: page.elements.map(resolve),
115
+ symbols: page.symbols ? page.symbols.map((s) => ({ ...s, elements: s.elements.map(resolve) })) : page.symbols
116
+ };
117
+ }
118
+
61
119
  // src/model.ts
62
- var SUPPORTED_VERSION = 2;
120
+ var SUPPORTED_VERSION = 3;
63
121
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
64
122
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
65
123
  var KNOWN_FIELDS = {
66
124
  file: ["version", "pages", "images"],
67
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
125
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "logic", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
68
126
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
69
127
  flowOverride: ["when", "volume"],
70
128
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
71
129
  layer: ["id", "name", "visible", "locked"],
72
130
  rgba: ["r", "g", "b", "a"],
73
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
131
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor", "named"],
132
+ colorNames: ["fill", "stroke", "textColor"],
133
+ customTheme: ["name", "background", "ink", "palette", "base"],
134
+ paletteColor: ["name", "color"],
74
135
  element: [
75
136
  "id",
76
137
  "kind",
@@ -108,8 +169,11 @@ var KNOWN_FIELDS = {
108
169
  "capacity",
109
170
  "delay",
110
171
  "queue",
111
- "gauge"
172
+ "gauge",
173
+ "containerID",
174
+ "container"
112
175
  ],
176
+ container: ["fixed", "lanes"],
113
177
  gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
114
178
  strokePoint: ["x", "y"],
115
179
  subpath: ["closed", "coords"],
@@ -118,7 +182,7 @@ var KNOWN_FIELDS = {
118
182
  extensionRef: ["id", "shape", "version", "params"],
119
183
  normalizedPoint: ["x", "y"],
120
184
  normalizedRect: ["x", "y", "width", "height"],
121
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
185
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "avoid", "cost"],
122
186
  waypoint: ["x", "y"],
123
187
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
124
188
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -127,7 +191,7 @@ var KNOWN_FIELDS = {
127
191
  repeat: ["start", "end", "count"],
128
192
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
129
193
  marker: ["t", "name", "loop"],
130
- click: ["scene", "url", "set", "toggle", "reset"],
194
+ click: ["scene", "url", "set", "toggle", "reset", "run"],
131
195
  hover: ["stroke", "fill", "opacity"],
132
196
  override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
133
197
  };
@@ -200,9 +264,36 @@ var Reader = class {
200
264
  gridSize: num(s.gridSize) ?? null,
201
265
  opacity: num(s.opacity) ?? null,
202
266
  dashOffset: num(s.dashOffset) ?? null,
203
- textColor: rgba(s.textColor) ?? null
267
+ textColor: rgba(s.textColor) ?? null,
268
+ named: this.colorNames(s.named)
204
269
  };
205
270
  }
271
+ container(v) {
272
+ if (!isObj(v)) return null;
273
+ this.fields(v, "container");
274
+ return { fixed: v.fixed === true ? true : null, lanes: v.lanes === "rows" || v.lanes === "columns" ? v.lanes : null };
275
+ }
276
+ colorNames(v) {
277
+ if (!isObj(v)) return null;
278
+ this.fields(v, "colorNames");
279
+ const n = { fill: str(v.fill) ?? null, stroke: str(v.stroke) ?? null, textColor: str(v.textColor) ?? null };
280
+ return n.fill === null && n.stroke === null && n.textColor === null ? null : n;
281
+ }
282
+ customTheme(v) {
283
+ if (!isObj(v)) return null;
284
+ this.fields(v, "customTheme");
285
+ const background = rgba(v.background), ink = rgba(v.ink);
286
+ if (!background || !ink) {
287
+ this.warn("A custom theme needs a background and an ink; drawn without it");
288
+ return null;
289
+ }
290
+ const palette2 = (Array.isArray(v.palette) ? v.palette : []).filter(isObj).flatMap((p) => {
291
+ this.fields(p, "paletteColor");
292
+ const name = str(p.name), color = rgba(p.color);
293
+ return name && color ? [{ name, color }] : [];
294
+ });
295
+ return { name: str(v.name) ?? "Custom", background, ink, palette: palette2, base: str(v.base) ?? null };
296
+ }
206
297
  endpoint(v) {
207
298
  const e = isObj(v) ? v : {};
208
299
  const port = str(e.port);
@@ -234,6 +325,7 @@ var Reader = class {
234
325
  return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
235
326
  }) : null,
236
327
  hops: typeof v.hops === "boolean" ? v.hops : null,
328
+ avoid: typeof v.avoid === "boolean" ? v.avoid : null,
237
329
  cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
238
330
  };
239
331
  }
@@ -302,7 +394,9 @@ var Reader = class {
302
394
  capacity: num(v.capacity) ?? null,
303
395
  delay: num(v.delay) ?? null,
304
396
  queue: v.queue === true ? true : null,
305
- gauge: this.gauge(v.gauge)
397
+ gauge: this.gauge(v.gauge),
398
+ containerID: uuid(v.containerID) ?? null,
399
+ container: this.container(v.container)
306
400
  };
307
401
  }
308
402
  gauge(v) {
@@ -344,7 +438,7 @@ var Reader = class {
344
438
  variables(v) {
345
439
  if (!isObj(v)) return null;
346
440
  const out = {};
347
- for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
441
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean" || typeof b === "number" && Number.isFinite(b)) out[k] = b;
348
442
  return Object.keys(out).length > 0 ? out : null;
349
443
  }
350
444
  click(v) {
@@ -356,12 +450,22 @@ var Reader = class {
356
450
  let set = null;
357
451
  if (isObj(v.set)) {
358
452
  set = {};
359
- for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
453
+ for (const [k, b] of Object.entries(v.set)) {
454
+ if (isName(k) && (typeof b === "boolean" || typeof b === "string" || typeof b === "number" && Number.isFinite(b))) set[k] = b;
455
+ }
360
456
  if (Object.keys(set).length === 0) set = null;
361
457
  }
362
458
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
363
459
  const toggle = raw ? raw.filter(isName) : null;
364
- return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
460
+ const run = str(v.run);
461
+ return {
462
+ scene: str(v.scene) ?? null,
463
+ url: safe,
464
+ set,
465
+ toggle: toggle && toggle.length > 0 ? toggle : null,
466
+ reset: v.reset === true ? true : null,
467
+ run: run !== void 0 && isName(run) ? run : null
468
+ };
365
469
  }
366
470
  playback(v) {
367
471
  const s = str(v);
@@ -523,6 +627,8 @@ var Reader = class {
523
627
  layers,
524
628
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
525
629
  theme: str(v.theme) ?? null,
630
+ customTheme: this.customTheme(v.customTheme),
631
+ logic: typeof v.logic === "string" && v.logic.length > 0 ? v.logic : null,
526
632
  timeline: this.timeline(v.timeline),
527
633
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
528
634
  variables: this.variables(v.variables),
@@ -586,7 +692,7 @@ function parseDottie(source2) {
586
692
  if (isObj(v) && typeof v.data === "string") images[k] = reader.picture(v.data, str(v.format));
587
693
  }
588
694
  }
589
- const pages = json.pages.map((p, i) => reader.page(p, images, i + 1));
695
+ const pages = json.pages.map((p, i) => resolvingPalette(reader.page(p, images, i + 1)));
590
696
  if (new Set(pages.map((p) => p.id)).size !== pages.length) throw new DottieFormatError("Two pages share an id");
591
697
  return { version, pages, warnings: reader.warnings };
592
698
  }
@@ -664,6 +770,7 @@ function playerAttributes(options2) {
664
770
  if (options2.noAutoplay) out["no-autoplay"] = "";
665
771
  if (options2.noLoop) out["no-loop"] = "";
666
772
  if (options2.label !== void 0) out.label = options2.label;
773
+ if (options2.logicEngine) out["allow-logic"] = "";
667
774
  return out;
668
775
  }
669
776
  function compile(source2, runtime, options2 = {}) {
@@ -696,14 +803,15 @@ function compile(source2, runtime, options2 = {}) {
696
803
  }
697
804
  const fill = {
698
805
  DOTTIE_DATA: () => scriptJSON(data),
699
- DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(options2)),
806
+ DOTTIE_OPTIONS: () => scriptJSON(playerAttributes(file.pages[i].logic ? options2 : { ...options2, logicEngine: void 0 })),
700
807
  DOTTIE_FONTS: () => scriptJSON(fonts)
701
808
  };
702
- return runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
809
+ const engine = options2.logicEngine && file.pages[i].logic ? options2.logicEngine.trimEnd() + "\n" : "";
810
+ return engine + runtime.trimEnd() + "\n" + LOADER.replace(/\bDOTTIE_(?:DATA|OPTIONS|FONTS)\b/g, (m) => fill[m]());
703
811
  }
704
812
 
705
813
  // src/index.ts
706
- var VERSION = "0.9.0";
814
+ var VERSION = "0.11.0";
707
815
 
708
816
  // src/cli.ts
709
817
  var usage = `dottie-compile ${VERSION}: a Dottie drawing as one script that plays it on any page.
@@ -720,6 +828,7 @@ OPTIONS
720
828
  --at <seconds> Show that moment as a still
721
829
  --clean Draw precisely instead of sketchy
722
830
  --no-controls, --no-autoplay, --no-loop
831
+ --allow-logic let the page's click logic run (ADR 0007); it carries the engine (about 850 KB)
723
832
  --label <text> The player's accessible name
724
833
  `;
725
834
  function fail(message, code = 2) {
@@ -729,6 +838,7 @@ function fail(message, code = 2) {
729
838
  }
730
839
  var args = process.argv.slice(2);
731
840
  var options = {};
841
+ var allowLogic = false;
732
842
  var input;
733
843
  var output;
734
844
  for (let i = 0; i < args.length; i++) {
@@ -770,6 +880,9 @@ for (let i = 0; i < args.length; i++) {
770
880
  case "--no-loop":
771
881
  options.noLoop = true;
772
882
  break;
883
+ case "--allow-logic":
884
+ allowLogic = true;
885
+ break;
773
886
  case "--label":
774
887
  options.label = value();
775
888
  break;
@@ -800,7 +913,8 @@ try {
800
913
  return void 0;
801
914
  }
802
915
  };
803
- const js = compile(source, runtime, { ...options, fontData });
916
+ const logicEngine = allowLogic ? readFileSync(new URL("./dottie-logic.min.js", import.meta.url), "utf8") : void 0;
917
+ const js = compile(source, runtime, { ...options, fontData, logicEngine });
804
918
  if (output === "-") process.stdout.write(js);
805
919
  else {
806
920
  const target = output ?? input.replace(/\.dottie$/i, "") + ".js";