presently 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/animated-diagrams.md +259 -0
- data/context/animating-slides.md +35 -7
- data/context/getting-started.md +12 -6
- data/context/index.yaml +5 -0
- data/lib/presently/display_view.rb +1 -1
- data/lib/presently/export.rb +1 -1
- data/lib/presently/export.xrb +3 -2
- data/lib/presently/page.rb +1 -0
- data/lib/presently/playback.xrb +3 -2
- data/lib/presently/presentation_controller.rb +4 -2
- data/lib/presently/presenter_view.rb +16 -17
- data/lib/presently/recording_view.rb +7 -6
- data/lib/presently/slide.rb +41 -9
- data/lib/presently/slide_renderer.rb +10 -5
- data/lib/presently/slide_view.rb +20 -0
- data/lib/presently/state.rb +1 -0
- data/lib/presently/version.rb +1 -1
- data/public/_components/@socketry/presently/Presently/Scripts.js +6 -4
- data/public/_components/@socketry/presently/Presently/Slide.js +39 -1
- data/public/_components/animejs/LICENSE.md +9 -0
- data/public/_components/animejs/dist/bundles/anime.esm.min.js +7 -0
- data/public/_static/index.css +177 -76
- data/readme.md +11 -4
- data/releases.md +9 -0
- data/templates/diagram.xrb +5 -0
- data.tar.gz.sig +0 -0
- metadata +4 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e645cb300c0b3005e89e9c384c420e5b0702d48798738dbd5caae47fdf829059
|
|
4
|
+
data.tar.gz: d3ed14a08c1df1ee1af2b3ae7188aa40742b2f2f08e202aaf46cd6d31646020e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3509544227b7ab61cdec9f70bcaf3487d098dc0f7a5e5c94aea4eff58b828c7c3502122e56e0f1f19cdce21846019b60fd35adfc8f4654e675dc65be25eb6a0
|
|
7
|
+
data.tar.gz: 8e701e3f6253f6c9a4385ce745a789df17027763193da89c31e72b5e8136ceec588800400d2576a1c743a25da094f768a96fab38a08096ddc8da19644b5355df
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Animated Diagrams
|
|
2
|
+
|
|
3
|
+
This guide explains how to design responsive, lifecycle-safe animated diagrams in Presently using semantic markup, slide-specific CSS, and Anime.js choreography.
|
|
4
|
+
|
|
5
|
+
## Why Animated Diagrams?
|
|
6
|
+
|
|
7
|
+
Animation is useful when a diagram describes change rather than merely structure. A carefully paced diagram can show causality, ordering, concurrency, or data movement without presenting every relationship simultaneously.
|
|
8
|
+
|
|
9
|
+
Use an animated diagram when you need:
|
|
10
|
+
|
|
11
|
+
- **A process unfolding over time:** Requests, protocols, state machines, and deployment workflows.
|
|
12
|
+
- **Attention control:** Introduce one relationship at a time while keeping the complete layout stable.
|
|
13
|
+
- **Coordinated motion:** Move packets, highlight participants, update counters, or trace paths together.
|
|
14
|
+
|
|
15
|
+
Prefer a static diagram when motion does not add meaning. Prefer Presently's build effects or `slide.after()` when elements only need to appear sequentially.
|
|
16
|
+
|
|
17
|
+
## Separate Structure, Appearance, and Choreography
|
|
18
|
+
|
|
19
|
+
A maintainable diagram has three layers:
|
|
20
|
+
|
|
21
|
+
1. The slide's Markdown file contains the complete semantic structure.
|
|
22
|
+
2. A matching sidecar stylesheet defines layout and visual language.
|
|
23
|
+
3. The slide script describes how the scene changes over time.
|
|
24
|
+
|
|
25
|
+
For `slides/040-request-flow.md`, place its styles in `slides/040-request-flow.css`. Presently scopes that stylesheet to the matching slide automatically.
|
|
26
|
+
|
|
27
|
+
Keeping the complete scene in the document makes the diagram understandable without animation, prevents layout shifts, and gives agentic tools clear boundaries for editing each concern.
|
|
28
|
+
|
|
29
|
+
## Choose the Right Rendering Medium
|
|
30
|
+
|
|
31
|
+
Presently does not require SVG. Choose the simplest medium that expresses the diagram:
|
|
32
|
+
|
|
33
|
+
| Medium | Best suited to |
|
|
34
|
+
|---|---|
|
|
35
|
+
| HTML with Grid or Flexbox | Cards, services, queues, labels, dashboards, and responsive process diagrams. |
|
|
36
|
+
| SVG with a `viewBox` | Edges, paths, graphs, precise coordinates, and shapes that must scale as one scene. |
|
|
37
|
+
| Canvas | Large numbers of particles or frequently redrawn objects where retained DOM elements become expensive. |
|
|
38
|
+
| Mixed HTML and SVG | Accessible HTML nodes over an SVG layer containing connectors and moving paths. |
|
|
39
|
+
|
|
40
|
+
Start with HTML and CSS. Introduce SVG when relationships or geometry require it, not merely because the result is called a diagram.
|
|
41
|
+
|
|
42
|
+
## Create a Scoped Anime.js Timeline
|
|
43
|
+
|
|
44
|
+
`slide.anime(callback)` creates an Anime.js scope rooted at the current slide body. The callback receives Anime.js's exports and the raw scope:
|
|
45
|
+
|
|
46
|
+
``` javascript
|
|
47
|
+
slide.anime(({createTimeline, stagger}, scope) => {
|
|
48
|
+
const timeline = createTimeline({
|
|
49
|
+
autoplay: slide.animated,
|
|
50
|
+
loop: slide.animated,
|
|
51
|
+
loopDelay: 1200,
|
|
52
|
+
})
|
|
53
|
+
.add(".diagram-node", {
|
|
54
|
+
opacity: [0.35, 1],
|
|
55
|
+
y: [12, 0],
|
|
56
|
+
delay: stagger(100),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (!slide.animated) timeline.seek(timeline.duration)
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
String selectors are resolved within the slide rather than the whole document. The same scope is returned from every call to `slide.anime()` and can be used for advanced Anime.js features:
|
|
64
|
+
|
|
65
|
+
``` javascript
|
|
66
|
+
const scope = slide.anime()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Presently calls `scope.revert()` when the slide is deactivated. This cancels its animations and restores properties modified through the scope. Additional resources such as audio, observers, and third-party controls should still use `slide.defer()` or `slide.signal`.
|
|
70
|
+
|
|
71
|
+
## Reuse a Diagram Across Slides
|
|
72
|
+
|
|
73
|
+
Shared Markdown can define both a diagram and its Anime.js timeline. Mark the reusable setup with a `javascript presently` fence so Presently removes it from the rendered diagram and executes it before the slide-specific script:
|
|
74
|
+
|
|
75
|
+
```` markdown
|
|
76
|
+
<div class="request-flow">
|
|
77
|
+
<!-- Shared diagram structure. -->
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
```javascript presently
|
|
81
|
+
slide.anime(({createTimeline}, scope) => {
|
|
82
|
+
scope.data.timeline = createTimeline({autoplay: false})
|
|
83
|
+
.label("request")
|
|
84
|
+
// Define the complete shared choreography.
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
````
|
|
88
|
+
|
|
89
|
+
Include that fragment in each slide:
|
|
90
|
+
|
|
91
|
+
``` markdown
|
|
92
|
+
![[shared/request-flow.md]]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The ordinary JavaScript block in the slide's presenter notes runs afterward and can select the state appropriate for that slide:
|
|
96
|
+
|
|
97
|
+
``` javascript
|
|
98
|
+
const timeline = slide.anime().data.timeline
|
|
99
|
+
timeline.seek("request")
|
|
100
|
+
timeline.play()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Every executable block has its own JavaScript lexical scope but receives the same `slide` object. Use the Anime scope's `data` or `methods` properties for intentional communication between shared setup and slide-specific control. All of those resources remain local to the rendered slide and are reverted together when it is deactivated.
|
|
104
|
+
|
|
105
|
+
## Design the Static Scene First
|
|
106
|
+
|
|
107
|
+
Build and style the final diagram before adding animation. Every node should have a stable position, and hidden elements should still reserve the space they require.
|
|
108
|
+
|
|
109
|
+
Use a descriptive accessible label for a primarily visual scene:
|
|
110
|
+
|
|
111
|
+
``` html
|
|
112
|
+
<div
|
|
113
|
+
class="request-flow"
|
|
114
|
+
role="img"
|
|
115
|
+
aria-label="A request travels from the browser through the application to the database, then returns as a response."
|
|
116
|
+
>
|
|
117
|
+
<!-- Complete diagram structure. -->
|
|
118
|
+
</div>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If individual controls are interactive, keep them accessible instead of hiding the entire subtree behind `role="img"`.
|
|
122
|
+
|
|
123
|
+
## Choreograph Meaning, Not Decoration
|
|
124
|
+
|
|
125
|
+
Organize the timeline into meaningful phases. Introduce the structure once, then repeat only the activity that represents ongoing work. The following example reveals a stable diagram before repeatedly sending a request and response through a traffic lane:
|
|
126
|
+
|
|
127
|
+
``` javascript
|
|
128
|
+
slide.anime(({createTimeline, stagger}) => {
|
|
129
|
+
const traffic = createTimeline({
|
|
130
|
+
autoplay: false,
|
|
131
|
+
loop: true,
|
|
132
|
+
loopDelay: 900,
|
|
133
|
+
defaults: {ease: "inOutQuad"},
|
|
134
|
+
})
|
|
135
|
+
.add(".request", {
|
|
136
|
+
left: ["0%", "100%"],
|
|
137
|
+
opacity: [0, 1, 1, 0],
|
|
138
|
+
duration: 2200,
|
|
139
|
+
})
|
|
140
|
+
.add(".response", {
|
|
141
|
+
left: ["100%", "0%"],
|
|
142
|
+
opacity: [0, 1, 1, 0],
|
|
143
|
+
duration: 1800,
|
|
144
|
+
}, "+=350")
|
|
145
|
+
|
|
146
|
+
const intro = createTimeline({
|
|
147
|
+
autoplay: slide.animated,
|
|
148
|
+
onComplete: () => {
|
|
149
|
+
if (slide.animated) traffic.restart()
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
.add(".diagram-lane", {
|
|
153
|
+
opacity: [0, 1],
|
|
154
|
+
scaleX: [0.85, 1],
|
|
155
|
+
duration: 450,
|
|
156
|
+
})
|
|
157
|
+
.add(".diagram-node", {
|
|
158
|
+
opacity: [0.35, 1],
|
|
159
|
+
y: [10, 0],
|
|
160
|
+
delay: stagger(120),
|
|
161
|
+
duration: 500,
|
|
162
|
+
}, 150)
|
|
163
|
+
.add(".event", {
|
|
164
|
+
opacity: [0, 1],
|
|
165
|
+
y: [8, 0],
|
|
166
|
+
delay: stagger(180),
|
|
167
|
+
duration: 450,
|
|
168
|
+
}, 550)
|
|
169
|
+
|
|
170
|
+
if (!slide.animated) intro.seek(intro.duration)
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Place each moving element inside the lane which defines its path. This makes `0%` and `100%` meaningful endpoints and keeps traffic from obscuring node labels. Use separate lanes only when they communicate a meaningful distinction, such as concurrent channels or different routes.
|
|
175
|
+
|
|
176
|
+
Give moving elements a hidden initial state in the sidecar stylesheet, since the repeating timeline remains paused while the scene is introduced:
|
|
177
|
+
|
|
178
|
+
``` css
|
|
179
|
+
.request,
|
|
180
|
+
.response {
|
|
181
|
+
opacity: 0;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Prefer transformations and opacity for continuous movement. Use CSS Grid, Flexbox, percentages, container query units, or an SVG `viewBox` to keep geometry responsive. Avoid repeatedly measuring layout inside animation callbacks.
|
|
186
|
+
|
|
187
|
+
## Establish a Consistent Visual Language
|
|
188
|
+
|
|
189
|
+
Use semantic classes and CSS custom properties so that meaning remains separate from a particular color or coordinate:
|
|
190
|
+
|
|
191
|
+
``` css
|
|
192
|
+
.diagram-node {
|
|
193
|
+
--node-color: var(--accent);
|
|
194
|
+
border: 0.08em solid var(--node-color);
|
|
195
|
+
background: color-mix(in srgb, var(--node-color) 10%, var(--slide-bg));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.diagram-node[data-tone="storage"] {
|
|
199
|
+
--node-color: #f90;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.diagram-node[data-state="active"] {
|
|
203
|
+
box-shadow: 0 0 1em color-mix(in srgb, var(--node-color) 25%, transparent);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Useful conventions include:
|
|
208
|
+
|
|
209
|
+
- `data-tone` for stable semantic roles such as client, processing, storage, success, and failure.
|
|
210
|
+
- `data-state` for runtime states such as idle, active, pending, complete, and unavailable.
|
|
211
|
+
- CSS variables for colors, line widths, spacing, timing, and repeated dimensions.
|
|
212
|
+
- Short labels and restrained color usage so motion remains the primary cue.
|
|
213
|
+
|
|
214
|
+
These conventions help separate agents generate diagrams that still look and behave like parts of the same presentation.
|
|
215
|
+
|
|
216
|
+
## Static Export and Reduced Motion
|
|
217
|
+
|
|
218
|
+
`slide.animated` is false during static export and when the viewer prefers reduced motion. Do not autoplay or loop in that case. Seek to a frame which communicates the diagram's result:
|
|
219
|
+
|
|
220
|
+
``` javascript
|
|
221
|
+
const timeline = createTimeline({
|
|
222
|
+
autoplay: slide.animated,
|
|
223
|
+
loop: slide.animated,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Add the complete choreography before selecting the static frame.
|
|
227
|
+
|
|
228
|
+
if (!slide.animated) timeline.seek(timeline.duration)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The final frame is not always the best summary. When necessary, add a timeline label and seek to that position instead.
|
|
232
|
+
|
|
233
|
+
## Common Pitfalls
|
|
234
|
+
|
|
235
|
+
- Do not create or remove the primary layout repeatedly during animation. Construct it once and animate its state.
|
|
236
|
+
- Do not select from `document` when a selector should be scoped to the current slide.
|
|
237
|
+
- Do not leave timelines, event listeners, media, or observers active after navigation.
|
|
238
|
+
- Do not rely on color alone to communicate state.
|
|
239
|
+
- Do not animate every available property. Motion should explain the system rather than compete with it.
|
|
240
|
+
- Do not assume animation will run during export or for every viewer.
|
|
241
|
+
|
|
242
|
+
## Instructions for Agentic Authoring
|
|
243
|
+
|
|
244
|
+
When asking an agent to create a diagram, provide the following constraints:
|
|
245
|
+
|
|
246
|
+
``` text
|
|
247
|
+
Create a Presently diagram slide that explains [process].
|
|
248
|
+
|
|
249
|
+
Keep semantic structure in the Markdown slide, appearance in the matching
|
|
250
|
+
sidecar CSS file, and choreography in the slide's JavaScript block. Construct
|
|
251
|
+
the complete scene before animating it. Use HTML/CSS unless SVG materially
|
|
252
|
+
simplifies connectors or geometry. Use slide.anime() for coordinated motion,
|
|
253
|
+
respect slide.animated, and choose a meaningful static export frame. Keep all
|
|
254
|
+
selectors scoped to the slide, use data-state/data-tone for semantic state,
|
|
255
|
+
include an accessible description, and ensure cleanup is owned by the slide.
|
|
256
|
+
Use motion only to clarify causality, ordering, concurrency, or data flow.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Ask the agent to verify the diagram at the audience display size and in the presenter preview. It should remain legible before the animation begins, at its busiest frame, and in its static exported state.
|
data/context/animating-slides.md
CHANGED
|
@@ -218,6 +218,32 @@ button.addEventListener("click", handleClick, {signal: slide.signal})
|
|
|
218
218
|
|
|
219
219
|
Aborting the signal removes this event listener; it does not invoke `handleClick`. Use `slide.defer(...)` for resources which do not accept an `AbortSignal`.
|
|
220
220
|
|
|
221
|
+
## Anime.js
|
|
222
|
+
|
|
223
|
+
Build effects and `slide.after()` work well for simple reveals. For coordinated motion, path animation, staggered elements, or a longer visual narrative, use `slide.anime(callback)` to create a lifecycle-managed [Anime.js](https://animejs.com/) scope.
|
|
224
|
+
|
|
225
|
+
``` javascript
|
|
226
|
+
slide.anime(({createTimeline, stagger}) => {
|
|
227
|
+
const timeline = createTimeline({
|
|
228
|
+
autoplay: slide.animated,
|
|
229
|
+
loop: slide.animated,
|
|
230
|
+
})
|
|
231
|
+
.add(".diagram-node", {
|
|
232
|
+
opacity: [0, 1],
|
|
233
|
+
y: [12, 0],
|
|
234
|
+
delay: stagger(100),
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
if (!slide.animated) timeline.seek(timeline.duration)
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The callback receives the Anime.js module API and runs inside a scope rooted at the slide body. Selectors cannot match elements in another slide. Presently reuses the scope across calls and automatically invokes `scope.revert()` when the slide is deactivated.
|
|
242
|
+
|
|
243
|
+
`slide.animated` is false during static export and when the browser requests reduced motion. Configure timelines not to autoplay or loop in that case, then seek to a meaningful static frame.
|
|
244
|
+
|
|
245
|
+
See [Animated Diagrams](../animated-diagrams/) for layout, choreography, accessibility, and agent-authoring guidance.
|
|
246
|
+
|
|
221
247
|
## Looping Animations with `slide.loop()`
|
|
222
248
|
|
|
223
249
|
To repeat an animation indefinitely, use `slide.loop()`. The callback receives a fresh `SlideContext` each iteration and can use `after()` to schedule steps in the same way as a regular chain. The loop waits for all steps to complete and then restarts, with an optional extra pause between iterations.
|
|
@@ -251,26 +277,28 @@ All slide templates support absolutely positioned elements since the slide conta
|
|
|
251
277
|
</div>
|
|
252
278
|
~~~
|
|
253
279
|
|
|
254
|
-
|
|
280
|
+
The `diagram` template centers its content by default. For coordinate-based layouts, use a `.diagram-freeform` wrapper; it fills the slide canvas and absolutely positions each direct child:
|
|
255
281
|
|
|
256
282
|
~~~ markdown
|
|
257
283
|
---
|
|
258
284
|
template: diagram
|
|
259
285
|
---
|
|
260
286
|
|
|
261
|
-
<div
|
|
262
|
-
|
|
263
|
-
|
|
287
|
+
<div class="diagram-freeform">
|
|
288
|
+
<div style="left: 10%; top: 20%; width: 35%; height: 30%; background: var(--surface-light);">
|
|
289
|
+
Node A
|
|
290
|
+
</div>
|
|
264
291
|
|
|
265
|
-
<div style="left: 55%; top: 20%; width: 35%; height: 30%; background: var(--surface-light);">
|
|
266
|
-
|
|
292
|
+
<div style="left: 55%; top: 20%; width: 35%; height: 30%; background: var(--surface-light);">
|
|
293
|
+
Node B
|
|
294
|
+
</div>
|
|
267
295
|
</div>
|
|
268
296
|
~~~
|
|
269
297
|
|
|
270
298
|
Combine with the scripting system to animate diagram elements into place:
|
|
271
299
|
|
|
272
300
|
``` javascript
|
|
273
|
-
const nodes = slide.find("div").builder({group: "node", effect: "fade"})
|
|
301
|
+
const nodes = slide.find(".diagram-freeform > div").builder({group: "node", effect: "fade"})
|
|
274
302
|
nodes.show(0)
|
|
275
303
|
slide
|
|
276
304
|
.after(400, () => nodes.next())
|
data/context/getting-started.md
CHANGED
|
@@ -336,7 +336,7 @@ System architecture overview
|
|
|
336
336
|
|
|
337
337
|
### Diagram
|
|
338
338
|
|
|
339
|
-
A
|
|
339
|
+
A centered canvas for diagrams and other custom visual layouts, with an optional title. A single grid or flex container is usually enough to create a diagram that remains centered as the slide scales:
|
|
340
340
|
|
|
341
341
|
``` markdown
|
|
342
342
|
---
|
|
@@ -344,15 +344,21 @@ template: diagram
|
|
|
344
344
|
duration: 60
|
|
345
345
|
---
|
|
346
346
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
347
|
+
# Title
|
|
348
|
+
|
|
349
|
+
Request lifecycle
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
351
|
+
# Body
|
|
352
|
+
|
|
353
|
+
<div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 2em; width: 80%;">
|
|
354
|
+
<div>Browser</div>
|
|
355
|
+
<div>→</div>
|
|
356
|
+
<div>Server</div>
|
|
353
357
|
</div>
|
|
354
358
|
```
|
|
355
359
|
|
|
360
|
+
For coordinate-based layouts, wrap the elements in `<div class="diagram-freeform">`. The wrapper fills the canvas and absolutely positions each direct child.
|
|
361
|
+
|
|
356
362
|
All other templates also support absolutely positioned overlays since the slide container is `position: relative`. This lets you add callouts, badges, or annotations on top of any template's normal content.
|
|
357
363
|
|
|
358
364
|
## Transitions
|
data/context/index.yaml
CHANGED
|
@@ -14,3 +14,8 @@ files:
|
|
|
14
14
|
title: Animating Slides
|
|
15
15
|
description: This guide explains how to animate content within slides using the
|
|
16
16
|
slide scripting system.
|
|
17
|
+
- path: animated-diagrams.md
|
|
18
|
+
title: Animated Diagrams
|
|
19
|
+
description: This guide explains how to design responsive, lifecycle-safe animated
|
|
20
|
+
diagrams in Presently using semantic markup, slide-specific CSS, and Anime.js
|
|
21
|
+
choreography.
|
|
@@ -60,7 +60,7 @@ module Presently
|
|
|
60
60
|
return unless slide
|
|
61
61
|
|
|
62
62
|
builder.tag(:div, class: "display", data: {transition: slide.transition}) do
|
|
63
|
-
builder.tag(:div, class: "slide-container") do
|
|
63
|
+
builder.tag(:div, class: "slide-container slide-viewport") do
|
|
64
64
|
@slide_renderer.render(builder, slide)
|
|
65
65
|
end
|
|
66
66
|
|
data/lib/presently/export.rb
CHANGED
|
@@ -117,7 +117,7 @@ module Presently
|
|
|
117
117
|
builder.text("Elapsed: #{format_duration(elapsed)}")
|
|
118
118
|
end
|
|
119
119
|
builder.tag(:span, class: "export-duration") do
|
|
120
|
-
builder.text("
|
|
120
|
+
builder.text("Duration: #{format_duration(slide.duration)}")
|
|
121
121
|
end
|
|
122
122
|
end
|
|
123
123
|
|
data/lib/presently/export.xrb
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
{
|
|
15
15
|
"imports": {
|
|
16
16
|
"@socketry/presently": "/_components/@socketry/presently/Presently.js",
|
|
17
|
-
"@socketry/syntax": "/_components/@socketry/syntax/Syntax.js"
|
|
17
|
+
"@socketry/syntax": "/_components/@socketry/syntax/Syntax.js",
|
|
18
|
+
"animejs": "/_components/animejs/dist/bundles/anime.esm.min.js"
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
</script>
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
<body class="export">
|
|
26
27
|
<?r self.slides.each_with_index do |slide, index| ?>
|
|
27
28
|
<div class="export-page" style="width: #{self.page_size.slide_width_px}px; height: #{self.page_size.slide_height_px + (self.notes ? self.page_size.notes_height_px : 0)}px;">
|
|
28
|
-
<div class="export-slide-area" style="width: #{self.page_size.slide_width_px}px; height: #{self.page_size.slide_height_px}px;">
|
|
29
|
+
<div class="export-slide-area slide-viewport" style="width: #{self.page_size.slide_width_px}px; height: #{self.page_size.slide_height_px}px;">
|
|
29
30
|
#{self.render_slide(slide)}
|
|
30
31
|
</div>
|
|
31
32
|
<?r if self.notes ?>
|
data/lib/presently/page.rb
CHANGED
|
@@ -24,6 +24,7 @@ module Presently
|
|
|
24
24
|
"morphdom" => "/_components/morphdom/morphdom-esm.js",
|
|
25
25
|
"@socketry/presently" => "/_components/@socketry/presently/Presently.js",
|
|
26
26
|
"@socketry/syntax" => "/_components/@socketry/syntax/Syntax.js",
|
|
27
|
+
"animejs" => "/_components/animejs/dist/bundles/anime.esm.min.js",
|
|
27
28
|
}.freeze
|
|
28
29
|
MODULES = ["/application.js"].freeze
|
|
29
30
|
|
data/lib/presently/playback.xrb
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
{
|
|
20
20
|
"imports": {
|
|
21
21
|
"@socketry/presently": "/_components/@socketry/presently/Presently.js",
|
|
22
|
-
"@socketry/syntax": "/_components/@socketry/syntax/Syntax.js"
|
|
22
|
+
"@socketry/syntax": "/_components/@socketry/syntax/Syntax.js",
|
|
23
|
+
"animejs": "/_components/animejs/dist/bundles/anime.esm.min.js"
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
</script>
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
<body class="playback" data-autoplay="#{self.autoplay}" data-controls="#{self.controls}">
|
|
31
32
|
<main class="playback-stage">
|
|
32
33
|
<?r self.slides.each_with_index do |slide, index| ?>
|
|
33
|
-
<div class="playback-frame" data-index="#{index}" data-transition="#{slide.transition}" data-duration="#{slide.duration}"<?r if index != 0 ?> hidden<?r end ?>>
|
|
34
|
+
<div class="playback-frame slide-viewport" data-index="#{index}" data-transition="#{slide.transition}" data-duration="#{slide.duration}"<?r if index != 0 ?> hidden<?r end ?>>
|
|
34
35
|
#{self.render_slide(slide)}
|
|
35
36
|
</div>
|
|
36
37
|
<?r end ?>
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
require "console"
|
|
7
|
+
|
|
6
8
|
require_relative "clock"
|
|
7
9
|
require_relative "presentation"
|
|
8
10
|
require_relative "state"
|
|
@@ -21,9 +23,9 @@ module Presently
|
|
|
21
23
|
@current_index = 0
|
|
22
24
|
@clock = Clock.new
|
|
23
25
|
@listeners = []
|
|
24
|
-
@state = state
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
state&.restore(self)
|
|
28
|
+
@state = state
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# @attribute [Presentation] The underlying presentation data.
|
|
@@ -110,7 +110,7 @@ module Presently
|
|
|
110
110
|
else "on-time"
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
builder.tag(:div, class: "timing-info #{pacing_class}") do
|
|
113
|
+
builder.tag(:div, class: "toolbar timing-info #{pacing_class}") do
|
|
114
114
|
builder.tag(:button,
|
|
115
115
|
class: "pause-button",
|
|
116
116
|
onClick: forward_event(action: "pause")
|
|
@@ -151,7 +151,7 @@ module Presently
|
|
|
151
151
|
|
|
152
152
|
if slide
|
|
153
153
|
builder.tag(:span, class: "slide-duration") do
|
|
154
|
-
builder.text("
|
|
154
|
+
builder.text("Duration: #{format_duration(slide.duration)}")
|
|
155
155
|
end
|
|
156
156
|
end
|
|
157
157
|
|
|
@@ -191,21 +191,26 @@ module Presently
|
|
|
191
191
|
|
|
192
192
|
builder.tag(:div, class: "presenter") do
|
|
193
193
|
# Controls bar
|
|
194
|
-
builder.tag(:div, class: "controls") do
|
|
194
|
+
builder.tag(:div, class: "toolbar controls") do
|
|
195
195
|
builder.tag(:button,
|
|
196
196
|
onClick: forward_event(action: "previous")
|
|
197
197
|
) do
|
|
198
198
|
builder.text("← Previous")
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
+
builder.tag(:button,
|
|
202
|
+
onClick: forward_event(action: "next")
|
|
203
|
+
) do
|
|
204
|
+
builder.text("Next →")
|
|
205
|
+
end
|
|
206
|
+
|
|
201
207
|
builder.tag(:span, class: "slide-info") do
|
|
202
|
-
builder.
|
|
208
|
+
builder.tag(:span, class: "slide-position") do
|
|
209
|
+
builder.text("Slide #{@controller.current_index + 1} of #{@controller.slide_count}")
|
|
210
|
+
end
|
|
203
211
|
|
|
204
212
|
if slide
|
|
205
|
-
builder.
|
|
206
|
-
builder.tag(:code, class: "slide-path") do
|
|
207
|
-
builder.text(slide.path)
|
|
208
|
-
end
|
|
213
|
+
render_slide_path(builder, slide.path)
|
|
209
214
|
|
|
210
215
|
if editor_url = editor_url_for(slide.source_path)
|
|
211
216
|
builder.tag(:a, href: editor_url, class: "edit-link") do
|
|
@@ -215,12 +220,6 @@ module Presently
|
|
|
215
220
|
end
|
|
216
221
|
end
|
|
217
222
|
|
|
218
|
-
builder.tag(:button,
|
|
219
|
-
onClick: forward_event(action: "next")
|
|
220
|
-
) do
|
|
221
|
-
builder.text("Next →")
|
|
222
|
-
end
|
|
223
|
-
|
|
224
223
|
# Jump-to dropdown for marked slides
|
|
225
224
|
markers = []
|
|
226
225
|
@controller.slides.each_with_index do |s, i|
|
|
@@ -232,7 +231,7 @@ module Presently
|
|
|
232
231
|
unless markers.empty?
|
|
233
232
|
builder.tag(:select,
|
|
234
233
|
class: "jump-to",
|
|
235
|
-
data:
|
|
234
|
+
"data-live-id": @id
|
|
236
235
|
) do
|
|
237
236
|
builder.tag(:option, value: "", disabled: true, selected: true) do
|
|
238
237
|
builder.text("Jump to…")
|
|
@@ -259,7 +258,7 @@ module Presently
|
|
|
259
258
|
# Current slide
|
|
260
259
|
builder.tag(:div, class: "preview current-preview") do
|
|
261
260
|
builder.tag(:h3){builder.text("Current")}
|
|
262
|
-
builder.tag(:div, class: "preview-frame") do
|
|
261
|
+
builder.tag(:div, class: "preview-frame slide-viewport") do
|
|
263
262
|
@preview_renderer.render(builder, slide)
|
|
264
263
|
end
|
|
265
264
|
end
|
|
@@ -267,7 +266,7 @@ module Presently
|
|
|
267
266
|
# Next slide
|
|
268
267
|
builder.tag(:div, class: "preview next-preview") do
|
|
269
268
|
builder.tag(:h3){builder.text("Next")}
|
|
270
|
-
builder.tag(:div, class: "preview-frame") do
|
|
269
|
+
builder.tag(:div, class: "preview-frame slide-viewport") do
|
|
271
270
|
if next_slide
|
|
272
271
|
@preview_renderer.render(builder, next_slide)
|
|
273
272
|
else
|
|
@@ -80,7 +80,7 @@ module Presently
|
|
|
80
80
|
render_navigation(builder, slide)
|
|
81
81
|
|
|
82
82
|
builder.tag(:div, class: "recording-workspace") do
|
|
83
|
-
builder.tag(:div, class: "recording-preview") do
|
|
83
|
+
builder.tag(:div, class: "recording-preview slide-viewport") do
|
|
84
84
|
@slide_renderer.render(builder, slide)
|
|
85
85
|
end
|
|
86
86
|
|
|
@@ -124,20 +124,21 @@ module Presently
|
|
|
124
124
|
# @parameter builder [XRB::Builder] The HTML builder.
|
|
125
125
|
# @parameter slide [Slide] The current slide.
|
|
126
126
|
def render_navigation(builder, slide)
|
|
127
|
-
builder.tag(:div, class: "controls recording-navigation") do
|
|
127
|
+
builder.tag(:div, class: "toolbar controls recording-navigation") do
|
|
128
128
|
builder.tag(:button, onClick: forward_event(action: "previous")){builder.text("← Previous")}
|
|
129
|
+
builder.tag(:button, onClick: forward_event(action: "next")){builder.text("Next →")}
|
|
129
130
|
|
|
130
131
|
builder.tag(:span, class: "slide-info") do
|
|
131
|
-
builder.
|
|
132
|
-
|
|
132
|
+
builder.tag(:span, class: "slide-position") do
|
|
133
|
+
builder.text("Slide #{@controller.current_index + 1} of #{@controller.slide_count}")
|
|
134
|
+
end
|
|
135
|
+
render_slide_path(builder, slide.path)
|
|
133
136
|
|
|
134
137
|
if editor_url = editor_url_for(slide.source_path)
|
|
135
138
|
builder.tag(:a, href: editor_url, class: "edit-link"){builder.text("✎")}
|
|
136
139
|
end
|
|
137
140
|
end
|
|
138
141
|
|
|
139
|
-
builder.tag(:button, onClick: forward_event(action: "next")){builder.text("Next →")}
|
|
140
|
-
|
|
141
142
|
markers = @controller.slides.each_with_index.filter_map do |candidate, index|
|
|
142
143
|
[index, candidate.marker] if candidate.marker
|
|
143
144
|
end
|