Skip to main content

physics_demo/
physics_demo.rs

1use motion_canvas_rs::prelude::*;
2use std::time::Duration;
3
4// ─── Palette ──────────────────────────────────────────────
5const BG: Color = Color::rgb8(0x0f, 0x0f, 0x14);
6const FLOOR_COLOR: Color = Color::rgb8(0x2a, 0x2d, 0x3a);
7const ACCENT_RED: Color = Color::rgb8(0xe1, 0x32, 0x38);
8const ACCENT_BLUE: Color = Color::rgb8(0x68, 0xab, 0xdf);
9const ACCENT_YELLOW: Color = Color::rgb8(0xe6, 0xa7, 0x00);
10const ACCENT_TEAL: Color = Color::rgb8(0x20, 0xb2, 0xaa);
11const ACCENT_PURPLE: Color = Color::rgb8(0x9b, 0x59, 0xb6);
12const ACCENT_EMERALD: Color = Color::rgb8(0x25, 0xc2, 0x81);
13const TEXT_DIM: Color = Color::rgb8(0x88, 0x88, 0x99);
14const TEXT_BRIGHT: Color = Color::rgb8(0xe8, 0xe8, 0xf0);
15const WALL_COLOR: Color = Color::rgb8(0x3a, 0x3d, 0x4a);
16
17const W: u32 = 960;
18const H: u32 = 540;
19const CX: f32 = W as f32 / 2.0;
20const FADE: Duration = Duration::from_millis(300);
21
22// ─── Helpers ──────────────────────────────────────────────
23
24fn make_floor(y: f32, width: f32, color: Color) -> StaticBodyNode {
25    StaticBodyNode::new(Box::new(
26        Rect::default()
27            .with_size(Vec2::new(width, 30.0))
28            .with_fill(color)
29            .with_radius(4.0),
30    ))
31    .with_position(Vec2::new(CX, y))
32    .with_shape(PhysicsShape::Cuboid(Vec2::new(width / 2.0, 15.0)))
33}
34
35fn make_wall(x: f32, y: f32, w: f32, h: f32) -> StaticBodyNode {
36    StaticBodyNode::new(Box::new(
37        Rect::default()
38            .with_size(Vec2::new(w, h))
39            .with_fill(WALL_COLOR)
40            .with_radius(2.0),
41    ))
42    .with_position(Vec2::new(x, y))
43    .with_shape(PhysicsShape::Cuboid(Vec2::new(w / 2.0, h / 2.0)))
44}
45
46fn make_title(text: &str) -> TextNode {
47    TextNode::default()
48        .with_text(text)
49        .with_position(Vec2::new(CX, 40.0))
50        .with_anchor(Vec2::ZERO)
51        .with_font_size(32.0)
52        .with_fill(TEXT_BRIGHT)
53        .with_font("JetBrains Mono")
54        .with_opacity(0.0)
55}
56
57fn make_subtitle(text: &str) -> TextNode {
58    TextNode::default()
59        .with_text(text)
60        .with_position(Vec2::new(CX, 75.0))
61        .with_anchor(Vec2::ZERO)
62        .with_font_size(16.0)
63        .with_fill(TEXT_DIM)
64        .with_font("JetBrains Mono")
65        .with_opacity(0.0)
66}
67
68fn make_label(text: &str, x: f32, y: f32, color: Color) -> TextNode {
69    TextNode::default()
70        .with_text(text)
71        .with_position(Vec2::new(x, y))
72        .with_anchor(Vec2::ZERO)
73        .with_font_size(14.0)
74        .with_fill(color)
75        .with_font("JetBrains Mono")
76        .with_opacity(0.0)
77}
78
79// ═══════════════════════════════════════════════════════════
80// SCENE BUILDERS
81// ═══════════════════════════════════════════════════════════
82
83fn build_scene1() -> (PhysicsNode, TextNode, TextNode) {
84    let title = make_title("Gravity");
85    let subtitle = make_subtitle("A single box falls under gravity");
86
87    let mut p = PhysicsNode::new()
88        .with_timestep(1.0 / 60.0)
89        .with_opacity(0.0);
90
91    p.add_static(make_floor(460.0, 600.0, FLOOR_COLOR));
92    p.add_dynamic(
93        RigidBodyNode::new(Box::new(
94            Rect::default()
95                .with_size(Vec2::new(60.0, 60.0))
96                .with_fill(ACCENT_RED)
97                .with_radius(6.0),
98        ))
99        .with_position(Vec2::new(CX, 120.0))
100        .with_shape(PhysicsShape::Cuboid(Vec2::new(30.0, 30.0)))
101        .with_bounciness(0.3),
102    );
103
104    (p, title, subtitle)
105}
106
107fn build_scene2() -> (PhysicsNode, TextNode, TextNode, Vec<TextNode>) {
108    let title = make_title("Bounciness");
109    let subtitle = make_subtitle("Same ball, three values: 0.0 · 0.5 · 0.95");
110
111    let mut p = PhysicsNode::new().with_opacity(0.0);
112
113    p.add_static(make_floor(460.0, 800.0, FLOOR_COLOR));
114
115    let configs: [(f32, f32, Color, &str); 3] = [
116        (CX - 200.0, 0.0, ACCENT_RED, "0.0 (dead)"),
117        (CX, 0.5, ACCENT_YELLOW, "0.5 (rubber)"),
118        (CX + 200.0, 0.95, ACCENT_EMERALD, "0.95 (super ball)"),
119    ];
120
121    let mut labels = Vec::new();
122    for (x, bounce, color, label_text) in &configs {
123        p.add_dynamic(
124            RigidBodyNode::new(Box::new(
125                Circle::default().with_radius(25.0).with_fill(*color),
126            ))
127            .with_position(Vec2::new(*x, 100.0))
128            .with_shape(PhysicsShape::Ball(25.0))
129            .with_bounciness(*bounce),
130        );
131        labels.push(make_label(label_text, *x, 500.0, *color));
132    }
133
134    (p, title, subtitle, labels)
135}
136
137fn build_scene3() -> (PhysicsNode, TextNode, TextNode, Vec<TextNode>) {
138    let title = make_title("Shapes");
139    let subtitle = make_subtitle("Circles roll, rectangles tumble — shape matters");
140
141    let mut p = PhysicsNode::new().with_opacity(0.0);
142
143    p.add_static(make_floor(460.0, 800.0, FLOOR_COLOR));
144
145    p.add_static(
146        StaticBodyNode::new(Box::new(
147            Rect::default()
148                .with_size(Vec2::new(300.0, 16.0))
149                .with_fill(WALL_COLOR)
150                .with_radius(2.0),
151        ))
152        .with_position(Vec2::new(300.0, 320.0))
153        .with_rotation(0.25)
154        .with_shape(PhysicsShape::Cuboid(Vec2::new(150.0, 8.0))),
155    );
156    p.add_static(
157        StaticBodyNode::new(Box::new(
158            Rect::default()
159                .with_size(Vec2::new(300.0, 16.0))
160                .with_fill(WALL_COLOR)
161                .with_radius(2.0),
162        ))
163        .with_position(Vec2::new(660.0, 320.0))
164        .with_rotation(0.25)
165        .with_shape(PhysicsShape::Cuboid(Vec2::new(150.0, 8.0))),
166    );
167
168    p.add_dynamic(
169        RigidBodyNode::new(Box::new(
170            Circle::default().with_radius(25.0).with_fill(ACCENT_BLUE),
171        ))
172        .with_position(Vec2::new(220.0, 150.0))
173        .with_shape(PhysicsShape::Ball(25.0))
174        .with_bounciness(0.2),
175    );
176
177    p.add_dynamic(
178        RigidBodyNode::new(Box::new(
179            Rect::default()
180                .with_size(Vec2::new(50.0, 50.0))
181                .with_fill(ACCENT_PURPLE)
182                .with_radius(4.0),
183        ))
184        .with_position(Vec2::new(580.0, 150.0))
185        .with_shape(PhysicsShape::Cuboid(Vec2::new(25.0, 25.0)))
186        .with_bounciness(0.2),
187    );
188
189    let labels = vec![
190        make_label("Circle (rolls)", 250.0, 500.0, ACCENT_BLUE),
191        make_label("Rect (tumbles)", 610.0, 500.0, ACCENT_PURPLE),
192    ];
193
194    (p, title, subtitle, labels)
195}
196
197fn build_scene4() -> (PhysicsNode, TextNode, TextNode) {
198    let title = make_title("Stacking");
199    let subtitle = make_subtitle("Bodies interact — stacking, settling, and toppling");
200
201    let mut p = PhysicsNode::new().with_opacity(0.0);
202
203    p.add_static(make_floor(460.0, 500.0, FLOOR_COLOR));
204
205    let colors = [
206        ACCENT_RED,
207        ACCENT_BLUE,
208        ACCENT_YELLOW,
209        ACCENT_TEAL,
210        ACCENT_PURPLE,
211        ACCENT_EMERALD,
212    ];
213    let sizes = [
214        Vec2::new(80.0, 30.0),
215        Vec2::new(60.0, 40.0),
216        Vec2::new(70.0, 35.0),
217        Vec2::new(50.0, 50.0),
218        Vec2::new(65.0, 25.0),
219        Vec2::new(45.0, 45.0),
220    ];
221
222    for (i, (size, color)) in sizes.iter().zip(colors.iter()).enumerate() {
223        let x_offset = if i % 2 == 0 { 3.0 } else { -5.0 };
224        p.add_dynamic(
225            RigidBodyNode::new(Box::new(
226                Rect::default()
227                    .with_size(*size)
228                    .with_fill(*color)
229                    .with_radius(4.0),
230            ))
231            .with_position(Vec2::new(CX + x_offset, 100.0 + i as f32 * 55.0))
232            .with_shape(PhysicsShape::Cuboid(*size * 0.5))
233            .with_bounciness(0.05),
234        );
235    }
236
237    (p, title, subtitle)
238}
239
240fn build_scene5() -> (
241    PhysicsNode,
242    TextNode,
243    TextNode,
244    Signal<f32>,
245    Vec<Box<dyn Node>>,
246) {
247    let title = make_title("Kinematic Containers");
248    let subtitle = make_subtitle("Have infinite mass, but can be moved and used for obstacles");
249
250    let mut p = PhysicsNode::new().with_opacity(0.0);
251
252    // 1. Declare independent clock signal variable parameter
253    let time_var = Signal::new(0.0f32);
254
255    // 2. Instantiate uielding layout container walls inside Kinematic mode
256    let floor = RigidBodyNode::new(Box::new(
257        Rect::default()
258            .with_size(Vec2::new(400.0, 30.0))
259            .with_fill(FLOOR_COLOR)
260            .with_radius(4.0),
261    ))
262    .with_position(Vec2::new(CX, 450.0))
263    .with_shape(PhysicsShape::Cuboid(Vec2::new(200.0, 15.0)))
264    .with_mode(PhysicsMode::Kinematic);
265
266    let left_wall = RigidBodyNode::new(Box::new(
267        Rect::default()
268            .with_size(Vec2::new(20.0, 220.0))
269            .with_fill(WALL_COLOR)
270            .with_radius(2.0),
271    ))
272    .with_position(Vec2::new(CX - 200.0, 350.0))
273    .with_shape(PhysicsShape::Cuboid(Vec2::new(10.0, 110.0)))
274    .with_mode(PhysicsMode::Kinematic);
275
276    let right_wall = RigidBodyNode::new(Box::new(
277        Rect::default()
278            .with_size(Vec2::new(20.0, 220.0))
279            .with_fill(WALL_COLOR)
280            .with_radius(2.0),
281    ))
282    .with_position(Vec2::new(CX + 200.0, 350.0))
283    .with_shape(PhysicsShape::Cuboid(Vec2::new(10.0, 110.0)))
284    .with_mode(PhysicsMode::Kinematic);
285
286    // 3. Connect bindings mapping 2x horizontal sin and 3x vertical cos oscillation waves
287    let floor_link = floor.position.bind(time_var.clone(), move |t| {
288        let dx = (t * 4.0).sin() * 70.0; // 2x Horizontal speed wave frequency
289        let dy = (t * 6.0).cos() * 15.0; // 3x Vertical speed wave frequency
290        Vec2::new(CX + dx, 450.0 + dy)
291    });
292
293    let left_link = left_wall.position.bind(time_var.clone(), move |t| {
294        let dx = (t * 4.0).sin() * 70.0;
295        let dy = (t * 6.0).cos() * 15.0;
296        Vec2::new((CX - 200.0) + dx, 350.0 + dy)
297    });
298
299    let right_link = right_wall.position.bind(time_var.clone(), move |t| {
300        let dx = (t * 4.0).sin() * 70.0;
301        let dy = (t * 6.0).cos() * 15.0;
302        Vec2::new((CX + 200.0) + dx, 350.0 + dy)
303    });
304
305    p.add_dynamic(floor);
306    p.add_dynamic(left_wall);
307    p.add_dynamic(right_wall);
308
309    let ball_colors = [
310        ACCENT_RED,
311        ACCENT_BLUE,
312        ACCENT_YELLOW,
313        ACCENT_TEAL,
314        ACCENT_PURPLE,
315        ACCENT_EMERALD,
316    ];
317    let radii = [
318        12.0, 15.0, 10.0, 18.0, 13.0, 11.0, 14.0, 16.0, 9.0, 12.0, 15.0, 10.0,
319    ];
320
321    for (i, radius) in radii.iter().enumerate() {
322        p.add_dynamic(
323            RigidBodyNode::new(Box::new(
324                Circle::default()
325                    .with_radius(*radius)
326                    .with_fill(ball_colors[i % ball_colors.len()]),
327            ))
328            .with_position(Vec2::new(
329                CX - 120.0 + (i as f32 * 25.0),
330                80.0 + (i as f32 * 15.0),
331            ))
332            .with_shape(PhysicsShape::Ball(*radius))
333            .with_bounciness(0.4),
334        );
335    }
336
337    let links: Vec<Box<dyn Node>> = vec![
338        Box::new(floor_link),
339        Box::new(left_link),
340        Box::new(right_link),
341    ];
342    (p, title, subtitle, time_var, links)
343}
344
345fn build_scene6() -> (PhysicsNode, TextNode, TextNode) {
346    let title = make_title("Zero Gravity");
347    let subtitle = make_subtitle("gravity = (0, 0) — objects float, collisions still work");
348
349    let mut p = PhysicsNode::new()
350        .with_gravity(Vec2::new(0.0, 0.0))
351        .with_opacity(0.0);
352
353    p.add_static(make_wall(CX, 100.0, 800.0, 20.0));
354    p.add_static(make_wall(CX, 480.0, 800.0, 20.0));
355    p.add_static(make_wall(80.0, 290.0, 20.0, 400.0));
356    p.add_static(make_wall(880.0, 290.0, 20.0, 400.0));
357
358    let positions = [
359        (250.0, 200.0),
360        (480.0, 180.0),
361        (700.0, 250.0),
362        (350.0, 350.0),
363        (600.0, 380.0),
364        (200.0, 400.0),
365    ];
366    let velocities = [
367        Vec2::new(150.0, 80.0),
368        Vec2::new(-200.0, 100.0),
369        Vec2::new(-100.0, -150.0),
370        Vec2::new(180.0, -120.0),
371        Vec2::new(220.0, 180.0),
372        Vec2::new(-140.0, -200.0),
373    ];
374    let spins = [1.0, -1.5, 2.0, -0.8, 1.2, -2.5];
375    let colors = [
376        ACCENT_RED,
377        ACCENT_BLUE,
378        ACCENT_YELLOW,
379        ACCENT_TEAL,
380        ACCENT_PURPLE,
381        ACCENT_EMERALD,
382    ];
383
384    for (i, (((x, y), vel), spin)) in positions
385        .iter()
386        .zip(velocities.iter())
387        .zip(spins.iter())
388        .enumerate()
389    {
390        let color = colors[i % colors.len()];
391        if i % 2 == 0 {
392            p.add_dynamic(
393                RigidBodyNode::new(Box::new(
394                    Circle::default().with_radius(22.0).with_fill(color),
395                ))
396                .with_position(Vec2::new(*x, *y))
397                .with_shape(PhysicsShape::Ball(22.0))
398                .with_bounciness(0.8)
399                .with_initial_velocity(*vel)
400                .with_initial_angular_velocity(*spin),
401            );
402        } else {
403            p.add_dynamic(
404                RigidBodyNode::new(Box::new(
405                    Rect::default()
406                        .with_size(Vec2::new(40.0, 40.0))
407                        .with_fill(color)
408                        .with_radius(4.0),
409                ))
410                .with_position(Vec2::new(*x, *y))
411                .with_shape(PhysicsShape::Cuboid(Vec2::new(20.0, 20.0)))
412                .with_bounciness(0.8)
413                .with_initial_velocity(*vel)
414                .with_initial_angular_velocity(*spin),
415            );
416        }
417    }
418
419    (p, title, subtitle)
420}
421
422fn build_scene7() -> (
423    PhysicsNode,
424    TextNode,
425    TextNode,
426    Vec<RigidBodyNode>,
427    Vec<TextNode>,
428    Vec<Box<dyn Node>>,
429) {
430    let title = make_title("Friction");
431    let subtitle = make_subtitle("Varying friction: 0.0 (ice) · 0.7 (medium) · 0.9 (rough)");
432
433    let mut p = PhysicsNode::new().with_opacity(0.0);
434
435    p.add_static(make_floor(460.0, 800.0, FLOOR_COLOR));
436
437    p.add_static(
438        StaticBodyNode::new(Box::new(
439            Rect::default()
440                .with_size(Vec2::new(180.0, 16.0))
441                .with_fill(WALL_COLOR)
442                .with_radius(2.0),
443        ))
444        .with_position(Vec2::new(240.0, 280.0))
445        .with_rotation(0.35)
446        .with_shape(PhysicsShape::Cuboid(Vec2::new(90.0, 8.0)))
447        .with_friction(0.0),
448    );
449
450    p.add_static(
451        StaticBodyNode::new(Box::new(
452            Rect::default()
453                .with_size(Vec2::new(180.0, 16.0))
454                .with_fill(WALL_COLOR)
455                .with_radius(2.0),
456        ))
457        .with_position(Vec2::new(480.0, 280.0))
458        .with_rotation(0.35)
459        .with_shape(PhysicsShape::Cuboid(Vec2::new(90.0, 8.0)))
460        .with_friction(0.7),
461    );
462
463    p.add_static(
464        StaticBodyNode::new(Box::new(
465            Rect::default()
466                .with_size(Vec2::new(180.0, 16.0))
467                .with_fill(WALL_COLOR)
468                .with_radius(2.0),
469        ))
470        .with_position(Vec2::new(720.0, 280.0))
471        .with_rotation(0.35)
472        .with_shape(PhysicsShape::Cuboid(Vec2::new(90.0, 8.0)))
473        .with_friction(0.9),
474    );
475
476    // Create cubes with refs before adding to physics
477    let cube_configs: [(f32, Color, f32, &str); 3] = [
478        (190.0, ACCENT_TEAL, 0.0, "Friction: 0.0"),
479        (430.0, ACCENT_YELLOW, 0.2, "Friction: 0.7"),
480        (670.0, ACCENT_RED, 0.9, "Friction: 0.9"),
481    ];
482
483    let mut cube_refs = Vec::new();
484    let mut labels = Vec::new();
485    let mut bindings: Vec<Box<dyn Node>> = Vec::new();
486
487    for (x, color, friction, label_text) in &cube_configs {
488        let cube = RigidBodyNode::new(Box::new(
489            Rect::default()
490                .with_size(Vec2::new(40.0, 40.0))
491                .with_fill(*color)
492                .with_radius(3.0),
493        ))
494        .with_position(Vec2::new(*x, 150.0))
495        .with_shape(PhysicsShape::Cuboid(Vec2::new(20.0, 20.0)))
496        .with_bounciness(0.1)
497        .with_friction(*friction);
498
499        // Floating label that tracks the cube
500        let label = TextNode::default()
501            .with_text(label_text)
502            .with_font_size(14.0)
503            .with_fill(*color)
504            .with_font("JetBrains Mono")
505            .with_anchor(Vec2::ZERO)
506            .with_opacity(0.0);
507
508        // Bind label position to cube position (offset above)
509        let binding = label
510            .position
511            .bind(cube.position.clone(), |pos| Vec2::new(pos.x, pos.y - 35.0));
512
513        cube_refs.push(cube.clone());
514        labels.push(label);
515        bindings.push(Box::new(binding));
516
517        p.add_dynamic(cube);
518    }
519
520    (p, title, subtitle, cube_refs, labels, bindings)
521}
522
523fn build_scene8() -> (
524    PhysicsNode,
525    TextNode,
526    TextNode,
527    RigidBodyNode,
528    Vec<RigidBodyNode>,
529    TextNode,
530) {
531    let title = make_title("Mode Switching");
532    let subtitle =
533        make_subtitle("Disabled → Dynamic → Kinematic — seamless signal ↔ physics transitions");
534
535    let mut p = PhysicsNode::new().with_opacity(0.0);
536
537    p.add_static(make_floor(460.0, 800.0, FLOOR_COLOR));
538    // Side walls for kinematic phase
539    p.add_static(make_wall(80.0, 350.0, 20.0, 300.0));
540    p.add_static(make_wall(880.0, 350.0, 20.0, 300.0));
541
542    // Main text body — starts Disabled, positioned at center
543    let text_body = RigidBodyNode::new(Box::new(
544        TextNode::default()
545            .with_text("motion-canvas-rs")
546            .with_font_size(36.0)
547            .with_fill(ACCENT_BLUE)
548            .with_font("JetBrains Mono")
549            .with_anchor(Vec2::ZERO),
550    ))
551    .with_position(Vec2::new(CX, 270.0))
552    .with_shape(PhysicsShape::Cuboid(Vec2::new(170.0, 20.0)))
553    .with_bounciness(0.3)
554    .with_mode(PhysicsMode::Disabled);
555
556    // Balls — spawn above, appear during Disabled, Kinematic and Dynamic phase
557    let ball_colors = [ACCENT_RED, ACCENT_YELLOW, ACCENT_EMERALD, ACCENT_PURPLE];
558    let mut balls = Vec::new();
559    for i in 0..12 {
560        let color = ball_colors[(i % 4) as usize];
561        let ball = RigidBodyNode::new(Box::new(
562            Circle::default().with_radius(15.0).with_fill(color),
563        ))
564        .with_position(Vec2::new(CX - 90.0 + ((i % 4) as f32 * 60.0), -50.0))
565        .with_shape(PhysicsShape::Ball(15.0))
566        .with_bounciness(0.6)
567        .with_mode(PhysicsMode::Disabled);
568        balls.push(ball);
569    }
570
571    // Status label
572    let status = TextNode::default()
573        .with_text("")
574        .with_position(Vec2::new(CX, 500.0))
575        .with_anchor(Vec2::ZERO)
576        .with_font_size(16.0)
577        .with_fill(TEXT_DIM)
578        .with_font("JetBrains Mono")
579        .with_opacity(0.0);
580
581    let text_ref = text_body.clone();
582    let ball_refs: Vec<RigidBodyNode> = balls.iter().map(|b| b.clone()).collect();
583
584    p.add_dynamic(text_body);
585    for ball in balls {
586        p.add_dynamic(ball);
587    }
588
589    (p, title, subtitle, text_ref, ball_refs, status)
590}
591
592fn build_scene9() -> (PhysicsNode, TextNode, TextNode) {
593    let title = make_title("Custom Colliders");
594    let subtitle = make_subtitle("Custom convex polygons falling through static pegs");
595
596    let mut p = PhysicsNode::new().with_opacity(0.0);
597
598    // Floor
599    p.add_static(make_floor(490.0, 600.0, FLOOR_COLOR));
600
601    // Staggered rows of pegs
602    const NUM_ROWS: usize = 5;
603    let peg_radius = 8.0;
604
605    for r in 0..NUM_ROWS {
606        let y = 180.0 + r as f32 * 60.0;
607        let is_even = r % 2 == 0;
608        let num_pegs = if is_even { 5 } else { 6 };
609
610        for c in 0..num_pegs {
611            let offset_x = if is_even {
612                (c as f32 - 2.0) * 80.0
613            } else {
614                (c as f32 - 2.5) * 80.0
615            };
616            let x = CX + offset_x;
617
618            p.add_static(
619                StaticBodyNode::new(Box::new(
620                    Circle::default()
621                        .with_radius(peg_radius)
622                        .with_fill(WALL_COLOR),
623                ))
624                .with_position(Vec2::new(x, y))
625                .with_shape(PhysicsShape::Ball(peg_radius))
626                .with_bounciness(0.6),
627            );
628        }
629    }
630
631    // Spawn falling diamonds with Custom physics colliders
632    for i in 0..12 {
633        let x_offset = CX - 100.0 + (i as f32 * 18.0);
634        let y_offset = 80.0 - (i as f32 * 65.0);
635
636        let points = vec![
637            Vec2::new(0.0, -22.0),
638            Vec2::new(14.0, 0.0),
639            Vec2::new(0.0, 22.0),
640            Vec2::new(-14.0, 0.0),
641        ];
642
643        let diamond_visual = Polygon::default()
644            .with_points(points)
645            .with_fill(ACCENT_PURPLE);
646
647        let custom_col = PhysicsShape::Custom(std::sync::Arc::new(move || {
648            rapier2d::prelude::ColliderBuilder::convex_polyline(vec![
649                rapier2d::prelude::Point::new(0.0, -22.0),
650                rapier2d::prelude::Point::new(14.0, 0.0),
651                rapier2d::prelude::Point::new(0.0, 22.0),
652                rapier2d::prelude::Point::new(-14.0, 0.0),
653            ])
654            .expect("Invalid custom shape polygon")
655        }));
656
657        p.add_dynamic(
658            RigidBodyNode::new(Box::new(diamond_visual))
659                .with_position(Vec2::new(x_offset, y_offset))
660                .with_shape(custom_col)
661                .with_bounciness(0.5)
662                .with_friction(0.15),
663        );
664    }
665
666    (p, title, subtitle)
667}
668
669fn build_scene10() -> (PhysicsNode, TextNode, TextNode) {
670    let title = make_title("Final Example");
671    let subtitle = make_subtitle("100 shapes, high bounciness, one container");
672
673    let mut p = PhysicsNode::new().with_opacity(0.0);
674
675    p.add_static(make_floor(500.0, 700.0, FLOOR_COLOR));
676    p.add_static(make_wall(CX - 350.0, 350.0, 20.0, 320.0));
677    p.add_static(make_wall(CX + 350.0, 350.0, 20.0, 320.0));
678
679    let all_colors = [
680        ACCENT_RED,
681        ACCENT_BLUE,
682        ACCENT_YELLOW,
683        ACCENT_TEAL,
684        ACCENT_PURPLE,
685        ACCENT_EMERALD,
686    ];
687
688    for i in 0..100 {
689        let x = CX + (if i % 2 == 0 { 8.0 } else { -8.0 });
690        let y = -100.0 - (i as f32 * 140.0);
691        let color = all_colors[i % all_colors.len()];
692
693        if i % 3 == 0 {
694            let r = 24.0 + (i % 4) as f32 * 8.0;
695            p.add_dynamic(
696                RigidBodyNode::new(Box::new(Circle::default().with_radius(r).with_fill(color)))
697                    .with_position(Vec2::new(x, y))
698                    .with_shape(PhysicsShape::Ball(r))
699                    .with_bounciness(0.85),
700            );
701        } else {
702            let s = 45.0 + (i % 3) as f32 * 12.0;
703            p.add_dynamic(
704                RigidBodyNode::new(Box::new(
705                    Rect::default()
706                        .with_size(Vec2::new(s, s))
707                        .with_fill(color)
708                        .with_radius(4.0),
709                ))
710                .with_position(Vec2::new(x, y))
711                .with_shape(PhysicsShape::Cuboid(Vec2::new(s / 2.0, s / 2.0)))
712                .with_bounciness(0.7),
713            );
714        }
715    }
716
717    (p, title, subtitle)
718}
719
720fn main() {
721    let mut project = Project::default()
722        .with_dimensions(W, H)
723        .with_title("Physics Demo")
724        .with_background(BG)
725        .close_on_finish();
726
727    // ── Build all scenes ──
728    let (p1, t1, s1) = build_scene1();
729    let (p2, t2, s2, labels2) = build_scene2();
730    let (p3, t3, s3, labels3) = build_scene3();
731    let (p4, t4, s4) = build_scene4();
732    let (p5, t5, s5, time5_var, links5) = build_scene5();
733    let (p6, t6, s6) = build_scene6();
734    let (p7, t7, s7, _cube_refs7, labels7, bindings7) = build_scene7();
735    let (p8, t8, s8, text8, balls8, status8) = build_scene8();
736    let (p9, t9, s9) = build_scene9();
737    let (p10, t10, s10) = build_scene10();
738
739    // ── Add all to scene (all invisible) ──
740    project.scene.add(&p1);
741    project.scene.add(&t1);
742    project.scene.add(&s1);
743
744    project.scene.add(&p2);
745    project.scene.add(&t2);
746    project.scene.add(&s2);
747    let labels2_c: Vec<_> = labels2
748        .iter()
749        .map(|l| {
750            project.scene.add(l);
751            l.clone()
752        })
753        .collect();
754
755    project.scene.add(&p3);
756    project.scene.add(&t3);
757    project.scene.add(&s3);
758    let labels3_c: Vec<_> = labels3
759        .iter()
760        .map(|l| {
761            project.scene.add(l);
762            l.clone()
763        })
764        .collect();
765
766    project.scene.add(&p4);
767    project.scene.add(&t4);
768    project.scene.add(&s4);
769
770    project.scene.add(&p5);
771    project.scene.add(&t5);
772    project.scene.add(&s5);
773
774    for link in links5 {
775        project.scene.add(link);
776    }
777
778    project.scene.add(&p6);
779    project.scene.add(&t6);
780    project.scene.add(&s6);
781
782    project.scene.add(&p7);
783    project.scene.add(&t7);
784    project.scene.add(&s7);
785    let labels7_c: Vec<_> = labels7
786        .iter()
787        .map(|l| {
788            project.scene.add(l);
789            l.clone()
790        })
791        .collect();
792    for binding in bindings7 {
793        project.scene.add(binding);
794    }
795
796    project.scene.add(&p8);
797    project.scene.add(&t8);
798    project.scene.add(&s8);
799    project.scene.add(&status8);
800
801    project.scene.add(&p9);
802    project.scene.add(&t9);
803    project.scene.add(&s9);
804
805    project.scene.add(&p10);
806    project.scene.add(&t10);
807    project.scene.add(&s10);
808
809    // ── Timeline: one scene at a time ──
810    project.scene.video_timeline.add(chain![
811        // Scene 1: Gravity
812        t1.opacity.to(1.0, FADE),
813        wait!(1),
814        all![p1.opacity.to(1.0, FADE), s1.opacity.to(1.0, FADE),],
815        wait!(4.0),
816        all![
817            p1.opacity.to(0.0, FADE),
818            t1.opacity.to(0.0, FADE),
819            s1.opacity.to(0.0, FADE),
820        ],
821        // Scene 2: Bounciness
822        t2.opacity.to(1.0, FADE),
823        wait!(1),
824        all![
825            p2.opacity.to(1.0, FADE),
826            s2.opacity.to(1.0, FADE),
827            labels2_c[0].opacity.to(1.0, FADE),
828            labels2_c[1].opacity.to(1.0, FADE),
829            labels2_c[2].opacity.to(1.0, FADE),
830        ],
831        wait!(5.0),
832        all![
833            p2.opacity.to(0.0, FADE),
834            s2.opacity.to(0.0, FADE),
835            t2.opacity.to(0.0, FADE),
836            labels2_c[0].opacity.to(0.0, FADE),
837            labels2_c[1].opacity.to(0.0, FADE),
838            labels2_c[2].opacity.to(0.0, FADE),
839        ],
840        // Scene 3: Shapes
841        t3.opacity.to(1.0, FADE),
842        wait!(1),
843        all![
844            p3.opacity.to(1.0, FADE),
845            s3.opacity.to(1.0, FADE),
846            labels3_c[0].opacity.to(1.0, FADE),
847            labels3_c[1].opacity.to(1.0, FADE),
848        ],
849        wait!(5.0),
850        all![
851            p3.opacity.to(0.0, FADE),
852            t3.opacity.to(0.0, FADE),
853            s3.opacity.to(0.0, FADE),
854            labels3_c[0].opacity.to(0.0, FADE),
855            labels3_c[1].opacity.to(0.0, FADE),
856        ],
857        // Scene 4: Stacking
858        t4.opacity.to(1.0, FADE),
859        wait!(1),
860        all![p4.opacity.to(1.0, FADE), s4.opacity.to(1.0, FADE),],
861        wait!(5.0),
862        all![
863            p4.opacity.to(0.0, FADE),
864            t4.opacity.to(0.0, FADE),
865            s4.opacity.to(0.0, FADE),
866        ],
867        // Scene 5: Kinematic Containers
868        t5.opacity.to(1.0, FADE),
869        wait!(1),
870        all![
871            p5.opacity.to(1.0, FADE),
872            s5.opacity.to(1.0, FADE),
873            time5_var.to(4.0, Duration::from_secs(4)),
874        ],
875        wait!(4.0),
876        all![
877            p5.opacity.to(0.0, FADE),
878            t5.opacity.to(0.0, FADE),
879            s5.opacity.to(0.0, FADE),
880        ],
881        // Scene 6: Zero Gravity
882        t6.opacity.to(1.0, FADE),
883        wait!(1),
884        all![p6.opacity.to(1.0, FADE), s6.opacity.to(1.0, FADE),],
885        wait!(5.0),
886        all![
887            p6.opacity.to(0.0, FADE),
888            t6.opacity.to(0.0, FADE),
889            s6.opacity.to(0.0, FADE),
890        ],
891        // Scene 7: Friction
892        t7.opacity.to(1.0, FADE),
893        wait!(1),
894        all![
895            p7.opacity.to(1.0, FADE),
896            s7.opacity.to(1.0, FADE),
897            labels7_c[0].opacity.to(1.0, FADE),
898            labels7_c[1].opacity.to(1.0, FADE),
899            labels7_c[2].opacity.to(1.0, FADE),
900        ],
901        wait!(3.0),
902        all![
903            p7.opacity.to(0.0, FADE),
904            t7.opacity.to(0.0, FADE),
905            s7.opacity.to(0.0, FADE),
906            labels7_c[0].opacity.to(0.0, FADE),
907            labels7_c[1].opacity.to(0.0, FADE),
908            labels7_c[2].opacity.to(0.0, FADE),
909        ],
910        // Scene 8: Mode Switching — full demonstration
911        t8.opacity.to(1.0, FADE),
912        wait!(1),
913        all![
914            p8.opacity.to(1.0, FADE),
915            s8.opacity.to(1.0, FADE),
916            status8.opacity.to(1.0, FADE),
917        ],
918        // ── Phase 1: Disabled — signal-driven tweens ──
919        status8
920            .text
921            .to("mode: Disabled".to_string(), Duration::from_millis(1)),
922        wait!(0.5),
923        // Slide text left
924        text8
925            .position
926            .to(Vec2::new(CX - 200.0, 270.0), Duration::from_millis(800)),
927        wait!(0.3),
928        // Slide text right
929        text8
930            .position
931            .to(Vec2::new(CX + 200.0, 270.0), Duration::from_millis(800)),
932        wait!(0.3),
933        // Slide back to center
934        text8
935            .position
936            .to(Vec2::new(CX, 270.0), Duration::from_millis(600)),
937        wait!(1.0),
938        // Enable balls as Dynamic so they fall onto the disabled text
939        all![
940            balls8[0]
941                .mode
942                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
943            balls8[1]
944                .mode
945                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
946            balls8[2]
947                .mode
948                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
949            balls8[3]
950                .mode
951                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
952        ],
953        wait!(3.0),
954        // ── Phase 2: Dynamic — physics takes over, text falls ──
955        status8
956            .text
957            .to("mode: Dynamic".to_string(), Duration::from_millis(1)),
958        text8
959            .mode
960            .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
961        wait!(1.0),
962        // Enable balls as Dynamic so they fall onto the dynamic text
963        all![
964            balls8[4]
965                .mode
966                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
967            balls8[5]
968                .mode
969                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
970            balls8[6]
971                .mode
972                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
973            balls8[7]
974                .mode
975                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
976        ],
977        wait!(3.0),
978        // ── Phase 3: Kinematic — text becomes a platform, balls drop onto it ──
979        status8
980            .text
981            .to("mode: Kinematic".to_string(), Duration::from_millis(1)),
982        text8
983            .mode
984            .to(PhysicsMode::Kinematic, Duration::from_millis(1)),
985        // Tween text up to act as a shelf
986        text8
987            .position
988            .to(Vec2::new(CX, 300.0), Duration::from_millis(800)),
989        // Enable remaining balls as Dynamic so they fall onto the kinematic text
990        all![
991            balls8[8]
992                .mode
993                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
994            balls8[9]
995                .mode
996                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
997            balls8[10]
998                .mode
999                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
1000            balls8[11]
1001                .mode
1002                .to(PhysicsMode::Dynamic, Duration::from_millis(1)),
1003        ],
1004        wait!(2.0),
1005        // Oscillate the kinematic shelf to show balls react
1006        text8
1007            .position
1008            .to(Vec2::new(CX - 100.0, 280.0), Duration::from_millis(600)),
1009        text8
1010            .position
1011            .to(Vec2::new(CX + 100.0, 320.0), Duration::from_millis(600)),
1012        text8
1013            .position
1014            .to(Vec2::new(CX, 300.0), Duration::from_millis(400)),
1015        wait!(2.0),
1016        all![
1017            p8.opacity.to(0.0, FADE),
1018            t8.opacity.to(0.0, FADE),
1019            s8.opacity.to(0.0, FADE),
1020            status8.opacity.to(0.0, FADE),
1021        ],
1022        // Scene 9: Custom Colliders
1023        t9.opacity.to(1.0, FADE),
1024        wait!(1),
1025        all![p9.opacity.to(1.0, FADE), s9.opacity.to(1.0, FADE),],
1026        wait!(6.0),
1027        all![
1028            p9.opacity.to(0.0, FADE),
1029            t9.opacity.to(0.0, FADE),
1030            s9.opacity.to(0.0, FADE),
1031        ],
1032        // Scene 10: Final Example
1033        t10.opacity.to(1.0, FADE),
1034        wait!(1),
1035        all![p10.opacity.to(1.0, FADE), s10.opacity.to(1.0, FADE),],
1036        wait!(10.0),
1037        all![
1038            p10.opacity.to(0.0, FADE),
1039            t10.opacity.to(0.0, FADE),
1040            s10.opacity.to(0.0, FADE),
1041        ],
1042        wait!(1),
1043    ]);
1044
1045    project.show().expect("Failed to render");
1046}