39 Commits

Author SHA1 Message Date
607f65c913 cargo fix 2025-07-19 18:56:22 +02:00
82ca2ced24 historic , undo and redo 2025-07-19 18:56:06 +02:00
bb64de1955 remove to do list and add information 2025-07-19 17:08:35 +02:00
54949a9db0 remove file not used 2025-07-19 17:03:14 +02:00
d841c9fdb4 zoom on time line 2025-07-19 13:34:15 +02:00
ad6d09b8a5 cargo fix 2025-07-19 01:18:50 +02:00
e7b1000205 load menu and save carfully 2025-07-19 01:18:34 +02:00
625828f7ed save 2025-07-18 20:33:00 +02:00
5cd8bf90c7 fix cargo 2025-07-18 20:18:22 +02:00
fc54c4b9ee menu bar 2025-07-18 19:51:28 +02:00
46861440fd fix by cargo 2025-07-18 18:22:19 +02:00
fdfea98c50 shortcut 2025-07-18 18:21:36 +02:00
a512eff61d fix 2025-07-18 17:51:36 +02:00
bbeff369e8 fix 2025-07-18 17:51:17 +02:00
3788c3e631 fix 2025-07-17 22:18:44 +02:00
2e61e5b629 bug fix 2025-07-10 20:21:27 +02:00
ea708dfe85 fix 2025-07-10 15:26:15 +02:00
9f59e68b2c fix by cargo 2025-07-10 15:09:45 +02:00
607cf17888 add color picker and better buttons 2025-07-10 15:07:52 +02:00
984cec5741 antialiazing 2025-07-10 00:33:29 +02:00
8eb84f6780 del notused font 2025-07-10 00:26:31 +02:00
719f0c1332 fonts and glowup 2025-07-10 00:25:42 +02:00
064190d1a7 add small canvas on time line 2025-07-09 20:50:39 +02:00
a426c7bb98 colorfull 2025-07-09 18:07:53 +02:00
440032ffdb better color 2025-07-09 17:20:00 +02:00
7047679ace add to do list 2025-07-09 15:24:20 +02:00
68f8cff151 padding 2025-07-09 11:21:02 +02:00
e85ed8371d timeline clickable 2025-07-09 11:18:15 +02:00
bf2266955b add remove point 2025-07-09 09:33:26 +02:00
4d31708481 change sounds 2025-07-09 01:35:16 +02:00
8f850e9323 fix all polyframe update now 2025-07-09 01:31:03 +02:00
51757c3922 Merge branch 'master' of gitlab.dukantic.fr:dukantic/polymusic 2025-07-09 00:56:55 +02:00
50251b3053 fix assets 2025-07-09 00:56:33 +02:00
00ea324d48 Delete .gitlab-ci.yml 2025-07-08 21:39:21 +00:00
1192c22691 Update .gitlab-ci.yml file 2025-07-08 21:38:08 +00:00
01ad529024 Update .gitlab-ci.yml file 2025-07-08 21:34:48 +00:00
44b96aef15 Edit README.md 2025-07-08 21:32:52 +00:00
27529e4fc5 Update .gitlab-ci.yml file 2025-07-08 21:29:21 +00:00
a56f5ddba4 Update .gitlab-ci.yml file 2025-07-08 21:15:57 +00:00
57 changed files with 1382 additions and 463 deletions

188
Cargo.lock generated
View File

