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