keyframes 0.1.0__py3-none-any.whl

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.
Files changed (77) hide show
  1. keyframes/__init__.py +1 -0
  2. keyframes/build.py +102 -0
  3. keyframes/check.py +107 -0
  4. keyframes/cli.py +149 -0
  5. keyframes/deck.py +67 -0
  6. keyframes/export.py +106 -0
  7. keyframes/guide/__init__.py +25 -0
  8. keyframes/guide/components/keyframes-code.md +60 -0
  9. keyframes/guide/components/keyframes-excalidraw.md +148 -0
  10. keyframes/guide/components/keyframes-mujoco.md +76 -0
  11. keyframes/guide/components/keyframes-slider.md +65 -0
  12. keyframes/guide/components/keyframes-three.md +81 -0
  13. keyframes/guide/core.md +113 -0
  14. keyframes/patch.py +77 -0
  15. keyframes/render.py +32 -0
  16. keyframes/scaffold/.gitignore +3 -0
  17. keyframes/scaffold/CLAUDE.md +1 -0
  18. keyframes/scaffold/assets/deck.css +1 -0
  19. keyframes/scaffold/assets/figure.svg +1 -0
  20. keyframes/scaffold/deck.toml +10 -0
  21. keyframes/scaffold/references/.gitkeep +0 -0
  22. keyframes/scaffold/slides/01-title.html +4 -0
  23. keyframes/scaffold/slides/02-intro.html +17 -0
  24. keyframes/server.py +204 -0
  25. keyframes/static/components/keyframes-code.js +423 -0
  26. keyframes/static/components/keyframes-excalidraw.js +189 -0
  27. keyframes/static/components/keyframes-mujoco.js +515 -0
  28. keyframes/static/components/keyframes-slider.js +255 -0
  29. keyframes/static/components/keyframes-three.js +224 -0
  30. keyframes/static/editor/editor.css +228 -0
  31. keyframes/static/editor/editor.js +594 -0
  32. keyframes/static/editor/panel.css +127 -0
  33. keyframes/static/editor/panel.js +357 -0
  34. keyframes/static/index.html +23 -0
  35. keyframes/static/runtime/component.js +126 -0
  36. keyframes/static/runtime/live.js +31 -0
  37. keyframes/static/runtime/math.js +33 -0
  38. keyframes/static/runtime/presenter.js +91 -0
  39. keyframes/static/runtime/runtime.css +91 -0
  40. keyframes/static/runtime/runtime.js +215 -0
  41. keyframes/static/runtime/steps.js +15 -0
  42. keyframes/terminal.py +144 -0
  43. keyframes/themes/default/STYLE_GUIDE.md +73 -0
  44. keyframes/themes/default/assets/arc-top.webp +0 -0
  45. keyframes/themes/default/assets/orbit-ring.webp +0 -0
  46. keyframes/themes/default/assets/streaks-top.webp +0 -0
  47. keyframes/themes/default/assets/streaks-wide.webp +0 -0
  48. keyframes/themes/default/assets/sun-top.webp +0 -0
  49. keyframes/themes/default/assets/title-orbit.webp +0 -0
  50. keyframes/themes/default/layouts/content.html +16 -0
  51. keyframes/themes/default/layouts/figure.html +7 -0
  52. keyframes/themes/default/layouts/grid-2.html +19 -0
  53. keyframes/themes/default/layouts/grid-3x2.html +28 -0
  54. keyframes/themes/default/layouts/grid-4.html +44 -0
  55. keyframes/themes/default/layouts/media-side.html +16 -0
  56. keyframes/themes/default/layouts/section-arc.html +4 -0
  57. keyframes/themes/default/layouts/section-streaks.html +4 -0
  58. keyframes/themes/default/layouts/section.html +4 -0
  59. keyframes/themes/default/layouts/split-wide.html +7 -0
  60. keyframes/themes/default/layouts/split.html +13 -0
  61. keyframes/themes/default/layouts/statement.html +4 -0
  62. keyframes/themes/default/layouts/title.html +5 -0
  63. keyframes/themes/default/layouts/two-column.html +5 -0
  64. keyframes/themes/default/theme.css +330 -0
  65. keyframes/themes/minimal/STYLE_GUIDE.md +41 -0
  66. keyframes/themes/minimal/layouts/figure.html +7 -0
  67. keyframes/themes/minimal/layouts/section.html +4 -0
  68. keyframes/themes/minimal/layouts/statement.html +3 -0
  69. keyframes/themes/minimal/layouts/title-body.html +10 -0
  70. keyframes/themes/minimal/layouts/title.html +5 -0
  71. keyframes/themes/minimal/layouts/two-column.html +11 -0
  72. keyframes/themes/minimal/theme.css +196 -0
  73. keyframes/vendor.py +181 -0
  74. keyframes-0.1.0.dist-info/METADATA +119 -0
  75. keyframes-0.1.0.dist-info/RECORD +77 -0
  76. keyframes-0.1.0.dist-info/WHEEL +4 -0
  77. keyframes-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,127 @@
