@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/PARITY.md +3 -0
- package/README.md +8 -3
- package/dist/compile.js +30 -7
- package/dist/dottie-compile.mjs +31 -8
- package/dist/dottie-player.js +145 -27
- package/dist/dottie-player.min.js +46 -46
- package/dist/dottie.js +145 -27
- package/dist/types/clicks.d.ts +8 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/model.d.ts +10 -1
- package/dist/types/player.d.ts +19 -2
- package/dist/types/state.d.ts +20 -0
- package/dist/types/timeline.d.ts +5 -3
- package/package.json +1 -1
package/PARITY.md
CHANGED
|
@@ -47,6 +47,9 @@ fixture, so the gap is visible rather than silent.
|
|
|
47
47
|
| Symbol parameters (`{{name}}` in a symbol's text, filled per copy) | Supported | [edge/params](../fixtures/edge/params.dottie) | Defaults, undeclared names left as written, values passed into nested copies, filled before text types out |
|
|
48
48
|
| Copies that play when clicked | Supported | [edge/buttons](../fixtures/edge/buttons.dottie) | `<dottie-player>` only: the HTML export is drawn ahead of time and shows them at rest |
|
|
49
49
|
| Clicks: play a scene, open a link, hover looks, keyboard | Supported | [edge/clicks](../fixtures/edge/clicks.dottie) | As the HTML export does; only http, https and mailto links are followed |
|
|
50
|
+
| Page variables and conditions (`variables`, `when`) | Supported | [edge/state](../fixtures/edge/state.dottie) | Starting values in every still and export; connectors go with a shape the state hides |
|
|
51
|
+
| Clicks that set and toggle variables | Supported | [edge/state](../fixtures/edge/state.dottie) | Change what shows without touching the playhead. The HTML export of a page with state is this player |
|
|
52
|
+
| Looping scenes (`loop` on a marker) | Supported | [edge/state](../fixtures/edge/state.dottie) | As the HTML export plays them: no stop on entering, round from the scene's end to its start |
|
|
50
53
|
| Links out of the drawing (`url`) | Supported | [edge/links](../fixtures/edge/links.dottie) | A web or mail `url` opens when clicked if there is no `click`, and SVG export wraps it in `<a>`; a link to another drawing does nothing on the web |
|
|
51
54
|
| Links between shapes and pages | Partial | [edge/clicks](../fixtures/edge/clicks.dottie) | A link to a shape on another page shows that page; one on the same page does nothing. The HTML export holds one page, so it has neither |
|
|
52
55
|
| Editing | Missing | | |
|
package/README.md
CHANGED
|
@@ -33,11 +33,16 @@ Elements the drawing makes clickable work with the mouse and the keyboard (Tab,
|
|
|
33
33
|
a named scene on to the next one, opens a link in a new tab (only `http`, `https` and `mailto`), or shows the page a
|
|
34
34
|
shape's link points at. Each shows its hover look under the pointer; button symbols show their over and down looks
|
|
35
35
|
and take clicks on their hit area, and a symbol's copy set to play when clicked plays its animation through. Every
|
|
36
|
-
click first fires a cancelable `action` event whose `detail` is `{element, scene | url | page,
|
|
37
|
-
`preventDefault()` to handle it yourself.
|
|
36
|
+
click first fires a cancelable `action` event whose `detail` is `{element, scene | url | page | state, set?, toggle?,
|
|
37
|
+
replay?}`; call `preventDefault()` to handle it yourself.
|
|
38
|
+
|
|
39
|
+
A click can also set or toggle the page's variables, which show and hide the elements with a `when` while the
|
|
40
|
+
animation keeps playing: a diagram you leave running that people poke. A looping scene plays round until a click
|
|
41
|
+
or next leaves it, instead of stopping.
|
|
38
42
|
|
|
39
43
|
The element has `play()`, `pause()`, `nextScene()`, `prevScene()`, `activate(elementId)`, `currentTime`
|
|
40
|
-
(settable), `duration`, `paused`, and fires `load`, `frame`, `action
|
|
44
|
+
(settable), `duration`, `paused`, `variables` and `setState({name: value})`, and fires `load`, `frame`, `action`,
|
|
45
|
+
`state` (with `{variables}`, after a change) and `error` events. Its page color comes from the drawing's theme; set
|
|
41
46
|
`--dottie-radius` to round it, and style its parts with `::part(stage)` and `::part(controls)`.
|
|
42
47
|
|
|
43
48
|
Without modules, `dist/dottie-player.min.js` is a classic script that registers the element and sets
|
package/dist/compile.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! dottie.js 0.
|
|
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, 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);
|
|
@@ -44,13 +44,19 @@ function defaultBase() {
|
|
|
44
44
|
}
|
|
45
45
|
var base = defaultBase();
|
|
46
46
|
|
|
47
|
+
// src/state.ts
|
|
48
|
+
var MAX_NAME = 64;
|
|
49
|
+
function isName(name) {
|
|
50
|
+
return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
// src/model.ts
|
|
48
54
|
var SUPPORTED_VERSION = 2;
|
|
49
55
|
var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
|
|
50
56
|
var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
|
|
51
57
|
var KNOWN_FIELDS = {
|
|
52
58
|
file: ["version", "pages", "images"],
|
|
53
|
-
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
|
|
59
|
+
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables"],
|
|
54
60
|
symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
|
|
55
61
|
layer: ["id", "name", "visible", "locked"],
|
|
56
62
|
rgba: ["r", "g", "b", "a"],
|
|
@@ -78,6 +84,7 @@ var KNOWN_FIELDS = {
|
|
|
78
84
|
"anchor",
|
|
79
85
|
"click",
|
|
80
86
|
"hover",
|
|
87
|
+
"when",
|
|
81
88
|
"symbol",
|
|
82
89
|
"playback",
|
|
83
90
|
"offset",
|
|
@@ -103,8 +110,8 @@ var KNOWN_FIELDS = {
|
|
|
103
110
|
follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
|
|
104
111
|
repeat: ["start", "end", "count"],
|
|
105
112
|
cameraKey: ["t", "x", "y", "width", "height", "ease"],
|
|
106
|
-
marker: ["t", "name"],
|
|
107
|
-
click: ["scene", "url"],
|
|
113
|
+
marker: ["t", "name", "loop"],
|
|
114
|
+
click: ["scene", "url", "set", "toggle"],
|
|
108
115
|
hover: ["stroke", "fill", "opacity"]
|
|
109
116
|
};
|
|
110
117
|
var MAX_DURATION = 3600;
|
|
@@ -258,6 +265,7 @@ var Reader = class {
|
|
|
258
265
|
anchor: str(v.anchor) ?? null,
|
|
259
266
|
click: this.click(v.click),
|
|
260
267
|
hover: this.hover(v.hover),
|
|
268
|
+
when: str(v.when) ?? null,
|
|
261
269
|
symbol: uuid(v.symbol) ?? null,
|
|
262
270
|
playback: this.playback(v.playback),
|
|
263
271
|
offset: num(v.offset) ?? null,
|
|
@@ -293,13 +301,27 @@ var Reader = class {
|
|
|
293
301
|
}
|
|
294
302
|
return { id, shape, version, params };
|
|
295
303
|
}
|
|
304
|
+
variables(v) {
|
|
305
|
+
if (!isObj(v)) return null;
|
|
306
|
+
const out = {};
|
|
307
|
+
for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
|
|
308
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
309
|
+
}
|
|
296
310
|
click(v) {
|
|
297
311
|
if (!isObj(v)) return null;
|
|
298
312
|
this.fields(v, "click");
|
|
299
313
|
const url = str(v.url);
|
|
300
314
|
const safe = url === void 0 ? null : allowedURL(url);
|
|
301
315
|
if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
|
|
302
|
-
|
|
316
|
+
let set = null;
|
|
317
|
+
if (isObj(v.set)) {
|
|
318
|
+
set = {};
|
|
319
|
+
for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
|
|
320
|
+
if (Object.keys(set).length === 0) set = null;
|
|
321
|
+
}
|
|
322
|
+
const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
|
|
323
|
+
const toggle = raw ? raw.filter(isName) : null;
|
|
324
|
+
return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null };
|
|
303
325
|
}
|
|
304
326
|
playback(v) {
|
|
305
327
|
const s = str(v);
|
|
@@ -400,7 +422,7 @@ var Reader = class {
|
|
|
400
422
|
if (camera) stableSortBy(camera, (c) => c.t);
|
|
401
423
|
const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
|
|
402
424
|
const t = num(m.t);
|
|
403
|
-
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
|
|
425
|
+
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
|
|
404
426
|
}) : null;
|
|
405
427
|
if (markers) stableSortBy(markers, (m) => m.t);
|
|
406
428
|
const poster = num(v.poster);
|
|
@@ -433,7 +455,8 @@ var Reader = class {
|
|
|
433
455
|
elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
|
|
434
456
|
theme: str(v.theme) ?? null,
|
|
435
457
|
timeline: this.timeline(v.timeline),
|
|
436
|
-
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
|
|
458
|
+
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
|
|
459
|
+
variables: this.variables(v.variables)
|
|
437
460
|
};
|
|
438
461
|
}
|
|
439
462
|
};
|
package/dist/dottie-compile.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/*! dottie.js 0.
|
|
2
|
+
/*! dottie.js 0.7.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);
|
|
@@ -52,13 +52,19 @@ function defaultBase() {
|
|
|
52
52
|
}
|
|
53
53
|
var base = defaultBase();
|
|
54
54
|
|
|
55
|
+
// src/state.ts
|
|
56
|
+
var MAX_NAME = 64;
|
|
57
|
+
function isName(name) {
|
|
58
|
+
return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
// src/model.ts
|
|
56
62
|
var SUPPORTED_VERSION = 2;
|
|
57
63
|
var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
|
|
58
64
|
var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
|
|
59
65
|
var KNOWN_FIELDS = {
|
|
60
66
|
file: ["version", "pages", "images"],
|
|
61
|
-
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
|
|
67
|
+
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables"],
|
|
62
68
|
symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
|
|
63
69
|
layer: ["id", "name", "visible", "locked"],
|
|
64
70
|
rgba: ["r", "g", "b", "a"],
|
|
@@ -86,6 +92,7 @@ var KNOWN_FIELDS = {
|
|
|
86
92
|
"anchor",
|
|
87
93
|
"click",
|
|
88
94
|
"hover",
|
|
95
|
+
"when",
|
|
89
96
|
"symbol",
|
|
90
97
|
"playback",
|
|
91
98
|
"offset",
|
|
@@ -111,8 +118,8 @@ var KNOWN_FIELDS = {
|
|
|
111
118
|
follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
|
|
112
119
|
repeat: ["start", "end", "count"],
|
|
113
120
|
cameraKey: ["t", "x", "y", "width", "height", "ease"],
|
|
114
|
-
marker: ["t", "name"],
|
|
115
|
-
click: ["scene", "url"],
|
|
121
|
+
marker: ["t", "name", "loop"],
|
|
122
|
+
click: ["scene", "url", "set", "toggle"],
|
|
116
123
|
hover: ["stroke", "fill", "opacity"]
|
|
117
124
|
};
|
|
118
125
|
var MAX_DURATION = 3600;
|
|
@@ -266,6 +273,7 @@ var Reader = class {
|
|
|
266
273
|
anchor: str(v.anchor) ?? null,
|
|
267
274
|
click: this.click(v.click),
|
|
268
275
|
hover: this.hover(v.hover),
|
|
276
|
+
when: str(v.when) ?? null,
|
|
269
277
|
symbol: uuid(v.symbol) ?? null,
|
|
270
278
|
playback: this.playback(v.playback),
|
|
271
279
|
offset: num(v.offset) ?? null,
|
|
@@ -301,13 +309,27 @@ var Reader = class {
|
|
|
301
309
|
}
|
|
302
310
|
return { id, shape, version, params };
|
|
303
311
|
}
|
|
312
|
+
variables(v) {
|
|
313
|
+
if (!isObj(v)) return null;
|
|
314
|
+
const out = {};
|
|
315
|
+
for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
|
|
316
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
317
|
+
}
|
|
304
318
|
click(v) {
|
|
305
319
|
if (!isObj(v)) return null;
|
|
306
320
|
this.fields(v, "click");
|
|
307
321
|
const url = str(v.url);
|
|
308
322
|
const safe = url === void 0 ? null : allowedURL(url);
|
|
309
323
|
if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
|
|
310
|
-
|
|
324
|
+
let set = null;
|
|
325
|
+
if (isObj(v.set)) {
|
|
326
|
+
set = {};
|
|
327
|
+
for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
|
|
328
|
+
if (Object.keys(set).length === 0) set = null;
|
|
329
|
+
}
|
|
330
|
+
const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
|
|
331
|
+
const toggle = raw ? raw.filter(isName) : null;
|
|
332
|
+
return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null };
|
|
311
333
|
}
|
|
312
334
|
playback(v) {
|
|
313
335
|
const s = str(v);
|
|
@@ -408,7 +430,7 @@ var Reader = class {
|
|
|
408
430
|
if (camera) stableSortBy(camera, (c) => c.t);
|
|
409
431
|
const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
|
|
410
432
|
const t = num(m.t);
|
|
411
|
-
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
|
|
433
|
+
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
|
|
412
434
|
}) : null;
|
|
413
435
|
if (markers) stableSortBy(markers, (m) => m.t);
|
|
414
436
|
const poster = num(v.poster);
|
|
@@ -441,7 +463,8 @@ var Reader = class {
|
|
|
441
463
|
elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
|
|
442
464
|
theme: str(v.theme) ?? null,
|
|
443
465
|
timeline: this.timeline(v.timeline),
|
|
444
|
-
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
|
|
466
|
+
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
|
|
467
|
+
variables: this.variables(v.variables)
|
|
445
468
|
};
|
|
446
469
|
}
|
|
447
470
|
};
|
|
@@ -588,7 +611,7 @@ function compile(source2, runtime, options2 = {}) {
|
|
|
588
611
|
}
|
|
589
612
|
|
|
590
613
|
// src/index.ts
|
|
591
|
-
var VERSION = "0.
|
|
614
|
+
var VERSION = "0.7.0";
|
|
592
615
|
|
|
593
616
|
// src/cli.ts
|
|
594
617
|
var usage = `dottie-compile ${VERSION}: a Dottie drawing as one script that plays it on any page.
|
package/dist/dottie-player.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! dottie.js 0.
|
|
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 __export = (target, all) => {
|
|
@@ -66,13 +66,67 @@ __export(index_exports, {
|
|
|
66
66
|
withRoughness: () => withRoughness
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
// src/state.ts
|
|
70
|
+
var MAX_NAME = 64;
|
|
71
|
+
function isName(name) {
|
|
72
|
+
return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
73
|
+
}
|
|
74
|
+
function conditionNames(condition) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const any of condition.split("||")) {
|
|
77
|
+
for (const all of any.split("&&")) {
|
|
78
|
+
let term = trim(all);
|
|
79
|
+
if (term.startsWith("!")) term = trim(term.slice(1));
|
|
80
|
+
if (!isName(term)) return null;
|
|
81
|
+
out.push(term);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function holds(condition, state) {
|
|
87
|
+
if (conditionNames(condition) === null) return false;
|
|
88
|
+
return condition.split("||").some((any) => any.split("&&").every((all) => {
|
|
89
|
+
let term = trim(all);
|
|
90
|
+
let negated = false;
|
|
91
|
+
if (term.startsWith("!")) {
|
|
92
|
+
negated = true;
|
|
93
|
+
term = trim(term.slice(1));
|
|
94
|
+
}
|
|
95
|
+
return (state[term] ?? false) !== negated;
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
function stateValues(page, choice) {
|
|
99
|
+
if (choice === "all") return null;
|
|
100
|
+
if (choice === "initial") return { ...page.variables ?? {} };
|
|
101
|
+
return { ...page.variables ?? {}, ...choice };
|
|
102
|
+
}
|
|
103
|
+
function stateHidden(page, choice) {
|
|
104
|
+
const values = stateValues(page, choice);
|
|
105
|
+
const out = /* @__PURE__ */ new Set();
|
|
106
|
+
if (!values) return out;
|
|
107
|
+
for (const e of page.elements) if (e.when !== null && !holds(e.when, values)) out.add(e.id);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function changesState(c) {
|
|
111
|
+
return !!c && (Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
|
|
112
|
+
}
|
|
113
|
+
function applyClick(c, state) {
|
|
114
|
+
const out = { ...state };
|
|
115
|
+
for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
|
|
116
|
+
for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function trim(s) {
|
|
120
|
+
return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
|
|
121
|
+
}
|
|
122
|
+
|
|
69
123
|
// src/model.ts
|
|
70
124
|
var SUPPORTED_VERSION = 2;
|
|
71
125
|
var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
|
|
72
126
|
var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
|
|
73
127
|
var KNOWN_FIELDS = {
|
|
74
128
|
file: ["version", "pages", "images"],
|
|
75
|
-
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
|
|
129
|
+
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables"],
|
|
76
130
|
symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
|
|
77
131
|
layer: ["id", "name", "visible", "locked"],
|
|
78
132
|
rgba: ["r", "g", "b", "a"],
|
|
@@ -100,6 +154,7 @@ var KNOWN_FIELDS = {
|
|
|
100
154
|
"anchor",
|
|
101
155
|
"click",
|
|
102
156
|
"hover",
|
|
157
|
+
"when",
|
|
103
158
|
"symbol",
|
|
104
159
|
"playback",
|
|
105
160
|
"offset",
|
|
@@ -125,8 +180,8 @@ var KNOWN_FIELDS = {
|
|
|
125
180
|
follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
|
|
126
181
|
repeat: ["start", "end", "count"],
|
|
127
182
|
cameraKey: ["t", "x", "y", "width", "height", "ease"],
|
|
128
|
-
marker: ["t", "name"],
|
|
129
|
-
click: ["scene", "url"],
|
|
183
|
+
marker: ["t", "name", "loop"],
|
|
184
|
+
click: ["scene", "url", "set", "toggle"],
|
|
130
185
|
hover: ["stroke", "fill", "opacity"]
|
|
131
186
|
};
|
|
132
187
|
var MAX_DURATION = 3600;
|
|
@@ -285,6 +340,7 @@ var Reader = class {
|
|
|
285
340
|
anchor: str(v.anchor) ?? null,
|
|
286
341
|
click: this.click(v.click),
|
|
287
342
|
hover: this.hover(v.hover),
|
|
343
|
+
when: str(v.when) ?? null,
|
|
288
344
|
symbol: uuid(v.symbol) ?? null,
|
|
289
345
|
playback: this.playback(v.playback),
|
|
290
346
|
offset: num(v.offset) ?? null,
|
|
@@ -320,13 +376,27 @@ var Reader = class {
|
|
|
320
376
|
}
|
|
321
377
|
return { id, shape, version, params };
|
|
322
378
|
}
|
|
379
|
+
variables(v) {
|
|
380
|
+
if (!isObj(v)) return null;
|
|
381
|
+
const out = {};
|
|
382
|
+
for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
|
|
383
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
384
|
+
}
|
|
323
385
|
click(v) {
|
|
324
386
|
if (!isObj(v)) return null;
|
|
325
387
|
this.fields(v, "click");
|
|
326
388
|
const url = str(v.url);
|
|
327
389
|
const safe = url === void 0 ? null : allowedURL(url);
|
|
328
390
|
if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
|
|
329
|
-
|
|
391
|
+
let set = null;
|
|
392
|
+
if (isObj(v.set)) {
|
|
393
|
+
set = {};
|
|
394
|
+
for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
|
|
395
|
+
if (Object.keys(set).length === 0) set = null;
|
|
396
|
+
}
|
|
397
|
+
const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
|
|
398
|
+
const toggle = raw ? raw.filter(isName) : null;
|
|
399
|
+
return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null };
|
|
330
400
|
}
|
|
331
401
|
playback(v) {
|
|
332
402
|
const s = str(v);
|
|
@@ -427,7 +497,7 @@ var Reader = class {
|
|
|
427
497
|
if (camera) stableSortBy(camera, (c) => c.t);
|
|
428
498
|
const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
|
|
429
499
|
const t = num(m.t);
|
|
430
|
-
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
|
|
500
|
+
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
|
|
431
501
|
}) : null;
|
|
432
502
|
if (markers) stableSortBy(markers, (m) => m.t);
|
|
433
503
|
const poster = num(v.poster);
|
|
@@ -460,7 +530,8 @@ var Reader = class {
|
|
|
460
530
|
elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
|
|
461
531
|
theme: str(v.theme) ?? null,
|
|
462
532
|
timeline: this.timeline(v.timeline),
|
|
463
|
-
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
|
|
533
|
+
symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
|
|
534
|
+
variables: this.variables(v.variables)
|
|
464
535
|
};
|
|
465
536
|
}
|
|
466
537
|
};
|
|
@@ -2130,17 +2201,20 @@ function trackMap(tl) {
|
|
|
2130
2201
|
for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
|
|
2131
2202
|
return m;
|
|
2132
2203
|
}
|
|
2133
|
-
function resolved(page, t) {
|
|
2134
|
-
return expandingSymbols(posed(page, t), clampTime(t));
|
|
2204
|
+
function resolved(page, t, state = "initial") {
|
|
2205
|
+
return expandingSymbols(posed(page, t, state), clampTime(t));
|
|
2135
2206
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2207
|
+
var EMPTY_TIMELINE = { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
|
|
2208
|
+
function posed(page, t, state = "initial") {
|
|
2209
|
+
const off = stateHidden(page, state);
|
|
2210
|
+
const timeline = page.timeline ?? (off.size === 0 ? null : EMPTY_TIMELINE);
|
|
2138
2211
|
if (!timeline) return page;
|
|
2139
2212
|
t = clampTime(t);
|
|
2140
2213
|
const tracks = trackMap(timeline);
|
|
2141
2214
|
const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
|
|
2142
2215
|
const hidden = /* @__PURE__ */ new Set();
|
|
2143
2216
|
for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
|
|
2217
|
+
for (const id of off) hidden.add(id);
|
|
2144
2218
|
let moving = null;
|
|
2145
2219
|
if (timeline.settle === true) {
|
|
2146
2220
|
moving = /* @__PURE__ */ new Set();
|
|
@@ -2179,7 +2253,7 @@ function posed(page, t) {
|
|
|
2179
2253
|
elements = elements.map((e) => {
|
|
2180
2254
|
const c = e.connector;
|
|
2181
2255
|
if (!c || ![c.start.elementID, c.end.elementID].some((id) => id !== null && hidden.has(id))) return e;
|
|
2182
|
-
if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false)) {
|
|
2256
|
+
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))) {
|
|
2183
2257
|
gone.add(e.id);
|
|
2184
2258
|
return e;
|
|
2185
2259
|
}
|
|
@@ -2195,7 +2269,7 @@ function posterTime(page) {
|
|
|
2195
2269
|
return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
|
|
2196
2270
|
}
|
|
2197
2271
|
function still(page) {
|
|
2198
|
-
return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(page, 0);
|
|
2272
|
+
return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(posed(page, 0), 0);
|
|
2199
2273
|
}
|
|
2200
2274
|
function cameraRect(tl, t) {
|
|
2201
2275
|
const keys = tl.camera ?? [];
|
|
@@ -2304,7 +2378,7 @@ function filled(s, args) {
|
|
|
2304
2378
|
}) };
|
|
2305
2379
|
}
|
|
2306
2380
|
function symbolPage(s) {
|
|
2307
|
-
return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null };
|
|
2381
|
+
return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null };
|
|
2308
2382
|
}
|
|
2309
2383
|
function hasInstances(page) {
|
|
2310
2384
|
return page.elements.some((e) => e.kind === "symbol");
|
|
@@ -2433,16 +2507,21 @@ function clickTargets(page, file) {
|
|
|
2433
2507
|
let pageOf = null;
|
|
2434
2508
|
for (const e of page.elements) {
|
|
2435
2509
|
const c = e.click;
|
|
2510
|
+
const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {} } : null;
|
|
2436
2511
|
if (c?.scene) {
|
|
2437
2512
|
const i = sceneNamed(c.scene, markers);
|
|
2438
2513
|
if (i >= 0) {
|
|
2439
|
-
out.set(e.id, { scene: i });
|
|
2514
|
+
out.set(e.id, { scene: i, ...state });
|
|
2440
2515
|
continue;
|
|
2441
2516
|
}
|
|
2442
2517
|
}
|
|
2443
2518
|
const url = (c?.url ? allowedURL(c.url) : null) ?? webLink(e.url);
|
|
2444
2519
|
if (url) {
|
|
2445
|
-
out.set(e.id, { url });
|
|
2520
|
+
out.set(e.id, { url, ...state });
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
if (state) {
|
|
2524
|
+
out.set(e.id, { state: true, ...state });
|
|
2446
2525
|
continue;
|
|
2447
2526
|
}
|
|
2448
2527
|
if (e.link && file && file.pages.length > 1) {
|
|
@@ -3356,6 +3435,10 @@ var Playback = class {
|
|
|
3356
3435
|
__publicField(this, "plan");
|
|
3357
3436
|
__publicField(this, "scenes");
|
|
3358
3437
|
__publicField(this, "sceneNames");
|
|
3438
|
+
/** Whether each scene loops (Marker.loop). */
|
|
3439
|
+
__publicField(this, "sceneLoops");
|
|
3440
|
+
/** The page's variables now: their starting values until clicks change them. */
|
|
3441
|
+
__publicField(this, "state");
|
|
3359
3442
|
__publicField(this, "background");
|
|
3360
3443
|
__publicField(this, "ink");
|
|
3361
3444
|
/** What clicking each clickable element does, by element id. */
|
|
@@ -3415,6 +3498,8 @@ var Playback = class {
|
|
|
3415
3498
|
};
|
|
3416
3499
|
this.scenes = (page.timeline?.markers ?? []).map((m) => near(m.t));
|
|
3417
3500
|
this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
|
|
3501
|
+
this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
|
|
3502
|
+
this.state = { ...page.variables ?? {} };
|
|
3418
3503
|
const theme = themeNamed(page.theme);
|
|
3419
3504
|
this.background = theme?.background ?? WHITE;
|
|
3420
3505
|
this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
|
|
@@ -3465,7 +3550,7 @@ var Playback = class {
|
|
|
3465
3550
|
const times = /* @__PURE__ */ new Map();
|
|
3466
3551
|
for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
|
|
3467
3552
|
const tc = clampTime(t);
|
|
3468
|
-
const pose = posed(this.page, tc);
|
|
3553
|
+
const pose = posed(this.page, tc, this.state);
|
|
3469
3554
|
const frame2 = expandingSymbols(pose, tc, true, times);
|
|
3470
3555
|
const { parts } = svgParts(frame2, this.background, r);
|
|
3471
3556
|
const owned = (m, parts2) => {
|
|
@@ -3587,6 +3672,20 @@ ${h}</g>
|
|
|
3587
3672
|
});
|
|
3588
3673
|
return s;
|
|
3589
3674
|
}
|
|
3675
|
+
/** The frame after scene `s`'s last: the next scene's, or the frame count after the last scene. */
|
|
3676
|
+
sceneEnd(s) {
|
|
3677
|
+
return this.scenes.find((f, j) => j > s && f > this.scenes[s]) ?? this.frameCount;
|
|
3678
|
+
}
|
|
3679
|
+
/** Where playing goes from frame `i`, and whether it stops there: on into a looping scene, round to the start of
|
|
3680
|
+
* the looping scene `i` ends, or a stop at a scene or the end (`loops`: whether the timeline loops at its end). */
|
|
3681
|
+
step(i, loops) {
|
|
3682
|
+
const next = i + 1;
|
|
3683
|
+
const s = this.sceneAt(i);
|
|
3684
|
+
if (s >= 0 && this.sceneLoops[s] && next >= this.sceneEnd(s)) return { frame: this.scenes[s], stop: false };
|
|
3685
|
+
if (next >= this.frameCount) return { frame: 0, stop: !loops || this.scenes.length > 0 };
|
|
3686
|
+
const entered = this.scenes.indexOf(next);
|
|
3687
|
+
return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
|
|
3688
|
+
}
|
|
3590
3689
|
};
|
|
3591
3690
|
var ElementBase = typeof HTMLElement === "undefined" ? class {
|
|
3592
3691
|
} : HTMLElement;
|
|
@@ -3740,7 +3839,7 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3740
3839
|
this.setPlaying(false);
|
|
3741
3840
|
return;
|
|
3742
3841
|
}
|
|
3743
|
-
this.setPlaying(!this.hasAttribute("no-autoplay") && !p.scenes.includes(0));
|
|
3842
|
+
this.setPlaying(!this.hasAttribute("no-autoplay") && (!p.scenes.includes(0) || p.sceneLoops[p.scenes.indexOf(0)]));
|
|
3744
3843
|
}
|
|
3745
3844
|
// MARK: Playback
|
|
3746
3845
|
get paused() {
|
|
@@ -3766,11 +3865,13 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3766
3865
|
pause() {
|
|
3767
3866
|
this.setPlaying(false);
|
|
3768
3867
|
}
|
|
3769
|
-
/** Plays on to the next scene (from the start after the last). */
|
|
3868
|
+
/** Plays on to the next scene (from the start after the last). Out of a looping scene it jumps to the next. */
|
|
3770
3869
|
nextScene() {
|
|
3771
3870
|
const p = this.playback;
|
|
3772
3871
|
if (!p) return;
|
|
3773
|
-
|
|
3872
|
+
const s = p.sceneAt(p.frame);
|
|
3873
|
+
if (s >= 0 && p.sceneLoops[s]) p.show(s + 1 < p.scenes.length ? p.scenes[s + 1] : 0);
|
|
3874
|
+
else if (p.frame >= p.frameCount - 1) p.show(0);
|
|
3774
3875
|
this.setPlaying(true);
|
|
3775
3876
|
}
|
|
3776
3877
|
/** Back to the start of this scene, or the one before if already there. */
|
|
@@ -3799,6 +3900,8 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3799
3900
|
this.scheduleReplay();
|
|
3800
3901
|
}
|
|
3801
3902
|
if (!click) return;
|
|
3903
|
+
if (click.set || click.toggle) this.setState(applyClick({ scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null }, p.state));
|
|
3904
|
+
if ("state" in click) return;
|
|
3802
3905
|
if ("url" in click) {
|
|
3803
3906
|
if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
|
|
3804
3907
|
} else if ("page" in click) {
|
|
@@ -3806,13 +3909,27 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3806
3909
|
} else if (click.scene >= 0 && click.scene < p.scenes.length) {
|
|
3807
3910
|
if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
|
|
3808
3911
|
this.setPlaying(false);
|
|
3809
|
-
p.show(click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
|
|
3912
|
+
p.show(p.sceneLoops[click.scene] ? p.scenes[click.scene] : click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
|
|
3810
3913
|
} else {
|
|
3811
3914
|
p.show(p.scenes[click.scene]);
|
|
3812
3915
|
this.setPlaying(true);
|
|
3813
3916
|
}
|
|
3814
3917
|
}
|
|
3815
3918
|
}
|
|
3919
|
+
/** The page's variables now. */
|
|
3920
|
+
get variables() {
|
|
3921
|
+
return { ...this.playback?.state ?? {} };
|
|
3922
|
+
}
|
|
3923
|
+
/** Gives the page's variables these values (the others keep theirs), redraws, and fires `state`. */
|
|
3924
|
+
setState(values) {
|
|
3925
|
+
const p = this.playback;
|
|
3926
|
+
if (!p) return;
|
|
3927
|
+
const next = { ...p.state, ...values };
|
|
3928
|
+
if (Object.keys(next).every((k) => next[k] === p.state[k])) return;
|
|
3929
|
+
p.state = next;
|
|
3930
|
+
p.redraw();
|
|
3931
|
+
this.dispatchEvent(new CustomEvent("state", { detail: { variables: { ...next } } }));
|
|
3932
|
+
}
|
|
3816
3933
|
/** Redraws, while the page itself stands still, as long as a clicked copy is playing through. */
|
|
3817
3934
|
scheduleReplay() {
|
|
3818
3935
|
if (this.replayRaf) return;
|
|
@@ -3838,7 +3955,8 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3838
3955
|
schedule() {
|
|
3839
3956
|
if (this.playing && this.visible && !this.raf && this.isConnected) this.raf = requestAnimationFrame((now) => this.tick(now));
|
|
3840
3957
|
}
|
|
3841
|
-
/** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end
|
|
3958
|
+
/** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end, except that a
|
|
3959
|
+
* looping scene plays round (Playback.step). */
|
|
3842
3960
|
tick(now) {
|
|
3843
3961
|
this.raf = 0;
|
|
3844
3962
|
const p = this.playback;
|
|
@@ -3849,14 +3967,14 @@ var DottiePlayer = class extends ElementBase {
|
|
|
3849
3967
|
let advanced = false;
|
|
3850
3968
|
while (this.playing && this.acc >= step) {
|
|
3851
3969
|
this.acc -= step;
|
|
3852
|
-
const
|
|
3853
|
-
if (
|
|
3970
|
+
const { frame: frame2, stop } = p.step(p.frame, !this.hasAttribute("no-loop"));
|
|
3971
|
+
if (stop && frame2 === 0 && p.frame + 1 >= p.frameCount) {
|
|
3854
3972
|
this.setPlaying(false);
|
|
3855
3973
|
break;
|
|
3856
3974
|
}
|
|
3857
|
-
p.frame =
|
|
3975
|
+
p.frame = frame2;
|
|
3858
3976
|
advanced = true;
|
|
3859
|
-
if (
|
|
3977
|
+
if (stop) {
|
|
3860
3978
|
this.setPlaying(false);
|
|
3861
3979
|
break;
|
|
3862
3980
|
}
|
|
@@ -4006,7 +4124,7 @@ function render(source, options = {}) {
|
|
|
4006
4124
|
function isFile(v) {
|
|
4007
4125
|
return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
|
|
4008
4126
|
}
|
|
4009
|
-
var VERSION = "0.
|
|
4127
|
+
var VERSION = "0.7.0";
|
|
4010
4128
|
|
|
4011
4129
|
// src/player-entry.ts
|
|
4012
4130
|
definePlayer();
|