use super::super::draw;
use super::super::{BarContext, ProgressStyle};
use crate::{BrailleGrid, DotmaxError};
use std::f32::consts::PI;
pub fn styles() -> Vec<Box<dyn ProgressStyle>> {
vec![
Box::new(MusicalStaff),
Box::new(PianoKeyboard),
Box::new(Metronome),
Box::new(VinylRecord),
Box::new(DrumKit),
Box::new(SoundWaveform),
Box::new(CassetteReels),
Box::new(ConductorBaton),
Box::new(SheetScroll),
Box::new(TuningFork),
Box::new(VolumeKnob),
Box::new(BeatPulse),
]
}
struct MusicalStaff;
impl ProgressStyle for MusicalStaff {
fn name(&self) -> &str {
"musical-staff"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"5-line musical staff with treble clef; notes materialize left-to-right with progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cw, ch) = grid.dimensions();
if dw == 0 || dh == 0 {
return Ok(());
}
let line_count = 5usize.min(dh);
let spacing = dh / (line_count + 1);
let mut staff_ys = Vec::with_capacity(line_count);
for i in 0..line_count {
let y = spacing * (i + 1);
if y < dh {
draw::hline(grid, 0, dw - 1, y);
staff_ys.push(y);
}
}
if ch >= 1 && cw >= 1 {
draw::glyph(grid, 0, ch / 2, '𝄞');
}
let note_cols = cw.saturating_sub(1);
if note_cols == 0 || staff_ys.is_empty() {
return Ok(());
}
let notes_shown = (ctx.eased * note_cols as f32).round() as usize;
let note_glyphs = ['♪', '♫', '♩', '♪', '♫'];
for i in 0..notes_shown.min(note_cols) {
let cx = i + 1;
if cx >= cw {
break;
}
let line_idx = i % staff_ys.len();
let dot_y = staff_ys[line_idx];
let cy = (dot_y / 4).min(ch.saturating_sub(1));
let g = note_glyphs[i % note_glyphs.len()];
draw::glyph(grid, cx, cy, g);
}
let cursor_x = (ctx.eased * dw as f32) as usize;
if !staff_ys.is_empty() {
let t = (ctx.time * 4.0).sin() * 0.5 + 0.5;
let line_idx = (t * staff_ys.len() as f32) as usize;
let cy = staff_ys[line_idx.min(staff_ys.len() - 1)];
draw::dot(grid, cursor_x.min(dw - 1), cy);
}
Ok(())
}
}
struct PianoKeyboard;
impl ProgressStyle for PianoKeyboard {
fn name(&self) -> &str {
"piano-keyboard"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Piano keys across the full width; black keys overlay white; melody note depresses as time ticks"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cw, ch) = grid.dimensions();
if dw == 0 || dh == 0 {
return Ok(());
}
let black_offsets: [usize; 5] = [1, 2, 4, 5, 6];
let filled_cols = (ctx.eased * cw as f32).round() as usize;
let beat_period = 0.5_f32; let beat_phase = (ctx.time / beat_period).floor() as usize;
let melody = [0usize, 2, 4, 5, 7, 9, 11, 9, 7, 5, 4, 2];
let melody_note = melody[beat_phase % melody.len()];
for cx in 0..cw {
let octave_pos = cx % 7;
let is_black = black_offsets.contains(&octave_pos);
let is_filled = cx < filled_cols;
let note_col = melody_note % 12;
let white_equiv = [0usize, 2, 4, 5, 7, 9, 11]; let is_playing = white_equiv[octave_pos] == note_col && !is_black;
if is_black {
let black_h = (dh / 2).max(1);
draw::fill_rect(grid, cx * 2, 0, 2, black_h);
} else if is_playing {
draw::fill_rect(grid, cx * 2, 0, 2, dh);
} else if is_filled {
let mark_y = dh.saturating_sub(2);
draw::hline(grid, cx * 2, cx * 2 + 1, mark_y);
draw::hline(grid, cx * 2, cx * 2 + 1, mark_y + 1);
} else {
draw::vline(grid, cx * 2, 0, dh - 1);
}
}
let _ = ch;
for cy in 0..ch {
draw::tint_row(
grid,
cy,
0,
filled_cols.min(cw).saturating_sub(1),
ctx.palette.sample(ctx.eased),
);
}
Ok(())
}
}
struct Metronome;
impl ProgressStyle for Metronome {
fn name(&self) -> &str {
"metronome"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Pendulum metronome arm swinging; beat tempo rises with progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let px = (dw / 2) as i32;
let py = (dh / 6) as i32;
let arm_len = (dh.saturating_sub(2)) as f32;
let period = 1.5 - ctx.eased * 1.2; let period = period.max(0.15);
let max_angle = PI / 6.0; let angle = (ctx.time / period * PI).sin() * max_angle;
let steps = arm_len.ceil() as usize;
for s in 0..=steps {
let frac = s as f32 / steps.max(1) as f32;
let dx = (angle.sin() * frac * arm_len).round() as i32;
let dy = (angle.cos() * frac * arm_len).round() as i32;
draw::dot_i(grid, px + dx, py + dy);
}
let tip_x = px + (angle.sin() * arm_len).round() as i32;
let tip_y = py + (angle.cos() * arm_len).round() as i32;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
if dx.abs() + dy.abs() <= 1 {
draw::dot_i(grid, tip_x + dx, tip_y + dy);
}
}
}
let at_extreme = angle.abs() > max_angle * 0.85;
if at_extreme {
draw::dot_i(
grid,
px + (angle.signum() as i32) * ((dw as i32 / 4).min(6)),
py,
);
}
let filled_w = (ctx.eased * dw as f32).round() as usize;
let base_y = (dh - 1) as usize;
draw::hline(grid, 0, filled_w.min(dw - 1), base_y);
Ok(())
}
}
struct VinylRecord;
impl ProgressStyle for VinylRecord {
fn name(&self) -> &str {
"vinyl-record"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Spinning vinyl disc with groove rings; tonearm tracks inward with progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let cx = (dw / 2) as i32;
let cy = (dh / 2) as i32;
let max_r = ((dh / 2).min(dw / 2)) as i32;
if max_r == 0 {
return Ok(());
}
let groove_count = (max_r / 2).max(1) as usize;
for g in 0..groove_count {
let r = (g + 1) as i32 * (max_r / groove_count.max(1) as i32).max(1);
if r > max_r {
break;
}
let steps = (2.0 * PI * r as f32).ceil() as usize;
let steps = steps.max(4);
for s in 0..steps {
let theta = 2.0 * PI * s as f32 / steps as f32 + ctx.time * 2.0;
let dx = (r as f32 * theta.cos()).round() as i32;
let dy = (r as f32 * theta.sin()).round() as i32;
draw::dot_i(grid, cx + dx, cy + dy);
}
}
let hole_r = (max_r / 5).max(1);
for dy in -hole_r..=hole_r {
for dx in -hole_r..=hole_r {
if dx * dx + dy * dy <= hole_r * hole_r {
draw::dot_i(grid, cx + dx, cy + dy);
}
}
}
let arm_pivot_x = dw as i32 - 1;
let arm_pivot_y = 0i32;
let track_r = max_r - (ctx.eased * (max_r - hole_r - 1) as f32).round() as i32;
let arm_angle = -PI / 4.0; let arm_tip_x = cx + (track_r as f32 * arm_angle.sin()).round() as i32;
let arm_tip_y = cy - (track_r as f32 * arm_angle.cos()).round() as i32;
let dx_total = arm_tip_x - arm_pivot_x;
let dy_total = arm_tip_y - arm_pivot_y;
let steps = (dx_total.abs().max(dy_total.abs())).max(1) as usize;
for s in 0..=steps {
let t = s as f32 / steps as f32;
let ax = arm_pivot_x + (dx_total as f32 * t).round() as i32;
let ay = arm_pivot_y + (dy_total as f32 * t).round() as i32;
draw::dot_i(grid, ax, ay);
}
Ok(())
}
}
struct DrumKit;
impl ProgressStyle for DrumKit {
fn name(&self) -> &str {
"drum-kit"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Radial impact rings exploding from centre on each beat; ghost rings accumulate with progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let cx = (dw / 2) as i32;
let cy = (dh / 2) as i32;
let max_r = ((dh / 2).min(dw / 2)) as i32;
if max_r == 0 {
return Ok(());
}
let beat_period = 0.5_f32;
let beat_phase = ctx.time / beat_period;
let beat_frac = beat_phase.fract();
let ghost_count = (ctx.eased * max_r as f32).round() as usize;
for g in 0..ghost_count.min(max_r as usize) {
let r = (g + 1) as i32;
let steps = (2.0 * PI * r as f32).ceil() as usize;
for s in (0..steps).step_by(2) {
let theta = 2.0 * PI * s as f32 / steps.max(1) as f32;
let dx = (r as f32 * theta.cos()).round() as i32;
let dy = (r as f32 * theta.sin()).round() as i32;
draw::dot_i(grid, cx + dx, cy + dy);
}
}
let live_r = (beat_frac * max_r as f32).round() as i32;
if live_r > 0 {
let steps = (2.0 * PI * live_r as f32).ceil() as usize;
for s in 0..steps {
let theta = 2.0 * PI * s as f32 / steps.max(1) as f32;
let dx = (live_r as f32 * theta.cos()).round() as i32;
let dy = (live_r as f32 * theta.sin()).round() as i32;
draw::dot_i(grid, cx + dx, cy + dy);
}
}
if beat_frac < 0.1 {
draw::dot_i(grid, cx, cy);
draw::dot_i(grid, cx + 1, cy);
draw::dot_i(grid, cx - 1, cy);
}
Ok(())
}
}
struct SoundWaveform;
impl ProgressStyle for SoundWaveform {
fn name(&self) -> &str {
"sound-waveform"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Centered oscillating waveform; amplitude grows with progress, morph sine→saw with time"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let cy = (dh / 2) as i32;
let half_h = (dh / 2) as f32;
let amp = ctx.eased * (half_h - 1.0).max(0.0);
let morph = ((ctx.time * 0.1).sin() * 0.5 + 0.5).clamp(0.0, 1.0);
let freq = 3.0_f32;
let prev_y: Option<i32> = None;
let _ = prev_y;
let mut last_y: Option<i32> = None;
for xi in 0..dw {
let xn = xi as f32 / dw.max(1) as f32;
let phase = xn * freq * 2.0 * PI - ctx.time * 2.0;
let sine = phase.sin();
let saw_phase = phase / (2.0 * PI);
let saw = 2.0 * (saw_phase - (saw_phase + 0.5).floor());
let sample = sine * (1.0 - morph) + saw * morph;
let dot_y = cy - (sample * amp).round() as i32;
if let Some(ly) = last_y {
let (lo, hi) = if ly <= dot_y {
(ly, dot_y)
} else {
(dot_y, ly)
};
for y in lo..=hi {
draw::dot_i(grid, xi as i32, y);
}
} else {
draw::dot_i(grid, xi as i32, dot_y);
}
last_y = Some(dot_y);
}
if ctx.eased < 0.01 {
draw::hline(grid, 0, dw - 1, cy as usize);
}
Ok(())
}
}
struct CassetteReels;
impl ProgressStyle for CassetteReels {
fn name(&self) -> &str {
"cassette-reels"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Two cassette reels: supply shrinks, take-up grows, both spin as tape transfers"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let cy = (dh / 2) as i32;
let max_r = ((dh / 2).max(1) - 1) as i32;
let hub_r = (max_r / 3).max(1);
let left_cx = (dw / 4) as i32;
let right_cx = (dw * 3 / 4) as i32;
let supply_r = max_r - ((ctx.eased * (max_r - hub_r) as f32).round() as i32);
let supply_r = supply_r.clamp(hub_r, max_r);
let takeup_r = hub_r + ((ctx.eased * (max_r - hub_r) as f32).round() as i32);
let takeup_r = takeup_r.clamp(hub_r, max_r);
let spin = ctx.time * 3.0;
let draw_reel =
|grid: &mut BrailleGrid, rcx: i32, rcy: i32, outer: i32, hub: i32, angle: f32| {
let steps = (2.0 * PI * outer as f32).ceil() as usize;
for s in 0..steps {
let theta = 2.0 * PI * s as f32 / steps.max(1) as f32;
let dx = (outer as f32 * theta.cos()).round() as i32;
let dy = (outer as f32 * theta.sin()).round() as i32;
draw::dot_i(grid, rcx + dx, rcy + dy);
}
for dy in -hub..=hub {
for dx in -hub..=hub {
if dx * dx + dy * dy <= hub * hub {
draw::dot_i(grid, rcx + dx, rcy + dy);
}
}
}
for spoke in 0..3 {
let theta = angle + spoke as f32 * 2.0 * PI / 3.0;
let len = outer - hub;
let spoke_steps = len.max(1) as usize;
for s in 1..=spoke_steps {
let frac = s as f32 / spoke_steps as f32;
let r = hub as f32 + frac * len as f32;
let dx = (r * theta.cos()).round() as i32;
let dy = (r * theta.sin()).round() as i32;
draw::dot_i(grid, rcx + dx, rcy + dy);
}
}
};
draw_reel(grid, left_cx, cy, supply_r, hub_r, spin);
draw_reel(grid, right_cx, cy, takeup_r, hub_r, -spin);
let tape_y = cy - max_r;
let gap_x0 = (left_cx + supply_r + 1).min(dw as i32 - 1);
let gap_x1 = (right_cx - takeup_r - 1).max(0);
if gap_x0 <= gap_x1 {
draw::hline(
grid,
gap_x0 as usize,
gap_x1 as usize,
tape_y.max(0) as usize,
);
}
Ok(())
}
}
struct ConductorBaton;
impl ProgressStyle for ConductorBaton {
fn name(&self) -> &str {
"conductor-baton"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Baton tracing a 4/4 conducting pattern; beat positions illuminate with time"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let waypoints: [(f32, f32); 5] = [
(0.5, 0.0), (0.5, 1.0), (0.75, 0.5), (0.25, 0.5), (0.5, 0.0), ];
let beat_period = 0.6_f32; let measure_time = (ctx.time / (beat_period * 4.0)).fract() * 4.0; let beat_idx = measure_time as usize;
let beat_frac = measure_time.fract();
let (ax, ay) = waypoints[beat_idx.min(3)];
let (bx, by) = waypoints[(beat_idx + 1).min(4)];
let t = (beat_frac * PI / 2.0).sin().powi(2);
let tip_xn = ax + (bx - ax) * t;
let tip_yn = ay + (by - ay) * t;
let tip_x = (tip_xn * (dw - 1) as f32).round() as i32;
let tip_y = (tip_yn * (dh - 1) as f32).round() as i32;
let handle_x = (dw - 1) as i32;
let handle_y = 0i32;
let dx = tip_x - handle_x;
let dy = tip_y - handle_y;
let steps = (dx.abs().max(dy.abs())).max(1) as usize;
for s in 0..=steps {
let ft = s as f32 / steps as f32;
let px = handle_x + (dx as f32 * ft).round() as i32;
let py = handle_y + (dy as f32 * ft).round() as i32;
draw::dot_i(grid, px, py);
}
for &(wx, wy) in &waypoints[..4] {
let wpx = (wx * (dw - 1) as f32).round() as i32;
let wpy = (wy * (dh - 1) as f32).round() as i32;
draw::dot_i(grid, wpx, wpy);
}
let filled_w = (ctx.eased * dw as f32).round() as usize;
draw::hline(
grid,
0,
filled_w.min(dw - 1),
(dh - 1).min(dh.saturating_sub(1)),
);
Ok(())
}
}
struct SheetScroll;
impl ProgressStyle for SheetScroll {
fn name(&self) -> &str {
"sheet-scroll"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Sheet music scrolling left; bar-lines and note heads reveal with progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cw, ch) = grid.dimensions();
if dw == 0 || dh == 0 {
return Ok(());
}
let scroll_speed = 2.0_f32; let scroll_offset = (ctx.time * scroll_speed + ctx.eased * dw as f32) as usize;
let line_count = 5usize.min(dh);
let spacing = (dh / (line_count + 1)).max(1);
let mut staff_ys = Vec::with_capacity(line_count);
for i in 0..line_count {
let y = spacing * (i + 1);
if y < dh {
draw::hline(grid, 0, dw - 1, y);
staff_ys.push(y);
}
}
let bar_period = 16usize; for xi in 0..dw {
let world_x = xi + scroll_offset;
if world_x % bar_period == 0 {
draw::vline(grid, xi, 0, dh - 1);
}
}
let note_pattern: &[(usize, usize)] = &[
(2, 0),
(6, 1),
(10, 2),
(14, 3),
(18, 4),
(22, 1),
(26, 0),
(30, 2),
(34, 3),
(38, 4),
(3, 0),
(7, 2),
(11, 4),
(15, 1),
(19, 3),
];
for &(world_col, line_idx) in note_pattern {
if staff_ys.is_empty() {
break;
}
let line = line_idx % staff_ys.len();
let dot_y = staff_ys[line];
let world_x = world_col;
let period_dots = 48usize;
let adjusted_offset = scroll_offset % period_dots;
let visible_x = if world_x >= adjusted_offset {
world_x - adjusted_offset
} else {
world_x + period_dots - adjusted_offset
};
if visible_x < dw {
draw::dot(grid, visible_x, dot_y);
if visible_x + 1 < dw {
draw::dot(grid, visible_x + 1, dot_y);
}
if visible_x > 0 {
draw::dot(grid, visible_x - 1, dot_y);
}
let stem_top = dot_y.saturating_sub(4);
for sy in stem_top..dot_y {
draw::dot(grid, visible_x + 1, sy);
}
}
}
let cursor_x = (ctx.eased * (dw - 1) as f32).round() as usize;
draw::vline(grid, cursor_x.min(dw - 1), 0, dh - 1);
let _ = cw;
let _ = ch;
Ok(())
}
}
struct TuningFork;
impl ProgressStyle for TuningFork {
fn name(&self) -> &str {
"tuning-fork"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Tuning fork tines vibrating; amplitude decays as pitch locks in with progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let cx = (dw / 2) as i32;
let handle_top = (dh * 2 / 3) as i32;
let handle_bot = (dh - 1) as i32;
draw::vline(grid, cx as usize, handle_top as usize, handle_bot as usize);
let tine_base_sep = ((dw as i32) / 6).max(1);
let tine_len = (handle_top - 1).max(0) as usize;
let raw_amp = 1.0 - ctx.eased;
let _freq = 440.0_f32; let vis_freq = 4.0; let amp = raw_amp * tine_base_sep as f32;
for row in 0..tine_len {
let frac = row as f32 / tine_len.max(1) as f32;
let phase = ctx.time * vis_freq * 2.0 * PI;
let vibration = (phase + frac * PI).sin() * amp * frac; let vibration = vibration.round() as i32;
let dy = (handle_top as usize - 1 - row) as i32;
let lx = cx - tine_base_sep + vibration;
let rx = cx + tine_base_sep - vibration;
draw::dot_i(grid, lx, dy);
draw::dot_i(grid, rx, dy);
}
let tip_y = 0i32;
let tip_phase = ctx.time * vis_freq * 2.0 * PI;
let tip_vib = ((tip_phase + PI).sin() * amp).round() as i32;
draw::dot_i(grid, cx - tine_base_sep + tip_vib - 1, tip_y);
draw::dot_i(grid, cx - tine_base_sep + tip_vib, tip_y);
draw::dot_i(grid, cx + tine_base_sep - tip_vib, tip_y);
draw::dot_i(grid, cx + tine_base_sep - tip_vib + 1, tip_y);
let (_, ch) = grid.dimensions();
let (cw, _) = grid.dimensions();
if ctx.eased > 0.9 && cw > 0 && ch > 0 {
draw::glyph(grid, 0, ch / 2, '♩');
}
Ok(())
}
}
struct VolumeKnob;
impl ProgressStyle for VolumeKnob {
fn name(&self) -> &str {
"volume-knob"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Potentiometer knob with rotating pointer; sweep arc fills from 7 o'clock to 5 o'clock"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let cx = (dw / 2) as i32;
let cy = (dh / 2) as i32;
let r = ((dh / 2).min(dw / 2)) as i32;
let r = (r - 1).max(1);
let start_deg = 210.0_f32;
let sweep_deg = 300.0_f32;
let arc_steps = 64usize;
for s in 0..=arc_steps {
let t = s as f32 / arc_steps as f32;
let deg = start_deg + t * sweep_deg;
let rad = deg * PI / 180.0;
let dx = (r as f32 * rad.sin()).round() as i32;
let dy = (-(r as f32 * rad.cos())).round() as i32;
draw::dot_i(grid, cx + dx, cy + dy);
}
let filled_steps = (ctx.eased * arc_steps as f32).round() as usize;
let inner_r = (r * 2 / 3).max(1);
for s in 0..=filled_steps.min(arc_steps) {
let t = s as f32 / arc_steps as f32;
let deg = start_deg + t * sweep_deg;
let rad = deg * PI / 180.0;
for ri in inner_r..=r {
let dx = (ri as f32 * rad.sin()).round() as i32;
let dy = (-(ri as f32 * rad.cos())).round() as i32;
draw::dot_i(grid, cx + dx, cy + dy);
}
}
for dy in -inner_r..=inner_r {
for dx in -inner_r..=inner_r {
let d2 = dx * dx + dy * dy;
if d2 <= inner_r * inner_r && d2 >= (inner_r / 2) * (inner_r / 2) {
draw::dot_i(grid, cx + dx, cy + dy);
}
}
}
let pointer_deg = start_deg + ctx.eased * sweep_deg;
let pointer_rad = pointer_deg * PI / 180.0;
let steps = r as usize;
for s in 0..=steps {
let frac = s as f32 / steps.max(1) as f32;
let pdx = (r as f32 * pointer_rad.sin() * frac).round() as i32;
let pdy = (-(r as f32 * pointer_rad.cos()) * frac).round() as i32;
draw::dot_i(grid, cx + pdx, cy + pdy);
}
Ok(())
}
}
struct BeatPulse;
impl ProgressStyle for BeatPulse {
fn name(&self) -> &str {
"beat-pulse"
}
fn theme(&self) -> &str {
"music"
}
fn describe(&self) -> &str {
"Concentric rings expanding outward on each beat; ring density grows with progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
if dw == 0 || dh == 0 {
return Ok(());
}
let cx = (dw / 2) as i32;
let cy = (dh / 2) as i32;
let max_r = ((dh / 2).min(dw / 2)) as i32;
if max_r == 0 {
return Ok(());
}
let ring_count = (1.0 + ctx.eased * 3.0).round() as usize;
let beat_period = 0.5_f32;
for ring in 0..ring_count {
let offset = ring as f32 / ring_count as f32;
let phase = ((ctx.time / beat_period) + offset).fract(); let r = (phase * max_r as f32).round() as i32;
if r <= 0 {
continue;
}
let fade = 1.0 - phase;
let steps = (2.0 * PI * r as f32).ceil() as usize;
let steps = steps.max(4);
for s in 0..steps {
let skip = if fade > 0.5 { 1 } else { 2 };
if s % skip != 0 {
continue;
}
let theta = 2.0 * PI * s as f32 / steps as f32;
let dx = (r as f32 * theta.cos()).round() as i32;
let dy = (r as f32 * theta.sin()).round() as i32;
draw::dot_i(grid, cx + dx, cy + dy);
}
}
let beat_phase = (ctx.time / beat_period).fract();
if beat_phase < 0.1 {
draw::dot_i(grid, cx, cy);
}
let (_, ch) = grid.dimensions();
let (cw, _) = grid.dimensions();
for cy_cell in 0..ch {
draw::tint_row(
grid,
cy_cell,
0,
cw.saturating_sub(1),
ctx.palette.sample(ctx.eased),
);
}
Ok(())
}
}