metronome/rust/uisim/src/main.rs
Me Here 676d9879fa pm-ui: first real metronome screen (header/BPM/transport + polymeter lane grid)
draw_metronome() renders the screen for any parsed track: track name + big BPM,
play/stop transport, and the polymeter lane grid — per-lane beat cells coloured by
level (accent amber / normal cyan / ghost purple / rest dark), playhead highlight,
beat gridlines, poly (~) marker. Pure no_std view over borrowed data (LaneView/
Screen) so the firmware build stays allocator-free.

uisim now parses a real track (track-format) and renders draw_metronome to PNG —
iterate the UI on the host, no bench. Firmware still draws the bring-up diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:38:48 -05:00

79 lines
2.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Render the metronome UI (pm-ui) for a real parsed track onto a 320×480 framebuffer and save a
//! PNG — no device. `cargo run` [program-string]. Lets the UI be developed/reviewed off the bench.
use embedded_graphics::{pixelcolor::Rgb565, prelude::*};
use pm_ui::{LaneView, Screen};
const W: u32 = 320;
const H: u32 = 480;
struct Fb {
px: Vec<Rgb565>,
}
impl Fb {
fn new() -> Self {
Fb { px: vec![Rgb565::BLACK; (W * H) as usize] }
}
}
impl DrawTarget for Fb {
type Color = Rgb565;
type Error = core::convert::Infallible;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
for Pixel(p, c) in pixels {
if p.x >= 0 && p.y >= 0 && (p.x as u32) < W && (p.y as u32) < H {
self.px[(p.y as u32 * W + p.x as u32) as usize] = c;
}
}
Ok(())
}
}
impl OriginDimensions for Fb {
fn size(&self) -> Size {
Size::new(W, H)
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let prog = args
.get(1)
.cloned()
.unwrap_or_else(|| "t128;kick:4=Xxxx;snare:4=.x.x;hatClosed:4/2;ride:4/2s=X.x.x.x.;cowbell:3~".into());
let track = track_format::parse(&prog);
let lanes: Vec<LaneView> = track
.lanes
.iter()
.map(|l| LaneView {
name: &l.sound,
levels: &l.levels,
beats: l.groups.iter().sum::<u32>().min(255) as u8,
poly: l.poly,
muted: l.mute,
})
.collect();
let screen = Screen {
name: "Four-on-the-floor",
bpm: track.bpm,
playing: true,
phase: 0.30,
lanes: &lanes,
};
let mut fb = Fb::new();
pm_ui::draw_metronome(&mut fb, &screen).unwrap();
let img = image::RgbImage::from_fn(W, H, |x, y| {
let c = fb.px[(y * W + x) as usize];
let r = (c.r() << 3) | (c.r() >> 2);
let g = (c.g() << 2) | (c.g() >> 4);
let b = (c.b() << 3) | (c.b() >> 2);
image::Rgb([r, g, b])
});
let out = "pm-kit-ui.png";
img.save(out).unwrap();
println!("wrote {out} ({W}x{H}) for: {prog}");
}