]> git.huck.website - cellseq.git/commitdiff
added: map mask interaction
authorHuck Boles <huck@huck.website>
Wed, 21 Jun 2023 23:13:03 +0000 (18:13 -0500)
committerHuck Boles <huck@huck.website>
Wed, 21 Jun 2023 23:13:03 +0000 (18:13 -0500)
src/lib.rs
src/map.rs
src/mask.rs

index 814651b13c6fddbd208c74c190d62c67e726490a..ac87d2b70c6b7d91c6b1e74416e85f2db4495cf2 100644 (file)
@@ -1,7 +1,7 @@
 use iced::executor;
 use iced::theme::{self, Theme};
 use iced::time;
-use iced::widget::{button, column, container, row, slider, text};
+use iced::widget::{button, column, container, row, text};
 use iced::{Alignment, Application, Command, Element, Length, Point, Subscription};
 
 use rustc_hash::FxHashSet;
@@ -54,6 +54,9 @@ pub struct CellSeq {
     mask: Mask,
     is_playing: bool,
     bpm: usize,
+    is_looping: bool,
+    loop_len: usize,
+    step_num: usize,
 }
 
 #[derive(Debug, Clone)]
@@ -61,12 +64,14 @@ pub enum Message {
     Map(map::Message),
     Mask(mask::Message),
     Tick(Instant),
-    TogglePlayback,
     Randomize,
     Reset,
     Clear,
     Save,
+    TogglePlayback,
     SpeedChanged(usize),
+    ToggleLoop,
+    LoopLength(usize),
 }
 
 impl Application for CellSeq {
@@ -79,6 +84,7 @@ impl Application for CellSeq {
         (
             Self {
                 bpm: 120,
+                loop_len: 16,
                 ..Self::default()
             },
             Command::none(),
@@ -94,18 +100,35 @@ impl Application for CellSeq {
             Message::Map(message) => self.map.update(message),
             Message::Mask(message) => self.mask.update(message),
             Message::Tick(_) => {
-                return Command::perform(self.map.tick(), Message::Map);
+                let life = if self.step_num == self.loop_len && self.is_looping {
+                    self.step_num = 0;
+                    self.map.reset_loop()
+                } else {
+                    self.step_num += 1;
+                    self.map.tick()
+                };
+
+                self.map.update(map::Message::Ticked(life.clone()));
+                self.mask.update(mask::Message::Tick(life));
             }
             Message::TogglePlayback => {
                 self.is_playing = !self.is_playing;
+                if self.is_playing {
+                    self.map.save()
+                }
             }
-            Message::SpeedChanged(bpm) => {
-                self.bpm = bpm;
-            }
+            Message::SpeedChanged(bpm) => self.bpm = bpm,
             Message::Clear => self.map.clear(),
             Message::Randomize => self.map.randomize(),
             Message::Reset => self.map.reset(),
             Message::Save => self.map.save(),
+            Message::ToggleLoop => {
+                self.is_looping = !self.is_looping;
+                if self.is_looping {
+                    self.step_num = 0;
+                }
+            }
+            Message::LoopLength(len) => self.loop_len = len,
         }
 
         Command::none()
@@ -120,14 +143,21 @@ impl Application for CellSeq {
     }
 
     fn view(&self) -> Element<Message> {
-        let bpm = self.bpm;
-        let controls = view_controls(self.is_playing, bpm);
+        let controls = view_controls(
+            self.is_playing,
+            self.bpm,
+            self.is_looping,
+            self.loop_len,
+            self.step_num,
+        );
         let map = row![
             self.map.view().map(Message::Map),
             self.mask.view().map(Message::Mask)
         ]
+        .align_items(Alignment::Center)
         .width(Length::Fill)
-        .spacing(40);
+        .spacing(40)
+        .padding(20);
 
         let content = column![controls, map,];
 
@@ -142,24 +172,38 @@ impl Application for CellSeq {
     }
 }
 
-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);
+fn view_controls<'a>(
+    is_playing: bool,
+    bpm: usize,
+    is_looping: bool,
+    loop_len: usize,
+    step_num: usize,
+) -> Element<'a, Message> {
+    let playback_controls = row![
+        button(if is_playing { "pause" } else { "play" }).on_press(Message::TogglePlayback),
+        button(if is_looping { "free" } else { "loop" }).on_press(Message::ToggleLoop),
+        button("-").on_press(Message::LoopLength(loop_len.saturating_sub(1))),
+        text(if is_looping {
+            format!("{step_num}/{loop_len}")
+        } else {
+            format!("{loop_len}")
+        }),
+        button("+").on_press(Message::LoopLength(loop_len.saturating_add(1)))
+    ]
+    .spacing(10);
 
     let speed_controls = row![
-        slider(1.0..=1000.0, bpm as f32, |m| Message::SpeedChanged(
-            m.round() as usize
-        )),
+        button("<<").on_press(Message::SpeedChanged(bpm.saturating_sub(5))),
+        button("<").on_press(Message::SpeedChanged(bpm.saturating_sub(1))),
         text(format!("{bpm}")).size(16),
+        button(">").on_press(Message::SpeedChanged(bpm.saturating_add(1))),
+        button(">>").on_press(Message::SpeedChanged(bpm.saturating_add(1))),
     ]
     .width(Length::Fill)
     .align_items(Alignment::Center)
     .spacing(10);
 
-    row![
-        playback_controls,
-        speed_controls,
+    let other_controls = row![
         button("save").on_press(Message::Save),
         button("reset")
             .on_press(Message::Reset)
@@ -171,8 +215,13 @@ fn view_controls<'a>(is_playing: bool, bpm: usize) -> Element<'a, Message> {
             .on_press(Message::Clear)
             .style(theme::Button::Destructive),
     ]
-    .padding(10)
-    .spacing(20)
+    .width(Length::Fill)
     .align_items(Alignment::Center)
-    .into()
+    .spacing(10);
+
+    row![playback_controls, speed_controls, other_controls]
+        .padding(10)
+        .spacing(40)
+        .align_items(Alignment::Center)
+        .into()
 }
index 7bb1060f798394c84dc9a40061425cb1c7ec77bb..2cc07ff3463343104a7e84adb175914576669a62 100644 (file)
@@ -11,7 +11,7 @@ use super::*;
 use itertools::Itertools;
 use rand::random;
 use rustc_hash::FxHashMap;
-use std::{fmt::Debug, future::Future};
+use std::fmt::Debug;
 
 #[derive(Default, Debug)]
 pub struct Map {
@@ -28,37 +28,37 @@ pub enum Message {
 }
 
 impl Map {
-    pub fn tick(&self) -> impl Future<Output = Message> {
+    pub fn reset_loop(&mut self) -> CellMap {
+        self.seed.clone()
+    }
+
+    pub fn tick(&self) -> CellMap {
         let mut life = self.cells.clone();
         let mut counts = FxHashMap::default();
 
-        let tick = tokio::task::spawn_blocking(move || {
-            for cell in &life {
-                counts.entry(*cell).or_insert(0);
+        for cell in &life {
+            counts.entry(*cell).or_insert(0);
 
-                for neighbor in Cell::neighbors(*cell) {
-                    let amount = counts.entry(neighbor).or_insert(0);
+            for neighbor in Cell::neighbors(*cell) {
+                let amount = counts.entry(neighbor).or_insert(0);
 
-                    *amount += 1;
-                }
+                *amount += 1;
             }
+        }
 
-            for (cell, amount) in counts.iter() {
-                match amount {
-                    2 => {}
-                    3 => {
-                        life.insert(*cell);
-                    }
-                    _ => {
-                        life.remove(cell);
-                    }
+        for (cell, amount) in counts.iter() {
+            match amount {
+                2 => {}
+                3 => {
+                    life.insert(*cell);
+                }
+                _ => {
+                    life.remove(cell);
                 }
             }
+        }
 
-            life
-        });
-
-        async move { Message::Ticked(tick.await.unwrap()) }
+        life
     }
 
     pub fn update(&mut self, message: Message) {
@@ -109,10 +109,6 @@ impl Map {
         self.seed = self.cells.clone();
         self.life_cache.clear();
     }
-
-    pub fn contains(&self, cell: &Cell) -> bool {
-        self.cells.contains(cell)
-    }
 }
 
 impl Program<Message> for Map {
@@ -130,7 +126,7 @@ impl Program<Message> for Map {
                 let cell = Cell::at(position);
                 return (
                     event::Status::Captured,
-                    if self.contains(&cell) {
+                    if self.cells.contains(&cell) {
                         Some(Message::Unpopulate(cell))
                     } else {
                         Some(Message::Populate(cell))
@@ -158,7 +154,7 @@ impl Program<Message> for Map {
 
                 (0..24)
                     .cartesian_product(0..24)
-                    .filter(|x| self.contains(&Cell { i: x.1, j: x.0 }))
+                    .filter(|x| self.cells.contains(&Cell { i: x.1, j: x.0 }))
                     .for_each(|x| {
                         frame.fill_rectangle(
                             Point::new(x.0 as f32, x.1 as f32),
index 2948952cf01dbdf79c6a88a005c984313109b9d2..99ad8f1cbfe99745f3c4ebe65a2dd319f662c1b1 100644 (file)
@@ -28,7 +28,23 @@ pub enum State {
 #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub struct Note {
     value: usize,
-    action: State,
+    state: State,
+}
+
+impl Note {
+    pub fn is_on(&self) -> bool {
+        match self.state {
+            State::On => true,
+            State::Off => false,
+        }
+    }
+
+    pub fn switch(&mut self) {
+        self.state = match self.state {
+            State::On => State::Off,
+            State::Off => State::On,
+        }
+    }
 }
 
 #[derive(Default, Debug)]
@@ -38,31 +54,34 @@ pub struct Mask {
 }
 
 impl Mask {
-    pub fn contains(&self, cell: &Cell) -> bool {
-        self.cells.contains_key(cell)
-    }
-
-    pub fn check(&mut self, cell: Cell) {
-        self.cells.insert(cell, Note::default());
-    }
-
-    pub fn uncheck(&mut self, cell: Cell) {
-        let _ = self.cells.remove(&cell);
-    }
-
-    pub fn get_note(&self, cell: &Cell) -> Option<&Note> {
-        self.cells.get(cell)
-    }
-
-    pub fn cells(&self) -> impl Iterator<Item = &Cell> {
-        self.cells.keys()
-    }
-
     pub fn update(&mut self, message: Message) {
         match message {
-            Message::Check(cell) => self.check(cell),
-            Message::Uncheck(cell) => self.uncheck(cell),
-            Message::Tick(life) => {}
+            Message::Check(cell) => {
+                self.cells.insert(cell, Note::default());
+                self.mask_cache.clear()
+            }
+            Message::Uncheck(cell) => {
+                self.cells.remove(&cell);
+                self.mask_cache.clear();
+            }
+            Message::Tick(life) => {
+                for cell in life.iter() {
+                    if self.cells.contains_key(cell) {
+                        let note = self.cells.entry(*cell).or_default();
+                        note.switch()
+                    }
+                }
+
+                self.mask_cache.clear();
+            }
+        }
+    }
+
+    pub fn is_on(&self, cell: &Cell) -> bool {
+        if let Some(note) = self.cells.get(cell) {
+            note.is_on()
+        } else {
+            false
         }
     }
 
@@ -92,13 +111,15 @@ impl Program<Message> for Mask {
 
                 (0..24)
                     .cartesian_product(0..24)
-                    .filter(|x| self.contains(&Cell { i: x.1, j: x.0 }))
+                    .filter(|x| self.cells.contains_key(&Cell { i: x.1, j: x.0 }))
                     .for_each(|x| {
-                        frame.fill_rectangle(
-                            Point::new(x.0 as f32, x.1 as f32),
-                            Size::UNIT,
-                            Color::WHITE,
-                        );
+                        let color = if self.is_on(&Cell { i: x.0, j: x.1 }) {
+                            Color::from_rgb8(0xF0, 0xC0, 0xC0)
+                        } else {
+                            Color::WHITE
+                        };
+
+                        frame.fill_rectangle(Point::new(x.0 as f32, x.1 as f32), Size::UNIT, color);
                     })
             });
         })]
@@ -116,7 +137,7 @@ impl Program<Message> for Mask {
                 let cell = Cell::at(position);
                 return (
                     event::Status::Captured,
-                    if self.contains(&cell) {
+                    if self.cells.contains_key(&cell) {
                         Some(Message::Uncheck(cell))
                     } else {
                         Some(Message::Check(cell))