]> git.huck.website - cellseq.git/commitdiff
edited: improving gui interface
authorHuck Boles <huck@huck.website>
Fri, 7 Jul 2023 01:00:53 +0000 (20:00 -0500)
committerHuck Boles <huck@huck.website>
Fri, 7 Jul 2023 01:00:53 +0000 (20:00 -0500)
src/display.rs
src/lib.rs
src/mask.rs
src/midi.rs
src/music.rs

index eb9021b4ced157f74aca45e1c47dd3bb26165d53..948400201f53cf951bc2328adf21372b86c5d610 100644 (file)
@@ -1,6 +1,8 @@
 use iced::{
     theme,
-    widget::{button, column, pick_list, row, slider, text, Column, Row},
+    widget::{
+        button, checkbox, column, pick_list, row, slider, text, vertical_slider, Column, Row,
+    },
     Alignment, Element, Length,
 };
 
@@ -19,11 +21,17 @@ pub struct ControlMessage {
 pub fn top_controls<'a>(is_playing: bool) -> Element<'a, Message> {
     let play_button = row![
         button(if is_playing { "stop" } else { "play" }).on_press(Message::TogglePlayback),
-        button("save")
+        button("save map")
             .on_press(Message::Save)
             .style(theme::Button::Positive),
-        button("clear")
-            .on_press(Message::Clear)
+        button("reset map")
+            .on_press(Message::Reset)
+            .style(theme::Button::Secondary),
+        button("clear map")
+            .on_press(Message::ClearMap)
+            .style(theme::Button::Destructive),
+        button("clear mask")
+            .on_press(Message::ClearMask)
             .style(theme::Button::Destructive),
     ]
     .width(Length::Fill)
@@ -33,6 +41,7 @@ pub fn top_controls<'a>(is_playing: bool) -> Element<'a, Message> {
         .on_press(Message::Quit)
         .style(theme::Button::Destructive),]
     .width(Length::Fill)
+    .align_items(Alignment::End)
     .spacing(10);
 
     row![play_button, other_controls]
@@ -45,8 +54,9 @@ pub fn top_controls<'a>(is_playing: bool) -> Element<'a, Message> {
 pub fn bottom_controls<'a>(message: ControlMessage) -> Element<'a, Message> {
     column![
         map_section(&message),
-        probability_section(message.info.probability).width(Length::Fixed(600.0)),
-        midi_section(&message),
+        probability_section(&message).width(Length::Fixed(600.0)),
+        scale_selector(&message),
+        music_controls(&message)
     ]
     .width(Length::Fill)
     .height(Length::Fill)
@@ -56,180 +66,201 @@ pub fn bottom_controls<'a>(message: ControlMessage) -> Element<'a, Message> {
     .into()
 }
 
-fn map_section<'a>(message: &ControlMessage) -> Row<'a, Message> {
+fn music_controls<'a>(message: &ControlMessage) -> Row<'a, Message> {
     row![
-        map_buttons(),
-        randomize_section(message.randomness).width(Length::Fixed(500.0))
+        song_section(message),
+        midi_section(message),
+        velocity_sliders(message)
     ]
-    .spacing(10)
+    .height(Length::Fill)
+    .padding(10)
+    .spacing(40)
+    .into()
 }
 
-fn probability_section<'a>(p: f32) -> Row<'a, Message> {
-    row![
-        text("probability a cell gets triggered")
-            .style(theme::Text::Color(iced::Color::from_rgb8(0x60, 0x60, 0x60))),
-        slider(0.0..=100.0, p * 100.0, |x| {
-            Message::ProbChanged(x / 100.0)
-        }),
-        text(format!("{p}")),
-    ]
-    .spacing(10)
+fn song_section<'a>(message: &ControlMessage) -> Row<'a, Message> {
+    row![song_params(), song_vals(message)]
+        .height(Length::Fill)
+        .padding(10)
+        .spacing(20)
+        .into()
 }
 
-fn randomize_section<'a>(r: f32) -> Row<'a, Message> {
-    row![
-        text("percent of board to fill on randomize")
-            .style(theme::Text::Color(iced::Color::from_rgb8(0x60, 0x60, 0x60))),
-        slider(0.0..=100.0, r * 100.0, |x| {
-            Message::RandChanged(x / 100.0)
-        }),
-        text(format!("{r}")),
-    ]
-    .spacing(10)
+fn midi_section<'a>(message: &ControlMessage) -> Row<'a, Message> {
+    row![midi_params(), midi_vals(message)]
+        .height(Length::Fill)
+        .padding(10)
+        .spacing(20)
+        .into()
 }
 
-fn map_buttons<'a>() -> Column<'a, Message> {
-    column![row![
-        button("reset")
-            .on_press(Message::Reset)
-            .style(theme::Button::Secondary),
-        button("random")
-            .on_press(Message::Randomize)
-            .style(theme::Button::Primary),
+fn song_params<'a>() -> Column<'a, Message> {
+    column![
+        text("loop section"),
+        text("number of steps"),
+        text("bpm"),
+        text("note division"),
     ]
-    .spacing(10)]
-    .spacing(10)
+    .height(Length::Fill)
+    .padding(10)
+    .spacing(20)
+    .into()
 }
 
-fn velocity_sliders<'a>(min: u8, max: u8) -> Column<'a, Message> {
+fn song_vals<'a>(message: &ControlMessage) -> Column<'a, Message> {
     column![
+        checkbox("", message.song.is_looping, |_| { Message::ToggleLoop }),
         row![
-            text("maximum velocity")
-                .style(theme::Text::Color(iced::Color::from_rgb8(0x60, 0x60, 0x60))),
-            slider(0..=127, max, Message::NewVMax),
-            text(format!("{max}")),
-        ]
-        .spacing(10),
+            button("-").on_press(Message::LoopLength(message.song.loop_len.saturating_sub(1))),
+            text(if message.song.is_looping {
+                format!("{}/{}", message.song.step_num, message.song.loop_len)
+            } else {
+                format!("{}", message.song.loop_len)
+            }),
+            button("+").on_press(Message::LoopLength(message.song.loop_len.saturating_add(1))),
+        ],
         row![
-            text("minimum velocity")
-                .style(theme::Text::Color(iced::Color::from_rgb8(0x60, 0x60, 0x60))),
-            slider(0..=127, min, Message::NewVMin),
-            text(format!("{min}")),
+            button("-").on_press(Message::SpeedChanged(message.song.bpm.saturating_sub(1))),
+            text(format!("{}", message.song.bpm)),
+            button("+").on_press(Message::SpeedChanged(message.song.bpm.saturating_add(1))),
+        ],
+        row![
+            button("-").on_press(Message::NewDivision(message.song.divisor.saturating_sub(1))),
+            text(format!("{}", message.song.divisor)),
+            button("+").on_press(Message::NewDivision(message.song.divisor.saturating_add(1))),
         ]
-        .spacing(10),
     ]
-    .spacing(10)
+    .height(Length::Fill)
+    .padding(10)
+    .spacing(20)
+    .into()
 }
 
-fn midi_section<'a>(message: &ControlMessage) -> Column<'a, Message> {
+fn midi_params<'a>() -> Column<'a, Message> {
     column![
-        song_section(message),
-        velocity_sliders(message.info.velocity.min(), message.info.velocity.max())
-            .width(Length::Fixed(500.0))
+        text("center octave"),
+        text("octave range"),
+        text("number of voices"),
+        text("midi channel"),
     ]
-    .spacing(10)
+    .height(Length::Fill)
+    .padding(10)
+    .spacing(20)
+    .into()
 }
 
-fn song_section<'a>(message: &ControlMessage) -> Row<'a, Message> {
-    row![
-        column![
-            loop_controls(
-                message.song.is_looping,
-                message.song.loop_len,
-                message.song.step_num
-            ),
-            speed_controls(message.song.bpm),
-            division_controls(message.song.divisor),
-        ]
-        .padding(10)
-        .spacing(10)
-        .align_items(Alignment::Center),
-        column![
-            voice_controls(message.info.voices, message.info.channel),
-            octave_selector(message.info.octave.center(), message.info.octave.range()),
-            scale_selector(
-                message.info.scale,
-                message.info.root.get_note(),
-                message.info.root.get_accidental()
-            )
+fn midi_vals<'a>(message: &ControlMessage) -> Column<'a, Message> {
+    column![
+        row![
+            button("-").on_press(Message::NewOctave(
+                message.info.octave.center.saturating_sub(1)
+            )),
+            text(format!("{}", message.info.octave.center)),
+            button("+").on_press(Message::NewOctave(
+                message.info.octave.center.saturating_add(1)
+            )),
+        ],
+        row![
+            button("-").on_press(Message::OctaveRange(
+                message.info.octave.range.saturating_sub(1)
+            )),
+            text(format!("{}", message.info.octave.range)),
+            button("+").on_press(Message::OctaveRange(
+                message.info.octave.range.saturating_add(1)
+            )),
+        ],
+        row![
+            button("-").on_press(Message::Voices(message.info.voices.saturating_sub(1))),
+            text(format!("{}", message.info.voices)),
+            button("+").on_press(Message::Voices(message.info.voices.saturating_add(1))),
+        ],
+        row![
+            button("-").on_press(Message::ChannelChange(
+                message.info.channel.saturating_sub(1)
+            )),
+            text(format!("{}", message.info.channel)),
+            button("+").on_press(Message::ChannelChange(
+                message.info.channel.saturating_add(1)
+            )),
         ]
-        .padding(10)
-        .spacing(10)
-        .align_items(Alignment::Center)
     ]
-    .spacing(10)
+    .height(Length::Fill)
+    .padding(10)
+    .spacing(20)
+    .into()
 }
 
-fn octave_selector<'a>(oct: u8, range: u8) -> Row<'a, Message> {
+fn map_section<'a>(message: &ControlMessage) -> Row<'a, Message> {
     row![
-        button("-").on_press(Message::NewOctave(oct.saturating_sub(1))),
-        text(format!("octave: {oct}")),
-        button("+").on_press(Message::NewOctave(oct.saturating_add(1))),
-        button("-").on_press(Message::OctaveRange(range.saturating_sub(1))),
-        text(format!("range: +/-{range}")),
-        button("+").on_press(Message::OctaveRange(range.saturating_add(1)))
+        map_buttons(),
+        randomize_section(message.randomness).width(Length::Fixed(500.0))
     ]
     .spacing(10)
 }
 
-fn loop_controls<'a>(looping: bool, len: usize, step: usize) -> Row<'a, Message> {
+fn probability_section<'a>(message: &ControlMessage) -> Row<'a, Message> {
     row![
-        button(if looping { "free" } else { "loop" }).on_press(Message::ToggleLoop),
-        button("-").on_press(Message::LoopLength(len.saturating_sub(1))),
-        text(if looping {
-            format!("{step}/{len}")
-        } else {
-            format!("{len}")
+        text("probability a cell triggers a note"),
+        slider(0.0..=100.0, message.info.probability * 100.0, |x| {
+            Message::ProbChanged(x / 100.0)
         }),
-        button("+").on_press(Message::LoopLength(len.saturating_add(1)))
+        text(format!("{}", message.info.probability)),
     ]
     .spacing(10)
 }
 
-fn speed_controls<'a>(bpm: usize) -> Row<'a, Message> {
+fn randomize_section<'a>(r: f32) -> Row<'a, Message> {
     row![
-        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(5))),
+        slider(0.0..=100.0, r * 100.0, |x| {
+            Message::RandChanged(x / 100.0)
+        }),
+        text(format!("{r}")),
     ]
     .spacing(10)
 }
 
-fn division_controls<'a>(divisor: usize) -> Row<'a, Message> {
+fn map_buttons<'a>() -> Row<'a, Message> {
     row![
-        button("-").on_press(if divisor > 1 {
-            Message::NewDivision(divisor.saturating_sub(1))
-        } else {
-            Message::None
-        }),
-        text(format!("note division: {divisor}")),
-        button("+").on_press(Message::NewDivision(divisor.saturating_add(1)))
+        button("randomize map")
+            .on_press(Message::RandomizeMap)
+            .style(theme::Button::Primary),
+        button("randomize mask")
+            .on_press(Message::RandomizeMask)
+            .style(theme::Button::Primary),
     ]
     .spacing(10)
 }
 
-fn voice_controls<'a>(voices: u8, channel: u8) -> Row<'a, Message> {
-    row![
-        button("-").on_press(Message::ChannelChange(channel.saturating_sub(1))),
-        text(format!("channel: {channel}")),
-        button("+").on_press(Message::ChannelChange(channel.saturating_add(1))),
-        button("-").on_press(Message::Voices(voices.saturating_sub(1))),
-        text(format!("voices: {voices}")),
-        button("+").on_press(Message::Voices(voices.saturating_add(1))),
+fn velocity_sliders<'a>(message: &ControlMessage) -> Column<'a, Message> {
+    column![
+        text("velocity range"),
+        row![
+            column![
+                text(format!("{}", message.info.velocity.max())),
+                vertical_slider(0..=127, message.info.velocity.max(), Message::NewVMax),
+                text("max")
+            ],
+            column![
+                text(format!("{}", message.info.velocity.min())),
+                vertical_slider(0..=127, message.info.velocity.min(), Message::NewVMin),
+                text("min")
+            ],
+        ]
     ]
     .spacing(10)
 }
 
-fn scale_selector<'a>(scale: Scale, note: RootNote, acc: Accidental) -> Row<'a, Message> {
+fn scale_selector<'a>(message: &ControlMessage) -> Row<'a, Message> {
+    let scale = message.info.scale;
+    let note = message.info.root.note;
+    let accidental = message.info.root.accidental;
     row![
         pick_list(&RootNote::ALL[..], Some(note), move |note| {
-            Message::NewNote(Root::new(note, acc))
+            Message::NewNote(Root { note, accidental })
         })
         .width(Length::Fixed(50.0)),
-        pick_list(&Accidental::ALL[..], Some(acc), move |acc| {
-            Message::NewNote(Root::new(note, acc))
+        pick_list(&Accidental::ALL[..], Some(accidental), move |accidental| {
+            Message::NewNote(Root { note, accidental })
         })
         .width(Length::Fixed(90.0)),
         pick_list(&Scale::ALL[..], Some(scale), Message::Scale).width(Length::Fixed(160.0)),
index 345c09d4fcc85fac64e0a555485ca994b2673847..ba7430f7ed0e9d082fe7552ee416c23acf47dd6c 100644 (file)
@@ -96,9 +96,11 @@ pub enum Message {
     NewMap(CellMap),
     HitCount(u8),
     Tick(Instant),
-    Randomize,
+    RandomizeMap,
+    RandomizeMask,
     Reset,
-    Clear,
+    ClearMap,
+    ClearMask,
     Save,
     TogglePlayback,
     SpeedChanged(usize),
@@ -180,6 +182,9 @@ impl Application for CellSeq {
                 let mut commands = Vec::new();
                 commands.push(Command::perform(async move { map }, Message::NewMap));
                 commands.push(Command::perform(midi, |_| Message::None));
+                commands.push(Command::perform(async move {}, |_| {
+                    Message::MaskMessage(mask::Message::Ticked)
+                }));
 
                 return Command::batch(commands);
             }
@@ -195,21 +200,26 @@ impl Application for CellSeq {
                     self.song.step_num = 0;
                 }
             }
-            Message::Clear => self.map.clear(),
-            Message::Randomize => self.map.randomize(),
+            Message::RandChanged(r) => {
+                self.map.set_randomness(r);
+                self.mask.set_randomness(r);
+            }
+            Message::RandomizeMap => self.map.randomize(),
+            Message::RandomizeMask => self.mask.randomize(),
+            Message::ClearMap => self.map.clear(),
+            Message::ClearMask => self.mask.clear(),
             Message::Reset => self.map.reset(),
             Message::Save => self.map.save(),
             Message::SpeedChanged(b) => self.song.bpm = b,
             Message::NewDivision(d) => self.song.divisor = d,
             Message::LoopLength(l) => self.song.loop_len = l,
             Message::ProbChanged(p) => self.info.probability = p,
-            Message::RandChanged(r) => self.map.set_randomness(r),
             Message::NewVMin(v) => self.info.velocity.set_min(v),
             Message::NewVMax(v) => self.info.velocity.set_max(v),
             Message::ChannelChange(c) => self.info.channel = c,
             Message::Scale(s) => self.info.scale = s,
-            Message::NewOctave(o) => self.info.octave.set_center(o),
-            Message::OctaveRange(r) => self.info.octave.set_range(r),
+            Message::NewOctave(o) => self.info.octave.center = o,
+            Message::OctaveRange(r) => self.info.octave.range = r,
             Message::NewNote(r) => self.info.root = r,
             Message::Voices(v) => self.info.voices = v,
             Message::Quit => return window::close(),
@@ -236,9 +246,10 @@ impl Application for CellSeq {
             self.map.view().map(Message::MapMessage),
             self.mask.view().map(Message::MaskMessage)
         ]
+        .width(Length::Fill)
+        .height(Length::Fill)
         .align_items(Alignment::Center)
-        .spacing(40)
-        .padding(40);
+        .spacing(40);
 
         let bottom = bottom_controls(self.control_message());
 
index e6b256958e55bad7f954e2eb3cac6733347d1ec8..e5b28ff2ccaa37e8bbced0646d6b71b3247eb735 100644 (file)
@@ -7,6 +7,7 @@ use iced::{
     },
     {Color, Element, Length, Point, Rectangle, Size, Theme},
 };
+use rand::random;
 
 use crate::{Cell, CellMap};
 use itertools::Itertools;
@@ -16,12 +17,26 @@ use rustc_hash::FxHashSet;
 pub enum Message {
     Check(Cell),
     Uncheck(Cell),
+    Ticked,
 }
 
-#[derive(Default, Debug)]
+#[derive(Debug)]
 pub struct Mask {
     cells: FxHashSet<Cell>,
+    hits: FxHashSet<Cell>,
     mask_cache: Cache,
+    randomness: f32,
+}
+
+impl Default for Mask {
+    fn default() -> Self {
+        Self {
+            cells: FxHashSet::default(),
+            mask_cache: Cache::default(),
+            randomness: 0.5,
+            hits: FxHashSet::default(),
+        }
+    }
 }
 
 impl Mask {
@@ -35,6 +50,7 @@ impl Mask {
                 self.cells.remove(&cell);
                 self.mask_cache.clear();
             }
+            Message::Ticked => self.mask_cache.clear(),
         }
     }
 
@@ -46,13 +62,32 @@ impl Mask {
     }
 
     pub fn tick(&mut self, life: CellMap) -> u8 {
-        let mut hits = 0;
+        self.hits.clear();
         for cell in self.cells.iter() {
             if life.contains(cell) {
-                hits += 1;
+                self.hits.insert(*cell);
+            }
+        }
+        self.hits.len().try_into().unwrap_or_default()
+    }
+
+    pub fn randomize(&mut self) {
+        self.cells.clear();
+        for (i, j) in (-32..=32).cartesian_product(-32..=32) {
+            if random::<f32>() < self.randomness {
+                self.cells.insert(Cell { i, j });
             }
         }
-        hits
+        self.mask_cache.clear();
+    }
+
+    pub fn set_randomness(&mut self, value: f32) {
+        self.randomness = value;
+    }
+
+    pub fn clear(&mut self) {
+        self.cells.clear();
+        self.mask_cache.clear();
     }
 }
 
@@ -79,7 +114,11 @@ impl Program<Message> for Mask {
                         frame.fill_rectangle(
                             Point::new(x.0 as f32, x.1 as f32),
                             Size::UNIT,
-                            Color::WHITE,
+                            if self.hits.contains(&Cell { i: x.1, j: x.0 }) {
+                                Color::from_rgb8(0xFF, 0x00, 0x00)
+                            } else {
+                                Color::WHITE
+                            },
                         );
                     })
             });
index 2b97ba9338018c7c4277c277b047b38fce1a78a1..74a8b6b68f2a823a606cf52737f0f9e9acd8a9ff 100644 (file)
@@ -25,9 +25,12 @@ impl Default for MidiInfo {
         Self {
             channel: 0,
             velocity: Velocity::new(64, 127),
-            octave: Octave::new(4, 1),
+            octave: Octave::default(),
             scale: Scale::Chromatic,
-            root: Root::new(RootNote::C, Accidental::Natural),
+            root: Root {
+                note: RootNote::C,
+                accidental: Accidental::Natural,
+            },
             voices: 6,
             probability: 0.5,
         }
@@ -71,6 +74,7 @@ impl MidiLink {
             if count > info.voices {
                 break;
             } else if random::<f32>() < info.probability {
+                count += 1;
                 continue;
             } else {
                 count += 1;
index 6a89cdc07e830be66529132cbeaeb4f293efceb3..eeb3e966f0ea27cc059c78917f239fe59189fa4c 100644 (file)
@@ -24,30 +24,8 @@ pub enum Scale {
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub struct Octave {
-    center: u8,
-    range: u8,
-}
-
-impl Octave {
-    pub fn new(center: u8, range: u8) -> Self {
-        Self { center, range }
-    }
-
-    pub fn set_center(&mut self, center: u8) {
-        self.center = center;
-    }
-
-    pub fn set_range(&mut self, range: u8) {
-        self.range = range;
-    }
-
-    pub fn center(&self) -> u8 {
-        self.center
-    }
-
-    pub fn range(&self) -> u8 {
-        self.range
-    }
+    pub center: u8,
+    pub range: u8,
 }
 
 impl Default for Octave {
@@ -89,22 +67,8 @@ impl Velocity {
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
 pub struct Root {
-    note: RootNote,
-    accidental: Accidental,
-}
-
-impl Root {
-    pub fn new(note: RootNote, accidental: Accidental) -> Self {
-        Self { note, accidental }
-    }
-
-    pub fn get_note(&self) -> RootNote {
-        self.note
-    }
-
-    pub fn get_accidental(&self) -> Accidental {
-        self.accidental
-    }
+    pub note: RootNote,
+    pub accidental: Accidental,
 }
 
 impl Display for Root {