在 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 提供了多种内置的插值函数,我们可以在着色器中直接使用:
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}
插值在动画中非常重要,因为它能:
比如,当你想要一个按钮在被点击时平滑地改变颜色,或者让一个菜单优雅地滑入滑出,都需要使用插值来创造流畅的动画效果。
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}