1use ascii_assets::{AsciiSprite, AsciiVideo, Color, TerminalChar};
2use common_stdx::{Point, Rect};
3use crossterm::event::{Event, KeyCode, MouseEventKind, poll, read};
4use crossterm::terminal::size;
5use log::info;
6use ltrender::display_screen::AreaRect;
7use ltrender::rendering::render_handle::RenderHandle;
8use ltrender::rendering::render_thread::start_renderer;
9use ltrender::rendering::renderer::Buffered;
10use ltrender::terminal_buffer::buffer_and_celldrawer::CrosstermCellDrawer;
11use ltrender::terminal_buffer::buffer_and_celldrawer::DefaultScreenBuffer;
12use rand::Rng;
13use std::sync::mpsc::SyncSender;
14use std::time::{Duration, Instant};
15
16use ltrender::draw_object_builder::SpriteDrawableBuilder;
17use ltrender::draw_object_builder::videostream_drawable_builder::make_videostream_drawable;
18use ltrender::drawable_register::ObjectLifetime;
19use ltrender::error::AppError;
20use ltrender::{Renderer, init_logger, init_terminal};
21
22use ltrender::terminal_buffer::buffer_and_celldrawer::shaders::Grayscale;
23use ltrender::terminal_buffer::standard_drawables::sprite_drawable::{
24 AnimationInfo, FrameIdent, VideoLoopType, VideoSpeed,
25};
26use ltrender::terminal_buffer::standard_drawables::videostream_drawable::StreamFrame;
27use ltrender::terminal_buffer::standard_drawables::{PolygonDrawable, text_drawable};
28use ltrender::{DrawObject, DrawObjectBuilder, DrawObjectKey};
29
30use tick_manager_rs::{TickManager, TickMember};
31
32#[derive(Clone, Copy, Debug)]
33struct Agent {
34 id: DrawObjectKey,
35 pos: (f64, f64),
36 dir: f64,
37 speed: f64,
38 wobble: f64,
39 size: f64,
40}
41
42#[derive(Default, Debug, Clone, Copy)]
43struct MouseState {
44 x: i32,
45 y: i32,
46 left: bool,
47 right: bool,
48 strength: f64,
49}
50
51fn wrap(v: f64, max: f64) -> f64 {
52 if v < 0.0 {
53 v + max
54 } else if v >= max {
55 v - max
56 } else {
57 v
58 }
59}
60
61fn lerp(a: f64, b: f64, t: f64) -> f64 {
62 a + (b - a) * t
63}
64
65fn triangle_points(cx: f64, cy: f64, angle: f64, size: f64) -> Vec<Point<i32>> {
66 let a = angle;
67 let s = size.max(1.0);
68 let p1 = (cx + a.cos() * s, cy + a.sin() * s);
69 let p2 = (
70 cx + (a + 2.4).cos() * (s * 0.6),
71 cy + (a + 2.4).sin() * (s * 0.6),
72 );
73 let p3 = (
74 cx + (a - 2.4).cos() * (s * 0.6),
75 cy + (a - 2.4).sin() * (s * 0.6),
76 );
77 vec![
78 Point {
79 x: p1.0 as i32,
80 y: p1.1 as i32,
81 },
82 Point {
83 x: p2.0 as i32,
84 y: p2.1 as i32,
85 },
86 Point {
87 x: p3.0 as i32,
88 y: p3.1 as i32,
89 },
90 ]
91}
92
93fn make_agent_obj(layer: usize, col: Color) -> DrawObject {
94 let border = TerminalChar::with_fg('#', col);
95 let fill = TerminalChar::with_fg('#', col);
96 DrawObject {
97 layer,
98 shaders: Vec::new(),
99 drawable: Box::new(PolygonDrawable {
100 points: vec![],
101 fill_style: Some(fill),
102 border_style: border,
103 }),
104 lifetime: ObjectLifetime::ExplicitRemove,
105 creation_time: Instant::now(),
106 }
107}
108
109fn seeded_swarm(
110 r: &mut RenderHandle<Buffered>,
111 screen_id: usize,
112 n: usize,
113 cols: u16,
114 rows: u16,
115) -> Vec<Agent> {
116 let mut rng = rand::thread_rng();
117 let palette = [
118 Color::Teal,
119 Color::Blue,
120 Color::Green,
121 Color::Yellow,
122 Color::White,
123 Color::Grey,
124 ];
125 let mut out = Vec::with_capacity(n);
126
127 for i in 0..n {
128 let col = palette[i % palette.len()];
129 let obj = make_agent_obj(100 + i, col);
130 let key = r
131 .register_drawable(screen_id, obj)
132 .expect("register_drawable");
133 let x = rng.gen_range(1..cols.saturating_sub(2)) as f64;
134 let y = rng.gen_range(1..rows.saturating_sub(2)) as f64;
135 let dir = rng.gen_range(0.0..std::f64::consts::TAU);
136 let spd = rng.gen_range(0.6..1.8);
137 let wob = rng.gen_range(0.2..1.0);
138 let sz = rng.gen_range(1.5..3.5);
139 out.push(Agent {
140 id: key,
141 pos: (x, y),
142 dir,
143 speed: spd,
144 wobble: wob,
145 size: sz,
146 });
147 }
148 out
149}
150
151fn spawn_soft_wave(
152 sender: SyncSender<StreamFrame>,
153 ren: RenderHandle<Buffered>,
154 key: DrawObjectKey,
155 size: (u16, u16),
156 bg_member: TickMember,
157) {
158 std::thread::spawn(move || {
159 let mut t = 0.0f32;
160 loop {
161 let mut data: Vec<Option<TerminalChar>> =
162 Vec::with_capacity((size.0 as usize) * (size.1 as usize));
163 for y in 0..size.1 {
164 for x in 0..size.0 {
165 let xf = x as f32 / size.1 as f32;
166 let yf = y as f32 / size.1 as f32;
167 let w = ((xf * 10.0 + t).sin() + (yf * 7.0 - t * 0.7).cos()) * 0.5 + 0.5;
168 let ch = match w {
169 w if w > 0.70 => Some(TerminalChar::from_char(' ')),
170 w if w > 0.50 => Some(TerminalChar::from_char('‧')),
171 w if w > 0.30 => Some(TerminalChar::from_char(' ')),
172 _ => Some(TerminalChar::with_fg('.', Color::Grey)),
173 };
174
175 data.push(ch);
176 }
177 }
178 let _ = sender.send(StreamFrame { data, size });
179 let _ = ren.render_drawable(key);
180 t += 0.04;
181 bg_member.wait_for_tick();
182 }
183 });
184}
185
186pub fn main() -> Result<(), AppError> {
187 init_terminal!();
188 init_logger("./organic_swarm.log")?;
189
190 let (_manager, manager_handle) = TickManager::new(tick_manager_rs::Speed::Fps(50));
192 let main_member = tick_manager_rs::TickMember::new(manager_handle.clone(), 1);
193 let bg_member = tick_manager_rs::TickMember::new(manager_handle, 6);
194
195 let (cols, rows) = size()?;
196 let renderer = Renderer::<DefaultScreenBuffer<CrosstermCellDrawer>, Buffered>::create_renderer(
197 (cols, rows),
198 );
199 let mut r = start_renderer(renderer);
200 r.set_update_interval(16)?;
201
202 let screen = r.create_screen(AreaRect::FullScreen, 5)?;
203
204 let (tx, video_drawable) = make_videostream_drawable((0, 0), 1)?;
205 let video_id = DrawObjectBuilder::default()
206 .layer(1)
207 .screen(screen)
208 .drawable(video_drawable)
209 .shader(Grayscale)
210 .add_lifetime(ObjectLifetime::ExplicitRemove)
211 .build_and_register(&mut r)?;
212 spawn_soft_wave(
213 tx,
214 RenderHandle::<Buffered>::clone(&r),
215 video_id,
216 (cols, rows),
217 bg_member,
218 );
219
220 if let Ok(sprite) = r.register_sprite_from_source("./assets/debugging/test_video.ascv") {
222 let spr_draw = SpriteDrawableBuilder::default()
223 .sprite_id(sprite)
224 .position((0, 50))
225 .animation_type(AnimationInfo::Video {
226 loop_type: VideoLoopType::Loop,
227 speed: VideoSpeed::Fps(4),
228 start_frame: FrameIdent::FirstFrame,
229 end_frame: FrameIdent::LastFrame,
230 })
231 .build()?;
232 let _spr_id = DrawObjectBuilder::default()
233 .layer(2)
234 .screen(screen)
235 .drawable(spr_draw)
236 .add_lifetime(ObjectLifetime::ExplicitRemove)
237 .build_and_register(&mut r)?;
238 }
239 let rect_id: DrawObjectKey = DrawObjectBuilder::default()
240 .layer(100)
241 .screen(screen)
242 .rect_drawable(|r| {
243 r.border_style(
244 ltrender::terminal_buffer::standard_drawables::rect_drawable::BorderStyle::Custom {
245 top: TerminalChar::from_char('T'),
246 bottom: TerminalChar::from_char('B'),
247 left: TerminalChar::from_char('L'),
248 right: TerminalChar::from_char('R'),
249 },
250 )
251 .border_thickness(2)
252 .fill_style(' ')
253 .rect(Rect::from_coords(20, 20, 40, 40))
254 })?
255 .add_lifetime(ObjectLifetime::ExplicitRemove)
256 .build_and_register(&mut r)?;
257
258 let mut show_hud = true;
260 let hud_drawable = Box::new(text_drawable::TextDrawable {
261 area: Rect::from_coords(1, 1, 50, 10),
262 lines: vec![text_drawable::LineInfo {
263 text: String::from(
264 "organic swarm — drag mouse, left: attract, right: repel, scroll: strength, n: reseed, h: HUD, q: quit",
265 ),
266 spans: vec![],
267 alignment: text_drawable::TextAlignment::Left,
268 default_style: text_drawable::TextStyle {
269 foreground: Some(Color::White),
270 bold: Some(true),
271 ..Default::default()
272 },
273 }],
274 wrapping: true,
275 scroll_y: 0,
276 });
277 let hud_id = DrawObjectBuilder::default()
278 .layer(9)
279 .screen(screen)
280 .drawable(hud_drawable)
281 .add_lifetime(ObjectLifetime::ExplicitRemove)
282 .build_and_register(&mut r)?;
283
284 let mut agents = seeded_swarm(&mut r, screen, 140, cols, rows);
286
287 let start = Instant::now();
289 let mut mouse = MouseState {
290 x: (cols / 2) as i32,
291 y: (rows / 2) as i32,
292 left: false,
293 right: false,
294 strength: 18.0,
295 };
296 let mut running = true;
297
298 let mut render_start;
299 let mut fps_vec: Vec<f64> = Vec::new();
300 let mut time_since_last_fps_log = Instant::now();
301
302 while running {
303 render_start = Instant::now();
304 if poll(Duration::from_millis(0))? {
306 match read()? {
307 Event::Key(k) => {
308 match k.code {
309 KeyCode::Esc | KeyCode::Char('q') => running = false,
310 KeyCode::Char('n') => {
311 for a in &agents {
313 r.explicit_remove_drawable(&a.id)?;
314 }
315 agents = seeded_swarm(&mut r, screen, 140, cols, rows);
316 }
317 KeyCode::Char('h') => {
318 show_hud = !show_hud;
319 if show_hud {
320 r.render_drawable(hud_id)?;
321 } else {
322 r.explicit_remove_drawable(&hud_id)?;
323 }
324 }
325 _ => {}
326 }
327 }
328 Event::Mouse(me) => match me.kind {
329 MouseEventKind::Moved | MouseEventKind::Drag(_) => {
330 mouse.x = me.column as i32;
331 mouse.y = me.row as i32;
332 }
333 MouseEventKind::Down(btn) => {
334 use crossterm::event::MouseButton;
335 match btn {
336 MouseButton::Left => mouse.left = true,
337 MouseButton::Right => mouse.right = true,
338 MouseButton::Middle => {
339 mouse.strength = 12.0;
340 }
341 }
342 }
343 MouseEventKind::Up(btn) => {
344 use crossterm::event::MouseButton;
345 match btn {
346 MouseButton::Left => mouse.left = false,
347 MouseButton::Right => mouse.right = false,
348 MouseButton::Middle => {}
349 }
350 }
351 MouseEventKind::ScrollDown => {
352 mouse.strength = (mouse.strength - 2.0).max(4.0);
353 }
354 MouseEventKind::ScrollUp => {
355 mouse.strength = (mouse.strength + 2.0).min(40.0);
356 }
357 _ => {}
358 },
359 Event::Resize(c, rws) => {
360 r.handle_resize((c, rws))?;
361 }
362 _ => {}
363 }
364 }
365
366 let (cw, ch) = (cols as f64, rows as f64);
368 let t = start.elapsed().as_secs_f64();
369
370 for a in &mut agents {
371 let fx = ((a.pos.1 * 0.05 + t * 0.3).sin() * 0.8) + (t * 0.1 + a.wobble).cos() * 0.2;
372 let fy = ((a.pos.0 * 0.05 - t * 0.25).cos() * 0.8) + (t * 0.13).sin() * 0.2;
373
374 let mut steer_x = fx;
375 let mut steer_y = fy;
376
377 let dx = mouse.x as f64 - a.pos.0;
379 let dy = mouse.y as f64 - a.pos.1;
380 let d2 = (dx * dx + dy * dy).max(0.0001);
381 let inv = 1.0 / d2.sqrt();
382 let toward = (dx * inv, dy * inv);
383
384 if mouse.left {
385 steer_x += toward.0 * (mouse.strength * inv);
387 steer_y += toward.1 * (mouse.strength * inv);
388 }
389 if mouse.right {
390 steer_x -= toward.0 * (mouse.strength * inv);
392 steer_y -= toward.1 * (mouse.strength * inv);
393 }
394
395 let desired = steer_y.atan2(steer_x);
397 let mut diff = desired - a.dir;
398 while diff > std::f64::consts::PI {
399 diff -= std::f64::consts::TAU;
400 }
401 while diff < -std::f64::consts::PI {
402 diff += std::f64::consts::TAU;
403 }
404
405 a.dir += diff * 0.15 + (t * a.wobble).sin() * 0.02;
406 a.speed = lerp(a.speed, 1.2 + (t * 0.7 + a.wobble).sin().abs(), 0.05);
407
408 let step = a.speed;
410 a.pos.0 = wrap(a.pos.0 + a.dir.cos() * step, cw);
411 a.pos.1 = wrap(a.pos.1 + a.dir.sin() * step, ch);
412
413 let pts = triangle_points(a.pos.0, a.pos.1, a.dir, a.size);
415 r.replace_drawable_points(a.id, pts)?;
416 r.render_drawable(a.id)?;
417 }
418
419 r.render_drawable(rect_id)?;
420
421 let frame_preperation_duration = render_start.elapsed();
422
423 if show_hud {
424 let fps = 1.0 / frame_preperation_duration.as_secs_f64();
425 fps_vec.push(fps);
426
427 if time_since_last_fps_log.elapsed() > Duration::from_secs(5) {
428 let count = fps_vec.len();
429
430 if count > 0 {
431 let sum: f64 = fps_vec.iter().sum();
432 let avg_fps = sum / count as f64;
433 info!("Avg FPS over the last 5s: {:.2} ", avg_fps);
434 }
435 fps_vec.clear();
436 time_since_last_fps_log = Instant::now();
437 }
438
439 let txt = format!(
440 "time spent rendering frame: ~{fps:.1} | agents: {} | force: {:.1} | mouse: ({}, {}) L:{} R:{}",
441 agents.len(),
442 mouse.strength,
443 mouse.x,
444 mouse.y,
445 mouse.left as u8,
446 mouse.right as u8
447 );
448
449 let hud = text_drawable::TextDrawable {
450 area: Rect::from_coords(1, 1, 100, 6),
451 lines: vec![text_drawable::LineInfo {
452 text: txt,
453 spans: vec![],
454 alignment: text_drawable::TextAlignment::Left,
455 default_style: text_drawable::TextStyle {
456 foreground: Some(Color::White),
457 bold: Some(true),
458 ..Default::default()
459 },
460 }],
461 wrapping: true,
462 scroll_y: 0,
463 };
464 r.replace_drawable(hud_id, Box::new(hud))?;
465 r.render_drawable(hud_id)?;
466 }
467 main_member.wait_for_tick();
468 r.render_frame()?;
469 }
470
471 for a in &agents {
473 r.explicit_remove_drawable(&a.id)?;
474 }
475 r.explicit_remove_drawable(&video_id)?;
476
477 ltrender::restore_terminal()?;
478 Ok(())
479}
480
481use ltrender::error::DrawError;
482pub fn generate_sprites() -> Result<(), DrawError> {
483 let width = 2;
484 let height = 2;
485 let frame1 = vec![
486 TerminalChar {
487 chr: 'A',
488 fg_color: Some(Color {
489 rgb: (255, 0, 0),
490 reset: false,
491 }),
492 bg_color: None,
493 },
494 TerminalChar {
495 chr: 'B',
496 fg_color: None,
497 bg_color: Some(Color {
498 rgb: (0, 255, 0),
499 reset: false,
500 }),
501 },
502 TerminalChar {
503 chr: 'C',
504 fg_color: None,
505 bg_color: None,
506 },
507 TerminalChar {
508 chr: 'D',
509 fg_color: None,
510 bg_color: None,
511 },
512 ];
513
514 let frame2 = vec![
515 TerminalChar {
516 chr: 'E',
517 fg_color: None,
518 bg_color: None,
519 },
520 TerminalChar {
521 chr: 'F',
522 fg_color: None,
523 bg_color: None,
524 },
525 TerminalChar {
526 chr: 'G',
527 fg_color: None,
528 bg_color: None,
529 },
530 TerminalChar {
531 chr: 'H',
532 fg_color: None,
533 bg_color: None,
534 },
535 ];
536
537 let video = AsciiVideo {
538 width,
539 height,
540 frames: vec![
541 AsciiSprite {
542 pixels: frame1,
543 width,
544 height,
545 },
546 AsciiSprite {
547 pixels: frame2,
548 width,
549 height,
550 },
551 ],
552 };
553
554 let path = "assets/debugging/test_video.ascv";
555
556 video.write_to_file(path)?;
557 Ok(())
558}