Skip to main content

swarm/
swarm.rs

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    // creating the tick manager for syncing the waves with the main swarm
191    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    // Optional: tiny sprite clock in the corner (if you have a .ascv around, otherwise harmless).
221    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    // HUD text (toggle with 'h').
259    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    // Seed agents.
285    let mut agents = seeded_swarm(&mut r, screen, 140, cols, rows);
286
287    // Input and timing state.
288    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        // Input ----------------------------------------------------
305        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                            // Reseed: drop old agents visually and create new ones.
312                            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        // Simulation ----------------------------------------------
367        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            // Mouse field
378            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                // attract
386                steer_x += toward.0 * (mouse.strength * inv);
387                steer_y += toward.1 * (mouse.strength * inv);
388            }
389            if mouse.right {
390                // repel
391                steer_x -= toward.0 * (mouse.strength * inv);
392                steer_y -= toward.1 * (mouse.strength * inv);
393            }
394
395            // Convert steer into angle change.
396            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            // Integrate position with gentle easing near mouse to make it feel alive.
409            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            // Push to renderer
414            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    // cleanup
472    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}