@phin-tech/dottie 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,3018 @@
1
+ /*! dottie.js 0.1.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
+ var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
4
+ var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
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"];
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"],
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"],
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 BOIL_DRAWINGS = 3;
54
+ var black = { r: 0.12, g: 0.12, b: 0.13, a: 1 };
55
+ var clear = { r: 0, g: 0, b: 0, a: 0 };
56
+ var inkOnDark = { r: 0.93, g: 0.93, b: 0.94, a: 1 };
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
+ };
112
+ }
113
+ endpoint(v) {
114
+ const e = isObj(v) ? v : {};
115
+ const port = str(e.port);
116
+ return {
117
+ elementID: uuid(e.elementID) ?? null,
118
+ port: port && ["top", "right", "bottom", "left"].includes(port) ? port : null,
119
+ x: num(e.x) ?? 0,
120
+ y: num(e.y) ?? 0
121
+ };
122
+ }
123
+ connector(v) {
124
+ if (!isObj(v)) return null;
125
+ this.fields(v, "connector");
126
+ const head = (h, d) => {
127
+ const s = str(h);
128
+ return s && ["none", "arrow", "triangle", "dot", "bar"].includes(s) ? s : d;
129
+ };
130
+ const routing = str(v.routing);
131
+ return {
132
+ start: this.endpoint(v.start),
133
+ end: this.endpoint(v.end),
134
+ routing: routing && ["straight", "curved", "elbow"].includes(routing) ? routing : "straight",
135
+ startHead: head(v.startHead, "none"),
136
+ endHead: head(v.endHead, "arrow")
137
+ };
138
+ }
139
+ image(v, table) {
140
+ if (!isObj(v)) return null;
141
+ const ref = str(v.ref);
142
+ if (ref !== void 0) return table[ref] ?? { data: "", format: "png" };
143
+ const data = str(v.data);
144
+ return data === void 0 ? null : this.picture(data, str(v.format));
145
+ }
146
+ /** A picture's bytes, only if they really are base64: they go into an attribute of the drawn SVG. */
147
+ picture(data, format) {
148
+ if (!BASE64.test(data)) {
149
+ this.warn("Skipped a picture whose data isn't base64");
150
+ return { data: "", format: "png" };
151
+ }
152
+ return { data: data.replace(/\s+/g, ""), format: format === "jpeg" ? "jpeg" : "png" };
153
+ }
154
+ element(v, images) {
155
+ if (!isObj(v)) return null;
156
+ this.fields(v, "element");
157
+ const id = uuid(v.id), kind = str(v.kind), layerID = uuid(v.layerID);
158
+ if (!id || !kind || !layerID) {
159
+ this.warn("Skipped an element without an id, kind or layer");
160
+ return null;
161
+ }
162
+ if (!KINDS.includes(kind)) this.warn(`Unknown element kind "${kind}", drawn as a placeholder`);
163
+ 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;
164
+ return {
165
+ id,
166
+ kind,
167
+ layerID,
168
+ x: num(v.x) ?? 0,
169
+ y: num(v.y) ?? 0,
170
+ width: num(v.width) ?? 0,
171
+ height: num(v.height) ?? 0,
172
+ style: this.style(v.style),
173
+ seed: (num(v.seed) ?? 1) >>> 0,
174
+ text: str(v.text) ?? null,
175
+ connector: this.connector(v.connector),
176
+ groupID: uuid(v.groupID) ?? null,
177
+ points: Array.isArray(v.points) ? v.points.filter(isObj).map((p) => ({ x: num(p.x) ?? 0, y: num(p.y) ?? 0 })) : null,
178
+ path: Array.isArray(v.path) ? v.path.filter((c) => typeof c === "number") : null,
179
+ subpaths,
180
+ evenOdd: bool(v.evenOdd) ?? null,
181
+ rotation: num(v.rotation) ?? null,
182
+ link: uuid(v.link) ?? null,
183
+ anchor: str(v.anchor) ?? null,
184
+ image: this.image(v.image, images),
185
+ reveal: num(v.reveal) ?? null
186
+ };
187
+ }
188
+ ease(v) {
189
+ const s = str(v);
190
+ if (s === void 0) return null;
191
+ if (!EASES.includes(s)) {
192
+ this.warn(`Unknown ease "${s}", played linear`);
193
+ return null;
194
+ }
195
+ return s;
196
+ }
197
+ keyframe(v) {
198
+ if (!isObj(v)) return null;
199
+ this.fields(v, "keyframe");
200
+ const t = num(v.t);
201
+ if (t === void 0) return null;
202
+ const k = { t };
203
+ for (const p of ["x", "y", "width", "height", "rotation", "strokeWidth", "reveal", "opacity"]) {
204
+ const n = num(v[p]);
205
+ if (n !== void 0) k[p] = n;
206
+ }
207
+ const stroke = rgba(v.stroke), fill = rgba(v.fill), visible = bool(v.visible), ease = this.ease(v.ease);
208
+ if (stroke) k.stroke = stroke;
209
+ if (fill) k.fill = fill;
210
+ if (visible !== void 0) k.visible = visible;
211
+ if (ease) k.ease = ease;
212
+ return k;
213
+ }
214
+ track(v) {
215
+ if (!isObj(v)) return null;
216
+ this.fields(v, "track");
217
+ const element = uuid(v.element);
218
+ if (!element) return null;
219
+ const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
220
+ stableSortBy(keys, (k) => k.t);
221
+ const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
222
+ const path = uuid(f.path), start = num(f.start), end = num(f.end);
223
+ if (!path || start === void 0 || end === void 0 || start < 0 || end > MAX_DURATION) return [];
224
+ return [{ path, start, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
225
+ }) : null;
226
+ const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
227
+ const start = num(r.start), end = num(r.end);
228
+ if (start === void 0 || end === void 0) return [];
229
+ const count = num(r.count);
230
+ const rep = { start, end, count: count === void 0 ? null : Math.trunc(count) };
231
+ return repeatIsValid(rep) ? [rep] : [];
232
+ }) : null;
233
+ return { element, keys, boil: bool(v.boil) ?? null, follow, repeats };
234
+ }
235
+ timeline(v) {
236
+ if (!isObj(v)) return null;
237
+ this.fields(v, "timeline");
238
+ const boil = num(v.boil);
239
+ const camera = Array.isArray(v.camera) ? v.camera.filter(isObj).flatMap((c) => {
240
+ const t = num(c.t), x = num(c.x), y = num(c.y), width = num(c.width), height = num(c.height);
241
+ if (t === void 0 || x === void 0 || y === void 0 || width === void 0 || height === void 0) return [];
242
+ if (!(width > 1 && height > 1 && t >= 0)) return [];
243
+ return [{ t, x, y, width, height, ease: this.ease(c.ease) }];
244
+ }) : null;
245
+ if (camera) stableSortBy(camera, (c) => c.t);
246
+ const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
247
+ const t = num(m.t);
248
+ return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
249
+ }) : null;
250
+ if (markers) stableSortBy(markers, (m) => m.t);
251
+ const poster = num(v.poster);
252
+ return {
253
+ fps: clampFPS(num(v.fps) ?? 12),
254
+ duration: clampTime(num(v.duration) ?? 0),
255
+ boil: boil !== void 0 && Number.isInteger(boil) && boil > 0 ? Math.min(boil, 1e4) : null,
256
+ settle: bool(v.settle) ?? null,
257
+ poster: poster === void 0 ? null : clampTime(poster),
258
+ tracks: (Array.isArray(v.tracks) ? v.tracks : []).map((t) => this.track(t)).filter((t) => t !== null),
259
+ camera,
260
+ markers
261
+ };
262
+ }
263
+ page(v, images, n) {
264
+ if (!isObj(v)) throw new DottieFormatError(`Page ${n} is not an object`);
265
+ this.fields(v, "page");
266
+ const layers = (Array.isArray(v.layers) ? v.layers : []).filter(isObj).flatMap((l) => {
267
+ const id = uuid(l.id);
268
+ return id ? [{ id, name: str(l.name) ?? "", visible: bool(l.visible) ?? true, locked: bool(l.locked) ?? false }] : [];
269
+ });
270
+ if (layers.length === 0) {
271
+ this.warn(`Page ${n} has no layers`);
272
+ layers.push({ id: "LAYER", name: "Layer 1", visible: true, locked: false });
273
+ }
274
+ return {
275
+ id: uuid(v.id) ?? `PAGE-${n}`,
276
+ name: str(v.name) ?? `Page ${n}`,
277
+ layers,
278
+ elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
279
+ theme: str(v.theme) ?? null,
280
+ timeline: this.timeline(v.timeline)
281
+ };
282
+ }
283
+ };
284
+ var BASE64 = /^[A-Za-z0-9+/=\s]*$/;
285
+ function repeatIsValid(r) {
286
+ 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);
287
+ }
288
+ function stableSortBy(a, key) {
289
+ const indexed = a.map((v, i) => [v, i]);
290
+ indexed.sort((p, q) => key(p[0]) - key(q[0]) || p[1] - q[1]);
291
+ for (let i = 0; i < a.length; i++) a[i] = indexed[i][0];
292
+ }
293
+ function parseDottie(source) {
294
+ let json = source;
295
+ if (typeof source === "string") {
296
+ try {
297
+ json = JSON.parse(source);
298
+ } catch (e) {
299
+ throw new DottieFormatError(`Not JSON: ${e.message}`);
300
+ }
301
+ }
302
+ if (!isObj(json)) throw new DottieFormatError("Not a Dottie drawing");
303
+ const version = num(json.version);
304
+ if (version === void 0) throw new DottieFormatError("Not a Dottie drawing (no version)");
305
+ if (!Array.isArray(json.pages) || json.pages.length === 0) throw new DottieFormatError("A drawing needs at least one page");
306
+ const reader = new Reader();
307
+ reader.fields(json, "file");
308
+ if (version > SUPPORTED_VERSION) reader.warn(`File version ${version} is newer than this player knows (${SUPPORTED_VERSION}); some things may not show`);
309
+ const images = {};
310
+ if (isObj(json.images)) {
311
+ for (const [k, v] of Object.entries(json.images)) {
312
+ if (isObj(v) && typeof v.data === "string") images[k] = reader.picture(v.data, str(v.format));
313
+ }
314
+ }
315
+ const pages = json.pages.map((p, i) => reader.page(p, images, i + 1));
316
+ if (new Set(pages.map((p) => p.id)).size !== pages.length) throw new DottieFormatError("Two pages share an id");
317
+ return { version, pages, warnings: reader.warnings };
318
+ }
319
+ function luminance(c) {
320
+ return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
321
+ }
322
+ function sameColor(a, b) {
323
+ return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a;
324
+ }
325
+ function isAutomaticInk(c) {
326
+ return sameColor(c, black) || sameColor(c, inkOnDark);
327
+ }
328
+ function cloneElement(e) {
329
+ return {
330
+ ...e,
331
+ style: { ...e.style, stroke: { ...e.style.stroke }, fill: { ...e.style.fill } },
332
+ connector: e.connector ? { ...e.connector, start: { ...e.connector.start }, end: { ...e.connector.end } } : null
333
+ };
334
+ }
335
+ function orderedElements(page) {
336
+ if (page.layers.length <= 1) return page.elements;
337
+ const rank = /* @__PURE__ */ new Map();
338
+ page.layers.forEach((l, i) => {
339
+ if (!rank.has(l.id)) rank.set(l.id, i);
340
+ });
341
+ const buckets = page.layers.map(() => []);
342
+ for (const e of page.elements) buckets[rank.get(e.layerID) ?? 0].push(e);
343
+ return buckets.flat();
344
+ }
345
+ function elementIndex(page) {
346
+ const m = /* @__PURE__ */ new Map();
347
+ for (const e of page.elements) if (!m.has(e.id)) m.set(e.id, e);
348
+ return m;
349
+ }
350
+
351
+ // src/path.ts
352
+ var KAPPA = 0.5522847498307936;
353
+ var Path = class _Path {
354
+ constructor() {
355
+ __publicField(this, "segments", []);
356
+ }
357
+ get isEmpty() {
358
+ return this.segments.length === 0;
359
+ }
360
+ moveTo(p) {
361
+ this.segments.push({ op: "M", p });
362
+ return this;
363
+ }
364
+ lineTo(p) {
365
+ this.segments.push({ op: "L", p });
366
+ return this;
367
+ }
368
+ quadTo(p, c) {
369
+ this.segments.push({ op: "Q", c, p });
370
+ return this;
371
+ }
372
+ curveTo(p, c1, c2) {
373
+ this.segments.push({ op: "C", c1, c2, p });
374
+ return this;
375
+ }
376
+ close() {
377
+ this.segments.push({ op: "Z" });
378
+ return this;
379
+ }
380
+ addPath(other) {
381
+ this.segments.push(...other.segments);
382
+ return this;
383
+ }
384
+ /** move, then a line to each point (CGMutablePath.addLines). */
385
+ addLines(pts) {
386
+ pts.forEach((p, i) => i === 0 ? this.moveTo(p) : this.lineTo(p));
387
+ return this;
388
+ }
389
+ /** CGPath(rect:): clockwise (on screen) from the top-left corner. */
390
+ static rect(r) {
391
+ return new _Path().addLines([
392
+ { x: r.x, y: r.y },
393
+ { x: r.x + r.width, y: r.y },
394
+ { x: r.x + r.width, y: r.y + r.height },
395
+ { x: r.x, y: r.y + r.height }
396
+ ]).close();
397
+ }
398
+ /** CGPath(ellipseIn:): four cubics clockwise (on screen) from the right-hand point. */
399
+ addEllipse(r) {
400
+ const cx = r.x + r.width / 2, cy = r.y + r.height / 2, rx = r.width / 2, ry = r.height / 2;
401
+ const kx = rx * KAPPA, ky = ry * KAPPA;
402
+ this.moveTo({ x: cx + rx, y: cy });
403
+ this.curveTo({ x: cx, y: cy + ry }, { x: cx + rx, y: cy + ky }, { x: cx + kx, y: cy + ry });
404
+ this.curveTo({ x: cx - rx, y: cy }, { x: cx - kx, y: cy + ry }, { x: cx - rx, y: cy + ky });
405
+ this.curveTo({ x: cx, y: cy - ry }, { x: cx - rx, y: cy - ky }, { x: cx - kx, y: cy - ry });
406
+ this.curveTo({ x: cx + rx, y: cy }, { x: cx + kx, y: cy - ry }, { x: cx + rx, y: cy - ky });
407
+ return this.close();
408
+ }
409
+ static ellipse(r) {
410
+ return new _Path().addEllipse(r);
411
+ }
412
+ translated(dx, dy) {
413
+ const t = (p) => ({ x: p.x + dx, y: p.y + dy });
414
+ const out = new _Path();
415
+ out.segments = this.segments.map((s) => {
416
+ switch (s.op) {
417
+ case "M":
418
+ case "L":
419
+ return { op: s.op, p: t(s.p) };
420
+ case "Q":
421
+ return { op: "Q", c: t(s.c), p: t(s.p) };
422
+ case "C":
423
+ return { op: "C", c1: t(s.c1), c2: t(s.c2), p: t(s.p) };
424
+ case "Z":
425
+ return s;
426
+ }
427
+ });
428
+ return out;
429
+ }
430
+ /** Every contour as points, curves flattened into short segments (CGPath.contours). */
431
+ contours() {
432
+ const out = [];
433
+ let current = [];
434
+ let start = { x: 0, y: 0 };
435
+ for (const s of this.segments) {
436
+ const last = current.length ? current[current.length - 1] : start;
437
+ switch (s.op) {
438
+ case "M":
439
+ if (current.length > 1) out.push(current);
440
+ start = s.p;
441
+ current = [s.p];
442
+ break;
443
+ case "L":
444
+ current.push(s.p);
445
+ break;
446
+ case "Q":
447
+ for (let i = 1; i <= 12; i++) {
448
+ const t = i / 12, u = 1 - t;
449
+ current.push({ x: u * u * last.x + 2 * u * t * s.c.x + t * t * s.p.x, y: u * u * last.y + 2 * u * t * s.c.y + t * t * s.p.y });
450
+ }
451
+ break;
452
+ case "C":
453
+ for (let i = 1; i <= 16; i++) {
454
+ const t = i / 16, u = 1 - t;
455
+ current.push({
456
+ x: u * u * u * last.x + 3 * u * u * t * s.c1.x + 3 * u * t * t * s.c2.x + t * t * t * s.p.x,
457
+ y: u * u * u * last.y + 3 * u * u * t * s.c1.y + 3 * u * t * t * s.c2.y + t * t * t * s.p.y
458
+ });
459
+ }
460
+ break;
461
+ case "Z":
462
+ current.push(start);
463
+ if (current.length > 1) out.push(current);
464
+ current = [start];
465
+ break;
466
+ }
467
+ }
468
+ if (current.length > 1) out.push(current);
469
+ return out;
470
+ }
471
+ /** The first `fraction` of the path by length, contours in drawing order: a stroke being drawn on. */
472
+ trimmed(fraction) {
473
+ const lines = this.contours().map((c) => new Polyline(c));
474
+ let left = Math.min(Math.max(fraction, 0), 1) * lines.reduce((s, l) => s + l.length, 0);
475
+ const out = new _Path();
476
+ for (const line2 of lines) {
477
+ if (!(left > 0)) break;
478
+ out.moveTo(line2.points[0]);
479
+ for (let i = 1; i < line2.points.length; i++) {
480
+ const seg = line2.lengths[i] - line2.lengths[i - 1];
481
+ if (seg <= left) {
482
+ out.lineTo(line2.points[i]);
483
+ left -= seg;
484
+ continue;
485
+ }
486
+ const a = line2.points[i - 1], b = line2.points[i], f = left / seg;
487
+ out.lineTo({ x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f });
488
+ left = 0;
489
+ break;
490
+ }
491
+ }
492
+ return out;
493
+ }
494
+ /** SVG path data, as Dottie's SVG export writes it. */
495
+ data() {
496
+ const d = [];
497
+ for (const s of this.segments) {
498
+ switch (s.op) {
499
+ case "M":
500
+ d.push(`M${fmt(s.p.x)} ${fmt(s.p.y)}`);
501
+ break;
502
+ case "L":
503
+ d.push(`L${fmt(s.p.x)} ${fmt(s.p.y)}`);
504
+ break;
505
+ case "Q":
506
+ d.push(`Q${fmt(s.c.x)} ${fmt(s.c.y)} ${fmt(s.p.x)} ${fmt(s.p.y)}`);
507
+ break;
508
+ case "C":
509
+ d.push(`C${fmt(s.c1.x)} ${fmt(s.c1.y)} ${fmt(s.c2.x)} ${fmt(s.c2.y)} ${fmt(s.p.x)} ${fmt(s.p.y)}`);
510
+ break;
511
+ case "Z":
512
+ d.push("Z");
513
+ break;
514
+ }
515
+ }
516
+ return d.join(" ");
517
+ }
518
+ };
519
+ function roundHalfAway(v) {
520
+ return v < 0 ? -Math.round(-v) : Math.round(v);
521
+ }
522
+ function fmt(v) {
523
+ const r = roundHalfAway(v * 100) / 100;
524
+ if (Number.isInteger(r)) return String(r === 0 ? 0 : r);
525
+ return String(r);
526
+ }
527
+ var Polyline = class {
528
+ constructor(points) {
529
+ __publicField(this, "points");
530
+ __publicField(this, "lengths");
531
+ this.points = points;
532
+ let run = 0;
533
+ this.lengths = points.map((p, i) => {
534
+ if (i > 0) run += Math.hypot(p.x - points[i - 1].x, p.y - points[i - 1].y);
535
+ return run;
536
+ });
537
+ }
538
+ get length() {
539
+ return this.lengths.length ? this.lengths[this.lengths.length - 1] : 0;
540
+ }
541
+ /** The point `u` (0...1) of the way along, by distance. */
542
+ point(u) {
543
+ if (this.points.length <= 1 || !(this.length > 0)) return this.points[0] ?? { x: 0, y: 0 };
544
+ const d = Math.min(Math.max(u, 0), 1) * this.length;
545
+ let i = 1;
546
+ while (i < this.lengths.length - 1 && this.lengths[i] < d) i++;
547
+ const seg = this.lengths[i] - this.lengths[i - 1];
548
+ const f = seg > 0 ? (d - this.lengths[i - 1]) / seg : 0;
549
+ const a = this.points[i - 1], b = this.points[i];
550
+ return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
551
+ }
552
+ };
553
+ var midX = (r) => r.x + r.width / 2;
554
+ var midY = (r) => r.y + r.height / 2;
555
+ var maxX = (r) => r.x + r.width;
556
+ var maxY = (r) => r.y + r.height;
557
+ function union(a, b) {
558
+ if (!a) return { ...b };
559
+ const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
560
+ return { x, y, width: Math.max(maxX(a), maxX(b)) - x, height: Math.max(maxY(a), maxY(b)) - y };
561
+ }
562
+ function inset(r, dx, dy) {
563
+ return { x: r.x + dx, y: r.y + dy, width: r.width - 2 * dx, height: r.height - 2 * dy };
564
+ }
565
+ function rotate(p, c, a) {
566
+ if (a === 0) return p;
567
+ const dx = p.x - c.x, dy = p.y - c.y;
568
+ return { x: c.x + dx * Math.cos(a) - dy * Math.sin(a), y: c.y + dx * Math.sin(a) + dy * Math.cos(a) };
569
+ }
570
+ function bezierPoint(b, t) {
571
+ const s = 1 - t;
572
+ return {
573
+ x: b[0].x * (s * s * s) + b[1].x * (3 * t * s * s) + b[2].x * (3 * t * t * s) + b[3].x * (t * t * t),
574
+ y: b[0].y * (s * s * s) + b[1].y * (3 * t * s * s) + b[2].y * (3 * t * t * s) + b[3].y * (t * t * t)
575
+ };
576
+ }
577
+ function flatten(cp, maxStep) {
578
+ const out = [cp[0]];
579
+ for (let s = 0; s < Math.floor((cp.length - 1) / 3); s++) {
580
+ const seg = cp.slice(3 * s, 3 * s + 4);
581
+ let hull = 0;
582
+ for (let i = 1; i < seg.length; i++) hull += Math.hypot(seg[i].x - seg[i - 1].x, seg[i].y - seg[i - 1].y);
583
+ const n = Math.min(400, Math.max(4, Math.ceil(hull / Math.max(maxStep, 0.5))));
584
+ for (let k = 1; k <= n; k++) out.push(bezierPoint(seg, k / n));
585
+ }
586
+ return out;
587
+ }
588
+
589
+ // src/element.ts
590
+ var frame = (e) => ({ x: e.x, y: e.y, width: e.width, height: e.height });
591
+ var angle = (e) => e.rotation ?? 0;
592
+ var center = (e) => ({ x: e.x + e.width / 2, y: e.y + e.height / 2 });
593
+ var rotated = (e, p) => rotate(p, center(e), angle(e));
594
+ var unrotated = (e, p) => rotate(p, center(e), -angle(e));
595
+ function rotatedBounds(e) {
596
+ const r = frame(e);
597
+ if (angle(e) === 0) return r;
598
+ const corners2 = [
599
+ { x: r.x, y: r.y },
600
+ { x: maxX(r), y: r.y },
601
+ { x: maxX(r), y: maxY(r) },
602
+ { x: r.x, y: maxY(r) }
603
+ ].map((p) => rotated(e, p));
604
+ const xs = corners2.map((p) => p.x), ys = corners2.map((p) => p.y);
605
+ const x = Math.min(...xs), y = Math.min(...ys);
606
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
607
+ }
608
+ function resolvingInk(e, ink) {
609
+ if (!isAutomaticInk(e.style.stroke)) return e;
610
+ const copy = cloneElement(e);
611
+ const covers = e.style.fillStyle === "solid" || e.style.fillStyle === "grid";
612
+ const onLightFill = covers && e.style.fill.a >= 0.5 && luminance(e.style.fill) > 0.5;
613
+ copy.style.stroke = luminance(ink) > 0.5 && onLightFill ? { ...black } : { ...ink };
614
+ return copy;
615
+ }
616
+ function shapeContours(e) {
617
+ if (e.kind !== "path" || !e.subpaths) return [];
618
+ const out = [];
619
+ for (const s of e.subpaths) {
620
+ if (s.coords.length < 8 || (s.coords.length - 2) % 6 !== 0) continue;
621
+ const pts = [];
622
+ for (let i = 0; i < s.coords.length; i += 2) pts.push({ x: e.x + s.coords[i] * e.width, y: e.y + s.coords[i + 1] * e.height });
623
+ out.push({ closed: s.closed, points: pts });
624
+ }
625
+ return out;
626
+ }
627
+ function shapePath(e) {
628
+ const path = new Path();
629
+ for (const c of shapeContours(e)) {
630
+ path.moveTo(c.points[0]);
631
+ for (let k = 0; k < (c.points.length - 1) / 3; k++) {
632
+ path.curveTo(c.points[3 * k + 3], c.points[3 * k + 1], c.points[3 * k + 2]);
633
+ }
634
+ if (c.closed) path.close();
635
+ }
636
+ return path;
637
+ }
638
+
639
+ // src/connector.ts
640
+ function portPoint(shape, port) {
641
+ const r = frame(shape);
642
+ const p = port === "top" ? { x: midX(r), y: r.y } : port === "right" ? { x: maxX(r), y: midY(r) } : port === "bottom" ? { x: midX(r), y: maxY(r) } : { x: r.x, y: midY(r) };
643
+ return rotated(shape, p);
644
+ }
645
+ function edgePoint(shape, toward) {
646
+ const r = frame(shape);
647
+ const c = center(shape);
648
+ const target = unrotated(shape, toward);
649
+ const dx = target.x - c.x, dy = target.y - c.y;
650
+ if (!((dx !== 0 || dy !== 0) && r.width > 0 && r.height > 0)) return c;
651
+ const hw = r.width / 2, hh = r.height / 2;
652
+ let t;
653
+ if (shape.kind === "ellipse") t = 1 / Math.sqrt(dx * dx / (hw * hw) + dy * dy / (hh * hh));
654
+ else if (shape.kind === "diamond") t = 1 / (Math.abs(dx) / hw + Math.abs(dy) / hh);
655
+ else t = Math.min(dx !== 0 ? hw / Math.abs(dx) : Infinity, dy !== 0 ? hh / Math.abs(dy) : Infinity);
656
+ return rotated(shape, { x: c.x + dx * t, y: c.y + dy * t });
657
+ }
658
+ function reference(ep, index) {
659
+ const s = ep.elementID ? index.get(ep.elementID) : void 0;
660
+ return s ? center(s) : { x: ep.x, y: ep.y };
661
+ }
662
+ function resolve(ep, other, index) {
663
+ const s = ep.elementID ? index.get(ep.elementID) : void 0;
664
+ if (!s) return { x: ep.x, y: ep.y };
665
+ return ep.port ? portPoint(s, ep.port) : edgePoint(s, other);
666
+ }
667
+ function endpoints(e, index) {
668
+ const c = e.connector;
669
+ if (!c) return [{ x: 0, y: 0 }, { x: 0, y: 0 }];
670
+ return [resolve(c.start, reference(c.end, index), index), resolve(c.end, reference(c.start, index), index)];
671
+ }
672
+ function routePoints(r) {
673
+ if (r.kind === "polyline") return r.points;
674
+ const out = [];
675
+ for (let i = 0; i <= 24; i++) {
676
+ const t = i / 24, u = 1 - t;
677
+ out.push({
678
+ x: u * u * u * r.a.x + 3 * u * u * t * r.b.x + 3 * u * t * t * r.c.x + t * t * t * r.d.x,
679
+ y: u * u * u * r.a.y + 3 * u * u * t * r.b.y + 3 * u * t * t * r.c.y + t * t * t * r.d.y
680
+ });
681
+ }
682
+ return out;
683
+ }
684
+ function route(e, index) {
685
+ const c = e.connector;
686
+ if (!c) return { kind: "polyline", points: [] };
687
+ const [a, b] = endpoints(e, index);
688
+ const dx = b.x - a.x, dy = b.y - a.y;
689
+ const horizontal = Math.abs(dx) >= Math.abs(dy);
690
+ switch (c.routing) {
691
+ case "straight":
692
+ return { kind: "polyline", points: [a, b] };
693
+ case "curved":
694
+ if (horizontal) return { kind: "cubic", a, b: { x: a.x + dx / 2, y: a.y }, c: { x: b.x - dx / 2, y: b.y }, d: b };
695
+ return { kind: "cubic", a, b: { x: a.x, y: a.y + dy / 2 }, c: { x: b.x, y: b.y - dy / 2 }, d: b };
696
+ case "elbow": {
697
+ const vertical = (p) => p === null ? null : p === "top" || p === "bottom";
698
+ const startV = vertical(c.start.port) ?? !horizontal;
699
+ const endV = vertical(c.end.port) ?? !horizontal;
700
+ if (startV && endV) {
701
+ const my = a.y + dy / 2;
702
+ return { kind: "polyline", points: [a, { x: a.x, y: my }, { x: b.x, y: my }, b] };
703
+ }
704
+ if (!startV && !endV) {
705
+ const mx = a.x + dx / 2;
706
+ return { kind: "polyline", points: [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b] };
707
+ }
708
+ if (startV) return { kind: "polyline", points: [a, { x: a.x, y: b.y }, b] };
709
+ return { kind: "polyline", points: [a, { x: b.x, y: a.y }, b] };
710
+ }
711
+ }
712
+ }
713
+ function arrowhead(tip, from, size) {
714
+ const a = Math.atan2(tip.y - from.y, tip.x - from.x);
715
+ const spread = Math.PI / 7;
716
+ const wing = (s) => ({ x: tip.x - size * Math.cos(a + s), y: tip.y - size * Math.sin(a + s) });
717
+ return [wing(spread), wing(-spread)];
718
+ }
719
+ function connectorBounds(e, index) {
720
+ const pts = routePoints(route(e, index));
721
+ if (pts.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
722
+ let x0 = pts[0].x, y0 = pts[0].y, x1 = x0, y1 = y0;
723
+ for (const p of pts) {
724
+ x0 = Math.min(x0, p.x);
725
+ y0 = Math.min(y0, p.y);
726
+ x1 = Math.max(x1, p.x);
727
+ y1 = Math.max(y1, p.y);
728
+ }
729
+ return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
730
+ }
731
+ function midpoint(e, index) {
732
+ const pts = routePoints(route(e, index));
733
+ if (pts.length <= 1) return pts[0] ?? { x: 0, y: 0 };
734
+ const lengths = [];
735
+ let total = 0;
736
+ for (let i = 0; i < pts.length - 1; i++) {
737
+ const l = Math.hypot(pts[i + 1].x - pts[i].x, pts[i + 1].y - pts[i].y);
738
+ lengths.push(l);
739
+ total += l;
740
+ }
741
+ let remaining = total / 2;
742
+ for (let i = 0; i < lengths.length; i++) {
743
+ const l = lengths[i];
744
+ if (remaining <= l || i === lengths.length - 1) {
745
+ const t = l === 0 ? 0 : remaining / l;
746
+ return { x: pts[i].x + (pts[i + 1].x - pts[i].x) * t, y: pts[i].y + (pts[i + 1].y - pts[i].y) * t };
747
+ }
748
+ remaining -= l;
749
+ }
750
+ return pts[0];
751
+ }
752
+ function indexOf(page) {
753
+ const m = /* @__PURE__ */ new Map();
754
+ for (const e of page.elements) if (!m.has(e.id)) m.set(e.id, e);
755
+ return m;
756
+ }
757
+
758
+ // src/rng.ts
759
+ var GOLDEN_HI = 2654435769;
760
+ var GOLDEN_LO = 2135587861;
761
+ var M1_HI = 3210233709;
762
+ var M1_LO = 484763065;
763
+ var M2_HI = 2496678331;
764
+ var M2_LO = 321982955;
765
+ function add(a, bh, bl) {
766
+ const lo = a[1] + bl >>> 0;
767
+ const carry = lo < a[1] ? 1 : 0;
768
+ return [a[0] + bh + carry >>> 0, lo];
769
+ }
770
+ function mul32(a, b) {
771
+ const a0 = a & 65535, a1 = a >>> 16, b0 = b & 65535, b1 = b >>> 16;
772
+ const p00 = a0 * b0, p01 = a0 * b1, p10 = a1 * b0, p11 = a1 * b1;
773
+ const mid = p01 + p10;
774
+ const lo = p00 + mid % 65536 * 65536;
775
+ const hi = p11 + Math.floor(mid / 65536) + Math.floor(lo / 4294967296);
776
+ return [hi >>> 0, lo >>> 0];
777
+ }
778
+ function mul(a, bh, bl) {
779
+ const low = mul32(a[1], bl);
780
+ const hi = low[0] + Math.imul(a[0], bl) + Math.imul(a[1], bh) >>> 0;
781
+ return [hi, low[1]];
782
+ }
783
+ function xorShift(a, s) {
784
+ const lo = (a[1] >>> s | a[0] << 32 - s) >>> 0;
785
+ return [(a[0] ^ a[0] >>> s) >>> 0, (a[1] ^ lo) >>> 0];
786
+ }
787
+ function mix(z) {
788
+ z = mul(xorShift(z, 30), M1_HI, M1_LO);
789
+ z = mul(xorShift(z, 27), M2_HI, M2_LO);
790
+ return xorShift(z, 31);
791
+ }
792
+ var SeededRNG = class {
793
+ constructor(seed) {
794
+ __publicField(this, "state");
795
+ this.state = add([0, seed >>> 0], GOLDEN_HI, GOLDEN_LO);
796
+ }
797
+ /** The next 64 bits, as (hi, lo). */
798
+ next() {
799
+ this.state = add(this.state, GOLDEN_HI, GOLDEN_LO);
800
+ return mix(this.state);
801
+ }
802
+ /** The top 53 bits of `next()`, exactly. */
803
+ top53() {
804
+ const [hi, lo] = this.next();
805
+ return hi * 2097152 + (lo >>> 11);
806
+ }
807
+ /** Uniform in -1...1. */
808
+ signed() {
809
+ return Math.floor(this.top53()) / 2 ** 52 - 1;
810
+ }
811
+ /** Uniform in 0...1. */
812
+ unit() {
813
+ return this.top53() / 2 ** 53;
814
+ }
815
+ };
816
+ function boiled(seed, step) {
817
+ let z = add([0, seed >>> 0], ...mulConst(step));
818
+ z = mix(z);
819
+ return Math.max(1, z[1] >>> 0);
820
+ }
821
+ function mulConst(step) {
822
+ const p = mul([0, step >>> 0], GOLDEN_HI, GOLDEN_LO);
823
+ return [p[0], p[1]];
824
+ }
825
+
826
+ // src/rough.ts
827
+ var offset = (rng, amount, roughness) => rng.signed() * amount * roughness;
828
+ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
829
+ const length = Math.hypot(b.x - a.x, b.y - a.y);
830
+ if (!(length > 0.5)) return;
831
+ const gain = length < 200 ? 1 : length > 500 ? 0.4 : -16668e-7 * length + 1.233334;
832
+ const maxOff = length < 20 ? length / 10 : 2;
833
+ for (let pass = 0; pass < 2; pass++) {
834
+ const spread = (pass === 0 ? 1 : 0.5) * gain;
835
+ const j = () => offset(rng, maxOff, roughness) * spread;
836
+ const mid = rng.unit() * 0.4 + 0.3;
837
+ const bow = offset(rng, length / 60, roughness) * bowing * gain;
838
+ const dx = b.x - a.x, dy = b.y - a.y;
839
+ const nx = -dy / length, ny = dx / length;
840
+ const start = preserveEnds ? a : (() => {
841
+ const x = a.x + j();
842
+ return { x, y: a.y + j() };
843
+ })();
844
+ const end = preserveEnds ? b : (() => {
845
+ const x = b.x + j();
846
+ return { x, y: b.y + j() };
847
+ })();
848
+ const c1x = a.x + dx * mid * 0.66 + nx * bow + j() * 0.5;
849
+ const c1y = a.y + dy * mid * 0.66 + ny * bow + j() * 0.5;
850
+ const c2x = a.x + dx * (0.5 + mid * 0.5) + nx * bow + j() * 0.5;
851
+ const c2y = a.y + dy * (0.5 + mid * 0.5) + ny * bow + j() * 0.5;
852
+ into.moveTo(start);
853
+ into.curveTo(end, { x: c1x, y: c1y }, { x: c2x, y: c2y });
854
+ }
855
+ }
856
+ function polygon(pts, rng, roughness, bowing) {
857
+ const path = new Path();
858
+ for (let i = 0; i < pts.length; i++) line(pts[i], pts[(i + 1) % pts.length], rng, roughness, bowing, false, path);
859
+ return path;
860
+ }
861
+ function ellipse(r, rng, roughness) {
862
+ const path = new Path();
863
+ const cx = r.x + r.width / 2, cy = r.y + r.height / 2;
864
+ const rx = r.width / 2, ry = r.height / 2;
865
+ if (!(rx > 0.5 && ry > 0.5)) return path;
866
+ const n = Math.max(8, Math.min(24, Math.trunc((rx + ry) / 6)));
867
+ const wobble = Math.min(2.5, (rx + ry) / 25) * roughness;
868
+ for (let pass = 0; pass < 2; pass++) {
869
+ const phase = rng.unit() * 2 * Math.PI;
870
+ const sweep = 2 * Math.PI * (pass === 0 ? 1 : 1.04);
871
+ const count = Math.trunc(n * sweep / (2 * Math.PI)) + 1;
872
+ const pts = [];
873
+ for (let i = 0; i < count; i++) {
874
+ const t = phase + sweep * i / n;
875
+ const wx = rng.signed() * wobble, wy = rng.signed() * wobble;
876
+ pts.push({ x: cx + rx * Math.cos(t) + wx, y: cy + ry * Math.sin(t) + wy });
877
+ }
878
+ catmullRom(pts, path);
879
+ }
880
+ return path;
881
+ }
882
+ function catmullRom(p, path) {
883
+ if (p.length <= 2) return;
884
+ path.moveTo(p[0]);
885
+ for (let i = 0; i < p.length - 1; i++) {
886
+ const p0 = p[Math.max(i - 1, 0)], p1 = p[i], p2 = p[i + 1], p3 = p[Math.min(i + 2, p.length - 1)];
887
+ path.curveTo(p2, { x: p1.x + (p2.x - p0.x) / 6, y: p1.y + (p2.y - p0.y) / 6 }, { x: p2.x - (p3.x - p1.x) / 6, y: p2.y - (p3.y - p1.y) / 6 });
888
+ }
889
+ }
890
+ var HACHURE_ANGLE = -41 * Math.PI / 180;
891
+ function hachureLines(polygons, angle2, gap) {
892
+ if (!(gap > 0)) return [];
893
+ const c = Math.cos(angle2), s = Math.sin(angle2);
894
+ const rots = polygons.filter((p) => p.length >= 3).map((poly) => poly.map((p) => ({ x: p.x * c + p.y * s, y: -p.x * s + p.y * c })));
895
+ const all = rots.flat();
896
+ if (all.length === 0) return [];
897
+ let minY = Infinity, maxY2 = -Infinity;
898
+ for (const p of all) {
899
+ minY = Math.min(minY, p.y);
900
+ maxY2 = Math.max(maxY2, p.y);
901
+ }
902
+ const back = (x, y2) => ({ x: x * c - y2 * s, y: x * s + y2 * c });
903
+ const out = [];
904
+ let y = minY + gap / 2;
905
+ while (y < maxY2) {
906
+ const xs = [];
907
+ for (const rot of rots) {
908
+ for (let i = 0; i < rot.length; i++) {
909
+ const a = rot[i], b = rot[(i + 1) % rot.length];
910
+ if (a.y <= y && y < b.y || b.y <= y && y < a.y) xs.push(a.x + (y - a.y) / (b.y - a.y) * (b.x - a.x));
911
+ }
912
+ }
913
+ xs.sort((p, q) => p - q);
914
+ for (let k = 0; k + 1 < xs.length; k += 2) {
915
+ if (xs[k + 1] - xs[k] > 0.5) out.push([back(xs[k], y), back(xs[k + 1], y)]);
916
+ }
917
+ y += gap;
918
+ }
919
+ return out;
920
+ }
921
+
922
+ // src/shapes.ts
923
+ var Cache = class {
924
+ constructor(limit = 6e3) {
925
+ __publicField(this, "map", /* @__PURE__ */ new Map());
926
+ __publicField(this, "limit");
927
+ this.limit = limit;
928
+ }
929
+ get(key, make) {
930
+ const hit = this.map.get(key);
931
+ if (hit !== void 0) return hit;
932
+ const v = make();
933
+ if (this.map.size >= this.limit) this.map.clear();
934
+ this.map.set(key, v);
935
+ return v;
936
+ }
937
+ };
938
+ var shapes = new Cache();
939
+ var connectors = new Cache();
940
+ function shapeKey(e, tag) {
941
+ const s = e.style;
942
+ return [
943
+ tag,
944
+ e.kind,
945
+ e.x,
946
+ e.y,
947
+ e.width,
948
+ e.height,
949
+ e.seed,
950
+ s.roughness,
951
+ s.bowing,
952
+ s.strokeWidth,
953
+ s.fillStyle,
954
+ s.fillStyle === "grid" ? cellSize(e) : 0,
955
+ e.path ? e.path.join(",") : e.subpaths ? JSON.stringify(e.subpaths) : ""
956
+ ].join("|");
957
+ }
958
+ var cellSize = (e) => Math.max(8, e.style.gridSize ?? 50);
959
+ function outline(e) {
960
+ if (e.kind === "freehand" && e.path === null) return computeOutline(e);
961
+ return shapes.get(shapeKey(e, 0), () => computeOutline(e));
962
+ }
963
+ function corners(r) {
964
+ return [{ x: r.x, y: r.y }, { x: maxX(r), y: r.y }, { x: maxX(r), y: maxY(r) }, { x: r.x, y: maxY(r) }];
965
+ }
966
+ function diamondPoints(r) {
967
+ return [{ x: midX(r), y: r.y }, { x: maxX(r), y: midY(r) }, { x: midX(r), y: maxY(r) }, { x: r.x, y: midY(r) }];
968
+ }
969
+ function computeOutline(e) {
970
+ const r = frame(e);
971
+ const rng = new SeededRNG(e.seed);
972
+ const rough = e.style.roughness;
973
+ switch (e.kind) {
974
+ case "ellipse":
975
+ return rough <= 0 ? Path.ellipse(r) : ellipse(r, rng, rough);
976
+ case "diamond":
977
+ if (rough <= 0) return new Path().addLines(diamondPoints(r)).close();
978
+ return polygon(diamondPoints(r), rng, rough, e.style.bowing);
979
+ case "text":
980
+ case "connector":
981
+ return Path.rect(r);
982
+ case "freehand":
983
+ return freehandPath(e);
984
+ case "path":
985
+ return rough <= 0 ? shapePath(e) : sketchyShape(e, rng);
986
+ default:
987
+ return rough <= 0 ? Path.rect(r) : polygon(corners(r), rng, rough, e.style.bowing);
988
+ }
989
+ }
990
+ function sketchyShape(e, rng) {
991
+ const rough = e.style.roughness;
992
+ const out = new Path();
993
+ for (const sub of shapeContours(e)) {
994
+ const p = sub.points;
995
+ for (let k = 0; k < (p.length - 1) / 3; k++) {
996
+ const a = p[3 * k], c1 = p[3 * k + 1], c2 = p[3 * k + 2], b = p[3 * k + 3];
997
+ if (c1.x === a.x && c1.y === a.y && c2.x === b.x && c2.y === b.y) {
998
+ line(a, b, rng, rough, e.style.bowing, true, out);
999
+ continue;
1000
+ }
1001
+ const hull = Math.hypot(c1.x - a.x, c1.y - a.y) + Math.hypot(c2.x - c1.x, c2.y - c1.y) + Math.hypot(b.x - c2.x, b.y - c2.y);
1002
+ const wobble = Math.min(2.5, hull / 30) * rough;
1003
+ for (let pass = 0; pass < 2; pass++) {
1004
+ const spread = pass === 0 ? 1 : 0.5;
1005
+ const j = () => rng.signed() * wobble * spread;
1006
+ out.moveTo(a);
1007
+ const c1x = c1.x + j(), c1y = c1.y + j(), c2x = c2.x + j(), c2y = c2.y + j();
1008
+ out.curveTo(b, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1009
+ }
1010
+ }
1011
+ }
1012
+ return out;
1013
+ }
1014
+ function freehandPath(e) {
1015
+ const r = frame(e);
1016
+ const flat = e.path;
1017
+ if (flat && flat.length >= 8 && (flat.length - 2) % 6 === 0) {
1018
+ const pt2 = (i) => ({ x: r.x + flat[2 * i] * r.width, y: r.y + flat[2 * i + 1] * r.height });
1019
+ const path2 = new Path().moveTo(pt2(0));
1020
+ for (let s = 0; s < Math.floor((flat.length / 2 - 1) / 3); s++) path2.curveTo(pt2(3 * s + 3), pt2(3 * s + 1), pt2(3 * s + 2));
1021
+ return path2;
1022
+ }
1023
+ const pts = (e.points ?? []).map((p) => ({ x: r.x + p.x * r.width, y: r.y + p.y * r.height }));
1024
+ const path = new Path();
1025
+ if (pts.length === 0) return path;
1026
+ path.moveTo(pts[0]);
1027
+ if (pts.length === 1) return path.lineTo(pts[0]);
1028
+ if (pts.length === 2) return path.lineTo(pts[1]);
1029
+ for (let i = 1; i < pts.length - 1; i++) {
1030
+ path.quadTo({ x: (pts[i].x + pts[i + 1].x) / 2, y: (pts[i].y + pts[i + 1].y) / 2 }, pts[i]);
1031
+ }
1032
+ return path.lineTo(pts[pts.length - 1]);
1033
+ }
1034
+ function fillPolygons(e) {
1035
+ if (e.kind !== "path") return [fillPolygon(e)];
1036
+ return shapeContours(e).map((c) => flatten(c.points, 3));
1037
+ }
1038
+ function fillPolygon(e) {
1039
+ const r = frame(e);
1040
+ if (e.kind === "ellipse") {
1041
+ const out = [];
1042
+ for (let i = 0; i < 48; i++) {
1043
+ const t = 2 * Math.PI * i / 48;
1044
+ out.push({ x: midX(r) + r.width / 2 * Math.cos(t), y: midY(r) + r.height / 2 * Math.sin(t) });
1045
+ }
1046
+ return out;
1047
+ }
1048
+ return e.kind === "diamond" ? diamondPoints(r) : corners(r);
1049
+ }
1050
+ function fillPath(e) {
1051
+ const r = frame(e);
1052
+ if (e.kind === "ellipse") return Path.ellipse(r);
1053
+ if (e.kind === "path") return shapePath(e);
1054
+ if (e.kind === "diamond") return new Path().addLines(diamondPoints(r)).close();
1055
+ return Path.rect(r);
1056
+ }
1057
+ function hachurePath(e) {
1058
+ if (e.style.fillStyle === "solid" || ["freehand", "connector", "text", "image"].includes(e.kind)) return new Path();
1059
+ return shapes.get(shapeKey(e, 1), () => computeHachure(e));
1060
+ }
1061
+ function computeHachure(e) {
1062
+ const polys = fillPolygons(e);
1063
+ const rng = new SeededRNG((e.seed ^ 1540483477) >>> 0);
1064
+ const path = new Path();
1065
+ if (e.style.fillStyle === "grid") {
1066
+ for (const [a, b] of gridLines(polys, cellSize(e))) {
1067
+ if (e.style.roughness <= 0) path.moveTo(a).lineTo(b);
1068
+ else line(a, b, rng, e.style.roughness * 0.3, 0.3, false, path);
1069
+ }
1070
+ return path;
1071
+ }
1072
+ const gap = Math.max(4, e.style.strokeWidth * 4);
1073
+ const angles = [HACHURE_ANGLE];
1074
+ if (e.style.fillStyle === "crossHatch") angles.push(HACHURE_ANGLE + Math.PI / 2);
1075
+ for (const angle2 of angles) {
1076
+ for (const [a, b] of hachureLines(polys, angle2, gap)) {
1077
+ if (e.style.roughness <= 0) path.moveTo(a).lineTo(b);
1078
+ else line(a, b, rng, e.style.roughness * 0.6, 0.5, false, path);
1079
+ }
1080
+ }
1081
+ return path;
1082
+ }
1083
+ function dashPattern(dash, width) {
1084
+ switch (dash) {
1085
+ case "solid":
1086
+ return null;
1087
+ case "dashed":
1088
+ return [Math.max(8, width * 4), Math.max(6, width * 3)];
1089
+ case "dotted":
1090
+ return [0.01, Math.max(5, width * 2.6)];
1091
+ }
1092
+ }
1093
+ function connectorParts(e, index) {
1094
+ const c = e.connector;
1095
+ if (!c) return [new Path(), new Path()];
1096
+ const [a, b] = endpoints(e, index);
1097
+ const key = [
1098
+ e.seed,
1099
+ e.style.roughness,
1100
+ e.style.bowing,
1101
+ e.style.strokeWidth,
1102
+ c.routing,
1103
+ c.startHead,
1104
+ c.endHead,
1105
+ c.start.port,
1106
+ c.end.port,
1107
+ a.x,
1108
+ a.y,
1109
+ b.x,
1110
+ b.y
1111
+ ].join("|");
1112
+ return connectors.get(key, () => computeConnector(e, index));
1113
+ }
1114
+ function computeConnector(e, index) {
1115
+ const c = e.connector;
1116
+ const rng = new SeededRNG(e.seed);
1117
+ const rough = e.style.roughness, bow = e.style.bowing;
1118
+ const path = new Path(), solid = new Path();
1119
+ const r = route(e, index);
1120
+ if (r.kind === "polyline") {
1121
+ const pts2 = r.points;
1122
+ for (let i = 0; i < Math.max(pts2.length - 1, 0); i++) {
1123
+ if (rough <= 0) path.moveTo(pts2[i]).lineTo(pts2[i + 1]);
1124
+ else line(pts2[i], pts2[i + 1], rng, rough, bow, true, path);
1125
+ }
1126
+ } else {
1127
+ path.moveTo(r.a);
1128
+ const w = rough * 3;
1129
+ const j = () => rng.signed() * w;
1130
+ const c1x = r.b.x + j(), c1y = r.b.y + j(), c2x = r.c.x + j(), c2y = r.c.y + j();
1131
+ path.curveTo(r.d, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1132
+ }
1133
+ const pts = routePoints(r);
1134
+ const size = Math.max(10, e.style.strokeWidth * 5);
1135
+ const line2 = (p, q) => {
1136
+ if (rough <= 0) path.moveTo(p).lineTo(q);
1137
+ else line(p, q, rng, rough * 0.5, 0, true, path);
1138
+ };
1139
+ const head = (kind, tip, from) => {
1140
+ const [w1, w2] = arrowhead(tip, from, size);
1141
+ switch (kind) {
1142
+ case "arrow":
1143
+ line2(w1, tip);
1144
+ line2(w2, tip);
1145
+ break;
1146
+ case "triangle":
1147
+ line2(w1, tip);
1148
+ line2(w2, tip);
1149
+ line2(w1, w2);
1150
+ solid.moveTo(tip).lineTo(w1).lineTo(w2).close();
1151
+ break;
1152
+ case "dot": {
1153
+ const rad = size * 0.32;
1154
+ const ang = Math.atan2(tip.y - from.y, tip.x - from.x);
1155
+ const cx = tip.x - rad * Math.cos(ang), cy = tip.y - rad * Math.sin(ang);
1156
+ solid.addEllipse({ x: cx - rad, y: cy - rad, width: 2 * rad, height: 2 * rad });
1157
+ break;
1158
+ }
1159
+ case "bar": {
1160
+ const ang = Math.atan2(tip.y - from.y, tip.x - from.x) + Math.PI / 2;
1161
+ const h = size * 0.5;
1162
+ line2({ x: tip.x - h * Math.cos(ang), y: tip.y - h * Math.sin(ang) }, { x: tip.x + h * Math.cos(ang), y: tip.y + h * Math.sin(ang) });
1163
+ break;
1164
+ }
1165
+ }
1166
+ };
1167
+ if (pts.length > 1) {
1168
+ head(c.endHead, pts[pts.length - 1], pts[pts.length - 2]);
1169
+ head(c.startHead, pts[0], pts[1]);
1170
+ }
1171
+ return [path, solid];
1172
+ }
1173
+
1174
+ // src/dungeon.ts
1175
+ var SUB = 2;
1176
+ var BAND = 0.75;
1177
+ var GROW = 0.8;
1178
+ var JITTER = 0.6;
1179
+ function gridLines(polygons, cell) {
1180
+ return [...scan(polygons, cell, false), ...scan(polygons, cell, true)];
1181
+ }
1182
+ function scan(polygons, cell, transposed) {
1183
+ if (!(cell > 0)) return [];
1184
+ const flip = (p) => transposed ? { x: p.y, y: p.x } : p;
1185
+ const polys = polygons.filter((p) => p.length >= 3).map((p) => p.map(flip));
1186
+ const ys = polys.flat().map((p) => p.y);
1187
+ if (ys.length === 0) return [];
1188
+ const minY = Math.min(...ys), maxY2 = Math.max(...ys);
1189
+ const out = [];
1190
+ for (let y = Math.floor(minY / cell) * cell; y < maxY2 - 0.5; y += cell) {
1191
+ if (!(y > minY + 0.5)) continue;
1192
+ const xs = [];
1193
+ for (const poly of polys) {
1194
+ for (let i = 0; i < poly.length; i++) {
1195
+ const a = poly[i], b = poly[(i + 1) % poly.length];
1196
+ if (a.y <= y && y < b.y || b.y <= y && y < a.y) xs.push(a.x + (y - a.y) / (b.y - a.y) * (b.x - a.x));
1197
+ }
1198
+ }
1199
+ xs.sort((p, q) => p - q);
1200
+ for (let k = 0; k + 1 < xs.length; k += 2) {
1201
+ if (xs[k + 1] - xs[k] > 0.5) out.push([flip({ x: xs[k], y }), flip({ x: xs[k + 1], y })]);
1202
+ }
1203
+ }
1204
+ return out;
1205
+ }
1206
+ function lineColor(floor) {
1207
+ if (0.2126 * floor.r + 0.7152 * floor.g + 0.0722 * floor.b > 0.5) {
1208
+ return { r: floor.r * 0.84, g: floor.g * 0.84, b: floor.b * 0.8, a: floor.a };
1209
+ }
1210
+ return { r: floor.r + (1 - floor.r) * 0.25, g: floor.g + (1 - floor.g) * 0.25, b: floor.b + (1 - floor.b) * 0.25, a: floor.a };
1211
+ }
1212
+ function isFloor(e) {
1213
+ return e.style.fillStyle === "grid" && e.style.fill.a > 0 && ["rectangle", "ellipse", "diamond", "path"].includes(e.kind);
1214
+ }
1215
+ function hash01(i, j, salt) {
1216
+ let h = (Math.imul(i, 668265261) ^ Math.imul(j, 374761393) ^ Math.imul(salt, 2654435761)) >>> 0;
1217
+ h = Math.imul(h ^ h >>> 15, 2246822507) >>> 0;
1218
+ h = Math.imul(h ^ h >>> 13, 3266489909) >>> 0;
1219
+ h = (h ^ h >>> 16) >>> 0;
1220
+ return h / 4294967296;
1221
+ }
1222
+ function spot(i, j, cell) {
1223
+ const s = cell / SUB;
1224
+ return { x: (i + 0.5 + (hash01(i, j, 1) - 0.5) * JITTER) * s, y: (j + 0.5 + (hash01(i, j, 2) - 0.5) * JITTER) * s };
1225
+ }
1226
+ function clusterSize(i, j, at, cell) {
1227
+ let nearest2 = Infinity;
1228
+ for (let di = -1; di <= 1; di++) {
1229
+ for (let dj = -1; dj <= 1; dj++) {
1230
+ if (di === 0 && dj === 0) continue;
1231
+ const o = spot(i + di, j + dj, cell);
1232
+ nearest2 = Math.min(nearest2, Math.hypot(o.x - at.x, o.y - at.y));
1233
+ }
1234
+ }
1235
+ return nearest2 * GROW;
1236
+ }
1237
+ function clusterStrokes(c, angle2, size) {
1238
+ const dx = Math.cos(angle2), dy = Math.sin(angle2);
1239
+ const half = size * 0.8, gap = size * 0.5;
1240
+ return [-1, 0, 1].map((k) => {
1241
+ const cx = c.x - dy * gap * k, cy = c.y + dx * gap * k;
1242
+ return [{ x: cx - dx * half, y: cy - dy * half }, { x: cx + dx * half, y: cy + dy * half }];
1243
+ });
1244
+ }
1245
+ function contains(contours, p) {
1246
+ let inside = false;
1247
+ for (const poly of contours) {
1248
+ if (poly.length < 3) continue;
1249
+ let k = poly.length - 1;
1250
+ for (let i = 0; i < poly.length; i++) {
1251
+ const a = poly[i], b = poly[k];
1252
+ if (a.y > p.y !== b.y > p.y && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x) inside = !inside;
1253
+ k = i;
1254
+ }
1255
+ }
1256
+ return inside;
1257
+ }
1258
+ function distance(p, contours) {
1259
+ let best = Infinity;
1260
+ for (const poly of contours) {
1261
+ if (poly.length < 2) continue;
1262
+ for (let i = 0; i < poly.length; i++) {
1263
+ const a = poly[i], b = poly[(i + 1) % poly.length];
1264
+ const dx = b.x - a.x, dy = b.y - a.y;
1265
+ const len2 = dx * dx + dy * dy;
1266
+ const t = len2 > 0 ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2)) : 0;
1267
+ best = Math.min(best, Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy)));
1268
+ }
1269
+ }
1270
+ return best;
1271
+ }
1272
+ var containsPoint = (r, p) => p.x >= r.x && p.x < r.x + r.width && p.y >= r.y && p.y < r.y + r.height;
1273
+ var rockCache = /* @__PURE__ */ new Map();
1274
+ function rock(floors) {
1275
+ if (floors.length === 0) return [];
1276
+ const key = floors.map((e) => [
1277
+ e.kind,
1278
+ e.x,
1279
+ e.y,
1280
+ e.width,
1281
+ e.height,
1282
+ e.rotation ?? 0,
1283
+ cellSize(e),
1284
+ JSON.stringify(e.subpaths ?? null),
1285
+ e.style.stroke.r,
1286
+ e.style.stroke.g,
1287
+ e.style.stroke.b,
1288
+ e.style.stroke.a,
1289
+ e.style.roughness,
1290
+ e.style.strokeWidth
1291
+ ].join(",")).join(";");
1292
+ const hit = rockCache.get(key);
1293
+ if (hit) return hit;
1294
+ const v = computeRock(floors);
1295
+ if (rockCache.size >= 16) rockCache.clear();
1296
+ rockCache.set(key, v);
1297
+ return v;
1298
+ }
1299
+ function computeRock(elements) {
1300
+ const floors = elements.map((e) => {
1301
+ const contours = fillPolygons(e).map((c) => c.map((p) => rotated(e, p)));
1302
+ const pts = contours.flat();
1303
+ const xs = pts.map((p) => p.x), ys = pts.map((p) => p.y);
1304
+ const x0 = xs.length ? Math.min(...xs) : 0, y0 = ys.length ? Math.min(...ys) : 0;
1305
+ const box = { x: x0, y: y0, width: (xs.length ? Math.max(...xs) : 0) - x0, height: (ys.length ? Math.max(...ys) : 0) - y0 };
1306
+ return { contours, box, cell: cellSize(e), element: e };
1307
+ });
1308
+ const strokes = floors.map(() => []);
1309
+ const seen = /* @__PURE__ */ new Set();
1310
+ for (const f of floors) {
1311
+ const subSize = f.cell / SUB;
1312
+ const around = inset(f.box, -f.cell, -f.cell);
1313
+ const i0 = Math.floor(around.x / subSize), i1 = Math.ceil((around.x + around.width) / subSize);
1314
+ const j0 = Math.floor(around.y / subSize), j1 = Math.ceil((around.y + around.height) / subSize);
1315
+ const reach = BAND * f.cell;
1316
+ for (let i = i0; i < i1; i++) {
1317
+ for (let j = j0; j < j1; j++) {
1318
+ const k = `${f.cell}|${i}|${j}`;
1319
+ if (seen.has(k)) continue;
1320
+ seen.add(k);
1321
+ const at = spot(i, j, f.cell);
1322
+ let nearest2 = Infinity, owner = -1;
1323
+ for (let n = 0; n < floors.length; n++) {
1324
+ const g = floors[n];
1325
+ if (!containsPoint(inset(g.box, -reach, -reach), at)) continue;
1326
+ if (contains(g.contours, at)) {
1327
+ nearest2 = 0;
1328
+ break;
1329
+ }
1330
+ const d = distance(at, g.contours);
1331
+ if (d < nearest2) {
1332
+ nearest2 = d;
1333
+ owner = n;
1334
+ }
1335
+ }
1336
+ if (!(nearest2 > 0 && nearest2 <= reach && owner >= 0)) continue;
1337
+ const angle2 = hash01(i, j, 3) * Math.PI;
1338
+ const size = clusterSize(i, j, at, f.cell);
1339
+ const seed = Math.trunc(hash01(i, j, 4) * 4294967296) >>> 0;
1340
+ for (const [a, b] of clusterStrokes(at, angle2, size)) strokes[owner].push([a, b, seed]);
1341
+ }
1342
+ }
1343
+ }
1344
+ const out = [];
1345
+ floors.forEach((f, n) => {
1346
+ if (strokes[n].length === 0) return;
1347
+ const e = f.element;
1348
+ const path = new Path();
1349
+ for (const [a, b, seed] of strokes[n]) {
1350
+ if (e.style.roughness <= 0) path.moveTo(a).lineTo(b);
1351
+ else line(a, b, new SeededRNG(seed), e.style.roughness * 0.35, 0.2, false, path);
1352
+ }
1353
+ out.push({ ink: { ...e.style.stroke, a: e.style.stroke.a * 0.6 }, width: Math.max(0.8, e.style.strokeWidth * 0.45), path });
1354
+ });
1355
+ return out;
1356
+ }
1357
+
1358
+ // src/metrics.ts
1359
+ var ascent = 0.951995849609375;
1360
+ var descent = 0.2129974365234375;
1361
+ var leading = 0.0279998779296875;
1362
+ var boldAscent = 0.975006103515625;
1363
+ var boldDescent = 0.2169952392578125;
1364
+ var boldLeading = 0.0290069580078125;
1365
+ var italicAscent = 0.957000732421875;
1366
+ var italicDescent = 0.2129974365234375;
1367
+ var italicLeading = 0.0279998779296875;
1368
+ var boldItalicAscent = 0.975006103515625;
1369
+ var boldItalicDescent = 0.2169952392578125;
1370
+ var boldItalicLeading = 0.0290069580078125;
1371
+ var monoAdvance = 0.60205078125;
1372
+ var monoAscent = 0.92822265625;
1373
+ var monoDescent = 0.23583984375;
1374
+ var fonts = { "bold": { "k": { "\u2018\u2018": -95, "\u2019\u2019": -95, "\u2019s": -74, "\u2019t": -18, "A\u2019": -74, "AT": -92, "Av": -18, "AV": -46, "Aw": -18, "AW": -24, "Ay": -18, "AY": -74, "F,": -157, "F.": -157, "f\u2019": 18, "FA": -55, "ff": -30, "fi": 20, "fl": 20, "L\u2019": -74, "LT": -92, "LV": -92, "LW": -55, "Ly": -37, "LY": -111, "P,": -179, "P.": -179, "PA": -74, "r-": -55, "r,": -92, "r.": -92, "rc": -18, "rd": -18, "re": -18, "rg": -28, "rn": 18, "ro": -18, "rq": -18, "RT": -18, "RV": -18, "RW": -18, "RY": -37, "T-": -129, "T,": -111, "T;": -111, "T:": -111, "T.": -111, "Ta": -111, "TA": -92, "Tc": -111, "Te": -111, "To": -111, "Tr": -92, "Ts": -111, "Tu": -92, "Tw": -111, "Ty": -111, "V-": -55, "v,": -74, "V,": -129, "V;": -46, "V:": -46, "v.": -74, "V.": -129, "Va": -55, "VA": -46, "Ve": -55, "Vi": -18, "Vo": -55, "Vr": -37, "Vu": -37, "Vy": -18, "w,": -55, "W,": -74, "W;": -18, "W:": -18, "w.": -55, "W.": -74, "Wa": -37, "WA": -24, "We": -57, "Wo": -57, "Wr": -18, "Wu": -18, "Y-": -111, "y,": -74, "Y,": -139, "Y;": -92, "Y:": -92, "y.": -74, "Y.": -139, "Ya": -92, "YA": -74, "Ye": -111, "Yi": -37, "Yo": -111, "Yp": -74, "Yq": -92, "Yu": -74, "Yv": -55 }, "w": { " ": 278, "_": 500, "-": 407, "\u2013": 500, "\u2014": 1e3, ",": 278, ";": 278, ":": 278, "!": 278, "?": 556, ".": 278, "\u2026": 1e3, "\xB7": 278, "'": 278, "\u2018": 278, "\u2019": 278, '"': 463, "\u201C": 463, "\u201D": 463, "(": 296, ")": 296, "[": 333, "]": 333, "{": 333, "}": 333, "@": 800, "*": 407, "/": 371, "\\": 371, "&": 685, "#": 556, "%": 1e3, "\u2022": 350, "`": 259, "^": 600, "\u2190": 1e3, "\u2192": 1e3, "\u2191": 722.66, "\u2193": 722.66, "+": 600, "\xD7": 600, "<": 600, "=": 600, ">": 600, "|": 223, "~": 600, "\u25E6": 608.4, "\u2610": 830.08, "\u2611": 830.08, "\u2713": 764.16, "$": 556, "0": 556, "1": 556, "2": 556, "3": 556, "4": 556, "5": 556, "6": 556, "7": 556, "8": 556, "9": 556, "a": 574, "A": 685, "b": 611, "B": 704, "c": 574, "C": 741, "d": 611, "D": 741, "e": 574, "E": 648, "\xE9": 574, "f": 333, "F": 593, "g": 611, "G": 759, "h": 593, "H": 741, "i": 258, "I": 295, "j": 278, "J": 556, "k": 574, "K": 722, "l": 258, "L": 593, "m": 906, "M": 907, "n": 593, "N": 741, "o": 611, "O": 778, "p": 611, "P": 667, "q": 611, "Q": 778, "r": 389, "R": 722, "s": 537, "S": 649, "t": 352, "T": 611, "u": 593, "U": 741, "v": 520, "V": 630, "w": 814, "W": 944, "x": 537, "X": 667, "y": 519, "Y": 667, "z": 519, "Z": 648 } }, "boldItalic": { "k": { "\u2018\u2018": -95, "\u2019\u2019": -95, "\u2019s": -74, "\u2019t": -18, "A\u2019": -74, "AT": -92, "Av": -18, "AV": -46, "Aw": -18, "AW": -24, "Ay": -18, "AY": -74, "F,": -157, "F.": -157, "f\u2019": 18, "FA": -55, "ff": -68, "L\u2019": -74, "LT": -92, "LV": -92, "LW": -55, "Ly": -37, "LY": -111, "P,": -179, "P.": -179, "PA": -74, "r-": -55, "r,": -92, "r.": -92, "rc": -18, "rd": -18, "re": -18, "rg": -28, "rn": 18, "ro": -18, "rq": -18, "RT": -18, "RV": -18, "RW": -18, "RY": -37, "T-": -129, "T,": -111, "T;": -111, "T:": -111, "T.": -111, "Ta": -111, "TA": -92, "Tc": -111, "Te": -111, "To": -111, "Tr": -92, "Ts": -111, "Tu": -92, "Tw": -111, "Ty": -111, "V-": -55, "v,": -74, "V,": -129, "V;": -46, "V:": -46, "v.": -74, "V.": -129, "Va": -55, "VA": -46, "Ve": -55, "Vi": -18, "Vo": -55, "Vr": -37, "Vu": -37, "Vy": -18, "w,": -55, "W,": -74, "W;": -18, "W:": -18, "w.": -55, "W.": -74, "Wa": -37, "WA": -24, "We": -57, "Wo": -57, "Wr": -18, "Wu": -18, "Y-": -111, "y,": -74, "Y,": -139, "Y;": -92, "Y:": -92, "y.": -74, "Y.": -139, "Ya": -92, "YA": -74, "Ye": -111, "Yi": -37, "Yo": -111, "Yp": -74, "Yq": -92, "Yu": -74, "Yv": -55 }, "w": { " ": 278, "_": 500, "-": 407, "\u2013": 500, "\u2014": 1e3, ",": 278, ";": 278, ":": 278, "!": 296, "?": 574, ".": 278, "\u2026": 1e3, "\xB7": 278, "'": 278, "\u2018": 278, "\u2019": 278, '"': 481, "\u201C": 481, "\u201D": 481, "(": 296, ")": 296, "[": 333, "]": 333, "{": 333, "}": 333, "@": 800, "*": 407, "/": 389, "\\": 389, "&": 685, "#": 556, "%": 963, "\u2022": 350, "`": 259, "^": 600, "\u2190": 1e3, "\u2192": 1e3, "\u2191": 722.66, "\u2193": 722.66, "+": 600, "\xD7": 600, "<": 600, "=": 600, ">": 600, "|": 222, "~": 600, "\u25E6": 608.4, "\u2610": 830.08, "\u2611": 830.08, "\u2713": 764.16, "$": 556, "0": 556, "1": 556, "2": 556, "3": 556, "4": 556, "5": 556, "6": 556, "7": 556, "8": 556, "9": 556, "a": 574, "A": 685, "b": 611, "B": 722, "c": 556, "C": 741, "d": 611, "D": 741, "e": 574, "E": 667, "\xE9": 574, "f": 352, "F": 593, "g": 611, "G": 759, "h": 611, "H": 741, "i": 259, "I": 296, "j": 259, "J": 556, "k": 556, "K": 722, "l": 259, "L": 574, "m": 907, "M": 907, "n": 611, "N": 741, "o": 593, "O": 778, "p": 611, "P": 667, "q": 611, "Q": 778, "r": 389, "R": 722, "s": 519, "S": 648, "t": 370, "T": 611, "u": 611, "U": 741, "v": 519, "V": 630, "w": 815, "W": 944, "x": 519, "X": 667, "y": 519, "Y": 648, "z": 500, "Z": 648 } }, "italic": { "k": { "\u2018\u2018": -95, "\u2019\u2019": -95, "\u2019s": -74, "\u2019t": -18, "A\u2019": -74, "AT": -92, "Av": -18, "AV": -46, "Aw": -18, "AW": -24, "Ay": -18, "AY": -74, "F,": -157, "F.": -157, "f\u2019": 18, "FA": -55, "ff": -15, "fi": 1, "fl": 1, "L\u2019": -74, "LT": -92, "LV": -92, "LW": -55, "Ly": -37, "LY": -111, "P,": -179, "P.": -179, "PA": -74, "r-": -55, "r,": -92, "r.": -92, "rc": -18, "rd": -18, "re": -18, "rg": -28, "rn": 18, "ro": -18, "rq": -18, "RT": -18, "RV": -18, "RW": -18, "RY": -37, "T-": -129, "T,": -111, "T;": -111, "T:": -111, "T.": -111, "Ta": -111, "TA": -92, "Tc": -111, "Te": -111, "To": -111, "Tr": -92, "Ts": -111, "Tu": -92, "Tw": -111, "Ty": -111, "V-": -55, "v,": -74, "V,": -129, "V;": -46, "V:": -46, "v.": -74, "V.": -129, "Va": -55, "VA": -46, "Ve": -55, "Vi": -18, "Vo": -55, "Vr": -37, "Vu": -37, "Vy": -18, "w,": -55, "W,": -74, "W;": -18, "W:": -18, "w.": -55, "W.": -74, "Wa": -37, "WA": -24, "We": -57, "Wo": -57, "Wr": -18, "Wu": -18, "Y-": -111, "y,": -74, "Y,": -139, "Y;": -92, "Y:": -92, "y.": -74, "Y.": -139, "Ya": -92, "YA": -74, "Ye": -111, "Yi": -37, "Yo": -111, "Yp": -74, "Yq": -92, "Yu": -74, "Yv": -55 }, "w": { " ": 278, "_": 500, "-": 389, "\u2013": 500, "\u2014": 1e3, ",": 278, ";": 278, ":": 278, "!": 259, "?": 556, ".": 278, "\u2026": 1e3, "\xB7": 278, "'": 278, "\u2018": 278, "\u2019": 278, '"': 426, "\u201C": 426, "\u201D": 426, "(": 259, ")": 259, "[": 259, "]": 259, "{": 333, "}": 333, "@": 800, "*": 352, "/": 333, "\\": 333, "&": 630, "#": 556, "%": 926, "\u2022": 500, "`": 222, "^": 600, "\u2190": 1e3, "\u2192": 1e3, "\u2191": 645.02, "\u2193": 645.02, "+": 600, "\xD7": 600, "<": 600, "=": 600, ">": 600, "|": 222, "~": 600, "\u25E6": 578.13, "\u2610": 830.08, "\u2611": 830.08, "\u2713": 764.16, "$": 556, "0": 556, "1": 556, "2": 556, "3": 556, "4": 556, "5": 556, "6": 556, "7": 556, "8": 556, "9": 556, "a": 519, "A": 667, "b": 593, "B": 685, "c": 537, "C": 722, "d": 593, "D": 704, "e": 537, "E": 611, "\xE9": 537, "f": 296, "F": 574, "g": 574, "G": 759, "h": 556, "H": 722, "i": 222, "I": 259, "j": 222, "J": 519, "k": 481, "K": 667, "l": 222, "L": 556, "m": 852, "M": 870, "n": 556, "N": 722, "o": 574, "O": 759, "p": 593, "P": 648, "q": 593, "Q": 759, "r": 333, "R": 685, "s": 481, "S": 648, "t": 315, "T": 574, "u": 556, "U": 722, "v": 481, "V": 611, "w": 759, "W": 926, "x": 481, "X": 611, "y": 481, "Y": 611, "z": 444, "Z": 611 } }, "regular": { "k": { "\u2018\u2018": -95, "\u2019\u2019": -95, "\u2019s": -74, "\u2019t": -18, "A\u2019": -74, "AT": -92, "Av": -18, "AV": -46, "Aw": -18, "AW": -24, "Ay": -18, "AY": -74, "F,": -157, "F.": -157, "f\u2019": 18, "FA": -55, "ff": -15, "L\u2019": -74, "LT": -92, "LV": -92, "LW": -55, "Ly": -37, "LY": -111, "P,": -179, "P.": -179, "PA": -74, "r-": -55, "r,": -92, "r.": -92, "rc": -18, "rd": -18, "re": -18, "rg": -28, "rn": 18, "ro": -18, "rq": -18, "RT": -18, "RV": -18, "RW": -18, "RY": -37, "T-": -129, "T,": -111, "T;": -111, "T:": -111, "T.": -111, "Ta": -111, "TA": -92, "Tc": -111, "Te": -111, "To": -111, "Tr": -92, "Ts": -111, "Tu": -92, "Tw": -111, "Ty": -111, "V-": -55, "v,": -74, "V,": -129, "V;": -46, "V:": -46, "v.": -74, "V.": -129, "Va": -55, "VA": -46, "Ve": -55, "Vi": -18, "Vo": -55, "Vr": -37, "Vu": -37, "Vy": -18, "w,": -55, "W,": -74, "W;": -18, "W:": -18, "w.": -55, "W.": -74, "Wa": -37, "WA": -24, "We": -57, "Wo": -57, "Wr": -18, "Wu": -18, "Y-": -111, "y,": -74, "Y,": -139, "Y;": -92, "Y:": -92, "y.": -74, "Y.": -139, "Ya": -92, "YA": -74, "Ye": -111, "Yi": -37, "Yo": -111, "Yp": -74, "Yq": -92, "Yu": -74, "Yv": -55 }, "w": { " ": 278, "_": 500, "-": 389, "\u2013": 500, "\u2014": 1e3, ",": 278, ";": 278, ":": 278, "!": 259, "?": 556, ".": 278, "\u2026": 1e3, "\xB7": 278, "'": 278, "\u2018": 278, "\u2019": 278, '"': 426, "\u201C": 426, "\u201D": 426, "(": 259, ")": 259, "[": 259, "]": 259, "{": 333, "}": 333, "@": 800, "*": 352, "/": 333, "\\": 333, "&": 630, "#": 556, "%": 1e3, "\u2022": 350, "`": 222, "^": 600, "\u2190": 1e3, "\u2192": 1e3, "\u2191": 645.02, "\u2193": 645.02, "+": 600, "\xD7": 600, "<": 600, "=": 600, ">": 600, "|": 222, "~": 600, "\u25E6": 578.13, "\u2610": 830.08, "\u2611": 830.08, "\u2713": 764.16, "$": 556, "0": 556, "1": 556, "2": 556, "3": 556, "4": 556, "5": 556, "6": 556, "7": 556, "8": 556, "9": 556, "a": 537, "A": 648, "b": 593, "B": 685, "c": 537, "C": 722, "d": 593, "D": 704, "e": 537, "E": 611, "\xE9": 537, "f": 296, "F": 574, "g": 574, "G": 759, "h": 556, "H": 722, "i": 222, "I": 259, "j": 222, "J": 519, "k": 519, "K": 667, "l": 222, "L": 556, "m": 853, "M": 871, "n": 556, "N": 722, "o": 574, "O": 760, "p": 593, "P": 648, "q": 593, "Q": 760, "r": 333, "R": 685, "s": 500, "S": 648, "t": 315, "T": 574, "u": 556, "U": 722, "v": 500, "V": 611, "w": 758, "W": 926, "x": 518, "X": 611, "y": 500, "Y": 648, "z": 480, "Z": 611 } } };
1375
+ var fallbackMetrics = { "\u2190": [0.966796875, 0.2109375, 0], "\u2192": [0.966796875, 0.2109375, 0], "\u2191": [0.966796875, 0.2109375, 0], "\u2193": [0.966796875, 0.2109375, 0], "\u25E6": [0.966796875, 0.2109375, 0], "\u2610": [0.66650390625, 0.25, 0.08349609375], "\u2611": [0.66650390625, 0.25, 0.08349609375], "\u2713": [0.966796875, 0.2109375, 0] };
1376
+
1377
+ // src/svg-util.ts
1378
+ function color(c) {
1379
+ const h = (v) => Math.round(Math.min(Math.max(v, 0), 1) * 255).toString(16).padStart(2, "0");
1380
+ return `#${h(c.r)}${h(c.g)}${h(c.b)}`;
1381
+ }
1382
+ function escapeXML(s) {
1383
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1384
+ }
1385
+
1386
+ // src/text.ts
1387
+ function measure(s, fontSize, face2 = "regular") {
1388
+ const f = fonts[face2];
1389
+ const fallback = f.w["n"];
1390
+ let w = 0;
1391
+ let prev = "";
1392
+ for (const ch of s) {
1393
+ w += f.w[ch] ?? fallback;
1394
+ if (prev) w += f.k[prev + ch] ?? 0;
1395
+ prev = ch;
1396
+ }
1397
+ return w * fontSize / 1e3;
1398
+ }
1399
+ function lineHeights(fontSize) {
1400
+ const a = Math.round(ascent * fontSize), d = Math.round(descent * fontSize), l = Math.round(leading * fontSize);
1401
+ return [a + d, a + d + l];
1402
+ }
1403
+ function breaksAfter(ch, next) {
1404
+ if (ch === " ") return true;
1405
+ return (ch === "/" || ch === "-") && next !== "" && !/[\d\s/-]/.test(next);
1406
+ }
1407
+ function pieces(para) {
1408
+ const chars = [...para];
1409
+ const out = [];
1410
+ let cur = "";
1411
+ chars.forEach((ch, i) => {
1412
+ cur += ch;
1413
+ const next = chars[i + 1] ?? "";
1414
+ if (breaksAfter(ch, next) && next !== " ") {
1415
+ out.push(cur);
1416
+ cur = "";
1417
+ }
1418
+ });
1419
+ if (cur || out.length === 0) out.push(cur);
1420
+ return out;
1421
+ }
1422
+ function wrap(s, fontSize, maxWidth, face2 = "regular") {
1423
+ const out = [];
1424
+ for (const para of s.split(/\r\n|\n|\r|\u2028|\u2029/)) {
1425
+ let line2 = "";
1426
+ for (const piece of pieces(para)) {
1427
+ const candidate = line2 + piece;
1428
+ if (line2 === "" || measure(candidate.trimEnd(), fontSize, face2) <= maxWidth) {
1429
+ line2 = candidate;
1430
+ } else {
1431
+ out.push(line2);
1432
+ line2 = piece;
1433
+ }
1434
+ while (measure(line2.trimEnd(), fontSize, face2) > maxWidth && [...line2.trimEnd()].length > 1) {
1435
+ const chars = [...line2];
1436
+ let n = chars.length - 1;
1437
+ while (n > 1 && measure(chars.slice(0, n).join(""), fontSize, face2) > maxWidth) n--;
1438
+ out.push(chars.slice(0, n).join(""));
1439
+ line2 = chars.slice(n).join("");
1440
+ }
1441
+ }
1442
+ out.push(line2);
1443
+ }
1444
+ return out;
1445
+ }
1446
+ function textSize(s, fontSize, maxWidth) {
1447
+ if (s === "") return { width: 0, height: 0 };
1448
+ const lines = wrap(s, fontSize, maxWidth);
1449
+ const width = Math.max(...lines.map((l) => measure(l.trimEnd(), fontSize)));
1450
+ const [first, next] = lineHeights(fontSize);
1451
+ return { width: Math.ceil(width - 1e-9), height: Math.ceil(first + next * (lines.length - 1) - 1e-9) };
1452
+ }
1453
+
1454
+ // src/note.ts
1455
+ var PADDING = 12;
1456
+ var SEP = "\u2028";
1457
+ function textRect(f) {
1458
+ const dx = Math.min(PADDING, f.width / 2), dy = Math.min(PADDING, f.height / 2);
1459
+ return { x: f.x + dx, y: f.y + dy, width: f.width - 2 * dx, height: f.height - 2 * dy };
1460
+ }
1461
+ var reHeading = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
1462
+ var reRule = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
1463
+ var reFence = /^ {0,3}(`{3,}|~{3,})/;
1464
+ var reItem = /^( *)([-*+]|\d{1,9}[.)])([ \t]+|$)(.*)$/;
1465
+ var reQuote = /^ {0,3}> ?(.*)$/;
1466
+ var reTableSep = /^ *\|? *:?-+:? *(\| *:?-+:? *)*\|? *$/;
1467
+ function splitRow(line2) {
1468
+ let s = line2.trim();
1469
+ if (s.startsWith("|")) s = s.slice(1);
1470
+ if (s.endsWith("|") && !s.endsWith("\\|")) s = s.slice(0, -1);
1471
+ return s.split(/(?<!\\)\|/).map((c) => c.trim());
1472
+ }
1473
+ function parseBlocks(source) {
1474
+ return blocks(source.replace(/\r\n?/g, "\n").split("\n"));
1475
+ }
1476
+ function blocks(lines) {
1477
+ const out = [];
1478
+ let i = 0;
1479
+ let para = null;
1480
+ const flush = () => {
1481
+ if (para) {
1482
+ out.push({ kind: "paragraph", inline: inline(para.join("\n")) });
1483
+ para = null;
1484
+ }
1485
+ };
1486
+ while (i < lines.length) {
1487
+ const line2 = lines[i];
1488
+ if (line2.trim() === "") {
1489
+ flush();
1490
+ i++;
1491
+ continue;
1492
+ }
1493
+ const fence = reFence.exec(line2);
1494
+ if (fence) {
1495
+ flush();
1496
+ const close = new RegExp(`^ {0,3}${fence[1][0] === "`" ? "`" : "~"}{${fence[1].length},}[ \\t]*$`);
1497
+ const body = [];
1498
+ i++;
1499
+ while (i < lines.length && !close.test(lines[i])) body.push(lines[i++]);
1500
+ i++;
1501
+ out.push({ kind: "code", text: body.join("\n") });
1502
+ continue;
1503
+ }
1504
+ const h = reHeading.exec(line2);
1505
+ if (h) {
1506
+ flush();
1507
+ out.push({ kind: "heading", level: h[1].length, inline: inline(h[2] ?? "") });
1508
+ i++;
1509
+ continue;
1510
+ }
1511
+ if (para && /^ {0,3}(=+|-+)[ \t]*$/.test(line2)) {
1512
+ const level = line2.trim()[0] === "=" ? 1 : 2;
1513
+ out.push({ kind: "heading", level, inline: inline(para.join("\n")) });
1514
+ para = null;
1515
+ i++;
1516
+ continue;
1517
+ }
1518
+ if (reRule.test(line2)) {
1519
+ flush();
1520
+ out.push({ kind: "rule" });
1521
+ i++;
1522
+ continue;
1523
+ }
1524
+ if (reQuote.test(line2)) {
1525
+ flush();
1526
+ const inner = [];
1527
+ while (i < lines.length && lines[i].trim() !== "") {
1528
+ const q = reQuote.exec(lines[i]);
1529
+ inner.push(q ? q[1] : lines[i]);
1530
+ i++;
1531
+ }
1532
+ out.push({ kind: "quote", children: blocks(inner) });
1533
+ continue;
1534
+ }
1535
+ const item = reItem.exec(line2);
1536
+ if (item && (!para || item[4].trim() !== "")) {
1537
+ flush();
1538
+ const list = parseList(lines, i);
1539
+ out.push(list.block);
1540
+ i = list.next;
1541
+ continue;
1542
+ }
1543
+ if (!para && line2.includes("|") && i + 1 < lines.length && reTableSep.test(lines[i + 1]) && lines[i + 1].includes("-")) {
1544
+ const head = splitRow(line2);
1545
+ const align = splitRow(lines[i + 1]).map((c) => c.startsWith(":") && c.endsWith(":") ? "center" : c.endsWith(":") ? "right" : c.startsWith(":") ? "left" : null);
1546
+ const rows = [];
1547
+ i += 2;
1548
+ while (i < lines.length && lines[i].trim() !== "" && !reHeading.test(lines[i]) && !reQuote.test(lines[i]) && !reFence.test(lines[i])) {
1549
+ rows.push(splitRow(lines[i]).slice(0, head.length).map((c) => inline(c)));
1550
+ i++;
1551
+ }
1552
+ out.push({ kind: "table", align, head: head.map((c) => inline(c)), rows });
1553
+ continue;
1554
+ }
1555
+ if (!para) para = [];
1556
+ para.push(line2.replace(/^ +/, ""));
1557
+ i++;
1558
+ }
1559
+ flush();
1560
+ return out;
1561
+ }
1562
+ function parseList(lines, start) {
1563
+ const first = reItem.exec(lines[start]);
1564
+ const indent = first[1].length;
1565
+ const bullet = /^\d/.test(first[2]) ? null : first[2];
1566
+ const ordered = bullet === null ? parseInt(first[2], 10) : null;
1567
+ const items = [];
1568
+ let i = start;
1569
+ while (i < lines.length) {
1570
+ const m = reItem.exec(lines[i]);
1571
+ if (!m || m[1].length !== indent || bullet === null !== /^\d/.test(m[2]) || bullet !== null && m[2] !== bullet) break;
1572
+ const contentIndent = m[1].length + m[2].length + Math.max(1, Math.min(m[3].length, 4));
1573
+ const body = [m[4]];
1574
+ i++;
1575
+ let blank = false;
1576
+ while (i < lines.length) {
1577
+ const l = lines[i];
1578
+ if (l.trim() === "") {
1579
+ blank = true;
1580
+ body.push("");
1581
+ i++;
1582
+ continue;
1583
+ }
1584
+ const lead = l.length - l.trimStart().length;
1585
+ if (lead >= contentIndent) {
1586
+ body.push(l.slice(contentIndent));
1587
+ blank = false;
1588
+ i++;
1589
+ continue;
1590
+ }
1591
+ if (!blank && !reItem.test(l) && !reHeading.test(l) && !reQuote.test(l) && !reFence.test(l) && !reRule.test(l)) {
1592
+ body.push(l.trimStart());
1593
+ i++;
1594
+ continue;
1595
+ }
1596
+ break;
1597
+ }
1598
+ while (body.length && body[body.length - 1].trim() === "") body.pop();
1599
+ let checkbox = null;
1600
+ const task = /^\[([ xX])\](?:[ \t]+|$)/.exec(body[0] ?? "");
1601
+ if (task) {
1602
+ checkbox = task[1] !== " ";
1603
+ body[0] = body[0].slice(task[0].length);
1604
+ }
1605
+ items.push({ checkbox, children: blocks(body) });
1606
+ if (blank && i < lines.length && lines[i - 1]?.trim() === "") {
1607
+ const next = reItem.exec(lines[i]);
1608
+ if (!next || next[1].length !== indent) break;
1609
+ }
1610
+ }
1611
+ return { block: { kind: "list", ordered, items }, next: i };
1612
+ }
1613
+ function inline(src, style = {}) {
1614
+ const out = [];
1615
+ const push = (text, s) => {
1616
+ if (text) out.push({ ...s, text });
1617
+ };
1618
+ let i = 0, buf = "";
1619
+ const emit = () => {
1620
+ push(buf, style);
1621
+ buf = "";
1622
+ };
1623
+ while (i < src.length) {
1624
+ const rest = src.slice(i);
1625
+ let m;
1626
+ if (rest[0] === "\\" && /^\\[\\`*_{}[\]()#+\-.!|~>]/.test(rest)) {
1627
+ buf += rest[1];
1628
+ i += 2;
1629
+ continue;
1630
+ }
1631
+ if (rest.startsWith("\\\n")) {
1632
+ buf += SEP;
1633
+ i += 2;
1634
+ while (src[i] === " ") i++;
1635
+ continue;
1636
+ }
1637
+ if (rest[0] === "\n") {
1638
+ buf = buf.replace(/ +$/, "");
1639
+ buf += SEP;
1640
+ i++;
1641
+ while (src[i] === " ") i++;
1642
+ continue;
1643
+ }
1644
+ if (m = /^(`+)([\s\S]*?[^`])\1(?!`)/.exec(rest)) {
1645
+ emit();
1646
+ let code = m[2].replace(/\n/g, " ");
1647
+ if (/^ .* $/.test(code)) code = code.slice(1, -1);
1648
+ push(code, { ...style, mono: true });
1649
+ i += m[0].length;
1650
+ continue;
1651
+ }
1652
+ if (m = /^!\[([^\]]*)\]\([^)]*\)/.exec(rest)) {
1653
+ emit();
1654
+ out.push(...inline(m[1], style));
1655
+ i += m[0].length;
1656
+ continue;
1657
+ }
1658
+ if (m = /^\[([^\]]+)\]\([^)]*\)/.exec(rest)) {
1659
+ emit();
1660
+ out.push(...inline(m[1], { ...style, link: true }));
1661
+ i += m[0].length;
1662
+ continue;
1663
+ }
1664
+ if (m = /^<((?:https?|ftp|mailto):[^>\s]+)>/.exec(rest)) {
1665
+ emit();
1666
+ push(m[1], { ...style, link: true });
1667
+ i += m[0].length;
1668
+ continue;
1669
+ }
1670
+ if (m = /^~~(?=\S)([\s\S]*?\S)~~/.exec(rest)) {
1671
+ emit();
1672
+ out.push(...inline(m[1], { ...style, strike: true }));
1673
+ i += m[0].length;
1674
+ continue;
1675
+ }
1676
+ if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
1677
+ emit();
1678
+ out.push(...inline(m[2], { ...style, bold: true }));
1679
+ i += m[0].length;
1680
+ continue;
1681
+ }
1682
+ if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
1683
+ emit();
1684
+ out.push(...inline(m[2], { ...style, italic: true }));
1685
+ i += m[0].length;
1686
+ continue;
1687
+ }
1688
+ buf += rest[0];
1689
+ i++;
1690
+ }
1691
+ emit();
1692
+ return underlineBareURLs(out);
1693
+ }
1694
+ function underlineBareURLs(runs) {
1695
+ const url = /\b(?:(?:https?|ftp):\/\/|www\.)[^\s<]*[^\s<.,:;"')\]!?]/g;
1696
+ return runs.flatMap((r) => {
1697
+ if (r.link || r.mono || !(r.text.includes("://") || r.text.includes("www."))) return [r];
1698
+ const out = [];
1699
+ let last = 0;
1700
+ for (const m of r.text.matchAll(url)) {
1701
+ if (m.index > last) out.push({ ...r, text: r.text.slice(last, m.index) });
1702
+ out.push({ ...r, text: m[0], link: true });
1703
+ last = m.index + m[0].length;
1704
+ }
1705
+ if (last < r.text.length) out.push({ ...r, text: r.text.slice(last) });
1706
+ return out;
1707
+ });
1708
+ }
1709
+ function face(r) {
1710
+ return r.bold ? r.italic ? "boldItalic" : "bold" : r.italic ? "italic" : "regular";
1711
+ }
1712
+ function charWidth(ch, r) {
1713
+ if (r.mono && fallbackMetrics[ch] === void 0) return monoAdvance * r.size;
1714
+ const f = fonts[face(r)];
1715
+ return (f.w[ch] ?? fonts.regular.w[ch] ?? f.w["n"]) * r.size / 1e3;
1716
+ }
1717
+ function kern(a, b, r) {
1718
+ if (r.mono) return 0;
1719
+ return (fonts[face(r)].k[a + b] ?? 0) * r.size / 1e3;
1720
+ }
1721
+ function metrics(ch, r) {
1722
+ const fb = fallbackMetrics[ch];
1723
+ if (fb) return [fb[0] * r.size, fb[1] * r.size, fb[2] * r.size];
1724
+ if (r.mono) return [monoAscent * r.size, monoDescent * r.size, 0];
1725
+ if (r.bold && r.italic) return [boldItalicAscent * r.size, boldItalicDescent * r.size, boldItalicLeading * r.size];
1726
+ if (r.bold) return [boldAscent * r.size, boldDescent * r.size, boldLeading * r.size];
1727
+ if (r.italic) return [italicAscent * r.size, italicDescent * r.size, italicLeading * r.size];
1728
+ return [ascent * r.size, descent * r.size, leading * r.size];
1729
+ }
1730
+ var Builder = class {
1731
+ constructor(fontSize, width) {
1732
+ __publicField(this, "paras", []);
1733
+ __publicField(this, "nextSpacing", null);
1734
+ __publicField(this, "fontSize");
1735
+ __publicField(this, "width");
1736
+ this.fontSize = fontSize;
1737
+ this.width = width;
1738
+ }
1739
+ get gap() {
1740
+ return this.fontSize * 0.5;
1741
+ }
1742
+ spacing(normal) {
1743
+ return this.paras.length === 0 ? 0 : this.nextSpacing ?? normal;
1744
+ }
1745
+ run(i, size, alpha, base = {}) {
1746
+ const mono = !!i.mono;
1747
+ return {
1748
+ text: i.text,
1749
+ size: mono ? size * 0.9 : size,
1750
+ bold: !!(i.bold || base.bold),
1751
+ italic: !!(i.italic || base.italic),
1752
+ mono,
1753
+ strike: !!i.strike,
1754
+ underline: !!i.link,
1755
+ alpha
1756
+ };
1757
+ }
1758
+ paragraph(p, quote) {
1759
+ this.paras.push({ tabs: [], rule: null, tableRule: null, ...p, quote });
1760
+ this.nextSpacing = null;
1761
+ }
1762
+ text(runs, ctx, spacing, extra = {}) {
1763
+ if (ctx.marker) {
1764
+ const lead = { text: ctx.marker.text + " ", size: this.fontSize, bold: false, italic: false, mono: false, strike: false, underline: false, alpha: 1 };
1765
+ this.paragraph({ runs: [lead, ...runs], spacing, firstIndent: ctx.marker.start, indent: ctx.marker.hang, tabs: [{ align: "left", x: ctx.marker.hang }], ...extra }, ctx.quote);
1766
+ } else {
1767
+ this.paragraph({ runs, spacing, firstIndent: ctx.indent, indent: ctx.indent, ...extra }, ctx.quote);
1768
+ }
1769
+ }
1770
+ block(b, ctx) {
1771
+ const quoted = ctx.quote !== null;
1772
+ const alpha = quoted ? 0.7 : 1;
1773
+ switch (b.kind) {
1774
+ case "heading": {
1775
+ const scale = [1.5, 1.3, 1.15, 1.05, 1, 1][Math.min(Math.max(b.level, 1), 6) - 1];
1776
+ this.text(b.inline.map((i) => this.run(i, this.fontSize * scale, alpha, { bold: true, italic: quoted })), ctx, this.spacing(this.gap * 1.4));
1777
+ break;
1778
+ }
1779
+ case "paragraph":
1780
+ this.text(b.inline.map((i) => this.run(i, this.fontSize, alpha, { italic: quoted })), ctx, this.spacing(this.gap));
1781
+ break;
1782
+ case "list": {
1783
+ const markers = b.items.map((it, i) => it.checkbox === true ? "\u2611" : it.checkbox === false ? "\u2610" : b.ordered !== null ? `${b.ordered + i}.` : ctx.listDepth % 2 === 0 ? "\u2022" : "\u25E6");
1784
+ const plain = (s) => measureRun({ text: s, size: this.fontSize, bold: false, italic: false, mono: false, strike: false, underline: false, alpha: 1 });
1785
+ const widest = Math.max(0, ...markers.map(plain));
1786
+ const hang = ctx.indent + Math.max(widest + this.fontSize * 0.45, this.fontSize * 1.2);
1787
+ b.items.forEach((item, i) => {
1788
+ if (i > 0 || ctx.listDepth > 0 || ctx.marker) this.nextSpacing = this.fontSize * 0.15;
1789
+ const inner = { ...ctx, marker: { text: markers[i], start: ctx.indent, hang }, indent: hang, listDepth: ctx.listDepth + 1 };
1790
+ if (item.children.length === 0) this.text([], inner, this.spacing(this.gap));
1791
+ for (const child of item.children) {
1792
+ this.block(child, inner);
1793
+ inner.marker = null;
1794
+ this.nextSpacing = this.fontSize * 0.3;
1795
+ }
1796
+ this.nextSpacing = null;
1797
+ });
1798
+ break;
1799
+ }
1800
+ case "quote": {
1801
+ const inner = { ...ctx, quote: ctx.indent + 3, indent: ctx.indent + this.fontSize };
1802
+ for (const child of b.children) this.block(child, inner);
1803
+ break;
1804
+ }
1805
+ case "code": {
1806
+ const body = b.text === "" ? " " : b.text.replace(/\n/g, SEP);
1807
+ const run = { text: body, size: this.fontSize * 0.85, bold: false, italic: false, mono: true, strike: false, underline: false, alpha };
1808
+ this.text([run], { ...ctx, indent: ctx.indent + this.fontSize * 0.5 }, this.spacing(this.gap));
1809
+ break;
1810
+ }
1811
+ case "rule": {
1812
+ const run = { text: " ", size: this.fontSize * 0.6, bold: false, italic: false, mono: false, strike: false, underline: false, alpha: 1 };
1813
+ this.text([run], ctx, this.spacing(this.gap), { rule: ctx.indent });
1814
+ break;
1815
+ }
1816
+ case "table":
1817
+ this.table(b, ctx);
1818
+ break;
1819
+ }
1820
+ }
1821
+ table(t, ctx) {
1822
+ const rows = [{ cells: t.head, header: true }, ...t.rows.map((r) => ({ cells: r, header: false }))];
1823
+ const columns = Math.max(1, ...rows.map((r) => r.cells.length));
1824
+ const quoted = ctx.quote !== null;
1825
+ const cell = (c, header, size2) => c.map((i) => ({ ...this.run({ ...i, text: i.text.replaceAll(SEP, " ") }, size2, quoted ? 0.7 : 1, { bold: header, italic: quoted }) }));
1826
+ const widths = (size2) => {
1827
+ const w2 = new Array(columns).fill(0);
1828
+ for (const row of rows) row.cells.forEach((c, i) => {
1829
+ if (i < columns) w2[i] = Math.max(w2[i], cell(c, row.header, size2).reduce((s, r) => s + measureRun(r), 0));
1830
+ });
1831
+ return w2;
1832
+ };
1833
+ const available = Math.max(this.width - ctx.indent, 1);
1834
+ let size = this.fontSize;
1835
+ let w = widths(size);
1836
+ let gutter = size * 1.2;
1837
+ const natural = w.reduce((a, b) => a + b, 0) + gutter * (columns - 1);
1838
+ if (natural > available) {
1839
+ size = Math.max(this.fontSize * available / natural, Math.min(this.fontSize, 9));
1840
+ w = widths(size);
1841
+ gutter = size * 1.2;
1842
+ }
1843
+ const starts = [];
1844
+ let x = ctx.indent;
1845
+ for (const wi of w) {
1846
+ starts.push(x);
1847
+ x += wi + gutter;
1848
+ }
1849
+ const total = x - gutter - ctx.indent;
1850
+ const tabs = [];
1851
+ let leadingTab = false;
1852
+ for (let i = 0; i < columns; i++) {
1853
+ const align = t.align[i] ?? null;
1854
+ if (align === "center") tabs.push({ align: "center", x: starts[i] + w[i] / 2 });
1855
+ else if (align === "right") tabs.push({ align: "right", x: starts[i] + w[i] });
1856
+ else if (i > 0) tabs.push({ align: "left", x: starts[i] });
1857
+ if (i === 0 && (align === "center" || align === "right")) leadingTab = true;
1858
+ }
1859
+ const plain = (s) => ({ text: s, size, bold: false, italic: false, mono: false, strike: false, underline: false, alpha: 1 });
1860
+ rows.forEach((row, n) => {
1861
+ const runs = [];
1862
+ if (leadingTab) runs.push(plain(" "));
1863
+ for (let i = 0; i < columns; i++) {
1864
+ if (i > 0) runs.push(plain(" "));
1865
+ if (i < row.cells.length) runs.push(...cell(row.cells[i], row.header, size));
1866
+ }
1867
+ if (runs.length === 0) runs.push(plain(" "));
1868
+ const before = n === 0 ? this.spacing(this.gap) : n === 1 ? size * 0.45 : size * 0.2;
1869
+ this.paragraph({ runs, spacing: before, firstIndent: ctx.indent, indent: ctx.indent, tabs, tableRule: row.header ? [ctx.indent, total] : null }, ctx.quote);
1870
+ });
1871
+ }
1872
+ };
1873
+ function measureRun(r) {
1874
+ let w = 0, prev = "";
1875
+ for (const ch of r.text) {
1876
+ w += charWidth(ch, r);
1877
+ if (prev) w += kern(prev, ch, r);
1878
+ prev = ch;
1879
+ }
1880
+ return w;
1881
+ }
1882
+ var TAB_INTERVAL = 28;
1883
+ function layoutLines(paras, width, height, fontSize) {
1884
+ const lines = [];
1885
+ let baseline = 0;
1886
+ let prev = null;
1887
+ paras.forEach((p, pi) => {
1888
+ const runs = pi < paras.length - 1 ? [...p.runs, { text: "\n", size: fontSize, bold: false, italic: false, mono: false, strike: false, underline: false, alpha: 1 }] : p.runs;
1889
+ const chars = [];
1890
+ runs.forEach((r, ri) => {
1891
+ for (const ch of r.text) chars.push({ ch, run: ri });
1892
+ });
1893
+ let k = 0;
1894
+ let firstLine = true;
1895
+ while (k < chars.length || firstLine && chars.length === 0) {
1896
+ const start = firstLine ? p.firstIndent : p.indent;
1897
+ const maxX2 = width;
1898
+ const glyphs = [];
1899
+ let x = start, lastSpace = -1, end = k;
1900
+ for (; end < chars.length; end++) {
1901
+ const { ch, run } = chars[end];
1902
+ const r = runs[run];
1903
+ if (ch === SEP || ch === "\n") {
1904
+ glyphs.push({ ch, run, x, w: 0 });
1905
+ end++;
1906
+ break;
1907
+ }
1908
+ if (ch === " ") {
1909
+ const stop = p.tabs.find((t) => t.x > x + 0.01);
1910
+ let tx;
1911
+ if (stop) {
1912
+ if (stop.align === "left") tx = stop.x;
1913
+ else {
1914
+ let seg = 0, prevCh = "";
1915
+ for (let q = end + 1; q < chars.length && chars[q].ch !== " " && chars[q].ch !== "\n" && chars[q].ch !== SEP; q++) {
1916
+ const rr = runs[chars[q].run];
1917
+ seg += charWidth(chars[q].ch, rr) + (prevCh ? kern(prevCh, chars[q].ch, rr) : 0);
1918
+ prevCh = chars[q].ch;
1919
+ }
1920
+ tx = Math.max(x, stop.align === "right" ? stop.x - seg : stop.x - seg / 2);
1921
+ }
1922
+ } else tx = (Math.floor(x / TAB_INTERVAL) + 1) * TAB_INTERVAL;
1923
+ glyphs.push({ ch, run, x, w: tx - x });
1924
+ x = tx;
1925
+ continue;
1926
+ }
1927
+ const prevGlyph = glyphs[glyphs.length - 1];
1928
+ const kx = prevGlyph && prevGlyph.run === run && prevGlyph.ch !== " " ? kern(prevGlyph.ch, ch, r) : 0;
1929
+ if (prevGlyph) {
1930
+ prevGlyph.w += kx;
1931
+ x += kx;
1932
+ }
1933
+ const w = charWidth(ch, r);
1934
+ if (ch !== " " && x + w > maxX2 + 0.01 && glyphs.some((g) => g.ch !== " ")) {
1935
+ if (lastSpace >= 0) {
1936
+ glyphs.length = lastSpace + 1;
1937
+ end = k + lastSpace + 1;
1938
+ }
1939
+ if (prevGlyph) prevGlyph.w -= kx;
1940
+ break;
1941
+ }
1942
+ glyphs.push({ ch, run, x, w });
1943
+ x += w;
1944
+ if (breaksAfter(ch, chars[end + 1]?.ch ?? "")) lastSpace = glyphs.length - 1;
1945
+ }
1946
+ if (glyphs.length === 0 && end === k && k < chars.length) {
1947
+ const { ch, run } = chars[k];
1948
+ glyphs.push({ ch, run, x: start, w: charWidth(ch, runs[run]) });
1949
+ end = k + 1;
1950
+ }
1951
+ let asc = 0, desc = 0, lead = 0;
1952
+ for (const g of glyphs) {
1953
+ const [a, d, l] = metrics(g.ch, runs[g.run]);
1954
+ asc = Math.max(asc, a);
1955
+ desc = Math.max(desc, d);
1956
+ lead = Math.max(lead, l);
1957
+ }
1958
+ if (glyphs.length === 0) [asc, desc, lead] = metrics(" ", runs[0] ?? { text: " ", size: fontSize, bold: false, italic: false, mono: false, strike: false, underline: false, alpha: 1 });
1959
+ baseline = prev ? baseline + Math.round(prev.desc) + Math.round(prev.lead) + (firstLine ? p.spacing : 0) + Math.round(asc) : Math.round(asc);
1960
+ if (baseline + desc > height + 0.5) return;
1961
+ const line2 = { glyphs, asc, desc, lead, baseline, para: p, first: firstLine };
1962
+ line2.glyphs.forEach((g) => {
1963
+ g.r = runs[g.run];
1964
+ });
1965
+ lines.push(line2);
1966
+ prev = line2;
1967
+ firstLine = false;
1968
+ k = end;
1969
+ if (chars.length === 0) break;
1970
+ }
1971
+ });
1972
+ return lines;
1973
+ }
1974
+ function noteMarkup(source, e) {
1975
+ const rect2 = textRect({ x: e.x, y: e.y, width: e.width, height: e.height });
1976
+ if (!(rect2.width > 1 && rect2.height > 1)) return "";
1977
+ const b = new Builder(e.style.fontSize, rect2.width);
1978
+ for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
1979
+ const lines = layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize);
1980
+ const ink = color(e.style.stroke);
1981
+ let s = "";
1982
+ const marks = [];
1983
+ for (const l of lines) {
1984
+ const p = l.para;
1985
+ if (p.quote !== null) marks.push([rect2.x + p.quote, rect2.y + l.baseline - l.asc, rect2.x + p.quote, rect2.y + l.baseline + l.desc]);
1986
+ if (p.rule !== null) marks.push([rect2.x + p.rule, rect2.y + l.baseline - 3, rect2.x + rect2.width, rect2.y + l.baseline - 3]);
1987
+ if (p.tableRule) {
1988
+ const y = rect2.y + l.baseline + l.desc + 2;
1989
+ marks.push([rect2.x + p.tableRule[0], y, Math.min(rect2.x + rect2.width, rect2.x + p.tableRule[0] + p.tableRule[1]), y]);
1990
+ }
1991
+ let spans = "";
1992
+ let i = 0;
1993
+ const gs = l.glyphs;
1994
+ while (i < gs.length) {
1995
+ const r = gs[i].r;
1996
+ let j = i;
1997
+ let text = "";
1998
+ const font = fontOf(gs[i].ch);
1999
+ while (j < gs.length && sameStyle(gs[j].r, r) && gs[j].ch !== " " && fontOf(gs[j].ch) === font) {
2000
+ text += gs[j].ch;
2001
+ j++;
2002
+ }
2003
+ const shown = text.replaceAll("\n", "").replaceAll(SEP, "");
2004
+ if (shown.trim() !== "") {
2005
+ let a = `font-size="${fmt(r.size)}"`;
2006
+ if (r.mono) a += ` font-family="Menlo, Consolas, monospace"`;
2007
+ if (r.bold) a += ` font-weight="bold"`;
2008
+ if (r.italic) a += ` font-style="italic"`;
2009
+ if (r.underline) a += ` text-decoration="underline"`;
2010
+ if (r.alpha < 1) a += ` fill-opacity="${fmt(r.alpha * e.style.stroke.a)}"`;
2011
+ spans += `<tspan x="${fmt(rect2.x + gs[i].x)}" ${a}>${escapeXML(shown)}</tspan>`;
2012
+ }
2013
+ if (r.strike && j > i) {
2014
+ const y = l.baseline - metrics(gs[i].ch, r)[0] * 0.32;
2015
+ marks.push([rect2.x + gs[i].x, rect2.y + y, rect2.x + gs[j - 1].x + gs[j - 1].w, rect2.y + y]);
2016
+ }
2017
+ if (j === i) j++;
2018
+ i = j;
2019
+ }
2020
+ if (spans) s += `<text y="${fmt(rect2.y + l.baseline)}" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" fill="${ink}" xml:space="preserve">${spans}</text>
2021
+ `;
2022
+ }
2023
+ const width = fmt(Math.max(1, e.style.fontSize / 14));
2024
+ for (const [x1, y1, x2, y2] of marks) {
2025
+ s += `<line x1="${fmt(x1)}" y1="${fmt(y1)}" x2="${fmt(x2)}" y2="${fmt(y2)}" stroke="${ink}" stroke-width="${width}"/>
2026
+ `;
2027
+ }
2028
+ return s;
2029
+ }
2030
+ function fontOf(ch) {
2031
+ return fallbackMetrics[ch] ? `fb:${ch}` : "";
2032
+ }
2033
+ function sameStyle(a, b) {
2034
+ return a === b || a.size === b.size && a.bold === b.bold && a.italic === b.italic && a.mono === b.mono && a.strike === b.strike && a.underline === b.underline && a.alpha === b.alpha;
2035
+ }
2036
+
2037
+ // src/theme.ts
2038
+ var rgb = (r, g, b) => ({ r, g, b, a: 1 });
2039
+ var themes = [
2040
+ { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13) },
2041
+ { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13) },
2042
+ { id: "sepia", name: "Sepia", background: rgb(0.953, 0.914, 0.824), ink: rgb(0.23, 0.18, 0.13) },
2043
+ { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984) },
2044
+ { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918) },
2045
+ { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94) }
2046
+ ];
2047
+ function themeNamed(id) {
2048
+ return themes.find((t) => t.id === id) ?? null;
2049
+ }
2050
+
2051
+ // src/timeline.ts
2052
+ function applyEase(ease, u) {
2053
+ switch (ease) {
2054
+ case "linear":
2055
+ return u;
2056
+ case "easeIn":
2057
+ return u * u * u;
2058
+ case "easeOut":
2059
+ return 1 - Math.pow(1 - u, 3);
2060
+ case "easeInOut":
2061
+ return u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
2062
+ case "hold":
2063
+ return 0;
2064
+ }
2065
+ }
2066
+ var rem = (a, b) => a % b;
2067
+ function localTime(track, t) {
2068
+ const reps = (track.repeats ?? []).filter(repeatIsValid).slice().sort((a, b) => a.start - b.start);
2069
+ if (reps.length === 0) return t;
2070
+ for (const r of reps) {
2071
+ if (!(t >= r.end)) continue;
2072
+ const length = r.end - r.start;
2073
+ if (r.count === null) return r.start + rem(t - r.start, length);
2074
+ const played = length * r.count;
2075
+ if (t < r.start + played) return r.start + rem(t - r.start, length);
2076
+ t -= played - length;
2077
+ }
2078
+ return t;
2079
+ }
2080
+ function value(track, t, get, base, lerp) {
2081
+ t = localTime(track, t);
2082
+ let before, after;
2083
+ for (const k of track.keys) {
2084
+ if (get(k) === void 0) continue;
2085
+ if (k.t <= t) before = k;
2086
+ else {
2087
+ after = k;
2088
+ break;
2089
+ }
2090
+ }
2091
+ if (!before && !after) return base;
2092
+ if (before && !after) return get(before);
2093
+ if (!before && after) return get(after);
2094
+ const b = before, a = after;
2095
+ const u = a.t - b.t > 0 ? (t - b.t) / (a.t - b.t) : 1;
2096
+ return lerp(get(b), get(a), applyEase(b.ease ?? "linear", u));
2097
+ }
2098
+ var lerpNum = (a, b, u) => a + (b - a) * u;
2099
+ var lerpColor = (a, b, u) => ({
2100
+ r: a.r + (b.r - a.r) * u,
2101
+ g: a.g + (b.g - a.g) * u,
2102
+ b: a.b + (b.b - a.b) * u,
2103
+ a: a.a + (b.a - a.a) * u
2104
+ });
2105
+ function num2(track, t, p, base) {
2106
+ return value(track, t, (k) => k[p], base, lerpNum);
2107
+ }
2108
+ function isChanging(track, t) {
2109
+ t = localTime(track, t);
2110
+ const changing = (get, eq) => {
2111
+ let b;
2112
+ for (const k of track.keys) if (k.t <= t && get(k) !== void 0) b = k;
2113
+ const a = track.keys.find((k) => k.t > t && get(k) !== void 0);
2114
+ if (!b || !a) return false;
2115
+ return b.ease !== "hold" && !eq(get(b), get(a));
2116
+ };
2117
+ const n = (p) => changing((k) => k[p], (a, b) => a === b);
2118
+ return n("x") || n("y") || n("width") || n("height") || n("rotation") || n("strokeWidth") || changing((k) => k.stroke, sameColor) || changing((k) => k.fill, sameColor) || n("reveal") || n("opacity");
2119
+ }
2120
+ function isVisible(track, t) {
2121
+ t = localTime(track, t);
2122
+ const keyed = track.keys.filter((k) => k.visible !== void 0);
2123
+ if (keyed.length === 0) return true;
2124
+ let last;
2125
+ for (const k of keyed) if (k.t <= t) last = k;
2126
+ return last ? last.visible : !keyed[0].visible;
2127
+ }
2128
+ function applyTrack(track, e, t) {
2129
+ e = cloneElement(e);
2130
+ if (track.keys.some((k) => k.reveal !== void 0)) {
2131
+ const r = num2(track, t, "reveal", e.reveal ?? 1);
2132
+ e.reveal = r >= 1 ? null : Math.max(0, r);
2133
+ }
2134
+ if (track.keys.some((k) => k.opacity !== void 0)) {
2135
+ const o = Math.min(Math.max(num2(track, t, "opacity", e.style.opacity ?? 1), 0), 1);
2136
+ e.style.opacity = o >= 1 ? null : o;
2137
+ }
2138
+ if (e.kind === "connector") return e;
2139
+ e.x = num2(track, t, "x", e.x);
2140
+ e.y = num2(track, t, "y", e.y);
2141
+ e.width = num2(track, t, "width", e.width);
2142
+ e.height = num2(track, t, "height", e.height);
2143
+ if (track.keys.some((k) => k.rotation !== void 0)) e.rotation = num2(track, t, "rotation", angle(e));
2144
+ e.style.strokeWidth = num2(track, t, "strokeWidth", e.style.strokeWidth);
2145
+ e.style.stroke = value(track, t, (k) => k.stroke, e.style.stroke, lerpColor);
2146
+ e.style.fill = value(track, t, (k) => k.fill, e.style.fill, lerpColor);
2147
+ return e;
2148
+ }
2149
+ function followSpan(track, t) {
2150
+ t = localTime(track, t);
2151
+ let f = null;
2152
+ for (const s of track.follow ?? []) if (s.start <= t && (f === null || s.start > f.start)) f = s;
2153
+ if (!f) return null;
2154
+ const span = f;
2155
+ if (track.keys.some((k) => k.t >= span.end && k.t <= t && (k.x !== void 0 || k.y !== void 0))) return null;
2156
+ return f;
2157
+ }
2158
+ function tracing(e, page) {
2159
+ if (e.kind === "connector") return new Polyline(routePoints(route(e, indexOf(page))));
2160
+ const clean = cloneElement(e);
2161
+ clean.style.roughness = 0;
2162
+ const pts = outline(clean).contours()[0] ?? [];
2163
+ return new Polyline(angle(e) === 0 ? pts : pts.map((p) => rotated(e, p)));
2164
+ }
2165
+ function following(track, e, t, page) {
2166
+ const f = followSpan(track, t);
2167
+ if (!f) return e;
2168
+ t = localTime(track, t);
2169
+ const pathElement = page.elements.find((x) => x.id === f.path);
2170
+ if (!pathElement || pathElement.id === e.id) return e;
2171
+ const line2 = tracing(pathElement, page);
2172
+ const span = f.end - f.start;
2173
+ let u = applyEase(f.ease ?? "easeInOut", span > 0 ? Math.min(1, (t - f.start) / span) : 1) * (f.laps ?? 1);
2174
+ if (u > 1) u = rem(u, 1) === 0 ? 1 : rem(u, 1);
2175
+ if (f.reverse === true) u = 1 - u;
2176
+ const p = line2.point(u);
2177
+ const moved = cloneElement(e);
2178
+ moved.x = p.x - e.width / 2;
2179
+ moved.y = p.y - e.height / 2;
2180
+ if (f.orient === true) {
2181
+ const step = f.reverse === true ? -0.01 : 0.01;
2182
+ const a = line2.point(u - step / 2), b = line2.point(u + step / 2);
2183
+ if (a.x !== b.x || a.y !== b.y) moved.rotation = Math.atan2(b.y - a.y, b.x - a.x);
2184
+ }
2185
+ return moved;
2186
+ }
2187
+ function trackMap(tl) {
2188
+ const m = /* @__PURE__ */ new Map();
2189
+ for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
2190
+ return m;
2191
+ }
2192
+ function resolved(page, t) {
2193
+ const timeline = page.timeline;
2194
+ if (!timeline) return page;
2195
+ t = clampTime(t);
2196
+ const tracks = trackMap(timeline);
2197
+ const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
2198
+ const hidden = /* @__PURE__ */ new Set();
2199
+ for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
2200
+ let moving = null;
2201
+ if (timeline.settle === true) {
2202
+ moving = /* @__PURE__ */ new Set();
2203
+ for (const tr of tracks.values()) if (isChanging(tr, t)) moving.add(tr.element);
2204
+ }
2205
+ const boils = (e) => {
2206
+ const override = tracks.get(e.id)?.boil;
2207
+ if (override !== void 0 && override !== null) return override;
2208
+ if (!moving) return true;
2209
+ if (e.connector) return [e.connector.start.elementID, e.connector.end.elementID].some((id) => id !== null && moving.has(id));
2210
+ return moving.has(e.id);
2211
+ };
2212
+ let elements = page.elements.map((e) => {
2213
+ const track = tracks.get(e.id);
2214
+ let out = track ? applyTrack(track, e, t) : e;
2215
+ if (reseed !== null && reseed > 0 && boils(e)) {
2216
+ if (out === e) out = cloneElement(e);
2217
+ out.seed = boiled(e.seed, reseed);
2218
+ }
2219
+ return out;
2220
+ });
2221
+ elements = elements.map((e, i) => hidden.has(e.id) ? page.elements[i] : e);
2222
+ const posed = { ...page, elements, timeline: null };
2223
+ elements = elements.map((e) => {
2224
+ const track = tracks.get(e.id);
2225
+ return track ? following(track, e, t, posed) : e;
2226
+ });
2227
+ const current = { ...page, elements, timeline: null };
2228
+ const index = indexOf(current);
2229
+ const gone = new Set(hidden);
2230
+ elements = elements.map((e) => {
2231
+ const c = e.connector;
2232
+ if (!c || ![c.start.elementID, c.end.elementID].some((id) => id !== null && hidden.has(id))) return e;
2233
+ if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false)) {
2234
+ gone.add(e.id);
2235
+ return e;
2236
+ }
2237
+ const [a, b] = endpoints(e, index);
2238
+ const pinned = cloneElement(e);
2239
+ pinned.connector.start = { elementID: null, port: c.start.port, x: a.x, y: a.y };
2240
+ pinned.connector.end = { elementID: null, port: c.end.port, x: b.x, y: b.y };
2241
+ return pinned;
2242
+ });
2243
+ return { ...page, elements: elements.filter((e) => !gone.has(e.id)), timeline: null };
2244
+ }
2245
+ function posterTime(page) {
2246
+ return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
2247
+ }
2248
+ function still(page) {
2249
+ return page.timeline ? resolved(page, posterTime(page)) : page;
2250
+ }
2251
+ function cameraRect(tl, t) {
2252
+ const keys = tl.camera ?? [];
2253
+ if (keys.length === 0) return null;
2254
+ let before;
2255
+ for (const k of keys) if (k.t <= t) before = k;
2256
+ const after = keys.find((k) => k.t > t);
2257
+ if (!before) return rectOf(keys[0]);
2258
+ if (!after) return rectOf(before);
2259
+ const u = applyEase(before.ease ?? "easeInOut", (t - before.t) / Math.max(after.t - before.t, 1e-9));
2260
+ const mix2 = (p, q) => p + (q - p) * u;
2261
+ return { x: mix2(before.x, after.x), y: mix2(before.y, after.y), width: mix2(before.width, after.width), height: mix2(before.height, after.height) };
2262
+ }
2263
+ var rectOf = (k) => ({ x: k.x, y: k.y, width: k.width, height: k.height });
2264
+ function cameraView(tl, t) {
2265
+ const first = tl.camera?.[0];
2266
+ const r = cameraRect(tl, t);
2267
+ if (!first || !r || !(r.width > 0 && r.height > 0)) return null;
2268
+ return fit(r, first.width / first.height);
2269
+ }
2270
+ function fit(r, aspect) {
2271
+ if (!(r.width > 0 && r.height > 0 && aspect > 0)) return r;
2272
+ let w = r.width, h = r.height;
2273
+ if (r.width / r.height < aspect) w = r.height * aspect;
2274
+ else h = r.width / aspect;
2275
+ return { x: r.x + r.width / 2 - w / 2, y: r.y + r.height / 2 - h / 2, width: w, height: h };
2276
+ }
2277
+ function frameCount(tl) {
2278
+ return Math.max(1, Math.ceil(clampTime(tl.duration) * clampFPS(tl.fps)));
2279
+ }
2280
+ function frameTimes(tl) {
2281
+ const fps = clampFPS(tl.fps);
2282
+ const times = [];
2283
+ for (let i = 0; i < frameCount(tl); i++) times.push(i / fps);
2284
+ const last = times[times.length - 1];
2285
+ if (tl.duration - last > 1e-6) times.push(tl.duration);
2286
+ return times;
2287
+ }
2288
+ function sceneAt(tl, t) {
2289
+ let s = -1;
2290
+ (tl.markers ?? []).forEach((m, i) => {
2291
+ if (m.t <= t) s = i;
2292
+ });
2293
+ return s;
2294
+ }
2295
+ function transientElements(page) {
2296
+ const tl = page.timeline;
2297
+ if (!tl) return /* @__PURE__ */ new Set();
2298
+ const end = tl.poster ?? tl.duration;
2299
+ return new Set(tl.tracks.filter((tr) => !isVisible(tr, 0) && !isVisible(tr, end)).map((tr) => tr.element));
2300
+ }
2301
+
2302
+ // src/svg.ts
2303
+ function bounds(page, padding = 24) {
2304
+ const index = indexOf(page);
2305
+ const box = (e) => {
2306
+ if (e.kind === "connector") return connectorBounds(e, index);
2307
+ const reach = isFloor(e) ? (BAND + 0.25) * cellSize(e) : 0;
2308
+ return inset(rotatedBounds(e), -reach, -reach);
2309
+ };
2310
+ if (page.elements.length === 0) return { x: 0, y: 0, width: 200, height: 200 };
2311
+ let r = null;
2312
+ for (const e of page.elements) r = union(r, box(e));
2313
+ return inset(r, -padding, -padding);
2314
+ }
2315
+ function plan(page) {
2316
+ const tl = page.timeline ?? { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
2317
+ const times = frameTimes(tl);
2318
+ const first = tl.camera?.find((k) => k.width > 1 && k.height > 1);
2319
+ if (first) {
2320
+ const aspect = first.width / first.height;
2321
+ const rects = times.map((t) => cameraView(tl, t) ?? { x: first.x, y: first.y, width: first.width, height: first.height });
2322
+ const width = Math.min(Math.max(...rects.map((r) => r.width)), 4096);
2323
+ return { times, rects, size: { width, height: width / aspect } };
2324
+ }
2325
+ let b = null;
2326
+ for (const t of times) {
2327
+ const f = resolved(page, t);
2328
+ if (f.elements.length > 0) b = union(b, bounds(f));
2329
+ }
2330
+ if (!b) b = bounds(resolved(page, times[0]));
2331
+ return { times, rects: times.map(() => b), size: { width: b.width, height: b.height } };
2332
+ }
2333
+ var opacity = (c, attr) => c.a < 1 ? ` ${attr}="${fmt(c.a)}"` : "";
2334
+ function svgOpen(b, background, paint) {
2335
+ let out = `<svg xmlns="http://www.w3.org/2000/svg" width="${fmt(b.width)}" height="${fmt(b.height)}" viewBox="${fmt(b.x)} ${fmt(b.y)} ${fmt(b.width)} ${fmt(b.height)}">
2336
+ `;
2337
+ if (paint && background) {
2338
+ out += `<rect x="${fmt(b.x)}" y="${fmt(b.y)}" width="${fmt(b.width)}" height="${fmt(b.height)}" fill="${color(background)}"/>
2339
+ `;
2340
+ }
2341
+ return out;
2342
+ }
2343
+ function svgParts(page, background, fixed) {
2344
+ const theme = themeNamed(page.theme);
2345
+ const bg = background ? theme?.background ?? background : null;
2346
+ const ink = theme?.ink ?? black;
2347
+ const d = { ...page, elements: page.elements.map((e) => resolvingInk(e, ink)) };
2348
+ const b = fixed ?? bounds(d);
2349
+ const visible = new Set(d.layers.filter((l) => l.visible).map((l) => l.id));
2350
+ const parts = [];
2351
+ const index = indexOf(d);
2352
+ const ordered = orderedElements(d);
2353
+ const floors = ordered.filter((e) => visible.has(e.layerID) && isFloor(e));
2354
+ const rockMarkup = rock(floors).map((r) => `<path d="${r.path.data()}" fill="none" stroke="${color(r.ink)}"${opacity(r.ink, "stroke-opacity")} stroke-width="${fmt(r.width)}" stroke-linecap="round"/>
2355
+ `).join("");
2356
+ if (rockMarkup) parts.push({ key: "rock", markup: rockMarkup });
2357
+ for (const e of ordered) {
2358
+ if (!visible.has(e.layerID)) continue;
2359
+ const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
2360
+ if (alpha <= 0) continue;
2361
+ let body = svgElement(e, index, bg);
2362
+ if (angle(e) !== 0 && e.kind !== "connector") {
2363
+ body = `<g transform="rotate(${fmt(angle(e) * 180 / Math.PI)} ${fmt(midX(frame(e)))} ${fmt(midY(frame(e)))})">
2364
+ ${body}</g>
2365
+ `;
2366
+ }
2367
+ parts.push({ key: e.id, markup: alpha < 1 ? `<g opacity="${fmt(alpha)}">
2368
+ ${body}</g>
2369
+ ` : body });
2370
+ }
2371
+ return { bounds: b, parts };
2372
+ }
2373
+ function svg(page, background = { r: 1, g: 1, b: 1, a: 1 }, fixed) {
2374
+ const { bounds: b, parts } = svgParts(page, background, fixed);
2375
+ return svgOpen(b, themeNamed(page.theme)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
2376
+ }
2377
+ function labelRect(e, index) {
2378
+ const m = midpoint(e, index);
2379
+ const text = e.text ?? "";
2380
+ const size = textSize(text === "" ? "Label" : text, e.style.fontSize, 200);
2381
+ const w = Math.max(size.width + 12, 44), h = size.height + 6;
2382
+ return { x: m.x - w / 2, y: m.y - h / 2, width: w, height: h };
2383
+ }
2384
+ function typed(s, progress) {
2385
+ const chars = [...s];
2386
+ return chars.slice(0, Math.floor(chars.length * progress)).join("");
2387
+ }
2388
+ function revealProgress(e) {
2389
+ return e.reveal !== null && e.reveal < 1 ? Math.max(0, e.reveal) : null;
2390
+ }
2391
+ function svgElement(e, index, labelBackground) {
2392
+ const stroke = color(e.style.stroke);
2393
+ let strokeAttrs = `stroke="${stroke}" stroke-width="${fmt(e.style.strokeWidth)}" stroke-linecap="round" stroke-linejoin="round"`;
2394
+ const dash = dashPattern(e.style.dash, e.style.strokeWidth);
2395
+ if (dash) strokeAttrs += ` stroke-dasharray="${dash.map(fmt).join(" ")}"`;
2396
+ let text = e.text;
2397
+ const r = revealProgress(e);
2398
+ if (r !== null) {
2399
+ if (e.kind === "text") text = typed(text ?? "", r);
2400
+ else if (e.kind === "connector") {
2401
+ return `<path d="${connectorParts(e, index)[0].trimmed(r).data()}" fill="none" ${strokeAttrs}/>
2402
+ `;
2403
+ } else {
2404
+ return `<path d="${outline(e).trimmed(r).data()}" fill="none" ${strokeAttrs}/>
2405
+ `;
2406
+ }
2407
+ }
2408
+ switch (e.kind) {
2409
+ case "connector": {
2410
+ const [line2, filled] = connectorParts(e, index);
2411
+ let s = `<path d="${line2.data()}" fill="none" ${strokeAttrs}/>
2412
+ `;
2413
+ if (!filled.isEmpty) s += `<path d="${filled.data()}" fill="${stroke}" stroke="none"/>
2414
+ `;
2415
+ if (text) {
2416
+ const lr = labelRect(e, index);
2417
+ if (labelBackground) s += `<rect x="${fmt(lr.x)}" y="${fmt(lr.y)}" width="${fmt(lr.width)}" height="${fmt(lr.height)}" fill="${color(labelBackground)}"/>
2418
+ `;
2419
+ s += textElement(text, lr, e, true);
2420
+ }
2421
+ return s;
2422
+ }
2423
+ case "text":
2424
+ return textElement(text ?? "", frame(e), e, false);
2425
+ case "image": {
2426
+ const f = frame(e);
2427
+ let s = "";
2428
+ if (e.image) {
2429
+ const mime = e.image.format === "jpeg" ? "image/jpeg" : "image/png";
2430
+ s += `<image x="${fmt(f.x)}" y="${fmt(f.y)}" width="${fmt(f.width)}" height="${fmt(f.height)}" preserveAspectRatio="none" href="data:${mime};base64,${e.image.data}"/>
2431
+ `;
2432
+ }
2433
+ if (e.style.stroke.a > 0 && e.style.strokeWidth > 0) s += `<path d="${outline(e).data()}" fill="none" ${strokeAttrs}/>
2434
+ `;
2435
+ return s;
2436
+ }
2437
+ case "freehand":
2438
+ return `<path d="${outline(e).data()}" fill="none" ${strokeAttrs}/>
2439
+ `;
2440
+ case "rectangle":
2441
+ case "ellipse":
2442
+ case "diamond":
2443
+ case "path":
2444
+ case "note": {
2445
+ let s = "";
2446
+ if (e.kind === "note" && e.style.fill.a > 0) {
2447
+ s += `<path d="${fillPath(e).translated(2, 3).data()}" fill="#000000" fill-opacity="${fmt(0.14 * e.style.fill.a)}" stroke="none"/>
2448
+ `;
2449
+ }
2450
+ if (e.style.fill.a > 0) {
2451
+ const rule = e.evenOdd === true ? ` fill-rule="evenodd"` : "";
2452
+ if (e.style.fillStyle === "solid") {
2453
+ s += `<path d="${fillPath(e).data()}" fill="${color(e.style.fill)}"${opacity(e.style.fill, "fill-opacity")}${rule} stroke="none"/>
2454
+ `;
2455
+ } else if (e.style.fillStyle === "grid") {
2456
+ const line2 = lineColor(e.style.fill);
2457
+ s += `<path d="${fillPath(e).data()}" fill="${color(e.style.fill)}"${opacity(e.style.fill, "fill-opacity")}${rule} stroke="none"/>
2458
+ `;
2459
+ s += `<path d="${hachurePath(e).data()}" fill="none" stroke="${color(line2)}"${opacity(line2, "stroke-opacity")} stroke-width="1" stroke-linecap="round"/>
2460
+ `;
2461
+ } else {
2462
+ s += `<path d="${hachurePath(e).data()}" fill="none" stroke="${color(e.style.fill)}"${opacity(e.style.fill, "stroke-opacity")} stroke-width="${fmt(Math.max(1, e.style.strokeWidth / 2))}" stroke-linecap="round"/>
2463
+ `;
2464
+ }
2465
+ }
2466
+ s += `<path d="${outline(e).data()}" fill="none" ${strokeAttrs}/>
2467
+ `;
2468
+ if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, frame(e), e, true);
2469
+ return s;
2470
+ }
2471
+ default:
2472
+ return placeholder(e, strokeAttrs);
2473
+ }
2474
+ }
2475
+ function placeholder(e, strokeAttrs) {
2476
+ const f = frame(e);
2477
+ const p = Path.rect(f).moveTo({ x: f.x, y: f.y }).lineTo({ x: f.x + f.width, y: f.y + f.height });
2478
+ return `<path d="${p.data()}" fill="none" ${strokeAttrs} stroke-opacity="0.5" stroke-dasharray="6 4"/>
2479
+ ` + textElement(`${e.kind}${e.text ? `: ${e.text}` : ""}`, f, { ...e, style: { ...e.style, fontSize: Math.min(e.style.fontSize, 12) } }, true);
2480
+ }
2481
+ function textElement(text, f, e, centered) {
2482
+ const size = e.style.fontSize;
2483
+ const lines = text.split("\n");
2484
+ const lineHeight = size * 1.2;
2485
+ const total = lineHeight * lines.length;
2486
+ const x = centered ? midX(f) : f.x;
2487
+ const top = centered ? midY(f) - total / 2 : f.y;
2488
+ let s = `<text x="${fmt(x)}" font-family="Helvetica Neue, Helvetica, Arial, sans-serif" font-size="${fmt(size)}" fill="${color(e.style.stroke)}"${centered ? ` text-anchor="middle"` : ""}>`;
2489
+ lines.forEach((line2, i) => {
2490
+ s += `<tspan x="${fmt(x)}" y="${fmt(top + lineHeight * (i + 0.8))}">${escapeXML(line2)}</tspan>`;
2491
+ });
2492
+ return s + "</text>\n";
2493
+ }
2494
+
2495
+ // src/player.ts
2496
+ var WHITE = { r: 1, g: 1, b: 1, a: 1 };
2497
+ function pageIndex(file, spec) {
2498
+ const s = (spec ?? "").trim();
2499
+ if (!s) return 0;
2500
+ const byName = file.pages.findIndex((p) => p.name.toLowerCase() === s.toLowerCase());
2501
+ if (byName >= 0) return byName;
2502
+ const n = Number(s);
2503
+ return Number.isInteger(n) && n >= 1 && n <= file.pages.length ? n - 1 : 0;
2504
+ }
2505
+ function withRoughness(page, roughness) {
2506
+ return { ...page, elements: page.elements.map((e) => ({ ...e, style: { ...e.style, roughness } })) };
2507
+ }
2508
+ var STYLE = `
2509
+ :host { display: block; position: relative; color: var(--dottie-ink); background: var(--dottie-page);
2510
+ font: 14px -apple-system, system-ui, sans-serif; border-radius: var(--dottie-radius, 0); }
2511
+ :host([hidden]) { display: none; }
2512
+ .stage { display: block; width: 100%; height: auto; }
2513
+ .stage svg { display: block; width: 100%; height: auto; margin: 0 auto; }
2514
+ .bar { display: flex; align-items: center; gap: 12px; padding: 8px 12px 12px; max-width: 560px; margin: 0 auto; box-sizing: border-box; }
2515
+ .bar[hidden] { display: none; }
2516
+ button { font: inherit; color: inherit; background: none; border: 1.5px solid currentColor; border-radius: 6px;
2517
+ width: 34px; height: 30px; cursor: pointer; flex: none; padding: 0; }
2518
+ button:focus-visible, input:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
2519
+ input[type=range] { flex: 1; min-width: 60px; accent-color: currentColor; }
2520
+ output { font-variant-numeric: tabular-nums; min-width: 4.5em; text-align: right; }
2521
+ .scene { min-width: 0; flex: 0 1 auto; text-align: center; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2522
+ .error { padding: 16px; font-family: ui-monospace, Menlo, monospace; white-space: pre-wrap; }
2523
+ `;
2524
+ var Playback = class {
2525
+ constructor(page, into, options = {}) {
2526
+ __publicField(this, "page");
2527
+ __publicField(this, "plan");
2528
+ __publicField(this, "scenes");
2529
+ __publicField(this, "sceneNames");
2530
+ __publicField(this, "background");
2531
+ __publicField(this, "ink");
2532
+ __publicField(this, "svg");
2533
+ __publicField(this, "groups", /* @__PURE__ */ new Map());
2534
+ __publicField(this, "order", []);
2535
+ __publicField(this, "frame", -1);
2536
+ __publicField(this, "onFrame", null);
2537
+ this.page = options.clean ? withRoughness(page, 0) : page;
2538
+ this.plan = plan(this.page);
2539
+ const near = (t) => {
2540
+ let best = 0;
2541
+ this.plan.times.forEach((x, i) => {
2542
+ if (Math.abs(x - t) < Math.abs(this.plan.times[best] - t)) best = i;
2543
+ });
2544
+ return best;
2545
+ };
2546
+ this.scenes = (page.timeline?.markers ?? []).map((m) => near(m.t));
2547
+ this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
2548
+ const theme = themeNamed(page.theme);
2549
+ this.background = theme?.background ?? WHITE;
2550
+ this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
2551
+ const ns = "http://www.w3.org/2000/svg";
2552
+ this.svg = document.createElementNS(ns, "svg");
2553
+ this.svg.setAttribute("width", fmt(this.plan.size.width));
2554
+ this.svg.setAttribute("height", fmt(this.plan.size.height));
2555
+ this.svg.style.maxWidth = `${Math.ceil(this.plan.size.width)}px`;
2556
+ into.replaceChildren(this.svg);
2557
+ }
2558
+ get frameCount() {
2559
+ return this.plan.times.length;
2560
+ }
2561
+ get posterFrame() {
2562
+ const t = posterTime(this.page);
2563
+ let best = 0;
2564
+ this.plan.times.forEach((x, i) => {
2565
+ if (Math.abs(x - t) < Math.abs(this.plan.times[best] - t)) best = i;
2566
+ });
2567
+ return best;
2568
+ }
2569
+ /** Draws frame `i`, replacing only the pieces whose markup changed since the last frame. */
2570
+ show(i) {
2571
+ const n = this.frameCount;
2572
+ i = (i % n + n) % n;
2573
+ this.drawAt(this.plan.times[i], i);
2574
+ this.frame = i;
2575
+ this.onFrame?.(i);
2576
+ }
2577
+ /** Draws the moment `t` (seconds) in the frame-`i` view. */
2578
+ drawAt(t, i = 0) {
2579
+ const r = this.plan.rects[Math.min(i, this.plan.rects.length - 1)];
2580
+ const { parts } = svgParts(resolved(this.page, t), this.background, r);
2581
+ this.svg.setAttribute("viewBox", `${fmt(r.x)} ${fmt(r.y)} ${fmt(r.width)} ${fmt(r.height)}`);
2582
+ const ns = "http://www.w3.org/2000/svg";
2583
+ const keys = parts.map((p) => p.key);
2584
+ for (const p of parts) {
2585
+ const have = this.groups.get(p.key);
2586
+ if (have) {
2587
+ if (have.markup !== p.markup) {
2588
+ have.g.innerHTML = p.markup;
2589
+ have.markup = p.markup;
2590
+ }
2591
+ } else {
2592
+ const g = document.createElementNS(ns, "g");
2593
+ g.innerHTML = p.markup;
2594
+ this.groups.set(p.key, { g, markup: p.markup });
2595
+ }
2596
+ }
2597
+ const live = new Set(keys);
2598
+ for (const [k, v] of this.groups) if (!live.has(k)) {
2599
+ v.g.remove();
2600
+ this.groups.delete(k);
2601
+ }
2602
+ if (keys.length !== this.order.length || keys.some((k, j) => k !== this.order[j])) {
2603
+ this.svg.replaceChildren(...keys.map((k) => this.groups.get(k).g));
2604
+ this.order = keys;
2605
+ }
2606
+ }
2607
+ /** The scene frame `i` is in, or -1 before the first. */
2608
+ sceneAt(i) {
2609
+ let s = -1;
2610
+ this.scenes.forEach((f, j) => {
2611
+ if (f <= i) s = j;
2612
+ });
2613
+ return s;
2614
+ }
2615
+ };
2616
+ var ElementBase = typeof HTMLElement === "undefined" ? class {
2617
+ } : HTMLElement;
2618
+ var DottiePlayer = class extends ElementBase {
2619
+ constructor() {
2620
+ super();
2621
+ __publicField(this, "file", null);
2622
+ __publicField(this, "playback", null);
2623
+ __publicField(this, "root");
2624
+ __publicField(this, "stage");
2625
+ __publicField(this, "bar");
2626
+ __publicField(this, "playButton");
2627
+ __publicField(this, "scrub");
2628
+ __publicField(this, "time");
2629
+ __publicField(this, "sceneLabel");
2630
+ __publicField(this, "prev");
2631
+ __publicField(this, "next");
2632
+ __publicField(this, "playing", false);
2633
+ __publicField(this, "last", 0);
2634
+ __publicField(this, "acc", 0);
2635
+ __publicField(this, "raf", 0);
2636
+ __publicField(this, "loadToken", 0);
2637
+ __publicField(this, "visible", true);
2638
+ __publicField(this, "observer", null);
2639
+ this.root = this.attachShadow({ mode: "open" });
2640
+ this.root.innerHTML = `<style>${STYLE}</style><div class="stage" part="stage" role="img"></div>
2641
+ <div class="bar" part="controls" hidden>
2642
+ <button class="play" aria-label="Play">\u25B6</button>
2643
+ <button class="prev" aria-label="Previous scene" hidden>\u23EE</button>
2644
+ <output class="scene" hidden></output>
2645
+ <button class="next" aria-label="Next scene" hidden>\u23ED</button>
2646
+ <input class="scrub" type="range" min="0" max="0" value="0" aria-label="Frame">
2647
+ <output class="time">0.0s</output>
2648
+ </div>`;
2649
+ const q = (s) => this.root.querySelector(s);
2650
+ this.stage = q(".stage");
2651
+ this.bar = q(".bar");
2652
+ this.playButton = q(".play");
2653
+ this.scrub = q(".scrub");
2654
+ this.time = q(".time");
2655
+ this.sceneLabel = q(".scene");
2656
+ this.prev = q(".prev");
2657
+ this.next = q(".next");
2658
+ this.playButton.onclick = () => {
2659
+ const p = this.playback;
2660
+ if (p && !this.playing && p.scenes.length && p.frame >= p.frameCount - 1) p.show(0);
2661
+ this.setPlaying(!this.playing);
2662
+ };
2663
+ this.scrub.oninput = () => {
2664
+ this.setPlaying(false);
2665
+ this.playback?.show(Number(this.scrub.value));
2666
+ };
2667
+ this.next.onclick = () => this.nextScene();
2668
+ this.prev.onclick = () => this.prevScene();
2669
+ this.addEventListener("keydown", (e) => this.key(e));
2670
+ }
2671
+ connectedCallback() {
2672
+ if (!this.hasAttribute("tabindex")) this.tabIndex = 0;
2673
+ if ("IntersectionObserver" in window) {
2674
+ this.observer = new IntersectionObserver((entries) => {
2675
+ this.visible = entries.some((e) => e.isIntersecting);
2676
+ this.schedule();
2677
+ });
2678
+ this.observer.observe(this);
2679
+ }
2680
+ void this.load();
2681
+ }
2682
+ disconnectedCallback() {
2683
+ cancelAnimationFrame(this.raf);
2684
+ this.raf = 0;
2685
+ this.observer?.disconnect();
2686
+ }
2687
+ attributeChangedCallback(name, old, value2) {
2688
+ if (old === value2 || !this.isConnected) return;
2689
+ if (name === "src") void this.load();
2690
+ else if (name === "label") this.stage.setAttribute("aria-label", value2 ?? "");
2691
+ else if (this.file) this.mount();
2692
+ }
2693
+ /** Loads from `src`, or from an inline JSON script child. */
2694
+ async load() {
2695
+ const token = ++this.loadToken;
2696
+ try {
2697
+ let text;
2698
+ const src = this.getAttribute("src");
2699
+ if (src) {
2700
+ const res = await fetch(src);
2701
+ if (!res.ok) throw new Error(`${src}: ${res.status} ${res.statusText}`);
2702
+ text = await res.text();
2703
+ } else {
2704
+ const inline2 = this.querySelector('script[type="application/json"], script[type="application/vnd.dottie+json"]');
2705
+ if (!inline2) return;
2706
+ text = inline2.textContent ?? "";
2707
+ }
2708
+ if (token !== this.loadToken) return;
2709
+ this.setFile(parseDottie(text));
2710
+ } catch (err) {
2711
+ if (token !== this.loadToken) return;
2712
+ this.stage.innerHTML = "";
2713
+ const box = document.createElement("div");
2714
+ box.className = "error";
2715
+ box.textContent = `Couldn't show this drawing: ${err.message}`;
2716
+ this.stage.append(box);
2717
+ this.dispatchEvent(new CustomEvent("error", { detail: err }));
2718
+ }
2719
+ }
2720
+ /** Shows an already-parsed file. */
2721
+ setFile(file) {
2722
+ this.file = file;
2723
+ for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
2724
+ this.mount();
2725
+ this.dispatchEvent(new CustomEvent("load", { detail: { file } }));
2726
+ }
2727
+ mount() {
2728
+ const file = this.file;
2729
+ const page = file.pages[pageIndex(file, this.getAttribute("page"))];
2730
+ const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean") });
2731
+ this.playback = p;
2732
+ this.style.setProperty("--dottie-page", color(p.background));
2733
+ this.style.setProperty("--dottie-ink", color(p.ink));
2734
+ this.style.colorScheme = luminance(p.background) < 0.5 ? "dark" : "light";
2735
+ this.stage.setAttribute("aria-label", this.getAttribute("label") ?? page.name);
2736
+ const at = this.getAttribute("at");
2737
+ const animated = page.timeline !== null && p.frameCount > 1 && at === null;
2738
+ this.bar.hidden = !animated || this.hasAttribute("no-controls");
2739
+ const scenes = p.scenes.length > 0;
2740
+ this.prev.hidden = this.next.hidden = this.sceneLabel.hidden = !scenes;
2741
+ this.scrub.max = String(p.frameCount - 1);
2742
+ p.onFrame = (i) => {
2743
+ this.scrub.value = String(i);
2744
+ this.time.textContent = `${p.plan.times[i].toFixed(1)}s`;
2745
+ if (scenes) {
2746
+ const s = p.sceneAt(i);
2747
+ this.sceneLabel.textContent = s < 0 ? "" : p.sceneNames[s];
2748
+ }
2749
+ this.dispatchEvent(new CustomEvent("frame", { detail: { frame: i, time: p.plan.times[i] } }));
2750
+ };
2751
+ if (!animated) {
2752
+ p.drawAt(at !== null ? Number(at) || 0 : posterTime(page), at !== null ? nearest(p, Number(at) || 0) : p.posterFrame);
2753
+ this.setPlaying(false);
2754
+ return;
2755
+ }
2756
+ p.show(0);
2757
+ const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
2758
+ if (reduced) {
2759
+ p.show(p.posterFrame);
2760
+ this.setPlaying(false);
2761
+ return;
2762
+ }
2763
+ this.setPlaying(!this.hasAttribute("no-autoplay") && !p.scenes.includes(0));
2764
+ }
2765
+ // MARK: Playback
2766
+ get paused() {
2767
+ return !this.playing;
2768
+ }
2769
+ get currentTime() {
2770
+ const p = this.playback;
2771
+ return p && p.frame >= 0 ? p.plan.times[p.frame] : 0;
2772
+ }
2773
+ set currentTime(t) {
2774
+ const p = this.playback;
2775
+ if (p) {
2776
+ this.setPlaying(false);
2777
+ p.show(nearest(p, t));
2778
+ }
2779
+ }
2780
+ get duration() {
2781
+ return this.playback?.page.timeline?.duration ?? 0;
2782
+ }
2783
+ play() {
2784
+ this.setPlaying(true);
2785
+ }
2786
+ pause() {
2787
+ this.setPlaying(false);
2788
+ }
2789
+ /** Plays on to the next scene (from the start after the last). */
2790
+ nextScene() {
2791
+ const p = this.playback;
2792
+ if (!p) return;
2793
+ if (p.frame >= p.frameCount - 1) p.show(0);
2794
+ this.setPlaying(true);
2795
+ }
2796
+ /** Back to the start of this scene, or the one before if already there. */
2797
+ prevScene() {
2798
+ const p = this.playback;
2799
+ if (!p) return;
2800
+ let s = p.sceneAt(p.frame);
2801
+ if (s >= 0 && p.scenes[s] === p.frame) s -= 1;
2802
+ this.setPlaying(false);
2803
+ p.show(s >= 0 ? p.scenes[s] : 0);
2804
+ }
2805
+ setPlaying(on) {
2806
+ const p = this.playback;
2807
+ this.playing = on && !!p && p.frameCount > 1 && p.page.timeline !== null;
2808
+ this.playButton.textContent = this.playing ? "\u275A\u275A" : "\u25B6";
2809
+ this.playButton.setAttribute("aria-label", this.playing ? "Pause" : "Play");
2810
+ this.last = 0;
2811
+ this.acc = 0;
2812
+ this.schedule();
2813
+ }
2814
+ schedule() {
2815
+ if (this.playing && this.visible && !this.raf && this.isConnected) this.raf = requestAnimationFrame((now) => this.tick(now));
2816
+ }
2817
+ /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end. */
2818
+ tick(now) {
2819
+ this.raf = 0;
2820
+ const p = this.playback;
2821
+ if (!this.playing || !p) return;
2822
+ const step = 1e3 / (p.page.timeline?.fps ?? 12);
2823
+ if (this.last) this.acc += Math.min(now - this.last, 1e3);
2824
+ this.last = now;
2825
+ let advanced = false;
2826
+ while (this.playing && this.acc >= step) {
2827
+ this.acc -= step;
2828
+ const next = p.frame + 1;
2829
+ if (next >= p.frameCount && (p.scenes.length || this.hasAttribute("no-loop"))) {
2830
+ this.setPlaying(false);
2831
+ break;
2832
+ }
2833
+ p.frame = next % p.frameCount;
2834
+ advanced = true;
2835
+ if (p.scenes.includes(p.frame)) {
2836
+ this.setPlaying(false);
2837
+ break;
2838
+ }
2839
+ }
2840
+ if (advanced) p.show(p.frame);
2841
+ if (this.playing && !this.visible) this.last = 0;
2842
+ this.schedule();
2843
+ }
2844
+ key(e) {
2845
+ const p = this.playback;
2846
+ if (!p || p.page.timeline === null || p.frameCount <= 1 || this.hasAttribute("at")) return;
2847
+ if (e.target === this.scrub && e.key !== " ") return;
2848
+ const scenes = p.scenes.length > 0;
2849
+ let handled = true;
2850
+ if (e.key === " ") scenes ? this.playing ? this.setPlaying(false) : this.nextScene() : this.setPlaying(!this.playing);
2851
+ else if (e.key === "ArrowRight") {
2852
+ if (scenes) this.nextScene();
2853
+ else {
2854
+ this.setPlaying(false);
2855
+ p.show(p.frame + 1);
2856
+ }
2857
+ } else if (e.key === "ArrowLeft") {
2858
+ if (scenes) this.prevScene();
2859
+ else {
2860
+ this.setPlaying(false);
2861
+ p.show(p.frame - 1);
2862
+ }
2863
+ } else if (e.key === ".") {
2864
+ this.setPlaying(false);
2865
+ p.show(p.frame + 1);
2866
+ } else if (e.key === ",") {
2867
+ this.setPlaying(false);
2868
+ p.show(p.frame - 1);
2869
+ } else handled = false;
2870
+ if (handled) e.preventDefault();
2871
+ }
2872
+ };
2873
+ __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label"]);
2874
+ function nearest(p, t) {
2875
+ let best = 0;
2876
+ p.plan.times.forEach((x, i) => {
2877
+ if (Math.abs(x - t) < Math.abs(p.plan.times[best] - t)) best = i;
2878
+ });
2879
+ return best;
2880
+ }
2881
+ function definePlayer(tag = "dottie-player") {
2882
+ if (typeof customElements !== "undefined" && !customElements.get(tag)) customElements.define(tag, class extends DottiePlayer {
2883
+ });
2884
+ }
2885
+
2886
+ // src/inspect.ts
2887
+ var r4 = (v) => {
2888
+ const r = roundHalfAway(v * 1e4) / 1e4;
2889
+ return r === 0 ? 0 : r;
2890
+ };
2891
+ var pt = (p) => [r4(p.x), r4(p.y)];
2892
+ var rect = (r) => [r4(r.x), r4(r.y), r4(r.width), r4(r.height)];
2893
+ var rgba2 = (c) => [r4(c.r), r4(c.g), r4(c.b), r4(c.a)];
2894
+ function sampleTimes(page) {
2895
+ const tl = page.timeline;
2896
+ if (!tl) return [0];
2897
+ const d = tl.duration;
2898
+ const times = [0, d];
2899
+ for (let i = 1; i < 16; i++) times.push(d * (i + 0.37) / 16);
2900
+ for (const m of tl.markers ?? []) times.push(m.t);
2901
+ if (tl.poster !== null) times.push(tl.poster);
2902
+ return [...new Set(times.map(r4))].filter((t) => t >= 0 && t <= d).sort((a, b) => a - b);
2903
+ }
2904
+ function inspectFrame(page, t) {
2905
+ const f = resolved(page, t);
2906
+ const index = indexOf(f);
2907
+ const visible = new Set(f.layers.filter((l) => l.visible).map((l) => l.id));
2908
+ const out = { t: r4(t) };
2909
+ const tl = page.timeline;
2910
+ if (tl) {
2911
+ const v = cameraView(tl, t);
2912
+ out.view = v ? rect(v) : null;
2913
+ let scene = -1;
2914
+ (tl.markers ?? []).forEach((m, i) => {
2915
+ if (m.t <= t) scene = i;
2916
+ });
2917
+ out.scene = scene;
2918
+ }
2919
+ if (!tl?.camera?.length) out.bounds = rect(bounds(f));
2920
+ out.elements = orderedElements(f).map((e, z) => {
2921
+ const d = { id: e.id, kind: e.kind, z, seed: e.seed, frame: rect({ x: e.x, y: e.y, width: e.width, height: e.height }) };
2922
+ if (!visible.has(e.layerID)) d.hiddenLayer = true;
2923
+ if (e.rotation !== null) d.rotation = r4(e.rotation);
2924
+ if (e.style.opacity !== null) d.opacity = r4(e.style.opacity);
2925
+ if (e.reveal !== null) d.reveal = r4(e.reveal);
2926
+ d.strokeWidth = r4(e.style.strokeWidth);
2927
+ d.stroke = rgba2(e.style.stroke);
2928
+ d.fill = rgba2(e.style.fill);
2929
+ if (e.kind === "connector") {
2930
+ const [a, b] = endpoints(e, index);
2931
+ d.start = pt(a);
2932
+ d.end = pt(b);
2933
+ d.route = routePoints(route(e, index)).map(pt);
2934
+ }
2935
+ return d;
2936
+ });
2937
+ return out;
2938
+ }
2939
+ function inspectPage(page, n, times) {
2940
+ const out = { page: n + 1, name: page.name, id: page.id };
2941
+ const tl = page.timeline;
2942
+ if (tl) {
2943
+ const p = plan(page);
2944
+ out.duration = r4(tl.duration);
2945
+ out.fps = r4(tl.fps);
2946
+ out.frameCount = p.times.length;
2947
+ out.poster = r4(tl.poster ?? tl.duration);
2948
+ out.size = [r4(p.size.width), r4(p.size.height)];
2949
+ if (!tl.camera?.length) out.bounds = rect(p.rects[0]);
2950
+ out.markers = (tl.markers ?? []).map((m) => ({ t: r4(m.t), name: m.name }));
2951
+ } else {
2952
+ out.bounds = rect(bounds(page));
2953
+ }
2954
+ out.frames = times.map((t) => inspectFrame(page, t));
2955
+ return out;
2956
+ }
2957
+ function inspect(file, times) {
2958
+ return { version: file.version, pages: file.pages.map((p, i) => inspectPage(p, i, times ?? sampleTimes(p))) };
2959
+ }
2960
+
2961
+ // src/index.ts
2962
+ function render(source, options = {}) {
2963
+ const file = isFile(source) ? source : parseDottie(source);
2964
+ let page = file.pages[pageIndex(file, options.page === void 0 ? null : String(options.page))];
2965
+ page = resolved(page, options.at ?? posterTime(page));
2966
+ if (options.clean) page = withRoughness(page, 0);
2967
+ return svg(page, options.background === void 0 ? { r: 1, g: 1, b: 1, a: 1 } : options.background);
2968
+ }
2969
+ function isFile(v) {
2970
+ return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
2971
+ }
2972
+ var VERSION = "0.1.0";
2973
+ export {
2974
+ BOIL_DRAWINGS,
2975
+ DottieFormatError,
2976
+ DottiePlayer,
2977
+ KINDS,
2978
+ KNOWN_FIELDS,
2979
+ MAX_DURATION,
2980
+ Playback,
2981
+ SUPPORTED_VERSION,
2982
+ VERSION,
2983
+ black,
2984
+ bounds,
2985
+ cameraView,
2986
+ clampFPS,
2987
+ clampTime,
2988
+ clear,
2989
+ cloneElement,
2990
+ definePlayer,
2991
+ elementIndex,
2992
+ frameTimes,
2993
+ inkOnDark,
2994
+ inspect,
2995
+ inspectFrame,
2996
+ inspectPage,
2997
+ isAutomaticInk,
2998
+ luminance,
2999
+ orderedElements,
3000
+ pageIndex,
3001
+ parseDottie,
3002
+ plan,
3003
+ posterTime,
3004
+ render,
3005
+ repeatIsValid,
3006
+ resolved,
3007
+ sameColor,
3008
+ sampleTimes,
3009
+ sceneAt,
3010
+ stableSortBy,
3011
+ still,
3012
+ svg,
3013
+ svgParts,
3014
+ themeNamed,
3015
+ themes,
3016
+ transientElements,
3017
+ withRoughness
3018
+ };