keyframes 0.1.0__tar.gz → 0.3.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.
- {keyframes-0.1.0 → keyframes-0.3.0}/PKG-INFO +10 -2
- {keyframes-0.1.0 → keyframes-0.3.0}/README.md +9 -1
- {keyframes-0.1.0 → keyframes-0.3.0}/pyproject.toml +1 -1
- {keyframes-0.1.0 → keyframes-0.3.0}/pyproject.toml.orig +1 -1
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/patch.py +47 -3
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/server.py +12 -1
- keyframes-0.3.0/src/keyframes/static/runtime/controls.js +181 -0
- keyframes-0.3.0/src/keyframes/static/runtime/presenter.js +202 -0
- keyframes-0.3.0/src/keyframes/static/runtime/runtime.css +185 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/runtime/runtime.js +23 -4
- keyframes-0.1.0/src/keyframes/static/runtime/presenter.js +0 -91
- keyframes-0.1.0/src/keyframes/static/runtime/runtime.css +0 -91
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/__init__.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/build.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/check.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/cli.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/deck.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/export.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/guide/__init__.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/guide/components/keyframes-code.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/guide/components/keyframes-excalidraw.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/guide/components/keyframes-mujoco.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/guide/components/keyframes-slider.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/guide/components/keyframes-three.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/guide/core.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/render.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/scaffold/.gitignore +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/scaffold/CLAUDE.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/scaffold/assets/deck.css +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/scaffold/assets/figure.svg +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/scaffold/deck.toml +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/scaffold/references/.gitkeep +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/scaffold/slides/01-title.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/scaffold/slides/02-intro.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/components/keyframes-code.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/components/keyframes-excalidraw.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/components/keyframes-mujoco.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/components/keyframes-slider.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/components/keyframes-three.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/editor/editor.css +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/editor/editor.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/editor/panel.css +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/editor/panel.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/index.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/runtime/component.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/runtime/live.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/runtime/math.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/static/runtime/steps.js +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/terminal.py +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/STYLE_GUIDE.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/assets/arc-top.webp +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/assets/orbit-ring.webp +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/assets/streaks-top.webp +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/assets/streaks-wide.webp +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/assets/sun-top.webp +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/assets/title-orbit.webp +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/content.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/figure.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/grid-2.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/grid-3x2.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/grid-4.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/media-side.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/section-arc.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/section-streaks.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/section.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/split-wide.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/split.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/statement.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/title.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/two-column.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/theme.css +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/minimal/STYLE_GUIDE.md +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/minimal/layouts/figure.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/minimal/layouts/section.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/minimal/layouts/statement.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/minimal/layouts/title-body.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/minimal/layouts/title.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/minimal/layouts/two-column.html +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/minimal/theme.css +0 -0
- {keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/vendor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: keyframes
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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,16 @@ 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).
|
|
62
|
+
|
|
63
|
+
Speaker notes live in each slide as `<aside class="notes">…</aside>`. Under `keyframes dev`
|
|
64
|
+
you can also double-click the notes in presenter view to edit them; ⌘/Ctrl+Enter or clicking
|
|
65
|
+
away saves straight into the slide file, Esc cancels.
|
|
58
66
|
|
|
59
67
|
Editor: drag to move (Shift locks axis, Alt disables snapping), handles resize (Shift keeps
|
|
60
68
|
aspect), arrows nudge (Shift ×10), `[`/`]` z-order, double-click to interact with a live
|
|
@@ -32,8 +32,16 @@ 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).
|
|
41
|
+
|
|
42
|
+
Speaker notes live in each slide as `<aside class="notes">…</aside>`. Under `keyframes dev`
|
|
43
|
+
you can also double-click the notes in presenter view to edit them; ⌘/Ctrl+Enter or clicking
|
|
44
|
+
away saves straight into the slide file, Esc cancels.
|
|
37
45
|
|
|
38
46
|
Editor: drag to move (Shift locks axis, Alt disables snapping), handles resize (Shift keeps
|
|
39
47
|
aspect), arrows nudge (Shift ×10), `[`/`]` z-order, double-click to interact with a live
|
|
@@ -16,12 +16,21 @@ class BlockTag:
|
|
|
16
16
|
attrs: dict[str, str | None]
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class
|
|
19
|
+
class _OffsetParser(HTMLParser):
|
|
20
20
|
def __init__(self, source: str):
|
|
21
21
|
super().__init__(convert_charrefs=False)
|
|
22
22
|
self.line_offsets = [0]
|
|
23
23
|
for line in source.split("\n"):
|
|
24
24
|
self.line_offsets.append(self.line_offsets[-1] + len(line) + 1)
|
|
25
|
+
|
|
26
|
+
def source_offset(self) -> int:
|
|
27
|
+
line, col = self.getpos()
|
|
28
|
+
return self.line_offsets[line - 1] + col
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _BlockFinder(_OffsetParser):
|
|
32
|
+
def __init__(self, source: str):
|
|
33
|
+
super().__init__(source)
|
|
25
34
|
self.blocks: list[BlockTag] = []
|
|
26
35
|
|
|
27
36
|
def handle_starttag(self, tag, attrs):
|
|
@@ -29,14 +38,34 @@ class _BlockFinder(HTMLParser):
|
|
|
29
38
|
block_id = attr_map.get("data-block")
|
|
30
39
|
if block_id is None:
|
|
31
40
|
return
|
|
32
|
-
|
|
33
|
-
start = self.line_offsets[line - 1] + col
|
|
41
|
+
start = self.source_offset()
|
|
34
42
|
text = self.get_starttag_text()
|
|
35
43
|
self.blocks.append(BlockTag(block_id, start, start + len(text), text, attr_map))
|
|
36
44
|
|
|
37
45
|
handle_startendtag = handle_starttag
|
|
38
46
|
|
|
39
47
|
|
|
48
|
+
class _NotesFinder(_OffsetParser):
|
|
49
|
+
"""Locates the inner content of the first <aside class="notes"> and the last </section>."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, source: str):
|
|
52
|
+
super().__init__(source)
|
|
53
|
+
self.content: tuple[int, int] | None = None
|
|
54
|
+
self.content_start: int | None = None
|
|
55
|
+
self.section_end: int | None = None
|
|
56
|
+
|
|
57
|
+
def handle_starttag(self, tag, attrs):
|
|
58
|
+
classes = (dict(attrs).get("class") or "").split()
|
|
59
|
+
if tag == "aside" and "notes" in classes and self.content_start is None:
|
|
60
|
+
self.content_start = self.source_offset() + len(self.get_starttag_text())
|
|
61
|
+
|
|
62
|
+
def handle_endtag(self, tag):
|
|
63
|
+
if tag == "aside" and self.content_start is not None and self.content is None:
|
|
64
|
+
self.content = (self.content_start, self.source_offset())
|
|
65
|
+
elif tag == "section":
|
|
66
|
+
self.section_end = self.source_offset()
|
|
67
|
+
|
|
68
|
+
|
|
40
69
|
def find_block_tags(source: str) -> list[BlockTag]:
|
|
41
70
|
finder = _BlockFinder(source)
|
|
42
71
|
finder.feed(source)
|
|
@@ -75,3 +104,18 @@ def patch_blocks(source: str, updates: dict[str, dict[str, str | None]]) -> str:
|
|
|
75
104
|
new_text = set_attrs(tag.text, updates[tag.id])
|
|
76
105
|
source = source[: tag.start] + new_text + source[tag.end :]
|
|
77
106
|
return source
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def set_notes(source: str, notes: str) -> str:
|
|
110
|
+
"""Replace the speaker notes' inner HTML, adding an <aside class="notes"> if the slide has none."""
|
|
111
|
+
finder = _NotesFinder(source)
|
|
112
|
+
finder.feed(source)
|
|
113
|
+
finder.close()
|
|
114
|
+
if finder.content:
|
|
115
|
+
start, end = finder.content
|
|
116
|
+
return source[:start] + notes + source[end:]
|
|
117
|
+
if not notes.strip():
|
|
118
|
+
return source
|
|
119
|
+
aside = f' <aside class="notes">{notes}</aside>\n'
|
|
120
|
+
end = finder.section_end if finder.section_end is not None else len(source)
|
|
121
|
+
return source[:end] + aside + source[end:]
|
|
@@ -16,7 +16,7 @@ from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
|
16
16
|
from watchfiles import Change, awatch
|
|
17
17
|
|
|
18
18
|
from .deck import STATIC_DIR, Deck, DeckError
|
|
19
|
-
from .patch import geometry_attrs, patch_blocks
|
|
19
|
+
from .patch import geometry_attrs, patch_blocks, set_notes
|
|
20
20
|
from .render import render_index, render_slide
|
|
21
21
|
from .terminal import TerminalManager
|
|
22
22
|
from .vendor import VendorCache
|
|
@@ -132,6 +132,16 @@ def create_app(deck: Deck, mode: str = "dev") -> Starlette:
|
|
|
132
132
|
return JSONResponse({"ok": True})
|
|
133
133
|
return Response(path.read_text(), media_type="text/plain", headers={"Cache-Control": "no-store"})
|
|
134
134
|
|
|
135
|
+
async def save_notes(request: Request):
|
|
136
|
+
if not same_origin(request.headers):
|
|
137
|
+
return JSONResponse({"error": "cross-origin request"}, status_code=403)
|
|
138
|
+
try:
|
|
139
|
+
path = deck.slide_path(request.path_params["slide"])
|
|
140
|
+
except DeckError as e:
|
|
141
|
+
return JSONResponse({"error": str(e)}, status_code=404)
|
|
142
|
+
live.write(path, set_notes(path.read_text(), (await request.body()).decode()))
|
|
143
|
+
return JSONResponse({"ok": True})
|
|
144
|
+
|
|
135
145
|
async def info(request: Request):
|
|
136
146
|
return JSONResponse({"terminals": list(terminals.commands)})
|
|
137
147
|
|
|
@@ -162,6 +172,7 @@ def create_app(deck: Deck, mode: str = "dev") -> Starlette:
|
|
|
162
172
|
dev_routes = [
|
|
163
173
|
Route("/_api/geometry", save_geometry, methods=["POST"]),
|
|
164
174
|
Route("/_api/source/{slide}", source, methods=["GET", "PUT"]),
|
|
175
|
+
Route("/_api/notes/{slide}", save_notes, methods=["PUT"]),
|
|
165
176
|
Route("/_api/info", info),
|
|
166
177
|
WebSocketRoute("/_api/live", websocket),
|
|
167
178
|
WebSocketRoute("/_api/term/{name}", terminal),
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
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;
|
|
6
|
+
|
|
7
|
+
const STYLE = `
|
|
8
|
+
body { background: #1b1c1f; color: #e8e8e8; font: 16px/1.45 system-ui, sans-serif; overflow: hidden; }
|
|
9
|
+
.keyframes-deck { display: none; }
|
|
10
|
+
.pv { display: grid; grid-template-columns: 1.7fr 1fr; grid-template-rows: auto minmax(0, 1fr) auto; gap: 16px;
|
|
11
|
+
height: 100vh; padding: 16px; box-sizing: border-box; }
|
|
12
|
+
.pv-bar { grid-column: 1 / -1; display: flex; gap: 24px; align-items: baseline; font-variant-numeric: tabular-nums; }
|
|
13
|
+
.pv-bar b { font-size: 28px; }
|
|
14
|
+
.pv-bar button { background: #333; color: inherit; border: 0; border-radius: 6px; padding: 4px 10px; cursor: pointer; }
|
|
15
|
+
.pv-frame { position: relative; aspect-ratio: var(--ar); background: #000; border-radius: 8px; overflow: hidden; }
|
|
16
|
+
.pv-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
|
|
17
|
+
.pv-side { display: flex; flex-direction: column; gap: 16px; min-height: 0; }
|
|
18
|
+
.pv-label { color: #999; font-size: 13px; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 4px; }
|
|
19
|
+
.pv-notes { flex: 1; overflow: auto; font-size: 20px; background: #26272b; border-radius: 8px; padding: 16px; }
|
|
20
|
+
.pv-notes[contenteditable="true"] { outline: 2px solid #4c8dff; }
|
|
21
|
+
.pv-notes-status { margin-left: 8px; color: #777; text-transform: none; letter-spacing: 0; }
|
|
22
|
+
.pv-close { margin-left: auto; }
|
|
23
|
+
.pv-strip { grid-column: 1 / -1; display: flex; gap: 12px; overflow-x: auto; padding: 2px 2px 8px; }
|
|
24
|
+
.pv-thumb { position: relative; flex: none; height: ${THUMB_HEIGHT}px; aspect-ratio: var(--ar); padding: 0; border: 0;
|
|
25
|
+
border-radius: 6px; overflow: hidden; background: #000; cursor: pointer; opacity: .6; }
|
|
26
|
+
.pv-thumb:hover { opacity: 1; }
|
|
27
|
+
.pv-thumb.is-current { opacity: 1; }
|
|
28
|
+
.pv-thumb.is-current::after { content: ""; position: absolute; inset: 0; border: 3px solid #4c8dff; border-radius: 6px; }
|
|
29
|
+
.pv-thumb .keyframes-slide { visibility: visible; left: 0; top: 0; transform: scale(var(--thumb-scale));
|
|
30
|
+
transform-origin: 0 0; pointer-events: none; }
|
|
31
|
+
.pv-thumb-n { position: absolute; left: 6px; bottom: 4px; padding: 0 6px; border-radius: 4px;
|
|
32
|
+
background: rgba(0, 0, 0, .6); color: #e8e8e8; font-size: 12px; }
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export function startPresenter(deck) {
|
|
36
|
+
const { width, height } = deck.config;
|
|
37
|
+
const slides = deck.slides;
|
|
38
|
+
const state = { index: 0, step: 0, started: Date.now() };
|
|
39
|
+
|
|
40
|
+
document.head.insertAdjacentHTML("beforeend", `<style>${STYLE}</style>`);
|
|
41
|
+
document.title = `Presenter · ${document.title}`;
|
|
42
|
+
document.documentElement.style.setProperty("--keyframes-w", `${width}px`);
|
|
43
|
+
document.documentElement.style.setProperty("--keyframes-h", `${height}px`);
|
|
44
|
+
document.body.insertAdjacentHTML(
|
|
45
|
+
"beforeend",
|
|
46
|
+
`<div class="pv" style="--ar:${width}/${height}">
|
|
47
|
+
<div class="pv-bar"><b class="pv-count"></b><b class="pv-timer">00:00</b>
|
|
48
|
+
<button class="pv-reset">reset timer</button><span class="pv-steps"></span>
|
|
49
|
+
<button class="pv-close">close presenter</button></div>
|
|
50
|
+
<div><div class="pv-label">Current</div><div class="pv-frame"><iframe class="pv-current"></iframe></div></div>
|
|
51
|
+
<div class="pv-side">
|
|
52
|
+
<div><div class="pv-label">Next</div><div class="pv-frame"><iframe class="pv-next"></iframe></div></div>
|
|
53
|
+
<div class="pv-label">Notes<span class="pv-notes-status"></span></div><div class="pv-notes"></div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="pv-strip" style="--thumb-scale:${THUMB_HEIGHT / height}"></div>
|
|
56
|
+
</div>`,
|
|
57
|
+
);
|
|
58
|
+
const $ = (s) => document.querySelector(s);
|
|
59
|
+
const current = $(".pv-current");
|
|
60
|
+
const next = $(".pv-next");
|
|
61
|
+
current.src = `?embed${location.hash}`;
|
|
62
|
+
next.src = "?embed&static";
|
|
63
|
+
const thumbs = buildStrip($(".pv-strip"), slides, (index) => goTo(index, 0));
|
|
64
|
+
const notes = createNotes($(".pv-notes"), $(".pv-notes-status"), deck.config.mode === "dev");
|
|
65
|
+
|
|
66
|
+
const send = (frame, index, step) =>
|
|
67
|
+
frame.contentWindow?.postMessage({ type: "keyframes:goto", index, step }, "*");
|
|
68
|
+
|
|
69
|
+
function render() {
|
|
70
|
+
const slide = slides[state.index];
|
|
71
|
+
const steps = maxStep(slide);
|
|
72
|
+
const [nextIndex, nextStep] =
|
|
73
|
+
state.step < steps ? [state.index, state.step + 1] : [state.index + 1, 0];
|
|
74
|
+
send(current, state.index, state.step);
|
|
75
|
+
send(next, Math.min(nextIndex, slides.length - 1), nextStep);
|
|
76
|
+
$(".pv-count").textContent = `${state.index + 1} / ${slides.length}`;
|
|
77
|
+
$(".pv-steps").textContent = steps ? `build ${state.step}/${steps}` : "";
|
|
78
|
+
notes.show(slide);
|
|
79
|
+
thumbs.forEach((thumb, i) => thumb.classList.toggle("is-current", i === state.index));
|
|
80
|
+
thumbs[state.index].scrollIntoView({ inline: "center", block: "nearest", behavior: "smooth" });
|
|
81
|
+
history.replaceState(null, "", `?presenter#/${state.index + 1}/${state.step}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function goTo(index, step, broadcast = true) {
|
|
85
|
+
state.index = Math.max(0, Math.min(index, slides.length - 1));
|
|
86
|
+
state.step = Math.max(0, Math.min(step, maxStep(slides[state.index])));
|
|
87
|
+
render();
|
|
88
|
+
if (broadcast) deck.channel.postMessage({ type: "nav", index: state.index, step: state.step });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function advance(delta) {
|
|
92
|
+
const steps = maxStep(slides[state.index]);
|
|
93
|
+
if (delta > 0) {
|
|
94
|
+
if (state.step < steps) goTo(state.index, state.step + 1);
|
|
95
|
+
else goTo(state.index + 1, 0);
|
|
96
|
+
} else if (state.step > 0) goTo(state.index, state.step - 1);
|
|
97
|
+
else if (state.index > 0) goTo(state.index - 1, maxStep(slides[state.index - 1]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
deck.channel.onmessage = ({ data }) => data.type === "nav" && goTo(data.index, data.step, false);
|
|
101
|
+
document.addEventListener("keydown", (e) => {
|
|
102
|
+
if (e.target.isContentEditable) return;
|
|
103
|
+
if (["ArrowRight", "ArrowDown", "PageDown", " "].includes(e.key)) advance(1);
|
|
104
|
+
else if (["ArrowLeft", "ArrowUp", "PageUp"].includes(e.key)) advance(-1);
|
|
105
|
+
else return;
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
});
|
|
108
|
+
$(".pv-reset").onclick = () => (state.started = Date.now());
|
|
109
|
+
$(".pv-close").onclick = () => window.close();
|
|
110
|
+
setInterval(() => {
|
|
111
|
+
const s = Math.floor((Date.now() - state.started) / 1000);
|
|
112
|
+
$(".pv-timer").textContent = `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
|
|
113
|
+
}, 500);
|
|
114
|
+
|
|
115
|
+
window.addEventListener("message", ({ data }) => data?.type === "keyframes:ready" && render());
|
|
116
|
+
const [, n, s] = location.hash.match(/^#\/(\d+)(?:\/(\d+))?/) || [];
|
|
117
|
+
goTo(n ? Number(n) - 1 : 0, s ? Number(s) : 0, false);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildStrip(strip, slides, onPick) {
|
|
121
|
+
return slides.map((slide, i) => {
|
|
122
|
+
const clone = slide.cloneNode(true);
|
|
123
|
+
clone.classList.remove("is-current");
|
|
124
|
+
for (const video of clone.querySelectorAll("video")) video.removeAttribute("autoplay");
|
|
125
|
+
applyGeometry(clone);
|
|
126
|
+
applySteps(clone, Infinity);
|
|
127
|
+
renderMath(clone);
|
|
128
|
+
const thumb = document.createElement("button");
|
|
129
|
+
thumb.className = "pv-thumb";
|
|
130
|
+
thumb.title = `Go to slide ${i + 1}`;
|
|
131
|
+
thumb.append(clone);
|
|
132
|
+
thumb.insertAdjacentHTML("beforeend", `<span class="pv-thumb-n">${i + 1}</span>`);
|
|
133
|
+
thumb.onmousedown = (e) => e.preventDefault();
|
|
134
|
+
thumb.onclick = () => onPick(i);
|
|
135
|
+
strip.append(thumb);
|
|
136
|
+
return thumb;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createNotes(el, status, editable) {
|
|
141
|
+
const hint = editable ? "double-click to edit" : "";
|
|
142
|
+
const notesOf = (slide) => slide.querySelector("aside.notes");
|
|
143
|
+
let shown = null;
|
|
144
|
+
let editing = null;
|
|
145
|
+
status.textContent = hint;
|
|
146
|
+
|
|
147
|
+
function show(slide) {
|
|
148
|
+
shown = slide;
|
|
149
|
+
if (!editing) el.innerHTML = notesOf(slide)?.innerHTML || "<i>No notes</i>";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function start() {
|
|
153
|
+
if (editing) return;
|
|
154
|
+
editing = shown;
|
|
155
|
+
el.innerHTML = notesOf(editing)?.innerHTML ?? "";
|
|
156
|
+
el.contentEditable = "true";
|
|
157
|
+
document.execCommand("defaultParagraphSeparator", false, "p");
|
|
158
|
+
el.focus();
|
|
159
|
+
status.textContent = "editing · ⌘/Ctrl+Enter to save · Esc to cancel";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function finish(save) {
|
|
163
|
+
if (!editing) return;
|
|
164
|
+
const slide = editing;
|
|
165
|
+
const html = el.textContent.trim() ? el.innerHTML.trim() : "";
|
|
166
|
+
editing = null;
|
|
167
|
+
el.contentEditable = "false";
|
|
168
|
+
status.textContent = hint;
|
|
169
|
+
if (save && html !== (notesOf(slide)?.innerHTML.trim() ?? "")) await persist(slide, html);
|
|
170
|
+
show(shown);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function persist(slide, html) {
|
|
174
|
+
const url = `_api/notes/${encodeURIComponent(slide.dataset.slide)}`;
|
|
175
|
+
const response = await fetch(url, { method: "PUT", body: html }).catch(() => null);
|
|
176
|
+
if (!response?.ok) {
|
|
177
|
+
status.textContent = "save failed";
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let aside = notesOf(slide);
|
|
181
|
+
if (!aside) {
|
|
182
|
+
aside = document.createElement("aside");
|
|
183
|
+
aside.className = "notes";
|
|
184
|
+
(slide.querySelector("section") ?? slide).append(aside);
|
|
185
|
+
}
|
|
186
|
+
aside.innerHTML = html;
|
|
187
|
+
status.textContent = "saved";
|
|
188
|
+
setTimeout(() => !editing && (status.textContent = hint), 2000);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (editable) {
|
|
192
|
+
el.addEventListener("dblclick", start);
|
|
193
|
+
el.addEventListener("blur", () => finish(true));
|
|
194
|
+
el.addEventListener("keydown", (e) => {
|
|
195
|
+
if (e.key === "Escape") finish(false);
|
|
196
|
+
else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) finish(true);
|
|
197
|
+
else return;
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return { show };
|
|
202
|
+
}
|
|
@@ -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"))
|
|
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: () =>
|
|
162
|
-
b: () =>
|
|
163
|
-
s: () =>
|
|
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
|
-
import { maxStep } from "./steps.js";
|
|
2
|
-
|
|
3
|
-
const STYLE = `
|
|
4
|
-
body { background: #1b1c1f; color: #e8e8e8; font: 16px/1.45 system-ui, sans-serif; overflow: hidden; }
|
|
5
|
-
.keyframes-deck { display: none; }
|
|
6
|
-
.pv { display: grid; grid-template-columns: 1.7fr 1fr; grid-template-rows: auto 1fr; gap: 16px;
|
|
7
|
-
height: 100vh; padding: 16px; box-sizing: border-box; }
|
|
8
|
-
.pv-bar { grid-column: 1 / -1; display: flex; gap: 24px; align-items: baseline; font-variant-numeric: tabular-nums; }
|
|
9
|
-
.pv-bar b { font-size: 28px; }
|
|
10
|
-
.pv-bar button { background: #333; color: inherit; border: 0; border-radius: 6px; padding: 4px 10px; cursor: pointer; }
|
|
11
|
-
.pv-frame { position: relative; aspect-ratio: var(--ar); background: #000; border-radius: 8px; overflow: hidden; }
|
|
12
|
-
.pv-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
|
|
13
|
-
.pv-side { display: flex; flex-direction: column; gap: 16px; min-height: 0; }
|
|
14
|
-
.pv-label { color: #999; font-size: 13px; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 4px; }
|
|
15
|
-
.pv-notes { flex: 1; overflow: auto; font-size: 20px; background: #26272b; border-radius: 8px; padding: 16px; }
|
|
16
|
-
`;
|
|
17
|
-
|
|
18
|
-
export function startPresenter(deck) {
|
|
19
|
-
const { width, height } = deck.config;
|
|
20
|
-
const slides = deck.slides;
|
|
21
|
-
const state = { index: 0, step: 0, started: Date.now() };
|
|
22
|
-
|
|
23
|
-
document.head.insertAdjacentHTML("beforeend", `<style>${STYLE}</style>`);
|
|
24
|
-
document.title = `Presenter · ${document.title}`;
|
|
25
|
-
document.body.insertAdjacentHTML(
|
|
26
|
-
"beforeend",
|
|
27
|
-
`<div class="pv" style="--ar:${width}/${height}">
|
|
28
|
-
<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>
|
|
30
|
-
<div><div class="pv-label">Current</div><div class="pv-frame"><iframe class="pv-current"></iframe></div></div>
|
|
31
|
-
<div class="pv-side">
|
|
32
|
-
<div><div class="pv-label">Next</div><div class="pv-frame"><iframe class="pv-next"></iframe></div></div>
|
|
33
|
-
<div class="pv-label">Notes</div><div class="pv-notes"></div>
|
|
34
|
-
</div>
|
|
35
|
-
</div>`,
|
|
36
|
-
);
|
|
37
|
-
const $ = (s) => document.querySelector(s);
|
|
38
|
-
const current = $(".pv-current");
|
|
39
|
-
const next = $(".pv-next");
|
|
40
|
-
current.src = `?embed${location.hash}`;
|
|
41
|
-
next.src = "?embed&static";
|
|
42
|
-
|
|
43
|
-
const send = (frame, index, step) =>
|
|
44
|
-
frame.contentWindow?.postMessage({ type: "keyframes:goto", index, step }, "*");
|
|
45
|
-
|
|
46
|
-
function render() {
|
|
47
|
-
const slide = slides[state.index];
|
|
48
|
-
const steps = maxStep(slide);
|
|
49
|
-
const [nextIndex, nextStep] =
|
|
50
|
-
state.step < steps ? [state.index, state.step + 1] : [state.index + 1, 0];
|
|
51
|
-
send(current, state.index, state.step);
|
|
52
|
-
send(next, Math.min(nextIndex, slides.length - 1), nextStep);
|
|
53
|
-
$(".pv-count").textContent = `${state.index + 1} / ${slides.length}`;
|
|
54
|
-
$(".pv-steps").textContent = steps ? `build ${state.step}/${steps}` : "";
|
|
55
|
-
$(".pv-notes").innerHTML = slide.querySelector("aside.notes")?.innerHTML || "<i>No notes</i>";
|
|
56
|
-
history.replaceState(null, "", `?presenter#/${state.index + 1}/${state.step}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function goTo(index, step, broadcast = true) {
|
|
60
|
-
state.index = Math.max(0, Math.min(index, slides.length - 1));
|
|
61
|
-
state.step = Math.max(0, Math.min(step, maxStep(slides[state.index])));
|
|
62
|
-
render();
|
|
63
|
-
if (broadcast) deck.channel.postMessage({ type: "nav", index: state.index, step: state.step });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function advance(delta) {
|
|
67
|
-
const steps = maxStep(slides[state.index]);
|
|
68
|
-
if (delta > 0) {
|
|
69
|
-
if (state.step < steps) goTo(state.index, state.step + 1);
|
|
70
|
-
else goTo(state.index + 1, 0);
|
|
71
|
-
} else if (state.step > 0) goTo(state.index, state.step - 1);
|
|
72
|
-
else if (state.index > 0) goTo(state.index - 1, maxStep(slides[state.index - 1]));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
deck.channel.onmessage = ({ data }) => data.type === "nav" && goTo(data.index, data.step, false);
|
|
76
|
-
document.addEventListener("keydown", (e) => {
|
|
77
|
-
if (["ArrowRight", "ArrowDown", "PageDown", " "].includes(e.key)) advance(1);
|
|
78
|
-
else if (["ArrowLeft", "ArrowUp", "PageUp"].includes(e.key)) advance(-1);
|
|
79
|
-
else return;
|
|
80
|
-
e.preventDefault();
|
|
81
|
-
});
|
|
82
|
-
$(".pv-reset").onclick = () => (state.started = Date.now());
|
|
83
|
-
setInterval(() => {
|
|
84
|
-
const s = Math.floor((Date.now() - state.started) / 1000);
|
|
85
|
-
$(".pv-timer").textContent = `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
|
|
86
|
-
}, 500);
|
|
87
|
-
|
|
88
|
-
window.addEventListener("message", ({ data }) => data?.type === "keyframes:ready" && render());
|
|
89
|
-
const [, n, s] = location.hash.match(/^#\/(\d+)(?:\/(\d+))?/) || [];
|
|
90
|
-
goTo(n ? Number(n) - 1 : 0, s ? Number(s) : 0, false);
|
|
91
|
-
}
|
|
@@ -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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{keyframes-0.1.0 → keyframes-0.3.0}/src/keyframes/themes/default/layouts/section-streaks.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|