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(Sprint100m),
Box::new(BasketballArc),
Box::new(SoccerGoal),
Box::new(SwimmingLaps),
Box::new(Archery),
Box::new(Bowling),
Box::new(Darts),
Box::new(HighJump),
Box::new(Weightlifting),
Box::new(TennisRally),
Box::new(CyclingPeloton),
]
}
struct Sprint100m;
impl ProgressStyle for Sprint100m {
fn name(&self) -> &str {
"sprint-100m"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"100m sprinter advances right with cycling legs; finish-line tape snaps at 100%"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let base = h.saturating_sub(1);
let mid = h / 2;
draw::hline(grid, 0, w.saturating_sub(1), base);
let finish_x = w.saturating_sub(2);
draw::vline(grid, finish_x, 0, base);
let runner_x = (ctx.eased * finish_x.saturating_sub(4) as f32) as usize;
let runner_x = runner_x.min(finish_x.saturating_sub(4));
let phase = ctx.time * 8.0;
let fwd = (phase.sin() * 1.5).round() as i32;
let bk = (-(phase.sin()) * 1.5).round() as i32;
let torso_bot = base as i32 - 1;
let torso_top = torso_bot - (h as i32 / 3).max(1);
draw::dot_i(grid, runner_x as i32 + 1, torso_bot);
draw::dot_i(grid, runner_x as i32 + 1, torso_bot - 1);
draw::dot_i(grid, runner_x as i32 + 1, torso_top);
draw::dot_i(grid, runner_x as i32 + 2, torso_top - 1);
let arm_phase = -phase;
let arm_fwd = (arm_phase.sin() * 1.2).round() as i32;
draw::dot_i(grid, runner_x as i32 + 2, torso_bot - 1 + arm_fwd);
draw::dot_i(grid, runner_x as i32, torso_bot - 1 - arm_fwd);
draw::dot_i(grid, runner_x as i32 + 2, torso_bot + fwd);
draw::dot_i(grid, runner_x as i32, torso_bot + bk);
draw::dot_i(grid, runner_x as i32 + 3, base as i32 + fwd.min(0));
draw::dot_i(grid, runner_x as i32 - 1, base as i32 + bk.min(0));
if ctx.progress >= 0.999 {
draw::hline(grid, finish_x.saturating_sub(1), w.saturating_sub(1), mid);
for k in 0..3usize {
let fx = finish_x + k * 2;
let fy = (mid as i32 + (k as i32 % 3) - 1).max(0) as usize;
draw::dot(
grid,
fx.min(w.saturating_sub(1)),
fy.min(h.saturating_sub(1)),
);
}
}
let (cw, ch) = grid.dimensions();
let swept = (ctx.eased * cw as f32) as usize;
for cy in 0..ch {
for cx in 0..swept.min(cw) {
let t = if swept <= 1 {
0.0
} else {
cx as f32 / (swept - 1) as f32
};
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct BasketballArc;
impl ProgressStyle for BasketballArc {
fn name(&self) -> &str {
"basketball-arc"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Basketball traces a parabolic arc toward a hoop; swish flash lights up at 100%"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let base = h.saturating_sub(1);
let hoop_x = w.saturating_sub(3);
let hoop_y = h / 4;
draw::hline(grid, hoop_x, w.saturating_sub(1), hoop_y);
let net_h = (h / 4).max(1);
for nx in [hoop_x, w.saturating_sub(1)] {
draw::vline(
grid,
nx.min(w.saturating_sub(1)),
hoop_y,
(hoop_y + net_h).min(base),
);
}
for ny in (hoop_y..=(hoop_y + net_h).min(base)).step_by(2) {
draw::hline(grid, hoop_x, w.saturating_sub(1), ny.min(base));
}
let t = ctx.eased;
let bx = (t * hoop_x as f32) as i32;
let start_y = base as f32;
let end_y = hoop_y as f32;
let peak_lift = (h as f32 * 0.6).min((base - hoop_y) as f32 + h as f32 * 0.4);
let linear_y = start_y + t * (end_y - start_y);
let arc_y = linear_y - peak_lift * 4.0 * t * (1.0 - t);
let by = arc_y.round().clamp(0.0, base as f32) as i32;
draw::dot_i(grid, bx, by);
draw::dot_i(grid, bx + 1, by);
draw::dot_i(grid, bx, by + 1);
draw::dot_i(grid, bx + 1, by + 1);
if ctx.progress >= 0.999 {
for ny in hoop_y..=(hoop_y + net_h).min(base) {
draw::hline(grid, hoop_x, w.saturating_sub(1), ny);
}
}
for step in 0..20usize {
let pt = step as f32 / 20.0;
if pt >= t {
break;
}
let tx = (pt * hoop_x as f32) as i32;
let ty_lin = start_y + pt * (end_y - start_y);
let ty = (ty_lin - peak_lift * 4.0 * pt * (1.0 - pt))
.round()
.clamp(0.0, base as f32) as i32;
if step % 2 == 0 {
draw::dot_i(grid, tx, ty);
}
}
Ok(())
}
}
struct SoccerGoal;
impl ProgressStyle for SoccerGoal {
fn name(&self) -> &str {
"soccer-goal"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Soccer ball curves into a goal net; net bulges on score"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let base = h.saturating_sub(1);
let goal_left = w.saturating_sub((w / 5).max(3));
let goal_top = 0usize;
draw::hline(grid, goal_left, w.saturating_sub(1), goal_top);
draw::vline(grid, goal_left, goal_top, base);
draw::vline(grid, w.saturating_sub(1), goal_top, base);
let net_bulge: i32 = if ctx.progress >= 0.999 {
((ctx.time * 10.0).sin() * 1.5).round() as i32
} else {
0
};
for ny in (goal_top..=base).step_by(2) {
for nx in (goal_left..w).step_by(3) {
let bx = nx as i32 + if ny % 4 == 0 { net_bulge } else { -net_bulge };
draw::dot_i(grid, bx, ny as i32);
}
}
let t = ctx.eased;
let bx = (t * goal_left as f32) as i32;
let start_y = (h as f32 * 0.7) as i32;
let end_y = (h / 2) as i32;
let curve_peak = ((h as f32 * 0.25) * (PI * t).sin()) as i32;
let by =
(start_y + (t * (end_y - start_y) as f32) as i32 - curve_peak).clamp(0, base as i32);
draw::dot_i(grid, bx, by);
draw::dot_i(grid, bx + 1, by);
draw::dot_i(grid, bx, by + 1);
draw::dot_i(grid, bx + 1, by + 1);
if (ctx.time * 5.0) as usize % 2 == 0 {
draw::dot_i(grid, bx + 1, by - 1);
} else {
draw::dot_i(grid, bx - 1, by + 1);
}
for gx in (0..w).step_by(4) {
draw::dot(grid, gx.min(w.saturating_sub(1)), base);
if gx + 1 < w {
draw::dot(grid, gx + 1, base.saturating_sub(1));
}
}
let (cw, ch) = grid.dimensions();
let swept = (ctx.eased * cw as f32) as usize;
for cy in 0..ch {
for cx in 0..swept.min(cw) {
let t2 = if swept <= 1 {
0.0
} else {
cx as f32 / (swept - 1) as f32
};
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t2));
}
}
Ok(())
}
}
struct SwimmingLaps;
impl ProgressStyle for SwimmingLaps {
fn name(&self) -> &str {
"swimming-laps"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Swimmer bobs across a lane; vblock lap counter fills on the right as laps complete"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let (cw, ch) = grid.dimensions();
let mid = h / 2;
draw::hline(grid, 0, w.saturating_sub(1), 0);
draw::hline(grid, 0, w.saturating_sub(1), h.saturating_sub(1));
let rope_y = mid;
for rx in (0..w).step_by(3) {
draw::dot(grid, rx.min(w.saturating_sub(1)), rope_y);
}
let n_laps = 4usize;
let lap_frac_f = ctx.eased * n_laps as f32;
let current_lap = (lap_frac_f as usize).min(n_laps.saturating_sub(1));
let within_lap = lap_frac_f.fract();
let going_right = current_lap % 2 == 0;
let swim_x = if going_right {
(within_lap * w as f32) as usize
} else {
w.saturating_sub(1)
.saturating_sub((within_lap * w as f32) as usize)
};
let swim_x = swim_x.min(w.saturating_sub(3));
let bob_amp = ((h / 4).max(1)) as f32 * 0.5;
let lane_y = if going_right {
mid.saturating_sub(2)
} else {
(mid + 2).min(h.saturating_sub(2))
};
let bob = (ctx.time * 6.0).sin() * bob_amp;
let sy = (lane_y as f32 + bob)
.round()
.clamp(1.0, h.saturating_sub(2) as f32) as usize;
draw::dot(grid, swim_x, sy);
draw::dot(grid, swim_x + 1, sy);
let head_off: i32 = if going_right { 2 } else { -1 };
draw::dot_i(grid, swim_x as i32 + head_off, sy as i32 - 1);
let stroke = (ctx.time * 4.0).sin();
let arm_x = if going_right {
swim_x as i32 + 3
} else {
swim_x as i32 - 2
};
draw::dot_i(grid, arm_x, sy as i32 + (stroke * 1.5).round() as i32);
let kick_x = if going_right {
swim_x as i32 - 1
} else {
swim_x as i32 + 2
};
let kick_off = (stroke * -1.0).round() as i32;
draw::dot_i(grid, kick_x, sy as i32 + kick_off);
draw::dot_i(grid, kick_x, sy as i32 + kick_off + 1);
let laps_done = current_lap.min(n_laps);
let counter_cells = (n_laps).min(cw);
let start_cell = cw.saturating_sub(counter_cells);
for i in 0..counter_cells {
let filled = if i < laps_done { 8 } else { 0 };
for cy in 0..ch {
draw::vblock(grid, start_cell + i, cy, filled);
}
}
let ripple_dir: i32 = if going_right { -1 } else { 1 };
for k in 1..4usize {
let rx = swim_x as i32 + ripple_dir * (k as i32 * 2);
if rx >= 0 && (rx as usize) < w {
let ry_off = (k as f32 * 0.7 * (ctx.time * 3.0 + k as f32).sin()) as i32;
draw::dot_i(grid, rx, sy as i32 + ry_off);
}
}
Ok(())
}
}
struct Archery;
impl ProgressStyle for Archery {
fn name(&self) -> &str {
"archery"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Bow draws back with eased progress; arrow flies across to a multi-ring bullseye"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let mid = h / 2;
let target_cx = w.saturating_sub(3) as i32;
let target_cy = mid as i32;
let max_r = ((h / 2).min(3)).max(1) as i32;
for r in (1..=max_r).rev() {
for &(dx, dy) in &[
(r, 0),
(-r, 0),
(0, r),
(0, -r),
(r, r),
(-r, r),
(r, -r),
(-r, -r),
] {
draw::dot_i(grid, target_cx + dx, target_cy + dy);
}
}
draw::dot_i(grid, target_cx, target_cy);
let bow_x = 2i32;
let bow_h = (h * 3 / 4).max(2) as i32;
let bow_top = (mid as i32) - bow_h / 2;
let bow_bot = bow_top + bow_h;
for step in 0..=bow_h {
let by = bow_top + step;
let pull = ctx.eased; let arc = (PI * step as f32 / bow_h as f32).sin();
let bx = bow_x - (arc * 2.0 * pull).round() as i32;
draw::dot_i(grid, bx, by);
}
let string_x = bow_x + 1 - (ctx.eased * 1.5).round() as i32;
draw::vline(
grid,
string_x.max(0) as usize,
bow_top.max(0) as usize,
bow_bot.min(h as i32 - 1) as usize,
);
let t = ctx.eased;
let arrow_head_x = if t < 0.8 {
string_x + 1
} else {
let flight_t = (t - 0.8) / 0.2;
let nock_x = string_x + 1;
(nock_x as f32 + flight_t * (target_cx - nock_x as i32) as f32).round() as i32
};
let shaft_start = (string_x + 1).max(0);
let shaft_end = arrow_head_x.max(shaft_start);
for ax in shaft_start..=shaft_end.min(target_cx) {
draw::dot_i(grid, ax, mid as i32);
}
draw::dot_i(grid, arrow_head_x, mid as i32 - 1);
draw::dot_i(grid, arrow_head_x + 1, mid as i32);
draw::dot_i(grid, arrow_head_x, mid as i32 + 1);
let tail_x = shaft_start - 1;
draw::dot_i(grid, tail_x, mid as i32 - 1);
draw::dot_i(grid, tail_x, mid as i32 + 1);
Ok(())
}
}
struct Bowling;
impl ProgressStyle for Bowling {
fn name(&self) -> &str {
"bowling"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Ball rolls down lane toward pins; pins scatter progressively at the end"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let base = h.saturating_sub(1);
let mid = h / 2;
let lane_top = mid.saturating_sub(1);
let lane_bot = (mid + 1).min(base);
let guide_step = (w / 8).max(1);
for gx in (0..w).step_by(guide_step) {
draw::dot(grid, gx.min(w.saturating_sub(1)), lane_top);
draw::dot(grid, gx.min(w.saturating_sub(1)), lane_bot);
}
let pin_area_x = w.saturating_sub((w / 6).max(4));
let knock_t = ((ctx.eased - 0.8) / 0.2).clamp(0.0, 1.0);
let rows = [4usize, 3, 2, 1];
let mut pin_idx = 0usize;
let total_pins = 10usize;
for (row, &count) in rows.iter().enumerate() {
for col in 0..count {
let px = pin_area_x + col * 2 + row;
let px = px.min(w.saturating_sub(1));
let pin_frac = pin_idx as f32 / total_pins as f32;
let knocked = knock_t > pin_frac;
if knocked {
let scatter = ((ctx.time * 3.0 + pin_idx as f32).sin() * 1.5).round() as i32;
draw::dot_i(grid, px as i32 + 1 + scatter, base as i32);
} else {
draw::dot(grid, px, mid);
draw::dot(grid, px, mid.saturating_sub(1));
}
pin_idx += 1;
}
}
let ball_x = (ctx.eased * pin_area_x as f32) as usize;
let ball_x = ball_x.min(pin_area_x.saturating_sub(2));
let wobble = ((ctx.time * 12.0).sin() * 0.4).round() as i32;
let by = (mid as i32 + wobble).clamp(0, base as i32) as usize;
draw::dot(grid, ball_x, by);
draw::dot(grid, (ball_x + 1).min(w.saturating_sub(1)), by);
draw::dot(grid, ball_x, (by + 1).min(base));
draw::dot(
grid,
(ball_x + 1).min(w.saturating_sub(1)),
(by + 1).min(base),
);
let spin_angle = ctx.time * 10.0;
let sdx = spin_angle.cos().round() as i32;
let sdy = spin_angle.sin().round() as i32;
draw::dot_i(grid, ball_x as i32 + sdx, by as i32 + sdy);
let (cw, ch) = grid.dimensions();
let filled_cells = (ctx.eased * cw as f32) as usize;
for cy in 0..ch {
for cx in 0..filled_cells.min(cw) {
let t = if filled_cells <= 1 {
0.0
} else {
cx as f32 / (filled_cells - 1) as f32
};
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct Darts;
impl ProgressStyle for Darts {
fn name(&self) -> &str {
"darts"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Dart flies toward concentric scoring rings; board glows as the dart closes in"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let mid = h / 2;
let board_cx = w.saturating_sub(2) as i32;
let board_cy = mid as i32;
let n_rings = ((h / 2).min(4)).max(1);
for r in 1..=n_rings {
let radius = r as i32;
let steps = (radius * 8).max(8) as usize;
for s in 0..steps {
let angle = 2.0 * PI * s as f32 / steps as f32;
let ex = (board_cx + (angle.cos() * radius as f32 * 1.5).round() as i32).max(0);
let ey = (board_cy + (angle.sin() * radius as f32 * 0.75).round() as i32).max(0);
draw::dot_i(grid, ex, ey);
}
}
draw::dot_i(grid, board_cx, board_cy);
let dart_x = (ctx.eased * board_cx as f32).round() as i32;
let dart_y = board_cy;
let drop = if dart_x < board_cx {
let flight_t = dart_x as f32 / board_cx.max(1) as f32;
(flight_t * (1.0 - flight_t) * 2.0 * h as f32 * 0.2).round() as i32
} else {
0
};
let dart_y_dropped = (dart_y + drop).clamp(0, h as i32 - 1);
draw::dot_i(grid, dart_x, dart_y_dropped);
draw::dot_i(grid, dart_x - 1, dart_y_dropped);
draw::dot_i(grid, dart_x - 2, dart_y_dropped);
draw::dot_i(grid, dart_x - 3, dart_y_dropped - 1);
draw::dot_i(grid, dart_x - 3, dart_y_dropped + 1);
draw::dot_i(grid, dart_x - 4, dart_y_dropped - 2);
draw::dot_i(grid, dart_x - 4, dart_y_dropped + 2);
if ctx.eased > 0.8 {
let glow_t = (ctx.eased - 0.8) / 0.2;
let glow_r = (glow_t * n_rings as f32).round() as i32;
for gr in 1..=glow_r.min(n_rings as i32) {
draw::dot_i(grid, board_cx, board_cy - gr);
draw::dot_i(grid, board_cx, board_cy + gr);
draw::dot_i(grid, board_cx - gr, board_cy);
draw::dot_i(grid, board_cx + gr, board_cy);
}
}
Ok(())
}
}
struct HighJump;
impl ProgressStyle for HighJump {
fn name(&self) -> &str {
"high-jump"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Athlete arcs over a bar that rises with progress; backflop pose at peak"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let base = h.saturating_sub(1);
let mat_x = w.saturating_sub((w / 5).max(2));
draw::hline(grid, mat_x, w.saturating_sub(1), base);
let post_h = (ctx.eased * base as f32).round() as usize;
let post_h = post_h.max(1);
let lpost = mat_x.saturating_sub(2).min(w.saturating_sub(1));
let rpost = (mat_x + 1).min(w.saturating_sub(1));
draw::vline(grid, lpost, base.saturating_sub(post_h), base);
draw::vline(grid, rpost, base.saturating_sub(post_h), base);
let bar_y = base.saturating_sub(post_h);
draw::hline(grid, lpost, rpost, bar_y);
let t = ctx.eased;
let approach_x = (t * lpost as f32) as usize;
let arc_frac = 4.0 * t * (1.0 - t); let arc_lift = (arc_frac * post_h as f32).round() as usize;
let ath_base = base.saturating_sub(arc_lift);
let ax = approach_x.min(w.saturating_sub(3));
if t < 0.4 {
draw::dot(grid, ax, ath_base);
draw::dot(grid, ax + 1, ath_base);
draw::dot(grid, ax, ath_base.saturating_sub(1));
draw::dot_i(grid, ax as i32 + 1, ath_base as i32 - 2);
let leg_phase = (ctx.time * 8.0).sin();
draw::dot_i(grid, ax as i32 + 1, ath_base as i32 + 1);
draw::dot_i(
grid,
ax as i32 - 1 + (leg_phase * 1.0).round() as i32,
ath_base as i32 + 1,
);
} else {
let bod_y = ath_base;
draw::dot(grid, ax, bod_y);
draw::dot(grid, ax + 1, bod_y);
draw::dot(grid, ax + 2, bod_y);
draw::dot_i(grid, ax as i32 + 1, bod_y as i32 - 1);
draw::dot_i(grid, ax as i32 - 1, bod_y as i32 - 1);
draw::dot_i(grid, ax as i32 + 3, bod_y as i32 - 1);
draw::dot_i(grid, ax as i32 + 4, bod_y as i32 - 2);
}
draw::hline(grid, 0, lpost.saturating_sub(1), base);
Ok(())
}
}
struct Weightlifting;
impl ProgressStyle for Weightlifting {
fn name(&self) -> &str {
"weightlifting"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Barbell lifted overhead; plate stack height and arm angle track eased progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let base = h.saturating_sub(1);
let cx = w / 2;
let lift = ctx.eased;
let bar_range = (base.saturating_sub(2)) as f32;
let bar_y = base
.saturating_sub(2)
.saturating_sub((lift * bar_range) as usize);
let bar_y = bar_y.min(base.saturating_sub(1));
let shaft_half = (w / 4).max(2);
draw::hline(
grid,
cx.saturating_sub(shaft_half),
(cx + shaft_half).min(w.saturating_sub(1)),
bar_y,
);
let max_plates = 4usize;
let plates_on = (lift * max_plates as f32).ceil() as usize;
for p in 0..plates_on.min(max_plates) {
let plate_off = p + 1;
let lpx = cx.saturating_sub(shaft_half).saturating_sub(plate_off);
draw::vline(grid, lpx, bar_y.saturating_sub(1), (bar_y + 1).min(base));
let rpx = (cx + shaft_half + plate_off).min(w.saturating_sub(1));
draw::vline(grid, rpx, bar_y.saturating_sub(1), (bar_y + 1).min(base));
}
let lcollar = cx.saturating_sub(shaft_half + plates_on + 1);
let rcollar = (cx + shaft_half + plates_on + 1).min(w.saturating_sub(1));
draw::dot(grid, lcollar, bar_y);
draw::dot(grid, rcollar, bar_y);
let torso_top = bar_y.saturating_sub(1);
let hip_y = (base.saturating_sub(1)).min(base);
let leg_spread = (w / 10).max(1);
draw::vline(grid, cx.saturating_sub(leg_spread), hip_y, base);
draw::vline(
grid,
(cx + leg_spread).min(w.saturating_sub(1)),
hip_y,
base,
);
draw::vline(grid, cx, torso_top, hip_y);
let shoulder_y = torso_top.saturating_sub(1);
let lshoulder_x = cx.saturating_sub(1);
let rshoulder_x = (cx + 1).min(w.saturating_sub(1));
let arm_steps = 3usize;
for step in 0..=arm_steps {
let at = step as f32 / arm_steps as f32;
let ax = (lshoulder_x as f32 + at * (lcollar as i32 - lshoulder_x as i32) as f32)
.round() as i32;
let ay =
(shoulder_y as f32 + at * (bar_y as i32 - shoulder_y as i32) as f32).round() as i32;
draw::dot_i(grid, ax, ay);
}
for step in 0..=arm_steps {
let at = step as f32 / arm_steps as f32;
let ax = (rshoulder_x as f32 + at * (rcollar as i32 - rshoulder_x as i32) as f32)
.round() as i32;
let ay =
(shoulder_y as f32 + at * (bar_y as i32 - shoulder_y as i32) as f32).round() as i32;
draw::dot_i(grid, ax, ay);
}
draw::dot_i(grid, cx as i32, torso_top as i32 - 2);
draw::dot_i(grid, cx as i32, torso_top as i32 - 1);
if ctx.progress >= 0.9 {
let shake = ((ctx.time * 20.0).sin() * 0.5).round() as i32;
draw::dot_i(grid, cx as i32 + shake, bar_y as i32 - 1);
}
Ok(())
}
}
struct TennisRally;
impl ProgressStyle for TennisRally {
fn name(&self) -> &str {
"tennis-rally"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Ball bounces between baselines; a smooth hbar fill tracks rally progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let (cw, ch) = grid.dimensions();
if ch > 0 {
draw::hbar(grid, ch.saturating_sub(1), ctx.eased);
}
let court_rows = ch.saturating_sub(1);
if court_rows == 0 {
return Ok(());
}
let court_h = court_rows * 4;
let court_base = court_h.saturating_sub(1);
draw::hline(grid, 0, w.saturating_sub(1), court_base);
draw::hline(grid, 0, w.saturating_sub(1), 0);
let net_x = w / 2;
draw::vline(grid, net_x, 0, court_base);
let sl = w / 4;
let sr = w * 3 / 4;
for sy in (0..court_base).step_by(2) {
draw::dot(grid, sl.min(w.saturating_sub(1)), sy);
draw::dot(grid, sr.min(w.saturating_sub(1)), sy);
}
let rally_speed = 1.5 + ctx.progress * 3.0;
let bx_raw = ((ctx.time * rally_speed).sin() * 0.5 + 0.5) * (w - 1) as f32;
let bx = bx_raw.round() as usize;
let bx = bx.min(w.saturating_sub(1));
let bounce_freq = rally_speed * 2.0;
let bounce = ((ctx.time * bounce_freq).sin()).abs();
let by_raw = court_base as f32 - bounce * (court_base as f32 * 0.7);
let by = by_raw.round().clamp(0.0, court_base as f32) as usize;
draw::dot(grid, bx, by);
if bx + 1 < w {
draw::dot(grid, bx + 1, by);
}
draw::dot(grid, bx, court_base);
let lp_x = 1usize;
let lp_mid = court_h / 2;
draw::dot(grid, lp_x, lp_mid.saturating_sub(1));
draw::dot(grid, lp_x, lp_mid);
draw::dot(grid, lp_x, (lp_mid + 1).min(court_base));
let rp_x = w.saturating_sub(2);
draw::dot(grid, rp_x, lp_mid.saturating_sub(1));
draw::dot(grid, rp_x, lp_mid);
draw::dot(grid, rp_x, (lp_mid + 1).min(court_base));
for cy in 0..court_rows {
for cx in 0..cw {
let t = if cw <= 1 {
0.5
} else {
cx as f32 / (cw - 1) as f32
};
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy, cx, cx, color);
}
}
Ok(())
}
}
struct CyclingPeloton;
impl ProgressStyle for CyclingPeloton {
fn name(&self) -> &str {
"cycling-peloton"
}
fn theme(&self) -> &str {
"sports"
}
fn describe(&self) -> &str {
"Tightly-packed peloton advances; each bike's wheels spin with time via dot rotation"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
if w == 0 || h == 0 {
return Ok(());
}
let base = h.saturating_sub(1);
let rider_h = (h / 2).max(2);
let body_y = base.saturating_sub(rider_h);
draw::hline(grid, 0, w.saturating_sub(1), base);
let center_y = (base + body_y) / 2;
for rx in (0..w).step_by(5) {
draw::dot(grid, rx.min(w.saturating_sub(1)), center_y);
}
let lead_x = (ctx.eased * w as f32) as usize;
let lead_x = lead_x.min(w.saturating_sub(1));
let n_riders = (w / 10).max(2).min(8);
let pack_depth = (n_riders as f32 * 5.0) as usize;
let spin = ctx.time * 8.0;
let wheel_r = ((h / 4).max(1)) as f32;
for i in 0..n_riders {
let offset = i * (pack_depth / n_riders.max(1));
let rx = lead_x.saturating_sub(offset);
if rx == 0 && i > 0 {
continue;
}
let rx = rx.min(w.saturating_sub(3));
let front_wheel_x = (rx + 2).min(w.saturating_sub(1)) as i32;
let rear_wheel_x = rx as i32;
let wheel_y = (base - 1) as i32;
for spoke in 0..2usize {
let angle = spin + spoke as f32 * PI;
let sdx = (angle.cos() * wheel_r * 0.8).round() as i32;
let sdy = (angle.sin() * wheel_r * 0.5).round() as i32;
draw::dot_i(grid, front_wheel_x + sdx, wheel_y + sdy);
draw::dot_i(grid, front_wheel_x - sdx, wheel_y - sdy);
draw::dot_i(grid, rear_wheel_x + sdx, wheel_y + sdy);
draw::dot_i(grid, rear_wheel_x - sdx, wheel_y - sdy);
}
draw::dot_i(grid, front_wheel_x, wheel_y);
draw::dot_i(grid, rear_wheel_x, wheel_y);
draw::dot_i(grid, rear_wheel_x + 1, wheel_y - 1);
draw::dot_i(grid, front_wheel_x - 1, wheel_y - 1);
let seat_x = rear_wheel_x + 1;
let seat_y = wheel_y - 2;
draw::dot_i(grid, seat_x, seat_y);
draw::dot_i(grid, seat_x + 1, seat_y - 1);
draw::dot_i(grid, seat_x + 1, seat_y - 2);
draw::dot_i(grid, front_wheel_x, seat_y - 2);
}
let (cw, ch) = grid.dimensions();
let filled = (ctx.eased * cw as f32) as usize;
for cy in 0..ch {
for cx in 0..filled.min(cw) {
let t = if filled <= 1 {
0.0
} else {
cx as f32 / (filled - 1) as f32
};
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}