@phin-tech/dottie 0.1.0 → 0.3.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 +2 -1
- package/README.md +19 -0
- package/dist/compile.js +394 -0
- package/dist/dottie-compile.mjs +496 -0
- package/dist/dottie-loader.js +20 -0
- package/dist/dottie-player.js +77 -18
- package/dist/dottie-player.min.js +36 -36
- package/dist/dottie.js +75 -18
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/compile.d.ts +19 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/model.d.ts +8 -1
- package/dist/types/timeline.d.ts +3 -0
- package/package.json +9 -2
package/PARITY.md
CHANGED
|
@@ -24,11 +24,12 @@ 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
|
| Arrowheads: arrow, triangle, dot, bar | Supported | [edge/connectors](../fixtures/edge/connectors.dottie) | |
|
|
26
26
|
| Connector labels | Supported | [edge/connectors](../fixtures/edge/connectors.dottie) | Label boxes sized with CoreText's measurements. The player paints them on the page color, like the app; Dottie's flipbook export leaves them unpainted |
|
|
27
|
-
| Keys and eases (linear, in, out, in-out, hold) | Supported | [edge/timeline](../fixtures/edge/timeline.dottie) | |
|
|
27
|
+
| Keys and eases (linear, in, out, in-out, hold, spring, bounce, elastic) | Supported | [edge/timeline](../fixtures/edge/timeline.dottie), [edge/eases](../fixtures/edge/eases.dottie) | Unknown ease names play linear, with a warning |
|
|
28
28
|
| Visibility, reveal (draw-on, typing), opacity | Supported | [edge/timeline](../fixtures/edge/timeline.dottie) | |
|
|
29
29
|
| Transitions | Supported | [edge/transitions](../fixtures/edge/transitions.dottie) | They are ordinary keys in the file |
|
|
30
30
|
| Follow paths (connectors, outlines, orient, reverse, laps) | Supported | [edge/follow](../fixtures/edge/follow.dottie) | |
|
|
31
31
|
| Packets, labelled packets | Supported | [design/labels](../fixtures/design/labels.dottie) | |
|
|
32
|
+
| Marching ants (dashes travelling along a line) | Supported | [edge/march](../fixtures/edge/march.dottie) | `stroke-dashoffset` per frame, evened out to loop |
|
|
32
33
|
| Repeats (counted and forever) | Supported | [edge/timeline](../fixtures/edge/timeline.dottie) | |
|
|
33
34
|
| Boil (three drawings) and settle | Supported | [edge/timeline](../fixtures/edge/timeline.dottie) | Boiled seeds match bit for bit |
|
|
34
35
|
| Camera views | Supported | [edge/camera](../fixtures/edge/camera.dottie), [design/zoom](../fixtures/design/zoom.dottie) | |
|
package/README.md
CHANGED
|
@@ -36,6 +36,25 @@ The element has `play()`, `pause()`, `nextScene()`, `prevScene()`, `currentTime`
|
|
|
36
36
|
Without modules, `dist/dottie-player.min.js` is a classic script that registers the element and sets
|
|
37
37
|
`window.Dottie`.
|
|
38
38
|
|
|
39
|
+
## Compile a drawing into one script
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npx -p @phin-tech/dottie dottie-compile tour.dottie # writes tour.js
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`tour.js` holds the player and the drawing (about 75 KB plus the drawing; around 25 KB gzipped), so one file
|
|
46
|
+
shows it anywhere you can put a script tag:
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<script src="tour.js"></script> <!-- a player right here -->
|
|
50
|
+
<script src="tour.js" data-target="#demo"></script> <!-- a player inside #demo -->
|
|
51
|
+
<script src="tour.js" data-clean data-no-loop></script> <!-- data-* set the player's attributes -->
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`dottie-compile` takes `--page`, `--at`, `--clean`, `--no-controls`, `--no-autoplay`, `--no-loop` and `--label`;
|
|
55
|
+
only the chosen page (and its pictures) goes in. The Dottie Mac app's command-line tool writes the same file:
|
|
56
|
+
`dottie render tour.dottie -f js`. From code: `import { compile } from "@phin-tech/dottie/compile"`.
|
|
57
|
+
|
|
39
58
|
## Use the library
|
|
40
59
|
|
|
41
60
|
```js
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/*! dottie.js 0.3.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
|
|
6
|
+
// src/model.ts
|
|
7
|
+
var SUPPORTED_VERSION = 2;
|
|
8
|
+
var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image"];
|
|
9
|
+
var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
|
|
10
|
+
var KNOWN_FIELDS = {
|
|
11
|
+
file: ["version", "pages", "images"],
|
|
12
|
+
page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline"],
|
|
13
|
+
layer: ["id", "name", "visible", "locked"],
|
|
14
|
+
rgba: ["r", "g", "b", "a"],
|
|
15
|
+
style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "gridSize", "opacity", "dashOffset"],
|
|
16
|
+
element: [
|
|
17
|
+
"id",
|
|
18
|
+
"kind",
|
|
19
|
+
"x",
|
|
20
|
+
"y",
|
|
21
|
+
"width",
|
|
22
|
+
"height",
|
|
23
|
+
"style",
|
|
24
|
+
"seed",
|
|
25
|
+
"layerID",
|
|
26
|
+
"text",
|
|
27
|
+
"connector",
|
|
28
|
+
"groupID",
|
|
29
|
+
"points",
|
|
30
|
+
"path",
|
|
31
|
+
"subpaths",
|
|
32
|
+
"evenOdd",
|
|
33
|
+
"rotation",
|
|
34
|
+
"link",
|
|
35
|
+
"anchor",
|
|
36
|
+
"image",
|
|
37
|
+
"reveal"
|
|
38
|
+
],
|
|
39
|
+
strokePoint: ["x", "y"],
|
|
40
|
+
subpath: ["closed", "coords"],
|
|
41
|
+
inlineImage: ["data", "format"],
|
|
42
|
+
endpoint: ["elementID", "port", "x", "y"],
|
|
43
|
+
connector: ["start", "end", "routing", "startHead", "endHead"],
|
|
44
|
+
timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
|
|
45
|
+
track: ["element", "keys", "boil", "follow", "repeats", "march"],
|
|
46
|
+
keyframe: ["t", "x", "y", "width", "height", "rotation", "strokeWidth", "stroke", "fill", "visible", "reveal", "opacity", "ease"],
|
|
47
|
+
follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
|
|
48
|
+
repeat: ["start", "end", "count"],
|
|
49
|
+
cameraKey: ["t", "x", "y", "width", "height", "ease"],
|
|
50
|
+
marker: ["t", "name"]
|
|
51
|
+
};
|
|
52
|
+
var MAX_DURATION = 3600;
|
|
53
|
+
var MAX_MARCH = 1e4;
|
|
54
|
+
var clampMarch = (v) => v === void 0 || !Number.isFinite(v) || v === 0 ? null : Math.min(Math.max(v, -MAX_MARCH), MAX_MARCH);
|
|
55
|
+
var black = { r: 0.12, g: 0.12, b: 0.13, a: 1 };
|
|
56
|
+
var clear = { r: 0, g: 0, b: 0, a: 0 };
|
|
57
|
+
var DottieFormatError = class extends Error {
|
|
58
|
+
};
|
|
59
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
60
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
61
|
+
var str = (v) => typeof v === "string" ? v : void 0;
|
|
62
|
+
var bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
63
|
+
var uuid = (v) => typeof v === "string" ? v.toUpperCase() : void 0;
|
|
64
|
+
function clampTime(t) {
|
|
65
|
+
if (!Number.isFinite(t)) return t === Infinity ? MAX_DURATION : 0;
|
|
66
|
+
return Math.min(Math.max(t, 0), MAX_DURATION);
|
|
67
|
+
}
|
|
68
|
+
function clampFPS(f) {
|
|
69
|
+
return Number.isFinite(f) ? Math.min(Math.max(f, 1), 60) : 12;
|
|
70
|
+
}
|
|
71
|
+
function rgba(v) {
|
|
72
|
+
if (!isObj(v)) return void 0;
|
|
73
|
+
const r = num(v.r), g = num(v.g), b = num(v.b);
|
|
74
|
+
if (r === void 0 || g === void 0 || b === void 0) return void 0;
|
|
75
|
+
return { r, g, b, a: num(v.a) ?? 1 };
|
|
76
|
+
}
|
|
77
|
+
var Reader = class {
|
|
78
|
+
constructor() {
|
|
79
|
+
__publicField(this, "warnings", []);
|
|
80
|
+
__publicField(this, "seen", /* @__PURE__ */ new Set());
|
|
81
|
+
}
|
|
82
|
+
warn(message) {
|
|
83
|
+
if (!this.seen.has(message)) {
|
|
84
|
+
this.seen.add(message);
|
|
85
|
+
this.warnings.push(message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Reports fields this version doesn't read (they're ignored, as Dottie ignores them). */
|
|
89
|
+
fields(v, kind) {
|
|
90
|
+
const known = KNOWN_FIELDS[kind];
|
|
91
|
+
for (const k of Object.keys(v)) if (!known.includes(k)) this.warn(`Unknown field "${k}" on ${kind}, ignored`);
|
|
92
|
+
}
|
|
93
|
+
style(v) {
|
|
94
|
+
const s = isObj(v) ? v : {};
|
|
95
|
+
this.fields(s, "style");
|
|
96
|
+
const fillStyle = str(s.fillStyle) ?? "solid";
|
|
97
|
+
const dash = str(s.dash) ?? "solid";
|
|
98
|
+
if (!["solid", "hachure", "crossHatch", "grid"].includes(fillStyle)) this.warn(`Unknown fill style "${fillStyle}", drawn solid`);
|
|
99
|
+
if (!["solid", "dashed", "dotted"].includes(dash)) this.warn(`Unknown dash "${dash}", drawn solid`);
|
|
100
|
+
return {
|
|
101
|
+
stroke: rgba(s.stroke) ?? { ...black },
|
|
102
|
+
fill: rgba(s.fill) ?? { ...clear },
|
|
103
|
+
fillStyle: ["solid", "hachure", "crossHatch", "grid"].includes(fillStyle) ? fillStyle : "solid",
|
|
104
|
+
strokeWidth: num(s.strokeWidth) ?? 2,
|
|
105
|
+
dash: ["solid", "dashed", "dotted"].includes(dash) ? dash : "solid",
|
|
106
|
+
roughness: num(s.roughness) ?? 1,
|
|
107
|
+
bowing: num(s.bowing) ?? 1,
|
|
108
|
+
fontSize: num(s.fontSize) ?? 18,
|
|
109
|
+
gridSize: num(s.gridSize) ?? null,
|
|
110
|
+
opacity: num(s.opacity) ?? null,
|
|
111
|
+
dashOffset: num(s.dashOffset) ?? null
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
endpoint(v) {
|
|
115
|
+
const e = isObj(v) ? v : {};
|
|
116
|
+
const port = str(e.port);
|
|
117
|
+
return {
|
|
118
|
+
elementID: uuid(e.elementID) ?? null,
|
|
119
|
+
port: port && ["top", "right", "bottom", "left"].includes(port) ? port : null,
|
|
120
|
+
x: num(e.x) ?? 0,
|
|
121
|
+
y: num(e.y) ?? 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
connector(v) {
|
|
125
|
+
if (!isObj(v)) return null;
|
|
126
|
+
this.fields(v, "connector");
|
|
127
|
+
const head = (h, d) => {
|
|
128
|
+
const s = str(h);
|
|
129
|
+
return s && ["none", "arrow", "triangle", "dot", "bar"].includes(s) ? s : d;
|
|
130
|
+
};
|
|
131
|
+
const routing = str(v.routing);
|
|
132
|
+
return {
|
|
133
|
+
start: this.endpoint(v.start),
|
|
134
|
+
end: this.endpoint(v.end),
|
|
135
|
+
routing: routing && ["straight", "curved", "elbow"].includes(routing) ? routing : "straight",
|
|
136
|
+
startHead: head(v.startHead, "none"),
|
|
137
|
+
endHead: head(v.endHead, "arrow")
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
image(v, table) {
|
|
141
|
+
if (!isObj(v)) return null;
|
|
142
|
+
const ref = str(v.ref);
|
|
143
|
+
if (ref !== void 0) return table[ref] ?? { data: "", format: "png" };
|
|
144
|
+
const data = str(v.data);
|
|
145
|
+
return data === void 0 ? null : this.picture(data, str(v.format));
|
|
146
|
+
}
|
|
147
|
+
/** A picture's bytes, only if they really are base64: they go into an attribute of the drawn SVG. */
|
|
148
|
+
picture(data, format) {
|
|
149
|
+
if (!BASE64.test(data)) {
|
|
150
|
+
this.warn("Skipped a picture whose data isn't base64");
|
|
151
|
+
return { data: "", format: "png" };
|
|
152
|
+
}
|
|
153
|
+
return { data: data.replace(/\s+/g, ""), format: format === "jpeg" ? "jpeg" : "png" };
|
|
154
|
+
}
|
|
155
|
+
element(v, images) {
|
|
156
|
+
if (!isObj(v)) return null;
|
|
157
|
+
this.fields(v, "element");
|
|
158
|
+
const id = uuid(v.id), kind = str(v.kind), layerID = uuid(v.layerID);
|
|
159
|
+
if (!id || !kind || !layerID) {
|
|
160
|
+
this.warn("Skipped an element without an id, kind or layer");
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (!KINDS.includes(kind)) this.warn(`Unknown element kind "${kind}", drawn as a placeholder`);
|
|
164
|
+
const subpaths = Array.isArray(v.subpaths) ? v.subpaths.filter(isObj).map((s) => ({ closed: bool(s.closed) ?? false, coords: Array.isArray(s.coords) ? s.coords.filter((c) => typeof c === "number") : [] })) : null;
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
kind,
|
|
168
|
+
layerID,
|
|
169
|
+
x: num(v.x) ?? 0,
|
|
170
|
+
y: num(v.y) ?? 0,
|
|
171
|
+
width: num(v.width) ?? 0,
|
|
172
|
+
height: num(v.height) ?? 0,
|
|
173
|
+
style: this.style(v.style),
|
|
174
|
+
seed: (num(v.seed) ?? 1) >>> 0,
|
|
175
|
+
text: str(v.text) ?? null,
|
|
176
|
+
connector: this.connector(v.connector),
|
|
177
|
+
groupID: uuid(v.groupID) ?? null,
|
|
178
|
+
points: Array.isArray(v.points) ? v.points.filter(isObj).map((p) => ({ x: num(p.x) ?? 0, y: num(p.y) ?? 0 })) : null,
|
|
179
|
+
path: Array.isArray(v.path) ? v.path.filter((c) => typeof c === "number") : null,
|
|
180
|
+
subpaths,
|
|
181
|
+
evenOdd: bool(v.evenOdd) ?? null,
|
|
182
|
+
rotation: num(v.rotation) ?? null,
|
|
183
|
+
link: uuid(v.link) ?? null,
|
|
184
|
+
anchor: str(v.anchor) ?? null,
|
|
185
|
+
image: this.image(v.image, images),
|
|
186
|
+
reveal: num(v.reveal) ?? null
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
ease(v) {
|
|
190
|
+
const s = str(v);
|
|
191
|
+
if (s === void 0) return null;
|
|
192
|
+
if (!EASES.includes(s)) {
|
|
193
|
+
this.warn(`Unknown ease "${s}", played linear`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return s;
|
|
197
|
+
}
|
|
198
|
+
keyframe(v) {
|
|
199
|
+
if (!isObj(v)) return null;
|
|
200
|
+
this.fields(v, "keyframe");
|
|
201
|
+
const t = num(v.t);
|
|
202
|
+
if (t === void 0) return null;
|
|
203
|
+
const k = { t };
|
|
204
|
+
for (const p of ["x", "y", "width", "height", "rotation", "strokeWidth", "reveal", "opacity"]) {
|
|
205
|
+
const n = num(v[p]);
|
|
206
|
+
if (n !== void 0) k[p] = n;
|
|
207
|
+
}
|
|
208
|
+
const stroke = rgba(v.stroke), fill = rgba(v.fill), visible = bool(v.visible), ease = this.ease(v.ease);
|
|
209
|
+
if (stroke) k.stroke = stroke;
|
|
210
|
+
if (fill) k.fill = fill;
|
|
211
|
+
if (visible !== void 0) k.visible = visible;
|
|
212
|
+
if (ease) k.ease = ease;
|
|
213
|
+
return k;
|
|
214
|
+
}
|
|
215
|
+
track(v) {
|
|
216
|
+
if (!isObj(v)) return null;
|
|
217
|
+
this.fields(v, "track");
|
|
218
|
+
const element = uuid(v.element);
|
|
219
|
+
if (!element) return null;
|
|
220
|
+
const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
|
|
221
|
+
stableSortBy(keys, (k) => k.t);
|
|
222
|
+
const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
|
|
223
|
+
const path = uuid(f.path), start = num(f.start), end = num(f.end);
|
|
224
|
+
if (!path || start === void 0 || end === void 0 || start < 0 || end > MAX_DURATION) return [];
|
|
225
|
+
return [{ path, start, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
|
|
226
|
+
}) : null;
|
|
227
|
+
const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
|
|
228
|
+
const start = num(r.start), end = num(r.end);
|
|
229
|
+
if (start === void 0 || end === void 0) return [];
|
|
230
|
+
const count = num(r.count);
|
|
231
|
+
const rep = { start, end, count: count === void 0 ? null : Math.trunc(count) };
|
|
232
|
+
return repeatIsValid(rep) ? [rep] : [];
|
|
233
|
+
}) : null;
|
|
234
|
+
return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
|
|
235
|
+
}
|
|
236
|
+
timeline(v) {
|
|
237
|
+
if (!isObj(v)) return null;
|
|
238
|
+
this.fields(v, "timeline");
|
|
239
|
+
const boil = num(v.boil);
|
|
240
|
+
const camera = Array.isArray(v.camera) ? v.camera.filter(isObj).flatMap((c) => {
|
|
241
|
+
const t = num(c.t), x = num(c.x), y = num(c.y), width = num(c.width), height = num(c.height);
|
|
242
|
+
if (t === void 0 || x === void 0 || y === void 0 || width === void 0 || height === void 0) return [];
|
|
243
|
+
if (!(width > 1 && height > 1 && t >= 0)) return [];
|
|
244
|
+
return [{ t, x, y, width, height, ease: this.ease(c.ease) }];
|
|
245
|
+
}) : null;
|
|
246
|
+
if (camera) stableSortBy(camera, (c) => c.t);
|
|
247
|
+
const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
|
|
248
|
+
const t = num(m.t);
|
|
249
|
+
return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
|
|
250
|
+
}) : null;
|
|
251
|
+
if (markers) stableSortBy(markers, (m) => m.t);
|
|
252
|
+
const poster = num(v.poster);
|
|
253
|
+
return {
|
|
254
|
+
fps: clampFPS(num(v.fps) ?? 12),
|
|
255
|
+
duration: clampTime(num(v.duration) ?? 0),
|
|
256
|
+
boil: boil !== void 0 && Number.isInteger(boil) && boil > 0 ? Math.min(boil, 1e4) : null,
|
|
257
|
+
settle: bool(v.settle) ?? null,
|
|
258
|
+
poster: poster === void 0 ? null : clampTime(poster),
|
|
259
|
+
tracks: (Array.isArray(v.tracks) ? v.tracks : []).map((t) => this.track(t)).filter((t) => t !== null),
|
|
260
|
+
camera,
|
|
261
|
+
markers
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
page(v, images, n) {
|
|
265
|
+
if (!isObj(v)) throw new DottieFormatError(`Page ${n} is not an object`);
|
|
266
|
+
this.fields(v, "page");
|
|
267
|
+
const layers = (Array.isArray(v.layers) ? v.layers : []).filter(isObj).flatMap((l) => {
|
|
268
|
+
const id = uuid(l.id);
|
|
269
|
+
return id ? [{ id, name: str(l.name) ?? "", visible: bool(l.visible) ?? true, locked: bool(l.locked) ?? false }] : [];
|
|
270
|
+
});
|
|
271
|
+
if (layers.length === 0) {
|
|
272
|
+
this.warn(`Page ${n} has no layers`);
|
|
273
|
+
layers.push({ id: "LAYER", name: "Layer 1", visible: true, locked: false });
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
id: uuid(v.id) ?? `PAGE-${n}`,
|
|
277
|
+
name: str(v.name) ?? `Page ${n}`,
|
|
278
|
+
layers,
|
|
279
|
+
elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
|
|
280
|
+
theme: str(v.theme) ?? null,
|
|
281
|
+
timeline: this.timeline(v.timeline)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
var BASE64 = /^[A-Za-z0-9+/=\s]*$/;
|
|
286
|
+
function repeatIsValid(r) {
|
|
287
|
+
return Number.isFinite(r.start) && Number.isFinite(r.end) && r.start >= 0 && r.end - r.start > 1e-3 && r.end <= MAX_DURATION && (r.count === null || r.count >= 1 && r.count <= 1e4);
|
|
288
|
+
}
|
|
289
|
+
function stableSortBy(a, key) {
|
|
290
|
+
const indexed = a.map((v, i) => [v, i]);
|
|
291
|
+
indexed.sort((p, q) => key(p[0]) - key(q[0]) || p[1] - q[1]);
|
|
292
|
+
for (let i = 0; i < a.length; i++) a[i] = indexed[i][0];
|
|
293
|
+
}
|
|
294
|
+
function parseDottie(source) {
|
|
295
|
+
let json = source;
|
|
296
|
+
if (typeof source === "string") {
|
|
297
|
+
try {
|
|
298
|
+
json = JSON.parse(source);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
throw new DottieFormatError(`Not JSON: ${e.message}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (!isObj(json)) throw new DottieFormatError("Not a Dottie drawing");
|
|
304
|
+
const version = num(json.version);
|
|
305
|
+
if (version === void 0) throw new DottieFormatError("Not a Dottie drawing (no version)");
|
|
306
|
+
if (!Array.isArray(json.pages) || json.pages.length === 0) throw new DottieFormatError("A drawing needs at least one page");
|
|
307
|
+
const reader = new Reader();
|
|
308
|
+
reader.fields(json, "file");
|
|
309
|
+
if (version > SUPPORTED_VERSION) reader.warn(`File version ${version} is newer than this player knows (${SUPPORTED_VERSION}); some things may not show`);
|
|
310
|
+
const images = {};
|
|
311
|
+
if (isObj(json.images)) {
|
|
312
|
+
for (const [k, v] of Object.entries(json.images)) {
|
|
313
|
+
if (isObj(v) && typeof v.data === "string") images[k] = reader.picture(v.data, str(v.format));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const pages = json.pages.map((p, i) => reader.page(p, images, i + 1));
|
|
317
|
+
if (new Set(pages.map((p) => p.id)).size !== pages.length) throw new DottieFormatError("Two pages share an id");
|
|
318
|
+
return { version, pages, warnings: reader.warnings };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/player.ts
|
|
322
|
+
function pageIndex(file, spec) {
|
|
323
|
+
const s = (spec ?? "").trim();
|
|
324
|
+
if (!s) return 0;
|
|
325
|
+
const byName = file.pages.findIndex((p) => p.name.toLowerCase() === s.toLowerCase());
|
|
326
|
+
if (byName >= 0) return byName;
|
|
327
|
+
const n = Number(s);
|
|
328
|
+
return Number.isInteger(n) && n >= 1 && n <= file.pages.length ? n - 1 : 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/compile.ts
|
|
332
|
+
var LOADER = `;(function () {
|
|
333
|
+
var data = DOTTIE_DATA, options = DOTTIE_OPTIONS;
|
|
334
|
+
var script = document.currentScript;
|
|
335
|
+
function attr(el, name, value) { el.setAttribute(name.replace(/[A-Z]/g, function (c) { return "-" + c.toLowerCase(); }), value); }
|
|
336
|
+
function mount() {
|
|
337
|
+
var player = document.createElement("dottie-player"), k;
|
|
338
|
+
for (k in options) attr(player, k, options[k]);
|
|
339
|
+
if (script) for (k in script.dataset) if (k !== "target") attr(player, k, script.dataset[k]);
|
|
340
|
+
var target = script && script.dataset.target ? document.querySelector(script.dataset.target) : null;
|
|
341
|
+
if (target) target.appendChild(player);
|
|
342
|
+
else if (script && script.parentNode && script.parentNode !== document.head) script.parentNode.insertBefore(player, script);
|
|
343
|
+
else document.body.appendChild(player);
|
|
344
|
+
player.setFile(window.Dottie.parseDottie(data));
|
|
345
|
+
}
|
|
346
|
+
if (document.readyState === "loading" && (!script || script.parentNode === document.head || script.dataset.target)) {
|
|
347
|
+
document.addEventListener("DOMContentLoaded", mount);
|
|
348
|
+
} else {
|
|
349
|
+
mount();
|
|
350
|
+
}
|
|
351
|
+
})();
|
|
352
|
+
`;
|
|
353
|
+
function scriptJSON(value) {
|
|
354
|
+
return JSON.stringify(value).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
355
|
+
}
|
|
356
|
+
function playerAttributes(options) {
|
|
357
|
+
const out = {};
|
|
358
|
+
if (options.at !== void 0) out.at = String(options.at);
|
|
359
|
+
if (options.clean) out.clean = "";
|
|
360
|
+
if (options.noControls) out["no-controls"] = "";
|
|
361
|
+
if (options.noAutoplay) out["no-autoplay"] = "";
|
|
362
|
+
if (options.noLoop) out["no-loop"] = "";
|
|
363
|
+
if (options.label !== void 0) out.label = options.label;
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
function compile(source, runtime, options = {}) {
|
|
367
|
+
let raw = source;
|
|
368
|
+
if (typeof source === "string") {
|
|
369
|
+
try {
|
|
370
|
+
raw = JSON.parse(source);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
throw new DottieFormatError(`Not JSON: ${e.message}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const file = parseDottie(raw);
|
|
376
|
+
const json = raw;
|
|
377
|
+
const i = pageIndex(file, options.page === void 0 ? null : String(options.page));
|
|
378
|
+
const page = json.pages[i];
|
|
379
|
+
const refs = /* @__PURE__ */ new Set();
|
|
380
|
+
JSON.stringify(page, (k, v) => {
|
|
381
|
+
if (k === "ref" && typeof v === "string") refs.add(v);
|
|
382
|
+
return v;
|
|
383
|
+
});
|
|
384
|
+
const images = Object.fromEntries(Object.entries(json.images ?? {}).filter(([k]) => refs.has(k)));
|
|
385
|
+
const data = { version: json.version, pages: [page] };
|
|
386
|
+
if (Object.keys(images).length) data.images = images;
|
|
387
|
+
return runtime.trimEnd() + "\n" + LOADER.replace("DOTTIE_DATA", () => scriptJSON(data)).replace("DOTTIE_OPTIONS", () => scriptJSON(playerAttributes(options)));
|
|
388
|
+
}
|
|
389
|
+
export {
|
|
390
|
+
LOADER,
|
|
391
|
+
compile,
|
|
392
|
+
playerAttributes,
|
|
393
|
+
scriptJSON
|
|
394
|
+
};
|