1use motion_canvas_rs::prelude::*;
2use std::{ops::Add, time::Duration};
3
4struct Landmark {
6 name: &'static str,
7 x: f32,
8 y: f32,
9}
10
11const MAP_W: f32 = 800.0;
13const MAP_H: f32 = 600.0;
14
15const LANDMARKS: [Landmark; 22] = [
17 Landmark {
18 name: "Iceland",
19 x: 337.0,
20 y: 215.0,
21 },
22 Landmark {
23 name: "Norway",
24 x: 397.0,
25 y: 235.0,
26 },
27 Landmark {
28 name: "Sweden",
29 x: 412.0,
30 y: 225.0,
31 },
32 Landmark {
33 name: "Finland",
34 x: 437.0,
35 y: 220.0,
36 },
37 Landmark {
38 name: "Denmark",
39 x: 399.0,
40 y: 254.0,
41 },
42 Landmark {
43 name: "Netherlands",
44 x: 390.0,
45 y: 268.0,
46 },
47 Landmark {
48 name: "Luxembourg",
49 x: 392.0,
50 y: 278.0,
51 },
52 Landmark {
53 name: "Germany",
54 x: 400.0,
55 y: 270.0,
56 },
57 Landmark {
58 name: "Switzerland",
59 x: 396.0,
60 y: 286.0,
61 },
62 Landmark {
63 name: "Egypt",
64 x: 442.0,
65 y: 340.0,
66 },
67 Landmark {
68 name: "Russia",
69 x: 542.0,
70 y: 220.0,
71 },
72 Landmark {
73 name: "China",
74 x: 602.0,
75 y: 320.0,
76 },
77 Landmark {
78 name: "Singapore",
79 x: 600.0,
80 y: 395.0,
81 },
82 Landmark {
83 name: "Japan",
84 x: 677.0,
85 y: 315.0,
86 },
87 Landmark {
88 name: "Australia",
89 x: 665.0,
90 y: 455.0,
91 },
92 Landmark {
93 name: "New Zealand",
94 x: 749.0,
95 y: 499.0,
96 },
97 Landmark {
98 name: "Brazil",
99 x: 272.0,
100 y: 425.0,
101 },
102 Landmark {
103 name: "Paraguay",
104 x: 254.0,
105 y: 452.0,
106 },
107 Landmark {
108 name: "El Salvador",
109 x: 189.0,
110 y: 371.0,
111 },
112 Landmark {
113 name: "Mexico",
114 x: 157.0,
115 y: 345.0,
116 },
117 Landmark {
118 name: "USA",
119 x: 177.0,
120 y: 310.0,
121 },
122 Landmark {
123 name: "Canada",
124 x: 167.0,
125 y: 240.0,
126 },
127];
128
129fn tour_duration(index: usize) -> Duration {
131 let t = index as f32 / (LANDMARKS.len() - 1) as f32;
133 let secs = 4.0 * (1.1 - t) + 1.2 * t;
134 Duration::from_secs_f32(secs)
135}
136
137const SKY_BG: Color = Color::rgb8(0xcf, 0xe5, 0xe8);
139
140const MAP_CX: f32 = MAP_W / 2.0;
142const MAP_CY: f32 = MAP_H / 2.0;
143
144struct CloudDef {
146 x: f32,
147 y: f32,
148 w: f32,
149 h: f32,
150 img: &'static str,
151 flip_x: bool,
152 opacity: f32,
153 exit_x: f32,
155 exit_y: f32,
156}
157
158const CLOUDS: [CloudDef; 24] = [
159 CloudDef {
161 x: 150.0,
162 y: 180.0,
163 w: 400.0,
164 h: 200.0,
165 img: "cloud-1.png",
166 flip_x: false,
167 opacity: 0.95,
168 exit_x: -500.0,
169 exit_y: 100.0,
170 },
171 CloudDef {
172 x: 800.0,
173 y: 140.0,
174 w: 380.0,
175 h: 180.0,
176 img: "cloud-2.png",
177 flip_x: false,
178 opacity: 0.92,
179 exit_x: 1600.0,
180 exit_y: 50.0,
181 },
182 CloudDef {
183 x: 500.0,
184 y: 480.0,
185 w: 450.0,
186 h: 210.0,
187 img: "cloud-3.png",
188 flip_x: false,
189 opacity: 0.90,
190 exit_x: 500.0,
191 exit_y: 900.0,
192 },
193 CloudDef {
194 x: 920.0,
195 y: 350.0,
196 w: 350.0,
197 h: 160.0,
198 img: "cloud-1.png",
199 flip_x: true,
200 opacity: 0.88,
201 exit_x: 1500.0,
202 exit_y: 450.0,
203 },
204 CloudDef {
205 x: 400.0,
206 y: 320.0,
207 w: 420.0,
208 h: 200.0,
209 img: "cloud-2.png",
210 flip_x: true,
211 opacity: 0.93,
212 exit_x: -500.0,
213 exit_y: 400.0,
214 },
215 CloudDef {
216 x: 700.0,
217 y: 500.0,
218 w: 400.0,
219 h: 190.0,
220 img: "cloud-3.png",
221 flip_x: false,
222 opacity: 0.91,
223 exit_x: 1400.0,
224 exit_y: 800.0,
225 },
226 CloudDef {
228 x: 50.0,
229 y: 400.0,
230 w: 350.0,
231 h: 170.0,
232 img: "cloud-2.png",
233 flip_x: true,
234 opacity: 0.82,
235 exit_x: -450.0,
236 exit_y: 500.0,
237 },
238 CloudDef {
239 x: 350.0,
240 y: 90.0,
241 w: 320.0,
242 h: 150.0,
243 img: "cloud-3.png",
244 flip_x: true,
245 opacity: 0.80,
246 exit_x: 350.0,
247 exit_y: -300.0,
248 },
249 CloudDef {
250 x: 700.0,
251 y: 300.0,
252 w: 300.0,
253 h: 140.0,
254 img: "cloud-1.png",
255 flip_x: false,
256 opacity: 0.78,
257 exit_x: 1400.0,
258 exit_y: 300.0,
259 },
260 CloudDef {
261 x: 200.0,
262 y: 550.0,
263 w: 380.0,
264 h: 180.0,
265 img: "cloud-2.png",
266 flip_x: false,
267 opacity: 0.76,
268 exit_x: -400.0,
269 exit_y: 750.0,
270 },
271 CloudDef {
272 x: 950.0,
273 y: 100.0,
274 w: 330.0,
275 h: 155.0,
276 img: "cloud-1.png",
277 flip_x: true,
278 opacity: 0.79,
279 exit_x: 1500.0,
280 exit_y: -200.0,
281 },
282 CloudDef {
283 x: 100.0,
284 y: 100.0,
285 w: 340.0,
286 h: 160.0,
287 img: "cloud-3.png",
288 flip_x: false,
289 opacity: 0.77,
290 exit_x: -400.0,
291 exit_y: -200.0,
292 },
293 CloudDef {
295 x: 600.0,
296 y: 200.0,
297 w: 250.0,
298 h: 120.0,
299 img: "cloud-3.png",
300 flip_x: false,
301 opacity: 0.72,
302 exit_x: 1300.0,
303 exit_y: 100.0,
304 },
305 CloudDef {
306 x: 100.0,
307 y: 280.0,
308 w: 280.0,
309 h: 130.0,
310 img: "cloud-1.png",
311 flip_x: true,
312 opacity: 0.70,
313 exit_x: -380.0,
314 exit_y: 280.0,
315 },
316 CloudDef {
317 x: 450.0,
318 y: 350.0,
319 w: 260.0,
320 h: 120.0,
321 img: "cloud-2.png",
322 flip_x: true,
323 opacity: 0.68,
324 exit_x: 450.0,
325 exit_y: 800.0,
326 },
327 CloudDef {
328 x: 850.0,
329 y: 500.0,
330 w: 300.0,
331 h: 140.0,
332 img: "cloud-3.png",
333 flip_x: true,
334 opacity: 0.65,
335 exit_x: 1400.0,
336 exit_y: 700.0,
337 },
338 CloudDef {
339 x: 250.0,
340 y: 450.0,
341 w: 270.0,
342 h: 125.0,
343 img: "cloud-1.png",
344 flip_x: false,
345 opacity: 0.67,
346 exit_x: -350.0,
347 exit_y: 600.0,
348 },
349 CloudDef {
350 x: 550.0,
351 y: 100.0,
352 w: 290.0,
353 h: 135.0,
354 img: "cloud-2.png",
355 flip_x: true,
356 opacity: 0.69,
357 exit_x: 550.0,
358 exit_y: -300.0,
359 },
360 CloudDef {
362 x: 300.0,
363 y: 150.0,
364 w: 200.0,
365 h: 100.0,
366 img: "cloud-1.png",
367 flip_x: false,
368 opacity: 0.60,
369 exit_x: -300.0,
370 exit_y: 50.0,
371 },
372 CloudDef {
373 x: 750.0,
374 y: 450.0,
375 w: 220.0,
376 h: 100.0,
377 img: "cloud-2.png",
378 flip_x: false,
379 opacity: 0.58,
380 exit_x: 1300.0,
381 exit_y: 550.0,
382 },
383 CloudDef {
384 x: 550.0,
385 y: 550.0,
386 w: 240.0,
387 h: 110.0,
388 img: "cloud-3.png",
389 flip_x: true,
390 opacity: 0.55,
391 exit_x: 550.0,
392 exit_y: 850.0,
393 },
394 CloudDef {
395 x: 950.0,
396 y: 200.0,
397 w: 200.0,
398 h: 90.0,
399 img: "cloud-1.png",
400 flip_x: true,
401 opacity: 0.52,
402 exit_x: 1500.0,
403 exit_y: 100.0,
404 },
405 CloudDef {
406 x: 50.0,
407 y: 550.0,
408 w: 230.0,
409 h: 105.0,
410 img: "cloud-3.png",
411 flip_x: false,
412 opacity: 0.56,
413 exit_x: -350.0,
414 exit_y: 800.0,
415 },
416 CloudDef {
417 x: 830.0,
418 y: 250.0,
419 w: 210.0,
420 h: 95.0,
421 img: "cloud-2.png",
422 flip_x: true,
423 opacity: 0.54,
424 exit_x: 1400.0,
425 exit_y: 150.0,
426 },
427];
428
429fn main() {
430 let w = 800u32;
431 let h = 600u32;
432
433 let mut project = Project::new(w, h)
434 .with_fps(60)
435 .with_title("World Map")
436 .with_background(SKY_BG)
437 .close_on_finish();
438
439 let map = SvgNode::default()
441 .with_position(Vec2::new(MAP_CX, MAP_CY))
442 .with_path("./examples/images/world.svg")
443 .with_size(Vec2::new(MAP_W, MAP_H))
444 .with_opacity(0.0);
445
446 let mut cloud_nodes: Vec<ImageNode> = Vec::new();
448 for def in &CLOUDS {
449 let mut node = ImageNode::default()
450 .with_position(Vec2::new(def.x, def.y))
451 .with_path(&format!("./examples/images/{}", def.img))
452 .with_size(Vec2::new(def.w, def.h))
453 .with_opacity(def.opacity);
454 if def.flip_x {
455 node = node.with_scale_xy(Vec2::new(-1.0, 1.0));
456 }
457 cloud_nodes.push(node);
458 }
459
460 let camera = CameraNode::default()
462 .with_size(Vec2::new(w as f32, h as f32))
463 .with_position(Vec2::new(MAP_CX, MAP_CY))
464 .with_zoom(1.0)
465 .with_centered(true);
466
467 let plane = SvgNode::default()
469 .with_position(Vec2::new(LANDMARKS[0].x, LANDMARKS[0].y))
470 .with_path("./examples/images/plane.svg")
471 .with_size(Vec2::new(12.0, 12.0))
472 .with_anchor(Vec2::new(2.0, -2.65))
473 .with_scale(0.0)
474 .with_opacity(0.0);
475
476 let mut pins: Vec<Circle> = Vec::new();
478 let mut name_labels: Vec<TextNode> = Vec::new();
479 for lm in &LANDMARKS {
480 let pin = Circle::default()
481 .with_position(Vec2::new(lm.x, lm.y))
482 .with_radius(0.0)
483 .with_fill(Palette::RED)
484 .with_stroke(Color::rgba8(0xff, 0xff, 0xff, 180), 2.0)
485 .with_opacity(0.0);
486 pins.push(pin);
487
488 let name_lbl = TextNode::default()
490 .with_position(Vec2::new(lm.x, lm.y + 12.0))
491 .with_text(lm.name)
492 .with_font_size(10.0)
493 .with_fill(Palette::DARK_GRAY)
494 .with_opacity(0.0);
495 name_labels.push(name_lbl);
496 }
497
498 let mut route_lines: Vec<Line> = Vec::new();
500 for i in 0..LANDMARKS.len() - 1 {
501 let from = &LANDMARKS[i];
502 let line = Line::default()
503 .with_start(Vec2::new(from.x, from.y))
504 .with_end(Vec2::new(from.x, from.y))
505 .with_stroke(Color::rgba8(0xff, 0xff, 0xff, 180), 2.0)
506 .with_opacity(0.0);
507 route_lines.push(line);
508 }
509
510 let mut camera_children: Vec<Box<dyn Node>> = Vec::new();
513 camera_children.push(Box::new(map.clone()));
514 for rl in &route_lines {
516 camera_children.push(Box::new(rl.clone()));
517 }
518 camera_children.push(Box::new(plane.clone()));
520 for pin in &pins {
522 camera_children.push(Box::new(pin.clone()));
523 }
524
525 for nl in &name_labels {
526 camera_children.push(Box::new(nl.clone()));
527 }
528
529 for cn in &cloud_nodes {
531 camera_children.push(Box::new(cn.clone()));
532 }
533
534 let camera = camera.with_nodes(camera_children);
535 project.scene.add(&camera);
536
537 let mut phase1_anims: Vec<AnyAnimation> = Vec::new();
543
544 phase1_anims.push(
546 camera
547 .zoom
548 .to(2.25, Duration::from_secs(3))
549 .ease(easings::cubic_in_out)
550 .into(),
551 );
552 phase1_anims.push(
554 map.opacity
555 .to(1.0, Duration::from_secs(2))
556 .ease(easings::cubic_out)
557 .into(),
558 );
559
560 for (i, cn) in cloud_nodes.iter().enumerate() {
562 let def = &CLOUDS[i];
563 phase1_anims.push(
564 cn.position
565 .to(Vec2::new(def.exit_x, def.exit_y), Duration::from_secs(3))
566 .ease(easings::cubic_in)
567 .into(),
568 );
569 phase1_anims.push(
570 cn.opacity
571 .to(0.0, Duration::from_secs(2))
572 .ease(easings::cubic_in)
573 .into(),
574 );
575 }
576
577 let phase1_intro: AnyAnimation = chain![
578 all(phase1_anims),
580 wait(Duration::from_millis(500)),
581 ];
582
583 let first = &LANDMARKS[0];
585 let phase2_start: AnyAnimation = chain![
586 camera
587 .position
588 .to(Vec2::new(first.x, first.y), Duration::from_secs(2))
589 .ease(easings::cubic_in_out),
590 camera
591 .zoom
592 .to(5.0, Duration::from_secs(1))
593 .ease(easings::cubic_out),
594 all![
596 pins[0].opacity.to(1.0, Duration::from_millis(400)),
597 pins[0]
598 .radius
599 .to(3.0, Duration::from_millis(600))
600 .ease(easings::elastic_out),
601 name_labels[0].opacity.to(1.0, Duration::from_millis(400)),
602 ],
603 plane.opacity.to(1.0, Duration::from_millis(300)),
605 wait(Duration::from_millis(800)),
606 ];
607
608 let mut tour_legs: Vec<AnyAnimation> = Vec::new();
610 for i in 0..LANDMARKS.len() - 1 {
611 let from = &LANDMARKS[i];
612 let to = &LANDMARKS[i + 1];
613 let dur = tour_duration(i);
614
615 let dx = to.x - from.x;
617 let dy = to.y - from.y;
618 let angle = dy.atan2(dx) + (std::f32::consts::PI);
619
620 let leg: AnyAnimation = chain![
621 all![
623 plane
624 .rotation
625 .to(angle, Duration::from_millis(200))
626 .ease(easings::cubic_out),
627 plane
628 .scale
629 .to(Vec2::splat(1.0), Duration::from_millis(300))
630 .ease(easings::cubic_out),
631 name_labels[i]
633 .opacity
634 .to(0.0, Duration::from_millis(200))
635 .ease(easings::cubic_out),
636 ],
637 all![
639 route_lines[i].opacity.to(1.0, Duration::from_millis(100)),
641 route_lines[i]
643 .end
644 .to(Vec2::new(to.x, to.y), dur)
645 .ease(easings::cubic_in_out),
646 all![
647 plane
649 .position
650 .to(Vec2::new(to.x, to.y), dur)
651 .ease(easings::cubic_in_out),
652 camera
654 .position
655 .to(Vec2::new(to.x, to.y), dur.add(Duration::from_millis(500)))
656 .ease(easings::cubic_in_out),
657 ]
658 ],
659 plane
661 .scale
662 .to(Vec2::splat(0.0), Duration::from_millis(300))
663 .ease(easings::cubic_in),
664 all![
666 pins[i + 1].opacity.to(1.0, Duration::from_millis(300)),
667 pins[i + 1]
668 .radius
669 .to(3.0, Duration::from_millis(500))
670 .ease(easings::elastic_out),
671 name_labels[i + 1]
672 .opacity
673 .to(1.0, Duration::from_millis(300)),
674 ],
675 wait(Duration::from_millis(600)),
677 name_labels[i + 1]
679 .opacity
680 .to(0.0, Duration::from_millis(200))
681 .ease(easings::cubic_out),
682 ];
683 tour_legs.push(leg);
684 }
685
686 let phase3_tour = chain(tour_legs);
687
688 let phase4_finale: AnyAnimation = chain![
690 name_labels[LANDMARKS.len() - 1]
692 .opacity
693 .to(0.0, Duration::from_millis(200)),
694 wait(Duration::from_millis(300)),
695 all![
696 camera
697 .position
698 .to(Vec2::new(MAP_CX, MAP_CY), Duration::from_secs(3))
699 .ease(easings::cubic_in_out),
700 camera
701 .zoom
702 .to(1.0, Duration::from_secs(3))
703 .ease(easings::sine_in_out),
704 ],
705 {
707 let mut pulse_anims: Vec<AnyAnimation> = Vec::new();
708 for pin in &pins {
709 pulse_anims.push(
710 pin.radius
711 .to(10.0, Duration::from_millis(300))
712 .ease(easings::elastic_out)
713 .into(),
714 );
715 }
716 sequence(Duration::from_millis(60), pulse_anims)
717 },
718 plane.opacity.to(0.0, Duration::from_millis(500)),
719 wait(Duration::from_secs(3)),
720 ];
721
722 project.scene.video_timeline.add(chain![
723 phase1_intro,
724 phase2_start,
725 phase3_tour,
726 phase4_finale,
727 ]);
728
729 project.show().expect("Failed to render");
730}