keyframes 0.1.0__tar.gz → 0.2.0__tar.gz

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 (79) hide show
  1. {keyframes-0.1.0 → keyframes-0.2.0}/PKG-INFO +6 -2
  2. {keyframes-0.1.0 → keyframes-0.2.0}/README.md +5 -1
  3. {keyframes-0.1.0 → keyframes-0.2.0}/pyproject.toml +1 -1
  4. {keyframes-0.1.0 → keyframes-0.2.0}/pyproject.toml.orig +1 -1
  5. keyframes-0.2.0/src/keyframes/static/runtime/controls.js +181 -0
  6. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/runtime/presenter.js +46 -3
  7. keyframes-0.2.0/src/keyframes/static/runtime/runtime.css +185 -0
  8. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/runtime/runtime.js +23 -4
  9. keyframes-0.1.0/src/keyframes/static/runtime/runtime.css +0 -91
  10. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/__init__.py +0 -0
  11. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/build.py +0 -0
  12. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/check.py +0 -0
  13. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/cli.py +0 -0
  14. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/deck.py +0 -0
  15. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/export.py +0 -0
  16. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/guide/__init__.py +0 -0
  17. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/guide/components/keyframes-code.md +0 -0
  18. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/guide/components/keyframes-excalidraw.md +0 -0
  19. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/guide/components/keyframes-mujoco.md +0 -0
  20. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/guide/components/keyframes-slider.md +0 -0
  21. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/guide/components/keyframes-three.md +0 -0
  22. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/guide/core.md +0 -0
  23. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/patch.py +0 -0
  24. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/render.py +0 -0
  25. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/scaffold/.gitignore +0 -0
  26. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/scaffold/CLAUDE.md +0 -0
  27. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/scaffold/assets/deck.css +0 -0
  28. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/scaffold/assets/figure.svg +0 -0
  29. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/scaffold/deck.toml +0 -0
  30. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/scaffold/references/.gitkeep +0 -0
  31. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/scaffold/slides/01-title.html +0 -0
  32. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/scaffold/slides/02-intro.html +0 -0
  33. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/server.py +0 -0
  34. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/components/keyframes-code.js +0 -0
  35. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/components/keyframes-excalidraw.js +0 -0
  36. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/components/keyframes-mujoco.js +0 -0
  37. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/components/keyframes-slider.js +0 -0
  38. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/components/keyframes-three.js +0 -0
  39. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/editor/editor.css +0 -0
  40. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/editor/editor.js +0 -0
  41. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/editor/panel.css +0 -0
  42. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/editor/panel.js +0 -0
  43. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/index.html +0 -0
  44. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/runtime/component.js +0 -0
  45. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/runtime/live.js +0 -0
  46. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/runtime/math.js +0 -0
  47. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/static/runtime/steps.js +0 -0
  48. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/terminal.py +0 -0
  49. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/STYLE_GUIDE.md +0 -0
  50. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/assets/arc-top.webp +0 -0
  51. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/assets/orbit-ring.webp +0 -0
  52. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/assets/streaks-top.webp +0 -0
  53. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/assets/streaks-wide.webp +0 -0
  54. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/assets/sun-top.webp +0 -0
  55. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/assets/title-orbit.webp +0 -0
  56. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/content.html +0 -0
  57. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/figure.html +0 -0
  58. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/grid-2.html +0 -0
  59. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/grid-3x2.html +0 -0
  60. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/grid-4.html +0 -0
  61. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/media-side.html +0 -0
  62. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/section-arc.html +0 -0
  63. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/section-streaks.html +0 -0
  64. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/section.html +0 -0
  65. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/split-wide.html +0 -0
  66. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/split.html +0 -0
  67. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/statement.html +0 -0
  68. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/title.html +0 -0
  69. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/layouts/two-column.html +0 -0
  70. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/default/theme.css +0 -0
  71. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/minimal/STYLE_GUIDE.md +0 -0
  72. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/minimal/layouts/figure.html +0 -0
  73. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/minimal/layouts/section.html +0 -0
  74. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/minimal/layouts/statement.html +0 -0
  75. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/minimal/layouts/title-body.html +0 -0
  76. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/minimal/layouts/title.html +0 -0
  77. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/minimal/layouts/two-column.html +0 -0
  78. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/themes/minimal/theme.css +0 -0
  79. {keyframes-0.1.0 → keyframes-0.2.0}/src/keyframes/vendor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: keyframes
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Agent-written, interactive HTML slide decks with live 3D, physics, math, code and diagrams
5
5
  Keywords: slides,presentation,llm,agents,three.js,mujoco,katex