1
+ html {
2
+ --keyframes-panel-space: 0px;
3
+ }
4
+
5
+ html.keyframes-panel-open {
6
+ --keyframes-panel-space: var(--keyframes-panel-w, 640px);
7
+ }
8
+
9
+ html.keyframes-panel-open .keyframes-deck {
10
+ right: var(--keyframes-panel-space);
11
+ }
12
+
13
+ html.keyframes-panel-resizing,
14
+ html.keyframes-panel-resizing * {
15
+ cursor: col-resize !important;
16
+ user-select: none;
17
+ }
18
+
19
+ html.keyframes-panel-resizing iframe,
20
+ html.keyframes-panel-resizing .keyframes-panel-body {
21
+ pointer-events: none;
22
+ }
23
+
24
+ .keyframes-panel {
25
+ position: fixed;
26
+ top: 0;
27
+ right: 0;
28
+ bottom: 0;
29
+ z-index: 100002;
30
+ width: var(--keyframes-panel-w, 640px);
31
+ display: flex;
32
+ flex-direction: column;
33
+ background: #1b1c21;
34
+ border-left: 1px solid #2c2e36;
35
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.18);
36
+ font: 13px/1.3 system-ui, -apple-system, sans-serif;
37
+ color: #e9ecf2;
38
+ }
39
+
40
+ .keyframes-panel[hidden] {
41
+ display: none;
42
+ }
43
+
44
+ .keyframes-panel-resize {
45
+ position: absolute;
46
+ left: -4px;
47
+ top: 0;
48
+ bottom: 0;
49
+ width: 8px;
50
+ cursor: col-resize;
51
+ z-index: 1;
52
+ }
53
+
54
+ .keyframes-panel-tabs {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 2px;
58
+ padding: 6px 8px 0;
59
+ background: #23252c;
60
+ border-bottom: 1px solid #2c2e36;
61
+ }
62
+
63
+ .keyframes-panel-tabs button {
64
+ padding: 6px 12px;
65
+ color: #9aa3b2;
66
+ background: transparent;
67
+ border: 0;
68
+ border-radius: 6px 6px 0 0;
69
+ font: inherit;
70
+ cursor: pointer;
71
+ }
72
+
73
+ .keyframes-panel-tabs button:hover {
74
+ color: #e9ecf2;
75
+ }
76
+
77
+ .keyframes-panel-tabs button.is-active {
78
+ color: #fff;
79
+ background: #1b1c21;
80
+ }
81
+
82
+ .keyframes-panel-tabs .keyframes-panel-ref,
83
+ .keyframes-panel-tabs .keyframes-panel-close {
84
+ margin-bottom: 6px;
85
+ border-radius: 5px;
86
+ background: #2f323b;
87
+ color: #e9ecf2;
88
+ }
89
+
90
+ .keyframes-panel-tabs .keyframes-panel-ref[hidden] {
91
+ display: none;
92
+ }
93
+
94
+ .keyframes-panel-slide {
95
+ margin-left: auto;
96
+ padding: 0 8px 6px;
97
+ color: #6f7787;
98
+ font-family: ui-monospace, Menlo, monospace;
99
+ font-size: 12px;
100
+ }
101
+
102
+ .keyframes-panel-body {
103
+ position: relative;
104
+ flex: 1;
105
+ min-height: 0;
106
+ }
107
+
108
+ .keyframes-panel-pane {
109
+ position: absolute;
110
+ inset: 0;
111
+ overflow: hidden;
112
+ }
113
+
114
+ .keyframes-panel-pane[hidden] {
115
+ display: none;
116
+ }
117
+
118
+ .keyframes-panel-pane[data-pane="source"] .cm-editor {
119
+ height: 100%;
120
+ font-size: 13px;
121
+ }
122
+
123
+ .keyframes-panel-pane .xterm {
124
+ height: 100%;
125
+ padding: 6px 0 0 8px;
126
+ box-sizing: border-box;
127
+ }
@@ -0,0 +1,357 @@
1
+ // Edit-mode side panel: the current slide's source, plus Claude Code / Codex / shell terminals
2
+ // running in the deck folder.
3
+
4
+ const LABELS = { claude: "Claude", codex: "Codex", shell: "Shell" };
5
+ const SAVE_DELAY_MS = 700;
6
+
7
+ function addStylesheet(href) {
8
+ const link = document.createElement("link");
9
+ link.rel = "stylesheet";
10
+ link.href = href;
11
+ document.head.append(link);
12
+ }
13
+
14
+ function socketUrl(path) {
15
+ const url = new URL(path, location.href);
16
+ url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
17
+ url.search = url.hash = "";
18
+ return url;
19
+ }
20
+
21
+ function slideName(deck) {
22
+ return deck.current?.dataset.slide;
23
+ }
24
+
25
+ class SourceTab {
26
+ constructor(deck) {
27
+ this.deck = deck;
28
+ this.slide = null;
29
+ this.dirty = false;
30
+ this.applying = false;
31
+ }
32
+
33
+ async mount(container) {
34
+ const [state, view, commands, language, { html }, { oneDark }] = await Promise.all([
35
+ import("@codemirror/state"),
36
+ import("@codemirror/view"),
37
+ import("@codemirror/commands"),
38
+ import("@codemirror/language"),
39
+ import("@codemirror/lang-html"),
40
+ import("@codemirror/theme-one-dark"),
41
+ ]);
42
+ this.cm = { state, view };
43
+ this.view = new view.EditorView({
44
+ parent: container,
45
+ state: state.EditorState.create({
46
+ extensions: [
47
+ view.lineNumbers(),
48
+ view.highlightActiveLine(),
49
+ view.drawSelection(),
50
+ view.EditorView.lineWrapping,
51
+ commands.history(),
52
+ language.bracketMatching(),
53
+ html(),
54
+ oneDark,
55
+ view.keymap.of([
56
+ { key: "Mod-s", run: () => (this.save(), true) },
57
+ commands.indentWithTab,
58
+ ...commands.defaultKeymap,
59
+ ...commands.historyKeymap,
60
+ ]),
61
+ view.EditorView.updateListener.of((u) => u.docChanged && !this.applying && this.scheduleSave()),
62
+ ],
63
+ }),
64
+ });
65
+ await this.load(slideName(this.deck));
66
+ }
67
+
68
+ get text() {
69
+ return this.view.state.doc.toString();
70
+ }
71
+
72
+ async fetchSource(name) {
73
+ const response = await fetch(`_api/source/${encodeURIComponent(name)}`, { cache: "no-store" });
74
+ return response.ok ? response.text() : "";
75
+ }
76
+
77
+ replaceDoc(text) {
78
+ if (text === this.text) return;
79
+ const head = Math.min(this.view.state.selection.main.head, text.length);
80
+ this.applying = true;
81
+ this.view.dispatch({ changes: { from: 0, to: this.view.state.doc.length, insert: text }, selection: { anchor: head } });
82
+ this.applying = false;
83
+ }
84
+
85
+ async load(name) {
86
+ if (!name || !this.view) return;
87
+ if (this.dirty) await this.save();
88
+ this.slide = name;
89
+ this.replaceDoc(await this.fetchSource(name));
90
+ this.reveal(this.deck.selection?.[0]);
91
+ }
92
+
93
+ async refreshFromDisk() {
94
+ if (!this.view || this.dirty || this.slide !== slideName(this.deck)) return;
95
+ this.replaceDoc(await this.fetchSource(this.slide));
96
+ }
97
+
98
+ scheduleSave() {
99
+ this.dirty = true;
100
+ clearTimeout(this.timer);
101
+ this.timer = setTimeout(() => this.save(), SAVE_DELAY_MS);
102
+ }
103
+
104
+ async save() {
105
+ clearTimeout(this.timer);
106
+ if (!this.dirty || !this.slide) return;
107
+ this.dirty = false;
108
+ await fetch(`_api/source/${encodeURIComponent(this.slide)}`, { method: "PUT", body: this.text });
109
+ }
110
+
111
+ reveal(id) {
112
+ if (!this.view || !id) return;
113
+ const match = new RegExp(`data-block=["']${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`).exec(this.text);
114
+ if (!match) return;
115
+ const line = this.view.state.doc.lineAt(match.index);
116
+ this.view.dispatch({
117
+ selection: { anchor: line.from, head: line.to },
118
+ effects: this.cm.view.EditorView.scrollIntoView(line.from, { y: "center" }),
119
+ });
120
+ }
121
+
122
+ show() {
123
+ this.view?.requestMeasure();
124
+ }
125
+
126
+ focus() {
127
+ this.view?.focus();
128
+ }
129
+ }
130
+
131
+ class TerminalTab {
132
+ constructor(name) {
133
+ this.name = name;
134
+ this.exited = false;
135
+ this.visible = false;
136
+ }
137
+
138
+ async mount(container) {
139
+ const [{ Terminal }, { FitAddon }] = await Promise.all([import("@xterm/xterm"), import("@xterm/addon-fit")]);
140
+ addStylesheet(new URL("css/xterm.css", import.meta.resolve("@xterm/xterm/")).href);
141
+ this.term = new Terminal({
142
+ fontFamily: "Menlo, 'SF Mono', ui-monospace, monospace",
143
+ fontSize: 13,
144
+ cursorBlink: true,
145
+ scrollback: 5000,
146
+ theme: { background: "#1b1c21" },
147
+ });
148
+ this.fitAddon = new FitAddon();
149
+ this.term.loadAddon(this.fitAddon);
150
+ this.term.open(container);
151
+ this.fit();
152
+ this.term.onData((data) => {
153
+ if (this.exited) data === "\r" && this.start();
154
+ else this.send({ type: "input", data });
155
+ });
156
+ this.term.onResize(({ cols, rows }) => this.send({ type: "resize", cols, rows }));
157
+ new ResizeObserver(() => this.visible && this.fit()).observe(container);
158
+ this.connect();
159
+ }
160
+
161
+ connect() {
162
+ this.ws = new WebSocket(socketUrl(`_api/term/${this.name}`));
163
+ let replayed = "";
164
+ this.ws.onmessage = ({ data }) => {
165
+ const message = JSON.parse(data);
166
+ if (message.type === "replay") {
167
+ replayed = message.data;
168
+ this.term.reset();
169
+ this.term.write(replayed);
170
+ this.nudgeRedraw();
171
+ } else if (message.type === "output") this.term.write(message.data);
172
+ else if (message.type === "exit") {
173
+ if (!replayed && !this.everStarted) this.start();
174
+ else this.markExited();
175
+ }
176
+ };
177
+ this.ws.onclose = () => setTimeout(() => this.connect(), 1000);
178
+ }
179
+
180
+ send(message) {
181
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(message));
182
+ }
183
+
184
+ start() {
185
+ this.exited = false;
186
+ this.everStarted = true;
187
+ this.term.reset();
188
+ this.send({ type: "start", cols: this.term.cols, rows: this.term.rows });
189
+ }
190
+
191
+ markExited() {
192
+ this.exited = true;
193
+ this.term.write("\r\n\x1b[2m[process exited · press Enter to restart]\x1b[0m\r\n");
194
+ }
195
+
196
+ nudgeRedraw() {
197
+ const { cols, rows } = this.term;
198
+ this.send({ type: "resize", cols, rows: Math.max(1, rows - 1) });
199
+ this.send({ type: "resize", cols, rows });
200
+ }
201
+
202
+ fit() {
203
+ try {
204
+ this.fitAddon?.fit();
205
+ } catch {}
206
+ }
207
+
208
+ insert(text) {
209
+ this.send({ type: "input", data: text });
210
+ this.term.focus();
211
+ }
212
+
213
+ show() {
214
+ this.fit();
215
+ }
216
+
217
+ focus() {
218
+ this.term?.focus();
219
+ }
220
+ }
221
+
222
+ class Panel {
223
+ constructor(deck, terminals) {
224
+ this.deck = deck;
225
+ this.tabs = { source: new SourceTab(deck) };
226
+ for (const name of terminals) this.tabs[name] = new TerminalTab(name);
227
+ this.mounted = new Set();
228
+ this.active = localStorage.getItem("keyframes-panel-tab") || (terminals.includes("claude") ? "claude" : "source");
229
+ if (!this.tabs[this.active]) this.active = "source";
230
+ this.build(terminals);
231
+ this.listen();
232
+ this.setOpen(document.documentElement.classList.contains("keyframes-editing"));
233
+ }
234
+
235
+ build(terminals) {
236
+ const names = ["source", ...terminals];
237
+ document.body.insertAdjacentHTML(
238
+ "beforeend",
239
+ `<aside class="keyframes-panel" hidden>
240
+ <div class="keyframes-panel-resize"></div>
241
+ <header class="keyframes-panel-tabs">
242
+ ${names.map((n) => `<button data-tab="${n}">${LABELS[n] || "Source"}</button>`).join("")}
243
+ <span class="keyframes-panel-slide"></span>
244
+ <button class="keyframes-panel-ref" title="Insert selected block references into the terminal (R)">+ ref</button>
245
+ <button class="keyframes-panel-close" title="Hide panel (\`)">×</button>
246
+ </header>
247
+ <div class="keyframes-panel-body">${names.map((n) => `<div class="keyframes-panel-pane" data-pane="${n}"></div>`).join("")}</div>
248
+ </aside>`,
249
+ );
250
+ this.el = document.querySelector(".keyframes-panel");
251
+ this.el.querySelector(".keyframes-panel-tabs").addEventListener("click", (e) => {
252
+ const tab = e.target.closest("[data-tab]")?.dataset.tab;
253
+ if (tab) this.select(tab);
254
+ });
255
+ this.el.querySelector(".keyframes-panel-close").onclick = () => this.collapse(true);
256
+ this.el.querySelector(".keyframes-panel-ref").onclick = () => this.insertRefs();
257
+ this.setWidth(Number(localStorage.getItem("keyframes-panel-w")) || Math.min(760, innerWidth * 0.42));
258
+ this.enableResize(this.el.querySelector(".keyframes-panel-resize"));
259
+ }
260
+
261
+ listen() {
262
+ const deck = this.deck;
263
+ deck.addEventListener("editmode", ({ detail }) => this.setOpen(detail.on));
264
+ deck.addEventListener("change", () => {
265
+ this.el.querySelector(".keyframes-panel-slide").textContent = slideName(deck);
266
+ if (this.tabs.source.slide !== slideName(deck)) this.tabs.source.load(slideName(deck));
267
+ });
268
+ deck.addEventListener("slidereplaced", () => this.tabs.source.refreshFromDisk());
269
+ deck.addEventListener("saved", () => this.tabs.source.refreshFromDisk());
270
+ deck.addEventListener("selection", ({ detail }) => this.tabs.source.reveal(detail.ids[0]));
271
+ document.addEventListener(
272
+ "keydown",
273
+ (e) => {
274
+ if (!document.documentElement.classList.contains("keyframes-editing")) return;
275
+ if (e.target.closest?.("input, textarea, [contenteditable], .cm-editor")) return;
276
+ if (e.key === "`") this.collapse(!this.collapsed);
277
+ else if (e.key === "r" && !e.metaKey && !e.ctrlKey) this.insertRefs();
278
+ else return;
279
+ e.preventDefault();
280
+ e.stopImmediatePropagation();
281
+ },
282
+ true,
283
+ );
284
+ }
285
+
286
+ get collapsed() {
287
+ return localStorage.getItem("keyframes-panel") === "closed";
288
+ }
289
+
290
+ collapse(closed) {
291
+ localStorage.setItem("keyframes-panel", closed ? "closed" : "open");
292
+ this.setOpen(document.documentElement.classList.contains("keyframes-editing"));
293
+ }
294
+
295
+ setOpen(editing) {
296
+ const open = editing && !this.collapsed;
297
+ this.el.hidden = !open;
298
+ document.documentElement.classList.toggle("keyframes-panel-open", open);
299
+ if (open) this.select(this.active);
300
+ }
301
+
302
+ async select(name) {
303
+ this.active = name;
304
+ localStorage.setItem("keyframes-panel-tab", name);
305
+ for (const button of this.el.querySelectorAll("[data-tab]")) {
306
+ button.classList.toggle("is-active", button.dataset.tab === name);
307
+ }
308
+ for (const pane of this.el.querySelectorAll("[data-pane]")) pane.hidden = pane.dataset.pane !== name;
309
+ this.el.querySelector(".keyframes-panel-ref").hidden = name === "source";
310
+ for (const [key, tab] of Object.entries(this.tabs)) tab.visible = key === name;
311
+ const tab = this.tabs[name];
312
+ if (!this.mounted.has(name)) {
313
+ this.mounted.add(name);
314
+ await tab.mount(this.el.querySelector(`[data-pane="${name}"]`));
315
+ }
316
+ tab.show();
317
+ tab.focus();
318
+ }
319
+
320
+ insertRefs() {
321
+ const tab = this.tabs[this.active];
322
+ if (!(tab instanceof TerminalTab)) return;
323
+ const slide = slideName(this.deck);
324
+ const ids = this.deck.selection ?? [];
325
+ const at = tab.name === "claude" ? "@" : "";
326
+ const target = ids.length ? ` (block${ids.length > 1 ? "s" : ""} ${ids.map((id) => `"${id}"`).join(", ")})` : "";
327
+ tab.insert(`${at}slides/${slide}.html${target} `);
328
+ }
329
+
330
+ setWidth(width) {
331
+ const clamped = Math.max(320, Math.min(width, innerWidth - 360));
332
+ document.documentElement.style.setProperty("--keyframes-panel-w", `${clamped}px`);
333
+ localStorage.setItem("keyframes-panel-w", String(Math.round(clamped)));
334
+ }
335
+
336
+ enableResize(handle) {
337
+ handle.addEventListener("pointerdown", (e) => {
338
+ e.preventDefault();
339
+ handle.setPointerCapture(e.pointerId);
340
+ document.documentElement.classList.add("keyframes-panel-resizing");
341
+ const move = (ev) => this.setWidth(innerWidth - ev.clientX);
342
+ const up = () => {
343
+ handle.removeEventListener("pointermove", move);
344
+ handle.removeEventListener("pointerup", up);
345
+ document.documentElement.classList.remove("keyframes-panel-resizing");
346
+ };
347
+ handle.addEventListener("pointermove", move);
348
+ handle.addEventListener("pointerup", up);
349
+ });
350
+ }
351
+ }
352
+
353
+ export async function createPanel(deck) {
354
+ addStylesheet(new URL("panel.css", import.meta.url).href);
355
+ const { terminals } = await fetch("_api/info").then((r) => r.json());
356
+ return new Panel(deck, terminals);
357
+ }
@@ -0,0 +1,23 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <link rel="icon" href="data:,">
7
+ <title>{{title}}</title>
8
+ <script type="importmap">
9
+ {{importmap}}
10
+ </script>
11
+ <script>window.KEYFRAMES_CONFIG = {{config}};</script>
12
+ <link rel="stylesheet" href="_keyframes/runtime/runtime.css">
13
+ <link rel="stylesheet" href="_theme/theme.css">
14
+ {{head}}
15
+ <script type="module" src="_keyframes/runtime/runtime.js"></script>
16
+ {{dev}}
17
+ </head>
18
+ <body>
19
+ <main class="keyframes-deck">
20
+ {{slides}}
21
+ </main>
22
+ </body>
23
+ </html>
@@ -0,0 +1,126 @@
1
+ // Base class for <keyframes-*> embeds. The runtime drives the lifecycle:
2
+ // prepare() before the deck starts, cheap setup that affects layout or build-step count
3
+ // init() first time the slide is shown (lazy), load deps and build DOM
4
+ // play() slide became current
5
+ // pause() slide left
6
+ // destroy() slide is far away; free GPU/memory. init() runs again on return
7
+ // snapshot() print/PDF: freeze live canvases into images
8
+ // step(n) build step changed; also replayed right after init()
9
+ // paramChanged(name, value) a bound <keyframes-slider> moved; this.params holds every value
10
+ // Components that implement seek(t) can be driven by a <keyframes-slider timeline>.
11
+
12
+ export class SwComponent extends HTMLElement {
13
+ #state = "idle";
14
+ #ready = null;
15
+ #step = 0;
16
+ params = {};
17
+
18
+ get slide() {
19
+ return this.closest(".keyframes-slide");
20
+ }
21
+
22
+ get pixelRatio() {
23
+ return (window.devicePixelRatio || 1) * (window.keyframes?.scale || 1);
24
+ }
25
+
26
+ get ready() {
27
+ return this.#ready ?? Promise.resolve();
28
+ }
29
+
30
+ async prepare() {}
31
+ async init() {}
32
+ play() {}
33
+ pause() {}
34
+ destroy() {}
35
+ async snapshot() {}
36
+ step(n) {}
37
+ paramChanged(name, value) {}
38
+
39
+ async keyframesStart() {
40
+ if (this.#state === "idle") {
41
+ this.#state = "initializing";
42
+ const attempt = this.init().then(
43
+ () => true,
44
+ (e) => (this.showError(e), false),
45
+ );
46
+ this.#ready = attempt;
47
+ const ok = await attempt;
48
+ if (this.#ready !== attempt || this.#state !== "initializing") {
49
+ if (ok) this.#guard(() => this.destroy());
50
+ return;
51
+ }
52
+ this.#state = ok ? "ready" : "failed";
53
+ if (ok) this.#guard(() => this.step(this.#step));
54
+ }
55
+ if (this.#state === "ready" || this.#state === "paused") {
56
+ this.#state = "playing";
57
+ this.#guard(() => this.play());
58
+ }
59
+ }
60
+
61
+ keyframesStop() {
62
+ if (this.#state === "playing") {
63
+ this.#state = "paused";
64
+ this.#guard(() => this.pause());
65
+ }
66
+ }
67
+
68
+ keyframesDispose() {
69
+ if (this.#state === "idle") return;
70
+ this.keyframesStop();
71
+ const wasLive = this.#state !== "initializing" && this.#state !== "failed";
72
+ this.#state = "idle";
73
+ this.#ready = null;
74
+ if (wasLive) this.#guard(() => this.destroy());
75
+ }
76
+
77
+ keyframesPrepare() {
78
+ return Promise.resolve(this.prepare()).catch(() => {});
79
+ }
80
+
81
+ async keyframesSnapshot() {
82
+ await this.ready;
83
+ await this.snapshot();
84
+ }
85
+
86
+ keyframesStep(n) {
87
+ this.#step = n;
88
+ if (["ready", "playing", "paused"].includes(this.#state)) this.#guard(() => this.step(n));
89
+ }
90
+
91
+ setParam(name, value) {
92
+ this.params[name] = value;
93
+ if (["ready", "playing", "paused"].includes(this.#state)) this.#guard(() => this.paramChanged(name, value));
94
+ }
95
+
96
+ disconnectedCallback() {
97
+ this.keyframesDispose();
98
+ }
99
+
100
+ #guard(fn) {
101
+ try {
102
+ fn();
103
+ } catch (e) {
104
+ this.showError(e);
105
+ }
106
+ }
107
+
108
+ showError(error) {
109
+ console.error(`<${this.localName}>`, error);
110
+ const box = document.createElement("div");
111
+ box.className = "keyframes-error";
112
+ box.textContent = `<${this.localName}> ${error?.stack || error}`;
113
+ this.style.position ||= "relative";
114
+ this.append(box);
115
+ }
116
+
117
+ resolveUrl(path) {
118
+ return new URL(path, document.baseURI).href;
119
+ }
120
+ }
121
+
122
+ export function onResize(el, callback) {
123
+ const observer = new ResizeObserver(() => callback(el.clientWidth, el.clientHeight));
124
+ observer.observe(el);
125
+ return () => observer.disconnect();
126
+ }
@@ -0,0 +1,31 @@
1
+ export function connectLive(deck, retryDelay = 500) {
2
+ const url = new URL("_api/live", location.href);
3
+ url.protocol = location.protocol === "https:" ? "wss:" : "ws:";
4
+ url.search = url.hash = "";
5
+ const ws = new WebSocket(url);
6
+ let opened = false;
7
+
8
+ ws.onopen = () => {
9
+ if (retryDelay > 500) location.reload();
10
+ opened = true;
11
+ };
12
+ ws.onmessage = ({ data }) => {
13
+ const message = JSON.parse(data);
14
+ if (message.type === "slide") deck.replaceSlide(message.name, message.html);
15
+ if (message.type === "css") reloadStylesheets();
16
+ if (message.type === "reload") location.reload();
17
+ };
18
+ ws.onclose = () => {
19
+ const delay = opened ? 1000 : Math.min(retryDelay * 2, 5000);
20
+ setTimeout(() => connectLive(deck, delay), delay);
21
+ };
22
+ }
23
+
24
+ function reloadStylesheets() {
25
+ for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
26
+ const href = new URL(link.href);
27
+ if (href.pathname.includes("/_vendor/")) continue;
28
+ href.searchParams.set("v", Date.now());
29
+ link.href = href.href;
30
+ }
31
+ }
@@ -0,0 +1,33 @@
1
+ const MATH_PATTERN = /\$|\\\(|\\\[|\\begin\{/;
2
+ const DELIMITERS = [
3
+ { left: "$$", right: "$$", display: true },
4
+ { left: "\\[", right: "\\]", display: true },
5
+ { left: "\\begin{equation}", right: "\\end{equation}", display: true },
6
+ { left: "\\begin{align}", right: "\\end{align}", display: true },
7
+ { left: "\\(", right: "\\)", display: false },
8
+ { left: "$", right: "$", display: false },
9
+ ];
10
+
11
+ let katexReady = null;
12
+
13
+ function loadKatex() {
14
+ if (!katexReady) {
15
+ const css = document.createElement("link");
16
+ css.rel = "stylesheet";
17
+ css.href = new URL("katex.min.css", import.meta.resolve("katex/")).href;
18
+ document.head.append(css);
19
+ katexReady = import("katex/contrib/auto-render.mjs").then((m) => m.default);
20
+ }
21
+ return katexReady;
22
+ }
23
+
24
+ export async function renderMath(root) {
25
+ if (!MATH_PATTERN.test(root.textContent)) return;
26
+ const renderMathInElement = await loadKatex();
27
+ renderMathInElement(root, {
28
+ delimiters: DELIMITERS,
29
+ ignoredTags: ["script", "noscript", "style", "textarea", "pre", "code", "keyframes-code"],
30
+ ignoredClasses: ["no-math", "notes"],
31
+ throwOnError: false,
32
+ });
33
+ }