@phin-tech/dottie 0.6.3 → 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,15 +1,69 @@
1
- /*! dottie.js 0.6.3 | 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"],
@@ -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",
@@ -62,8 +117,8 @@ var KNOWN_FIELDS = {
62
117
  follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
63
118
  repeat: ["start", "end", "count"],
64
119
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
65
- marker: ["t", "name"],
66
- click: ["scene", "url"],
120
+ marker: ["t", "name", "loop"],
121
+ click: ["scene", "url", "set", "toggle"],
67
122
  hover: ["stroke", "fill", "opacity"]
68
123
  };
69
124
  var MAX_DURATION = 3600;
@@ -222,6 +277,7 @@ var Reader = class {
222
277
  anchor: str(v.anchor) ?? null,
223
278
  click: this.click(v.click),
224
279
  hover: this.hover(v.hover),
280
+ when: str(v.when) ?? null,
225
281
  symbol: uuid(v.symbol) ?? null,
226
282
  playback: this.playback(v.playback),
227
283
  offset: num(v.offset) ?? null,
@@ -257,13 +313,27 @@ var Reader = class {
257
313
  }
258
314
  return { id, shape, version, params };
259
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
+ }
260
322
  click(v) {
261
323
  if (!isObj(v)) return null;
262
324
  this.fields(v, "click");
263
325
  const url = str(v.url);
264
326
  const safe = url === void 0 ? null : allowedURL(url);
265
327
  if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
266
- 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 };
267
337
  }
268
338
  playback(v) {
269
339
  const s = str(v);
@@ -364,7 +434,7 @@ var Reader = class {
364
434
  if (camera) stableSortBy(camera, (c) => c.t);
365
435
  const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
366
436
  const t = num(m.t);
367
- 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 }] : [];
368
438
  }) : null;
369
439
  if (markers) stableSortBy(markers, (m) => m.t);
370
440
  const poster = num(v.poster);
@@ -397,7 +467,8 @@ var Reader = class {
397
467
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
398
468
  theme: str(v.theme) ?? null,
399
469
  timeline: this.timeline(v.timeline),
400
- 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)
401
472
  };
402
473
  }
403
474
  };
@@ -2067,17 +2138,20 @@ function trackMap(tl) {
2067
2138
  for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
2068
2139
  return m;
2069
2140
  }
2070
- function resolved(page, t) {
2071
- return expandingSymbols(posed(page, t), clampTime(t));
2141
+ function resolved(page, t, state = "initial") {
2142
+ return expandingSymbols(posed(page, t, state), clampTime(t));
2072
2143
  }
2073
- function posed(page, t) {
2074
- 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);
2075
2148
  if (!timeline) return page;
2076
2149
  t = clampTime(t);
2077
2150
  const tracks = trackMap(timeline);
2078
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;
2079
2152
  const hidden = /* @__PURE__ */ new Set();
2080
2153
  for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
2154
+ for (const id of off) hidden.add(id);
2081
2155
  let moving = null;
2082
2156
  if (timeline.settle === true) {
2083
2157
  moving = /* @__PURE__ */ new Set();
@@ -2116,7 +2190,7 @@ function posed(page, t) {
2116
2190
  elements = elements.map((e) => {
2117
2191
  const c = e.connector;
2118
2192
  if (!c || ![c.start.elementID, c.end.elementID].some((id) => id !== null && hidden.has(id))) return e;
2119
- 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))) {
2120
2194
  gone.add(e.id);
2121
2195
  return e;
2122
2196
  }
@@ -2132,7 +2206,7 @@ function posterTime(page) {
2132
2206
  return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
2133
2207
  }
2134
2208
  function still(page) {
2135
- return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(page, 0);
2209
+ return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(posed(page, 0), 0);
2136
2210
  }
2137
2211
  function cameraRect(tl, t) {
2138
2212
  const keys = tl.camera ?? [];
@@ -2241,7 +2315,7 @@ function filled(s, args) {
2241
2315
  }) };
