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(Speedometer),
Box::new(RingProgress),
Box::new(HalfGauge),
Box::new(Tachometer),
Box::new(Donut),
Box::new(SignalArc),
Box::new(Compass),
Box::new(VuNeedle),
Box::new(SegmentedRing),
Box::new(DualGauge),
Box::new(ClockFace),
Box::new(PressureDial),
]
}
fn bresenham(grid: &mut BrailleGrid, x0: i32, y0: i32, x1: i32, y1: i32) {
let mut x = x0;
let mut y = y0;
let dx = (x1 - x0).abs();
let dy = -(y1 - y0).abs();
let sx: i32 = if x0 < x1 { 1 } else { -1 };
let sy: i32 = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
let max_steps = (dx.abs() + dy.abs() + 2) as usize;
for _ in 0..=max_steps {
draw::dot_i(grid, x, y);
if x == x1 && y == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x += sx;
}
if e2 <= dx {
err += dx;
y += sy;
}
}
}
fn arc(grid: &mut BrailleGrid, cx: i32, cy: i32, r: i32, a0: f32, a1: f32, steps: usize) {
let steps = steps.max(4);
for i in 0..=steps {
let t = i as f32 / steps as f32;
let angle = a0 + t * (a1 - a0);
let x = cx + (r as f32 * angle.cos()).round() as i32;
let y = cy - (r as f32 * angle.sin()).round() as i32; draw::dot_i(grid, x, y);
}
}
fn thick_arc(
grid: &mut BrailleGrid,
cx: i32,
cy: i32,
r_inner: i32,
r_outer: i32,
a0: f32,
a1: f32,
) {
let r0 = r_inner.max(1);
let r1 = r_outer.max(r0);
for r in r0..=r1 {
let steps = ((r as f32 * (a1 - a0).abs() * 2.0).round() as usize).max(4);
arc(grid, cx, cy, r, a0, a1, steps);
}
}
fn dial_fit(dw: usize, dh: usize) -> (i32, i32, i32) {
let cx = (dw / 2) as i32;
let cy = (dh / 2) as i32;
let r = (cx.min(cy) - 1).max(1);
(cx, cy, r)
}
struct Speedometer;
impl ProgressStyle for Speedometer {
fn name(&self) -> &str {
"speedometer"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Classic 270° arc speedometer: tick marks at every 10%, a redline zone \
above 80%, and a line needle pointing to the eased progress value"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let a_start = 225_f32.to_radians(); let span = -(270_f32.to_radians());
let arc_steps = ((r as f32 * 2.0 * PI * 0.75).round() as usize).max(4);
arc(grid, cx, cy, r, a_start, a_start + span, arc_steps);
for i in 0..=10 {
let frac = i as f32 / 10.0;
let angle = a_start + frac * span;
let tick_len = if i % 5 == 0 {
(r / 4).max(1)
} else {
(r / 6).max(1)
};
let x0 = cx + ((r - tick_len) as f32 * angle.cos()).round() as i32;
let y0 = cy - ((r - tick_len) as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
let r_inner = (r - (r / 4).max(1)).max(1);
let a_red0 = a_start + 0.8 * span;
let a_red1 = a_start + span;
let red_steps = ((r as f32 * (a_red1 - a_red0).abs()).round() as usize).max(4);
for rr in r_inner..=r {
arc(grid, cx, cy, rr, a_red0, a_red1, red_steps);
}
let needle_angle = a_start + ctx.eased.clamp(0.0, 1.0) * span;
let nx = cx + (r as f32 * needle_angle.cos()).round() as i32;
let ny = cy - (r as f32 * needle_angle.sin()).round() as i32;
bresenham(grid, cx, cy, nx, ny);
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = cx_c as f32 / cells_w.max(1) as f32;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct RingProgress;
impl ProgressStyle for RingProgress {
fn name(&self) -> &str {
"ring-progress"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Thin full-circle ring that fills clockwise from 12 o'clock; \
the filled arc length = eased × 2π, the unfilled portion stays dim"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let a_top = PI / 2.0;
let full_steps = ((r as f32 * 2.0 * PI).round() as usize).max(4);
for i in (0..=full_steps).step_by(2) {
let t = i as f32 / full_steps as f32;
let angle = a_top - t * 2.0 * PI; let x = cx + (r as f32 * angle.cos()).round() as i32;
let y = cy - (r as f32 * angle.sin()).round() as i32;
draw::dot_i(grid, x, y);
}
let filled_steps = ((r as f32 * 2.0 * PI * ctx.eased.clamp(0.0, 1.0)).round() as usize)
.max(if ctx.eased > 0.0 { 1 } else { 0 });
for i in 0..=filled_steps {
let t = if filled_steps == 0 {
0.0
} else {
i as f32 / filled_steps as f32 * ctx.eased
};
let angle = a_top - t * 2.0 * PI;
let x = cx + (r as f32 * angle.cos()).round() as i32;
let y = cy - (r as f32 * angle.sin()).round() as i32;
draw::dot_i(grid, x, y);
if r > 2 {
let xi = cx + ((r - 1) as f32 * angle.cos()).round() as i32;
let yi = cy - ((r - 1) as f32 * angle.sin()).round() as i32;
draw::dot_i(grid, xi, yi);
}
}
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = ctx.eased;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct HalfGauge;
impl ProgressStyle for HalfGauge {
fn name(&self) -> &str {
"half-gauge"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"180° semicircle gauge (fuel/temperature style): arc spans left→right \
across the top of the cell, a needle swings from empty (left) to full (right)"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let a_left = PI; let a_right = 0.0_f32;
let arc_steps = ((r as f32 * PI).round() as usize).max(4);
arc(grid, cx, cy, r, a_left, a_right, arc_steps);
for i in 0..=4 {
let frac = i as f32 / 4.0;
let angle = a_left + frac * (a_right - a_left);
let tick_len = if i % 2 == 0 {
(r / 4).max(1)
} else {
(r / 6).max(1)
};
let x0 = cx + ((r - tick_len) as f32 * angle.cos()).round() as i32;
let y0 = cy - ((r - tick_len) as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
let lx = cx - r;
let rx = cx + r;
if lx >= 0 {
draw::hline(
grid,
lx as usize,
rx.min(dw as i32 - 1).max(0) as usize,
cy as usize,
);
}
let needle_angle = a_left + ctx.eased.clamp(0.0, 1.0) * (a_right - a_left);
let needle_r = (r - 1).max(1);
let nx = cx + (needle_r as f32 * needle_angle.cos()).round() as i32;
let ny = cy - (needle_r as f32 * needle_angle.sin()).round() as i32;
bresenham(grid, cx, cy, nx, ny);
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = cx_c as f32 / cells_w.max(1) as f32;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct Tachometer;
impl ProgressStyle for Tachometer {
fn name(&self) -> &str {
"tachometer"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Tachometer: 270° rev-counter dial with a needle that overshoots the \
target and damps back — the settle wobble is driven by ctx.time so \
the needle visibly hunts to the eased value"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let a_start = 225_f32.to_radians();
let span = -(270_f32.to_radians());
let arc_steps = ((r as f32 * 2.0 * PI * 0.75).round() as usize).max(4);
arc(grid, cx, cy, r, a_start, a_start + span, arc_steps);
for i in 0..=20 {
let frac = i as f32 / 20.0;
let angle = a_start + frac * span;
let tick_len = if i % 4 == 0 {
(r / 3).max(1)
} else {
(r / 7).max(1)
};
let x0 = cx + ((r - tick_len) as f32 * angle.cos()).round() as i32;
let y0 = cy - ((r - tick_len) as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
let wobble = 0.12 * (8.0 * ctx.time).sin() * (-1.5 * ctx.time).exp();
let effective = (ctx.eased + wobble).clamp(0.0, 1.0);
let needle_angle = a_start + effective * span;
let nx = cx + (r as f32 * needle_angle.cos()).round() as i32;
let ny = cy - (r as f32 * needle_angle.sin()).round() as i32;
bresenham(grid, cx, cy, nx, ny);
let back_angle = needle_angle + PI;
let bx = cx + ((r / 6).max(1) as f32 * back_angle.cos()).round() as i32;
let by = cy - ((r / 6).max(1) as f32 * back_angle.sin()).round() as i32;
bresenham(grid, cx, cy, bx, by);
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = cx_c as f32 / cells_w.max(1) as f32;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct Donut;
impl ProgressStyle for Donut {
fn name(&self) -> &str {
"donut"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Thick donut / pie: a filled wedge between an inner and outer radius \
sweeps clockwise from 12 o'clock as eased grows — the hole stays empty, \
the wedge fills with braille dots"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let r_inner = ((r as f32 * 0.45).round() as i32).max(1);
let a_top = PI / 2.0;
let sweep = ctx.eased.clamp(0.0, 1.0) * 2.0 * PI;
let a_end = a_top - sweep;
let rim_steps = ((r as f32 * 2.0 * PI).round() as usize).max(4);
arc(grid, cx, cy, r, a_top, a_top - 2.0 * PI, rim_steps);
let hole_steps = ((r_inner as f32 * 2.0 * PI).round() as usize).max(4);
arc(grid, cx, cy, r_inner, a_top, a_top - 2.0 * PI, hole_steps);
if sweep > 0.001 {
thick_arc(grid, cx, cy, r_inner, r, a_top, a_end);
}
let color = ctx.palette.sample(ctx.eased);
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
draw::tint_row(grid, cy_c, cx_c, cx_c, color);
}
}
Ok(())
}
}
struct SignalArc;
impl ProgressStyle for SignalArc {
fn name(&self) -> &str {
"signal-arc"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Concentric arc bands like a cell-signal or Wi-Fi icon: each tier is a \
thicker arc; bands light up from innermost outward as eased rises"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r_max) = dial_fit(dw, dh);
let a_left = 135_f32.to_radians();
let a_right = 45_f32.to_radians();
let n_tiers = ((r_max / 3).max(1) as usize).min(5);
let lit = (ctx.eased.clamp(0.0, 1.0) * n_tiers as f32).ceil() as usize;
draw::dot_i(grid, cx, cy);
for tier in 0..n_tiers {
if tier >= lit {
break;
}
let r_inner = (tier as i32 * (r_max / n_tiers as i32).max(1)) + 1;
let r_outer = ((tier as i32 + 1) * (r_max / n_tiers as i32).max(1)).min(r_max);
if r_inner > r_outer {
continue;
}
thick_arc(grid, cx, cy, r_inner, r_outer, a_left, a_right);
}
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = cx_c as f32 / cells_w.max(1) as f32;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct Compass;
impl ProgressStyle for Compass {
fn name(&self) -> &str {
"compass"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Compass rose: a full circle with N/E/S/W tick marks and a bearing \
needle that points to eased × 360° — progress literally steers the heading"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let steps = ((r as f32 * 2.0 * PI).round() as usize).max(4);
arc(grid, cx, cy, r, 0.0, 2.0 * PI, steps);
let cardinals = [
PI / 2.0, 0.0_f32, -PI / 2.0, PI, ];
for (i, &angle) in cardinals.iter().enumerate() {
let tick_len = if i % 2 == 0 {
(r / 3).max(1) } else {
(r / 4).max(1)
};
let x0 = cx + ((r - tick_len) as f32 * angle.cos()).round() as i32;
let y0 = cy - ((r - tick_len) as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
for i in 0..4 {
let angle = PI / 4.0 + i as f32 * PI / 2.0;
let tick_len = (r / 6).max(1);
let x0 = cx + ((r - tick_len) as f32 * angle.cos()).round() as i32;
let y0 = cy - ((r - tick_len) as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
let bearing = PI / 2.0 - ctx.eased.clamp(0.0, 1.0) * 2.0 * PI;
let needle_r = (r - (r / 5).max(1)).max(1);
let nx = cx + (needle_r as f32 * bearing.cos()).round() as i32;
let ny = cy - (needle_r as f32 * bearing.sin()).round() as i32;
bresenham(grid, cx, cy, nx, ny);
let tail_r = (r / 4).max(1);
let tx = cx + (tail_r as f32 * (bearing + PI).cos()).round() as i32;
let ty = cy - (tail_r as f32 * (bearing + PI).sin()).round() as i32;
bresenham(grid, cx, cy, tx, ty);
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
let color = ctx.palette.sample(ctx.eased);
draw::tint_row(grid, cy_c, 0, cells_w.saturating_sub(1), color);
}
Ok(())
}
}
struct VuNeedle;
impl ProgressStyle for VuNeedle {
fn name(&self) -> &str {
"vu-needle"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Analog VU-meter needle: the needle bounces rhythmically around the \
eased level driven by ctx.time, mimicking the momentum of a real \
moving-coil meter in a recording studio"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let a_left = (90.0 + 60.0_f32).to_radians();
let a_right = (90.0 - 60.0_f32).to_radians();
let span = a_right - a_left;
let arc_steps = ((r as f32 * (a_left - a_right)).round() as usize).max(4);
arc(grid, cx, cy, r, a_left, a_right, arc_steps);
for i in 0..=6 {
let frac = i as f32 / 6.0;
let angle = a_left + frac * span;
let tick_len = if i == 3 {
(r / 3).max(1)
} else {
(r / 5).max(1)
};
let x0 = cx + ((r - tick_len) as f32 * angle.cos()).round() as i32;
let y0 = cy - ((r - tick_len) as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
let freq = 3.0 + ctx.eased * 4.0;
let bounce = 0.08 * (freq * ctx.time).sin() + 0.04 * (freq * 1.7 * ctx.time + 0.3).sin();
let effective = (ctx.eased + bounce).clamp(0.0, 1.0);
let needle_angle = a_left + effective * span;
let nx = cx + (r as f32 * needle_angle.cos()).round() as i32;
let ny = cy - (r as f32 * needle_angle.sin()).round() as i32;
bresenham(grid, cx, cy, nx, ny);
let color = ctx.palette.sample(ctx.eased);
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
draw::tint_row(grid, cy_c, 0, cells_w.saturating_sub(1), color);
}
Ok(())
}
}
struct SegmentedRing;
impl ProgressStyle for SegmentedRing {
fn name(&self) -> &str {
"segmented-ring"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Discrete LED-style segments arranged around a full ring: each is a \
short arc of dots separated by a small gap; segments light up clockwise \
from 12 o'clock one by one as eased rises"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let n_segs: usize = 24;
let gap_frac = 0.12_f32; let lit = (ctx.eased.clamp(0.0, 1.0) * n_segs as f32).round() as usize;
for seg in 0..n_segs {
let seg_start = seg as f32 / n_segs as f32;
let seg_end = (seg + 1) as f32 / n_segs as f32;
let inner_start = seg_start + gap_frac / n_segs as f32;
let inner_end = seg_end - gap_frac / n_segs as f32;
let a0 = PI / 2.0 - inner_start * 2.0 * PI;
let a1 = PI / 2.0 - inner_end * 2.0 * PI;
let seg_steps = ((r as f32 * (a0 - a1).abs()).round() as usize).max(2);
if seg < lit {
let r_in = (r - 2).max(1);
thick_arc(grid, cx, cy, r_in, r, a0, a1);
} else {
arc(grid, cx, cy, r, a0, a1, seg_steps);
}
}
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = lit as f32 / n_segs as f32;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct DualGauge;
impl ProgressStyle for DualGauge {
fn name(&self) -> &str {
"dual-gauge"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Two concentric arc gauges: the outer ring tracks eased progress while \
the inner ring oscillates with time, showing a secondary live reading \
(e.g. instantaneous vs. average) in the same braille dial"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r_outer) = dial_fit(dw, dh);
let r_inner = ((r_outer as f32 * 0.55).round() as i32).max(2);
let a_top = PI / 2.0;
let outer_sweep = ctx.eased.clamp(0.0, 1.0) * 2.0 * PI;
let outer_track_steps = ((r_outer as f32 * 2.0 * PI).round() as usize).max(4);
for i in (0..=outer_track_steps).step_by(3) {
let t = i as f32 / outer_track_steps as f32;
let angle = a_top - t * 2.0 * PI;
let x = cx + (r_outer as f32 * angle.cos()).round() as i32;
let y = cy - (r_outer as f32 * angle.sin()).round() as i32;
draw::dot_i(grid, x, y);
}
if outer_sweep > 0.001 {
thick_arc(
grid,
cx,
cy,
r_outer - 1,
r_outer,
a_top,
a_top - outer_sweep,
);
}
let secondary = 0.5 + 0.5 * (ctx.time * 1.3).sin(); let inner_sweep = secondary * 2.0 * PI;
let inner_track_steps = ((r_inner as f32 * 2.0 * PI).round() as usize).max(4);
for i in (0..=inner_track_steps).step_by(3) {
let t = i as f32 / inner_track_steps as f32;
let angle = a_top - t * 2.0 * PI;
let x = cx + (r_inner as f32 * angle.cos()).round() as i32;
let y = cy - (r_inner as f32 * angle.sin()).round() as i32;
draw::dot_i(grid, x, y);
}
if inner_sweep > 0.001 {
thick_arc(
grid,
cx,
cy,
r_inner - 1,
r_inner,
a_top,
a_top - inner_sweep,
);
}
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = ctx.eased;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct ClockFace;
impl ProgressStyle for ClockFace {
fn name(&self) -> &str {
"clock-face"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Analog clock face: progress maps the full 12-hour cycle — hour hand \
completes one revolution at 100%, minute hand moves 12× faster, \
giving a two-hand clock whose time equals eased × 12:00"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let rim_steps = ((r as f32 * 2.0 * PI).round() as usize).max(4);
arc(grid, cx, cy, r, 0.0, 2.0 * PI, rim_steps);
for h in 0..12 {
let angle = PI / 2.0 - h as f32 * 2.0 * PI / 12.0;
let tick_len = if h % 3 == 0 {
(r / 4).max(1)
} else {
(r / 7).max(1)
};
let x0 = cx + ((r - tick_len) as f32 * angle.cos()).round() as i32;
let y0 = cy - ((r - tick_len) as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
let hour_angle = PI / 2.0 - ctx.eased.clamp(0.0, 1.0) * 2.0 * PI;
let hour_r = ((r as f32 * 0.6).round() as i32).max(1);
let hx = cx + (hour_r as f32 * hour_angle.cos()).round() as i32;
let hy = cy - (hour_r as f32 * hour_angle.sin()).round() as i32;
bresenham(grid, cx, cy, hx, hy);
let minute_angle = PI / 2.0 - ctx.eased.clamp(0.0, 1.0) * 12.0 * 2.0 * PI;
let minute_r = ((r as f32 * 0.85).round() as i32).max(1);
let mx = cx + (minute_r as f32 * minute_angle.cos()).round() as i32;
let my = cy - (minute_r as f32 * minute_angle.sin()).round() as i32;
bresenham(grid, cx, cy, mx, my);
draw::dot_i(grid, cx, cy);
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = cx_c as f32 / cells_w.max(1) as f32;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct PressureDial;
impl ProgressStyle for PressureDial {
fn name(&self) -> &str {
"pressure-dial"
}
fn theme(&self) -> &str {
"meter"
}
fn describe(&self) -> &str {
"Pressure gauge with a hatched danger arc beyond 75% and a needle that \
overdamps into place — slow creep at low pressure, urgent quiver at high; \
danger zone fills with a dense dot pattern when eased exceeds the threshold"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let (cx, cy, r) = dial_fit(dw, dh);
let a_start = 210_f32.to_radians();
let span = -(240_f32.to_radians());
let arc_steps = ((r as f32 * 2.0 * PI * (240.0 / 360.0)).round() as usize).max(4);
arc(grid, cx, cy, r, a_start, a_start + span, arc_steps);
for i in 0..=5 {
let frac = i as f32 / 5.0;
let angle = a_start + frac * span;
let tick_len = if i % 5 == 0 {
(r / 3).max(1)
} else {
(r / 6).max(1)
};
let x0 = cx + ((r - tick_len) as f32 * angle.cos()).round() as i32;
let y0 = cy - ((r - tick_len) as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
let danger_threshold = 0.75_f32;
let a_danger0 = a_start + danger_threshold * span;
let a_danger1 = a_start + span;
let r_in_danger = (r - (r / 4).max(1)).max(1);
let danger_steps = ((r as f32 * (a_danger0 - a_danger1).abs()).round() as usize).max(4);
arc(grid, cx, cy, r, a_danger0, a_danger1, danger_steps);
arc(
grid,
cx,
cy,
r_in_danger,
a_danger0,
a_danger1,
danger_steps,
);
let n_hatch = 4usize.max((r / 5) as usize);
for h in 0..=n_hatch {
let frac = h as f32 / n_hatch as f32;
let angle = a_danger0 + frac * (a_danger1 - a_danger0);
let x0 = cx + (r_in_danger as f32 * angle.cos()).round() as i32;
let y0 = cy - (r_in_danger as f32 * angle.sin()).round() as i32;
let x1 = cx + (r as f32 * angle.cos()).round() as i32;
let y1 = cy - (r as f32 * angle.sin()).round() as i32;
bresenham(grid, x0, y0, x1, y1);
}
if ctx.eased > danger_threshold {
let extra_frac = (ctx.eased - danger_threshold) / (1.0 - danger_threshold);
let a_fill_end = a_danger0 + extra_frac * (a_danger1 - a_danger0);
let fill_steps =
((r as f32 * (a_danger0 - a_fill_end).abs() * 2.0).round() as usize).max(2);
thick_arc(grid, cx, cy, r_in_danger, r, a_danger0, a_fill_end);
let _ = fill_steps;
}
let damp = (-2.0 * ctx.time).exp();
let effective = ctx.eased * (1.0 - damp);
let needle_angle = a_start + effective.clamp(0.0, 1.0) * span;
let nx = cx + (r as f32 * needle_angle.cos()).round() as i32;
let ny = cy - (r as f32 * needle_angle.sin()).round() as i32;
bresenham(grid, cx, cy, nx, ny);
if ctx.eased > 0.6 {
let quiver_amp = (ctx.eased - 0.6) / 0.4 * 0.03;
let quiver = quiver_amp * (20.0 * ctx.time).sin();
let q_angle = needle_angle + quiver;
let qx = cx + (r as f32 * q_angle.cos()).round() as i32;
let qy = cy - (r as f32 * q_angle.sin()).round() as i32;
bresenham(grid, cx, cy, qx, qy);
}
let (cells_w, cells_h) = grid.dimensions();
for cy_c in 0..cells_h {
for cx_c in 0..cells_w {
let t = ctx.eased;
draw::tint_row(grid, cy_c, cx_c, cx_c, ctx.palette.sample(t));
}
}
Ok(())
}
}