From ee8a70cc1647e1ca2bdc0f9afad16655356bd7a5 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Mon, 6 Jul 2026 22:03:00 -0700 Subject: [PATCH] mid refactor, saving my place --- life/src/main.rs | 201 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 147 insertions(+), 54 deletions(-) diff --git a/life/src/main.rs b/life/src/main.rs index 5b13607..da9f04a 100644 --- a/life/src/main.rs +++ b/life/src/main.rs @@ -1,7 +1,10 @@ use bevy::{ - feathers::{containers::*, display::*, theme::ThemeProps}, + feathers::{containers::*, theme::ThemeProps}, ui::{Checked, InteractionDisabled}, - ui_widgets::{ActivateOnPress, ValueChange, checkbox_self_update, SliderStep, SliderPrecision, slider_self_update}, + ui_widgets::{ + ActivateOnPress, SliderPrecision, SliderStep, ValueChange, checkbox_self_update, + slider_self_update, + }, }; use engine::*; @@ -57,6 +60,9 @@ fn main() { // TODO Visualize simulation run/pause buttons enable/disable toggle_interactive::, update_pitch_map_resource_ui.run_if(resource_changed::), + reassign_cell_pitch.run_if(resource_changed::), + update_audio, + update_material, ), ) .add_systems(Update, cell_lifecycle.run_if(on_message::)) @@ -187,6 +193,10 @@ fn the_ui() -> impl Scene { Transform pane() Children [ + pane_header() + Children [ + Text("Conways Game of Life With Sound") ThemedText, + ], pane_body() Children [ subpane() @@ -399,43 +409,24 @@ impl Coordinates { (Vec2::new(self.x as f32, self.y as f32) * SCALE).extend(0.0) } - fn as_color(&self) -> Color { - // hue (0..360.0) evenly spaced x in -2..4 - // chroma (0..1) evenly spaced y in -2..5 - let hue = (360.0 / 5.0) * ((self.x + 2) % 5) as f32; - let chroma = (1.0 / 5.0) * ((self.y + 2) % 5) as f32; - let lightness = (1.0 / 5.0) * ((self.y + 2) % 5) as f32; - Color::Oklcha(Oklcha { - hue, - chroma, - lightness, - ..default() - }) - } - - fn as_cell_pitch(&self) -> CellPitch { - debug!("Generating cell pitch for {:?}", self); - - let note = match (self.x + 2).abs() % 5 { - 0 => Note::A, - 1 => Note::B, - 2 => Note::CSharp, - 3 => Note::E, - 4 => Note::FSharp, + /// For the purposes of creating assests, what does the given coordinate translate to CellPitch wise + fn to_cell_pitch(&self) -> CellPitch { + let note = match self.x { + -6 => Note::A, + -5 => Note::ASharp, + -4 => Note::B, + -3 => Note::C, + -2 => Note::CSharp, + -1 => Note::D, + 0 => Note::DSharp, + 1 => Note::E, + 2 => Note::F, + 3 => Note::FSharp, + 4 => Note::G, + 5 => Note::GSharp, _ => panic!("This shouldn't happen!"), }; - - let octave = ((self.y + 2).abs() % 5) - 2; - debug_assert!((-2..=2).contains(&octave)); - - debug!( - "\t{:?}", - CellPitch { - note: note.clone(), - octave - } - ); - + let octave = self.y; CellPitch { note, octave } } } @@ -619,9 +610,9 @@ fn new_cell( Coordinates { x, y } template_value(playback_settings) template_value(Transform::from_translation(c.as_translation() + Vec3::new(0.0, 0.0, 1.0))) - template_value(AudioPlayerTemplate(cell_assets.pitches.get(&c.as_cell_pitch()).unwrap().clone().into())) template_value(Mesh2dTemplate(cell_assets.mesh.clone().into())) - template_value(MeshMaterial2dTemplate(cell_assets.materials.get(&c.as_cell_pitch()).unwrap().clone().into())) + // TODO: template_value(AudioPlayerTemplate(cell_assets.pitches.get(&pitch_map.coordinates_cell_pitch(c)).unwrap().clone().into())) + // TODO: template_value(MeshMaterial2dTemplate(cell_assets.materials.get(&pitch_map.coordinates_cell_pitch(c)).unwrap().clone().into())) } } @@ -728,7 +719,7 @@ impl Note { } } -#[derive(Hash, PartialEq, Eq, Debug)] +#[derive(Hash, PartialEq, Eq, Debug, Component)] struct CellPitch { note: Note, octave: isize, @@ -739,17 +730,59 @@ impl CellPitch { // Thanks Claude Opus 4.8 self.note.as_frequency() * 2f32.powi(self.octave as i32) } + + fn to_color(&self) -> Color { + let hue = { + let segment = 360.0 / 12.0; + match self.note { + // Equally distribute hue among the OkLab spectrum (0..360) + Note::A => segment * 1.0, + Note::ASharp => segment * 2.0, + Note::B => segment * 3.0, + Note::C => segment * 4.0, + Note::CSharp => segment * 5.0, + Note::D => segment * 6.0, + Note::DSharp => segment * 7.0, + Note::E => segment * 8.0, + Note::F => segment * 9.0, + Note::FSharp => segment * 10.0, + Note::G => segment * 11.0, + Note::GSharp => segment * 12.0, + } + }; + + let lightness = (self.octave as f32 + 5.0) / 11.0; + + Color::Oklcha(Oklcha { + hue, + lightness, + ..default() + }) + } } fn load_audio_tones(mut pitches: ResMut>, mut cell_assets: ResMut) { - let notes = [Note::A, Note::B, Note::CSharp, Note::E, Note::FSharp]; - let octaves = [-2, -1, 0, 1, 2]; - - notes.iter().for_each(|note| { - octaves.iter().for_each(|octave| { + let notes = [ + Note::A, + Note::ASharp, + Note::B, + Note::C, + Note::CSharp, + Note::D, + Note::DSharp, + Note::E, + Note::F, + Note::FSharp, + Note::G, + Note::GSharp, + ]; + let octaves = -5..6; + + octaves.for_each(|octave| { + notes.iter().for_each(|note| { let cell_pitch = CellPitch { note: note.clone(), - octave: *octave, + octave, }; let frequency = cell_pitch.to_frequency(); let duration = Duration::from_secs(100); @@ -764,7 +797,7 @@ fn load_audio_tones(mut pitches: ResMut>, mut cell_assets: ResMut< cell_assets.pitches.insert(cell_pitch, handle); }); }); - debug_assert!(cell_assets.pitches.iter().len() == 25); + debug_assert_eq!(cell_assets.pitches.iter().len(), 132); } fn control_volume( @@ -798,16 +831,22 @@ fn load_materials( mut cell_assets: ResMut, mut materials: ResMut>, ) { - let vertical = [-2, -1, 0, 1, 2]; - let horizontal = [-2, -1, 0, 1, 2]; + // Up to 11 octaves total + let vertical = -5..6; + debug_assert_eq!(vertical.len(), 11); + + // 12 total colors supported + let horizontal = -6..6; + debug_assert_eq!(horizontal.len(), 12); + let tile_handle = server.load(TILE); - vertical.iter().for_each(|&x| { - horizontal.iter().for_each(|&y| { + vertical.for_each(|x| { + horizontal.clone().for_each(|y| { let c = Coordinates { x, y }; - let pitch = c.as_cell_pitch(); + let pitch = c.to_cell_pitch(); let handle = { - let color = c.as_color(); + let color = pitch.to_color(); let texture = Some(tile_handle.clone()); let color_material = ColorMaterial { color, @@ -819,7 +858,7 @@ fn load_materials( cell_assets.materials.insert(pitch, handle); }); }); - debug_assert!(cell_assets.materials.iter().len() == 25); + debug_assert_eq!(cell_assets.materials.iter().len(), 132); } fn load_meshes(mut cell_assets: ResMut, mut meshes: ResMut>) { @@ -931,4 +970,58 @@ impl PitchMap { self.notes.push(n.clone()); } } + + /// Given the currently selected notes and octave range selected in the PitchMap convert the given coordinates to a CellPitch + fn coordinates_cell_pitch(&self, c: &Coordinates) -> CellPitch { + let note: Note = { + // Note is the x-axis pattern, repeating every 12 tiles because we support 12 pitches + let idx = (c.x as usize) % self.notes.len(); + // TODO: pretty sure this doesn't handle negative numbers at all... + // need to shift by ?? first + self.notes.get(idx).unwrap().clone() + }; + let octave: isize = { + // Octave is the y-axis pattern, repeating every 11 tiles because we support 11 octaves + let total_octaves = self.octaves_up as isize + self.octaves_down as isize + 1; + // TODO: pretty sure this doesn't handle negative numbers at all... + // need to shift by ?? first + c.y % total_octaves + }; + CellPitch { note, octave } + } +} + +/// When the pitch map changes, update all cells to use the correct pitch (note + octave) +fn reassign_cell_pitch( + mut query: Query<(&mut CellPitch, &Coordinates), With>, + pitch_map: Res, +) { + query.iter_mut().for_each(|(mut cell_pitch, c)| { + *cell_pitch = pitch_map.coordinates_cell_pitch(c); + }); +} + +// When CellPitch is added/updated on a cell, update the material +fn update_material( + mut query: Query< + (&CellPitch, &mut MeshMaterial2d), + Or<(Added, Changed)>, + >, + cell_assets: ResMut, +) { + query.iter_mut().for_each(|(cell_pitch, mut material)| { + material.0 = cell_assets.materials.get(cell_pitch).unwrap().clone(); + }) +} + +// When CellPitch is added/updated on a cell, update the Pitch +fn update_audio( + mut query: Query<(&CellPitch, Entity), Or<(Added, Changed)>>, + cell_assets: ResMut, + mut commands: Commands, +) { + query.iter_mut().for_each(|(cell_pitch, e)| { + let h = cell_assets.pitches.get(cell_pitch).unwrap().clone(); + commands.entity(e).insert(AudioPlayer(h)); + }); }