6
6
  Author: Pranay Thangeda
@@ -53,8 +53,12 @@ For exports install the browser once: `uv run playwright install chromium` (or h
53
53
  | --- | --- |
54
54
  | → / Space, ← | next / previous (build steps included) |
55
55
  | E | toggle the layout editor |
56
- | S | presenter view (notes, next slide, timer) |
56
+ | S | presenter view in a second window (notes, next slide, timer, clickable slide strip) |
57
57
  | F | fullscreen · B blackout |
58
+ | L | laser pointer on/off (on by default) |
59
+
60
+ Hover the bottom-left corner for the `⋯` options menu (laser pointer, fullscreen, black
61
+ screen, presenter view).
58
62
 
59
63
  Editor: drag to move (Shift locks axis, Alt disables snapping), handles resize (Shift keeps
60
64
  aspect), arrows nudge (Shift ×10), `[`/`]` z-order, double-click to interact with a live
@@ -32,8 +32,12 @@ For exports install the browser once: `uv run playwright install chromium` (or h
32
32
  | --- | --- |
33
33
  | → / Space, ← | next / previous (build steps included) |
34
34
  | E | toggle the layout editor |
35
- | S | presenter view (notes, next slide, timer) |
35
+ | S | presenter view in a second window (notes, next slide, timer, clickable slide strip) |
36
36
  | F | fullscreen · B blackout |
37
+ | L | laser pointer on/off (on by default) |
38
+
39
+ Hover the bottom-left corner for the `⋯` options menu (laser pointer, fullscreen, black
40
+ screen, presenter view).
37
41
 
38
42
  Editor: drag to move (Shift locks axis, Alt disables snapping), handles resize (Shift keeps
39
43
  aspect), arrows nudge (Shift ×10), `[`/`]` z-order, double-click to interact with a live
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "keyframes"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Agent-written, interactive HTML slide decks with live 3D, physics, math, code and diagrams"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "keyframes"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Agent-written, interactive HTML slide decks with live 3D, physics, math, code and diagrams"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1,181 @@
1
+ const TRAIL_MS = 400;
2
+ const TRAIL_WIDTH = 9;
3
+ const DOT_RADIUS = 15;
4
+ const RED = "255, 38, 38";
5
+ const LASER_STORAGE = "keyframes-laser";
6
+
7
+ const MENU_ITEMS = [
8
+ { action: "laser", label: "Laser pointer", key: "L" },
9
+ { action: "fullscreen", label: "Fullscreen", key: "F" },
10
+ { action: "blackout", label: "Black screen", key: "B" },
11
+ { action: "presenter", label: "Presenter view", key: "S" },
12
+ ];
13
+
14
+ export function createLaser(deck) {
15
+ const root = document.documentElement;
16
+ const canvas = document.createElement("canvas");
17
+ canvas.className = "keyframes-laser";
18
+ document.body.append(canvas);
19
+ const ctx = canvas.getContext("2d");
20
+ const trailCanvas = document.createElement("canvas");
21
+ const trailCtx = trailCanvas.getContext("2d");
22
+ const trail = [];
23
+ let on = localStorage.getItem(LASER_STORAGE) !== "0";
24
+ let dot = null;
25
+ let frame = 0;
26
+
27
+ function resize() {
28
+ const dpr = devicePixelRatio || 1;
29
+ for (const [c, context] of [[canvas, ctx], [trailCanvas, trailCtx]]) {
30
+ c.width = innerWidth * dpr;
31
+ c.height = innerHeight * dpr;
32
+ context.setTransform(dpr, 0, 0, dpr, 0, 0);
33
+ }
34
+ schedule();
35
+ }
36
+
37
+ function overSlide(e) {
38
+ if (!on || root.classList.contains("keyframes-editing") || e.target.closest?.(".keyframes-menu")) return false;
39
+ const r = deck.current?.getBoundingClientRect();
40
+ return !!r && e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
41
+ }
42
+
43
+ function onMove(e) {
44
+ if (e.pointerType === "touch") return;
45
+ const inside = overSlide(e);
46
+ root.classList.toggle("keyframes-lasing", inside);
47
+ dot = inside ? { x: e.clientX, y: e.clientY } : null;
48
+ if (inside) {
49
+ const now = performance.now();
50
+ for (const p of e.getCoalescedEvents?.() ?? [e]) trail.push({ x: p.clientX, y: p.clientY, t: now });
51
+ }
52
+ schedule();
53
+ }
54
+
55
+ function hide() {
56
+ root.classList.remove("keyframes-lasing");
57
+ dot = null;
58
+ schedule();
59
+ }
60
+
61
+ function schedule() {
62
+ frame ||= requestAnimationFrame(draw);
63
+ }
64
+
65
+ function draw() {
66
+ frame = 0;
67
+ const now = performance.now();
68
+ while (trail.length && now - trail[0].t > TRAIL_MS) trail.shift();
69
+ ctx.clearRect(0, 0, innerWidth, innerHeight);
70
+ if (trail.length > 1) drawTrail(now);
71
+ if (dot) drawDot(dot);
72
+ if (trail.length) schedule();
73
+ }
74
+
75
+ function drawTrail(now) {
76
+ trailCtx.clearRect(0, 0, innerWidth, innerHeight);
77
+ trailCtx.strokeStyle = `rgb(${RED})`;
78
+ trailCtx.lineCap = "round";
79
+ for (let i = 1; i < trail.length; i++) {
80
+ trailCtx.lineWidth = TRAIL_WIDTH * (1 - (now - trail[i].t) / TRAIL_MS);
81
+ trailCtx.beginPath();
82
+ trailCtx.moveTo(trail[i - 1].x, trail[i - 1].y);
83
+ trailCtx.lineTo(trail[i].x, trail[i].y);
84
+ trailCtx.stroke();
85
+ }
86
+ ctx.save();
87
+ ctx.globalAlpha = 0.55;
88
+ ctx.shadowColor = `rgb(${RED})`;
89
+ ctx.shadowBlur = 12;
90
+ ctx.drawImage(trailCanvas, 0, 0, innerWidth, innerHeight);
91
+ ctx.restore();
92
+ }
93
+
94
+ function drawDot({ x, y }) {
95
+ const glow = ctx.createRadialGradient(x, y, 0, x, y, DOT_RADIUS);
96
+ glow.addColorStop(0, "rgb(255, 236, 236)");
97
+ glow.addColorStop(0.2, `rgb(${RED})`);
98
+ glow.addColorStop(0.42, `rgba(${RED}, 0.6)`);
99
+ glow.addColorStop(1, `rgba(${RED}, 0)`);
100
+ ctx.fillStyle = glow;
101
+ ctx.beginPath();
102
+ ctx.arc(x, y, DOT_RADIUS, 0, 2 * Math.PI);
103
+ ctx.fill();
104
+ }
105
+
106
+ function toggle() {
107
+ on = !on;
108
+ localStorage.setItem(LASER_STORAGE, on ? "1" : "0");
109
+ trail.length = 0;
110
+ hide();
111
+ }
112
+
113
+ resize();
114
+ addEventListener("resize", resize);
115
+ addEventListener("pointermove", onMove, { capture: true, passive: true });
116
+ root.addEventListener("mouseleave", hide);
117
+ deck.addEventListener("editmode", hide);
118
+ return {
119
+ get on() {
120
+ return on;
121
+ },
122
+ toggle,
123
+ };
124
+ }
125
+
126
+ export function createMenu(deck) {
127
+ const root = document.createElement("div");
128
+ root.className = "keyframes-menu";
129
+ root.innerHTML = `
130
+ <div class="keyframes-menu-list" role="menu" hidden>
131
+ ${MENU_ITEMS.map(
132
+ ({ action, label, key }) =>
133
+ `<button role="menuitem" data-action="${action}"><span class="keyframes-menu-check"></span>${label} (${key})</button>`,
134
+ ).join("")}
135
+ </div>
136
+ <button class="keyframes-menu-toggle" aria-label="Presentation options" title="Presentation options">
137
+ <svg viewBox="0 0 24 24"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
138
+ </button>`;
139
+ document.body.append(root);
140
+ const list = root.querySelector(".keyframes-menu-list");
141
+
142
+ const actions = {
143
+ laser: () => deck.laser.toggle(),
144
+ fullscreen: () => deck.toggleFullscreen(),
145
+ blackout: () => deck.toggleBlackout(),
146
+ presenter: () => deck.openPresenter(),
147
+ };
148
+ const isOn = {
149
+ laser: () => deck.laser.on,
150
+ fullscreen: () => !!document.fullscreenElement,
151
+ blackout: () => document.body.classList.contains("keyframes-blackout"),
152
+ };
153
+
154
+ function sync() {
155
+ for (const button of list.querySelectorAll("[data-action]")) {
156
+ button.classList.toggle("is-on", !!isOn[button.dataset.action]?.());
157
+ }
158
+ }
159
+
160
+ function setOpen(open) {
161
+ list.hidden = !open;
162
+ root.classList.toggle("is-open", open);
163
+ if (open) sync();
164
+ }
165
+
166
+ root.addEventListener("mousedown", (e) => e.preventDefault());
167
+ root.addEventListener("click", (e) => {
168
+ if (e.target.closest(".keyframes-menu-toggle")) return setOpen(list.hidden);
169
+ const action = e.target.closest("[data-action]")?.dataset.action;
170
+ if (!action) return;
171
+ actions[action]();
172
+ if (action === "presenter") setOpen(false);
173
+ else sync();
174
+ });
175
+ document.addEventListener("pointerdown", (e) => !root.contains(e.target) && setOpen(false));
176
+ document.addEventListener("keydown", (e) => {
177
+ if (e.key === "Escape") setOpen(false);
178
+ else if (!list.hidden) requestAnimationFrame(sync);
179
+ });
180
+ document.addEventListener("fullscreenchange", sync);
181
+ }
@@ -1,9 +1,13 @@
1
- import { maxStep } from "./steps.js";
1
+ import { renderMath } from "./math.js";
2
+ import { applyGeometry } from "./runtime.js";
3
+ import { applySteps, maxStep } from "./steps.js";
4
+
5
+ const THUMB_HEIGHT = 108;
2
6
 
3
7
  const STYLE = `
4
8
  body { background: #1b1c1f; color: #e8e8e8; font: 16px/1.45 system-ui, sans-serif; overflow: hidden; }
5
9
  .keyframes-deck { display: none; }
6
- .pv { display: grid; grid-template-columns: 1.7fr 1fr; grid-template-rows: auto 1fr; gap: 16px;
10
+ .pv { display: grid; grid-template-columns: 1.7fr 1fr; grid-template-rows: auto minmax(0, 1fr) auto; gap: 16px;
7
11
  height: 100vh; padding: 16px; box-sizing: border-box; }
8
12
  .pv-bar { grid-column: 1 / -1; display: flex; gap: 24px; align-items: baseline; font-variant-numeric: tabular-nums; }
9
13
  .pv-bar b { font-size: 28px; }
@@ -13,6 +17,17 @@ const STYLE = `
13
17
  .pv-side { display: flex; flex-direction: column; gap: 16px; min-height: 0; }
14
18
  .pv-label { color: #999; font-size: 13px; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 4px; }
15
19
  .pv-notes { flex: 1; overflow: auto; font-size: 20px; background: #26272b; border-radius: 8px; padding: 16px; }
20
+ .pv-close { margin-left: auto; }
21
+ .pv-strip { grid-column: 1 / -1; display: flex; gap: 12px; overflow-x: auto; padding: 2px 2px 8px; }
22
+ .pv-thumb { position: relative; flex: none; height: ${THUMB_HEIGHT}px; aspect-ratio: var(--ar); padding: 0; border: 0;
23
+ border-radius: 6px; overflow: hidden; background: #000; cursor: pointer; opacity: .6; }
24
+ .pv-thumb:hover { opacity: 1; }
25
+ .pv-thumb.is-current { opacity: 1; }
26
+ .pv-thumb.is-current::after { content: ""; position: absolute; inset: 0; border: 3px solid #4c8dff; border-radius: 6px; }
27
+ .pv-thumb .keyframes-slide { visibility: visible; left: 0; top: 0; transform: scale(var(--thumb-scale));
28
+ transform-origin: 0 0; pointer-events: none; }
29
+ .pv-thumb-n { position: absolute; left: 6px; bottom: 4px; padding: 0 6px; border-radius: 4px;
30
+ background: rgba(0, 0, 0, .6); color: #e8e8e8; font-size: 12px; }
16
31
  `;
17
32
 
18
33
  export function startPresenter(deck) {
@@ -22,16 +37,20 @@ export function startPresenter(deck) {
22
37
 
23
38
  document.head.insertAdjacentHTML("beforeend", `<style>${STYLE}</style>`);
24
39
  document.title = `Presenter · ${document.title}`;
40
+ document.documentElement.style.setProperty("--keyframes-w", `${width}px`);
41
+ document.documentElement.style.setProperty("--keyframes-h", `${height}px`);
25
42
  document.body.insertAdjacentHTML(
26
43
  "beforeend",
27
44
  `<div class="pv" style="--ar:${width}/${height}">
28
45
  <div class="pv-bar"><b class="pv-count"></b><b class="pv-timer">00:00</b>
29
- <button class="pv-reset">reset timer</button><span class="pv-steps"></span></div>
46
+ <button class="pv-reset">reset timer</button><span class="pv-steps"></span>
47
+ <button class="pv-close">close presenter</button></div>
30
48
  <div><div class="pv-label">Current</div><div class="pv-frame"><iframe class="pv-current"></iframe></div></div>
31
49
  <div class="pv-side">
32
50
  <div><div class="pv-label">Next</div><div class="pv-frame"><iframe class="pv-next"></iframe></div></div>
33
51
  <div class="pv-label">Notes</div><div class="pv-notes"></div>
34
52
  </div>
53
+ <div class="pv-strip" style="--thumb-scale:${THUMB_HEIGHT / height}"></div>
35
54
  </div>`,
36
55
  );
37
56
  const $ = (s) => document.querySelector(s);
@@ -39,6 +58,7 @@ export function startPresenter(deck) {
39
58
  const next = $(".pv-next");
40
59
  current.src = `?embed${location.hash}`;
41
60
  next.src = "?embed&static";
61
+ const thumbs = buildStrip($(".pv-strip"), slides, (index) => goTo(index, 0));
42
62
 
43
63
  const send = (frame, index, step) =>
44
64
  frame.contentWindow?.postMessage({ type: "keyframes:goto", index, step }, "*");
@@ -53,6 +73,8 @@ export function startPresenter(deck) {
53
73
  $(".pv-count").textContent = `${state.index + 1} / ${slides.length}`;
54
74
  $(".pv-steps").textContent = steps ? `build ${state.step}/${steps}` : "";
55
75
  $(".pv-notes").innerHTML = slide.querySelector("aside.notes")?.innerHTML || "<i>No notes</i>";
76
+ thumbs.forEach((thumb, i) => thumb.classList.toggle("is-current", i === state.index));
77
+ thumbs[state.index].scrollIntoView({ inline: "center", block: "nearest", behavior: "smooth" });
56
78
  history.replaceState(null, "", `?presenter#/${state.index + 1}/${state.step}`);
57
79
  }
58
80
 
@@ -80,6 +102,7 @@ export function startPresenter(deck) {
80
102
  e.preventDefault();
81
103
  });
82
104
  $(".pv-reset").onclick = () => (state.started = Date.now());
105
+ $(".pv-close").onclick = () => window.close();
83
106
  setInterval(() => {
84
107
  const s = Math.floor((Date.now() - state.started) / 1000);
85
108
  $(".pv-timer").textContent = `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
@@ -89,3 +112,23 @@ export function startPresenter(deck) {
89
112
  const [, n, s] = location.hash.match(/^#\/(\d+)(?:\/(\d+))?/) || [];
90
113
  goTo(n ? Number(n) - 1 : 0, s ? Number(s) : 0, false);
91
114
  }
115
+
116
+ function buildStrip(strip, slides, onPick) {
117
+ return slides.map((slide, i) => {
118
+ const clone = slide.cloneNode(true);
119
+ clone.classList.remove("is-current");
120
+ for (const video of clone.querySelectorAll("video")) video.removeAttribute("autoplay");
121
+ applyGeometry(clone);
122
+ applySteps(clone, Infinity);
123
+ renderMath(clone);
124
+ const thumb = document.createElement("button");
125
+ thumb.className = "pv-thumb";
126
+ thumb.title = `Go to slide ${i + 1}`;
127
+ thumb.append(clone);
128
+ thumb.insertAdjacentHTML("beforeend", `<span class="pv-thumb-n">${i + 1}</span>`);
129
+ thumb.onmousedown = (e) => e.preventDefault();
130
+ thumb.onclick = () => onPick(i);
131
+ strip.append(thumb);
132
+ return thumb;
133
+ });
134
+ }
@@ -0,0 +1,185 @@
1
+ html, body {
2
+ margin: 0;
3
+ height: 100%;
4
+ overflow: hidden;
5
+ background: #111;
6
+ }
7
+
8
+ .keyframes-deck {
9
+ position: fixed;
10
+ inset: 0;
11
+ }
12
+
13
+ .keyframes-slide {
14
+ position: absolute;
15
+ left: 50%;
16
+ top: 50%;
17
+ width: var(--keyframes-w);
18
+ height: var(--keyframes-h);
19
+ transform: translate(-50%, -50%) scale(var(--keyframes-scale, 1));
20
+ visibility: hidden;
21
+ pointer-events: none;
22
+ }
23
+
24
+ .keyframes-slide.is-current {
25
+ visibility: visible;
26
+ pointer-events: auto;
27
+ }
28
+
29
+ .keyframes-slide > section {
30
+ position: relative;
31
+ width: 100%;
32
+ height: 100%;
33
+ overflow: hidden;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ [data-block] {
38
+ box-sizing: border-box;
39
+ }
40
+
41
+ [data-block][data-x] {
42
+ position: absolute;
43
+ }
44
+
45
+ [data-step] {
46
+ transition: opacity 0.35s ease;
47
+ }
48
+
49
+ .keyframes-step-hidden {
50
+ opacity: 0 !important;
51
+ visibility: hidden;
52
+ }
53
+
54
+ aside.notes {
55
+ display: none !important;
56
+ }
57
+
58
+ .keyframes-blackout .keyframes-slide.is-current {
59
+ visibility: hidden;
60
+ }
61
+
62
+ .keyframes-laser {
63
+ position: fixed;
64
+ inset: 0;
65
+ width: 100%;
66
+ height: 100%;
67
+ z-index: 1000;
68
+ pointer-events: none;
69
+ }
70
+
71
+ html.keyframes-lasing,
72
+ html.keyframes-lasing * {
73
+ cursor: none !important;
74
+ }
75
+
76
+ .keyframes-menu {
77
+ position: fixed;
78
+ left: 0;
79
+ bottom: 0;
80
+ z-index: 1001;
81
+ padding: 20px;
82
+ font: 14px/1.2 system-ui, sans-serif;
83
+ }
84
+
85
+ .keyframes-menu-toggle {
86
+ display: grid;
87
+ place-items: center;
88
+ width: 36px;
89
+ height: 36px;
90
+ padding: 0;
91
+ border: 0;
92
+ border-radius: 50%;
93
+ background: rgba(32, 33, 36, 0.85);
94
+ color: #e8e8e8;
95
+ cursor: pointer;
96
+ opacity: 0;
97
+ transition: opacity 0.2s ease;
98
+ }
99
+
100
+ .keyframes-menu-toggle svg {
101
+ width: 20px;
102
+ height: 20px;
103
+ fill: currentColor;
104
+ }
105
+
106
+ .keyframes-menu:hover .keyframes-menu-toggle,
107
+ .keyframes-menu.is-open .keyframes-menu-toggle {
108
+ opacity: 1;
109
+ }
110
+
111
+ .keyframes-menu-list {
112
+ margin-bottom: 8px;
113
+ padding: 6px;
114
+ min-width: 220px;
115
+ border-radius: 10px;
116
+ background: rgba(32, 33, 36, 0.95);
117
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
118
+ }
119
+
120
+ .keyframes-menu-list[hidden] {
121
+ display: none;
122
+ }
123
+
124
+ .keyframes-menu-list button {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 10px;
128
+ width: 100%;
129
+ padding: 8px 10px;
130
+ border: 0;
131
+ border-radius: 6px;
132
+ background: none;
133
+ color: #e8e8e8;
134
+ font: inherit;
135
+ text-align: left;
136
+ cursor: pointer;
137
+ }
138
+
139
+ .keyframes-menu-list button:hover {
140
+ background: rgba(255, 255, 255, 0.08);
141
+ }
142
+
143
+ .keyframes-menu-check {
144
+ width: 14px;
145
+ }
146
+
147
+ .keyframes-menu-list button.is-on .keyframes-menu-check::before {
148
+ content: "✓";
149
+ }
150
+
151
+ .keyframes-editing .keyframes-menu,
152
+ .keyframes-editing .keyframes-laser {
153
+ display: none;
154
+ }
155
+
156
+ .keyframes-error {
157
+ position: absolute;
158
+ inset: 0;
159
+ overflow: auto;
160
+ padding: 16px;
161
+ background: #fff0f0;
162
+ color: #a00;
163
+ font: 14px/1.4 ui-monospace, Menlo, monospace;
164
+ white-space: pre-wrap;
165
+ border: 2px solid #e55;
166
+ box-sizing: border-box;
167
+ }
168
+
169
+ html.keyframes-print, html.keyframes-print body {
170
+ background: transparent;
171
+ }
172
+
173
+ html.keyframes-print .keyframes-slide {
174
+ left: 0;
175
+ top: 0;
176
+ transform: none;
177
+ }
178
+
179
+ html.keyframes-print [data-step] {
180
+ transition: none;
181
+ }
182
+
183
+ @page {
184
+ margin: 0;
185
+ }
@@ -1,3 +1,4 @@
1
+ import { createLaser, createMenu } from "./controls.js";
1
2
  import { renderMath } from "./math.js";
2
3
  import { applySteps, maxStep } from "./steps.js";
3
4
 
@@ -68,7 +69,11 @@ class Deck extends EventTarget {
68
69
  this.fit();
69
70
  new ResizeObserver(() => this.fit()).observe(document.querySelector(".keyframes-deck"));
70
71
  window.addEventListener("hashchange", () => this.goToHash());
71
- if (!params.has("embed")) document.addEventListener("keydown", (e) => this.onKey(e));
72
+ if (!params.has("embed")) {
73
+ document.addEventListener("keydown", (e) => this.onKey(e));
74
+ this.laser = createLaser(this);
75
+ createMenu(this);
76
+ }
72
77
  this.channel.onmessage = ({ data }) => {
73
78
  if (data.type === "nav") this.goTo(data.index, data.step, { broadcast: false });
74
79
  };
@@ -158,9 +163,10 @@ class Deck extends EventTarget {
158
163
  PageUp: () => this.prev(),
159
164
  Home: () => this.goTo(0),
160
165
  End: () => this.goTo(this.slides.length - 1),
161
- f: () => document.documentElement.requestFullscreen?.(),
162
- b: () => document.body.classList.toggle("keyframes-blackout"),
163
- s: () => window.open(`?presenter${location.hash}`, "keyframes-presenter", "width=1400,height=850"),
166
+ f: () => this.toggleFullscreen(),
167
+ b: () => this.toggleBlackout(),
168
+ s: () => this.openPresenter(),
169
+ l: () => this.laser.toggle(),
164
170
  };
165
171
  const action = actions[e.key];
166
172
  if (action) {
@@ -169,6 +175,19 @@ class Deck extends EventTarget {
169
175
  }
170
176
  }
171
177
 
178
+ toggleFullscreen() {
179
+ if (document.fullscreenElement) document.exitFullscreen();
180
+ else document.documentElement.requestFullscreen?.();
181
+ }
182
+
183
+ toggleBlackout() {
184
+ document.body.classList.toggle("keyframes-blackout");
185
+ }
186
+
187
+ openPresenter() {
188
+ window.open(`?presenter${location.hash}`, "keyframes-presenter", "width=1400,height=850");
189
+ }
190
+
172
191
  async replaceSlide(name, html) {
173
192
  const old = this.slides.find((s) => s.dataset.slide === name);
174
193
  if (!old) return location.reload();
@@ -1,91 +0,0 @@
1
- html, body {
2
- margin: 0;
3
- height: 100%;
4
- overflow: hidden;
5
- background: #111;
6
- }
7
-
8
- .keyframes-deck {
9
- position: fixed;
10
- inset: 0;
11
- }
12
-
13
- .keyframes-slide {
14
- position: absolute;
15
- left: 50%;
16
- top: 50%;
17
- width: var(--keyframes-w);
18
- height: var(--keyframes-h);
19
- transform: translate(-50%, -50%) scale(var(--keyframes-scale, 1));
20
- visibility: hidden;
21
- pointer-events: none;
22
- }
23
-
24
- .keyframes-slide.is-current {
25
- visibility: visible;
26
- pointer-events: auto;
27
- }
28
-
29
- .keyframes-slide > section {
30
- position: relative;
31
- width: 100%;
32
- height: 100%;
33
- overflow: hidden;
34
- box-sizing: border-box;
35
- }
36
-
37
- [data-block] {
38
- box-sizing: border-box;
39
- }
40
-
41
- [data-block][data-x] {
42
- position: absolute;
43
- }
44
-
45
- [data-step] {
46
- transition: opacity 0.35s ease;
47
- }
48
-
49
- .keyframes-step-hidden {
50
- opacity: 0 !important;
51
- visibility: hidden;
52
- }
53
-
54
- aside.notes {
55
- display: none !important;
56
- }
57
-
58
- .keyframes-blackout .keyframes-slide.is-current {
59
- visibility: hidden;
60
- }
61
-
62
- .keyframes-error {
63
- position: absolute;
64
- inset: 0;
65
- overflow: auto;
66
- padding: 16px;
67
- background: #fff0f0;
68
- color: #a00;
69
- font: 14px/1.4 ui-monospace, Menlo, monospace;
70
- white-space: pre-wrap;
71
- border: 2px solid #e55;
72
- box-sizing: border-box;
73
- }
74
-
75
- html.keyframes-print, html.keyframes-print body {
76
- background: transparent;
77
- }
78
-
79
- html.keyframes-print .keyframes-slide {
80
- left: 0;
81
- top: 0;
82
- transform: none;
83
- }
84
-
85
- html.keyframes-print [data-step] {
86
- transition: none;
87
- }
88
-
89
- @page {
90
- margin: 0;
91
- }