@@ -121,6 +121,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -352,7 +358,7 @@ dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
"itertools",
"itertools 0.13.0",
"proc-macro2",
"quote",
"regex",
@@ -425,6 +431,15 @@ dependencies = [
"piper",
]
[[package]]
name = "borsh"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
dependencies = [
"cfg_aliases 0.2.1",
]
[[package]]
name = "bumpalo"
version = "3.17.0"
@@ -533,6 +548,20 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
@@ -1548,6 +1577,30 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.2",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "iced"
version = "0.13.1"
@@ -1562,6 +1615,23 @@ dependencies = [
"thiserror",
]
[[package]]
name = "iced_aw"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "582c517a94ce3205da98e9c10b26bb71aa36b7d7d084441d826dc912711d1bac"
dependencies = [
"cfg-if",
"chrono",
"getrandom 0.3.3",
"iced",
"iced_fonts 0.1.1",
"itertools 0.14.0",
"num-format",
"num-traits",
"web-time",
]
[[package]]
name = "iced_core"
version = "0.13.2"
@@ -1577,11 +1647,30 @@ dependencies = [
"once_cell",
"palette",
"rustc-hash 2.1.1",
"smol_str",
"smol_str 0.2.2",
"thiserror",
"web-time",
]
[[package]]
name = "iced_fonts"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df7deb0800a850ee25c8a42559f72c0f249e577feb3aad37b9b65dc1e517e52a"
dependencies = [
"iced_core",
]
[[package]]
name = "iced_fonts"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7db9d45e87bc3ac22ff82e2d9dea9139f1c055137f1e0f39df3d68110fcee7a9"
dependencies = [
"iced_core",
"iced_widget",
]
[[package]]
name = "iced_futures"
version = "0.13.2"
@@ -1757,6 +1846,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@@ -2176,6 +2274,16 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "num-format"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
dependencies = [
"arrayvec",
"itoa",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -2729,10 +2837,13 @@ name = "polygomusic"
version = "0.1.0"
dependencies = [
"iced",
"iced_aw",
"iced_fonts 0.2.1",
"kira",
"regex",
"serde",
"serde_json",
"smol_str 0.3.2",
"tokio",
]
@@ -3251,6 +3362,16 @@ dependencies = [
"serde",
]
[[package]]
name = "smol_str"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
dependencies = [
"borsh",
"serde",
]
[[package]]
name = "softbuffer"
version = "0.4.6"
@@ -4170,10 +4291,51 @@ version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
dependencies = [
"windows-result",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result 0.3.4",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
version = "0.1.2"
@@ -4183,6 +4345,24 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -4487,7 +4667,7 @@ dependencies = [
"rustix 0.38.44",
"sctk-adwaita",
"smithay-client-toolkit",
"smol_str",
"smol_str 0.2.2",
"tracing",
"unicode-segmentation",
"wasm-bindgen",

View File

@@ -2,6 +2,7 @@
name = "polygomusic"
version = "0.1.0"
edition = "2024"
build = "build.rs"
[dependencies]
iced = { version = "0.13.1", features = ["canvas", "tokio"] }
@@ -10,3 +11,6 @@ tokio = {version = "1.45.1", features = ["time"]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.140"
regex = "1.11.1"
iced_aw = {version = "0.12.2", default-features = true}
iced_fonts = "0.2.1"
smol_str = "0.3.2"

View File

@@ -1,6 +1,12 @@
# Polymusic
Polymusic is an innovative music application inspired
by two academic research papers that explore sound
generation through balanced polygons. By leveraging
geometric principles, Polymusic produces harmonious
and rich audio textures, creating a unique auditory experience.
## Instalation
### With source code
>Install Rust and Cargo
>https://www.rust-lang.org/tools/install
@@ -8,6 +14,9 @@
```
cargo run
```
### Release
Release is available on gitlab. Download windows ou linux version.
Execute `Polymusic` or `Polymusic.exe`
## Files

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/a-3.mp3 Normal file

Binary file not shown.

BIN
assets/a-4.mp3 Normal file

Binary file not shown.

BIN
assets/a-5.mp3 Normal file

Binary file not shown.

BIN
assets/a3.mp3 Normal file

Binary file not shown.

BIN
assets/a4.mp3 Normal file

Binary file not shown.

BIN
assets/a5.mp3 Normal file

Binary file not shown.

BIN
assets/b3.mp3 Normal file

Binary file not shown.

BIN
assets/b4.mp3 Normal file

Binary file not shown.

BIN
assets/b5.mp3 Normal file

Binary file not shown.

BIN
assets/c-3.mp3 Normal file

Binary file not shown.

BIN
assets/c-4.mp3 Normal file

Binary file not shown.

BIN
assets/c-5.mp3 Normal file

Binary file not shown.

BIN
assets/c3.mp3 Normal file

Binary file not shown.

BIN
assets/c4.mp3 Normal file

Binary file not shown.

BIN
assets/c5.mp3 Normal file

Binary file not shown.

BIN
assets/c6.mp3 Normal file

Binary file not shown.

BIN
assets/d-3.mp3 Normal file

Binary file not shown.

BIN
assets/d-4.mp3 Normal file

Binary file not shown.

BIN
assets/d-5.mp3 Normal file

Binary file not shown.

BIN
assets/d3.mp3 Normal file

Binary file not shown.

BIN
assets/d4.mp3 Normal file

Binary file not shown.

BIN
assets/d5.mp3 Normal file

Binary file not shown.

BIN
assets/e3.mp3 Normal file

Binary file not shown.

BIN
assets/e4.mp3 Normal file

Binary file not shown.

BIN
assets/e5.mp3 Normal file

Binary file not shown.

BIN
assets/f-3.mp3 Normal file

Binary file not shown.

BIN
assets/f-4.mp3 Normal file

Binary file not shown.

BIN
assets/f-5.mp3 Normal file

Binary file not shown.

BIN
assets/f3.mp3 Normal file

Binary file not shown.

BIN
assets/f4.mp3 Normal file

Binary file not shown.

BIN
assets/f5.mp3 Normal file

Binary file not shown.

BIN
assets/g-3.mp3 Normal file

Binary file not shown.

BIN
assets/g-4.mp3 Normal file

Binary file not shown.

BIN
assets/g-5.mp3 Normal file

Binary file not shown.

BIN
assets/g3.mp3 Normal file

Binary file not shown.

BIN
assets/g4.mp3 Normal file

Binary file not shown.

BIN
assets/g5.mp3 Normal file

Binary file not shown.

39
build.rs Normal file
View File

@@ -0,0 +1,39 @@
use std::env;
use std::fs;
use std::path::Path;
fn main() {
println!("cargo:rerun-if-changed=assets");
println!("cargo:rerun-if-changed=fonts");
// Chemin vers target/debug ou target/release
let out_dir = env::var("OUT_DIR").unwrap();
let target_dir = Path::new(&out_dir)
.ancestors()
.nth(3) // On remonte pour arriver dans target/debug ou target/release
.unwrap();
// Copie assets/
let _ = fs::remove_dir_all(target_dir.join("assets"));
fs::create_dir_all(target_dir.join("assets")).unwrap();
copy_dir("assets", target_dir.join("assets"));
// Copie fonts/
let _ = fs::remove_dir_all(target_dir.join("fonts"));
fs::create_dir_all(target_dir.join("fonts")).unwrap();
copy_dir("fonts", target_dir.join("fonts"));
}
fn copy_dir(src: impl AsRef<Path>, dst: impl AsRef<Path>) {
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let ty = entry.file_type().unwrap();
let dst_path = dst.as_ref().join(entry.file_name());
if ty.is_dir() {
fs::create_dir_all(&dst_path).unwrap();
copy_dir(entry.path(), &dst_path);
} else {
fs::copy(entry.path(), &dst_path).unwrap();
}
}
}

Binary file not shown.

257
src/gui.rs Normal file
View File

@@ -0,0 +1,257 @@
use crate::message::Message;
use iced::alignment::{Horizontal, Vertical};
use iced::widget::{TextInput, column, text};
use iced::{Element, Theme};
use iced::{
Length, Padding,
widget::{Column, Container, button, canvas, container, pick_list, row, scrollable, slider},
};
use iced_aw::menu::{self, Item};
use iced_aw::menu_bar;
use iced_aw::widget::color_picker;
use iced_aw::widget::menu::Menu;
use std::f32::consts::PI;
use crate::MyApp;
pub fn music_view(app: &MyApp) -> iced::Element<Message> {
let mut i = 0;
let entries = app.all_sounds.clone();
//Create all polygon options
let polygon_rows: Vec<Element<Message>> = app
.music
.current_frame(app.current_delta)
.polygons
.iter()
.map(|polygon| {
let current_index = i;
let but = button(text("").size(20).center()).on_press(Message::SubmitColor(i));
let c = column![
row![
text(&polygon.name).size(24),
button(text("").size(20)).on_press(Message::Remove(i)),
color_picker(
polygon.show_color_picker,
polygon.color,
but,
Message::CancelColor(i),
move |color| Message::ChooseColor(i, color)
),
pick_list(entries.clone(), Some(&polygon.sound_name), move |s| {
Message::ChangeSound(current_index, s)
})
.text_size(20),
]
.spacing(20),
row![
TextInput::new("90", &polygon.global_teta.to_degrees().floor().to_string())
.on_input(move |new_value| Message::ChangeDegree(current_index, new_value))
.width(Length::FillPortion(1)),
row![
slider(0.0..=2.0 * PI, polygon.global_teta, move |f| {
Message::ChangeTeta(current_index, f)
})
.step(2. * PI / 10_000.)
.width(Length::FillPortion(9))
]
.padding(Padding::from(16)),
]
.spacing(5),
]
.spacing(10)
.into();
i += 1;
c
})
.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 = scrollable(Column::with_children(polygon_rows).spacing(24));
let menu_tpl_1 = |items| Menu::new(items).max_width(200.0).offset(15.0).spacing(5.0);
let save_menu = menu_tpl_1(vec![
Item::new(button("Save File").on_press(Message::Save)),
Item::new(button("Load Menu").on_press(Message::GoToLoadView).style(
|theme: &Theme, status| {
let palette = theme.extended_palette();
match status {
button::Status::Active => {
if !app.already_save {
button::Style::default().with_background(palette.danger.strong.color)
} else {
button::primary(theme, status)
}
}
_ => button::primary(theme, status),
}
},
)),
Item::new("CTRL+F for quit without save."),
]);
column![
menu_bar!((text("File").size(20), save_menu)(
text("Shortcuts").size(20),
menu_tpl_1(vec![
Item::new("󰐊 SPACE"),
Item::new("󰆓 CTRL+S"),
Item::new("󰈼 ARROW UP"),
Item::new("󰕌 CTRL+Z"),
Item::new("󰑎 CTRL+Y"),
])
))
.draw_path(menu::DrawPath::Backdrop)
.spacing(20),
text(&app.music.file_name)
.width(Length::FillPortion(1))
.size(32),
row![
container(
canvas(app.music.current_frame(app.current_delta))
.height(Length::FillPortion(1))
.width(Length::FillPortion(1))
),
column![
text("Polygon options").size(26),
pick_list(all_options, Some("Choose polygon".to_string()), |s| {
Message::AddPolygon(s)
})
.text_size(18),
polygon_column,
]
.spacing(10)
.height(Length::FillPortion(1))
.width(Length::FillPortion(2)),
]
.height(Length::FillPortion(2))
.spacing(20),
column![
row![
button(text(if app.paused { "󰐊" } else { "󰏤" }).size(28).center())
.on_press(if app.can_unpaused {
Message::TogglePaused
} else {
Message::None
})
.width(Length::FillPortion(1)),
row![
TextInput::new("MM:SS:CS", &app.str_time)
.on_input(|new_value| Message::ChangeDeltaString(new_value))
.size(28),
text("/").size(30),
TextInput::new("MM:SS:CS", &app.str_music_length)
.on_input(|new_value| Message::LengthChange(new_value))
.size(28),
]
.width(Length::FillPortion(10)),
TextInput::new("1.0", &format!("{:.1} sec/rev", &app.music.nb_sec_for_rev))
.on_input(|new_value| Message::ChangeNbPerSec(new_value))
.size(28)
.width(Length::FillPortion(2)),
button(text("").size(28).center())
.on_press(Message::SlidePointLeft)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::AddPoint)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::SlidePointRight)
.width(Length::FillPortion(1)),
button(text("").size(28).center())
.on_press(Message::RemovePoint)
.width(Length::FillPortion(1)),
]
.spacing(20),
column![
/*
slider(0.0..=app.music.length, self.current_delta, move |f| {
Message::ChangeDelta(f)
})
.step(&app.music.length / 10_000.),*/
canvas(&app.music)
.height(Length::FillPortion(1))
.width(Length::FillPortion(1))
]
.spacing(0),
]
.spacing(20)
.height(Length::FillPortion(1))
.width(Length::FillPortion(1)),
]
.spacing(25)
.padding(25)
.into()
}
pub fn load_file_view(app: &MyApp) -> iced::Element<Message> {
Container::new(
column![
text("Polymusic").size(42),
row![
column![
TextInput::new("Name File", &app.file_name_to_creat)
.on_input(|new_value| Message::SetFileNameCreat(new_value)),
button("Create File").on_press(Message::CreatFile),
text(if app.show_warning_message_creat {
"Warning: File already exists. Delete it or change the file name!"
} else {
""
})
.style(|theme: &Theme| {
let palette = theme.extended_palette();
text::Style {
color: Some(palette.danger.strong.color),
}
},)
]
.spacing(10)
.width(200)
.height(200)
.align_x(Horizontal::Center),
column![
pick_list(
app.all_saves.clone(),
Some(&app.music.file_name),
move |s| Message::FileNameChanged(s),
),
button("Load File").on_press(Message::Load),
text("")
]
.spacing(10)
.width(200)
.height(200)
.align_x(Horizontal::Center)
]
.spacing(32)
.align_y(Vertical::Center),
]
.spacing(64)
.align_x(Horizontal::Center),
)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.into()
}

53
src/history.rs Normal file
View File

@@ -0,0 +1,53 @@
use crate::message::Message;
#[derive(Clone, Debug)]
struct HistoricItem {
old: Message,
new: Message,
delta: f32,
}
#[derive(Debug)]
pub struct Historic {
past: Vec<HistoricItem>,
future: Vec<HistoricItem>,
pub ignore_add: bool,
}
impl Historic {
pub fn new() -> Self {
Historic {
past: vec![],
future: vec![],
ignore_add: false,
}
}
pub fn add(&mut self, old: Message, new: Message, delta: f32) {
if !self.ignore_add {
self.future = vec![];
self.past.push(HistoricItem {
old: old,
new: new,
delta: delta,
});
}
}
pub fn undo(&mut self) -> Option<(Message, f32)> {
if let Some(item) = self.past.pop() {
self.future.push(item.clone());
Some((item.old.clone(), item.delta))
} else {
None
}
}
pub fn redo(&mut self) -> Option<(Message, f32)> {
if let Some(item) = self.future.pop() {
self.past.push(item.clone());
Some((item.new.clone(), item.delta))
} else {
None
}
}
}

View File

@@ -1,105 +1,123 @@
mod polygon_draw;
use iced::keyboard::Modifiers;
use polygon_draw::Polygon;
mod music;
use music::Music;
mod history;
use history::Historic;
mod message;
use message::Message;
mod utils;
use utils::str_to_sec;
use utils::{delta_to_string, is_delta_format_valid, str_to_sec};
use std::fs;
use iced::Element;
mod gui;
use gui::{load_file_view, music_view};
use iced::Font;
use iced::Theme;
use iced::{
Font,
widget::{
TextInput, column, text,
text_input::{Icon, Side},
},
};
use iced::{
Length, Task, Theme,
widget::{Column, button, canvas, container, pick_list, row, scrollable, slider},
Color, Event, Task,
event::{self, Status},
keyboard::{Key, key::Named},
};
use regex::Regex;
use std::f32::consts::PI;
use std::time::Instant;
use kira::{
AudioManager, AudioManagerSettings, DefaultBackend, sound::static_sound::StaticSoundData,
};
const FONT_BYTES: &[u8] = include_bytes!("../fonts/EnvyCodeRNerdFontMono-Regular.ttf");
const FONT: Font = Font::with_name("EnvyCodeR Nerd Font Mono");
use crate::utils::delta_to_string;
fn main() -> iced::Result {
let polytheme: Theme = {
let back = Color::from_rgb8(39, 63, 79);
let text = Color::from_rgb8(0xD3, 0xD4, 0xD9);
let prim = Color::from_rgb8(230, 82, 31);
let succ = Color::from_rgb8(0xFF, 0xF9, 0xFB);
let dan = Color::from_rgb8(0xBB, 0x0A, 0x21);
Theme::custom(
String::from("PolyTheme"),
iced::theme::Palette {
background: back,
text: text,
primary: prim,
success: succ,
danger: dan,
},
)
};
iced::application("My App", MyApp::update, MyApp::view)
.theme(|_| Theme::Dark)
.theme(move |_| polytheme.clone())
.font(iced_fonts::REQUIRED_FONT_BYTES)
.font(FONT_BYTES)
.default_font(FONT)
.antialiasing(true)
.subscription(MyApp::subscription)
.run_with(MyApp::new)
}
#[derive(Debug, Clone)]
enum Message {
ButtonPressedIncrement,
ButtonPressedDecrement,
Tick,
AddPolygon(String),
ChangeTeta(usize, f32),
Remove(usize),
ChangeColor(usize, String),
ChangeSound(usize, String),
ToggleSavePanel,
Save,
Load,
FileNameChanged(String),
TogglePaused,
SetMusicLength,
LengthChange(String),
ChangeDelta(f32),
AddPoint,
}
struct MyApp {
music: Music,
time_last_frame: Instant,
show_save_panel: bool,
paused: bool,
audio_manager: AudioManager,
all_sounds: Vec<String>,
all_saves: Vec<String>,
current_delta: f32,
str_music_length: String,
str_time: String,
can_unpaused: bool,
already_save: bool,
mode_file_load: bool,
file_name_to_creat: String,
show_warning_message_creat: bool,
historic: Historic,
}
impl MyApp {
fn new() -> (Self, Task<Message>) {
let manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())
.expect("Error to load AudioManager");
(
Self {
time_last_frame: Instant::now(),
audio_manager: manager,
show_save_panel: false,
paused: true,
all_sounds: load_path_sounds(),
all_saves: load_path_saves(),
music: Music::default(),
current_delta: 0.0,
str_music_length: "01:00:00".to_string(),
str_time: "00:00:00".to_string(),
can_unpaused: true,
already_save: true,
mode_file_load: true,
file_name_to_creat: "Default File Name".to_string(),
show_warning_message_creat: false,
historic: Historic::new(),
},
Task::none(),
)
}
fn update(&mut self, message: Message) {
match message {
Message::ButtonPressedIncrement => self.music.nb_sec_for_rev += 0.5,
Message::ButtonPressedDecrement => {
if self.music.nb_sec_for_rev > 0.5 {
self.music.nb_sec_for_rev -= 0.5;
}
}
Message::AddPolygon(s) => {
self.music.add_polygon(self.current_delta, s);
self.music.add_polygon(self.current_delta, s.clone());
self.already_save = false;
self.historic.add(
Message::Remove(usize::MAX),
Message::AddPolygon(s),
self.current_delta,
);
}
Message::Tick => {
if self.current_delta >= self.music.length {
@@ -112,26 +130,46 @@ impl MyApp {
self.music
.apply_tick(self.current_delta, time_btw, &mut self.audio_manager);
self.time_last_frame = Instant::now();
self.str_time = delta_to_string(self.current_delta);
}
}
Message::Remove(i) => {
self.music.remove_polygon(self.current_delta, i - 1);
let name = self.music.remove_polygon(self.current_delta, i);
self.already_save = false;
self.historic.add(
Message::AddPolygon(name),
Message::Remove(i),
self.current_delta,
);
}
Message::ChangeTeta(i, teta) => {
self.music.set_teta(self.current_delta, i, teta);
}
Message::ChangeColor(i, s) => {
self.music.set_color(self.current_delta, i, s);
let old_teta = self.music.set_teta(self.current_delta, i, teta);
self.already_save = false;
self.historic.add(
Message::ChangeTeta(i, old_teta),
Message::ChangeTeta(i, teta),
self.current_delta,
);
}
Message::ChangeSound(i, s) => {
let sound = StaticSoundData::from_file(format!("./assets/{s}"))
.expect("Fail to load audio");
self.music.set_sound(self.current_delta, i, sound, s);
let old_sound = self
.music
.set_sound(self.current_delta, i, sound, s.clone());
self.already_save = false;
self.historic.add(
Message::ChangeSound(i, old_sound),
Message::ChangeSound(i, s),
self.current_delta,
);
}
Message::Save => {
let json = serde_json::to_string_pretty(&self.music).unwrap();
fs::write(format!("./saves/{0}.pmx", &self.music.file_name), json).unwrap();
self.all_saves = load_path_saves();
self.already_save = true;
}
Message::Load => {
let json = fs::read_to_string(format!("./saves/{0}.pmx", &self.music.file_name));
@@ -139,214 +177,273 @@ impl MyApp {
Ok(j) => {
let decoded: Music = serde_json::from_str(&j).unwrap();
self.music = decoded;
self.music.update_frame(self.current_delta);
self.music.update_frame();
self.mode_file_load = false;
}
Err(e) => {
eprintln!("Error, no saves with this name to load, {e} ");
}
}
}
Message::ToggleSavePanel => self.show_save_panel = !self.show_save_panel,
Message::SetFileNameCreat(s) => self.file_name_to_creat = s,
Message::CreatFile => {
if self.all_saves.contains(&self.file_name_to_creat) {
self.show_warning_message_creat = true;
} else {
self.mode_file_load = false;
self.music = Music::default();
self.music.file_name = self.file_name_to_creat.clone();
self.update(Message::Save);
}
}
Message::FileNameChanged(s) => self.music.file_name = s,
Message::LengthChange(s) => self.str_music_length = s,
Message::LengthChange(s) => {
if is_delta_format_valid(&s) {
let sec = str_to_sec(&s);
if sec > 0. {
self.historic.add(
Message::LengthChange(delta_to_string(self.music.length)),
Message::LengthChange(s.clone()),
self.current_delta,
);
self.music.length = sec;
self.already_save = false;
}
}
self.str_music_length = s;
}
Message::TogglePaused => {
self.paused = !self.paused;
if !self.paused {
self.time_last_frame = Instant::now();
}
}
Message::SetMusicLength => {
if self.is_length_valid() {
self.music.length = str_to_sec(&self.str_music_length)
}
}
Message::ChangeDelta(f) => {
self.current_delta = f;
self.music.fix_teta(self.current_delta);
// update the red dot on canvas
self.update_canvas_if_paused();
}
Message::ChangeDeltaString(s) => {
if is_delta_format_valid(&s) {
self.already_save = false;
self.update(Message::ChangeDelta(str_to_sec(&s)));
}
self.str_time = s;
}
Message::ReAddPoint => {
self.already_save = false;
self.music.add_point_old();
}
Message::AddPoint => {
self.already_save = false;
self.music.add_point(self.current_delta);
self.historic
.add(Message::RemovePoint, Message::AddPoint, self.current_delta);
}
Message::RemovePoint => {
self.already_save = false;
self.music.remove_point(self.current_delta);
self.historic.add(
Message::ReAddPoint,
Message::RemovePoint,
self.current_delta,
);
}
Message::ClickedOnTimeLine(f) => {
self.update(Message::ChangeDelta(f));
}
Message::SlidePointLeft => {
self.already_save = false;
self.music.slide_to_left(self.current_delta);
self.music.fix_teta(self.current_delta);
self.update_canvas_if_paused();
self.historic.add(
Message::SlidePointRight,
Message::SlidePointLeft,
self.current_delta,
);
}
Message::SlidePointRight => {
self.already_save = false;
self.music.slide_to_right(self.current_delta);
self.music.fix_teta(self.current_delta);
self.update_canvas_if_paused();
self.historic.add(
Message::SlidePointLeft,
Message::SlidePointRight,
self.current_delta,
);
}
Message::ChangeDegree(i, s) => {
let mut mut_s = s;
if mut_s.len() == 0 {
mut_s = "0".to_string();
}
match mut_s.parse::<f32>() {
Ok(val) => {
if val >= 0. && val <= 360. {
self.update(Message::ChangeTeta(i, (val).to_radians()));
self.already_save = false;
}
}
Err(_) => {}
}
}
Message::ChangeNbPerSec(s) => {
let mut_s = s.trim_end_matches(" sec/rev");
if mut_s.len() != s.len() {
match mut_s.parse::<f32>() {
Ok(val) => {
let val = (val * 10.).floor() / 10.;
if val >= 1. && val < 1000. {
self.historic.add(
Message::ChangeNbPerSec(format!(
"{:.1} sec/rev",
self.music.nb_sec_for_rev
)),
Message::ChangeNbPerSec(s.clone()),
self.current_delta,
);
self.music.nb_sec_for_rev = val;
self.already_save = false;
}
}
Err(_) => {}
}
}
}
Message::CancelColor(i) => {
self.music.set_color_picker(self.current_delta, i, false);
self.can_unpaused = true;
}
Message::SubmitColor(i) => {
if !self.paused {
self.update(Message::TogglePaused);
}
self.can_unpaused = false;
self.music.set_color_picker(self.current_delta, i, true);
}
Message::ChooseColor(i, color) => {
let old_color = self.music.set_color(self.current_delta, i, color);
self.music.set_color_picker(self.current_delta, i, false);
self.can_unpaused = true;
self.already_save = false;
self.historic.add(
Message::ChooseColor(i, old_color),
Message::ChooseColor(i, color),
self.current_delta,
);
}
Message::GoToLoadView => {
if self.already_save {
self.historic = Historic::new();
self.paused = true;
self.file_name_to_creat = "Default File Name".to_string();
self.show_warning_message_creat = false;
self.mode_file_load = true
}
}
Message::ForceToQuit => {
self.already_save = true;
}
Message::Undo => {
if let Some(item) = self.historic.undo() {
self.historic.ignore_add = true;
self.update(Message::ChangeDelta(item.1));
self.update(item.0);
self.historic.ignore_add = false;
}
}
Message::Redo => {
if let Some(item) = self.historic.redo() {
self.historic.ignore_add = true;
self.update(Message::ChangeDelta(item.1));
self.update(item.0);
self.historic.ignore_add = false;
}
}
Message::None => {}
}
}
fn view(&self) -> iced::Element<Message> {
if !self.mode_file_load {
music_view(self)
} else {
load_file_view(self)
}
}
fn subscription(&self) -> iced::Subscription<Message> {
let subscr_key = event::listen_with(|event, status, _| match (event, status) {
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Character(c),
modifiers: Modifiers::CTRL,
..
}),
Status::Ignored,
) if c.as_ref() == "z" => Some(Message::Undo),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Character(c),
modifiers: Modifiers::CTRL,
..
}),
Status::Ignored,
) if c.as_ref() == "y" => Some(Message::Redo),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Character(c),
modifiers: Modifiers::CTRL,
..
}),
Status::Ignored,
) if c.as_ref() == "s" => Some(Message::Save),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Named(Named::Space),
..
}),
Status::Ignored,
) => Some(Message::TogglePaused),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Named(Named::ArrowUp),
..
}),
Status::Ignored,
) => Some(Message::ChangeDelta(0.0)),
(
Event::Keyboard(iced::keyboard::Event::KeyPressed {
key: Key::Character(c),
modifiers: Modifiers::CTRL,
..
}),
Status::Ignored,
) if c.as_ref() == "f" => Some(Message::ForceToQuit),
_ => None,
});
if self.paused {
subscr_key
} else {
iced::Subscription::batch([
subscr_key,
iced::time::every(std::time::Duration::from_millis(16)).map(|_| Message::Tick),
])
}
}
fn update_canvas_if_paused(&mut self) {
if self.paused {
self.update(Message::TogglePaused);
self.update(Message::Tick);
self.update(Message::TogglePaused);
}
}
Message::AddPoint => {
self.music.add_point(self.current_delta);
}
}
}
fn view(&self) -> iced::Element<Message> {
let txt_nb_rev = if self.music.nb_sec_for_rev % 1. != 0.0 {
format!("{} sec/revolution", self.music.nb_sec_for_rev)
} else {
format!("{}.0 sec/revolution", self.music.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
.music
.current_frame(self.current_delta)
.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(10)
.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 = scrollable(Column::with_children(polygon_rows).spacing(20));
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.music.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.music.file_name),
move |s| Message::FileNameChanged(s),
)
.into(),
);
save_panel.push(button("Load").on_press(Message::Load).into());
}
column![
text(&self.music.file_name).size(32.0),
row(save_panel).spacing(20),
row![
text("Music Length").size(20),
TextInput::new("MM:SS:CS", &self.str_music_length)
.on_input(|new_value| Message::LengthChange(new_value))
.icon(Icon {
font: Font::DEFAULT,
code_point: if self.is_length_valid() {
'\u{2705}'
} else {
'\u{274C}'
},
size: None,
spacing: 10.,
side: Side::Left,
}),
button("Valid").on_press(Message::SetMusicLength),
text(txt_nb_rev).size(20),
button("Increment").on_press(Message::ButtonPressedIncrement),
button("Decrement").on_press(Message::ButtonPressedDecrement),
]
.spacing(20),
row![
container(
canvas(self.music.current_frame(self.current_delta))
.height(Length::FillPortion(1))
.width(Length::FillPortion(1))
),
column![
text("Polygon options"),
pick_list(all_options, Some("Choose polygon".to_string()), |s| {
Message::AddPolygon(s)
}),
polygon_column,
]
.spacing(10)
.height(Length::FillPortion(1))
.width(Length::FillPortion(2)),
]
.height(Length::FillPortion(2))
.spacing(20),
column![
row![
button("Toggle Play").on_press(Message::TogglePaused),
text(format!(
"{}/{}",
delta_to_string(self.current_delta),
delta_to_string(self.music.length)
))
.size(20.0),
button("Add Point").on_press(Message::AddPoint)
]
.spacing(20),
column![
slider(0.0..=self.music.length, self.current_delta, move |f| {
Message::ChangeDelta(f)
})
.step(&self.music.length / 10_000.),
canvas(&self.music)
.height(Length::FillPortion(1))
.width(Length::FillPortion(1))
]
.spacing(0),
]
.height(Length::FillPortion(1))
.width(Length::FillPortion(1)),
]
.spacing(25)
.padding(25)
.into()
}
fn is_length_valid(&self) -> bool {
let re = Regex::new(r"^\d{2}:\d{2}:\d{2}$").unwrap();
re.is_match(self.str_music_length.as_str())
}
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 load_path_sounds() -> Vec<String> {
@@ -361,7 +458,7 @@ fn load_path_sounds() -> Vec<String> {
fn load_path_saves() -> Vec<String> {
fs::create_dir_all("./saves").expect("fail to creat 'saves' !");
fs::read_dir("./saves")
let mut saves: Vec<String> = fs::read_dir("./saves")
.unwrap()
.filter_map(|res| res.ok())
.map(|e| {
@@ -373,5 +470,7 @@ fn load_path_saves() -> Vec<String> {
.trim_end_matches(".pmx")
.to_string()
})
.collect()
.collect();
saves.sort();
saves
}

38
src/message.rs Normal file
View File

@@ -0,0 +1,38 @@
use iced::Color;
#[derive(Debug, Clone)]
pub enum Message {
None,
CreatFile,
SetFileNameCreat(String),
GoToLoadView,
ForceToQuit,
Tick,
Save,
Load,
FileNameChanged(String),
TogglePaused,
ChangeDelta(f32),
ClickedOnTimeLine(f32),
ChangeDeltaString(String),
ChangeNbPerSec(String),
AddPolygon(String),
ChangeTeta(usize, f32),
Remove(usize),
ChangeSound(usize, String),
LengthChange(String),
AddPoint,
RemovePoint,
SlidePointLeft,
SlidePointRight,
ChangeDegree(usize, String),
ReAddPoint,
ChooseColor(usize, Color),
CancelColor(usize),
SubmitColor(usize),
Undo,
Redo,
}

View File

@@ -1,15 +1,25 @@
use crate::message::Message;
use crate::polygon_draw::*;
use crate::utils::string_to_polygon;
use crate::{polygon_draw::*, utils::string_to_color};
use serde::{Deserialize, Serialize};
use kira::{AudioManager, sound::static_sound::StaticSoundData};
use iced::mouse;
use iced::event::Status;
use iced::mouse::Cursor;
use iced::widget::canvas::{Event, Geometry};
use iced::{
Size,
mouse::{self, ScrollDelta},
};
use iced::{Vector, keyboard};
use iced::widget::canvas;
use iced::widget::canvas::Stroke;
use iced::widget::canvas::Style;
use iced::{Color, Rectangle, Renderer, Theme};
use std::f32::consts::PI;
use std::mem::swap;
use std::time::Duration;
#[derive(Serialize, Deserialize)]
pub struct Music {
@@ -21,6 +31,8 @@ pub struct Music {
teta: f32,
#[serde(skip)]
pub current_delta: f32,
#[serde(skip)]
point_removed: Vec<(f32, PolygonFrame)>,
}
impl Music {
@@ -35,6 +47,17 @@ impl Music {
&mut self.poly_frame.last_mut().unwrap().1
}
}
fn find_index_frame(&mut self, delta: f32) -> usize {
if let Some(i) = self
.poly_frame
.windows(2)
.position(|w| w[0].0 <= delta && delta < w[1].0)
{
i
} else {
self.poly_frame.len() - 1
}
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~PUBLIC~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pub fn current_frame(&self, delta: f32) -> &PolygonFrame {
if let Some(i) = self
@@ -52,15 +75,18 @@ impl Music {
Music {
poly_frame: vec![(0.0, PolygonFrame::default())],
nb_sec_for_rev: 1.0,
file_name: "Default Name".to_string(),
file_name: "Default File Name".to_string(),
length: 60.0,
teta: 0.,
current_delta: 0.,
point_removed: vec![],
}
}
pub fn update_frame(&mut self, delta: f32) {
self.find_poly_frame(delta).update();
pub fn update_frame(&mut self) {
for (_, p) in &mut self.poly_frame {
p.update();
}
}
pub fn fix_teta(&mut self, delta: f32) {
@@ -93,20 +119,30 @@ impl Music {
index: usize,
sound: StaticSoundData,
sound_name: String,
) {
) -> String {
let current_frame = self.find_poly_frame(delta);
current_frame.polygons[index].sound = sound;
let out = current_frame.polygons[index].sound_name.clone();
current_frame.polygons[index].sound_name = sound_name;
out
}
pub fn set_teta(&mut self, delta: f32, index: usize, teta: f32) {
pub fn set_teta(&mut self, delta: f32, index: usize, teta: f32) -> f32 {
let out = self.find_poly_frame(delta).polygons[index].global_teta;
self.find_poly_frame(delta).polygons[index].global_teta = teta;
out
}
pub fn set_color(&mut self, delta: f32, index: usize, color_name: String) {
pub fn set_color(&mut self, delta: f32, index: usize, color: Color) -> Color {
let current_frame = self.find_poly_frame(delta);
current_frame.polygons[index].color = string_to_color(&color_name);
current_frame.polygons[index].color_name = color_name;
let out = current_frame.polygons[index].color.clone();
current_frame.polygons[index].color = color;
out
}
pub fn set_color_picker(&mut self, delta: f32, i: usize, b: bool) {
let current_frame = self.find_poly_frame(delta);
current_frame.polygons[i].show_color_picker = b;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ADD/REMOVE~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -119,6 +155,22 @@ impl Music {
self.poly_frame
.insert(pos, (delta, self.current_frame(delta).clone()));
}
pub fn add_point_old(&mut self) {
if let Some(pair) = self.point_removed.pop() {
let pos = self
.poly_frame
.binary_search_by(|(d, _)| d.partial_cmp(&pair.0).unwrap())
.unwrap_or_else(|e| e);
self.poly_frame.insert(pos, pair);
}
}
pub fn remove_point(&mut self, delta: f32) {
let i = self.find_index_frame(delta);
if i != 0 {
self.point_removed.push(self.poly_frame[i].clone());
self.poly_frame.remove(i);
}
}
pub fn add_polygon(&mut self, delta: f32, polygon_name: String) {
let current_frame = self.find_poly_frame(delta);
@@ -126,50 +178,108 @@ impl Music {
current_frame.polygons.push(poly);
}
pub fn remove_polygon(&mut self, delta: f32, i: usize) {
self.find_poly_frame(delta).polygons.remove(i);
pub fn remove_polygon(&mut self, delta: f32, i: usize) -> String {
let pf = self.find_poly_frame(delta);
let mut i = i;
if i == usize::MAX {
i = pf.polygons.len() - 1
}
let out = pf.polygons[i].name.clone();
pf.polygons.remove(i);
out
}
pub fn slide_to_left(&mut self, delta: f32) {
let i = self.find_index_frame(delta);
if i > 0 {
let (left, right) = self.poly_frame.split_at_mut(i);
swap(&mut left[i - 1].1, &mut right[0].1);
}
}
pub fn slide_to_right(&mut self, delta: f32) {
let i = self.find_index_frame(delta);
if i < self.poly_frame.len() - 1 {
let (left, right) = self.poly_frame.split_at_mut(i + 1);
swap(&mut left[i].1, &mut right[0].1);
}
}
}
impl<Message> canvas::Program<Message> for Music {
#[derive(Default, Debug)]
pub struct StateMusic {
mouse_left: bool,
ctrl: bool,
zoom: f32,
offset: f32,
}
impl canvas::Program<Message> for Music {
// No internal state
type State = ();
type State = StateMusic;
fn draw(
&self,
_state: &(),
state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let mut geo_small_frame: Vec<Geometry> = vec![];
let mut geo_cursor: Vec<Geometry> = vec![];
let frame = canvas::Frame::new(renderer, bounds.size());
let global_width = bounds.width * state.zoom;
let mut toggle_color = true;
let padding = 8.;
let w = bounds.width - (padding * 2.);
for (delta, _) in &self.poly_frame {
let x = delta / self.length * w + padding;
frame.fill_rectangle(
iced::Point { x: x, y: 0.0 },
frame.size(),
if toggle_color {
Color::from_rgb(0.3, 0.3, 0.3)
} else {
Color::from_rgb(0.1, 0.1, 0.1)
},
);
toggle_color = !toggle_color;
}
frame.stroke_rectangle(
iced::Point { x: 0.0, y: 0.0 },
frame.size(),
Stroke {
width: 16.0,
..Stroke::default()
let w = global_width - (padding * 2.);
for (delta, polyframe) in &self.poly_frame {
let x = delta / self.length * w + 8. - state.offset;
let mut back_frame = canvas::Frame::new(
renderer,
Size {
width: global_width.clamp(0.0, bounds.width),
height: bounds.height,
},
);
let x = self.current_delta / self.length * w + padding;
frame.stroke_rectangle(
back_frame.fill_rectangle(
iced::Point { x: x, y: 0.0 },
Size {
width: global_width,
height: bounds.height,
},
if toggle_color {
Color::from_rgb8(27, 60, 83)
} else {
Color::from_rgb8(69, 104, 130)
},
);
geo_small_frame.push(back_frame.into_geometry());
toggle_color = !toggle_color;
let mut small_frame = canvas::Frame::new(
renderer,
Size {
width: global_width.clamp(0.0, bounds.width),
height: bounds.height,
},
);
small_frame.translate(Vector {
x: x + (bounds.height / 10.),
y: (bounds.height / 10.),
});
polyframe.draw_in_frame(
&mut small_frame,
Size {
width: (8. * bounds.height) / 10.,
height: (8. * bounds.height) / 10.,
},
);
geo_small_frame.push(small_frame.into_geometry());
}
let x = self.current_delta / self.length * w + 8. - state.offset;
let mut frame_cursor = canvas::Frame::new(renderer, bounds.size());
frame_cursor.stroke_rectangle(
iced::Point::new(x, 0.),
iced::Size {
width: 0.,
@@ -177,12 +287,111 @@ impl<Message> canvas::Program<Message> for Music {
},
Stroke {
width: 4.0,
style: Style::Solid(Color::from_rgb(1.0, 0.0, 0.0)),
style: Style::Solid(Color::from_rgba(1.0, 0.0, 0.0, 1.)),
..Stroke::default()
},
);
frame_cursor.stroke_rectangle(
iced::Point {
x: -state.offset,
y: 0.0,
},
iced::Size {
width: global_width,
height: bounds.height,
},
Stroke {
width: 16.0,
style: Style::Solid(Color::from_rgb8(207, 74, 28)),
..Stroke::default()
},
);
geo_cursor.push(frame_cursor.into_geometry());
//vec_geo.push(frame.into_geometry());
// Then, we produce the geometry
vec![frame.into_geometry()]
let mut out = vec![frame.into_geometry()];
out.append(&mut geo_small_frame);
out.append(&mut geo_cursor);
out
}
fn update(
&self,
state: &mut Self::State,
event: Event,
bounds: Rectangle,
cursor: Cursor,
) -> (Status, Option<Message>) {
//eprintln!("event = {:?}", event);
if let Event::Keyboard(keyboard_event) = &event {
match keyboard_event {
keyboard::Event::ModifiersChanged(m) => {
state.ctrl = m.control();
}
_ => (),
}
}
if let Event::Mouse(mouse_event) = event {
match mouse_event {
mouse::Event::ButtonPressed(mouse::Button::Left) => {
state.mouse_left = true;
if let Some(position) = cursor.position_in(bounds) {
let pos_x =
(position.x + state.offset - 8.0) / (bounds.width * state.zoom - 16.);
let delta = (pos_x * self.length).clamp(0., self.length);
return (
Status::Captured,
Some(crate::message::Message::ClickedOnTimeLine(delta)),
);
}
}
mouse::Event::ButtonReleased(mouse::Button::Left) => state.mouse_left = false,
mouse::Event::CursorMoved { position: _ } => {
if let Some(position) = cursor.position_in(bounds)
&& state.mouse_left
{
let pos_x =
(position.x + state.offset - 8.0) / (bounds.width * state.zoom - 16.);
let delta = (pos_x * self.length).clamp(0., self.length);
return (Status::Captured, Some(Message::ClickedOnTimeLine(delta)));
}
}
mouse::Event::WheelScrolled { delta: d } => {
if state.ctrl {
match d {
ScrollDelta::Lines { x: _, y } => {
let before = bounds.width * state.zoom;
state.zoom += y / 10.;
state.offset = state.offset * (bounds.width * state.zoom) / before;
}
ScrollDelta::Pixels { x: _, y } => {
let before = bounds.width * state.zoom;
state.zoom += y / 10.;
state.offset = state.offset * (bounds.width * state.zoom) / before;
}
}
} else {
match d {
ScrollDelta::Lines { x: _, y } => {
state.offset -= y * 32. * state.zoom;
}
ScrollDelta::Pixels { x: _, y } => {
state.offset -= y * 32. * state.zoom;
}
}
}
}
_ => {}
}
}
state.zoom = state.zoom.clamp(1.0, 10.);
if state.offset + bounds.width >= bounds.width * state.zoom {
state.offset = bounds.width * state.zoom - bounds.width
}
if state.offset <= 0. {
state.offset = 0.
}
(Status::Ignored, None)
}
}

View File

@@ -2,12 +2,12 @@ use crate::utils::string_to_color;
use std::f32::consts::PI;
use iced::Size;
use iced::Vector;
use iced::mouse;
use iced::widget::canvas;
use iced::widget::canvas::Stroke;
use iced::widget::canvas::Style;
use iced::{Color, Rectangle, Renderer, Theme};
use iced::widget::canvas::{Frame, Stroke, Style};
use iced::{Color, Point, Rectangle, Renderer, Theme};
use kira::sound::static_sound::StaticSoundData;
use serde::{Deserialize, Serialize};
@@ -49,6 +49,65 @@ impl PolygonFrame {
polygons: vec![],
}
}
pub fn draw_in_frame(&self, frame: &mut Frame, size: Size) {
let radius = size.height / 2.0 - (size.height / 10.);
let mut vec = Vector::new(0.0, -radius);
let c = Point {
x: size.width / 2.,
y: size.height / 2.,
};
// Draw Circle
let circle = canvas::Path::circle(c, radius);
frame.stroke(
&circle,
Stroke {
width: 6.0,
style: Style::Solid(Color::from_rgba8(210, 193, 182, 0.25)),
..Stroke::default()
},
);
// Draw all polygons by there points
for poly in &self.polygons {
let l = poly.points_teta.len();
let mut line: canvas::Path;
let mut t1: f32;
let mut t2: f32;
let mut color: Color;
for i in 0..l {
t1 = poly.points_teta[i] + poly.global_teta;
t2 = poly.points_teta[(i + 1) % l] + poly.global_teta;
line = canvas::Path::line(c + vec.rotate(t1), c + vec.rotate(t2));
color = poly.color.clone();
color.a = 0.25;
frame.stroke(
&line,
Stroke {
width: 4.0,
style: Style::Solid(poly.color.clone()),
..Stroke::default()
},
);
}
}
/*
// Draw the red dot on the current position
let dot = canvas::Path::circle(frame.center() + vec.rotate(self.teta), 10.0);
frame.fill(&dot, Color::from_rgb(1.0, 0.0, 0.0));
*/
// Draw the frame
/*
frame.stroke_rectangle(
iced::Point { x: 0.0, y: 0.0 },
size,
Stroke {
width: 8.0,
style: Style::Solid(Color::from_rgb8(207, 74, 28)),
..Stroke::default()
},
);*/
}
}
impl<Message> canvas::Program<Message> for PolygonFrame {
@@ -67,13 +126,18 @@ impl<Message> canvas::Program<Message> for PolygonFrame {
let radius = frame.size().width.min(frame.size().height) / 2.0 - 32.0;
let mut vec = Vector::new(0.0, -radius);
let c = frame.center();
frame.fill_rectangle(
iced::Point { x: 0., y: 0. },
frame.size(),
Color::from_rgb8(27, 60, 83),
);
// Draw Circle
let circle = canvas::Path::circle(frame.center(), radius);
frame.stroke(
&circle,
Stroke {
width: 6.0,
style: Style::Solid(Color::from_rgba(0.0, 0.0, 0.0, 1.0)),
style: Style::Solid(Color::from_rgb8(210, 193, 182)),
..Stroke::default()
},
);
@@ -100,7 +164,7 @@ impl<Message> canvas::Program<Message> for PolygonFrame {
}
// Draw the red dot on the current position
let dot = canvas::Path::circle(frame.center() + vec.rotate(self.teta), 5.0);
let dot = canvas::Path::circle(frame.center() + vec.rotate(self.teta), 10.0);
frame.fill(&dot, Color::from_rgb(1.0, 0.0, 0.0));
// Draw the frame
@@ -109,6 +173,7 @@ impl<Message> canvas::Program<Message> for PolygonFrame {
frame.size(),
Stroke {
width: 16.0,
style: Style::Solid(Color::from_rgb8(207, 74, 28)),
..Stroke::default()
},
);
@@ -129,6 +194,8 @@ pub struct Polygon {
#[serde(skip)]
pub color: Color,
pub color_name: String,
#[serde(skip)]
pub show_color_picker: bool,
}
#[warn(dead_code)]
impl Polygon {
@@ -153,61 +220,55 @@ impl Polygon {
}
sound_to_play
}
pub fn default(sound: StaticSoundData) -> Self {
Polygon {
global_teta: 0.,
points_teta: vec![],
sound: sound,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
name: "".to_string(),
color: Color::BLACK,
show_color_picker: false,
}
}
pub fn n_gon(teta: f32, n_side: u8, sound: StaticSoundData) -> Self {
pub fn n_gon(n_side: u8, sound: StaticSoundData) -> Self {
let mut v: Vec<f32> = Vec::with_capacity(n_side as usize);
for i in 0..n_side {
v.push((i as f32 * 2.0 * PI) / n_side as f32);
}
Polygon {
global_teta: teta,
points_teta: v,
sound: sound,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
name: "".to_string(),
color: Color::BLACK,
let mut p = Polygon::default(sound);
p.points_teta = v;
p
}
}
pub fn segment(teta: f32, sound: StaticSoundData) -> Self {
Polygon::n_gon(teta, 2, sound)
pub fn segment(sound: StaticSoundData) -> Self {
Polygon::n_gon(2, sound)
}
pub fn triangle(teta: f32, sound: StaticSoundData) -> Self {
Polygon::n_gon(teta, 3, sound)
pub fn triangle(sound: StaticSoundData) -> Self {
Polygon::n_gon(3, sound)
}
pub fn square(teta: f32, sound: StaticSoundData) -> Self {
Polygon::n_gon(teta, 4, sound)
pub fn square(sound: StaticSoundData) -> Self {
Polygon::n_gon(4, sound)
}
pub fn nr_6_in_30(teta: f32, sound: StaticSoundData) -> Self {
Polygon {
sound: sound,
name: "".to_string(),
color: Color::BLACK,
global_teta: teta,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
points_teta: vec![
pub fn nr_6_in_30(sound: StaticSoundData) -> Self {
let mut p = Polygon::default(sound);
p.points_teta = vec![
2.0 * 5.0 * PI / 30.0,
2.0 * 6.0 * PI / 30.0,
2.0 * 12.0 * PI / 30.0,
2.0 * 18.0 * PI / 30.0,
2.0 * 24.0 * PI / 30.0,
2.0 * 25.0 * PI / 30.0,
],
];
p
}
}
pub fn nr_7_in_30(teta: f32, sound: StaticSoundData) -> Self {
Polygon {
sound: sound,
name: "".to_string(),
color: Color::BLACK,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
global_teta: teta,
points_teta: vec![
pub fn nr_7_in_30(sound: StaticSoundData) -> Self {
let mut p = Polygon::default(sound);
p.points_teta = vec![
0.0,
2.0 * 6.0 * PI / 30.0,
2.0 * 7.0 * PI / 30.0,
@@ -215,18 +276,12 @@ impl Polygon {
2.0 * 17.0 * PI / 30.0,
2.0 * 23.0 * PI / 30.0,
2.0 * 24.0 * PI / 30.0,
],
];
p
}
}
pub fn nr_8_in_30(teta: f32, sound: StaticSoundData) -> Self {
Polygon {
sound: sound,
name: "".to_string(),
color: Color::BLACK,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
global_teta: teta,
points_teta: vec![
pub fn nr_8_in_30(sound: StaticSoundData) -> Self {
let mut p = Polygon::default(sound);
p.points_teta = vec![
2.0 * PI / 30.0,
2.0 * 5.0 * PI / 30.0,
2.0 * 11.0 * PI / 30.0,
@@ -235,18 +290,12 @@ impl Polygon {
2.0 * 19.0 * PI / 30.0,
2.0 * 25.0 * PI / 30.0,
2.0 * 29.0 * PI / 30.0,
],
];
p
}
}
pub fn nr_9_in_30(teta: f32, sound: StaticSoundData) -> Self {
Polygon {
sound: sound,
name: "".to_string(),
color: Color::BLACK,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
global_teta: teta,
points_teta: vec![
pub fn nr_9_in_30(sound: StaticSoundData) -> Self {
let mut p = Polygon::default(sound);
p.points_teta = vec![
0.0,
2.0 * PI / 30.0,
2.0 * 7.0 * PI / 30.0,
@@ -256,18 +305,12 @@ impl Polygon {
2.0 * 19.0 * PI / 30.0,
2.0 * 23.0 * PI / 30.0,
2.0 * 29.0 * PI / 30.0,
],
];
p
}
}
pub fn nr_8_in_42(teta: f32, sound: StaticSoundData) -> Self {
Polygon {
sound: sound,
name: "".to_string(),
color: Color::BLACK,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
global_teta: teta,
points_teta: vec![
pub fn nr_8_in_42(sound: StaticSoundData) -> Self {
let mut p = Polygon::default(sound);
p.points_teta = vec![
2.0 * 3.0 * PI / 42.0,
2.0 * 9.0 * PI / 42.0,
2.0 * 14.0 * PI / 42.0,
@@ -276,18 +319,12 @@ impl Polygon {
2.0 * 28.0 * PI / 42.0,
2.0 * 33.0 * PI / 42.0,
2.0 * 39.0 * PI / 42.0,
],
];
p
}
}
pub fn nr_9_in_42(teta: f32, sound: StaticSoundData) -> Self {
Polygon {
sound: sound,
name: "".to_string(),
color: Color::BLACK,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
global_teta: teta,
points_teta: vec![
pub fn nr_9_in_42(sound: StaticSoundData) -> Self {
let mut p = Polygon::default(sound);
p.points_teta = vec![
0.0,
2.0 * 6.0 * PI / 42.0,
2.0 * 11.0 * PI / 42.0,
@@ -297,18 +334,12 @@ impl Polygon {
2.0 * 30.0 * PI / 42.0,
2.0 * 31.0 * PI / 42.0,
2.0 * 36.0 * PI / 42.0,
],
];
p
}
}
pub fn nr_10a_in_42(teta: f32, sound: StaticSoundData) -> Self {
Polygon {
sound: sound,
name: "".to_string(),
color: Color::BLACK,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
global_teta: teta,
points_teta: vec![
pub fn nr_10a_in_42(sound: StaticSoundData) -> Self {
let mut p = Polygon::default(sound);
p.points_teta = vec![
2.0 * 6.0 * PI / 42.0,
2.0 * 7.0 * PI / 42.0,
2.0 * 11.0 * PI / 42.0,
@@ -319,18 +350,12 @@ impl Polygon {
2.0 * 31.0 * PI / 42.0,
2.0 * 35.0 * PI / 42.0,
2.0 * 36.0 * PI / 42.0,
],
];
p
}
}
pub fn nr_10b_in_42(teta: f32, sound: StaticSoundData) -> Self {
Polygon {
sound: sound,
name: "".to_string(),
color: Color::BLACK,
sound_name: "tick.ogg".to_string(),
color_name: "Black".to_string(),
global_teta: teta,
points_teta: vec![
pub fn nr_10b_in_42(sound: StaticSoundData) -> Self {
let mut p = Polygon::default(sound);
p.points_teta = vec![
0.0,
2.0 * 1.0 * PI / 42.0,
2.0 * 5.0 * PI / 42.0,
@@ -341,8 +366,8 @@ impl Polygon {
2.0 * 29.0 * PI / 42.0,
2.0 * 30.0 * PI / 42.0,
2.0 * 41.0 * PI / 42.0,
],
}
];
p
}
}
fn dummy_sound() -> StaticSoundData {

View File

@@ -1,6 +1,12 @@
use crate::Polygon;
use iced::Color;
use kira::sound::static_sound::StaticSoundData;
use regex::Regex;
pub fn is_delta_format_valid(str: &str) -> bool {
let re = Regex::new(r"^\d{1,2}:\d{1,2}:\d{1,2}$").unwrap();
re.is_match(str)
}
pub fn string_to_color<S: AsRef<str>>(s: S) -> Color {
match s.as_ref() {
"Green" => Color::from_rgb(0.0, 1.0, 0.0),
@@ -43,46 +49,46 @@ pub fn string_to_polygon<S: AsRef<str>>(str: S) -> Polygon {
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, dummy_sound());
poly = Polygon::n_gon(sides, dummy_sound());
} else {
poly = Polygon::n_gon(0.0, 0, dummy_sound());
poly = Polygon::n_gon(0, dummy_sound());
}
} else {
match s {
"Segment" => {
poly = Polygon::segment(0.0, dummy_sound());
poly = Polygon::segment(dummy_sound());
}
"Triangle" => {
poly = Polygon::triangle(0.0, dummy_sound());
poly = Polygon::triangle(dummy_sound());
}
"Square" => {
poly = Polygon::square(0.0, dummy_sound());
poly = Polygon::square(dummy_sound());
}
"Nr6In30" => {
poly = Polygon::nr_6_in_30(0.0, dummy_sound());
poly = Polygon::nr_6_in_30(dummy_sound());
}
"Nr7In30" => {
poly = Polygon::nr_7_in_30(0.0, dummy_sound());
poly = Polygon::nr_7_in_30(dummy_sound());
}
"Nr8In30" => {
poly = Polygon::nr_8_in_30(0.0, dummy_sound());
poly = Polygon::nr_8_in_30(dummy_sound());
}
"Nr9In30" => {
poly = Polygon::nr_9_in_30(0.0, dummy_sound());
poly = Polygon::nr_9_in_30(dummy_sound());
}
"Nr8In42" => {
poly = Polygon::nr_8_in_42(0.0, dummy_sound());
poly = Polygon::nr_8_in_42(dummy_sound());
}
"Nr9In42" => {
poly = Polygon::nr_9_in_42(0.0, dummy_sound());
poly = Polygon::nr_9_in_42(dummy_sound());
}
"Nr10aIn42" => {
poly = Polygon::nr_10a_in_42(0.0, dummy_sound());
poly = Polygon::nr_10a_in_42(dummy_sound());
}
"Nr10bIn42" => {
poly = Polygon::nr_10b_in_42(0.0, dummy_sound());
poly = Polygon::nr_10b_in_42(dummy_sound());
}
_ => poly = Polygon::n_gon(0.0, 0, dummy_sound()),
_ => poly = Polygon::n_gon(0, dummy_sound()),
}
}
poly.name = s.to_string();