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>,
Life {
/// Whether we started expanding the grid yet.
expanding: 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,
// 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)
self.state = AutomataState::Life {
expanding: false,
iterations: 0,
};
}
}
AutomataState::Life { expanding, .. } => {
if *expanding {
// Done with the slide.
return true;
} else {
*expanding = true;
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);
}
}
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);
// 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) -> Grid {
// Allocate more room on left, right, top and bottom.
new.size_cells += expansion * 2;
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
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.