From b23da0f09c39a9d6033cf89b83a5848d1613aacc Mon Sep 17 00:00:00 2001 From: Huck Boles Date: Mon, 19 Jun 2023 18:27:30 -0500 Subject: [PATCH] added: life canvas with pointer interaction --- src/lib.rs | 76 ++++++------ src/life.rs | 41 +++++-- src/map.rs | 328 +++++++++------------------------------------------ src/state.rs | 39 ++---- 4 files changed, 137 insertions(+), 347 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2be8d64..03d57cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,7 @@ mod state; use life::*; use map::*; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct Cell { i: isize, j: isize, @@ -48,10 +48,10 @@ impl Cell { #[derive(Default)] pub struct CellSeq { - grid: Map, + map: Map, is_playing: bool, queued_ticks: usize, - speed: usize, + bpm: usize, next_speed: Option, version: usize, } @@ -61,9 +61,11 @@ pub enum Message { Grid(map::Message, usize), Tick(Instant), TogglePlayback, - Next, + Randomize, + Reset, Clear, - SpeedChanged(f32), + Save, + SpeedChanged(usize), } impl Application for CellSeq { @@ -75,7 +77,7 @@ impl Application for CellSeq { fn new(_flags: ()) -> (Self, Command) { ( Self { - speed: 5, + bpm: 120, ..Self::default() }, Command::none(), @@ -90,15 +92,15 @@ impl Application for CellSeq { match message { Message::Grid(message, version) => { if version == self.version { - self.grid.update(message); + self.map.update(message); } } - Message::Tick(_) | Message::Next => { - self.queued_ticks = (self.queued_ticks + 1).min(self.speed); + Message::Tick(_) => { + self.queued_ticks = (self.queued_ticks + 1).min(self.bpm); - if let Some(task) = self.grid.tick(self.queued_ticks) { + if let Some(task) = self.map.tick(self.queued_ticks) { if let Some(speed) = self.next_speed.take() { - self.speed = speed; + self.bpm = speed; } self.queued_ticks = 0; @@ -112,16 +114,15 @@ impl Application for CellSeq { self.is_playing = !self.is_playing; } Message::Clear => { - self.grid.clear(); + self.map.clear(); self.version += 1; } - Message::SpeedChanged(speed) => { - if self.is_playing { - self.next_speed = Some(speed.round() as usize); - } else { - self.speed = speed.round() as usize; - } + Message::SpeedChanged(bpm) => { + self.bpm = bpm; } + Message::Randomize => self.map.randomize(), + Message::Reset => self.map.reset(), + Message::Save => self.map.save(), } Command::none() @@ -129,7 +130,7 @@ impl Application for CellSeq { fn subscription(&self) -> Subscription { if self.is_playing { - time::every(Duration::from_millis(1000 / self.speed as u64)).map(Message::Tick) + time::every(Duration::from_millis(60000 / self.bpm as u64)).map(Message::Tick) } else { Subscription::none() } @@ -137,15 +138,11 @@ impl Application for CellSeq { fn view(&self) -> Element { let version = self.version; - let selected_speed = self.next_speed.unwrap_or(self.speed); + let selected_speed = self.next_speed.unwrap_or(self.bpm); let controls = view_controls(self.is_playing, selected_speed); + let map = self.map.view().map(move |m| Message::Grid(m, version)); - let content = column![ - self.grid - .view() - .map(move |message| Message::Grid(message, version)), - controls, - ]; + let content = column![controls, map,]; container(content) .width(Length::Fill) @@ -158,18 +155,16 @@ impl Application for CellSeq { } } -fn view_controls<'a>(is_playing: bool, speed: usize) -> Element<'a, Message> { - let playback_controls = row![ - button(if is_playing { "Pause" } else { "Play" }).on_press(Message::TogglePlayback), - button("Next") - .on_press(Message::Next) - .style(theme::Button::Secondary), - ] - .spacing(10); +fn view_controls<'a>(is_playing: bool, bpm: usize) -> Element<'a, Message> { + let playback_controls = + row![button(if is_playing { "pause" } else { "play" }).on_press(Message::TogglePlayback),] + .spacing(10); let speed_controls = row![ - slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), - text(format!("x{speed}")).size(16), + slider(1.0..=1000.0, bpm as f32, |m| Message::SpeedChanged( + m.round() as usize + )), + text(format!("{bpm}")).size(16), ] .width(Length::Fill) .align_items(Alignment::Center) @@ -178,7 +173,14 @@ fn view_controls<'a>(is_playing: bool, speed: usize) -> Element<'a, Message> { row![ playback_controls, speed_controls, - button("Clear") + button("save").on_press(Message::Save), + button("reset") + .on_press(Message::Reset) + .style(theme::Button::Secondary), + button("random") + .on_press(Message::Randomize) + .style(theme::Button::Positive), + button("clear") .on_press(Message::Clear) .style(theme::Button::Destructive), ] diff --git a/src/life.rs b/src/life.rs index 1010819..f7489f4 100644 --- a/src/life.rs +++ b/src/life.rs @@ -1,17 +1,16 @@ use super::*; +use itertools::Itertools; +use rand::random; use rustc_hash::{FxHashMap, FxHashSet}; #[derive(Clone, Default)] pub struct Life { + seed: FxHashSet, cells: FxHashSet, } impl Life { - pub fn len(&self) -> usize { - self.cells.len() - } - pub fn contains(&self, cell: &Cell) -> bool { self.cells.contains(cell) } @@ -21,14 +20,36 @@ impl Life { } pub fn unpopulate(&mut self, cell: &Cell) { - let _ = self.cells.remove(cell); + self.cells.remove(cell); + } + + pub fn clear(&mut self) { + self.cells = FxHashSet::default(); + } + + pub fn reset(&mut self) { + self.cells = self.seed.clone(); + } + + pub fn save_state(&mut self) { + self.seed = self.cells.clone(); + } + + pub fn randomize(&mut self) { + self.cells.clear(); + for (i, j) in (-32..=32).cartesian_product(-32..=32) { + if random::() > 0.5 { + self.populate(Cell { i, j }) + } + } + self.seed = self.cells.clone(); } pub fn tick(&mut self) { let mut adjacent_life = FxHashMap::default(); for cell in &self.cells { - let _ = adjacent_life.entry(*cell).or_insert(0); + adjacent_life.entry(*cell).or_insert(0); for neighbor in Cell::neighbors(*cell) { let amount = adjacent_life.entry(neighbor).or_insert(0); @@ -41,10 +62,10 @@ impl Life { match amount { 2 => {} 3 => { - let _ = self.cells.insert(*cell); + self.cells.insert(*cell); } _ => { - let _ = self.cells.remove(cell); + self.cells.remove(cell); } } } @@ -57,8 +78,10 @@ impl Life { impl std::iter::FromIterator for Life { fn from_iter>(iter: I) -> Self { + let cells: FxHashSet = iter.into_iter().collect(); Life { - cells: iter.into_iter().collect(), + seed: cells.clone(), + cells, } } } diff --git a/src/map.rs b/src/map.rs index 5d87515..6e62a5f 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,39 +1,23 @@ -use iced::alignment; use iced::mouse; use iced::widget::canvas; use iced::widget::canvas::event::{self, Event}; -use iced::widget::canvas::{Cache, Canvas, Cursor, Frame, Geometry, Path, Text}; -use iced::{Color, Element, Length, Point, Rectangle, Size, Theme, Vector}; +use iced::widget::canvas::{Cache, Canvas, Cursor, Geometry, Path}; +use iced::{Color, Element, Length, Point, Rectangle, Size, Theme}; +use itertools::Itertools; use std::future::Future; -use std::ops::RangeInclusive; -use std::time::{Duration, Instant}; -use crate::{mask::Note, state::State, Cell, Life}; +use crate::{state::State, Cell, Life}; pub struct Map { state: State, life_cache: Cache, - mask_cache: Cache, - translation: Vector, - last_tick_duration: Duration, - last_queued_ticks: usize, } #[derive(Debug, Clone)] pub enum Message { Populate(Cell), Unpopulate(Cell), - Check(Cell), - Uncheck(Cell), - Ticked { - result: Result, - tick_duration: Duration, - }, -} - -#[derive(Debug, Clone)] -pub enum TickError { - JoinFailed, + Ticked(Life), } impl Default for Map { @@ -41,10 +25,6 @@ impl Default for Map { Self { state: State::with_life(Life::default()), life_cache: Cache::default(), - mask_cache: Cache::default(), - translation: Vector::default(), - last_tick_duration: Duration::default(), - last_queued_ticks: 0, } } } @@ -53,30 +33,11 @@ impl Map { pub fn tick(&mut self, amount: usize) -> Option> { let tick = self.state.tick(amount)?; - self.last_queued_ticks = amount; - - Some(async move { - let start = Instant::now(); - let result = tick.await; - let tick_duration = start.elapsed() / amount as u32; - - Message::Ticked { - result, - tick_duration, - } - }) + Some(async move { Message::Ticked(tick.await) }) } pub fn update(&mut self, message: Message) { match message { - Message::Check(cell) => { - self.state.check(cell); - self.mask_cache.clear(); - } - Message::Uncheck(cell) => { - self.state.uncheck(cell); - self.mask_cache.clear(); - } Message::Populate(cell) => { self.state.populate(cell); self.life_cache.clear(); @@ -85,285 +46,102 @@ impl Map { self.state.unpopulate(&cell); self.life_cache.clear(); } - Message::Ticked { - result: Ok(life), - tick_duration, - } => { + Message::Ticked(life) => { self.state.update(life); self.life_cache.clear(); - - self.last_tick_duration = tick_duration; - } - Message::Ticked { - result: Err(error), .. - } => { - dbg!(error); } } } pub fn view(&self) -> Element { Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) + .width(Length::Fixed(Cell::SIZE as f32 * 24.0)) + .height(Length::Fixed(Cell::SIZE as f32 * 24.0)) .into() } pub fn clear(&mut self) { - self.state = State::default(); + self.state.clear(); self.life_cache.clear(); } - fn visible_region(&self, size: Size) -> Region { - let width = size.width; - let height = size.height; - - Region { - x: -self.translation.x - width / 2.0, - y: -self.translation.y - height / 2.0, - width, - height, - } + pub fn reset(&mut self) { + self.state.reset(); } - fn project(&self, position: Point, size: Size, in_life: bool) -> Point { - let region = self.visible_region(size); - - let center = Point { - x: size.width / 2.0, - y: size.height / 2.0, - }; - - let translation = Vector { - x: 0.0, - y: if in_life { - center.y - center.y / 2.0 - } else { - center.y + center.y / 2.0 - }, - }; + pub fn save(&mut self) { + self.state.save(); + } - Point::new(position.x + region.x, position.y + region.y) + translation + pub fn randomize(&mut self) { + self.life_cache.clear(); + self.state.randomize(); } } impl canvas::Program for Map { - type State = Interaction; + type State = bool; fn update( &self, - interaction: &mut Interaction, + _interaction: &mut bool, event: Event, bounds: Rectangle, cursor: Cursor, ) -> (event::Status, Option) { - let center = bounds.center(); - - if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { - *interaction = Interaction::None; - } - - let cursor_position = if let Some(position) = cursor.position_in(&bounds) { - position - } else { - return (event::Status::Ignored, None); - }; - - let cell: Cell; - let action: Option; - let is_populated: bool; - - if cursor_position.y < center.y { - cell = Cell::at(self.project(cursor_position, bounds.size(), true)); - is_populated = self.state.contains(&cell); - - action = if is_populated { - Some(Message::Unpopulate(cell)) - } else { - Some(Message::Populate(cell)) - }; - } else { - cell = Cell::at(self.project(cursor_position, bounds.size(), false)); - is_populated = self.state.mask_contains(&cell); - - action = if is_populated { - Some(Message::Uncheck(cell)) - } else { - Some(Message::Check(cell)) - }; - } - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - *interaction = if is_populated { - Interaction::Erasing - } else { - Interaction::Drawing - }; - (event::Status::Captured, action) + if let Some(pos) = cursor.position_in(&bounds) { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = event { + let location = Point { x: pos.x, y: pos.y }; + + let cell = Cell::at(location); + return ( + event::Status::Captured, + if self.state.contains(&cell) { + Some(Message::Unpopulate(cell)) + } else { + Some(Message::Populate(cell)) + }, + ); } - _ => (event::Status::Ignored, None), } + + (event::Status::Ignored, None) } fn draw( &self, - _interaction: &Interaction, + _interaction: &bool, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); - - let split_half = Size::new(bounds.width, bounds.height / 2.0); - - let life_canvas = Rectangle::new(bounds.position(), split_half); - let mask_canvas = - Rectangle::new(bounds.position() + Vector::new(0.0, center.y), split_half); - - let life = self.life_cache.draw(life_canvas.size(), |frame| { - let life_center = Vector { - x: bounds.x + life_canvas.center().x, - y: bounds.y + life_canvas.center().y, - }; - let background = Path::rectangle(Point::ORIGIN, frame.size()); - frame.fill(&background, Color::from_rgb8(0xA0, 0x44, 0x4B)); - - frame.with_save(|frame| { - frame.translate(life_center); - frame.scale(Cell::SIZE as f32); - - let region = self.visible_region(frame.size()); - - for cell in region.cull(self.state.cells()) { - frame.fill_rectangle( - Point::new(cell.j as f32, cell.i as f32), - Size::UNIT, - Color::WHITE, - ); - } - }); - }); - - let mask = self.mask_cache.draw(mask_canvas.size(), |frame| { - let mask_center = Vector { - x: bounds.x + life_canvas.center().x, - y: bounds.y + life_canvas.center().y, - }; + vec![self.life_cache.draw(bounds.size(), |frame| { let background = Path::rectangle(Point::ORIGIN, frame.size()); - frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0xA0)); + frame.fill(&background, Color::from_rgb8(0x2E, 0x26, 0x2D)); frame.with_save(|frame| { - frame.translate(mask_center); frame.scale(Cell::SIZE as f32); - let region = self.visible_region(frame.size()); - - for (cell, _) in region.cull_mask(self.state.mask()) { - frame.fill_rectangle( - Point::new(cell.j as f32, cell.i as f32), - Size::UNIT, - Color::WHITE, - ); - } + (0..24) + .cartesian_product(0..24) + .filter(|(i, j)| self.state.contains(&Cell { i: *i, j: *j })) + .for_each(|(i, j)| { + frame.fill_rectangle( + Point::new(j as f32, i as f32), + Size::UNIT, + Color::WHITE, + ); + }) }); - }); - - let overlay = { - let mut frame = Frame::new(bounds.size()); - - let text = Text { - color: Color::WHITE, - size: 14.0, - position: Point::new(frame.width(), frame.height()), - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Bottom, - ..Text::default() - }; - - let cell_count = self.state.cell_count(); - - frame.fill_text(Text { - content: format!( - "{} cell{} @ {:?} ({})", - cell_count, - if cell_count == 1 { "" } else { "s" }, - self.last_tick_duration, - self.last_queued_ticks - ), - ..text - }); - - frame.into_geometry() - }; - - vec![overlay, mask, life] + })] } fn mouse_interaction( &self, - interaction: &Interaction, - bounds: Rectangle, - cursor: Cursor, + _interaction: &bool, + _bounds: Rectangle, + _cursor: Cursor, ) -> mouse::Interaction { - match interaction { - Interaction::Drawing => mouse::Interaction::Crosshair, - Interaction::Erasing => mouse::Interaction::Crosshair, - Interaction::None if cursor.is_over(&bounds) => mouse::Interaction::Crosshair, - _ => mouse::Interaction::default(), - } - } -} -pub struct Region { - x: f32, - y: f32, - width: f32, - height: f32, -} - -impl Region { - fn rows(&self) -> RangeInclusive { - let first_row = (self.y / Cell::SIZE as f32).floor() as isize; - - let visible_rows = (self.height / Cell::SIZE as f32).ceil() as isize; - - first_row..=first_row + visible_rows - } - - fn columns(&self) -> RangeInclusive { - let first_column = (self.x / Cell::SIZE as f32).floor() as isize; - - let visible_columns = (self.width / Cell::SIZE as f32).ceil() as isize; - - first_column..=first_column + visible_columns - } - - fn cull<'a>(&self, cells: impl Iterator) -> impl Iterator { - let rows = self.rows(); - let columns = self.columns(); - - cells.filter(move |cell| rows.contains(&cell.i) && columns.contains(&cell.j)) - } - - fn cull_mask<'a>( - &self, - cells: impl Iterator, - ) -> impl Iterator { - let rows = self.rows(); - let columns = self.columns(); - - cells.filter(move |(cell, _)| rows.contains(&cell.i) && columns.contains(&cell.j)) - } -} - -pub enum Interaction { - None, - Drawing, - Erasing, -} - -impl Default for Interaction { - fn default() -> Self { - Self::None + mouse::Interaction::default() } } diff --git a/src/state.rs b/src/state.rs index 850b499..dcb2375 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,8 +1,4 @@ -use crate::{ - life::Life, - mask::{Mask, Note}, - Cell, TickError, -}; +use crate::{life::Life, Cell}; use rustc_hash::FxHashSet; use std::future::Future; @@ -10,7 +6,6 @@ use std::future::Future; #[derive(Default)] pub struct State { life: Life, - mask: Mask, births: FxHashSet, is_ticking: bool, } @@ -23,32 +18,25 @@ impl State { } } - pub fn cell_count(&self) -> usize { - self.life.len() + self.births.len() - } - pub fn contains(&self, cell: &Cell) -> bool { self.life.contains(cell) || self.births.contains(cell) } - pub fn mask_contains(&self, cell: &Cell) -> bool { - self.mask.contains(cell) + pub fn randomize(&mut self) { + self.life.randomize() } - pub fn cells(&self) -> impl Iterator { - self.life.iter().chain(self.births.iter()) + pub fn clear(&mut self) { + self.life.clear(); } - pub fn mask(&self) -> impl Iterator { - self.mask.iter() + pub fn save(&mut self) { + self.life.save_state(); } - pub fn check(&mut self, cell: Cell) { - self.mask.check(cell); - } - - pub fn uncheck(&mut self, cell: Cell) { - self.mask.uncheck(cell); + pub fn reset(&mut self) { + self.life.clear(); + self.life.reset(); } pub fn populate(&mut self, cell: Cell) { @@ -68,13 +56,13 @@ impl State { } pub fn update(&mut self, mut life: Life) { - self.births.drain().for_each(|cell| life.populate(cell)); + // self.births.drain().for_each(|cell| life.populate(cell)); self.life = life; self.is_ticking = false; } - pub fn tick(&mut self, amount: usize) -> Option>> { + pub fn tick(&mut self, amount: usize) -> Option> { if self.is_ticking { return None; } @@ -88,11 +76,10 @@ impl State { for _ in 0..amount { life.tick(); } - life }) .await - .map_err(|_| TickError::JoinFailed) + .unwrap() }) } } -- 2.45.2