]> git.huck.website - cellseq.git/commitdiff
added: life canvas with pointer interaction
authorHuck Boles <huck@huck.website>
Mon, 19 Jun 2023 23:27:30 +0000 (18:27 -0500)
committerHuck Boles <huck@huck.website>
Mon, 19 Jun 2023 23:27:30 +0000 (18:27 -0500)
src/lib.rs
src/life.rs
src/map.rs
src/state.rs

index 2be8d64f103300d67b6cf1c9fe6721471072f944..03d57cf696a187c4ebad049c5a18dce2927bda1c 100644 (file)
@@ -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<usize>,
     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<Message>) {
         (
             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<Message> {
         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<Message> {
         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),
     ]
index 101081932074aa22040867640146e40db7a175b6..f7489f4677409e59eccd4355fe0e73b7fba64ee8 100644 (file)
@@ -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<Cell>,
     cells: FxHashSet<Cell>,
 }
 
 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::<f32>() > 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<Cell> for Life {
     fn from_iter<I: IntoIterator<Item = Cell>>(iter: I) -> Self {
+        let cells: FxHashSet<Cell> = iter.into_iter().collect();
         Life {
-            cells: iter.into_iter().collect(),
+            seed: cells.clone(),
+            cells,
         }
     }
 }
index 5d87515bb28557fe5bd4fc6b64cf4d1d83a6331c..6e62a5f80d0ba961d82a74521b991cb4794eb9e0 100644 (file)
@@ -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<Life, TickError>,
-        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<impl Future<Output = Message>> {
         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<Message> {
         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<Message> 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<Message>) {
-        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<Message>;
-        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<Geometry> {
-        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<isize> {
-        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<isize> {
-        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<Item = &'a Cell>) -> impl Iterator<Item = &'a Cell> {
-        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<Item = (&'a Cell, &'a Note)>,
-    ) -> impl Iterator<Item = (&'a Cell, &'a Note)> {
-        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()
     }
 }
index 850b499cd591a14c0bf4ebb1e728f7ed1d148d9d..dcb237529b0016acee6f692507baa2bfe73306e9 100644 (file)
@@ -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<Cell>,
     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<Item = &Cell> {
-        self.life.iter().chain(self.births.iter())
+    pub fn clear(&mut self) {
+        self.life.clear();
     }
 
-    pub fn mask(&self) -> impl Iterator<Item = (&Cell, &Note)> {
-        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<impl Future<Output = Result<Life, TickError>>> {
+    pub fn tick(&mut self, amount: usize) -> Option<impl Future<Output = Life>> {
         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()
         })
     }
 }