use crate::color::set_alpha; use crate::component::arrow::Arrow; use crate::component::grid::Grid; use crate::component::image::Image; use crate::ctx_img; use crate::egui::{Align2, Context, TextureHandle, Vec2, Window}; use crate::fade_in::{fade_in, fade_in_manual}; use crate::governor::Governor; use crate::slide::Slide; use eframe::egui::style::Margin; use eframe::egui::{Color32, Frame, Ui}; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; /// Conway's Game of Life, etc. pub struct Automata { /// Persistent state of the grid. life: Grid, /// State of the transitions. state: AutomataState, /// For knowing when to advance the simulation. governor: Governor, /// John Conway portrait, loaded lazily. conway_portrait: Option<TextureHandle>, } #[derive(Default)] enum AutomataState { /// Nothing on the grid. #[default] Empty, /// Random colors on the grid, but no simulation. Static { /// When started fading in "alive" arrow. alive: Option<f64>, /// When started fading in "alive" arrow. dead: Option<f64>, /// When rules started fading in. rules: Option<f64>, }, /// Simulation. Life { /// Whether we started expanding the grid yet. expanding: bool, /// Conway iteration counter. iterations: usize, }, } impl Default for Automata { fn default() -> Self { let mut life = Grid::default(); // Unique identifier for the grid. life.name = "life"; Self { life, state: AutomataState::default(), governor: Governor::default(), conway_portrait: None, } } } impl Slide for Automata { fn transition(&mut self, ctx: &Context) -> bool { match &mut self.state { AutomataState::Empty => { self.state = AutomataState::Static { alive: None, dead: None, rules: None, }; // Seeded such that we get the same pattern every time, which is important since // we draw arrows to preset coordinates. let mut rng = ChaCha8Rng::seed_from_u64(123456789876543216); // Randomize grid. for y in 0..self.life.size_cells { for x in 0..self.life.size_cells { if rng.gen_bool(0.3) { self.life.set_fill(x, y, Color32::BLACK); } } } } AutomataState::Static { alive, dead, rules } => { // Enable arrows/text one by one. if alive.is_none() { *alive = Some(ctx.input().time); } else if dead.is_none() { *dead = Some(ctx.input().time); } else if rules.is_none() { *rules = Some(ctx.input().time) } else { self.state = AutomataState::Life { expanding: false, iterations: 0, }; } } AutomataState::Life { expanding, .. } => { if *expanding { // Done with the slide. return true; } else { *expanding = true; } } } false } fn show(&mut self, ui: &mut Ui) { Frame::none().margin(Margin::same(20.0)).show(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Cellular Automata"); }); }); // TODO: Fade/move/position as appropriate. Image::default().show( ui, self.conway_portrait .get_or_insert_with(|| ctx_img!(ui.ctx(), "conway_portrait.png")), ); // Need to continuously animate the grid, or at least poll the governor. ui.ctx().request_repaint(); // If [`None`], don't show rules. If [`Some`], fade in rules as if they started fading in // at this time. let mut rules_fade: Option<f64> = None; // How much grid goes left and rules go right. const HORIZONTAL_OFFSET: f32 = 250.0; match &mut self.state { AutomataState::Empty => {} AutomataState::Static { rules, .. } => { if let Some(rules) = *rules { let elapsed = ui.ctx().input().time - rules; self.life.offset_x = (elapsed as f32 * -400.0).max(-HORIZONTAL_OFFSET); rules_fade = Some(rules); } } AutomataState::Life { expanding, iterations, } => { // Iterate grid ~5 times a second. if self.governor.ready(ui.ctx().input().time, 0.2) { // Schedule some special events at some iteration counts. match *iterations { 10 => { // Automatically begin expansion. *expanding = true; } 20 => { for y in 4..self.life.size_cells - 4 { // Line on the left. self.life.set_fill(2, y, Color32::BLACK); // Line with gaps on the right. if y < self.life.size_cells - 11 && y % 8 != 0 { self.life .set_fill(self.life.size_cells - 2, y, Color32::BLACK); } } } 30 => { // Draw another set of lines to create a cool pattern. for x in 4..self.life.size_cells - 4 { // Solid line at the bottom. self.life .set_fill(x, self.life.size_cells - 2, Color32::BLACK); // Line with gaps at the top. if x % 6 != 0 { self.life.set_fill(x, 2, Color32::BLACK); } } } _ => {} } // Boolean to int cast yields 1 if true, 0 otherwise. let expansion = (*expanding && self.life.size_cells < 64) as usize; self.life = conways_game_of_life(&self.life, expansion); *iterations += 1; } // Kludge to render rules with full opacity. rules_fade = Some(0.0); } } if let Some(rules_fade) = rules_fade { // Give some time for the grid to slide left. const RULES_DELAY: f64 = 0.3; fade_in(ui, rules_fade + RULES_DELAY, |ui| { Window::new("conway_rules") .frame(Frame::window(&ui.style())) .anchor(Align2::CENTER_CENTER, Vec2::new(HORIZONTAL_OFFSET, 0.0)) .title_bar(false) .resizable(false) .show(ui.ctx(), |ui| { // Nested fade in since fade doesn't propagate from [`Window`] to children. fade_in(ui, rules_fade + RULES_DELAY, |ui| { ui.label(" ⏵ Cells with fewer than two neighbors die"); ui.label(" ⏵ Cells with more than three neighbors die"); ui.label(" ⏵ Cells with three neighbors become alive"); }) }); }); } self.life.show_with_overlays(ui.ctx(), |ui, to_screen| { if let &AutomataState::Static { alive, dead, .. } = &self.state { if let Some(alive) = alive { fade_in_manual(ui, alive, |ui, alpha| { let color = set_alpha(Color32::GREEN, alpha); Arrow { origin: to_screen * self.life.center(4, 2), tip: to_screen * self.life.center(7, 7), stroke: color, label: "Alive".into(), ..Arrow::default() } .show(ui); }); } if let Some(dead) = dead { fade_in_manual(ui, dead, |ui, alpha| { let color = set_alpha(Color32::DARK_GREEN, alpha); Arrow { origin: to_screen * self.life.center(11, 13), tip: to_screen * self.life.center(10, 10), stroke: color, label: "Dead".into(), ..Arrow::default() } .show(ui); }); } } }); } } /// Run one iteration of Conway's Game of Life on the grid, producing a new grid. /// /// A non-zero expansion parameter expands the grid on each side by that many squares. fn conways_game_of_life(grid: &Grid, expansion: usize) -> Grid { let mut new = grid.clone(); new.clear_fill(); // Allocate more room on left, right, top and bottom. new.size_cells += expansion * 2; for cy in 0..grid.size_cells { for cx in 0..grid.size_cells { let mut neighbors = 0; for dy in -1..=1 { for dx in -1..=1 { let (x, y) = match (cx.checked_add_signed(dx), cy.checked_add_signed(dy)) { (Some(x), Some(y)) if x < grid.size_cells && y < grid.size_cells => (x, y), _ => continue, }; if x == cx && y == cy { // Don't consider self. continue; } if grid.fill(x, y).a() > 0 { neighbors += 1; } } } let mut alive = grid.fill(cx, cy).a() > 0; alive = match (alive, neighbors) { (false, 3) => true, (false, _) => false, (true, 2) | (true, 3) => true, (true, _) => false, }; // Write back to the middle of the expanded area to avoid shift. new.set_fill( cx + expansion, cy + expansion, if alive { Color32::BLACK } else { Color32::TRANSPARENT }, ); } } new }