Skip to content

Commit 694238a

Browse files
Initial support for magic move transitions.
1 parent f8cdecf commit 694238a

10 files changed

Lines changed: 134 additions & 29 deletions

File tree

lib/presently/display_view.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def render(builder)
5959
slide = @controller.current_slide
6060
return unless slide
6161

62-
builder.tag(:div, class: "display") do
62+
builder.tag(:div, class: "display", data: {transition: slide.transition}) do
6363
builder.tag(:div, class: "slide-container") do
6464
@slide_renderer.render_slide(builder, slide)
6565
end

lib/presently/environment/application.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def slides_root
2525
# Defaults to the gem's bundled templates.
2626
# @returns [String] Absolute path to the templates root.
2727
def templates_root
28-
File.expand_path("../../templates", __dir__)
28+
File.expand_path("../../../templates", __dir__)
2929
end
3030

3131
# The application class to use.

lib/presently/slide.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def title
5353
@frontmatter&.fetch("title", File.basename(@path, ".md")) || File.basename(@path, ".md")
5454
end
5555

56+
# The transition type for animating into this slide.
57+
# @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"magic-move"`), or `nil` for instant swap.
58+
def transition
59+
@frontmatter&.fetch("transition", nil)
60+
end
61+
5662
# The line range to focus on for code slides.
5763
# @returns [Array(Integer, Integer) | Nil] The `[start, end]` line numbers (1-based), or `nil`.
5864
def focus
@@ -130,7 +136,7 @@ def parse_sections(text)
130136
# @returns [String] The rendered HTML.
131137
def render_markdown(text)
132138
return "" if text.nil? || text.empty?
133-
Markly.render_html(text)
139+
Markly.render_html(text, flags: Markly::UNSAFE)
134140
end
135141
end
136142
end

public/_static/index.css

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -259,10 +259,6 @@ pre > syntax-code {
259259
position: relative;
260260
}
261261

