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(LinearLR),
Box::new(LinearTB),
Box::new(BarnDoor),
Box::new(Iris),
Box::new(IrisDiamond),
Box::new(Diagonal),
Box::new(Checkerboard),
Box::new(VenetianBlinds),
Box::new(Dissolve),
Box::new(ClockWipe),
Box::new(Spiral),
Box::new(Split),
Box::new(Zigzag),
Box::new(Pixelate),
]
}
struct LinearLR;
impl ProgressStyle for LinearLR {
fn name(&self) -> &str {
"linear-lr"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Hard-edge curtain sweeping left → right: a clean vertical wipe line \
advances from the left edge to the right as progress rises 0→1"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let filled = (ctx.eased * dw as f32).round() as usize;
draw::fill_rect(grid, 0, 0, filled.min(dw), dh);
let (cw, ch) = grid.dimensions();
let filled_cells = (ctx.eased * cw as f32).round() as usize;
for cx in 0..filled_cells.min(cw) {
let t = if cw <= 1 {
0.5
} else {
cx as f32 / (cw - 1) as f32
};
let color = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, color);
}
}
Ok(())
}
}
struct LinearTB;
impl ProgressStyle for LinearTB {
fn name(&self) -> &str {
"linear-tb"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Horizontal curtain dropping top → bottom: the revealed band grows \
downward one dot-row at a time as progress rises"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let filled_rows = (ctx.eased * dh as f32).round() as usize;
draw::fill_rect(grid, 0, 0, dw, filled_rows.min(dh));
let (cw, ch) = grid.dimensions();
let filled_cell_rows = (ctx.eased * ch as f32).round() as usize;
for cy in 0..filled_cell_rows.min(ch) {
let t = if ch <= 1 {
0.5
} else {
cy as f32 / (ch - 1) as f32
};
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy, 0, cw.saturating_sub(1), color);
}
Ok(())
}
}
struct BarnDoor;
impl ProgressStyle for BarnDoor {
fn name(&self) -> &str {
"barn-door"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Barn-door wipe: the left half slides left and the right half slides \
right, parting from the vertical centre seam as progress rises"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let half = dw / 2;
let reach = (ctx.eased * half as f32).round() as usize;
let lx = half.saturating_sub(reach);
draw::fill_rect(grid, lx, 0, half.saturating_sub(lx), dh);
let rx_end = (half + reach).min(dw);
if rx_end > half {
draw::fill_rect(grid, half, 0, rx_end - half, dh);
}
let (cw, ch) = grid.dimensions();
let half_c = cw / 2;
let reach_c = (ctx.eased * half_c as f32).round() as usize;
let lx_c = half_c.saturating_sub(reach_c);
let rx_end_c = (half_c + reach_c).min(cw);
for cy in 0..ch {
let color_l = ctx.palette.sample(0.2);
let color_r = ctx.palette.sample(0.8);
if lx_c < half_c {
draw::tint_row(grid, cy, lx_c, half_c.saturating_sub(1), color_l);
}
if rx_end_c > half_c {
draw::tint_row(grid, cy, half_c, rx_end_c.saturating_sub(1), color_r);
}
}
Ok(())
}
}
struct Iris;
impl ProgressStyle for Iris {
fn name(&self) -> &str {
"iris"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Iris wipe: a filled circle expands from the grid's centre, its radius \
growing from zero to cover the full screen as progress reaches 1"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let cx = dw as f32 / 2.0;
let cy = dh as f32 / 2.0;
let max_r = ((cx * cx + cy * cy) as f32).sqrt() + 1.0;
let r = ctx.eased * max_r;
let r2 = r * r;
for dy in 0..dh {
let vy = dy as f32 + 0.5 - cy;
for dx in 0..dw {
let vx = dx as f32 + 0.5 - cx;
if vx * vx + vy * vy <= r2 {
draw::dot(grid, dx, dy);
}
}
}
let (cw, ch) = grid.dimensions();
let ccx = cw as f32 / 2.0;
let ccy = ch as f32 / 2.0;
let max_rc = ((ccx * ccx + ccy * ccy) as f32).sqrt().max(1.0);
for cy_idx in 0..ch {
for cx_idx in 0..cw {
let vx = cx_idx as f32 + 0.5 - ccx;
let vy = cy_idx as f32 + 0.5 - ccy;
let dist = (vx * vx + vy * vy).sqrt();
if dist / max_rc <= ctx.eased {
let t = (dist / max_rc).clamp(0.0, 1.0);
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy_idx, cx_idx, cx_idx, color);
}
}
}
Ok(())
}
}
struct IrisDiamond;
impl ProgressStyle for IrisDiamond {
fn name(&self) -> &str {
"iris-diamond"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Diamond iris wipe: a filled rhombus expands from the centre using \
Manhattan (L1) distance — produces sharp 45° diamond edges instead of \
the circular iris's smooth curve"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let cx = dw as f32 / 2.0;
let cy = dh as f32 / 2.0;
let max_r = cx + cy + 1.0;
let r = ctx.eased * max_r;
for dy in 0..dh {
let vy = (dy as f32 + 0.5 - cy).abs();
for dx in 0..dw {
let vx = (dx as f32 + 0.5 - cx).abs();
if vx + vy <= r {
draw::dot(grid, dx, dy);
}
}
}
let (cw, ch) = grid.dimensions();
let ccx = cw as f32 / 2.0;
let ccy = ch as f32 / 2.0;
let max_rc = (ccx + ccy).max(1.0);
for cy_idx in 0..ch {
for cx_idx in 0..cw {
let vx = (cx_idx as f32 + 0.5 - ccx).abs();
let vy = (cy_idx as f32 + 0.5 - ccy).abs();
let dist = vx + vy;
if dist / max_rc <= ctx.eased {
let t = (dist / max_rc).clamp(0.0, 1.0);
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy_idx, cx_idx, cx_idx, color);
}
}
}
Ok(())
}
}
struct Diagonal;
impl ProgressStyle for Diagonal {
fn name(&self) -> &str {
"diagonal"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Diagonal wipe: a 45° slanted edge sweeps from the top-left corner to \
the bottom-right, revealing the screen along the anti-diagonal axis"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let total = (dw + dh) as f32;
let threshold = ctx.eased * total;
for dy in 0..dh {
for dx in 0..dw {
if (dx as f32 + dy as f32) < threshold {
draw::dot(grid, dx, dy);
}
}
}
let (cw, ch) = grid.dimensions();
let total_c = (cw + ch) as f32;
for cy_idx in 0..ch {
for cx_idx in 0..cw {
let diag = (cx_idx + cy_idx) as f32;
if diag / total_c <= ctx.eased {
let t = (diag / total_c).clamp(0.0, 1.0);
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy_idx, cx_idx, cx_idx, color);
}
}
}
Ok(())
}
}
struct Checkerboard;
impl ProgressStyle for Checkerboard {
fn name(&self) -> &str {
"checkerboard"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Checkerboard wipe: the screen is split into an alternating tile grid; \
even tiles fill first and odd tiles follow, creating a two-pass \
chequerboard dissolve as progress crosses 0.5"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let tile_w = 4usize.max(1);
let tile_h = 4usize.max(1);
for ty in 0.. {
let y0 = ty * tile_h;
if y0 >= dh {
break;
}
for tx in 0.. {
let x0 = tx * tile_w;
if x0 >= dw {
break;
}
let phase = if (tx + ty) % 2 == 0 { 0.0f32 } else { 0.5 };
if ctx.eased > phase {
let tw = tile_w.min(dw.saturating_sub(x0));
let th = tile_h.min(dh.saturating_sub(y0));
draw::fill_rect(grid, x0, y0, tw, th);
}
}
}
let (cw, ch) = grid.dimensions();
for cy_idx in 0..ch {
for cx_idx in 0..cw {
let tx = cx_idx / 2;
let ty = cy_idx;
let phase = if (tx + ty) % 2 == 0 { 0.0f32 } else { 0.5 };
if ctx.eased > phase {
let t = if (tx + ty) % 2 == 0 { 0.3 } else { 0.7 };
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy_idx, cx_idx, cx_idx, color);
}
}
}
Ok(())
}
}
struct VenetianBlinds;
impl ProgressStyle for VenetianBlinds {
fn name(&self) -> &str {
"venetian-blinds"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Venetian-blinds wipe: the screen is divided into horizontal slats; each \
slat opens symmetrically from its own centreline, all in unison, so the \
full height is revealed in parallel stripes"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let slat_h = 8usize.max(1);
let num_slats = (dh + slat_h - 1) / slat_h;
for s in 0..num_slats {
let top = s * slat_h;
let slat_actual = slat_h.min(dh.saturating_sub(top));
let open_half = (ctx.eased * slat_actual as f32 / 2.0).round() as usize;
let mid = top + slat_actual / 2;
let y0 = mid.saturating_sub(open_half);
let y1 = (mid + open_half).min(top + slat_actual);
if y1 > y0 {
draw::fill_rect(grid, 0, y0, dw, y1 - y0);
}
}
let (cw, ch) = grid.dimensions();
let slat_c = 2usize.max(1);
for cy_idx in 0..ch {
let slat_idx = cy_idx / slat_c;
let t = if slat_idx % 2 == 0 { 0.25 } else { 0.75 };
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy_idx, 0, cw.saturating_sub(1), color);
}
Ok(())
}
}
const BAYER: [[f32; 4]; 4] = [
[0.0 / 16.0, 8.0 / 16.0, 2.0 / 16.0, 10.0 / 16.0],
[12.0 / 16.0, 4.0 / 16.0, 14.0 / 16.0, 6.0 / 16.0],
[3.0 / 16.0, 11.0 / 16.0, 1.0 / 16.0, 9.0 / 16.0],
[15.0 / 16.0, 7.0 / 16.0, 13.0 / 16.0, 5.0 / 16.0],
];
struct Dissolve;
impl ProgressStyle for Dissolve {
fn name(&self) -> &str {
"dissolve"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Ordered Bayer-dither dissolve: each dot has a threshold from a 4×4 \
matrix; it lights up when progress exceeds its threshold, producing a \
structured stipple that expands uniformly across the screen"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
for dy in 0..dh {
for dx in 0..dw {
let bx = dx % 4;
let by = dy % 4;
if BAYER[by][bx] < ctx.eased {
draw::dot(grid, dx, dy);
}
}
}
let (cw, ch) = grid.dimensions();
let color = ctx.palette.sample(0.5);
for cy_idx in 0..ch {
draw::tint_row(grid, cy_idx, 0, cw.saturating_sub(1), color);
}
Ok(())
}
}
struct ClockWipe;
impl ProgressStyle for ClockWipe {
fn name(&self) -> &str {
"clock-wipe"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Clock wipe: a radial pie slice rotates clockwise from 12 o'clock, \
sweeping the filled region around the centre like a clock hand until \
the full circle is revealed at progress 1"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let cx = dw as f32 / 2.0;
let cy = dh as f32 / 2.0;
let sweep = ctx.eased * 2.0 * PI;
let start = -PI / 2.0; for dy in 0..dh {
let vy = dy as f32 + 0.5 - cy;
for dx in 0..dw {
let vx = dx as f32 + 0.5 - cx;
let angle = vx.atan2(-vy); let norm_angle = if angle < start {
angle - start + 2.0 * PI
} else {
angle - start
};
if norm_angle < sweep {
draw::dot(grid, dx, dy);
}
}
}
let (cw, ch) = grid.dimensions();
let ccx = cw as f32 / 2.0;
let ccy = ch as f32 / 2.0;
for cy_idx in 0..ch {
for cx_idx in 0..cw {
let vx = cx_idx as f32 + 0.5 - ccx;
let vy = cy_idx as f32 + 0.5 - ccy;
let angle = vx.atan2(-vy);
let norm_angle = if angle < start {
angle - start + 2.0 * PI
} else {
angle - start
};
if norm_angle < sweep {
let t = (norm_angle / (2.0 * PI)).clamp(0.0, 1.0);
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy_idx, cx_idx, cx_idx, color);
}
}
}
Ok(())
}
}
struct Spiral;
impl ProgressStyle for Spiral {
fn name(&self) -> &str {
"spiral"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Archimedean spiral wipe: each dot is revealed according to its spiral \
parameter θ = √(r/r_max)·N_turns·2π, so the filled region uncoils \
outward from the centre in a continuous tight helix"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let cx = dw as f32 / 2.0;
let cy = dh as f32 / 2.0;
let max_r = ((cx * cx + cy * cy) as f32).sqrt().max(1.0);
let n_turns = 3.0f32;
let two_pi = 2.0 * PI;
for dy in 0..dh {
let vy = dy as f32 + 0.5 - cy;
for dx in 0..dw {
let vx = dx as f32 + 0.5 - cx;
let r = (vx * vx + vy * vy).sqrt();
let r_frac = (r / max_r).clamp(0.0, 1.0);
let raw_angle = vx.atan2(-vy); let angle = if raw_angle < 0.0 {
raw_angle + two_pi
} else {
raw_angle
};
let phase = (angle / two_pi + r_frac * n_turns) / n_turns;
if phase <= ctx.eased {
draw::dot(grid, dx, dy);
}
}
}
let (cw, ch) = grid.dimensions();
let ccx = cw as f32 / 2.0;
let ccy = ch as f32 / 2.0;
let max_rc = ((ccx * ccx + ccy * ccy) as f32).sqrt().max(1.0);
for cy_idx in 0..ch {
for cx_idx in 0..cw {
let vx = cx_idx as f32 + 0.5 - ccx;
let vy = cy_idx as f32 + 0.5 - ccy;
let r = (vx * vx + vy * vy).sqrt();
let r_frac = r / max_rc;
let raw_angle = vx.atan2(-vy);
let angle = if raw_angle < 0.0 {
raw_angle + two_pi
} else {
raw_angle
};
let phase = (angle / two_pi + r_frac * n_turns) / n_turns;
if phase <= ctx.eased {
let t = r_frac.clamp(0.0, 1.0);
let color = ctx.palette.sample(t);
draw::tint_row(grid, cy_idx, cx_idx, cx_idx, color);
}
}
}
Ok(())
}
}
struct Split;
impl ProgressStyle for Split {
fn name(&self) -> &str {
"split"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Split wipe: two horizontal bands advance from the top edge and bottom \
edge simultaneously, meeting at the vertical midline when progress = 1"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let half_h = dh / 2;
let reach = (ctx.eased * half_h as f32).round() as usize;
draw::fill_rect(grid, 0, 0, dw, reach.min(dh));
let bot_start = dh.saturating_sub(reach);
if bot_start < dh {
draw::fill_rect(grid, 0, bot_start, dw, dh - bot_start);
}
let (cw, ch) = grid.dimensions();
let half_c = ch / 2;
let reach_c = (ctx.eased * half_c as f32).round() as usize;
for cy_idx in 0..reach_c.min(ch) {
let color = ctx.palette.sample(0.15);
draw::tint_row(grid, cy_idx, 0, cw.saturating_sub(1), color);
}
let bot_start_c = ch.saturating_sub(reach_c);
for cy_idx in bot_start_c..ch {
let color = ctx.palette.sample(0.85);
draw::tint_row(grid, cy_idx, 0, cw.saturating_sub(1), color);
}
Ok(())
}
}
struct Zigzag;
impl ProgressStyle for Zigzag {
fn name(&self) -> &str {
"zigzag"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Zigzag wipe: a left-to-right curtain whose leading edge is a sine wave, \
producing a rippling serrated boundary that sweeps across the screen"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (dw, dh) = draw::dot_dims(grid);
let mean_x = ctx.eased * dw as f32;
let amp = (dh as f32 * 0.35).max(1.0);
let freq = 2.0 * PI / dh.max(1) as f32;
let phase = ctx.time * 1.2;
for dy in 0..dh {
let boundary = mean_x + amp * (freq * dy as f32 + phase).sin();
let col_x = boundary.round() as i32;
let fill_end = col_x.max(0) as usize;
for dx in 0..fill_end.min(dw) {
draw::dot(grid, dx, dy);
}
}
let (cw, ch) = grid.dimensions();
for cy_idx in 0..ch {
for cx_idx in 0..cw {
let t = if cw <= 1 {
0.5
} else {
cx_idx as f32 / (cw - 1) as f32
};
let color = ctx.palette.sample(t);
let dy_mid = cy_idx * 4 + 2;
let boundary = mean_x + amp * (freq * dy_mid as f32 + phase).sin();
if (cx_idx * 2) as f32 + 1.0 < boundary {
draw::tint_row(grid, cy_idx, cx_idx, cx_idx, color);
}
}
}
Ok(())
}
}
struct Pixelate;
impl ProgressStyle for Pixelate {
fn name(&self) -> &str {
"pixelate"
}
fn theme(&self) -> &str {
"wipe"
}
fn describe(&self) -> &str {
"Pixelate wipe: the screen is divided into coarse 2×1-cell blocks, each \
ramp from empty → ░ → ▒ → ▓ → █ as progress rises through four density \
thresholds — a block-density mosaic that fills by region, not by edge"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (cw, ch) = grid.dimensions();
let block_w = 2usize.max(1);
let block_h = 1usize.max(1);
let num_bx = (cw + block_w - 1) / block_w;
let num_by = (ch + block_h - 1) / block_h;
let total_blocks = (num_bx * num_by).max(1);
for bby in 0..num_by {
for bbx in 0..num_bx {
let hash = hash2(bbx as u32, bby as u32);
let threshold = hash as f32 / u32::MAX as f32;
let local = if ctx.eased <= threshold {
0.0
} else {
((ctx.eased - threshold) / (1.0 - threshold + 1e-6)).clamp(0.0, 1.0)
};
let level = (local * 4.0).floor() as usize;
let cx0 = bbx * block_w;
let cy0 = bby * block_h;
for dy in 0..block_h {
for dx in 0..block_w {
let cell_x = cx0 + dx;
let cell_y = cy0 + dy;
if cell_x < cw && cell_y < ch {
draw::shade(grid, cell_x, cell_y, level);
}
}
}
if level > 0 {
let t = threshold;
let color = ctx.palette.sample(t);
for dy in 0..block_h {
let cell_y = cy0 + dy;
if cell_y < ch {
let cx1 = (cx0 + block_w - 1).min(cw.saturating_sub(1));
draw::tint_row(grid, cell_y, cx0.min(cw.saturating_sub(1)), cx1, color);
}
}
}
let _ = total_blocks; }
}
Ok(())
}
}
#[inline]
fn hash2(x: u32, y: u32) -> u32 {
let mut h = x
.wrapping_mul(2654435761)
.wrapping_add(y.wrapping_mul(2246822519));
h ^= h >> 16;
h = h.wrapping_mul(0x45d9f3b);
h ^= h >> 16;
h
}