|
|
|
|
@ -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::<AudioPlayback>,
|
|
|
|
|
update_pitch_map_resource_ui.run_if(resource_changed::<PitchMap>),
|
|
|
|
|
reassign_cell_pitch.run_if(resource_changed::<PitchMap>),
|
|
|
|
|
update_audio,
|
|
|
|
|
update_material,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.add_systems(Update, cell_lifecycle.run_if(on_message::<Lifecycle>))
|
|
|
|
|
@ -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<Assets<Pitch>>, mut cell_assets: ResMut<CellAssets>) {
|
|
|
|
|
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<Assets<Pitch>>, 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<CellAssets>,
|
|
|
|
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
|
|
|
|
) {
|
|
|
|
|
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<CellAssets>, mut meshes: ResMut<Assets<Mesh>>) {
|
|
|
|
|
@ -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<Cell>>,
|
|
|
|
|
pitch_map: Res<PitchMap>,
|
|
|
|
|
) {
|
|
|
|
|
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<ColorMaterial>),
|
|
|
|
|
Or<(Added<CellPitch>, Changed<CellPitch>)>,
|
|
|
|
|
>,
|
|
|
|
|
cell_assets: ResMut<CellAssets>,
|
|
|
|
|
) {
|
|
|
|
|
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<CellPitch>, Changed<CellPitch>)>>,
|
|
|
|
|
cell_assets: ResMut<CellAssets>,
|
|
|
|
|
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));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|