Files
Polymusic/src/main.rs
2025-07-06 11:39:11 +02:00

369 lines
14 KiB
Rust

mod polygon_draw;
use polygon_draw::{Polygon, PolygonFrame};
mod utils;
use utils::string_to_color;
use std::fs;
use iced::{Application, Element, Settings, Subscription, window};
use iced::{
Color, Length, Task, Theme,
time::{self, Duration},
widget::{Column, TextInput, button, canvas, column, container, pick_list, row, slider, text},
};
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
use std::time::Instant;
use kira::{
AudioManager, AudioManagerSettings, DefaultBackend, sound::static_sound::StaticSoundData,
};
fn main() -> iced::Result {
iced::application("My App", MyApp::update, MyApp::view)
.theme(|_| Theme::Dark)
.subscription(MyApp::subscription)
.run_with(MyApp::new)
}
#[derive(Debug, Clone)]
enum Message {
WindowEvent(window::Event),
ButtonPressedIncrement,
ButtonPressedDecrement,
Tick,
AddPolygon(String),
ChangeTeta(usize, f32),
Remove(usize),
ChangeColor(usize, String),
ChangeSound(usize, String),
ToggleSavePanel,
Save,
Load,
FileNameChanged(String),
}
#[derive(Serialize, Deserialize)]
struct MyApp {
poly_frame: PolygonFrame,
#[serde(skip, default = "dummy_instant")]
time_last_frame: Instant,
nb_sec_for_rev: f32,
#[serde(skip, default = "dummy_audio_manager")]
audio_manager: AudioManager,
#[serde(skip, default = "dummy_sound")]
default_sound: StaticSoundData,
file_name: String,
show_save_panel: bool,
paused: bool,
#[serde(skip, default = "load_path_sounds")]
all_sounds: Vec<String>,
#[serde(skip, default = "load_path_saves")]
all_saves: Vec<String>,
}
impl MyApp {
fn new() -> (Self, Task<Message>) {
let manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())
.expect("Error to load AudioManager");
let sound_data = StaticSoundData::from_file("assets/tick.ogg").expect("Fail to load audio");
(
Self {
nb_sec_for_rev: 4.0,
time_last_frame: Instant::now(),
audio_manager: manager,
default_sound: sound_data.clone(),
file_name: "polymusic.json".to_string(),
show_save_panel: true,
paused: false,
all_sounds: load_path_sounds(),
all_saves: load_path_saves(),
poly_frame: PolygonFrame {
teta: 0.0,
polygons: vec![
//Polygon::n_gon(0.0, 12),
//Polygon::triangle(0.0),
],
},
},
Task::none(),
)
}
fn update(&mut self, message: Message) {
match message {
Message::WindowEvent(window::Event::Resized(size)) => {
println!("Resize detected: {}x{}", size.width, size.height);
}
Message::ButtonPressedIncrement => self.nb_sec_for_rev += 0.5,
Message::ButtonPressedDecrement => {
if self.nb_sec_for_rev > 0.5 {
self.nb_sec_for_rev -= 0.5;
}
}
Message::AddPolygon(s) => {
let mut poly: Polygon;
if s.starts_with("Ngon") {
if let Ok(sides) = s.trim_start_matches("Ngon").parse::<u8>() {
poly = Polygon::n_gon(0.0, sides, self.default_sound.clone());
poly.name = format!("Ngon_{sides}");
} else {
return;
}
} else {
match s.as_str() {
"Segment" => {
poly = Polygon::segment(0.0, self.default_sound.clone());
poly.name = "Segment".to_string();
}
"Triangle" => {
poly = Polygon::triangle(0.0, self.default_sound.clone());
poly.name = "Triangle".to_string();
}
"Square" => {
poly = Polygon::square(0.0, self.default_sound.clone());
poly.name = "Square".to_string();
}
"Nr6In30" => {
poly = Polygon::nr_6_in_30(0.0, self.default_sound.clone());
poly.name = "Nr6In30".to_string();
}
"Nr7In30" => {
poly = Polygon::nr_7_in_30(0.0, self.default_sound.clone());
poly.name = "Nr7In30".to_string();
}
"Nr8In30" => {
poly = Polygon::nr_8_in_30(0.0, self.default_sound.clone());
poly.name = "Nr8In30".to_string();
}
"Nr9In30" => {
poly = Polygon::nr_9_in_30(0.0, self.default_sound.clone());
poly.name = "Nr9In30".to_string();
}
"Nr8In42" => {
poly = Polygon::nr_8_in_42(0.0, self.default_sound.clone());
poly.name = "Nr8In42".to_string();
}
"Nr9In42" => {
poly = Polygon::nr_9_in_42(0.0, self.default_sound.clone());
poly.name = "Nr9In42".to_string();
}
"Nr10aIn42" => {
poly = Polygon::nr_10a_in_42(0.0, self.default_sound.clone());
poly.name = "Nr10aIn42".to_string();
}
"Nr10bIn42" => {
poly = Polygon::nr_10b_in_42(0.0, self.default_sound.clone());
poly.name = "Nr10bIn42".to_string();
}
_ => poly = Polygon::n_gon(0.0, 0, self.default_sound.clone()),
}
}
self.poly_frame.polygons.push(poly);
}
Message::Tick => {
if !self.paused {
let time_btw = Instant::now().duration_since(self.time_last_frame);
let teta_temp = self.poly_frame.teta;
self.poly_frame.teta += 2.0
* PI
* (1.0 / self.nb_sec_for_rev)
* (time_btw.as_millis() as f32 / 1_000.0);
self.poly_frame.teta %= 2.0 * PI;
let sound_to_play = self
.poly_frame
.all_sound_to_play_btw(teta_temp, self.poly_frame.teta);
for sound in sound_to_play {
self.audio_manager
.play(sound.clone())
.expect("Error to play sound");
}
self.time_last_frame = Instant::now();
}
}
Message::Remove(i) => {
self.poly_frame.polygons.remove(i - 1);
}
Message::ChangeTeta(i, teta) => {
self.poly_frame.polygons[i].global_teta = teta;
}
Message::ChangeColor(i, s) => {
let c = string_to_color(&s);
self.poly_frame.polygons[i].color = c;
self.poly_frame.polygons[i].color_name = s;
}
Message::ChangeSound(i, s) => {
self.poly_frame.polygons[i].sound =
StaticSoundData::from_file(format!("./assets/{s}"))
.expect("Fail to load audio");
self.poly_frame.polygons[i].sound_name = s;
}
Message::Save => {
let json = serde_json::to_string_pretty(&self).unwrap();
fs::write(format!("./saves/{0}", &self.file_name), json).unwrap();
}
Message::Load => {
let json = fs::read_to_string(format!("./saves/{0}", &self.file_name)).unwrap();
let decoded: MyApp = serde_json::from_str(&json).unwrap();
*self = decoded;
self.poly_frame.update();
}
Message::ToggleSavePanel => self.show_save_panel = !self.show_save_panel,
Message::FileNameChanged(s) => self.file_name = s,
_ => {}
}
}
fn view(&self) -> iced::Element<Message> {
let txt_nb_rev = format!("Number of second for revolution : {}", self.nb_sec_for_rev);
let mut i = 0;
let entries = self.all_sounds.clone();
//Create all polygon options
let polygon_rows: Vec<Element<Message>> = self
.poly_frame
.polygons
.iter()
.map(|polygon| {
let current_index = i;
i += 1;
column![
row![
text(&polygon.name),
button("Remove").on_press(Message::Remove(i)),
pick_list(
["Black", "Blue", "Green", "Pink", "Yellow", "Cyan"]
.map(|s| s.to_string())
.to_vec(),
Some(&polygon.color_name),
move |s| { Message::ChangeColor(current_index, s) }
),
pick_list(entries.clone(), Some(&polygon.sound_name), move |s| {
Message::ChangeSound(current_index, s)
}),
]
.spacing(20),
slider(0.0..=2.0 * PI, polygon.global_teta, move |f| {
Message::ChangeTeta(current_index, f)
})
.step(PI / 84f32), // 84 | 4 for do PI / 4
]
.spacing(20)
.into()
})
.collect();
let ngon_options: Vec<String> = (5..=42).map(|sides| format!("Ngon{sides}")).collect();
let all_options: Vec<String> = [
"Segment",
"Triangle",
"Square",
"Nr6In30",
"Nr7In30",
"Nr8In30",
"Nr9In30",
"Nr8In42",
"Nr9In42",
"Nr10aIn42",
"Nr10bIn42",
]
.iter()
.map(|s| s.to_string())
.chain(ngon_options)
.collect();
let polygon_column = Column::with_children(polygon_rows);
let mut save_panel: Vec<Element<Message>> = vec![
button("Toggle Save Panel")
.on_press(Message::ToggleSavePanel)
.into(),
];
if self.show_save_panel {
save_panel.push(
TextInput::new("Name File", &self.file_name)
.on_input(|new_value| Message::FileNameChanged(new_value))
.into(),
);
save_panel.push(button("Save").on_press(Message::Save).into());
save_panel.push(
pick_list(self.all_saves.clone(), Some(&self.file_name), move |s| {
Message::FileNameChanged(s)
})
.into(),
);
save_panel.push(button("Load").on_press(Message::Load).into());
}
column![
text("Polymusic").size(32.0),
row(save_panel).spacing(20),
row![
container(
canvas(&self.poly_frame)
.height(Length::FillPortion(2))
.width(Length::FillPortion(1))
),
column![
text(txt_nb_rev),
row![
button("Increment").on_press(Message::ButtonPressedIncrement),
button("Decrement").on_press(Message::ButtonPressedDecrement),
],
text("Polygon options"),
pick_list(all_options, Some("Choose polygon".to_string()), |s| {
Message::AddPolygon(s)
}),
polygon_column.spacing(10),
]
.spacing(10)
.height(Length::FillPortion(2))
.width(Length::FillPortion(2)),
]
.spacing(20),
row![text("futur time line")]
.height(Length::FillPortion(1))
.width(Length::FillPortion(1)),
]
.spacing(25)
.padding(25)
.into()
}
fn subscription(&self) -> iced::Subscription<Message> {
iced::Subscription::batch([
window::events().map(|(_id, event)| Message::WindowEvent(event)),
iced::time::every(std::time::Duration::from_millis(16)).map(|_| Message::Tick),
])
}
}
fn dummy_sound() -> StaticSoundData {
StaticSoundData::from_file("assets/tick.ogg").expect("Fail to load audio")
}
fn dummy_instant() -> Instant {
Instant::now()
}
fn dummy_audio_manager() -> AudioManager {
AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())
.expect("Error to load AudioManager")
}
fn load_path_sounds() -> Vec<String> {
let mut entries: Vec<String> = fs::read_dir("./assets")
.unwrap()
.filter_map(|res| res.ok())
.map(|e| e.path().file_name().unwrap().to_str().unwrap().to_string())
.collect();
entries.sort();
entries
}
fn load_path_saves() -> Vec<String> {
fs::read_dir("./saves")
.unwrap()
.filter_map(|res| res.ok())
.map(|e| e.path().file_name().unwrap().to_str().unwrap().to_string())
.collect()
}