Newer
Older
use crate::ctx_img;
use crate::egui::{Align2, Context, TextureHandle, Vec2, Window};
use crate::fade_in::{fade_in, fade_in_manual};
/// 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>,
/// Whether we inverted the color and turned on fading yet.
rule_changed: bool,
/// Conway iteration counter.
iterations: usize,
},
fn transition(&mut self, ctx: &Context) -> bool {
match &mut self.state {
AutomataState::Empty => {
self.state = AutomataState::Static {
alive: None,
dead: None,
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)
AutomataState::Life { rule_changed, .. } => {
if !*rule_changed {
*rule_changed = true;
} else {
Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
ui.vertical_centered(|ui| {
ui.heading("Cellular Automata");
});
});
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.
// 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;
let elapsed = ui.ctx().input().time - rules;
self.life.offset_x = (elapsed as f32 * -400.0).max(-HORIZONTAL_OFFSET);
rules_fade = Some(rules);
}
}
// 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 {
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);
// Kludge to render rules with full opacity.
rules_fade = Some(0.0);
// Give some time for the grid to slide left.
const RULES_DELAY: f64 = 0.3;
fade_in(ui, rules_fade + RULES_DELAY, |ui| {
.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.
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");
})
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()
/// 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, alive_color: Color32, fade: bool) -> Grid {
// 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;
}
(false, 3) => true,
(false, _) => false,
(true, 2) | (true, 3) => true,
(true, _) => false,
};
// Write back to the middle of the expanded area to avoid shift.
} else {
// Fade out gradually.
let previous_alpha = grid.fill(cx, cy).a();
set_alpha(alive_color, previous_alpha as f32 / (255.0 * 2.0))
// 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);
}
}
}
}