Skip to main content

explainer/
explainer.rs

1use motion_canvas_rs::prelude::*;
2use std::time::Duration;
3
4// ── Design Tokens ──────────────────────────────────────────────────────────
5const FONT: &str = "JetBrains Mono";
6const BG: Color = Color::rgb8(0x0e, 0x0e, 0x12);
7const WHITE: Color = Color::rgb8(0xf0, 0xf0, 0xf0);
8const DIM: Color = Color::rgb8(0x55, 0x55, 0x66);
9const ACCENT: Color = Color::rgb8(0x68, 0xab, 0xdf);
10const RED: Color = Color::rgb8(0xe1, 0x32, 0x38);
11const YELLOW: Color = Color::rgb8(0xe6, 0xa7, 0x00);
12const GREEN: Color = Color::rgb8(0x25, 0xc2, 0x81);
13const TEAL: Color = Color::rgb8(0x20, 0xb2, 0xaa);
14
15const CANVAS_W: u32 = 1280;
16const CANVAS_H: u32 = 720;
17const LEFT: f32 = 40.0;
18
19fn ms(n: u64) -> Duration {
20    Duration::from_millis(n)
21}
22fn secs(n: u64) -> Duration {
23    Duration::from_secs(n)
24}
25
26// ── Text Helpers ───────────────────────────────────────────────────────────
27fn title(text: &str, y: f32) -> TextNode {
28    TextNode::default()
29        .with_anchor(Vec2::new(-1.0, -1.0))
30        .with_position(Vec2::new(LEFT, y))
31        .with_text(text)
32        .with_font_size(36.0)
33        .with_fill(ACCENT)
34        .with_font(FONT)
35        .with_opacity(0.0)
36}
37fn h2(text: &str, y: f32) -> TextNode {
38    TextNode::default()
39        .with_anchor(Vec2::new(-1.0, -1.0))
40        .with_position(Vec2::new(LEFT, y))
41        .with_text(text)
42        .with_font_size(22.0)
43        .with_fill(WHITE)
44        .with_font(FONT)
45        .with_opacity(0.0)
46}
47fn body(text: &str, y: f32) -> TextNode {
48    TextNode::default()
49        .with_anchor(Vec2::new(-1.0, -1.0))
50        .with_position(Vec2::new(LEFT, y))
51        .with_text(text)
52        .with_font_size(17.0)
53        .with_fill(Color::rgb8(0xcc, 0xcc, 0xdd))
54        .with_font(FONT)
55        .with_opacity(0.0)
56}
57fn dim(text: &str, x: f32, y: f32) -> TextNode {
58    TextNode::default()
59        .with_anchor(Vec2::new(-1.0, -1.0))
60        .with_position(Vec2::new(x, y))
61        .with_text(text)
62        .with_font_size(13.0)
63        .with_fill(DIM)
64        .with_font(FONT)
65        .with_opacity(0.0)
66}
67fn note(text: &str, y: f32) -> TextNode {
68    TextNode::default()
69        .with_anchor(Vec2::new(-1.0, -1.0))
70        .with_position(Vec2::new(LEFT + 20.0, y))
71        .with_text(text)
72        .with_font_size(14.0)
73        .with_fill(YELLOW)
74        .with_font(FONT)
75        .with_opacity(0.0)
76}
77fn code_block(code: &str, y: f32) -> CodeNode {
78    CodeNode::default()
79        .with_position(Vec2::new(LEFT + 20.0, y))
80        .with_code(code)
81        .with_language("rust")
82        .with_font(FONT)
83        .with_font_size(14.0)
84        .with_opacity(0.0)
85}
86fn hline(y: f32) -> Line {
87    Line::default()
88        .with_start(Vec2::new(LEFT, y))
89        .with_end(Vec2::new(LEFT, y))
90        .with_stroke(Color::rgba8(255, 255, 255, 25), 1.0)
91}
92// Shorthand for show/hide
93fn show(opacity: &Signal<f32>, d: Duration) -> Box<dyn Animation> {
94    opacity.to(1.0, d).ease(easings::cubic_out).into()
95}
96fn hide(opacity: &Signal<f32>, d: Duration) -> Box<dyn Animation> {
97    opacity.to(0.0, d).ease(easings::cubic_in).into()
98}
99
100fn main() {
101    let mut project = Project::default()
102        .with_dimensions(CANVAS_W, CANVAS_H)
103        .with_fps(60)
104        .with_title("Explainer")
105        .with_background(BG)
106        .close_on_finish();
107
108    // =====================================================================
109    //  S1: TITLE CARD
110    // =====================================================================
111    let s1_line = hline(100.0);
112    let s1_title = TextNode::default()
113        .with_anchor(Vec2::new(-1.0, -1.0))
114        .with_position(Vec2::new(LEFT, 120.0))
115        .with_text("motion-canvas-rs")
116        .with_font_size(52.0)
117        .with_fill(ACCENT)
118        .with_font(FONT)
119        .with_opacity(0.0);
120    let s1_sub = h2("A GPU-Accelerated Vector Animation Engine", 185.0);
121    let s1_built = body(
122        "Built on Vello + Typst  —  Inspired by Motion Canvas",
123        220.0,
124    );
125    let s1_desc = body(
126        "This animation will teach you how the library works,",
127        280.0,
128    );
129    let s1_desc2 = body(
130        "from struct definitions to the GPU rendering pipeline.",
131        305.0,
132    );
133
134    let s1_logo = SvgNode::default()
135        .with_position(Vec2::new(1142.0, 543.0))
136        .with_path("examples/images/motion-canvas-rs.svg")
137        .with_scale(0.3)
138        .with_size(Vec2::new(768.0, 768.0))
139        .with_opacity(0.0);
140
141    for n in [&s1_title, &s1_sub, &s1_built, &s1_desc, &s1_desc2] {
142        project.scene.add(n);
143    }
144    project.scene.add(&s1_line);
145    project.scene.add(&s1_logo);
146
147    // =====================================================================
148    //  S2: THE 5 STEPS
149    // =====================================================================
150    let s2_h = title("Every program follows 5 steps", 50.0);
151    let steps = [
152        "1. Create a Project       — your canvas settings",
153        "2. Create Nodes           — shapes, text, images",
154        "3. Add Nodes to Scene     — what gets drawn",
155        "4. Animate the Timeline   — how things move",
156        "5. Show or Export         — live window or video",
157    ];
158    let s2_texts: Vec<TextNode> = steps
159        .iter()
160        .enumerate()
161        .map(|(i, s)| body(s, 120.0 + i as f32 * 35.0))
162        .collect();
163
164    project.scene.add(&s2_h);
165    for t in &s2_texts {
166        project.scene.add(t);
167    }
168
169    // =====================================================================
170    //  S3: WHAT IS A STRUCT?  +  The Project struct
171    // =====================================================================
172    let s3_h = title("What is a 'struct'?", 50.0);
173    let s3_explain = h2(
174        "A struct is a container that groups related data together.",
175        95.0,
176    );
177    let s3_analogy = body(
178        "Think of it like a class in Python/JS, but it only holds data.",
179        130.0,
180    );
181
182    let s3_code = code_block(
183        "pub struct Project {
184    pub width: u32,          // Canvas width in pixels
185    pub height: u32,         // Canvas height in pixels
186    pub fps: u32,            // Frames per second
187    pub title: String,       // Window title
188    pub scene: BaseScene,    // Holds nodes + timelines
189    pub background_color: Color,
190    pub close_on_finish: bool,
191}",
192        175.0,
193    );
194
195    let s3_note = note(
196        "^ This is the actual Project struct from the library.",
197        420.0,
198    );
199    let s3_note2 = body(
200        "'pub' means public — anyone can read/write these fields.",
201        455.0,
202    );
203    let s3_note3 = body(
204        "u32 = unsigned 32-bit integer,  String = text,  bool = true/false",
205        485.0,
206    );
207
208    for n in [
209        &s3_h,
210        &s3_explain,
211        &s3_analogy,
212        &s3_note,
213        &s3_note2,
214        &s3_note3,
215    ] {
216        project.scene.add(n);
217    }
218    project.scene.add(&s3_code);
219
220    // =====================================================================
221    //  S4: WHAT IS impl?  +  Builder pattern
222    // =====================================================================
223    let s4_h = title("What is 'impl'?", 50.0);
224    let s4_explain = h2("impl adds methods (functions) to a struct.", 95.0);
225    let s4_analogy = body(
226        "Like adding methods to a class. Separated from the data.",
227        130.0,
228    );
229
230    let s4_code = code_block(
231        "impl Project {
232    pub fn with_fps(mut self, fps: u32) -> Self {
233        self.fps = fps;   // set the value
234        self              // return yourself (builder pattern)
235    }
236    pub fn with_title(mut self, title: &str) -> Self {
237        self.title = title.to_string();
238        self
239    }
240}",
241        175.0,
242    );
243
244    let s4_usage = body("Usage — chain calls to configure:", 430.0);
245    let s4_usage_code = code_block(
246        "let project = Project::default()
247    .with_fps(60)
248    .with_title(\"My Animation\")
249    .with_dimensions(800, 600)
250    .close_on_finish();",
251        460.0,
252    );
253
254    let s4_note = note(
255        "Each .with_*() returns 'self', so you can chain them.",
256        590.0,
257    );
258
259    for n in [&s4_h, &s4_explain, &s4_analogy, &s4_usage, &s4_note] {
260        project.scene.add(n);
261    }
262    project.scene.add(&s4_code);
263    project.scene.add(&s4_usage_code);
264
265    // =====================================================================
266    //  S5: WHAT IS A TRAIT?  +  The Node trait
267    // =====================================================================
268    let s5_h = title("What is a 'trait'?", 50.0);
269    let s5_explain = h2(
270        "A trait is a contract — like an interface in Java/TypeScript.",
271        95.0,
272    );
273    let s5_analogy = body(
274        "Any type that implements a trait promises to provide those methods.",
275        130.0,
276    );
277
278    let s5_code = code_block(
279        "pub trait Node: Send + Sync + 'static {
280    fn render(&self, scene: &mut Scene,
281              parent_transform: Affine,
282              parent_opacity: f32);
283    fn update(&mut self, dt: Duration);
284    fn state_hash(&self) -> u64;
285    fn clone_node(&self) -> Box<dyn Node>;
286}",
287        175.0,
288    );
289
290    let s5_r = note(
291        "render()     — draw yourself using current signal values",
292        410.0,
293    );
294    let s5_u = note(
295        "update(dt)   — called every frame (for per-frame logic)",
296        435.0,
297    );
298    let s5_s = note(
299        "state_hash() — returns a number that changes when you change",
300        460.0,
301    );
302    let s5_c = note("clone_node() — make a deep copy of yourself", 485.0);
303    let s5_every = body(
304        "Circle, Rect, Line, TextNode, Polygon all implement Node.",
305        530.0,
306    );
307
308    for n in [
309        &s5_h,
310        &s5_explain,
311        &s5_analogy,
312        &s5_r,
313        &s5_u,
314        &s5_s,
315        &s5_c,
316        &s5_every,
317    ] {
318        project.scene.add(n);
319    }
320    project.scene.add(&s5_code);
321
322    // =====================================================================
323    //  S6: NODE GALLERY  (visual demo)
324    // =====================================================================
325    let s6_h = title("The Built-in Nodes", 50.0);
326    let s6_sub = body(
327        "Each node uses the builder pattern and stores properties as Signals.",
328        90.0,
329    );
330
331    let demo_c = Circle::default()
332        .with_position(Vec2::new(120.0, 230.0))
333        .with_radius(40.0)
334        .with_fill(RED)
335        .with_opacity(0.0);
336    let demo_r = Rect::default()
337        .with_position(Vec2::new(293.0, 230.0))
338        .with_size(Vec2::new(80.0, 80.0))
339        .with_fill(ACCENT)
340        .with_radius(8.0)
341        .with_opacity(0.0);
342    let demo_l = Line::default()
343        .with_start(Vec2::new(420.0, 200.0))
344        .with_end(Vec2::new(510.0, 270.0))
345        .with_stroke(WHITE, 3.0)
346        .with_opacity(0.0);
347    let demo_p = Polygon::regular(5, 40.0)
348        .with_position(Vec2::new(606.0, 230.0))
349        .with_fill(YELLOW)
350        .with_opacity(0.0);
351    let demo_t = TextNode::default()
352        .with_position(Vec2::new(765.0, 230.0))
353        .with_text("Abc")
354        .with_font_size(36.0)
355        .with_fill(GREEN)
356        .with_font(FONT)
357        .with_opacity(0.0);
358
359    let lc = dim("Circle", 95.0, 285.0);
360    let lr = dim("Rect", 280.0, 285.0);
361    let ll = dim("Line", 445.0, 285.0);
362    let lp = dim("Polygon", 580.0, 285.0);
363    let lt = dim("TextNode", 735.0, 285.0);
364
365    let s6_box_h = h2("Why Box<dyn Node>?", 340.0);
366    let s6_box1 = body("The scene stores different node types in one list:", 375.0);
367    let s6_box_code = code_block(
368        "pub struct BaseScene {
369    pub nodes: Vec<Box<dyn Node>>,  // a list of \"any Node\"
370}
371// 'Box' = heap-allocated,  'dyn Node' = any type implementing Node
372// Like List<INode> in Java or Array<Node> in TypeScript
373project.scene.add(circle);  // wrap + add",
374        405.0,
375    );
376
377    for n in [&s6_h, &s6_sub, &lc, &lr, &ll, &lp, &lt, &s6_box_h, &s6_box1] {
378        project.scene.add(n);
379    }
380    project.scene.add(&demo_c);
381    project.scene.add(&demo_r);
382    project.scene.add(&demo_l);
383    project.scene.add(&demo_p);
384    project.scene.add(&demo_t);
385    project.scene.add(&s6_box_code);
386
387    // =====================================================================
388    //  S7: SIGNALS — The Reactive Core
389    // =====================================================================
390    let s7_h = title("Signals — The Reactive Core", 50.0);
391    let s7_sub = h2("Every animatable property is a Signal<T>.", 95.0);
392
393    let s7_code = code_block(
394        "pub struct Signal<T> {
395    pub data: Arc<Mutex<SignalData<T>>>,
396}
397pub struct SignalData<T> {
398    pub value: T,  // the actual value (f32, Vec2, Color...)
399}",
400        140.0,
401    );
402
403    let s7_arc = note("Arc = shared pointer. Multiple owners, same data.", 310.0);
404    let s7_mutex = note(
405        "Mutex = lock. Only one thread reads/writes at a time.",
406        335.0,
407    );
408    let s7_why = body(
409        "Why? A node and its animation both need the same property:",
410        380.0,
411    );
412
413    let s7_diagram_code = code_block(
414        "let circle = Circle::default().with_radius(50.0);
415// circle.radius is a Signal<f32>
416
417circle.radius.to(100.0, Duration::from_secs(1));
418// Creates a SignalTween with a CLONE of circle.radius
419// Both point to the SAME underlying value (via Arc)
420
421// The animation WRITES new values each frame
422// The node READS them when rendering",
423        420.0,
424    );
425
426    // Live demo circle
427    let sig_demo = Circle::default()
428        .with_position(Vec2::new(1000.0, 400.0))
429        .with_radius(50.0)
430        .with_fill(RED)
431        .with_stroke(Color::rgba8(255, 255, 255, 50), 2.0)
432        .with_opacity(0.0);
433    let sig_lbl = dim("Live Signal demo", 900.0, 150.0);
434
435    for n in [&s7_h, &s7_sub, &s7_arc, &s7_mutex, &s7_why, &sig_lbl] {
436        project.scene.add(n);
437    }
438    project.scene.add(&s7_code);
439    project.scene.add(&s7_diagram_code);
440    project.scene.add(&sig_demo);
441
442    // =====================================================================
443    //  S8: SIGNAL TWEEN — How animations work per-frame
444    // =====================================================================
445    let s8_h = title("SignalTween — The Animation Engine", 50.0);
446    let s8_sub = body(
447        ".to() creates a SignalTween that interpolates over time:",
448        90.0,
449    );
450
451    let s8_code = code_block(
452        "pub struct SignalTween<T> {
453    data: Arc<Mutex<SignalData<T>>>,  // shared ref to signal
454    start_value: Option<T>,           // captured on FIRST update (lazy!)
455    target_value: Option<T>,          // where we're going
456    duration: Duration,               // how long
457    elapsed: Duration,                // how much time passed
458    easing: fn(f32) -> f32,           // curve function
459}",
460        125.0,
461    );
462
463    let s8_how = h2("Each frame update:", 340.0);
464    let s8_steps = [
465        "1. elapsed += dt",
466        "2. t_linear = elapsed / duration          (0.0 to 1.0)",
467        "3. t_eased  = easing(t_linear)            (curved)",
468        "4. value    = lerp(start, target, t)      (interpolate)",
469        "5. Write value into Signal                (node sees it)",
470        "6. If elapsed >= duration: finished!      (return leftover dt)",
471    ];
472    let s8_step_texts: Vec<TextNode> = s8_steps
473        .iter()
474        .enumerate()
475        .map(|(i, s)| body(s, 370.0 + i as f32 * 28.0))
476        .collect();
477
478    let s8_lazy = note(
479        "start_value is captured lazily — so chained tweens read the",
480        570.0,
481    );
482    let s8_lazy2 = note(
483        "correct value at their actual start time, not creation time.",
484        590.0,
485    );
486
487    // Progress bar
488    let prog_bg = Rect::default()
489        .with_position(Vec2::new(895.0, 150.0))
490        .with_size(Vec2::new(400.0, 16.0))
491        .with_fill(Color::rgba8(255, 255, 255, 15))
492        .with_radius(8.0)
493        .with_opacity(0.0);
494    let prog_fill = Rect::default()
495        .with_position(Vec2::new(695.0, 154.0))
496        .with_anchor(Vec2::new(-1.0, 0.5))
497        .with_size(Vec2::new(0.0, 16.0))
498        .with_fill(ACCENT)
499        .with_radius(8.0)
500        .with_opacity(0.0);
501    let plbl0 = dim("t=0", 700.0, 172.0);
502    let plbl1 = dim("t=1", 1070.0, 172.0);
503    let tween_ball = Circle::default()
504        .with_position(Vec2::new(900.0, 430.0))
505        .with_radius(30.0)
506        .with_fill(RED)
507        .with_opacity(0.0);
508    let tween_lbl = dim("radius animating: 30 -> 80", 760.0, 220.0);
509
510    for n in [
511        &s8_h, &s8_sub, &s8_how, &s8_lazy, &s8_lazy2, &plbl0, &plbl1, &tween_lbl,
512    ] {
513        project.scene.add(n);
514    }
515    project.scene.add(&s8_code);
516    for t in &s8_step_texts {
517        project.scene.add(t);
518    }
519    project.scene.add(&prog_bg);
520    project.scene.add(&prog_fill);
521    project.scene.add(&tween_ball);
522
523    // =====================================================================
524    //  S9: TWEENABLE + EASINGS
525    // =====================================================================
526    let s9_h = title("Tweenable — What Can Be Animated", 50.0);
527    let s9_code = code_block(
528        "pub trait Tweenable: Clone + Send + Sync {
529    fn interpolate(a: &Self, b: &Self, t: f32) -> Self;
530    fn state_hash(&self) -> u64;
531}
532// Implemented for: f32, Vec2, Color, String, Affine, Vec<Vec2>
533//   f32:    lerp(a, b, t) = a + (b-a)*t
534//   Vec2:   lerp x and y independently
535//   Color:  lerp R,G,B,A channels independently
536//   String: snap — returns 'a' until t>=1, then 'b'",
537        95.0,
538    );
539
540    let s9_easing_h = h2("Easing functions curve the linear t:", 310.0);
541    let s9_easing_desc = body("Same distance, same duration — different feel.", 340.0);
542
543    let enames = [
544        "linear",
545        "cubic_in_out",
546        "elastic_out",
547        "bounce_out",
548        "back_out",
549    ];
550    let ecolors = [WHITE, ACCENT, RED, YELLOW, GREEN];
551    let mut eballs: Vec<Circle> = Vec::new();
552    let mut elabels: Vec<TextNode> = Vec::new();
553    for (i, name) in enames.iter().enumerate() {
554        let y = 390.0 + i as f32 * 55.0;
555        let b = Circle::default()
556            .with_position(Vec2::new(250.0, y))
557            .with_radius(12.0)
558            .with_fill(ecolors[i])
559            .with_opacity(0.0);
560        let l = dim(name, LEFT, y - 5.0);
561        project.scene.add(&b);
562        project.scene.add(&l);
563        eballs.push(b);
564        elabels.push(l);
565    }
566    project.scene.add(&s9_h);
567    project.scene.add(&s9_code);
568    project.scene.add(&s9_easing_h);
569    project.scene.add(&s9_easing_desc);
570
571    // =====================================================================
572    //  S10: FLOW CONTROLS
573    // =====================================================================
574    let s10_h = title("Flow Controls — Composing Animations", 50.0);
575    let s10_sub = body(
576        "Individual tweens are simple. Power comes from composing them.",
577        90.0,
578    );
579
580    // chain
581    let s10_chain_h = h2("chain![ ] — one after another", 140.0);
582    let chain_d: Vec<Circle> = (0..3)
583        .map(|i| {
584            Circle::default()
585                .with_position(Vec2::new(LEFT + 30.0 + i as f32 * 60.0, 200.0))
586                .with_radius(18.0)
587                .with_fill([RED, ACCENT, YELLOW][i])
588                .with_opacity(0.0)
589        })
590        .collect();
591
592    // all
593    let s10_all_h = h2("all![ ] — all at the same time", 270.0);
594    let all_d: Vec<Circle> = (0..3)
595        .map(|i| {
596            Circle::default()
597                .with_position(Vec2::new(LEFT + 30.0 + i as f32 * 60.0, 330.0))
598                .with_radius(18.0)
599                .with_fill([RED, ACCENT, YELLOW][i])
600                .with_opacity(0.0)
601        })
602        .collect();
603
604    // sequence
605    let s10_seq_h = h2("sequence![ ] — staggered starts", 400.0);
606    let seq_d: Vec<Circle> = (0..3)
607        .map(|i| {
608            Circle::default()
609                .with_position(Vec2::new(LEFT + 30.0 + i as f32 * 60.0, 460.0))
610                .with_radius(18.0)
611                .with_fill([RED, ACCENT, YELLOW][i])
612                .with_opacity(0.0)
613        })
614        .collect();
615
616    let s10_code = code_block(
617        "chain![ a, b, c ]           // a then b then c
618all![ a, b, c ]             // a + b + c together
619sequence![ 200ms, a, b, c ] // staggered
620delay![ 500ms, a ]          // wait then play
621wait(1s)                    // pause
622any![ a, b ]                // race: first wins
623loop_anim![ a, 3 ]          // repeat 3 times",
624        510.0,
625    );
626
627    project.scene.add(&s10_h);
628    project.scene.add(&s10_sub);
629    project.scene.add(&s10_chain_h);
630    project.scene.add(&s10_all_h);
631    project.scene.add(&s10_seq_h);
632    project.scene.add(&s10_code);
633    for d in &chain_d {
634        project.scene.add(d);
635    }
636    for d in &all_d {
637        project.scene.add(d);
638    }
639    for d in &seq_d {
640        project.scene.add(d);
641    }
642
643    // =====================================================================
644    //  S11: TIMELINE + RENDERING
645    // =====================================================================
646    let s11_h = title("The Timeline — Animation Queue", 50.0);
647    let s11_code = code_block(
648        "pub struct Timeline {
649    pub animations: Vec<Box<dyn Animation>>,
650}
651impl Timeline {
652    fn update(&mut self, mut dt: Duration) {
653        while !self.animations.is_empty() {
654            let (finished, leftover) = self.animations[0].update(dt);
655            if finished {
656                self.animations.remove(0); // pop front
657                dt = leftover;             // pass leftover to next!
658            } else { break; }
659        }
660    }
661}",
662        95.0,
663    );
664
665    let s11_leftover = note(
666        "leftover propagation: if A finishes mid-frame, the remaining",
667        390.0,
668    );
669    let s11_leftover2 = note(
670        "dt is immediately given to B. No 'lost frames' at transitions.",
671        415.0,
672    );
673
674    let s11_render_h = h2("Rendering Pipeline (per frame):", 470.0);
675    let s11_steps = [
676        "1. Timeline.update(dt)  =>  SignalTween writes to Signals",
677        "2. Node.render()        =>  reads signals, draws shapes",
678        "3. Vello GPU            =>  compiles scene => wgpu => pixels",
679        "4. state_hash()         =>  seahash for hashing scene state, skip if unchanged",
680    ];
681    let s11_render_texts: Vec<TextNode> = s11_steps
682        .iter()
683        .enumerate()
684        .map(|(i, s)| body(s, 505.0 + i as f32 * 28.0))
685        .collect();
686
687    project.scene.add(&s11_h);
688    project.scene.add(&s11_code);
689    for n in [&s11_leftover, &s11_leftover2, &s11_render_h] {
690        project.scene.add(n);
691    }
692    for t in &s11_render_texts {
693        project.scene.add(t);
694    }
695
696    // =====================================================================
697    //  S12: EVENT LOOP — Why an infinite loop?
698    // =====================================================================
699    let s12_h = title("Why an Infinite Loop? — The Event Loop", 50.0);
700    let s12_sub = body(
701        "GPU rendering requires a persistent event loop (winit + wgpu).",
702        90.0,
703    );
704    let s12_code = code_block(
705        "event_loop.run(|event, elwt| {
706    match event {
707        Resumed => {           // GPU surface ready
708            renderer.resume(&window);
709        }
710        AboutToWait => {       // run every frame
711            scene.update(dt);  // advance animations
712            let hash = scene.state_hash();
713            if hash != last_hash {    // dirty?
714                window.request_redraw();
715            }
716        }
717        RedrawRequested => {   // GPU draw call
718            renderer.render(&scene, w, h);
719        }
720    }
721});",
722        130.0,
723    );
724    let s12_why = note(
725        "The window stays open because the GPU surface is tied to",
726        490.0,
727    );
728    let s12_why2 = note(
729        "the OS event loop. Without it, the surface is immediately dropped.",
730        510.0,
731    );
732    let s12_hash = body(
733        "state_hash() skips re-rendering unchanged frames (dirty-checking).",
734        550.0,
735    );
736
737    for n in [&s12_h, &s12_sub, &s12_why, &s12_why2, &s12_hash] {
738        project.scene.add(n);
739    }
740    project.scene.add(&s12_code);
741
742    // =====================================================================
743    //  S13: HEADLESS EXPORT — GPU without a window
744    // =====================================================================
745    let s13_h = title("Headless Export: GPU -> PNG -> FFmpeg", 50.0);
746    let s13_sub = body(
747        "Same GPU rendering, but without a window — output to files.",
748        90.0,
749    );
750    let s13_code = code_block(
751        "pub struct Exporter {
752    texture: wgpu::Texture,       // GPU-side image
753    output_buffer: wgpu::Buffer,  // CPU-readable copy
754    renderer: Renderer,           // Vello
755}
756fn export_frame(&mut self, scene) -> Vec<u8> {
757    scene.render(&mut self.scene);           // 1. build shapes
758    renderer.render_to_texture(..);          // 2. GPU draws
759    encoder.copy_texture_to_buffer(..);      // 3. GPU -> CPU
760    output_buffer.map_async(Read, ..);       // 4. read pixels
761    return pixels;                           // 5. raw RGBA
762}",
763        130.0,
764    );
765    let s13_cache = note(
766        "Cache: state_hash per frame. If unchanged, skip GPU entirely.",
767        420.0,
768    );
769    let s13_ffmpeg = note(
770        "FFmpeg: raw pixels piped to stdin -> libx264 -> .mkv video.",
771        445.0,
772    );
773    let s13_parallel = body(
774        "PNG saving runs on a background thread. Export is pipelined.",
775        485.0,
776    );
777
778    for n in [&s13_h, &s13_sub, &s13_cache, &s13_ffmpeg, &s13_parallel] {
779        project.scene.add(n);
780    }
781    project.scene.add(&s13_code);
782
783    // =====================================================================
784    //  S14: ENGINE UTILITIES
785    // =====================================================================
786    let s14_h = title("Under the Hood: Utility Modules", 50.0);
787    let s14_sub = body(
788        "Helper systems that power the engine behind the scenes.",
789        90.0,
790    );
791    let s14_code = code_block(
792        "// src/engine/util/
793font_manager.rs    // Lazy-loads system fonts via Typst
794                   // Global HashMap cache with lazy_static
795
796image_manager.rs   // Loads PNG + SVG (via resvg)
797                   // Caches decoded images as Arc<Image>
798
799code_tokenizer.rs  // Syntax highlighting via Syntect
800                   // Parses code -> colored spans for CodeNode
801
802hash.rs            // SeaHash: fast, deterministic fingerprints
803                   // Position-aware combination
804                   // Powers Rayon parallel state hashing
805
806export.rs          // FFmpeg pipe: rawvideo -> libx264
807                   // Audio merging with filter_complex
808                   // Title sanitization for filenames",
809        130.0,
810    );
811    let s14_lazy = note(
812        "lazy_static + Mutex = global singleton, created once, cached forever.",
813        495.0,
814    );
815    let s14_arc = body(
816        "Arc<Image> lets multiple nodes share one decoded image without copies.",
817        530.0,
818    );
819    let s14_hash = note(
820        "Rayon + SeaHash = Deterministic fingerprints across runs & threads.",
821        565.0,
822    );
823
824    for n in [&s14_h, &s14_sub, &s14_lazy, &s14_arc, &s14_hash] {
825        project.scene.add(n);
826    }
827    project.scene.add(&s14_code);
828
829    // =====================================================================
830    //  S15: FINALE
831    // =====================================================================
832    let fin = TextNode::default()
833        .with_anchor(Vec2::new(-1.0, -1.0))
834        .with_position(Vec2::new(LEFT, 200.0))
835        .with_text("That's how it works!")
836        .with_font_size(48.0)
837        .with_fill(ACCENT)
838        .with_font(FONT)
839        .with_opacity(0.0);
840    let fin_steps = [
841        "1.  struct          — data container",
842        "2.  impl            — methods / builder pattern",
843        "3.  trait Node      — interface contract",
844        "4.  Box<dyn Node>   — type-erased heap allocation",
845        "5.  Signal<T>       — Arc<Mutex> shared reactive state",
846        "6.  SignalTween     — per-frame lerp interpolation",
847        "7.  Timeline        — sequential queue + leftover dt",
848        "8.  Event Loop      — winit + wgpu infinite loop",
849        "9.  Exporter        — headless GPU -> PNG/FFmpeg",
850        "10. Utilities       — font/image cache, syntax highlight",
851    ];
852    let fin_texts: Vec<TextNode> = fin_steps
853        .iter()
854        .enumerate()
855        .map(|(i, s)| body(s, 270.0 + i as f32 * 28.0))
856        .collect();
857    let fin_hint = dim("cargo run --example getting_started", LEFT, 570.0);
858
859    project.scene.add(&fin);
860    for t in &fin_texts {
861        project.scene.add(t);
862    }
863    project.scene.add(&fin_hint);
864
865    // =====================================================================
866    //  ANIMATION TIMELINE
867    // =====================================================================
868    // Helper: hide_all takes a vec of opacity signals and fades them out
869    let hide_dur = ms(200);
870
871    project.scene.video_timeline.add(chain![
872        // ── S1: TITLE ──
873        s1_line
874            .end
875            .to(Vec2::new(500.0, 100.0), ms(500))
876            .ease(easings::cubic_out),
877        sequence![
878            ms(120),
879            show(&s1_title.opacity, ms(500)),
880            show(&s1_sub.opacity, ms(500)),
881            show(&s1_built.opacity, ms(500)),
882            show(&s1_logo.opacity, ms(600)),
883            show(&s1_desc.opacity, ms(500)),
884            show(&s1_desc2.opacity, ms(500)),
885        ],
886        wait(secs(5)),
887        all![
888            hide(&s1_title.opacity, hide_dur),
889            hide(&s1_sub.opacity, hide_dur),
890            hide(&s1_built.opacity, hide_dur),
891            hide(&s1_desc.opacity, hide_dur),
892            hide(&s1_desc2.opacity, hide_dur),
893            hide(&s1_logo.opacity, hide_dur),
894            s1_line.end.to(Vec2::new(LEFT, 100.0), hide_dur)
895        ],
896        wait(ms(150)),
897        // ── S2: FIVE STEPS ──
898        show(&s2_h.opacity, ms(500)),
899        wait(ms(400)),
900        sequence![
901            ms(250),
902            show(&s2_texts[0].opacity, ms(400)),
903            show(&s2_texts[1].opacity, ms(400)),
904            show(&s2_texts[2].opacity, ms(400)),
905            show(&s2_texts[3].opacity, ms(400)),
906            show(&s2_texts[4].opacity, ms(400)),
907        ],
908        wait(secs(8)),
909        all![
910            hide(&s2_h.opacity, hide_dur),
911            hide(&s2_texts[0].opacity, hide_dur),
912            hide(&s2_texts[1].opacity, hide_dur),
913            hide(&s2_texts[2].opacity, hide_dur),
914            hide(&s2_texts[3].opacity, hide_dur),
915            hide(&s2_texts[4].opacity, hide_dur)
916        ],
917        wait(ms(150)),
918        // ── S3: STRUCT ──
919        sequence![
920            ms(120),
921            show(&s3_h.opacity, ms(500)),
922            show(&s3_explain.opacity, ms(400)),
923            show(&s3_analogy.opacity, ms(400))
924        ],
925        wait(ms(500)),
926        show(&s3_code.opacity, ms(500)),
927        wait(secs(6)),
928        sequence![
929            ms(300),
930            show(&s3_note.opacity, ms(400)),
931            show(&s3_note2.opacity, ms(400)),
932            show(&s3_note3.opacity, ms(400))
933        ],
934        wait(secs(6)),
935        all![
936            hide(&s3_h.opacity, hide_dur),
937            hide(&s3_explain.opacity, hide_dur),
938            hide(&s3_analogy.opacity, hide_dur),
939            hide(&s3_code.opacity, hide_dur),
940            hide(&s3_note.opacity, hide_dur),
941            hide(&s3_note2.opacity, hide_dur),
942            hide(&s3_note3.opacity, hide_dur)
943        ],
944        wait(ms(150)),
945        // ── S4: IMPL / BUILDER ──
946        sequence![
947            ms(120),
948            show(&s4_h.opacity, ms(500)),
949            show(&s4_explain.opacity, ms(400)),
950            show(&s4_analogy.opacity, ms(400))
951        ],
952        wait(ms(500)),
953        show(&s4_code.opacity, ms(500)),
954        wait(secs(7)),
955        show(&s4_usage.opacity, ms(400)),
956        show(&s4_usage_code.opacity, ms(500)),
957        wait(secs(2)),
958        show(&s4_note.opacity, ms(400)),
959        wait(secs(5)),
960        all![
961            hide(&s4_h.opacity, hide_dur),
962            hide(&s4_explain.opacity, hide_dur),
963            hide(&s4_analogy.opacity, hide_dur),
964            hide(&s4_code.opacity, hide_dur),
965            hide(&s4_usage.opacity, hide_dur),
966            hide(&s4_usage_code.opacity, hide_dur),
967            hide(&s4_note.opacity, hide_dur)
968        ],
969        wait(ms(150)),
970        // ── S5: TRAIT / NODE ──
971        sequence![
972            ms(120),
973            show(&s5_h.opacity, ms(500)),
974            show(&s5_explain.opacity, ms(400)),
975            show(&s5_analogy.opacity, ms(400))
976        ],
977        wait(ms(500)),
978        show(&s5_code.opacity, ms(500)),
979        wait(secs(6)),
980        sequence![
981            ms(300),
982            show(&s5_r.opacity, ms(400)),
983            show(&s5_u.opacity, ms(400)),
984            show(&s5_s.opacity, ms(400)),
985            show(&s5_c.opacity, ms(400))
986        ],
987        wait(secs(2)),
988        show(&s5_every.opacity, ms(400)),
989        wait(secs(4)),
990        all![
991            hide(&s5_h.opacity, hide_dur),
992            hide(&s5_explain.opacity, hide_dur),
993            hide(&s5_analogy.opacity, hide_dur),
994            hide(&s5_code.opacity, hide_dur),
995            hide(&s5_r.opacity, hide_dur),
996            hide(&s5_u.opacity, hide_dur),
997            hide(&s5_s.opacity, hide_dur),
998            hide(&s5_c.opacity, hide_dur),
999            hide(&s5_every.opacity, hide_dur)
1000        ],
1001        wait(ms(150)),
1002        // ── S6: NODE GALLERY ──
1003        sequence![
1004            ms(120),
1005            show(&s6_h.opacity, ms(500)),
1006            show(&s6_sub.opacity, ms(400))
1007        ],
1008        wait(ms(400)),
1009        sequence![
1010            ms(200),
1011            all![show(&demo_c.opacity, ms(400)), show(&lc.opacity, ms(400))],
1012            all![show(&demo_r.opacity, ms(400)), show(&lr.opacity, ms(400))],
1013            all![show(&demo_l.opacity, ms(400)), show(&ll.opacity, ms(400))],
1014            all![show(&demo_p.opacity, ms(400)), show(&lp.opacity, ms(400))],
1015            all![show(&demo_t.opacity, ms(400)), show(&lt.opacity, ms(400))],
1016        ],
1017        wait(secs(2)),
1018        sequence![
1019            ms(200),
1020            show(&s6_box_h.opacity, ms(400)),
1021            show(&s6_box1.opacity, ms(400)),
1022            show(&s6_box_code.opacity, ms(500))
1023        ],
1024        wait(secs(7)),
1025        all![
1026            hide(&s6_h.opacity, hide_dur),
1027            hide(&s6_sub.opacity, hide_dur),
1028            hide(&demo_c.opacity, hide_dur),
1029            hide(&demo_r.opacity, hide_dur),
1030            hide(&demo_l.opacity, hide_dur),
1031            hide(&demo_p.opacity, hide_dur),
1032            hide(&demo_t.opacity, hide_dur),
1033            hide(&lc.opacity, hide_dur),
1034            hide(&lr.opacity, hide_dur),
1035            hide(&ll.opacity, hide_dur),
1036            hide(&lp.opacity, hide_dur),
1037            hide(&lt.opacity, hide_dur),
1038            hide(&s6_box_h.opacity, hide_dur),
1039            hide(&s6_box1.opacity, hide_dur),
1040            hide(&s6_box_code.opacity, hide_dur)
1041        ],
1042        wait(ms(150)),
1043        // ── S7: SIGNALS ──
1044        sequence![
1045            ms(120),
1046            show(&s7_h.opacity, ms(500)),
1047            show(&s7_sub.opacity, ms(400))
1048        ],
1049        wait(ms(400)),
1050        show(&s7_code.opacity, ms(500)),
1051        wait(secs(5)),
1052        sequence![
1053            ms(300),
1054            show(&s7_arc.opacity, ms(400)),
1055            show(&s7_mutex.opacity, ms(400))
1056        ],
1057        wait(secs(4)),
1058        show(&s7_why.opacity, ms(400)),
1059        show(&s7_diagram_code.opacity, ms(500)),
1060        wait(secs(6)),
1061        // Live demo
1062        all![
1063            show(&sig_demo.opacity, ms(300)),
1064            show(&sig_lbl.opacity, ms(300))
1065        ],
1066        chain![
1067            sig_demo.radius.to(80.0, ms(700)).ease(easings::elastic_out),
1068            sig_demo.fill_paint.to(Paint::Solid(TEAL), ms(500)),
1069            sig_demo
1070                .position
1071                .to(Vec2::new(950.0, 350.0), ms(500))
1072                .ease(easings::cubic_out),
1073            wait(ms(300)),
1074            all![
1075                sig_demo.radius.to(50.0, ms(400)),
1076                sig_demo.fill_paint.to(Paint::Solid(RED), ms(400)),
1077                sig_demo.position.to(Vec2::new(900.0, 300.0), ms(400))
1078            ],
1079        ],
1080        wait(secs(3)),
1081        all![
1082            hide(&s7_h.opacity, hide_dur),
1083            hide(&s7_sub.opacity, hide_dur),
1084            hide(&s7_code.opacity, hide_dur),
1085            hide(&s7_arc.opacity, hide_dur),
1086            hide(&s7_mutex.opacity, hide_dur),
1087            hide(&s7_why.opacity, hide_dur),
1088            hide(&s7_diagram_code.opacity, hide_dur),
1089            hide(&sig_demo.opacity, hide_dur),
1090            hide(&sig_lbl.opacity, hide_dur)
1091        ],
1092        wait(ms(150)),
1093        // ── S8: SIGNAL TWEEN ──
1094        sequence![
1095            ms(120),
1096            show(&s8_h.opacity, ms(500)),
1097            show(&s8_sub.opacity, ms(400))
1098        ],
1099        wait(ms(400)),
1100        show(&s8_code.opacity, ms(500)),
1101        wait(secs(6)),
1102        show(&s8_how.opacity, ms(300)),
1103        sequence![
1104            ms(100),
1105            show(&s8_step_texts[0].opacity, ms(250)),
1106            show(&s8_step_texts[1].opacity, ms(250)),
1107            show(&s8_step_texts[2].opacity, ms(250)),
1108            show(&s8_step_texts[3].opacity, ms(250)),
1109            show(&s8_step_texts[4].opacity, ms(250)),
1110            show(&s8_step_texts[5].opacity, ms(250)),
1111        ],
1112        wait(ms(500)),
1113        sequence![
1114            ms(100),
1115            show(&s8_lazy.opacity, ms(300)),
1116            show(&s8_lazy2.opacity, ms(300))
1117        ],
1118        wait(ms(500)),
1119        // Progress bar demo
1120        all![
1121            show(&prog_bg.opacity, ms(200)),
1122            show(&prog_fill.opacity, ms(200)),
1123            show(&plbl0.opacity, ms(200)),
1124            show(&plbl1.opacity, ms(200)),
1125            show(&tween_ball.opacity, ms(200)),
1126            show(&tween_lbl.opacity, ms(200))
1127        ],
1128        all![
1129            prog_fill
1130                .size
1131                .to(Vec2::new(400.0, 16.0), secs(2))
1132                .ease(easings::cubic_in_out),
1133            tween_ball
1134                .radius
1135                .to(80.0, secs(2))
1136                .ease(easings::cubic_in_out),
1137        ],
1138        wait(secs(3)),
1139        all![
1140            hide(&s8_h.opacity, hide_dur),
1141            hide(&s8_sub.opacity, hide_dur),
1142            hide(&s8_code.opacity, hide_dur),
1143            hide(&s8_how.opacity, hide_dur),
1144            hide(&s8_lazy.opacity, hide_dur),
1145            hide(&s8_lazy2.opacity, hide_dur),
1146            hide(&prog_bg.opacity, hide_dur),
1147            hide(&prog_fill.opacity, hide_dur),
1148            hide(&plbl0.opacity, hide_dur),
1149            hide(&plbl1.opacity, hide_dur),
1150            hide(&tween_ball.opacity, hide_dur),
1151            hide(&tween_lbl.opacity, hide_dur),
1152            hide(&s8_step_texts[0].opacity, hide_dur),
1153            hide(&s8_step_texts[1].opacity, hide_dur),
1154            hide(&s8_step_texts[2].opacity, hide_dur),
1155            hide(&s8_step_texts[3].opacity, hide_dur),
1156            hide(&s8_step_texts[4].opacity, hide_dur),
1157            hide(&s8_step_texts[5].opacity, hide_dur)
1158        ],
1159        wait(ms(150)),
1160        // ── S9: TWEENABLE + EASINGS ──
1161        show(&s9_h.opacity, ms(500)),
1162        show(&s9_code.opacity, ms(500)),
1163        wait(secs(4)),
1164        sequence![
1165            ms(60),
1166            show(&s9_easing_h.opacity, ms(300)),
1167            show(&s9_easing_desc.opacity, ms(300))
1168        ],
1169        sequence![
1170            ms(50),
1171            all![
1172                show(&eballs[0].opacity, ms(200)),
1173                show(&elabels[0].opacity, ms(200))
1174            ],
1175            all![
1176                show(&eballs[1].opacity, ms(200)),
1177                show(&elabels[1].opacity, ms(200))
1178            ],
1179            all![
1180                show(&eballs[2].opacity, ms(200)),
1181                show(&elabels[2].opacity, ms(200))
1182            ],
1183            all![
1184                show(&eballs[3].opacity, ms(200)),
1185                show(&elabels[3].opacity, ms(200))
1186            ],
1187            all![
1188                show(&eballs[4].opacity, ms(200)),
1189                show(&elabels[4].opacity, ms(200))
1190            ],
1191        ],
1192        wait(ms(300)),
1193        // Race!
1194        all![
1195            eballs[0]
1196                .position
1197                .to(Vec2::new(1050.0, 390.0), secs(2))
1198                .ease(easings::linear),
1199            eballs[1]
1200                .position
1201                .to(Vec2::new(1050.0, 445.0), secs(2))
1202                .ease(easings::cubic_in_out),
1203            eballs[2]
1204                .position
1205                .to(Vec2::new(1050.0, 500.0), secs(2))
1206                .ease(easings::elastic_out),
1207            eballs[3]
1208                .position
1209                .to(Vec2::new(1050.0, 555.0), secs(2))
1210                .ease(easings::bounce_out),
1211            eballs[4]
1212                .position
1213                .to(Vec2::new(1050.0, 610.0), secs(2))
1214                .ease(easings::back_out),
1215        ],
1216        wait(ms(500)),
1217        all![
1218            eballs[0]
1219                .position
1220                .to(Vec2::new(250.0, 390.0), secs(2))
1221                .ease(easings::linear),
1222            eballs[1]
1223                .position
1224                .to(Vec2::new(250.0, 445.0), secs(2))
1225                .ease(easings::cubic_in_out),
1226            eballs[2]
1227                .position
1228                .to(Vec2::new(250.0, 500.0), secs(2))
1229                .ease(easings::elastic_out),
1230            eballs[3]
1231                .position
1232                .to(Vec2::new(250.0, 555.0), secs(2))
1233                .ease(easings::bounce_out),
1234            eballs[4]
1235                .position
1236                .to(Vec2::new(250.0, 610.0), secs(2))
1237                .ease(easings::back_out),
1238        ],
1239        wait(ms(500)),
1240        all![
1241            hide(&s9_h.opacity, hide_dur),
1242            hide(&s9_code.opacity, hide_dur),
1243            hide(&s9_easing_h.opacity, hide_dur),
1244            hide(&s9_easing_desc.opacity, hide_dur),
1245            hide(&eballs[0].opacity, hide_dur),
1246            hide(&eballs[1].opacity, hide_dur),
1247            hide(&eballs[2].opacity, hide_dur),
1248            hide(&eballs[3].opacity, hide_dur),
1249            hide(&eballs[4].opacity, hide_dur),
1250            hide(&elabels[0].opacity, hide_dur),
1251            hide(&elabels[1].opacity, hide_dur),
1252            hide(&elabels[2].opacity, hide_dur),
1253            hide(&elabels[3].opacity, hide_dur),
1254            hide(&elabels[4].opacity, hide_dur)
1255        ],
1256        wait(ms(150)),
1257        // ── S10: FLOW CONTROLS ──
1258        sequence![
1259            ms(120),
1260            show(&s10_h.opacity, ms(500)),
1261            show(&s10_sub.opacity, ms(400))
1262        ],
1263        wait(ms(400)),
1264        // chain demo
1265        show(&s10_chain_h.opacity, ms(400)),
1266        all![
1267            show(&chain_d[0].opacity, ms(300)),
1268            show(&chain_d[1].opacity, ms(300)),
1269            show(&chain_d[2].opacity, ms(300))
1270        ],
1271        wait(ms(300)),
1272        chain![
1273            chain_d[0]
1274                .position
1275                .to(Vec2::new(700.0, 200.0), ms(500))
1276                .ease(easings::cubic_out),
1277            chain_d[1]
1278                .position
1279                .to(Vec2::new(800.0, 200.0), ms(500))
1280                .ease(easings::cubic_out),
1281            chain_d[2]
1282                .position
1283                .to(Vec2::new(900.0, 200.0), ms(500))
1284                .ease(easings::cubic_out),
1285        ],
1286        wait(secs(1)),
1287        // all demo
1288        show(&s10_all_h.opacity, ms(400)),
1289        all![
1290            show(&all_d[0].opacity, ms(300)),
1291            show(&all_d[1].opacity, ms(300)),
1292            show(&all_d[2].opacity, ms(300))
1293        ],
1294        wait(ms(300)),
1295        all![
1296            all_d[0]
1297                .position
1298                .to(Vec2::new(700.0, 330.0), ms(500))
1299                .ease(easings::cubic_out),
1300            all_d[1]
1301                .position
1302                .to(Vec2::new(800.0, 330.0), ms(500))
1303                .ease(easings::cubic_out),
1304            all_d[2]
1305                .position
1306                .to(Vec2::new(900.0, 330.0), ms(500))
1307                .ease(easings::cubic_out),
1308        ],
1309        wait(secs(1)),
1310        // sequence demo
1311        show(&s10_seq_h.opacity, ms(400)),
1312        all![
1313            show(&seq_d[0].opacity, ms(300)),
1314            show(&seq_d[1].opacity, ms(300)),
1315            show(&seq_d[2].opacity, ms(300))
1316        ],
1317        wait(ms(300)),
1318        sequence![
1319            ms(250),
1320            seq_d[0]
1321                .position
1322                .to(Vec2::new(700.0, 460.0), ms(500))
1323                .ease(easings::cubic_out),
1324            seq_d[1]
1325                .position
1326                .to(Vec2::new(800.0, 460.0), ms(500))
1327                .ease(easings::cubic_out),
1328            seq_d[2]
1329                .position
1330                .to(Vec2::new(900.0, 460.0), ms(500))
1331                .ease(easings::cubic_out),
1332        ],
1333        wait(secs(1)),
1334        show(&s10_code.opacity, ms(500)),
1335        wait(secs(8)),
1336        all![
1337            hide(&s10_h.opacity, hide_dur),
1338            hide(&s10_sub.opacity, hide_dur),
1339            hide(&s10_chain_h.opacity, hide_dur),
1340            hide(&s10_all_h.opacity, hide_dur),
1341            hide(&s10_seq_h.opacity, hide_dur),
1342            hide(&s10_code.opacity, hide_dur),
1343            hide(&chain_d[0].opacity, hide_dur),
1344            hide(&chain_d[1].opacity, hide_dur),
1345            hide(&chain_d[2].opacity, hide_dur),
1346            hide(&all_d[0].opacity, hide_dur),
1347            hide(&all_d[1].opacity, hide_dur),
1348            hide(&all_d[2].opacity, hide_dur),
1349            hide(&seq_d[0].opacity, hide_dur),
1350            hide(&seq_d[1].opacity, hide_dur),
1351            hide(&seq_d[2].opacity, hide_dur)
1352        ],
1353        wait(ms(150)),
1354        // ── S11: TIMELINE + RENDERING ──
1355        show(&s11_h.opacity, ms(500)),
1356        wait(ms(400)),
1357        show(&s11_code.opacity, ms(500)),
1358        wait(secs(7)),
1359        sequence![
1360            ms(200),
1361            show(&s11_leftover.opacity, ms(400)),
1362            show(&s11_leftover2.opacity, ms(400))
1363        ],
1364        wait(secs(4)),
1365        show(&s11_render_h.opacity, ms(400)),
1366        sequence![
1367            ms(200),
1368            show(&s11_render_texts[0].opacity, ms(350)),
1369            show(&s11_render_texts[1].opacity, ms(350)),
1370            show(&s11_render_texts[2].opacity, ms(350)),
1371            show(&s11_render_texts[3].opacity, ms(350)),
1372        ],
1373        wait(secs(7)),
1374        all![
1375            hide(&s11_h.opacity, hide_dur),
1376            hide(&s11_code.opacity, hide_dur),
1377            hide(&s11_leftover.opacity, hide_dur),
1378            hide(&s11_leftover2.opacity, hide_dur),
1379            hide(&s11_render_h.opacity, hide_dur),
1380            hide(&s11_render_texts[0].opacity, hide_dur),
1381            hide(&s11_render_texts[1].opacity, hide_dur),
1382            hide(&s11_render_texts[2].opacity, hide_dur),
1383            hide(&s11_render_texts[3].opacity, hide_dur)
1384        ],
1385        wait(ms(300)),
1386        // ── S12: EVENT LOOP ──
1387        sequence![
1388            ms(120),
1389            show(&s12_h.opacity, ms(500)),
1390            show(&s12_sub.opacity, ms(400))
1391        ],
1392        wait(ms(500)),
1393        show(&s12_code.opacity, ms(500)),
1394        wait(secs(8)),
1395        sequence![
1396            ms(200),
1397            show(&s12_why.opacity, ms(400)),
1398            show(&s12_why2.opacity, ms(400)),
1399            show(&s12_hash.opacity, ms(400))
1400        ],
1401        wait(secs(5)),
1402        all![
1403            hide(&s12_h.opacity, hide_dur),
1404            hide(&s12_sub.opacity, hide_dur),
1405            hide(&s12_code.opacity, hide_dur),
1406            hide(&s12_why.opacity, hide_dur),
1407            hide(&s12_why2.opacity, hide_dur),
1408            hide(&s12_hash.opacity, hide_dur)
1409        ],
1410        wait(ms(150)),
1411        // ── S13: HEADLESS EXPORT ──
1412        sequence![
1413            ms(120),
1414            show(&s13_h.opacity, ms(500)),
1415            show(&s13_sub.opacity, ms(400))
1416        ],
1417        wait(ms(500)),
1418        show(&s13_code.opacity, ms(500)),
1419        wait(secs(8)),
1420        sequence![
1421            ms(200),
1422            show(&s13_cache.opacity, ms(400)),
1423            show(&s13_ffmpeg.opacity, ms(400)),
1424            show(&s13_parallel.opacity, ms(400))
1425        ],
1426        wait(secs(5)),
1427        all![
1428            hide(&s13_h.opacity, hide_dur),
1429            hide(&s13_sub.opacity, hide_dur),
1430            hide(&s13_code.opacity, hide_dur),
1431            hide(&s13_cache.opacity, hide_dur),
1432            hide(&s13_ffmpeg.opacity, hide_dur),
1433            hide(&s13_parallel.opacity, hide_dur)
1434        ],
1435        wait(ms(150)),
1436        // ── S14: UTILITIES ──
1437        sequence![
1438            ms(120),
1439            show(&s14_h.opacity, ms(500)),
1440            show(&s14_sub.opacity, ms(400))
1441        ],
1442        wait(ms(500)),
1443        show(&s14_code.opacity, ms(500)),
1444        wait(secs(8)),
1445        sequence![
1446            ms(200),
1447            show(&s14_lazy.opacity, ms(400)),
1448            show(&s14_arc.opacity, ms(400))
1449        ],
1450        wait(secs(5)),
1451        all![
1452            hide(&s14_h.opacity, hide_dur),
1453            hide(&s14_sub.opacity, hide_dur),
1454            hide(&s14_code.opacity, hide_dur),
1455            hide(&s14_lazy.opacity, hide_dur),
1456            hide(&s14_arc.opacity, hide_dur)
1457        ],
1458        wait(ms(300)),
1459        // ── S15: FINALE ──
1460        show(&fin.opacity, ms(700)),
1461        wait(ms(500)),
1462        sequence![
1463            ms(150),
1464            show(&fin_texts[0].opacity, ms(350)),
1465            show(&fin_texts[1].opacity, ms(350)),
1466            show(&fin_texts[2].opacity, ms(350)),
1467            show(&fin_texts[3].opacity, ms(350)),
1468            show(&fin_texts[4].opacity, ms(350)),
1469            show(&fin_texts[5].opacity, ms(350)),
1470            show(&fin_texts[6].opacity, ms(350)),
1471            show(&fin_texts[7].opacity, ms(350)),
1472            show(&fin_texts[8].opacity, ms(350)),
1473            show(&fin_texts[9].opacity, ms(350)),
1474        ],
1475        wait(secs(2)),
1476        show(&fin_hint.opacity, ms(400)),
1477        wait(secs(6)),
1478    ]);
1479
1480    #[cfg(feature = "audio")]
1481    project
1482        .scene
1483        .audio_timeline
1484        .add(play!(AudioNode::new("background.mp3").with_volume(0.3)));
1485
1486    project.show().expect("Failed to render");
1487}