mod animations;
mod capture;
mod grid;
use std::thread;
use std::time::{Duration, Instant};
use rand::rngs::StdRng;
use rand::{rng, RngExt, SeedableRng};
use windows::Win32::UI::Input::KeyboardAndMouse::{
GetAsyncKeyState, VK_ESCAPE, VK_LEFT, VK_RIGHT, VK_SPACE,
};
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};
pub use animations::AnimationStyle;
use animations::AnimationController;
use capture::ScreenCapture;
use grid::GridCell;
#[derive(Debug, Clone, PartialEq)]
pub struct PartySettings {
pub grid_size: i32,
pub duration: Duration,
pub animation: AnimationStyle,
pub auto_cycle: Duration,
pub fps: u32,
pub cell_speed: f32,
pub seed: Option<u64>,
pub interactive: bool,
}
impl Default for PartySettings {
fn default() -> Self {
Self {
grid_size: 20,
duration: Duration::from_secs(120),
animation: AnimationStyle::Illuminati,
auto_cycle: Duration::ZERO,
fps: 60,
cell_speed: 0.2,
seed: None,
interactive: true,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PartyReport {
pub cells: usize,
pub styles_used: Vec<AnimationStyle>,
pub duration: Duration,
}
pub fn run(settings: &PartySettings) -> Result<PartyReport, String> {
let screen_width = unsafe { GetSystemMetrics(SM_CXSCREEN) };
let screen_height = unsafe { GetSystemMetrics(SM_CYSCREEN) };
let grid_size = settings.grid_size.clamp(2, 64);
let cell_width = screen_width / grid_size;
let cell_height = screen_height / grid_size;
if cell_width <= 0 || cell_height <= 0 {
return Err(format!(
"grid size {grid_size} too large for a {screen_width}x{screen_height} screen"
));
}
let mut rng: StdRng = match settings.seed {
Some(seed) => StdRng::seed_from_u64(seed),
None => StdRng::from_rng(&mut rng()),
};
let mut cells = Vec::with_capacity((grid_size * grid_size) as usize);
for y in 0..grid_size {
for x in 0..grid_size {
let start_x = x * cell_width;
let start_y = y * cell_height;
let capture = ScreenCapture::new(start_x, start_y, cell_width, cell_height)?;
let speed = settings.cell_speed.max(0.0) * rng.random_range(0.75..1.25);
cells.push(GridCell::new(capture, start_x, start_y, speed));
}
}
let mut controller = AnimationController::new(screen_width, screen_height, grid_size);
controller.current_style = settings.animation;
let frame_delay = Duration::from_secs_f64(1.0 / settings.fps.max(1) as f64);
let started = Instant::now();
let mut styles_used = vec![settings.animation];
let mut last_cycle = Instant::now();
let mut paused = false;
let mut last_key = Instant::now();
const KEY_DEBOUNCE: Duration = Duration::from_millis(200);
loop {
let elapsed = started.elapsed();
if elapsed >= settings.duration {
break;
}
if settings.interactive {
let key_debounce = last_key.elapsed() > KEY_DEBOUNCE;
unsafe {
if GetAsyncKeyState(VK_ESCAPE.0 as i32) as u16 & 0x8000 != 0 {
break;
}
if key_debounce && GetAsyncKeyState(VK_SPACE.0 as i32) as u16 & 0x8000 != 0 {
paused = !paused;
last_key = Instant::now();
println!("{}", if paused { "Paused" } else { "Resumed" });
}
if key_debounce && GetAsyncKeyState(VK_RIGHT.0 as i32) as u16 & 0x8000 != 0 {
let style = controller.next_style();
last_key = Instant::now();
push_unique(&mut styles_used, style);
println!("Switched to animation: {}", style.name());
}
if key_debounce && GetAsyncKeyState(VK_LEFT.0 as i32) as u16 & 0x8000 != 0 {
let style = controller.prev_style();
last_key = Instant::now();
push_unique(&mut styles_used, style);
println!("Switched to animation: {}", style.name());
}
}
}
if !settings.auto_cycle.is_zero() && last_cycle.elapsed() >= settings.auto_cycle {
let style = controller.next_style();
push_unique(&mut styles_used, style);
last_cycle = Instant::now();
println!("Auto-switched to animation: {}", style.name());
}
if !paused {
controller.apply_animation(&mut cells);
for cell in &mut cells {
cell.update();
}
controller.frame_count += 1;
}
thread::sleep(frame_delay);
}
for cell in &cells {
if let Err(e) = cell.reset() {
eprintln!("Failed to restore screen: {e}");
}
}
Ok(PartyReport {
cells: cells.len(),
styles_used,
duration: started.elapsed(),
})
}
fn push_unique(styles: &mut Vec<AnimationStyle>, style: AnimationStyle) {
if !styles.contains(&style) {
styles.push(style);
}
}