This commit is contained in:
2025-09-20 20:36:57 -05:00
commit 39208984f8
11 changed files with 4454 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/rossnome.iml" filepath="$PROJECT_DIR$/.idea/rossnome.iml" />
</modules>
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
Generated
+4162
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "rossnome"
version = "0.1.0"
edition = "2024"
[dependencies]
eframe = "0.32.3"
tokio = { version = "1.47.1", features = ["rt", "rt-multi-thread", "macros"] }
+100
View File
@@ -0,0 +1,100 @@
use eframe::egui;
use eframe::egui::{Color32, Context, Id, Margin, PointerButton, Pos2, Rangef, Response, Stroke, TopBottomPanel, Ui, Vec2, Widget};
use crate::nome::LOOP_LENGTH;
pub enum NoteType {
Sine,
Triangle,
Sawtooth,
Square,
Noise
}
impl NoteType {
fn color(&self) -> Color32 {
match self {
NoteType::Sine => Color32::from_rgb(3, 211, 252),
NoteType::Triangle => Color32::from_rgb(252, 202, 3),
NoteType::Sawtooth => Color32::from_rgb(252, 18, 61),
NoteType::Square => Color32::from_rgb(4, 219, 51),
NoteType::Noise => Color32::from_rgb(120, 120, 120),
}
}
}
pub struct Grid {
note_type: NoteType,
volume: f32,
notes: Vec<bool>,
pub id: String
}
impl Grid {
pub fn for_note_type(note_type: NoteType, counter: usize) -> Self {
Self {
note_type,
volume: 1.0,
notes: vec![false; (LOOP_LENGTH * LOOP_LENGTH) as usize],
id: format!("Track {}", counter)
}
}
pub fn show(&mut self, ctx: &Context, cursor: f32) {
let id = Id::from(self.id.clone());
let win = egui::Window::new(&self.id).resizable(false).id(id).scroll([false, false]);
win.show(ctx, |ui| {
self.frame(ui, cursor)
});
}
fn frame(&mut self, ui: &mut Ui, cursor: f32) {
egui::Frame::new().inner_margin(3).show(ui, |ui| {
let dim = 20.0 * LOOP_LENGTH as f32;
ui.set_width(dim);
ui.set_height(dim);
let rect = ui.clip_rect().translate(Vec2::new(6.0, 6.0));
//ui.painter().debug_rect(rect, Color32::from_rgb(0, 255, 0), "blah");
for (i, lit) in self.notes.iter().enumerate() {
let (x, y) = (i as u32 % LOOP_LENGTH, i as u32 / LOOP_LENGTH);
let center = Pos2::new(
(x * 20 + 10) as f32 + rect.left(),
(y * 20 + 10) as f32 + rect.top());
if *lit {
ui.painter().circle_filled(center, 10.0, self.note_type.color());
} else {
ui.painter().circle_stroke(center, 10.0, (1.0, Color32::from_gray(0x88)));
}
}
ui.painter().vline(
cursor * dim + rect.left(),
Rangef::new(rect.top(), rect.top() + dim),
(1.0, self.note_type.color())
);
if ui.response().contains_pointer() {
ui.input(|input| {
if input.pointer.button_clicked(PointerButton::Primary) {
if let Some(pos) = input.pointer.latest_pos() {
println!("window: {} pos: {} {}", self.id, pos.x - rect.left(), pos.y - rect.top());
let x = ((pos.x - rect.left()) / 20.0).floor() as usize;
let y = ((pos.y - rect.top()) / 20.0).floor() as usize;
let n = x + y * LOOP_LENGTH as usize;
self.notes[n] = !self.notes[n]
}
}
})
}
});
}
pub fn notes(&self, beat: u32) -> Vec<u32> {
let mut notes = vec![];
for y in 0..LOOP_LENGTH {
if self.notes[(y * LOOP_LENGTH + beat) as usize] {
notes.push(y)
}
}
notes
}
}
+26
View File
@@ -0,0 +1,26 @@
use std::ops::RangeInclusive;
use eframe::egui;
use eframe::egui::{Context, TopBottomPanel};
use crate::grid::{Grid, NoteType};
use crate::Nome;
impl Nome {
pub fn menu(&mut self, ctx: &Context) {
TopBottomPanel::top("menu_panel").show(ctx, |ui| {
egui::MenuBar::new().ui(ui, |ui| {
if ui.button("Sine").clicked() {
self.add_window(NoteType::Sine)
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add(egui::Slider::new(&mut self.tempo, RangeInclusive::new(20, 180)));
if self.playing {
if ui.button("||").clicked() { self.playing = false }
} else {
if ui.button(">").clicked() { self.playing = true }
}
});
})
});
}
}
+34
View File
@@ -0,0 +1,34 @@
mod gui;
mod nome;
mod grid;
use std::time::Duration;
use eframe::{App, Frame};
use eframe::egui::Context;
use crate::nome::Nome;
#[tokio::main]
async fn main() {
let native_options = eframe::NativeOptions::default();
eframe::run_native("Fleen", native_options, Box::new(|_cc| {
Ok(Box::new(Nome::default()))
})).expect("Error running application");
}
impl App for Nome {
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
let play = self.tick();
let cursor = self.ratio();
self.menu(&ctx);
for g in self.grids.iter_mut() {
g.show(ctx, cursor)
}
if play {
for (track, row) in self.notes_for_beat() {
println!("{}: {}", track, row)
}
}
ctx.request_repaint_after(Duration::from_millis(17))
}
}
+90
View File
@@ -0,0 +1,90 @@
use std::time::Instant;
use crate::grid::{Grid, NoteType};
pub const LOOP_LENGTH: u32 = 16;
pub struct Nome {
/// Tempo in beats per minute
pub tempo: u32,
// A count _in beats_ of where we are in the loop
timer: f32,
/// Whether or not we're playing; false == paused
pub playing: bool,
// The instant of the last time we called tick()
last_tick: Option<Instant>,
/// The grids that we currently have going
pub grids: Vec<Grid>,
// Running count of windows created (for ids)
window_counter: usize
}
impl Default for Nome {
fn default() -> Self {
Self {
tempo: 90,
timer: 0.0,
playing: true,
last_tick: None,
grids: vec![],
window_counter: 0
}
}
}
impl Nome {
/// Call this every frame to update the timer / last tick based on the current instant
/// and the tempo
pub fn tick(&mut self) -> bool {
let now = Instant::now();
let old_beat = self.beat();
if let Some(last) = self.last_tick && self.playing {
let dt = (now - last).as_secs_f32();
let bps = (self.tempo as f32) / 60.0;
// Timer is an amount of time _in beats_ and some of those beats might have been for a
// different tempo.
// We know now a time delta in seconds and a conversion factor to turn that to beats, so:
self.timer += dt * bps;
// 16 beats in the loop, so timer should never be over 16:
while self.timer > LOOP_LENGTH as f32 { self.timer -= LOOP_LENGTH as f32 }
}
// Playing or not, update last_tick so that the next frame adds the correct duration to timer
self.last_tick = Some(now);
// Did we enter a new beat?
self.beat() != old_beat
}
/// Which beat (0..loop_length) we're on
pub fn beat(&self) -> u32 {
self.timer.floor() as u32
}
/// What fraction we are (0.0..1.0) through the loop
/// (multiply by window width to find the x coord to draw the cursor line)
pub fn ratio(&self) -> f32 {
self.timer / LOOP_LENGTH as f32
}
pub fn add_window(&mut self, note_type: NoteType) {
self.window_counter += 1;
self.grids.push(Grid::for_note_type(note_type, self.window_counter));
}
pub fn notes_for_beat(&self) -> Vec<(String, u32)> {
let mut notes = vec![];
let beat = self.beat();
for grid in self.grids.iter() {
for note in grid.notes(beat).into_iter() {
notes.push((grid.id.clone(), note))
}
}
notes
}
}