use super::super::draw;
use super::super::{BarContext, ProgressStyle};
use crate::{BrailleGrid, DotmaxError};
use std::f32::consts::PI;
#[inline]
fn hash(n: u32) -> u32 {
let mut x = n.wrapping_mul(2_654_435_761);
x ^= x >> 15;
x.wrapping_mul(2_246_822_519)
}
#[inline]
fn hashf(n: u32) -> f32 {
(hash(n) % 1000) as f32 / 1000.0
}
fn beam_line(grid: &mut BrailleGrid, x0: i32, y0: i32, x1: i32, y1: i32, max_steps: usize) {
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 mut cx = x0;
let mut cy = y0;
let steps = (dx + dy + 2) as usize;
let limit = steps.min(max_steps);
for _ in 0..limit {
draw::dot_i(grid, cx, cy);
if cx == x1 && cy == y1 {
break;
}
let e2 = err * 2;
if e2 > -dy {
err -= dy;
cx += sx;
}
if e2 < dx {
err += dx;
cy += sy;
}
}
}
pub fn styles() -> Vec<Box<dyn ProgressStyle>> {
vec![
Box::new(ChargeAndFire),
Box::new(ScanningLine),
Box::new(SecurityGrid),
Box::new(PrismDispersion),
Box::new(LaserLightShow),
Box::new(RangeFinder),
Box::new(MirrorBounce),
Box::new(FiberPulse),
Box::new(PlasmaBolt),
Box::new(ParticleAccelerator),
Box::new(DiscoFan),
]
}
struct ChargeAndFire;
impl ProgressStyle for ChargeAndFire {
fn name(&self) -> &str {
"charge-and-fire"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Core charges with eased; at full power a beam lances across the entire bar"
}
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 (cells_w, cells_h) = grid.dimensions();
let mid = h / 2;
let core_x = w / 6;
let fired = ctx.eased >= 0.85;
let charge = (ctx.eased / 0.85).clamp(0.0, 1.0);
if fired {
for y in mid.saturating_sub(1)..=(mid + 1).min(h.saturating_sub(1)) {
draw::hline(grid, 0, w.saturating_sub(1), y);
}
draw::hline(grid, 0, w.saturating_sub(1), mid);
let color = ctx.palette.sample(1.0);
for cy in 0..cells_h {
draw::tint_row(grid, cy, 0, cells_w.saturating_sub(1), color);
}
} else {
let rings = ((charge * 6.0) as usize).max(1).min(6);
for r in 0..rings {
let radius = r + 1;
for dr in 0..radius {
draw::dot_i(grid, core_x as i32 + dr as i32, mid as i32);
if core_x >= dr {
draw::dot_i(grid, core_x as i32 - dr as i32, mid as i32);
}
}
for dv in 0..radius {
draw::dot_i(grid, core_x as i32, mid as i32 + dv as i32);
draw::dot_i(grid, core_x as i32, mid as i32 - dv as i32);
}
}
let pulse_r = (charge * 4.0) as i32;
if pulse_r > 0 {
let steps = 32usize;
for s in 0..steps {
let angle = s as f32 / steps as f32 * 2.0 * PI + ctx.time * 4.0;
let px = core_x as i32 + (angle.cos() * pulse_r as f32 * 1.5) as i32;
let py = mid as i32 + (angle.sin() * pulse_r as f32 * 0.7) as i32;
draw::dot_i(grid, px, py);
}
}
let beam_reach = ((charge * w as f32) as usize).min(w.saturating_sub(1));
if beam_reach > core_x {
draw::hline(grid, core_x, beam_reach, mid);
}
let filled_cells = (charge * cells_w as f32) as usize;
for cx in 0..filled_cells.min(cells_w) {
let t = cx as f32 / cells_w.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t);
for cy in 0..cells_h {
draw::tint_row(grid, cy, cx, cx, color);
}
}
}
Ok(())
}
}
struct ScanningLine;
impl ProgressStyle for ScanningLine {
fn name(&self) -> &str {
"scanning-line"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Vertical beam sweeps back and forth; swept fraction fills to eased"
}
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 (cells_w, cells_h) = grid.dimensions();
let period = 3.0f32;
let t_norm = (ctx.time % period) / period; let sweep_frac = if t_norm < 0.5 {
t_norm * 2.0
} else {
2.0 - t_norm * 2.0
};
let sweep_x = (sweep_frac * w as f32) as usize;
let sweep_x = sweep_x.min(w.saturating_sub(1));
let filled = ((ctx.eased * w as f32) as usize).min(w);
let mid = h / 2;
draw::hline(
grid,
0,
filled.saturating_sub(1),
mid.saturating_sub(1).min(h - 1),
);
draw::hline(grid, 0, filled.saturating_sub(1), mid);
draw::hline(
grid,
0,
filled.saturating_sub(1),
(mid + 1).min(h.saturating_sub(1)),
);
draw::vline(grid, sweep_x, 0, h.saturating_sub(1));
if sweep_x > 0 {
draw::vline(grid, sweep_x - 1, h / 4, h * 3 / 4);
}
if sweep_x + 1 < w {
draw::vline(grid, sweep_x + 1, h / 4, h * 3 / 4);
}
let filled_cells = (ctx.eased * cells_w as f32) as usize;
for cx in 0..filled_cells.min(cells_w) {
let t = cx as f32 / cells_w.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t);
for cy in 0..cells_h {
draw::tint_row(grid, cy, cx, cx, color);
}
}
let beam_cell = (sweep_x / 2).min(cells_w.saturating_sub(1));
let bright = ctx.palette.sample(1.0);
for cy in 0..cells_h {
draw::tint_row(grid, cy, beam_cell, beam_cell, bright);
}
Ok(())
}
}
struct SecurityGrid;
impl ProgressStyle for SecurityGrid {
fn name(&self) -> &str {
"security-grid"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Criss-crossing security beams; tripped beams flicker dangerously via time"
}
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 (cells_w, cells_h) = grid.dimensions();
let h_beams = ((ctx.eased * 5.0 + 1.0) as usize).min(8).max(1);
let v_beams = ((ctx.eased * 3.0 + 1.0) as usize).min(6).max(1);
for i in 0..h_beams {
let y = if h_beams <= 1 {
h / 2
} else {
i * h.saturating_sub(1) / (h_beams - 1)
};
let trip_x = ((ctx.time * 0.4).fract() * w as f32) as usize;
let tripped = trip_x < w / 2; let flicker_on = (ctx.time * 12.0 + i as f32 * 1.7).sin() > 0.0;
if tripped && flicker_on {
if trip_x > 0 {
draw::hline(grid, 0, trip_x.saturating_sub(1), y);
}
if trip_x + 2 < w {
draw::hline(grid, trip_x + 2, w.saturating_sub(1), y);
}
} else {
draw::hline(grid, 0, w.saturating_sub(1), y);
}
}
for j in 0..v_beams {
let x = if v_beams <= 1 {
w / 2
} else {
j * w.saturating_sub(1) / (v_beams - 1)
};
let on = (ctx.time * 8.0 + j as f32 * 2.3).cos() > -0.3;
if on {
draw::vline(grid, x, 0, h.saturating_sub(1));
}
}
for cx in 0..cells_w {
let t = cx as f32 / cells_w.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t * ctx.eased);
for cy in 0..cells_h {
draw::tint_row(grid, cy, cx, cx, color);
}
}
Ok(())
}
}
struct PrismDispersion;
impl ProgressStyle for PrismDispersion {
fn name(&self) -> &str {
"prism-dispersion"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"One white beam enters a prism and fans into a spectrum of angled beams"
}
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 (cells_w, cells_h) = grid.dimensions();
let prism_x = (w / 4) as i32;
let mid = (h / 2) as i32;
beam_line(grid, 0, mid, prism_x, mid, w + h);
let tri_h = (h / 3).max(1) as i32;
beam_line(
grid,
prism_x,
mid - tri_h,
prism_x + tri_h,
mid + tri_h,
w + h,
);
beam_line(grid, prism_x, mid - tri_h, prism_x, mid + tri_h, w + h);
beam_line(
grid,
prism_x,
mid + tri_h,
prism_x + tri_h,
mid + tri_h,
w + h,
);
let n_beams = ((ctx.eased * 7.0 + 1.0) as usize).max(1).min(8);
let fan_origin_x = prism_x + tri_h;
let fan_origin_y = mid;
let spread = (ctx.eased * PI * 0.7).max(0.05);
for b in 0..n_beams {
let angle = if n_beams == 1 {
0.0f32
} else {
-spread / 2.0 + b as f32 / (n_beams - 1) as f32 * spread
};
let remain_w = (w as i32 - fan_origin_x).max(1);
let end_x = fan_origin_x + remain_w;
let end_y = fan_origin_y + (angle.tan() * remain_w as f32) as i32;
beam_line(grid, fan_origin_x, fan_origin_y, end_x, end_y, (w + h) * 2);
let t = b as f32 / n_beams.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t);
let cx0 = (fan_origin_x / 2).max(0) as usize;
let cx1 = (end_x / 2).clamp(0, cells_w as i32 - 1) as usize;
for cy in 0..cells_h {
draw::tint_row(
grid,
cy,
cx0.min(cx1),
cx0.max(cx1).min(cells_w.saturating_sub(1)),
color,
);
}
}
Ok(())
}
}
struct LaserLightShow;
impl ProgressStyle for LaserLightShow {
fn name(&self) -> &str {
"laser-light-show"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Multiple Lissajous sweep beams crossing at different frequencies; count = eased"
}
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 (cells_w, cells_h) = grid.dimensions();
let n_active = ((ctx.eased * 6.0 + 1.0) as usize).min(7);
let freq_pairs: [(f32, f32); 7] = [
(1.0, 2.0),
(2.0, 3.0),
(3.0, 4.0),
(3.0, 5.0),
(5.0, 6.0),
(4.0, 7.0),
(7.0, 8.0),
];
let amp = (h as f32 / 2.0 - 1.0).max(0.5);
let cy_mid = h as f32 / 2.0;
for b in 0..n_active {
let (fx, fy) = freq_pairs[b % freq_pairs.len()];
let phase = hashf(b as u32 * 17) * 2.0 * PI + ctx.time * (0.5 + b as f32 * 0.15);
let steps = w * 2;
for s in 0..=steps {
let frac = s as f32 / steps as f32;
let px = (frac * w as f32) as i32;
let theta_x = frac * fx * PI * 2.0;
let theta_y = frac * fy * PI * 2.0 + phase;
let py = (cy_mid + amp * theta_y.sin() * (theta_x.cos() * 0.3 + 0.7)) as i32;
draw::dot_i(grid, px, py);
}
let t = b as f32 / n_active.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t);
for cy in 0..cells_h {
draw::tint_row(grid, cy, 0, cells_w.saturating_sub(1), color);
}
}
Ok(())
}
}
struct RangeFinder;
impl ProgressStyle for RangeFinder {
fn name(&self) -> &str {
"range-finder"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Rotating sweep beam radiates from center; target lock reticle at eased radius"
}
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 (cells_w, cells_h) = grid.dimensions();
let cx = (w / 2) as i32;
let cy = (h / 2) as i32;
let max_r = (w.min(h * 2) / 2) as f32;
for ring in 1..=3usize {
let ring_r = (ring as f32 / 3.0 * max_r) as i32;
if ring_r == 0 {
continue;
}
let steps = (ring_r * 8).max(16) as usize;
for s in 0..steps {
let angle = s as f32 / steps as f32 * 2.0 * PI;
let px = cx + (angle.cos() * ring_r as f32) as i32;
let py = cy + (angle.sin() * ring_r as f32 * 0.5) as i32;
if s % 3 == 0 {
draw::dot_i(grid, px, py);
}
}
}
let sweep_angle = ctx.time * 1.8; let sweep_dx = sweep_angle.cos();
let sweep_dy = sweep_angle.sin() * 0.5;
let beam_end_x = cx + (sweep_dx * max_r) as i32;
let beam_end_y = cy + (sweep_dy * max_r) as i32;
beam_line(grid, cx, cy, beam_end_x, beam_end_y, w + h);
for ghost in 1..=4usize {
let ga = sweep_angle - ghost as f32 * 0.12;
let gx = cx + (ga.cos() * max_r) as i32;
let gy = cy + (ga.sin() * 0.5 * max_r) as i32;
let dx = (gx - cx).abs();
let dy = (gy - cy).abs();
let steps = dx.max(dy).max(1) as usize;
for s in (0..steps).step_by(2) {
let t = s as f32 / steps as f32;
let px = cx + ((gx - cx) as f32 * t) as i32;
let py = cy + ((gy - cy) as f32 * t) as i32;
draw::dot_i(grid, px, py);
}
}
let lock_r = ctx.eased * max_r;
let lock_x = cx + (sweep_angle.cos() * lock_r) as i32;
let lock_y = cy + (sweep_angle.sin() * 0.5 * lock_r) as i32;
for d in 0..3i32 {
draw::dot_i(grid, lock_x + d, lock_y);
draw::dot_i(grid, lock_x - d, lock_y);
draw::dot_i(grid, lock_x, lock_y + d);
draw::dot_i(grid, lock_x, lock_y - d);
}
let color = ctx.palette.sample(0.8);
let dim = ctx.palette.sample(0.2);
for cx_c in 0..cells_w {
for cy_c in 0..cells_h {
draw::tint_row(grid, cy_c, cx_c, cx_c, dim);
}
}
let beam_cell = (beam_end_x / 2).clamp(0, cells_w as i32 - 1) as usize;
let center_cell = (cx / 2).clamp(0, cells_w as i32 - 1) as usize;
for cy_c in 0..cells_h {
let lo = center_cell.min(beam_cell);
let hi = center_cell.max(beam_cell);
draw::tint_row(grid, cy_c, lo, hi, color);
}
Ok(())
}
}
struct MirrorBounce;
impl ProgressStyle for MirrorBounce {
fn name(&self) -> &str {
"mirror-bounce"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Beam enters left and reflects off top/bottom walls; total path length = eased"
}
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 (cells_w, cells_h) = grid.dimensions();
let angle_base = PI / 4.0 + (ctx.time * 0.2).sin() * PI / 8.0;
let mut bx = 0i32;
let mut by = (h / 2) as i32;
let dx = angle_base.cos();
let mut dy = angle_base.sin();
let total_dots = ((ctx.eased * (w * 6) as f32) as usize).max(1);
let step = 1.0f32;
let mut prev_x = bx;
let mut prev_y = by;
for _ in 0..total_dots {
bx = (bx as f32 + dx * step) as i32;
by = (by as f32 + dy * step) as i32;
if by < 0 {
by = -by;
dy = -dy;
} else if by >= h as i32 {
by = 2 * h as i32 - by - 2;
dy = -dy;
}
if bx < 0 {
bx = 0;
}
if bx >= w as i32 {
bx = w as i32 - 1;
}
draw::dot_i(grid, bx, by);
if (dy > 0.0 && by <= 1) || (dy < 0.0 && by >= h as i32 - 2) {
draw::dot_i(grid, bx - 1, by);
draw::dot_i(grid, bx + 1, by);
}
prev_x = bx;
prev_y = by;
}
let _ = (prev_x, prev_y);
let filled_cells = (ctx.eased * cells_w as f32) as usize;
for cx in 0..filled_cells.min(cells_w) {
let t = cx as f32 / cells_w.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t);
for cy in 0..cells_h {
draw::tint_row(grid, cy, cx, cx, color);
}
}
Ok(())
}
}
struct FiberPulse;
impl ProgressStyle for FiberPulse {
fn name(&self) -> &str {
"fiber-pulse"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Pulses race down curved fiber-optic lines; pulse speed and fiber count = eased"
}
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 (cells_w, cells_h) = grid.dimensions();
let n_fibers = ((ctx.eased * 6.0 + 1.0) as usize).max(1).min(7);
let pulse_speed = 0.3 + ctx.eased * 2.5;
for f in 0..n_fibers {
let fiber_amp = (h as f32 / (n_fibers as f32 * 1.2 + 1.0)).max(0.5);
let freq = 1.5 + f as f32 * 0.4;
let phase_off = f as f32 * PI * 0.6;
let v_off = if n_fibers == 1 {
h as f32 / 2.0
} else {
(f as f32 + 0.5) * h as f32 / n_fibers as f32
};
for px in 0..w {
let x_frac = px as f32 / w.saturating_sub(1).max(1) as f32;
let py = v_off + fiber_amp * (x_frac * freq * 2.0 * PI + phase_off).sin();
draw::dot_i(grid, px as i32, py as i32);
}
let pulse_phase = (ctx.time * pulse_speed + f as f32 * 0.37).fract();
let pulse_x = (pulse_phase * w as f32) as usize;
let pulse_half = 4usize;
for dp in 0..=pulse_half * 2 {
let ppx = pulse_x.saturating_sub(pulse_half) + dp;
if ppx >= w {
break;
}
let x_frac = ppx as f32 / w.saturating_sub(1).max(1) as f32;
let py = v_off + fiber_amp * (x_frac * freq * 2.0 * PI + phase_off).sin();
let dist = (dp as i32 - pulse_half as i32).abs();
if dist <= 2 {
draw::dot_i(grid, ppx as i32, py as i32);
draw::dot_i(grid, ppx as i32, py as i32 - 1);
draw::dot_i(grid, ppx as i32, py as i32 + 1);
} else {
draw::dot_i(grid, ppx as i32, py as i32);
}
}
let t = f as f32 / n_fibers.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t);
for cy in 0..cells_h {
draw::tint_row(grid, cy, 0, cells_w.saturating_sub(1), color);
}
}
Ok(())
}
}
struct PlasmaBolt;
impl ProgressStyle for PlasmaBolt {
fn name(&self) -> &str {
"plasma-bolt"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Jagged plasma lightning beam jitters frame-by-frame via time; length = eased"
}
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 (cells_w, cells_h) = grid.dimensions();
let bolt_len = ((ctx.eased * w as f32) as usize).max(1).min(w);
let mid = h / 2;
let jitter_epoch = (ctx.time * 20.0) as u32;
let mut prev_y = mid as i32;
let max_jitter = ((h / 2) as i32).max(1);
for px in 0..bolt_len {
let jitter_raw = hashf(px as u32 * 7 + jitter_epoch * 31);
let jitter = ((jitter_raw * 2.0 - 1.0) * max_jitter as f32) as i32;
let mut cy = mid as i32 + jitter;
cy = cy.clamp(0, h as i32 - 1);
let y_lo = prev_y.min(cy).clamp(0, h as i32 - 1) as usize;
let y_hi = prev_y.max(cy).clamp(0, h as i32 - 1) as usize;
draw::vline(grid, px, y_lo, y_hi);
prev_y = cy;
}
let jitter_epoch2 = jitter_epoch.wrapping_add(7);
let mut prev_y2 = mid as i32;
for px in 0..bolt_len {
let jitter_raw = hashf(px as u32 * 13 + jitter_epoch2 * 41);
let jitter = ((jitter_raw * 2.0 - 1.0) * (max_jitter / 2) as f32) as i32;
let mut cy = mid as i32 + jitter;
cy = cy.clamp(0, h as i32 - 1);
draw::dot_i(grid, px as i32, cy);
prev_y2 = cy;
}
let _ = prev_y2;
let filled_cells = (ctx.eased * cells_w as f32) as usize;
for cx in 0..filled_cells.min(cells_w) {
let t = cx as f32 / cells_w.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t);
for cy in 0..cells_h {
draw::tint_row(grid, cy, cx, cx, color);
}
}
Ok(())
}
}
struct ParticleAccelerator;
impl ProgressStyle for ParticleAccelerator {
fn name(&self) -> &str {
"particle-accelerator"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Charged particles accelerate along a beam track; speed and density = eased"
}
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 (cells_w, cells_h) = grid.dimensions();
let mid = h / 2;
let rail_above = mid.saturating_sub(1);
let rail_below = (mid + 1).min(h.saturating_sub(1));
draw::hline(grid, 0, w.saturating_sub(1), rail_above);
draw::hline(grid, 0, w.saturating_sub(1), rail_below);
let tie_spacing = 5usize;
let mut tx = 0;
while tx < w {
draw::vline(grid, tx, rail_above, rail_below);
tx += tie_spacing;
}
let speed = 0.4 + ctx.eased * 4.0;
let n_particles = ((ctx.eased * 8.0 + 1.0) as usize).max(1).min(10);
let particle_gap = w / n_particles.max(1);
for p in 0..n_particles {
let phase_off = p as f32 / n_particles as f32;
let pos = ((ctx.time * speed + phase_off).fract() * w as f32) as usize;
let pos = pos.min(w.saturating_sub(1));
draw::dot_i(grid, pos as i32, mid as i32);
draw::dot_i(grid, pos as i32 + 1, mid as i32);
if pos > 0 {
draw::dot_i(grid, pos as i32 - 1, mid as i32);
}
let wake_len = (particle_gap / 2).max(2).min(w);
for w_step in 1..wake_len {
if w_step * 3 < wake_len * 2 {
let wx = if pos >= w_step { pos - w_step } else { 0 };
draw::dot_i(grid, wx as i32, mid as i32);
}
}
}
for cx in 0..cells_w {
let t = cx as f32 / cells_w.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t * ctx.eased + (1.0 - ctx.eased) * 0.1);
for cy in 0..cells_h {
draw::tint_row(grid, cy, cx, cx, color);
}
}
Ok(())
}
}
struct DiscoFan;
impl ProgressStyle for DiscoFan {
fn name(&self) -> &str {
"disco-fan"
}
fn theme(&self) -> &str {
"lasers"
}
fn describe(&self) -> &str {
"Radial fan of beams sweeps from a corner; beam count and arc = eased"
}
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 (cells_w, cells_h) = grid.dimensions();
let ox = 0i32;
let oy = h as i32 - 1;
let n_beams = ((ctx.eased * 9.0 + 1.0) as usize).max(1).min(10);
let arc = ctx.eased * PI * 0.9 + 0.1; let sweep_center = PI / 2.0 * (0.4 + 0.6 * ctx.eased) - (ctx.time * 0.6).sin() * arc * 0.3;
for b in 0..n_beams {
let angle_frac = if n_beams == 1 {
0.5f32
} else {
b as f32 / (n_beams - 1) as f32
};
let angle = sweep_center - arc / 2.0 + angle_frac * arc;
let beam_len = ((w as f32).hypot(h as f32) as i32 + 2).max(2);
let end_x = ox + (angle.cos() * beam_len as f32) as i32;
let end_y = oy - (angle.sin() * beam_len as f32) as i32;
beam_line(grid, ox, oy, end_x, end_y, (w + h) * 2);
let t = b as f32 / n_beams.saturating_sub(1).max(1) as f32;
let color = ctx.palette.sample(t);
let x_end = end_x.clamp(0, cells_w as i32 - 1) as usize;
let x_start = 0usize;
let (lo, hi) = if x_start <= x_end {
(x_start, x_end)
} else {
(x_end, x_start)
};
for cy in 0..cells_h {
draw::tint_row(grid, cy, lo, hi.min(cells_w.saturating_sub(1)), color);
}
}
for dy in -1i32..=1 {
for dx in -1i32..=1 {
draw::dot_i(grid, ox + dx, oy + dy);
}
}
Ok(())
}
}