262-
.display .slide {
263-
animation: slide-in 0.4s ease-out;
264-
}
265-
266262
.display .slide-counter {
267263
position: absolute;
268264
bottom: 1rem;
@@ -271,16 +267,88 @@ pre > syntax-code {
271267
opacity: 0.4;
272268
}
273269

274-
/* Slide transition animations */
275-
@keyframes slide-in {
276-
from {
277-
opacity: 0;
278-
transform: translateX(30px);
279-
}
280-
to {
281-
opacity: 1;
282-
transform: translateX(0);
283-
}
270+
/* ========================
271+
VIEW TRANSITIONS
272+
======================== */
273+
274+
/* Give the live-view element the transition name — there's only ever one */
275+
live-view:has(.display) {
276+
view-transition-name: slide-container;
277+
}
278+
279+
/* Default: no animation for the container (instant swap) */
280+
::view-transition-old(slide-container),
281+
::view-transition-new(slide-container) {
282+
animation: none;
283+
}
284+
285+
/* Fade */
286+
html[data-transition="fade"]::view-transition-old(slide-container) {
287+
animation: vt-fade-out 0.4s ease-in-out;
288+
}
289+
290+
html[data-transition="fade"]::view-transition-new(slide-container) {
291+
animation: vt-fade-in 0.4s ease-in-out;
292+
}
293+
294+
/* Slide left */
295+
html[data-transition="slide-left"]::view-transition-old(slide-container) {
296+
animation: vt-slide-out-left 0.4s ease-in-out;
297+
}
298+
299+
html[data-transition="slide-left"]::view-transition-new(slide-container) {
300+
animation: vt-slide-in-right 0.4s ease-in-out;
301+
}
302+
303+
/* Slide right */
304+
html[data-transition="slide-right"]::view-transition-old(slide-container) {
305+
animation: vt-slide-out-right 0.4s ease-in-out;
306+
}
307+
308+
html[data-transition="slide-right"]::view-transition-new(slide-container) {
309+
animation: vt-slide-in-left 0.4s ease-in-out;
310+
}
311+
312+
/* Magic move — the browser interpolates position/size for matched
313+
view-transition-name elements. No cross-fade on the container
314+
to avoid background dimming. */
315+
html[data-transition="magic-move"]::view-transition-old(slide-container) {
316+
animation: none;
317+
opacity: 0;
318+
}
319+
320+
html[data-transition="magic-move"]::view-transition-new(slide-container) {
321+
animation: none;
322+
}
323+
324+
@keyframes vt-fade-out {
325+
from { opacity: 1; }
326+
to { opacity: 0; }
327+
}
328+
329+
@keyframes vt-fade-in {
330+
from { opacity: 0; }
331+
to { opacity: 1; }
332+
}
333+
334+
@keyframes vt-slide-out-left {
335+
from { transform: translateX(0); opacity: 1; }
336+
to { transform: translateX(-100%); opacity: 0; }
337+
}
338+
339+
@keyframes vt-slide-in-right {
340+
from { transform: translateX(100%); opacity: 0; }
341+
to { transform: translateX(0); opacity: 1; }
342+
}
343+
344+
@keyframes vt-slide-out-right {
345+
from { transform: translateX(0); opacity: 1; }
346+
to { transform: translateX(100%); opacity: 0; }
347+
}
348+
349+
@keyframes vt-slide-in-left {
350+
from { transform: translateX(-100%); opacity: 0; }
351+
to { transform: translateX(0); opacity: 1; }
284352
}
285353

286354
/* ========================

public/application.js

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ function applyCodeFocus() {
1313
const focusEnd = parseInt(viewport.dataset.focusEnd);
1414

1515
if (!focusStart || !focusEnd) {
16-
// No focus specified - reset
1716
const scroll = viewport.querySelector('.code-scroll');
1817
const dimTop = viewport.querySelector('.code-dim-top');
1918
const dimBottom = viewport.querySelector('.code-dim-bottom');
@@ -28,33 +27,27 @@ function applyCodeFocus() {
2827
const dimBottom = viewport.querySelector('.code-dim-bottom');
2928
if (!scroll) return;
3029

31-
// Wait for content to render, then calculate positions
3230
requestAnimationFrame(() => {
33-
// Find the pre element and measure line height
3431
const pre = scroll.querySelector('pre');
3532
if (!pre) return;
3633

37-
// Get computed line height from the code content
3834
const code = pre.querySelector('code, syntax-code') || pre;
3935
const style = getComputedStyle(code);
4036
const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.6;
4137

4238
const padding = parseFloat(getComputedStyle(scroll).paddingTop) || 16;
4339
const viewportHeight = viewport.clientHeight;
4440

45-
// Calculate pixel positions for focus region
4641
const focusTopPx = padding + (focusStart - 1) * lineHeight;
4742
const focusBottomPx = padding + focusEnd * lineHeight;
4843
const focusHeight = focusBottomPx - focusTopPx;
4944

50-
// Center the focus region in the viewport
5145
const targetCenter = focusTopPx + focusHeight / 2;
5246
const viewportCenter = viewportHeight / 2;
5347
const translateY = Math.min(0, viewportCenter - targetCenter);
5448

5549
scroll.style.transform = `translateY(${translateY}px)`;
5650

57-
// Position the dim overlays
5851
const dimTopHeight = Math.max(0, focusTopPx + translateY);
5952
const dimBottomHeight = Math.max(0, viewportHeight - (focusBottomPx + translateY));
6053

@@ -64,8 +57,44 @@ function applyCodeFocus() {
6457
});
6558
}
6659

67-
// Re-highlight and apply focus after Live DOM updates:
60+
// Detect the transition type from the incoming HTML before morphdom applies it.
61+
function detectTransition(html) {
62+
const match = html.match(/data-transition="([^"]+)"/);
63+
return match ? match[1] : null;
64+
}
65+
66+
// Track the active view transition so we can skip overlapping ones.
67+
let activeTransition = null;
68+
69+
// Wrap Live's update method to support view transitions.
70+
const originalUpdate = live.update.bind(live);
71+
live.update = function(id, html, options) {
72+
// Only apply transitions on the display view, not the presenter:
73+
const transition = document.querySelector('.display') ? detectTransition(html) : null;
74+
75+
if (transition && document.startViewTransition && !activeTransition) {
76+
document.documentElement.dataset.transition = transition;
77+
78+
activeTransition = document.startViewTransition(() => {
79+
originalUpdate(id, html, options);
80+
});
81+
82+
activeTransition.finished.finally(() => {
83+
delete document.documentElement.dataset.transition;
84+
activeTransition = null;
85+
Syntax.highlight();
86+
applyCodeFocus();
87+
});
88+
} else {
89+
originalUpdate(id, html, options);
90+
Syntax.highlight();
91+
applyCodeFocus();
92+
}
93+
};
94+
95+
// Re-highlight and apply focus after non-update DOM mutations (e.g. replace):
6896
const observer = new MutationObserver(() => {
97+
if (activeTransition) return;
6998
Syntax.highlight();
7099
applyCodeFocus();
71100
});

slides/01-welcome.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ duration: 30
55

66
# Title
77

8-
Welcome to Presently
8+
<div style="view-transition-name: welcome-title">Welcome to Presently</div>
99

1010
# Subtitle
1111

slides/02-translation.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
---
22
template: translation
33
duration: 30
4+
transition: magic-move
45
---
56

67
# Title
78

8-
The best way to predict the future is to create it.
9+
<div style="view-transition-name: welcome-title">The best way to predict the future is to create it.</div>
910

1011
# Translation
1112

slides/04-code-example.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
template: code
33
duration: 60
4-
focus: 2-8
4+
focus: 2-10
55
title: Initialization
66
---
77

slides/05-code-navigation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
template: code
33
duration: 60
4-
focus: 21-35
4+
focus: 20-36
55
title: Navigation
66
---
77

slides/07-section.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
template: section
33
duration: 15
4+
transition: slide-left
45
---
56

67
# Heading

0 commit comments

Comments
 (0)