use crate::color::{get_style_alpha, set_alpha}; use crate::component::arrow::Arrow; use crate::component::grid::Grid; use crate::egui::{Align2, Context, Vec2, Window}; use crate::fade_in::{fade_in, fade_in_manual}; use crate::governor::Governor; use crate::slide::Slide; use crate::window::WindowPosition; 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, } enum AutomataState { /// 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 (each) started fading in. rules: [Option<f64>; 3], }, /// Simulation. Life { /// Whether we inverted the color and turned on fading yet. rule_changed: 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"; randomize_grid(&mut life); Self { life, state: AutomataState::Static { alive: None, dead: None, rules: [None; 3], }, governor: Governor::default(), } } } impl Slide for Automata { fn transition(&mut self, ctx: &Context) -> bool { match &mut self.state { 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.iter().any(|f| f.is_none()) { // Fade in the rules one by one. for fade_in in rules { if fade_in.is_none() { *fade_in = Some(ctx.input().time); break; } } } else { self.state = AutomataState::Life { rule_changed: false, iterations: 0, }; } } AutomataState::Life { rule_changed, .. } => { if !*rule_changed { *rule_changed = true; } else { // Done with the slide. return 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"); }); }); // 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>; 3] = [None; 3]; // How much grid goes left and rules go right. const HORIZONTAL_OFFSET: f32 = 250.0; match &mut self.state { AutomataState::Static { rules, .. } => { if let Some(first_rule_fade_start) = rules[0] { let elapsed = ui.ctx().input().time - first_rule_fade_start; self.life.position = WindowPosition::FromCenter(Vec2::new( (elapsed as f32 * -400.0).max(-HORIZONTAL_OFFSET), 0.0, )); rules_fade = *rules; } } AutomataState::Life { rule_changed, 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. let expanding = *iterations >= 10; match *iterations { 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; // Implement rule changes. let (background_color, alive_color, stroke_color, fade) = if *rule_changed { (Color32::BLACK, Color32::WHITE, Color32::TRANSPARENT, true) } else { (Color32::TRANSPARENT, Color32::BLACK, Color32::GRAY, false) }; self.life.background = background_color; self.life.stroke = stroke_color; self.life = conways_game_of_life(&self.life, expansion, alive_color, fade); *iterations += 1; } // Kludge to render rules with full opacity. rules_fade = [Some(0.0); 3]; } } if let Some(first_rule_fade_start) = rules_fade[0] { // Give some time for the grid to slide left. const RULES_DELAY: f64 = 0.3; fade_in(ui, first_rule_fade_start + 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| { let rules = [ "Cells with fewer than two neighbors die", "Cells with more than three neighbors die", "Cells with three neighbors become alive", ]; for (i, (rule, fade_start)) in rules.into_iter().zip(rules_fade).enumerate() { // Infinity is a kludge to make it invisible by still take up space. fade_in( ui, fade_start.unwrap_or(f64::INFINITY) + if i == 0 { RULES_DELAY } else { 0.0 }, |ui| { ui.label(format!(" ⏵ {}", rule)); }, ); } }); }); } // Hack to fade in stuff along with the slide. let inherent_alpha = get_style_alpha(ui.style()); self.life.alpha = inherent_alpha; 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. pub fn conways_game_of_life( grid: &Grid, expansion: usize, alive_color: Color32, fade: bool, ) -> 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).is_opaque() { neighbors += 1; } } } let was_alive = grid.fill(cx, cy).is_opaque(); let alive = match (was_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 { alive_color } else if !fade { Color32::TRANSPARENT } else { // Fade out gradually. let previous_alpha = grid.fill(cx, cy).a(); set_alpha(alive_color, previous_alpha as f32 / (255.0 * 2.0)) }, ); } } new } // Sets a deterministic subset of the grid (approximately 30% of it) to black. pub fn randomize_grid(grid: &mut Grid) { // 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..grid.size_cells { for x in 0..grid.size_cells { if rng.gen_bool(0.3) { grid.set_fill(x, y, Color32::BLACK); } } } }