Shader 动画

MPSL 中的动画状态机

在 MPSL 中,我们可以通过几种方式来实现动画效果。让我们通过「正在输入」动画的例子来理解:

正在输入动画:就是某些 IM 在用户输入时候提示「xxx 正在输入 ... 」,后面跟随几个小球依次闪现或者依次弹跳的动画。

1draw_bg: {
2    // 1. 通过 uniform 变量来控制动画参数
3    uniform freq: 5.0,        // 动画频率
4    uniform phase_offset: 102.0,  // 相位差
5    uniform dot_radius: 1.5,   // 点半径
6
7    // 2. 在 pixel 着色器中实现动画逻辑
8    fn pixel(self) -> vec4 {
9        let sdf = Sdf2d::viewport(self.pos * self.rect_size);
10
11        // 计算动画位置
12        let amplitude = self.rect_size.y * 0.22;  // 振幅
13        let center_y = self.rect_size.y * 0.5;    // 中心位置
14
15        // 通过 sin 函数创建周期运动
16        // self.time 是由 Rust 端传入的时间参数
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        // 使用 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 中的插值与过渡

MPSL 提供了多种内置的插值函数,我们可以在着色器中直接使用:

1fn pixel(self) -> vec4 {
2    // 1. 线性插值
3    let linear_value = mix(start_value, end_value, self.time);
4
5    // 2. 平滑插值
6    let smooth_value = smoothstep(0.0, 1.0, self.time);
7
8    // 3. 自定义缓动函数
9    fn custom_ease(t: float) -> float {
10        return t * t * (3.0 - 2.0 * t); // 平滑的 S 型曲线
11    }
12
13    // 4. 贝塞尔曲线插值
14    let bezier_value = Pal::bezier(
15        self.time,  // 时间参数
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}

插值在动画中非常重要,因为它能:

  1. 创建平滑的过渡效果
  2. 模拟自然的运动
  3. 控制动画的速度和节奏
  4. 实现各种视觉效果

比如,当你想要一个按钮在被点击时平滑地改变颜色,或者让一个菜单优雅地滑入滑出,都需要使用插值来创造流畅的动画效果。

MPSL 中的高级动画技巧

1draw_bg: {
2    // 1. 使用噪声函数创建随机运动
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. 使用极坐标实现旋转效果
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. 组合多个动画效果
21        let pos = self.pos - vec2(0.5);  // 中心化坐标
22        let rot_pos = rotate_point(pos, self.time);  // 旋转
23        let noise = noise_movement(rot_pos, self.time);  // 添加噪声
24
25        // 4. 使用 SDF 实现形状变形
26        let radius = 0.2 + noise * 0.1;
27        sdf.circle(rot_pos.x, rot_pos.y, radius);
28
29        // 5. 颜色动画
30        let color = mix(
31            #f00,  // 红色
32            #0f0,  // 绿色
33            sin(self.time) * 0.5 + 0.5
34        );
35
36        sdf.fill(color);
37        return sdf.result;
38    }
39}

正在输入动画完整代码

来自 Robrix 项目: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}
54impl Widget for TypingAnimation {
55    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
56        if let Some(ne) = self.next_frame.is_event(event) {
57            // ne.time 是以秒为单位的时间增量
58            self.time += ne.time as f32;
59            self.time = (self.time.round() as u32 % 360) as f32;
60            self.redraw(cx);
61            if !self.is_play {
62                return
63            }
64            // 请求下一帧
65            self.next_frame = cx.new_next_frame();
66        }
67
68        self.view.handle_event(cx, event, scope);
69    }
70
71    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
72        self.view.draw_walk(cx, scope, walk)
73    }
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}

对上述代码的一些可能的扩展与优化

在着色器中使用时间

1draw_bg: {
2    // 我们可以用不同方式处理时间来创造各种动画效果
3    fn pixel(self) -> vec4 {
4        // 1. 循环时间 - 创建周期性动画
5        let cycle_time = fract(self.time);  // 将时间限制在 0-1 之间
6
7        // 2. 弹性时间 - 创建弹跳效果
8        let bounce_time = abs(sin(self.time));
9
10        // 3. 累积时间 - 持续增长的效果
11        let grow_time = self.time;  // 不断增长的值
12
13        // 4. 延迟时间 - 创建序列动画
14        let delay_time = max(self.time - 1.0, 0.0); // 1秒后开始
15    }
16}

将用户的手势转换为着色器参数

1draw_bg: {
2    // 手势参数
3    uniform touch_pos: vec2,  // 触摸位置
4    uniform touch_strength: float,  // 触摸强度
5
6    fn pixel(self) -> vec4 {
7        let sdf = Sdf2d::viewport(self.pos * self.rect_size);
8
9        // 计算到触摸点的距离
10        let dist_to_touch = distance(self.pos, self.touch_pos);
11
12        // 创建波纹效果
13        let ripple = sin(dist_to_touch * 50.0 - self.time * 5.0)
14            * exp(-dist_to_touch * 3.0)  // 衰减
15            * self.touch_strength;        // 强度
16
17        // 应用波纹扰动
18        let distorted_pos = self.pos + ripple * 0.1;
19
20        // 绘制扭曲的形状
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}

优化技巧:

1// 优化的时间更新逻辑
2fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
3    if let Some(ne) = self.next_frame.is_event(event) {
4        // 限制更新频率
5        if ne.time > 1.0/60.0 {  // 最大60fps
6            self.time += 1.0/60.0;
7        } else {
8            self.time += ne.time;
9        }
10
11        // 只在必要时请求下一帧
12        if self.is_animating {
13            self.next_frame = cx.new_next_frame();
14        }
15    }
16}
17
18// MPSL 优化
19draw_bg: {
20    // 预计算常用值
21    fn pixel(self) -> vec4 {
22        // 缓存计算结果
23        let base_animation = sin(self.time);
24
25        // 复用计算结果
26        let effect1 = base_animation * 0.5;
27        let effect2 = base_animation * 0.3;
28        let effect3 = base_animation * 0.2;
29    }
30}