2242
2316
  }
2243
2317
  function symbolPage(s) {
2244
- 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 };
2245
2319
  }
2246
2320
  function hasInstances(page) {
2247
2321
  return page.elements.some((e) => e.kind === "symbol");
@@ -2370,16 +2444,21 @@ function clickTargets(page, file) {
2370
2444
  let pageOf = null;
2371
2445
  for (const e of page.elements) {
2372
2446
  const c = e.click;
2447
+ const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {} } : null;
2373
2448
  if (c?.scene) {
2374
2449
  const i = sceneNamed(c.scene, markers);
2375
2450
  if (i >= 0) {
2376
- out.set(e.id, { scene: i });
2451
+ out.set(e.id, { scene: i, ...state });
2377
2452
  continue;
2378
2453
  }
2379
2454
  }
2380
2455
  const url = (c?.url ? allowedURL(c.url) : null) ?? webLink(e.url);
2381
2456
  if (url) {
2382
- 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 });
2383
2462
  continue;
2384
2463
  }
2385
2464
  if (e.link && file && file.pages.length > 1) {
@@ -3293,6 +3372,10 @@ var Playback = class {
3293
3372
  __publicField(this, "plan");
3294
3373
  __publicField(this, "scenes");
3295
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");
3296
3379
  __publicField(this, "background");
3297
3380
  __publicField(this, "ink");
3298
3381
  /** What clicking each clickable element does, by element id. */
@@ -3352,6 +3435,8 @@ var Playback = class {
3352
3435
  };
3353
3436
  this.scenes = (page.timeline?.markers ?? []).map((m) => near(m.t));
3354
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 ?? {} };
3355
3440
  const theme = themeNamed(page.theme);
3356
3441
  this.background = theme?.background ?? WHITE;
3357
3442
  this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
@@ -3402,7 +3487,7 @@ var Playback = class {
3402
3487
  const times = /* @__PURE__ */ new Map();
3403
3488
  for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
3404
3489
  const tc = clampTime(t);
3405
- const pose = posed(this.page, tc);
3490
+ const pose = posed(this.page, tc, this.state);
3406
3491
  const frame2 = expandingSymbols(pose, tc, true, times);
3407
3492
  const { parts } = svgParts(frame2, this.background, r);
3408
3493
  const owned = (m, parts2) => {
@@ -3524,6 +3609,20 @@ ${h}</g>
3524
3609
  });
3525
3610
  return s;
3526
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
+ }
3527
3626
  };
3528
3627
  var ElementBase = typeof HTMLElement === "undefined" ? class {
3529
3628
  } : HTMLElement;
@@ -3677,7 +3776,7 @@ var DottiePlayer = class extends ElementBase {
3677
3776
  this.setPlaying(false);
3678
3777
  return;
3679
3778
  }
3680
- 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)]));
3681
3780
  }
3682
3781
  // MARK: Playback
3683
3782
  get paused() {
@@ -3703,11 +3802,13 @@ var DottiePlayer = class extends ElementBase {
3703
3802
  pause() {
3704
3803
  this.setPlaying(false);
3705
3804
  }
3706
- /** 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. */
3707
3806
  nextScene() {
3708
3807
  const p = this.playback;
3709
3808
  if (!p) return;
3710
- 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);
3711
3812
  this.setPlaying(true);
3712
3813
  }
3713
3814
  /** Back to the start of this scene, or the one before if already there. */
@@ -3736,6 +3837,8 @@ var DottiePlayer = class extends ElementBase {
3736
3837
  this.scheduleReplay();
3737
3838
  }
3738
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;
3739
3842
  if ("url" in click) {
3740
3843
  if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
3741
3844
  } else if ("page" in click) {
@@ -3743,13 +3846,27 @@ var DottiePlayer = class extends ElementBase {
3743
3846
  } else if (click.scene >= 0 && click.scene < p.scenes.length) {
3744
3847
  if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
3745
3848
  this.setPlaying(false);
3746
- 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);
3747
3850
  } else {
3748
3851
  p.show(p.scenes[click.scene]);
3749
3852
  this.setPlaying(true);
3750
3853
  }
3751
3854
  }
