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}