In MPSL, we can implement animation effects in several ways. Let's understand this through the example of a "typing" animation:
Typing animation: In some IM applications, when a user is typing, there's a "xxx is typing..." prompt followed by several dots that flash or bounce in sequence.
1draw_bg: {
2 // 1. Control animation parameters through uniform variables
3 uniform freq: 5.0, // Animation frequency
4 uniform phase_offset: 102.0, // Phase difference
5 uniform dot_radius: 1.5, // Dot radius
6
7 // 2. Implement animation logic in pixel shader
8 fn pixel(self) -> vec4 {
9 let sdf = Sdf2d::viewport(self.pos * self.rect_size);
10
11 // Calculate animation positions
12 let amplitude = self.rect_size.y * 0.22; // Amplitude
13 let center_y = self.rect_size.y * 0.5; // Center position
14
15 // Create periodic motion using sin function
16 // self.time is time parameter passed from Rust side
17 let y1 = amplitude * sin(self.time * self.freq) + center_y;
18 let y2 = amplitude * sin(self.time * self.freq + self.phase_offset) + center_y;
19 let y3 = amplitude * sin(self.time * self.freq + self.phase_offset * 2.0) + center_y;
20
21 // Draw dots using SDF
22 sdf.circle(self.rect_size.x * 0.25, y1, self.dot_radius);
23 sdf.fill(self.color);
24
25 sdf.circle(self.rect_size.x * 0.5, y2, self.dot_radius);
26 sdf.fill(self.color);
27
28 sdf.circle(self.rect_size.x * 0.75, y3, self.dot_radius);
29 sdf.fill(self.color);
30
31 return sdf.result;
32 }
33}
MPSL provides several built-in interpolation functions that we can use directly in shaders:
1fn pixel(self) -> vec4 {
2 // 1. Linear interpolation
3 let linear_value = mix(start_value, end_value, self.time);
4
5 // 2. Smooth interpolation
6 let smooth_value = smoothstep(0.0, 1.0, self.time);
7
8 // 3. Custom easing function
9 fn custom_ease(t: float) -> float {
10 return t * t * (3.0 - 2.0 * t); // Smooth S-curve
11 }
12
13 // 4. Bezier curve interpolation
14 let bezier_value = Pal::bezier(
15 self.time, // Time parameter
16 vec2(0.0, 0.0), // P0
17 vec2(0.42, 0.0), // P1
18 vec2(0.58, 1.0), // P2
19 vec2(1.0, 1.0) // P3
20 );
21}
Interpolation is crucial in animation because it:
For example, when you want a button to smoothly change color when clicked, or want a menu to elegantly slide in and out, you need to use interpolation to create fluid animation effects.
1draw_bg: {
2 // 1. Use noise function to create random motion
3 fn noise_movement(pos: vec2, time: float) -> float {
4 return sin(pos.x * 10.0 + time) * cos(pos.y * 10.0 + time) * 0.5;
5 }
6
7 // 2. Use polar coordinates for rotation effects
8 fn rotate_point(p: vec2, angle: float) -> vec2 {
9 let s = sin(angle);
10 let c = cos(angle);
11 return vec2(
12 p.x * c - p.y * s,
13 p.x * s + p.y * c
14 );
15 }
16
17 fn pixel(self) -> vec4 {
18 let sdf = Sdf2d::viewport(self.pos * self.rect_size);
19
20 // 3. Combine multiple animation effects
21 let pos = self.pos - vec2(0.5); // Center coordinates
22 let rot_pos = rotate_point(pos, self.time); // Rotation
23 let noise = noise_movement(rot_pos, self.time); // Add noise
24
25 // 4. Use SDF for shape morphing
26 let radius = 0.2 + noise * 0.1;
27 sdf.circle(rot_pos.x, rot_pos.y, radius);
28
29 // 5. Color animation
30 let color = mix(
31 #f00, // Red
32 #0f0, // Green
33 sin(self.time) * 0.5 + 0.5
34 );
35
36 sdf.fill(color);
37 return sdf.result;
38 }
39}
From Robrix project: https://github.com/project-robius/robrix/blob/main/src/shared/typing_animation.rs
1use makepad_widgets::*;
2
3live_design! {
4 import makepad_widgets::base::*;
5 import makepad_widgets::theme_desktop_dark::*;
6 import makepad_draw::shader::std::*;
7
8 TypingAnimation = {{TypingAnimation}} {
9 width: 24,
10 height: 12,
11 flow: Down,
12 show_bg: true,
13 draw_bg: {
14 color: #x000
15 uniform freq: 5.0, // Animation frequency
16 uniform phase_offset: 102.0, // Phase difference
17 uniform dot_radius: 1.5, // Dot radius
18 fn pixel(self) -> vec4 {
19 let sdf = Sdf2d::viewport(self.pos * self.rect_size);
20 let amplitude = self.rect_size.y * 0.22;
21 let center_y = self.rect_size.y * 0.5;
22 // Create three circle SDFs
23 sdf.circle(
24 self.rect_size.x * 0.25,
25 amplitude * sin(self.time * self.freq) + center_y,
26 self.dot_radius
27 );
28 sdf.fill(self.color);
29 sdf.circle(
30 self.rect_size.x * 0.5,
31 amplitude * sin(self.time * self.freq + self.phase_offset) + center_y,
32 self.dot_radius
33 );
34 sdf.fill(self.color);
35 sdf.circle(
36 self.rect_size.x * 0.75,
37 amplitude * sin(self.time * self.freq + self.phase_offset * 2) + center_y,
38 self.dot_radius
39 );
40 sdf.fill(self.color);
41 return sdf.result;
42 }
43 }
44 }
45}
46
47#[derive(Live, LiveHook, Widget)]
48pub struct TypingAnimation {
49 #[deref] view: View,
50 #[live] time: f32,
51 #[rust] next_frame: NextFrame,
52 #[rust] is_play: bool,
53}
54
55impl Widget for TypingAnimation {
56 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
57 if let Some(ne) = self.next_frame.is_event(event) {
58 // ne.time is time increment in seconds
59 self.time += ne.time as f32;
60 self.time = (self.time.round() as u32 % 360) as f32;
61 self.redraw(cx);
62 if !self.is_play {
63 return
64 }
65 // Request next frame
66 self.next_frame = cx.new_next_frame();
67 }
68
69 self.view.handle_event(cx, event, scope);
70 }
71
72 fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
73 self.view.draw_walk(cx, scope, walk)
74 }
75}
76
77impl TypingAnimationRef {
78 /// Starts animation of the bouncing dots.
79 pub fn animate(&self, cx: &mut Cx) {
80 if let Some(mut inner) = self.borrow_mut() {
81 inner.is_play = true;
82 inner.next_frame = cx.new_next_frame();
83 }
84 }
85 /// Stops animation of the bouncing dots.
86 pub fn stop_animation(&self) {
87 if let Some(mut inner) = self.borrow_mut() {
88 inner.is_play = false;
89 }
90 }
91}
Using Time in Shaders
1draw_bg: {
2 // We can process time in different ways to create various animation effects
3 fn pixel(self) -> vec4 {
4 // 1. Cyclic time - creates periodic animations
5 let cycle_time = fract(self.time); // Constrains time between 0-1
6
7 // 2. Bounce time - creates bouncing effects
8 let bounce_time = abs(sin(self.time));
9
10 // 3. Accumulative time - creates continuously growing effects
11 let grow_time = self.time; // Continuously increasing value
12
13 // 4. Delayed time - creates sequential animations
14 let delay_time = max(self.time - 1.0, 0.0); // Starts after 1 second
15 }
16}
Converting User Gestures to Shader Parameters
1draw_bg: {
2 // Gesture parameters
3 uniform touch_pos: vec2, // Touch position
4 uniform touch_strength: float, // Touch intensity
5
6 fn pixel(self) -> vec4 {
7 let sdf = Sdf2d::viewport(self.pos * self.rect_size);
8
9 // Calculate distance to touch point
10 let dist_to_touch = distance(self.pos, self.touch_pos);
11
12 // Create ripple effect
13 let ripple = sin(dist_to_touch * 50.0 - self.time * 5.0)
14 * exp(-dist_to_touch * 3.0) // Decay
15 * self.touch_strength; // Intensity
16
17 // Apply ripple distortion
18 let distorted_pos = self.pos + ripple * 0.1;
19
20 // Draw distorted shape
21 sdf.circle(
22 distorted_pos.x * self.rect_size.x,
23 distorted_pos.y * self.rect_size.y,
24 10.0
25 );
26 }
27}
Optimization tips:
1// Optimized time update logic
2fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
3 if let Some(ne) = self.next_frame.is_event(event) {
4 // Limit update frequency
5 if ne.time > 1.0/60.0 { // Maximum 60fps
6 self.time += 1.0/60.0;
7 } else {
8 self.time += ne.time;
9 }
10
11 // Only request next frame when necessary
12 if self.is_animating {
13 self.next_frame = cx.new_next_frame();
14 }
15 }
16}
17
18// MPSL optimization
19draw_bg: {
20 // Precalculate commonly used values
21 fn pixel(self) -> vec4 {
22 // Cache calculation results
23 let base_animation = sin(self.time);
24
25 // Reuse calculation results
26 let effect1 = base_animation * 0.5;
27 let effect2 = base_animation * 0.3;
28 let effect3 = base_animation * 0.2;
29 }
30}