3752
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
+ }
3753
3870
  /** Redraws, while the page itself stands still, as long as a clicked copy is playing through. */
3754
3871
  scheduleReplay() {
3755
3872
  if (this.replayRaf) return;
@@ -3775,7 +3892,8 @@ var DottiePlayer = class extends ElementBase {
3775
3892
  schedule() {
3776
3893
  if (this.playing && this.visible && !this.raf && this.isConnected) this.raf = requestAnimationFrame((now) => this.tick(now));
3777
3894
  }
3778
- /** 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). */
3779
3897
  tick(now) {
3780
3898
  this.raf = 0;
3781
3899
  const p = this.playback;
@@ -3786,14 +3904,14 @@ var DottiePlayer = class extends ElementBase {
3786
3904
  let advanced = false;
3787
3905
  while (this.playing && this.acc >= step) {
3788
3906
  this.acc -= step;
3789
- const next = p.frame + 1;
3790
- 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) {
3791
3909
  this.setPlaying(false);
3792
3910
  break;
3793
3911
  }
3794
- p.frame = next % p.frameCount;
3912
+ p.frame = frame2;
3795
3913
  advanced = true;
3796
- if (p.scenes.includes(p.frame)) {
3914
+ if (stop) {
3797
3915
  this.setPlaying(false);
3798
3916
  break;
3799
3917
  }
@@ -3943,7 +4061,7 @@ function render(source, options = {}) {
3943
4061
  function isFile(v) {
3944
4062
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
3945
4063
  }
3946
- var VERSION = "0.6.3";
4064
+ var VERSION = "0.7.0";
3947
4065
  export {
3948
4066
  BOIL_DRAWINGS,
3949
4067
  BUNDLED_FONTS,
@@ -1,12 +1,18 @@
1
1
  import { type DottieFile, type Element, type Marker, type Page } from "./model.ts";
2
2
  /** Jump to scene `scene` (an index into the page's markers) and play on to the next; open `url` in a new tab; or,
3
- * in <dottie-player> only, show page `page` (an index into the file's pages), where the element's link points. */
4
- export type Click = {
3
+ * in <dottie-player> only, show page `page` (an index into the file's pages), where the element's link points. Any
4
+ * of them may first `set` and `toggle` page variables (ClickAction.set, .toggle), and a click may do only that. */
5
+ export type Click = ({
5
6
  scene: number;
6
7
  } | {
7
8
  url: string;
8
9
  } | {
9
10
  page: number;
11
+ } | {
12
+ state: true;
13
+ }) & {
14
+ set?: Record<string, boolean>;
15
+ toggle?: string[];
10
16
  };
11
17
  /** The marker called `name`: an exact match, else one that differs only in case. */
12
18
  export declare function sceneNamed(name: string, markers: Marker[]): number;
@@ -20,4 +20,4 @@ export interface RenderOptions {
20
20
  /** One page of a drawing as an SVG document, as Dottie's own SVG export draws it. */
21
21
  export declare function render(source: string | DottieFile | unknown, options?: RenderOptions): string;
22
22
  /** The library's version. */
23
- export declare const VERSION = "0.6.3";
23
+ export declare const VERSION = "0.7.0";
@@ -68,10 +68,13 @@ export interface EmbeddedImage {
68
68
  data: string;
69
69
  format: string;
70
70
  }
71
- /** What clicking an element does in a played drawing: a scene to jump to and play on from, or a link. */
71
+ /** What clicking an element does in a played drawing: a scene to jump to and play on from, or a link; and page
72
+ * variables to set and flip (ClickAction.set, .toggle), which change what shows without touching the playhead. */
72
73
  export interface ClickAction {
73
74
  scene: string | null;
74
75
  url: string | null;
76
+ set: Record<string, boolean> | null;
77
+ toggle: string[] | null;
75
78
  }
76
79
  /** How a clickable element looks under the pointer or with keyboard focus; null keeps its own. */
77
80
  export interface HoverLook {
@@ -105,6 +108,8 @@ export interface Element {
105
108
  anchor: string | null;
106
109
  click: ClickAction | null;
107
110
  hover: HoverLook | null;
111
+ /** A condition on the page's variables: drawn only while it holds (Element.when; see state.ts). */
112
+ when: string | null;
108
113
  /** For a `symbol` element: the page's symbol it shows, how it plays it, and from how far in. */
109
114
  symbol: string | null;
110
115
  playback: SymbolPlayback | null;
@@ -179,9 +184,11 @@ export interface CameraKey {
179
184
  height: number;
180
185
  ease: Ease | null;
181
186
  }
187
+ /** A scene; `loop` plays it round until something leaves it, instead of stopping on it (Marker.loop). */
182
188
  export interface Marker {
183
189
  t: number;
184
190
  name: string;
191
+ loop: boolean | null;
185
192
  }
186
193
  export interface Timeline {
187
194
  fps: number;
@@ -201,6 +208,8 @@ export interface Page {
201
208
  theme: string | null;
202
209
  timeline: Timeline | null;
203
210
  symbols: Symbol[] | null;
211
+ /** The page's variables and their starting values (DottieDocument.variables). */
212
+ variables: Record<string, boolean> | null;
204
213
  }
205
214
  /** A drawing defined once on a page and placed by `symbol` elements, with its own timeline (Symbol). */
206
215
  export interface Symbol {
@@ -10,6 +10,10 @@ export declare class Playback {
10
10
  readonly plan: Plan;
11
11
  readonly scenes: number[];
12
12
  readonly sceneNames: string[];
13
+ /** Whether each scene loops (Marker.loop). */
14
+ readonly sceneLoops: boolean[];
15
+ /** The page's variables now: their starting values until clicks change them. */
16
+ state: Record<string, boolean>;
13
17
  readonly background: RGBA;
14
18
  readonly ink: RGBA;
15
19
  /** What clicking each clickable element does, by element id. */
@@ -56,6 +60,14 @@ export declare class Playback {
56
60
  private buttonPart;
57
61
  /** The scene frame `i` is in, or -1 before the first. */
58
62
  sceneAt(i: number): number;
63
+ /** The frame after scene `s`'s last: the next scene's, or the frame count after the last scene. */
64
+ sceneEnd(s: number): number;
65
+ /** Where playing goes from frame `i`, and whether it stops there: on into a looping scene, round to the start of
66
+ * the looping scene `i` ends, or a stop at a scene or the end (`loops`: whether the timeline loops at its end). */
67
+ step(i: number, loops: boolean): {
68
+ frame: number;
69
+ stop: boolean;
70
+ };
59
71
  }
60
72
  /** HTMLElement where there is a DOM; a stand-in elsewhere, so the library still loads in Node (to render SVG). */
61
73
  declare const ElementBase: typeof HTMLElement;
@@ -95,18 +107,23 @@ export declare class DottiePlayer extends ElementBase {
95
107
  get duration(): number;
96
108
  play(): void;
97
109
  pause(): void;
98
- /** Plays on to the next scene (from the start after the last). */
110
+ /** Plays on to the next scene (from the start after the last). Out of a looping scene it jumps to the next. */
99
111
  nextScene(): void;
100
112
  /** Back to the start of this scene, or the one before if already there. */
101
113
  prevScene(): void;
102
114
  /** Does what clicking element `id` does: jumps to its scene and plays on to the next (with reduced motion, or in a
103
115
  * still, straight to where that stops), opens its link in a new tab, or shows its page. */
104
116
  activate(id: string): void;
117
+ /** The page's variables now. */
118
+ get variables(): Record<string, boolean>;
119
+ /** Gives the page's variables these values (the others keep theirs), redraws, and fires `state`. */
120
+ setState(values: Record<string, boolean>): void;
105
121
  /** Redraws, while the page itself stands still, as long as a clicked copy is playing through. */
106
122
  private scheduleReplay;
107
123
  private setPlaying;
108
124
  private schedule;
109
- /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end. */
125
+ /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end, except that a
126
+ * looping scene plays round (Playback.step). */
110
127
  private tick;
111
128
  private key;
112
129
  }
@@ -0,0 +1,20 @@
1
+ import type { ClickAction, Page } from "./model.ts";
2
+ /** Whether `name` can name a variable: a letter or underscore, then letters, digits and underscores. */
3
+ export declare function isName(name: string): boolean;
4
+ /** The variable names `condition` reads, or null when it isn't one: names, each optionally after `!`, joined by
5
+ * `&&` and `||` (`&&` binds tighter). */
6
+ export declare function conditionNames(condition: string): string[] | null;
7
+ /** Whether `condition` holds with these values. A missing variable is false; a condition that can't be read never
8
+ * holds. */
9
+ export declare function holds(condition: string, state: Record<string, boolean>): boolean;
10
+ /** Which elements a page shows as far as its variables go: their starting values ("initial"), these values (a
11
+ * missing one takes its starting value), or every element ("all"). */
12
+ export type StateChoice = "initial" | "all" | Record<string, boolean>;
13
+ /** The values `choice` gives each variable; null for "all". */
14
+ export declare function stateValues(page: Page, choice: StateChoice): Record<string, boolean> | null;
15
+ /** The ids of the elements whose `when` doesn't hold under `choice`. */
16
+ export declare function stateHidden(page: Page, choice: StateChoice): Set<string>;
17
+ /** Whether a click sets or flips a variable. */
18
+ export declare function changesState(c: ClickAction | null): boolean;
19
+ /** `state` after the click: its `set` values, then each `toggle` flipped (a missing variable starts false). */
20
+ export declare function applyClick(c: ClickAction, state: Record<string, boolean>): Record<string, boolean>;
@@ -1,5 +1,6 @@
1
1
  import { type Ease, type Element, type Follow, type Page, type Timeline, type Track } from "./model.ts";
2
2
  import { Polyline, type Point, type Rect } from "./path.ts";
3
+ import { type StateChoice } from "./state.ts";
3
4
  export declare function applyEase(ease: Ease, u: number): number;
4
5
  /** The time inside the track's own keys that `t` plays (repeats folded back). */
5
6
  export declare function localTime(track: Track, t: number): number;
@@ -20,9 +21,10 @@ export declare function following(track: Track, e: Element, t: number, page: Pag
20
21
  export declare function marchOffset(tl: Timeline, speed: number, period: number, t: number): number;
21
22
  /** The page as a still drawing at `t` seconds, symbol instances expanded (DottieDocument.resolved(at:)). A page with
22
23
  * no timeline and no instances is returned as is. */
23
- export declare function resolved(page: Page, t: number): Page;
24
- /** The page posed at `t` seconds, instances kept (DottieDocument.posed(at:)). */
25
- export declare function posed(page: Page, t: number): Page;
24
+ export declare function resolved(page: Page, t: number, state?: StateChoice): Page;
25
+ /** The page posed at `t` seconds, instances kept, and the elements whose `when` doesn't hold under `state` left out
26
+ * with their connectors (DottieDocument.posed(at:state:)). */
27
+ export declare function posed(page: Page, t: number, state?: StateChoice): Page;
26
28
  /** The moment a still picture of the page shows: its poster, else the end. */
27
29
  export declare function posterTime(page: Page): number;
28
30
  /** The page as a still picture (its poster frame when animated). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phin-tech/dottie",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Render, play and compile Dottie (.dottie) drawings and animations in the browser",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",