Skip to main content

31_animated_canvas/
31_animated_canvas.rs

1//! Example 31: Animated Canvas
2//!
3//! Demonstrates the animated_canvas helper for frame-based animations
4//! using the Kitty graphics protocol.
5//!
6//! Features:
7//! - Automatic frame timing management
8//! - Bouncing ball animation
9//! - Sine wave visualization
10//! - Particle effects
11//!
12//! NOTE: Requires a Kitty-protocol compatible terminal:
13//! - Kitty
14//! - Ghostty
15//! - WezTerm
16//!
17//! Run with: cargo run -p telex-tui --example 31_animated_canvas
18
19use crossterm::event::KeyCode;
20use telex::canvas::animated_canvas;
21use telex::prelude::*;
22use telex::Color;
23
24telex::require_api!(0, 2);
25
26fn main() {
27    telex::run_with_theme(App, telex::theme::Theme::nord()).unwrap();
28}
29
30struct App;
31
32impl Component for App {
33    fn render(&self, cx: Scope) -> View {
34        let show_help = state!(cx, || false);
35
36        // F1 toggles help
37        cx.use_command(
38            KeyBinding::key(KeyCode::F(1)),
39            with!(show_help => move || show_help.update(|v| *v = !*v)),
40        );
41        View::vstack()
42            .spacing(1)
43            .child(
44                View::styled_text("Animated Canvas Demo (Kitty Graphics)")
45                    .bold()
46                    .build(),
47            )
48            .child(View::text("Requires Kitty, Ghostty, or WezTerm terminal"))
49            .child(View::text(""))
50            // Bouncing ball animation
51            .child(View::text("Bouncing Ball (60 FPS):"))
52            .child(
53                animated_canvas(cx.clone())
54                    .width(200)
55                    .height(60)
56                    .fps(60)
57                    .on_frame(|ctx, frame| {
58                        ctx.clear(Color::Rgb {
59                            r: 20,
60                            g: 20,
61                            b: 30,
62                        });
63
64                        // Ball physics
65                        let t = frame as f32 * 0.05;
66                        let x = 100.0 + 80.0 * t.cos();
67                        let y = 30.0 + 20.0 * (t * 1.5).sin().abs();
68
69                        // Shadow
70                        ctx.fill_circle(
71                            x as u16,
72                            55,
73                            8,
74                            Color::Rgb {
75                                r: 40,
76                                g: 40,
77                                b: 50,
78                            },
79                        );
80
81                        // Ball with gradient effect (outer to inner)
82                        ctx.fill_circle(
83                            x as u16,
84                            y as u16,
85                            12,
86                            Color::Rgb {
87                                r: 200,
88                                g: 50,
89                                b: 50,
90                            },
91                        );
92                        ctx.fill_circle(
93                            x as u16,
94                            y as u16,
95                            8,
96                            Color::Rgb {
97                                r: 255,
98                                g: 80,
99                                b: 80,
100                            },
101                        );
102                        ctx.fill_circle(
103                            x as u16 - 2,
104                            y as u16 - 2,
105                            3,
106                            Color::Rgb {
107                                r: 255,
108                                g: 200,
109                                b: 200,
110                            },
111                        );
112
113                        // Ground line
114                        ctx.line(10, 58, 190, 58, Color::Grey);
115                    })
116                    .build(),
117            )
118            .child(View::text(""))
119            // Sine wave animation
120            .child(View::text("Sine Waves (30 FPS):"))
121            .child(
122                animated_canvas(cx.clone())
123                    .width(200)
124                    .height(50)
125                    .fps(30)
126                    .on_frame(|ctx, frame| {
127                        ctx.clear(Color::Rgb {
128                            r: 15,
129                            g: 25,
130                            b: 35,
131                        });
132
133                        let phase = frame as f32 * 0.1;
134
135                        // Draw multiple sine waves
136                        let colors = [
137                            Color::Rgb {
138                                r: 255,
139                                g: 100,
140                                b: 100,
141                            },
142                            Color::Rgb {
143                                r: 100,
144                                g: 255,
145                                b: 100,
146                            },
147                            Color::Rgb {
148                                r: 100,
149                                g: 100,
150                                b: 255,
151                            },
152                        ];
153
154                        for (wave_idx, color) in colors.iter().enumerate() {
155                            let offset = wave_idx as f32 * 0.5;
156                            let amplitude = 15.0 - wave_idx as f32 * 3.0;
157
158                            for x in 0..200 {
159                                let t = x as f32 * 0.05 + phase + offset;
160                                let y = 25.0 + amplitude * t.sin();
161                                ctx.pixel(x, y as u16, *color);
162
163                                // Make line thicker
164                                if y as u16 > 0 {
165                                    ctx.pixel(x, y as u16 - 1, *color);
166                                }
167                            }
168                        }
169
170                        // Center line
171                        ctx.line(0, 25, 199, 25, Color::DarkGrey);
172                    })
173                    .build(),
174            )
175            .child(View::text(""))
176            // Particle effect
177            .child(View::text("Particle Fountain (45 FPS):"))
178            .child(
179                animated_canvas(cx.clone())
180                    .width(200)
181                    .height(60)
182                    .fps(45)
183                    .on_frame(|ctx, frame| {
184                        ctx.clear(Color::Rgb {
185                            r: 10,
186                            g: 10,
187                            b: 20,
188                        });
189
190                        // Simple particle system using deterministic "random" based on frame
191                        let num_particles = 30;
192                        for i in 0..num_particles {
193                            // Each particle has a lifecycle based on frame offset
194                            let particle_frame = (frame + i * 7) % 60;
195                            let life = particle_frame as f32 / 60.0;
196
197                            // Starting position (center bottom)
198                            let start_x = 100.0;
199                            let start_y = 55.0;
200
201                            // Velocity varies per particle (deterministic "random")
202                            let angle = -1.57 + (i as f32 * 0.21).sin() * 0.8; // Around -90 degrees
203                            let speed = 40.0 + (i as f32 * 0.37).cos() * 20.0;
204
205                            // Physics: position = start + velocity * time + 0.5 * gravity * time^2
206                            let vx = angle.cos() * speed;
207                            let vy = angle.sin() * speed;
208                            let gravity = 80.0;
209
210                            let x = start_x + vx * life;
211                            let y = start_y + vy * life + 0.5 * gravity * life * life;
212
213                            // Fade out as particle ages
214                            let alpha = (1.0 - life) * 255.0;
215
216                            // Color based on age (yellow -> orange -> red)
217                            let r = 255;
218                            let g = ((1.0 - life * 0.7) * 200.0) as u8;
219                            let b = ((1.0 - life) * 100.0) as u8;
220
221                            if (0.0..200.0).contains(&x) && (0.0..60.0).contains(&y) {
222                                let color = Color::Rgb { r, g, b };
223                                ctx.pixel_alpha(x as u16, y as u16, color, alpha as u8);
224                                // Make particles slightly larger
225                                if x > 0.0 {
226                                    ctx.pixel_alpha(
227                                        x as u16 - 1,
228                                        y as u16,
229                                        color,
230                                        (alpha * 0.5) as u8,
231                                    );
232                                }
233                                if y > 0.0 {
234                                    ctx.pixel_alpha(
235                                        x as u16,
236                                        y as u16 - 1,
237                                        color,
238                                        (alpha * 0.5) as u8,
239                                    );
240                                }
241                            }
242                        }
243
244                        // Emitter glow
245                        ctx.fill_circle(100, 55, 3, Color::Yellow);
246                    })
247                    .build(),
248            )
249            .child(View::text(""))
250            .child(View::styled_text("F1 help • Ctrl+Q quit").dim().build())
251            .child(
252                View::modal()
253                    .visible(show_help.get())
254                    .title("Example 31: Animated Canvas")
255                    .on_dismiss(with!(show_help => move || show_help.set(false)))
256                    .child(
257                        View::vstack()
258                            .child(View::styled_text("What you're seeing").bold().build())
259                            .child(View::text("• Bouncing ball animation (60 FPS)"))
260                            .child(View::text("• Sine wave visualization (30 FPS)"))
261                            .child(View::text("• Particle fountain (45 FPS)"))
262                            .child(View::gap(1))
263                            .child(View::styled_text("Key concepts").bold().build())
264                            .child(View::text("• animated_canvas(cx) helper"))
265                            .child(View::text("• .fps() sets frame rate"))
266                            .child(View::text("• .on_frame(|ctx, frame| { }) draws"))
267                            .child(View::text("• Frame counter enables animation"))
268                            .child(View::gap(1))
269                            .child(View::styled_text("Try this").bold().build())
270                            .child(View::text("• Watch the smooth animations"))
271                            .child(View::text("• Notice different FPS rates"))
272                            .child(View::gap(1))
273                            .child(View::styled_text("Next up").bold().build())
274                            .child(View::text("→ 32_effects: side effects"))
275                            .child(View::gap(1))
276                            .child(View::styled_text("Press Escape to close").dim().build())
277                            .build(),
278                    )
279                    .build(),
280            )
281            .build()
282    }
283}