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.
- keyframes/__init__.py +1 -0
- keyframes/build.py +102 -0
- keyframes/check.py +107 -0
- keyframes/cli.py +149 -0
- keyframes/deck.py +67 -0
- keyframes/export.py +106 -0
- keyframes/guide/__init__.py +25 -0
- keyframes/guide/components/keyframes-code.md +60 -0
- keyframes/guide/components/keyframes-excalidraw.md +148 -0
- keyframes/guide/components/keyframes-mujoco.md +76 -0
- keyframes/guide/components/keyframes-slider.md +65 -0
- keyframes/guide/components/keyframes-three.md +81 -0
- keyframes/guide/core.md +113 -0
- keyframes/patch.py +77 -0
- keyframes/render.py +32 -0
- keyframes/scaffold/.gitignore +3 -0
- keyframes/scaffold/CLAUDE.md +1 -0
- keyframes/scaffold/assets/deck.css +1 -0
- keyframes/scaffold/assets/figure.svg +1 -0
- keyframes/scaffold/deck.toml +10 -0
- keyframes/scaffold/references/.gitkeep +0 -0
- keyframes/scaffold/slides/01-title.html +4 -0
- keyframes/scaffold/slides/02-intro.html +17 -0
- keyframes/server.py +204 -0
- keyframes/static/components/keyframes-code.js +423 -0
- keyframes/static/components/keyframes-excalidraw.js +189 -0
- keyframes/static/components/keyframes-mujoco.js +515 -0
- keyframes/static/components/keyframes-slider.js +255 -0
- keyframes/static/components/keyframes-three.js +224 -0
- keyframes/static/editor/editor.css +228 -0
- keyframes/static/editor/editor.js +594 -0
- keyframes/static/editor/panel.css +127 -0
- keyframes/static/editor/panel.js +357 -0
- keyframes/static/index.html +23 -0
- keyframes/static/runtime/component.js +126 -0
- keyframes/static/runtime/live.js +31 -0
- keyframes/static/runtime/math.js +33 -0
- keyframes/static/runtime/presenter.js +91 -0
- keyframes/static/runtime/runtime.css +91 -0
- keyframes/static/runtime/runtime.js +215 -0
- keyframes/static/runtime/steps.js +15 -0
- keyframes/terminal.py +144 -0
- keyframes/themes/default/STYLE_GUIDE.md +73 -0
- keyframes/themes/default/assets/arc-top.webp +0 -0
- keyframes/themes/default/assets/orbit-ring.webp +0 -0
- keyframes/themes/default/assets/streaks-top.webp +0 -0
- keyframes/themes/default/assets/streaks-wide.webp +0 -0
- keyframes/themes/default/assets/sun-top.webp +0 -0
- keyframes/themes/default/assets/title-orbit.webp +0 -0
- keyframes/themes/default/layouts/content.html +16 -0
- keyframes/themes/default/layouts/figure.html +7 -0
- keyframes/themes/default/layouts/grid-2.html +19 -0
- keyframes/themes/default/layouts/grid-3x2.html +28 -0
- keyframes/themes/default/layouts/grid-4.html +44 -0
- keyframes/themes/default/layouts/media-side.html +16 -0
- keyframes/themes/default/layouts/section-arc.html +4 -0
- keyframes/themes/default/layouts/section-streaks.html +4 -0
- keyframes/themes/default/layouts/section.html +4 -0
- keyframes/themes/default/layouts/split-wide.html +7 -0
- keyframes/themes/default/layouts/split.html +13 -0
- keyframes/themes/default/layouts/statement.html +4 -0
- keyframes/themes/default/layouts/title.html +5 -0
- keyframes/themes/default/layouts/two-column.html +5 -0
- keyframes/themes/default/theme.css +330 -0
- keyframes/themes/minimal/STYLE_GUIDE.md +41 -0
- keyframes/themes/minimal/layouts/figure.html +7 -0
- keyframes/themes/minimal/layouts/section.html +4 -0
- keyframes/themes/minimal/layouts/statement.html +3 -0
- keyframes/themes/minimal/layouts/title-body.html +10 -0
- keyframes/themes/minimal/layouts/title.html +5 -0
- keyframes/themes/minimal/layouts/two-column.html +11 -0
- keyframes/themes/minimal/theme.css +196 -0
- keyframes/vendor.py +181 -0
- keyframes-0.1.0.dist-info/METADATA +119 -0
- keyframes-0.1.0.dist-info/RECORD +77 -0
- keyframes-0.1.0.dist-info/WHEEL +4 -0
- keyframes-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# `<keyframes-excalidraw>`: Excalidraw diagrams
|
|
2
|
+
|
|
3
|
+
Renders a `.excalidraw` file as crisp, hand-drawn SVG (via `exportToSvg` from `@excalidraw/excalidraw` 0.18), scaled to fit its block. Supports click-through build steps. Files drawn at excalidraw.com work as-is, and so do files an LLM writes by hand (see the schema below).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<div data-block="diagram" data-x="96" data-y="200" data-w="1728" data-h="820">
|
|
9
|
+
<keyframes-excalidraw src="assets/keyframes-excalidraw/diagram.excalidraw"></keyframes-excalidraw>
|
|
10
|
+
</div>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The element fills its block (`display: block; width: 100%; height: 100%`). The drawing keeps its aspect ratio and is centered.
|
|
14
|
+
|
|
15
|
+
## Attributes
|
|
16
|
+
|
|
17
|
+
| Attribute | Default | Meaning |
|
|
18
|
+
|--------------|-----------|---------|
|
|
19
|
+
| `src` | required | Path to the `.excalidraw` JSON, relative to the deck root. Fetched fresh on every load, so editing the file live-reloads the slide. |
|
|
20
|
+
| `fit` | `contain` | `contain` shows the whole drawing. `cover` fills the block and crops the overflow. |
|
|
21
|
+
| `background` | off | Off means transparent, so the slide shows through. Bare `background` uses the file's `appState.viewBackgroundColor`. `background="#f1f3f5"` uses that color. |
|
|
22
|
+
| `dark` | off | Excalidraw's dark-mode export, which inverts colors. Use it on dark slides. |
|
|
23
|
+
| `padding` | `16` | Padding around the drawing, in scene units. |
|
|
24
|
+
|
|
25
|
+
Attributes are read once, at init.
|
|
26
|
+
|
|
27
|
+
## Build steps
|
|
28
|
+
|
|
29
|
+
An element shows up at step N if either of these is true:
|
|
30
|
+
|
|
31
|
+
- **`"customData": { "step": N }`** is set on the element. This is the easiest option when writing JSON.
|
|
32
|
+
- **It sits inside a frame named `step-N`.** This is the easiest option when drawing at excalidraw.com: draw a frame (F), name it `step-1`, and put elements in it.
|
|
33
|
+
|
|
34
|
+
Elements with no step appear at step 0. A bound label inherits its container's step, so you only need to tag the shape. The component adds hidden `<span data-step="N">` markers to itself, so the slide's step count includes the diagram's steps. Diagram steps combine with other `data-step` elements on the slide. In print/PDF, every step is shown.
|
|
35
|
+
|
|
36
|
+
## Gotchas
|
|
37
|
+
|
|
38
|
+
- Each step is its own SVG layer, stacked on the others. Later steps always draw above earlier ones, whatever their z-order in the file.
|
|
39
|
+
- The full drawing's bounds are reserved from step 0, so the layout doesn't jump. If the diagram looks off-center at step 0, that's why.
|
|
40
|
+
- Frame chrome (outline and name) is never rendered. If you want a visible panel, use a rectangle.
|
|
41
|
+
- Deep-linking to a step inside a diagram (`#/6/2`) lands on step 0. The step markers only exist once the file has loaded, which is after the runtime clamps the step.
|
|
42
|
+
- Fonts are embedded in the SVG as subsets, and the font files go through the vendor cache. To make every font available offline, run `keyframes build`, which loads each slide once.
|
|
43
|
+
- Text in a container that is too tall for it will overflow (Excalidraw would grow the box). Make boxes big enough.
|
|
44
|
+
|
|
45
|
+
## Authoring a `.excalidraw` file by hand
|
|
46
|
+
|
|
47
|
+
The file is plain JSON. On load the component fills in every missing field (seed, version, stroke defaults, and so on), measures text, wraps and centers labels, and positions arrow labels. So you only need to write the fields below.
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"type": "excalidraw",
|
|
52
|
+
"version": 2,
|
|
53
|
+
"elements": [ ... ],
|
|
54
|
+
"appState": { "viewBackgroundColor": "#ffffff" },
|
|
55
|
+
"files": {}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Coordinates are scene units: x grows to the right and y grows downward. Only the relative layout matters, because the drawing is scaled to fit the block. A canvas around 1600×600 suits a full-width slide block.
|
|
60
|
+
|
|
61
|
+
### Shapes: `rectangle`, `ellipse`, `diamond`
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{ "id": "sim", "type": "rectangle", "x": 0, "y": 220, "width": 380, "height": 180,
|
|
65
|
+
"backgroundColor": "#a5d8ff", "fillStyle": "solid", "roundness": { "type": 3 },
|
|
66
|
+
"boundElements": [{ "type": "text", "id": "sim-label" }] }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Optional fields:
|
|
70
|
+
|
|
71
|
+
- `strokeColor` (default `#1e1e1e`) and `strokeWidth` (1, 2 or 4)
|
|
72
|
+
- `strokeStyle`: `solid`, `dashed` or `dotted`
|
|
73
|
+
- `fillStyle`: `solid`, `hachure` or `cross-hatch`
|
|
74
|
+
- `roughness`: 0 is clean, 1 is sketchy, 2 is very sketchy
|
|
75
|
+
- `opacity`: 0–100
|
|
76
|
+
- `roundness`: `{ "type": 3 }` gives rounded rectangles; use `null` for sharp corners
|
|
77
|
+
- `customData.step`
|
|
78
|
+
|
|
79
|
+
### Labels inside a shape (bound text)
|
|
80
|
+
|
|
81
|
+
Add a `text` element whose `containerId` is the shape's id. Also list the text in the shape's `boundElements`.
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{ "id": "sim-label", "type": "text", "x": 0, "y": 0,
|
|
85
|
+
"text": "Simulation\nMuJoCo × 4096 envs", "fontSize": 32, "fontFamily": 5,
|
|
86
|
+
"textAlign": "center", "verticalAlign": "middle", "containerId": "sim" }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **Position:** `x`/`y` are recomputed from the container, so `0, 0` is fine.
|
|
90
|
+
- **Size:** leave `width`/`height` out; they are measured.
|
|
91
|
+
- **Line breaks:** use `\n`. Long lines wrap to the container's width.
|
|
92
|
+
- **Alignment:** always set `"textAlign": "center", "verticalAlign": "middle"`. The defaults are left/top.
|
|
93
|
+
|
|
94
|
+
### Free text
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "id": "note", "type": "text", "x": 0, "y": 460, "text": "4096 parallel envs", "fontSize": 24, "fontFamily": 5 }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`x`/`y` is the top-left corner.
|
|
101
|
+
|
|
102
|
+
`fontFamily` values:
|
|
103
|
+
|
|
104
|
+
| Value | Font |
|
|
105
|
+
|-------|------|
|
|
106
|
+
| 5 | Excalifont (hand-drawn, default) |
|
|
107
|
+
| 1 | Virgil (older hand-drawn) |
|
|
108
|
+
| 6 | Nunito (clean sans) |
|
|
109
|
+
| 8 | Comic Shanns (mono) |
|
|
110
|
+
| 3 | Cascadia (code) |
|
|
111
|
+
|
|
112
|
+
### Arrows and lines
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "id": "obs", "type": "arrow", "x": 386, "y": 270, "points": [[0, 0], [248, 0]],
|
|
116
|
+
"endArrowhead": "arrow",
|
|
117
|
+
"startBinding": { "elementId": "sim", "focus": 0, "gap": 6 },
|
|
118
|
+
"endBinding": { "elementId": "policy", "focus": 0, "gap": 6 },
|
|
119
|
+
"boundElements": [{ "type": "text", "id": "obs-label" }] }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **Points:** `x`/`y` is the start point, and `points` are offsets from it. The first point is always `[0, 0]`.
|
|
123
|
+
- **Paths:** add more points for an L- or U-shaped path, e.g. `[[0,0],[0,150],[-1280,150],[-1280,0]]`.
|
|
124
|
+
- **Arrowheads:** `startArrowhead`/`endArrowhead` take `null`, `arrow`, `triangle`, `dot` or `bar`.
|
|
125
|
+
- **Lines:** use `"type": "line"` for a line with no heads.
|
|
126
|
+
- **Bindings:** `startBinding`/`endBinding` are optional. They only matter if someone later edits the file at excalidraw.com, and then each target shape should also list the arrow in its `boundElements` (`{ "type": "arrow", "id": "obs" }`). The renderer does not snap arrows to shapes, so make the endpoints touch the shape edges yourself: leave a gap of about 6.
|
|
127
|
+
- **Labels:** give an arrow a label with a `text` element whose `containerId` is the arrow's id (`x`/`y` `0, 0`, center/middle). It is drawn at the middle of the path, with a gap cut in the line.
|
|
128
|
+
|
|
129
|
+
### Layout tips
|
|
130
|
+
|
|
131
|
+
- Put shapes on a grid, e.g. 380×180 boxes with 260 gaps.
|
|
132
|
+
- Compute arrow endpoints from box edges: a box's right edge at mid-height is (`x + width`, `y + height/2`).
|
|
133
|
+
- Use Excalidraw's palette for fills:
|
|
134
|
+
|
|
135
|
+
| Color | Hex |
|
|
136
|
+
|-------|-----|
|
|
137
|
+
| blue | `#a5d8ff` |
|
|
138
|
+
| yellow | `#ffec99` |
|
|
139
|
+
| green | `#b2f2bb` |
|
|
140
|
+
| violet | `#eebefa` |
|
|
141
|
+
| red | `#ffc9c9` |
|
|
142
|
+
| gray | `#e9ecef` |
|
|
143
|
+
|
|
144
|
+
Keep strokes `#1e1e1e`.
|
|
145
|
+
- Use fontSize 28–36 for box labels and 20–24 for arrow labels, so they stay readable once the drawing is scaled down to the block.
|
|
146
|
+
- Give every element a unique, readable `id`.
|
|
147
|
+
|
|
148
|
+
The example `examples/showcase/assets/keyframes-excalidraw/diagram.excalidraw` (slide `06-excalidraw.html`) is a complete hand-written file with 3 build steps.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# `<keyframes-mujoco>`: live MuJoCo physics
|
|
2
|
+
|
|
3
|
+
Runs a MuJoCo model in the browser using the official DeepMind WASM bindings (`@mujoco/mujoco`), stepped in real time and rendered with three.js: orbit camera, soft shadows, a checker ground. An optional controller module sets `data.ctrl` every physics step and can react to slide build steps.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<div class="card" data-block="sim" data-x="784" data-y="200" data-w="1040" data-h="800" style="padding: 0; overflow: hidden">
|
|
9
|
+
<keyframes-mujoco model="assets/arm/scene.xml" controller="assets/arm/ctrl.js"
|
|
10
|
+
camera="1.9,-1.6,1.35" target="0.1,0,0.35" snapshot-time="6.6"></keyframes-mujoco>
|
|
11
|
+
</div>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The element fills its block (`display: block; width: 100%; height: 100%`). Put it in a block with no padding, plus `overflow: hidden` if the corners are rounded. The canvas background is transparent, so the block's background shows through and an infinite ground plane fades into it.
|
|
15
|
+
|
|
16
|
+
Interaction: drag to orbit, scroll to zoom, right-drag to pan, **Ctrl+drag (Cmd on macOS) on a body to pull it** with a spring, **double-click to reset**.
|
|
17
|
+
|
|
18
|
+
## Attributes
|
|
19
|
+
|
|
20
|
+
| Attribute | Default | Meaning |
|
|
21
|
+
|-----------------|--------------------|---------|
|
|
22
|
+
| `model` | required | MJCF file, relative to the deck. |
|
|
23
|
+
| `assets` | none | Comma-separated extra files (meshes, textures, `<include>`d XML), **relative to the model file**. Each is written into MuJoCo's virtual file system under that same relative path, so write it exactly as the MJCF resolves it, including `meshdir`. For example, `meshdir="meshes"` with `file="arm.stl"` needs `assets="meshes/arm.stl"`. |
|
|
24
|
+
| `controller` | none | ES module that drives the sim (see below), relative to the deck. |
|
|
25
|
+
| `camera` | auto from model | Camera position `x,y,z` in **MuJoCo world coordinates (Z is up)**. |
|
|
26
|
+
| `target` | model center | Orbit target `x,y,z`, also Z-up. |
|
|
27
|
+
| `speed` | `1` | Sim seconds per wall-clock second. Read every frame, so you can change it live. Ignored while a timeline is bound. |
|
|
28
|
+
| `paused` | off | When present, rendering continues but physics doesn't step. Can be toggled live. Ignored while a timeline is bound. |
|
|
29
|
+
| `snapshot-time` | `2` | For PDF export, the sim runs to this sim time (in seconds) before the frame is frozen. Pick a moment that shows something good. A bound timeline's `snapshot`/`value` takes precedence. |
|
|
30
|
+
|
|
31
|
+
## Model support
|
|
32
|
+
|
|
33
|
+
- Rendered geom types: `plane`, `sphere`, `capsule`, `ellipsoid`, `cylinder`, `box`, `mesh` (OBJ/STL/MSH, using MuJoCo's own processed vertices and normals). `hfield` and `sdf` are not drawn. Sites, tendons, and lights from the MJCF are not drawn either; the component uses its own lighting.
|
|
34
|
+
- Color comes from geom `rgba`, or from the material's `rgba` when the geom keeps the default color. Textures are ignored. Every plane gets a checker in its own color, with the checker size taken from the plane's third `size` value. A plane with `size="0 0 …"` is infinite and fades out at the edges.
|
|
35
|
+
- Geoms in `group` 3 or higher, and geoms with alpha 0, are hidden. This matches MuJoCo's convention for collision-only geoms.
|
|
36
|
+
- If the model has a `<keyframe>`, reset (on load and on double-click) goes to key 0. Otherwise it goes to `qpos0`.
|
|
37
|
+
- To show a moving target marker, use a `mocap="true"` body with a non-colliding geom (`contype="0" conaffinity="0"`) and set `data.mocap_pos` from the controller.
|
|
38
|
+
|
|
39
|
+
## Controller module
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
export default function setup({ mujoco, model, data, container, params }) {
|
|
43
|
+
return {
|
|
44
|
+
step(t) { data.ctrl[0] = (params.gain ?? 1) * Math.sin(t); }, // before every mj_step; t = data.time
|
|
45
|
+
onStep(n) {}, // slide build step changed (also called once at load)
|
|
46
|
+
onParam(name, value) {}, // a bound <keyframes-slider param> changed (also called at load for set values)
|
|
47
|
+
reset() {}, // after mj_resetData / keyframe, before mj_forward
|
|
48
|
+
dispose() {}, // the component is being destroyed
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
All fields are optional, and `setup` may be `async`. `mujoco` is the loaded WASM module (for `mj_name2id`, `mjtObj`, and so on), `model` and `data` are the live `MjModel` and `MjData`, `container` is the `<keyframes-mujoco>` element, and `params` holds the live values of bound `<keyframes-slider param="…">` elements (read it inside `step`). In print/PDF mode, `onStep` gets the slide's last build step.
|
|
54
|
+
|
|
55
|
+
Useful `data` arrays: `ctrl`, `qpos`, `qvel`, `qfrc_bias` (gravity + Coriolis, which is handy for gravity compensation), `xpos`/`xmat` (body poses), `geom_xpos`, `mocap_pos`/`mocap_quat`, `xfrc_applied`. Use `mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM.value, "hand")` to look things up by name.
|
|
56
|
+
|
|
57
|
+
From outside, the element exposes `el.mujoco`, `el.mjModel`, `el.mjData`, `el.reset()`, and `el.seek(t)` once it has loaded.
|
|
58
|
+
|
|
59
|
+
## Timeline
|
|
60
|
+
|
|
61
|
+
A bound `<keyframes-slider for="sim" timeline duration="12" loop>` takes over the clock. Play advances the sim in real time. Seeking forward steps the physics up to the new time. Seeking backward resets to the keyframe (calling the controller's `reset()`) and re-simulates up to that time, so with a deterministic controller the result is exactly the state the sim had at that moment. The PDF frame is the timeline's `snapshot` (or `value`).
|
|
62
|
+
|
|
63
|
+
- Seeking costs one `mj_step` per timestep of sim time from the keyframe (6,000 steps to seek to 12 s at `timestep="0.002"`). That is instant for small models but noticeable for heavy ones, so keep `duration` short for big scenes.
|
|
64
|
+
- Ctrl+drag pushes apply while time is advancing, but they are not recorded. Seeking backward drops them. Double-click reset re-simulates to the current timeline position without pushes.
|
|
65
|
+
- The controller must depend only on `data` and `t` (plus params and build step) for seeks to be exact. Keep state in `setup` closures and clear it in `reset()`.
|
|
66
|
+
|
|
67
|
+
## Gotchas
|
|
68
|
+
|
|
69
|
+
- **Get the arrays again on every access** (`data.ctrl`, `data.qpos`, and so on). Each one is a live view into WASM memory, and a view you keep around goes stale if that memory grows. Don't cache them in `setup`.
|
|
70
|
+
- Don't `.delete()` `model` or `data` in the controller; the component owns them. Do `.delete()` any MuJoCo objects you create yourself (`new mujoco.MjvPerturb()`, buffers, and so on), for example in `dispose()`.
|
|
71
|
+
- `step(t)` runs at physics rate (e.g. 500 Hz for `timestep="0.002"`), so keep it cheap. If the browser can't keep up, the sim runs slower than real time rather than taking huge steps.
|
|
72
|
+
- The bindings take the model from the virtual file system only. Any file the XML references must be listed in `assets`, or loading fails with a MuJoCo error in the red error box.
|
|
73
|
+
- Model `stat.extent` sets the default camera distance, and an infinite plane makes the extent large. Set `camera` and `target` explicitly for a well-framed shot.
|
|
74
|
+
- The sim keeps its state when you leave the slide and come back, unless it was disposed (more than 2 slides away). Then it reloads from the keyframe.
|
|
75
|
+
- The browser caches controller modules as ES modules. After editing `ctrl.js`, reload the page: hot slide reload re-runs `setup` but doesn't re-import the module.
|
|
76
|
+
- Every instance holds one WebGL context. Keep it to a few per deck.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# `<keyframes-slider>`: live parameters and timelines
|
|
2
|
+
|
|
3
|
+
A slider bound to another component on the same slide, so the presenter can drag a parameter or scrub time during the talk. Two modes:
|
|
4
|
+
|
|
5
|
+
- **param**: drives one named value in the target's `params` (`<keyframes-three>` scene modules, `<keyframes-mujoco>` controllers).
|
|
6
|
+
- **timeline**: a play/pause button plus a scrubber that owns the target's clock. Dragging seeks; play advances in real time.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
The slider is its own block, so the user can move and resize it in the editor like any other block. Put `data-block` and geometry directly on it.
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<div class="card" data-block="scene" data-x="784" data-y="200" data-w="1040" data-h="680" style="padding: 0; overflow: hidden">
|
|
14
|
+
<keyframes-three src="assets/wave/scene.js"></keyframes-three>
|
|
15
|
+
</div>
|
|
16
|
+
<keyframes-slider data-block="speed" data-x="96" data-y="800" data-w="640" data-h="72"
|
|
17
|
+
for="scene" param="speed" min="0" max="2" step="0.01" value="1" label="Speed"></keyframes-slider>
|
|
18
|
+
<keyframes-slider data-block="time" data-x="784" data-y="912" data-w="1040" data-h="72"
|
|
19
|
+
for="scene" timeline duration="10" loop snapshot="4"></keyframes-slider>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A height of 64–80 px fits one row at the default size. The slider draws no background, so it sits directly on the slide; wrap it in a `card` block if you want a panel.
|
|
23
|
+
|
|
24
|
+
## Attributes
|
|
25
|
+
|
|
26
|
+
| Attribute | Mode | Default | Meaning |
|
|
27
|
+
|------------|----------|---------------------|---------|
|
|
28
|
+
| `for` | both | required | `data-block` id (or element `id`) on the same slide. The slider binds to that element if it is a component, otherwise to the first component inside it. |
|
|
29
|
+
| `param` | param | required | Key in the target's `params`. |
|
|
30
|
+
| `min`, `max` | param | `0`, `1` | Range. |
|
|
31
|
+
| `step` | both | range / 100 (param), duration / 1000 (timeline) | Granularity of drag and arrow keys. Also sets the decimals shown for params. |
|
|
32
|
+
| `value` | both | midpoint (param), `0` (timeline) | Starting value. |
|
|
33
|
+
| `label` | both | none | Text shown left of the track. |
|
|
34
|
+
| `unit` | both | none (param), ` s` (timeline) | Suffix after the value readout, e.g. `unit="×"`. Include a leading space if you want one. |
|
|
35
|
+
| `timeline` | timeline | off | Timeline mode. |
|
|
36
|
+
| `duration` | timeline | `10` | Length in seconds. |
|
|
37
|
+
| `loop` | timeline | off | Wrap to 0 at the end. Without it, playback stops at `duration`; play again restarts from 0. |
|
|
38
|
+
| `paused` | timeline | off | Start paused instead of playing on slide entry. |
|
|
39
|
+
| `snapshot` | both | `value` | Value used for PDF/print instead of `value`. |
|
|
40
|
+
| `steps` | both | none | Comma-separated values per build step, e.g. `steps="0.5, 1, 2"`: step *n* sets the slider to item *n* (the last item repeats). For a timeline this seeks. |
|
|
41
|
+
|
|
42
|
+
From outside, `el.value` is the current value.
|
|
43
|
+
|
|
44
|
+
## Behaviour
|
|
45
|
+
|
|
46
|
+
- Values reach the target before it initializes, so a scene sees the slider's `value` from its first frame. They also survive the target being destroyed and re-created.
|
|
47
|
+
- Timeline position survives leaving and re-entering the slide. Playback pauses while the slide is off-screen.
|
|
48
|
+
- **Keyboard**: a focused slider owns the keys. Arrows and PageUp/PageDown adjust it and don't change slides. Space toggles play on a timeline. **Esc** hands keys back to slide navigation, and so does clicking anywhere else on the slide. Clickers send arrow/PageDown keys, so press Esc (or click the slide) after dragging.
|
|
49
|
+
- **Editor (E)**: click selects and drags the block. Double-click enters interaction mode, where the slider can be dragged.
|
|
50
|
+
- **PDF/print**: the slider renders as a static control showing `snapshot` (or `value`). The target freezes at that value. A timeline target freezes at that time (see the component docs). If `steps` is set, print uses the last item, because print shows the final build step.
|
|
51
|
+
- Styling uses the theme variables `--accent` (fill, thumb, play button), `--border` (track), `--ink`/`--fg` (label), `--muted` (readout), `--font-sans`, and `--font-mono`. Set `--keyframes-slider-size` (default `28px`) on the block to scale everything.
|
|
52
|
+
|
|
53
|
+
## Which components support it
|
|
54
|
+
|
|
55
|
+
| Target | `param` | `timeline` |
|
|
56
|
+
|--------|---------|------------|
|
|
57
|
+
| `<keyframes-three>` | `ctx.params` + `ctx.onParam` | `t` passed to `onFrame` follows the scrubber |
|
|
58
|
+
| `<keyframes-mujoco>` | controller `params` + `onParam` | seek by re-simulating from the keyframe |
|
|
59
|
+
| `<keyframes-code>`, `<keyframes-excalidraw>` | stored in `el.params`, unused | not supported (error box) |
|
|
60
|
+
|
|
61
|
+
## Gotchas
|
|
62
|
+
|
|
63
|
+
- `check` reports a `for` that matches nothing on the slide, or a slider with neither `param` nor `timeline`.
|
|
64
|
+
- With a timeline bound, write frame code as a function of `t` (e.g. `y = sin(k*x - w*t)`) so that seeking backwards is exact. Code that accumulates `dt` (`rotation += dt`) still runs, but it ignores the scrubber.
|
|
65
|
+
- One timeline per target. Any number of param sliders can share a target.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# `<keyframes-three>`: three.js scenes
|
|
2
|
+
|
|
3
|
+
Embeds a live three.js scene. The component owns the renderer, scene, camera, orbit controls, resizing, pixel ratio, the render loop, and GPU cleanup. You write a small ES module that fills in the scene.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<div class="card" data-block="scene" data-x="784" data-y="200" data-w="1040" data-h="800" style="padding: 0; overflow: hidden">
|
|
9
|
+
<keyframes-three src="assets/my-scene/scene.js" camera="4,3,5" target="0,1,0" fov="40"></keyframes-three>
|
|
10
|
+
</div>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The element fills its block (it is `display: block; width: 100%; height: 100%`). Put it in a block with no padding. If the block has rounded corners, add `overflow: hidden` so they clip the canvas.
|
|
14
|
+
|
|
15
|
+
## Attributes
|
|
16
|
+
|
|
17
|
+
| Attribute | Default | Meaning |
|
|
18
|
+
|--------------|---------------|---------|
|
|
19
|
+
| `src` | required | Scene module path, relative to the deck root (e.g. `assets/x/scene.js`). |
|
|
20
|
+
| `controls` | `orbit` | `orbit` gives OrbitControls with damping (drag to orbit, scroll to zoom, right-drag to pan). `none` turns them off. |
|
|
21
|
+
| `background` | `transparent` | `transparent` lets the slide or card show through. Any CSS color (`#0f1b3d`) sets `scene.background`. |
|
|
22
|
+
| `camera` | `4,3,6` | Camera position `x,y,z`. |
|
|
23
|
+
| `target` | `0,0,0` | Point the camera looks at, which is also the orbit pivot. |
|
|
24
|
+
| `fov` | `45` | Vertical field of view in degrees. |
|
|
25
|
+
|
|
26
|
+
Attributes are read once, at init.
|
|
27
|
+
|
|
28
|
+
## Scene module contract
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
export default async function setup(ctx) {
|
|
32
|
+
const { THREE, scene, camera, renderer, controls, container, onFrame, onStep, onResize, params, onParam, addons } = ctx;
|
|
33
|
+
const { RoomEnvironment } = await addons("environments/RoomEnvironment.js");
|
|
34
|
+
|
|
35
|
+
const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial({ color: 0x2f6fed }));
|
|
36
|
+
scene.add(cube, new THREE.HemisphereLight(0xffffff, 0x888888, 2));
|
|
37
|
+
|
|
38
|
+
let step = 0;
|
|
39
|
+
onStep((n) => (step = n));
|
|
40
|
+
onFrame((t, dt) => (cube.rotation.y += dt * (step >= 1 ? 2 : 0.3) * (params.speed ?? 1)));
|
|
41
|
+
onParam("color", (v) => cube.material.color.setHSL(v, 0.7, 0.5));
|
|
42
|
+
|
|
43
|
+
return () => {};
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| `ctx` field | Description |
|
|
48
|
+
|-------------|-------------|
|
|
49
|
+
| `THREE` | The `three` module from the import map. |
|
|
50
|
+
| `scene`, `camera`, `renderer` | `THREE.Scene`, `THREE.PerspectiveCamera`, and `THREE.WebGLRenderer` (antialias, alpha). Change them however you like: tone mapping, shadows, `camera.near`/`far`. |
|
|
51
|
+
| `controls` | The `OrbitControls` instance, or `null` when `controls="none"`. Example: `controls.maxPolarAngle = Math.PI * 0.47`. |
|
|
52
|
+
| `container` | The `<keyframes-three>` element. |
|
|
53
|
+
| `onFrame(cb(t, dt))` | Runs every frame while the slide is current. `t` counts seconds of play time (it does not advance while paused). `dt` is capped at 0.1 s. When a `<keyframes-slider timeline>` is bound, `t` is the scrubber position instead: it jumps when the presenter seeks and holds still while paused. `dt` stays the wall-clock frame time, so easing still settles. |
|
|
54
|
+
| `onStep(cb(n))` | Called with the slide's build step. It fires once after `setup` with the current step, then on every change. Use it to drive behaviour from `data-step` elements on the slide. |
|
|
55
|
+
| `onResize(cb(w, h))` | Called with the canvas size in unscaled slide px. It fires once after `setup`, then on every resize. The camera aspect is already updated. |
|
|
56
|
+
| `params` | Live values from bound `<keyframes-slider param="…">` elements, keyed by `param`. Read it in `onFrame` (`params.speed ?? 1`). Values set before `setup` runs are already present. |
|
|
57
|
+
| `onParam(name, cb(value))` | Called when that param changes, and once after `setup` if it already has a value. Use it for one-off rebuilds; for per-frame use, just read `params`. |
|
|
58
|
+
| `addons(path)` | `import("three/addons/" + path)`, e.g. `addons("loaders/GLTFLoader.js")`. |
|
|
59
|
+
|
|
60
|
+
`setup` may return a cleanup function. Use it for anything the component can't see, such as timers, workers, or listeners on `window`. You don't need to dispose meshes, materials, textures, `scene.environment` or `scene.background`: on destroy the component traverses the scene and disposes them, then calls `renderer.dispose()` and `forceContextLoss()`.
|
|
61
|
+
|
|
62
|
+
Your module can also `import * as THREE from "three"` directly, since the import map applies to it. Using `ctx.THREE` is equivalent.
|
|
63
|
+
|
|
64
|
+
## Lifecycle
|
|
65
|
+
|
|
66
|
+
- **init**: runs the first time the slide is shown. It creates the renderer, imports `src` with a `?v=<timestamp>` cache-buster so live reload always gets fresh code, then awaits `setup`. If anything throws, the GPU context is freed and an error box appears in the block.
|
|
67
|
+
- **play/pause**: the render loop runs only while the slide is current. Off-slide, a step change or resize renders a single frame.
|
|
68
|
+
- **destroy**: runs when the slide is more than 2 slides away or is live-replaced. It calls your cleanup and frees the WebGL context. Coming back re-runs `setup` from scratch, with `t` reset to 0.
|
|
69
|
+
- **snapshot (PDF/print)**: the loop stops, frame callbacks are simulated at 60 Hz until `t >= 2` s, and one frame is rendered and frozen into an `<img>`. In print the step is the slide's max step, so the PDF shows the final, settled build state. Bound sliders apply their `snapshot` (or `value`) first. With a timeline, `t` is pinned at that position and 2 s of frames run at fixed `t` so that easing settles.
|
|
70
|
+
|
|
71
|
+
## Gotchas
|
|
72
|
+
|
|
73
|
+
- Keep all motion in `onFrame`. Don't call `requestAnimationFrame` yourself, because it keeps running when the slide is off-screen.
|
|
74
|
+
- Base animation on `t`/`dt`, not wall-clock time. Exponential easing like `x += (goal - x) * (1 - Math.exp(-dt * k))` gives smooth step transitions and settles correctly in snapshots.
|
|
75
|
+
- Only the `src` module is cache-busted. Helper modules it imports relatively are cached for the page's lifetime, but editing any asset triggers a full reload in dev anyway.
|
|
76
|
+
- Pixel ratio is `devicePixelRatio * deck scale`, capped at 3, and is updated on deck rescale. Sizes in your code should be in world units, not pixels.
|
|
77
|
+
- Each `<keyframes-three>` holds one WebGL context, and browsers cap them at about 16. Several scenes per slide is fine. The runtime disposes far-away slides.
|
|
78
|
+
- With `background="transparent"`, receive shadows on a `THREE.ShadowMaterial` ground plane so only the shadow shows.
|
|
79
|
+
- Heavy setup, like sampling thousands of points or loading GLTF, belongs in `setup`, which is async. Show the result immediately rather than waiting for the first step.
|
|
80
|
+
- Addons must exist under `three@<version>/examples/jsm/`. Check the three version in `DEFAULT_IMPORTS` (`src/keyframes/vendor.py`).
|
|
81
|
+
- In the editor (E), pointer events are disabled on block contents, so orbiting doesn't interfere with dragging blocks.
|
keyframes/guide/core.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Keyframes deck: authoring guide
|
|
2
|
+
|
|
3
|
+
This folder is a [keyframes](https://pypi.org/project/keyframes/) deck. Slides are HTML
|
|
4
|
+
files you edit directly. The user lays out blocks by hand in a live browser editor, so
|
|
5
|
+
**respect existing geometry** unless asked to change layout.
|
|
6
|
+
|
|
7
|
+
Run `uv run keyframes guide` for the full, version-matched reference: components, the
|
|
8
|
+
active theme's style guide, and its layout templates.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. The user runs `uv run keyframes dev`: Chrome opens, every save hot-reloads the slide.
|
|
13
|
+
2. Read `references/` for source material the user wants the talk built from.
|
|
14
|
+
3. Write or edit `slides/NN-name.html`. Slides are ordered by filename; use two-digit
|
|
15
|
+
prefixes and insert with suffixes (`03b-ablation.html`) instead of renaming files.
|
|
16
|
+
4. Verify your work: `uv run keyframes check --render` (structure, missing files, browser
|
|
17
|
+
errors, text overflowing its box) and `uv run keyframes shot 3 4` (PNG screenshots in
|
|
18
|
+
`.keyframes/shots/`). Before finishing, look at every shot: one claim per slide, text
|
|
19
|
+
density, alignment of edges and captions. Fix and re-shoot.
|
|
20
|
+
|
|
21
|
+
## Writing slides
|
|
22
|
+
|
|
23
|
+
- Titles are claims written as one sentence ("Sim-trained grasps transfer to the real arm"),
|
|
24
|
+
not topics ("Results"). Keep them to one line where the layout allows.
|
|
25
|
+
- One main visual per slide, with at most two supporting bullets.
|
|
26
|
+
- Captions say what is happening and where it comes from (real / sim / scripted), with
|
|
27
|
+
numbers and units: "Real robot, 1× speed · 12/15 grasps succeed".
|
|
28
|
+
- Citations go in a short `cite` footer: `Author et al. Venue Year · Author et al. Venue Year`.
|
|
29
|
+
- Speaker notes are what to say: what to point at, caveats, the so-what. Not a transcript of
|
|
30
|
+
the slide.
|
|
31
|
+
|
|
32
|
+
## Slide format
|
|
33
|
+
|
|
34
|
+
Each file holds exactly one `<section>` drawn on a fixed **1920×1080 canvas**. Every
|
|
35
|
+
top-level element is a *block* with a unique id and absolute geometry in canvas pixels:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<section data-layout="split">
|
|
39
|
+
<h2 class="title" data-block="title" data-x="88" data-y="72" data-w="800" data-h="170">The policy transfers to the real arm</h2>
|
|
40
|
+
<div class="body" data-block="points" data-x="88" data-y="270" data-w="800" data-h="650">
|
|
41
|
+
Trained only in simulation:
|
|
42
|
+
<ul class="small">
|
|
43
|
+
<li>87% success over 40 real trials</li>
|
|
44
|
+
<li>No fine-tuning on hardware</li>
|
|
45
|
+
</ul>
|
|
46
|
+
<p class="lead" data-step="1">Takeaway, revealed on the first click</p>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="media" data-block="sim" data-x="1010" data-y="92" data-w="860" data-h="896">
|
|
49
|
+
<keyframes-mujoco model="assets/arm.xml"></keyframes-mujoco>
|
|
50
|
+
</div>
|
|
51
|
+
<aside class="notes">Speaker notes (presenter view: press S).</aside>
|
|
52
|
+
</section>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Rules:
|
|
56
|
+
|
|
57
|
+
- `data-block` ids are unique per slide and stable. The editor and the user refer to blocks
|
|
58
|
+
by id (`slides/04-results.html#sim`), so never rename ids casually.
|
|
59
|
+
- `data-x`, `data-y`, `data-w`, `data-h` are integers. Optional `data-z` sets stacking.
|
|
60
|
+
Keep blocks inside the canvas. Text does not auto-shrink: size the box for its content,
|
|
61
|
+
or `check --render` reports overflow.
|
|
62
|
+
- When the user has moved/resized blocks, keep their numbers. Change geometry only when the
|
|
63
|
+
request is about layout.
|
|
64
|
+
- Don't nest blocks. Put structure *inside* a block with normal HTML/CSS (flex, grid).
|
|
65
|
+
- Copy starting layouts from the theme's templates (see `keyframes guide`), then adapt.
|
|
66
|
+
- Use the theme's classes (`headline`, `title`, `subtitle`, `body`, `lead`, `subhead`, `small`,
|
|
67
|
+
`caption`, `cite`, `media`, `card`, … — see the theme style guide) and CSS variables
|
|
68
|
+
(`var(--accent)`) instead of hard-coded styling.
|
|
69
|
+
Small inline `style` tweaks are fine; repeated patterns belong in `assets/deck.css`
|
|
70
|
+
(loaded via `head` in `deck.toml`).
|
|
71
|
+
- Put every file a slide uses under `assets/` (images, videos, models, scene modules).
|
|
72
|
+
Reference them with relative paths: `assets/robot.mp4`.
|
|
73
|
+
|
|
74
|
+
## Builds (click-through reveals)
|
|
75
|
+
|
|
76
|
+
- `data-step="N"`: element appears at step N (steps start at 0 = slide entry).
|
|
77
|
+
- `data-step-out="M"`: element disappears at step M (combine for replace-in-place).
|
|
78
|
+
- Components receive the step too (e.g. a 3D scene can change camera per step).
|
|
79
|
+
- Use builds to reveal the takeaway or question last. Don't step through ordinary bullets.
|
|
80
|
+
|
|
81
|
+
## Math
|
|
82
|
+
|
|
83
|
+
KaTeX renders automatically: `$inline$`, `$$display$$`, `\(...\)`, `\[...\]`,
|
|
84
|
+
`\begin{align}...\end{align}`. Escape literal dollar signs as `\$`, or add class `no-math`
|
|
85
|
+
to a container to skip it.
|
|
86
|
+
|
|
87
|
+
## Media
|
|
88
|
+
|
|
89
|
+
- Video: `<video src="assets/clip.mp4" autoplay muted loop playsinline class="fill"></video>`
|
|
90
|
+
restarts every time the slide is entered and pauses when it's left.
|
|
91
|
+
- Images: `<img src="assets/fig.png" class="fill">` inside a `media` block.
|
|
92
|
+
- `fill` crops to the block; `fill contain` (on an img or video) letterboxes instead. Use
|
|
93
|
+
`contain` for plots, figures and clips whose edges matter.
|
|
94
|
+
- Arbitrary HTML/CSS/JS is allowed. Prefer components (below) for anything heavy so
|
|
95
|
+
it starts/stops with the slide and works in PDF export.
|
|
96
|
+
|
|
97
|
+
## ES modules
|
|
98
|
+
|
|
99
|
+
Bare imports `three`, `three/addons/...`, `katex`, `@mujoco/mujoco` and anything listed under
|
|
100
|
+
`[imports]` in `deck.toml` resolve through a local caching proxy: decks keep working offline
|
|
101
|
+
after the first load. Add libraries in `deck.toml` instead of `<script src="https://...">`.
|
|
102
|
+
|
|
103
|
+
## Commands
|
|
104
|
+
|
|
105
|
+
| command | purpose |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `keyframes dev` | live server + layout editor (press E in the browser) |
|
|
108
|
+
| `keyframes present` | serve without the editor |
|
|
109
|
+
| `keyframes check [--render]` | lint slides; `--render` adds browser errors and overflow |
|
|
110
|
+
| `keyframes shot [N ...]` | screenshot slides to `.keyframes/shots/` |
|
|
111
|
+
| `keyframes pdf` | export PDF (all build steps visible) |
|
|
112
|
+
| `keyframes build` | self-contained static site in `dist/` |
|
|
113
|
+
| `keyframes guide` | this guide + components + theme reference |
|
keyframes/patch.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Surgical edits to slide source: only the touched attributes of the touched tags change."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from html.parser import HTMLParser
|
|
6
|
+
|
|
7
|
+
GEOMETRY_KEYS = ("x", "y", "w", "h", "z")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class BlockTag:
|
|
12
|
+
id: str
|
|
13
|
+
start: int
|
|
14
|
+
end: int
|
|
15
|
+
text: str
|
|
16
|
+
attrs: dict[str, str | None]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _BlockFinder(HTMLParser):
|
|
20
|
+
def __init__(self, source: str):
|
|
21
|
+
super().__init__(convert_charrefs=False)
|
|
22
|
+
self.line_offsets = [0]
|
|
23
|
+
for line in source.split("\n"):
|
|
24
|
+
self.line_offsets.append(self.line_offsets[-1] + len(line) + 1)
|
|
25
|
+
self.blocks: list[BlockTag] = []
|
|
26
|
+
|
|
27
|
+
def handle_starttag(self, tag, attrs):
|
|
28
|
+
attr_map = dict(attrs)
|
|
29
|
+
block_id = attr_map.get("data-block")
|
|
30
|
+
if block_id is None:
|
|
31
|
+
return
|
|
32
|
+
line, col = self.getpos()
|
|
33
|
+
start = self.line_offsets[line - 1] + col
|
|
34
|
+
text = self.get_starttag_text()
|
|
35
|
+
self.blocks.append(BlockTag(block_id, start, start + len(text), text, attr_map))
|
|
36
|
+
|
|
37
|
+
handle_startendtag = handle_starttag
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def find_block_tags(source: str) -> list[BlockTag]:
|
|
41
|
+
finder = _BlockFinder(source)
|
|
42
|
+
finder.feed(source)
|
|
43
|
+
finder.close()
|
|
44
|
+
return finder.blocks
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def set_attrs(tag_text: str, values: dict[str, str | None]) -> str:
|
|
48
|
+
for name, value in values.items():
|
|
49
|
+
pattern = re.compile(rf"""(\s){re.escape(name)}(\s*=\s*("[^"]*"|'[^']*'|[^\s"'>]+))?(?=[\s/>])""")
|
|
50
|
+
if value is None:
|
|
51
|
+
tag_text = pattern.sub("", tag_text, count=1)
|
|
52
|
+
elif pattern.search(tag_text):
|
|
53
|
+
replacement = f'{name}="{value}"'
|
|
54
|
+
tag_text = pattern.sub(lambda m, r=replacement: m.group(1) + r, tag_text, count=1)
|
|
55
|
+
else:
|
|
56
|
+
close = re.search(r"\s*/?>$", tag_text)
|
|
57
|
+
tag_text = f'{tag_text[: close.start()]} {name}="{value}"{tag_text[close.start() :]}'
|
|
58
|
+
return tag_text
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def geometry_attrs(geometry: dict[str, float | None]) -> dict[str, str | None]:
|
|
62
|
+
unknown = set(geometry) - set(GEOMETRY_KEYS)
|
|
63
|
+
if unknown:
|
|
64
|
+
raise ValueError(f"Unknown geometry keys: {sorted(unknown)}")
|
|
65
|
+
return {f"data-{key}": None if value is None else str(round(value)) for key, value in geometry.items()}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def patch_blocks(source: str, updates: dict[str, dict[str, str | None]]) -> str:
|
|
69
|
+
"""Apply attribute updates keyed by block id. Raises KeyError for unknown ids."""
|
|
70
|
+
tags = {tag.id: tag for tag in find_block_tags(source)}
|
|
71
|
+
missing = set(updates) - set(tags)
|
|
72
|
+
if missing:
|
|
73
|
+
raise KeyError(f"Blocks not found: {sorted(missing)}")
|
|
74
|
+
for tag in sorted((tags[i] for i in updates), key=lambda t: t.start, reverse=True):
|
|
75
|
+
new_text = set_attrs(tag.text, updates[tag.id])
|
|
76
|
+
source = source[: tag.start] + new_text + source[tag.end :]
|
|
77
|
+
return source
|
keyframes/render.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from .deck import STATIC_DIR, Deck
|
|
5
|
+
from .vendor import vendor_import_map
|
|
6
|
+
|
|
7
|
+
DEV_SCRIPTS = """<link rel="stylesheet" href="_keyframes/editor/editor.css">
|
|
8
|
+
<script type="module" src="_keyframes/editor/editor.js"></script>"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render_slide(name: str, source: str) -> str:
|
|
12
|
+
return f'<article class="keyframes-slide" data-slide="{html.escape(name)}">\n{source}\n</article>'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_slides(deck: Deck) -> str:
|
|
16
|
+
return "\n".join(render_slide(p.stem, p.read_text()) for p in deck.slide_paths())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def render_index(deck: Deck, mode: str, import_map: dict[str, str] | None = None) -> str:
|
|
20
|
+
template = (STATIC_DIR / "index.html").read_text()
|
|
21
|
+
config = {"mode": mode, "width": deck.width, "height": deck.height, "title": deck.title}
|
|
22
|
+
replacements = {
|
|
23
|
+
"{{title}}": html.escape(deck.title),
|
|
24
|
+
"{{importmap}}": json.dumps({"imports": import_map or vendor_import_map(deck.imports)}, indent=2),
|
|
25
|
+
"{{config}}": json.dumps(config),
|
|
26
|
+
"{{head}}": "\n".join(deck.extra_head),
|
|
27
|
+
"{{dev}}": DEV_SCRIPTS if mode == "dev" else "",
|
|
28
|
+
"{{slides}}": render_slides(deck),
|
|
29
|
+
}
|
|
30
|
+
for key, value in replacements.items():
|
|
31
|
+
template = template.replace(key, value)
|
|
32
|
+
return template
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Deck-specific styles go here. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 600"><rect width="860" height="600" fill="#f1f3f4"/><text x="430" y="310" font-family="sans-serif" font-size="28" fill="#9aa0a6" text-anchor="middle">assets/figure.svg</text></svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
title = "{{title}}"
|
|
2
|
+
theme = "{{theme}}"
|
|
3
|
+
size = [1920, 1080]
|
|
4
|
+
|
|
5
|
+
# Extra tags injected into <head>:
|
|
6
|
+
head = ['<link rel="stylesheet" href="assets/deck.css">']
|
|
7
|
+
|
|
8
|
+
# Extra ES module imports available to slides and assets (proxied and cached for offline use):
|
|
9
|
+
# [imports]
|
|
10
|
+
# "d3" = "https://cdn.jsdelivr.net/npm/d3@7/+esm"
|
|
File without changes
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<section data-layout="title">
|
|
2
|
+
<h1 class="headline" data-block="title" data-x="220" data-y="193" data-w="735" data-h="220">Talk Title on Two Lines</h1>
|
|
3
|
+
<p class="byline" data-block="byline" data-x="220" data-y="465" data-w="735" data-h="130">Speaker Name<br>Venue · Date</p>
|
|
4
|
+
</section>
|