Shader Animation

Animation State Machine in MPSL

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}

Interpolation and Transitions in MPSL

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:

  1. Creates smooth transition effects
  2. Simulates natural motion
  3. Controls animation speed and rhythm
  4. Implements various visual effects

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.

Advanced Animation Techniques in MPSL

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}

Complete Typing Animation Code

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}

Possible Extensions and Optimizations

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}