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(RcCharge),
Box::new(Oscilloscope),
Box::new(LogicGate),
Box::new(SevenSegment),
Box::new(LedVuMeter),
Box::new(BinaryBus),
Box::new(SquareClock),
Box::new(PwmDuty),
Box::new(ResistorBands),
Box::new(SignalNoise),
Box::new(LissajousScope),
]
}
struct RcCharge;
impl ProgressStyle for RcCharge {
fn name(&self) -> &str {
"rc-charge"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"RC capacitor charging: exponential V=V₀(1−e^(−t/RC)) curve draws out, capacitor fills"
}
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 cap_w = 4usize;
let plot_w = w.saturating_sub(cap_w + 1);
let mid_y = (h - 1) as i32; let top_y = 0i32;
let mut prev_y: Option<i32> = None;
for xi in 0..plot_w {
let xn = if plot_w <= 1 {
ctx.eased
} else {
xi as f32 / (plot_w - 1) as f32
};
let effective = xn.min(ctx.eased);
let tau = 0.2_f32;
let v = 1.0 - (-effective / tau).exp();
let dot_y = (mid_y - (v * h as f32 * 0.9) as i32).clamp(top_y, mid_y);
draw::dot_i(grid, xi as i32, dot_y);
if let Some(py) = prev_y {
let lo = py.min(dot_y);
let hi = py.max(dot_y);
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
prev_y = Some(dot_y);
}
draw::hline(grid, 0, plot_w.saturating_sub(1), h - 1);
if cap_w + 1 <= w {
let plate_x1 = w.saturating_sub(cap_w);
let plate_x2 = w.saturating_sub(cap_w - 2);
let plate_top = h / 4;
let plate_bot = h.saturating_sub(h / 4).max(plate_top);
draw::vline(grid, plate_x1, plate_top, plate_bot);
draw::vline(grid, plate_x2, plate_top, plate_bot);
let wire_y = h / 2;
draw::hline(grid, plot_w, plate_x1, wire_y);
draw::hline(grid, plate_x2, w - 1, wire_y);
let fill_h = (ctx.eased * (plate_bot - plate_top + 1) as f32).round() as usize;
let fill_y0 = plate_bot.saturating_sub(fill_h.saturating_sub(1));
if fill_h > 0 {
for fy in fill_y0..=plate_bot {
draw::dot(grid, plate_x1 + 1, fy);
}
}
}
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 = cx as f32 / cw.max(1) as f32;
let col = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct Oscilloscope;
impl ProgressStyle for Oscilloscope {
fn name(&self) -> &str {
"oscilloscope"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"CRT oscilloscope with graticule grid and waveform sweeping sine→triangle→square"
}
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 h_divs = 4usize.max(1);
let v_divs = 8usize.max(1);
for di in 0..=h_divs {
let y = di * h / h_divs.max(1);
let y = y.min(h - 1);
for xi in (0..w).step_by(4) {
draw::dot(grid, xi, y);
if xi + 1 < w {
draw::dot(grid, xi + 1, y);
}
}
}
for di in 0..=v_divs {
let x = di * w / v_divs.max(1);
let x = x.min(w - 1);
for yi in (0..h).step_by(4) {
draw::dot(grid, x, yi);
if yi + 1 < h {
draw::dot(grid, x, yi + 1);
}
}
}
let shape = (ctx.eased * 3.0).floor() as usize; let freq = 3.0_f32; let phase = ctx.time * 2.0 * PI * 0.5;
let amp = (h as f32 * 0.42).max(1.0);
let mid = (h / 2) as i32;
let mut prev_y: Option<i32> = None;
for xi in 0..w {
let theta = (xi as f32 / w as f32) * freq * 2.0 * PI + phase;
let val: f32 = match shape {
0 => theta.sin(),
1 => {
(2.0 / PI) * theta.sin().asin()
}
_ => {
if theta.sin() >= 0.0 {
1.0
} else {
-1.0
}
}
};
let dy = (mid - (val * amp) as i32).clamp(0, h as i32 - 1);
draw::dot_i(grid, xi as i32, dy);
if let Some(py) = prev_y {
let lo = py.min(dy);
let hi = py.max(dy);
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
prev_y = Some(dy);
}
let (cw, ch) = grid.dimensions();
for cx in 0..cw {
let t = cx as f32 / cw.max(1) as f32;
let col = ctx.palette.sample(t * ctx.eased);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct LogicGate;
impl ProgressStyle for LogicGate {
fn name(&self) -> &str {
"logic-gate"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"Signal pulse cascades through AND→OR→XOR→NOT gates; active gate flickers"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, _h) = draw::dot_dims(grid);
let (cw, ch) = grid.dimensions();
if cw == 0 || ch == 0 {
return Ok(());
}
let gates = ["&", "|", "^", "!"];
let n_gates = gates.len();
let spacing = (cw / (n_gates + 1)).max(1);
let wire_row = ch / 2;
let wire_dot_y = wire_row * 4 + 1; let wire_dot_y = wire_dot_y.min(w.saturating_sub(1)); draw::hline(
grid,
0,
w.saturating_sub(1),
wire_dot_y.min({
let (_, dh) = draw::dot_dims(grid);
dh.saturating_sub(1)
}),
);
let lit_gates = (ctx.eased * n_gates as f32).floor() as usize;
let flicker_on = (ctx.time * 8.0) as usize % 2 == 0;
for (gi, &label) in gates.iter().enumerate() {
let gate_cell_x = (gi + 1) * spacing;
if gate_cell_x >= cw {
break;
}
let ch_sym = label.chars().next().unwrap_or('?');
draw::glyph(grid, gate_cell_x, wire_row, ch_sym);
if gi < lit_gates {
let wire_start = if gi == 0 { 0 } else { gi * spacing };
let wire_end = gate_cell_x.saturating_sub(1);
for cx in wire_start..=wire_end.min(cw.saturating_sub(1)) {
let col = ctx.palette.sample(gi as f32 / n_gates as f32);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
let col = ctx.palette.sample((gi + 1) as f32 / n_gates as f32);
for cy in 0..ch {
draw::tint_row(grid, cy, gate_cell_x, gate_cell_x, col);
}
} else if gi == lit_gates && flicker_on {
let col = ctx.palette.sample(0.8);
for cy in 0..ch {
draw::tint_row(grid, cy, gate_cell_x, gate_cell_x, col);
}
}
}
if lit_gates >= n_gates {
let last_gate_x = n_gates * spacing;
let trail_start = last_gate_x + 1;
for cx in trail_start..cw {
let col = ctx.palette.sample(1.0);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
}
Ok(())
}
}
const SEG7: [[bool; 7]; 10] = [
[true, true, true, false, true, true, true], [false, false, true, false, false, true, false], [true, false, true, true, true, false, true], [true, false, true, true, false, true, true], [false, true, true, true, false, true, false], [true, true, false, true, false, true, true], [true, true, false, true, true, true, true], [true, false, true, false, false, true, false], [true, true, true, true, true, true, true], [true, true, true, true, false, true, true], ];
fn draw_seg7_digit(
grid: &mut BrailleGrid,
digit: usize,
ox: usize,
oy: usize,
sw: usize,
sh: usize,
) {
let d = digit.min(9);
let segs = SEG7[d];
let mid_y = oy + sh / 2;
let bot_y = oy + sh.saturating_sub(1);
let right_x = ox + sw.saturating_sub(1);
if segs[0] {
draw::hline(grid, ox, right_x, oy);
}
if segs[1] {
draw::vline(grid, ox, oy, mid_y);
}
if segs[2] {
draw::vline(grid, right_x, oy, mid_y);
}
if segs[3] {
draw::hline(grid, ox, right_x, mid_y);
}
if segs[4] {
draw::vline(grid, ox, mid_y, bot_y);
}
if segs[5] {
draw::vline(grid, right_x, mid_y, bot_y);
}
if segs[6] {
draw::hline(grid, ox, right_x, bot_y);
}
}
struct SevenSegment;
impl ProgressStyle for SevenSegment {
fn name(&self) -> &str {
"seven-segment"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"7-segment numeric counter: digits drawn with segment lines, counting up with 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 digit_w = (w / 4).max(3).min(12);
let gap = 2usize;
let n_digits = ((w + gap) / (digit_w + gap)).max(1).min(4);
let max_val = 10usize.pow(n_digits as u32).saturating_sub(1);
let count = (ctx.eased * max_val as f32).round() as usize;
let total_w = n_digits * digit_w + (n_digits - 1) * gap;
let ox = w.saturating_sub(total_w) / 2;
for di in 0..n_digits {
let place = 10usize.pow((n_digits - 1 - di) as u32);
let digit = (count / place) % 10;
let dx = ox + di * (digit_w + gap);
if dx + digit_w <= w {
draw_seg7_digit(grid, digit, dx, 0, digit_w, h);
}
}
let (cw, ch) = grid.dimensions();
for cx in 0..cw {
let t = cx as f32 / cw.max(1) as f32;
let col = ctx.palette.sample(t * ctx.eased + ctx.eased * 0.5);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct LedVuMeter;
impl ProgressStyle for LedVuMeter {
fn name(&self) -> &str {
"led-vu-meter"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"LED VU bar graph: columns pulse to synthetic audio levels; peak-hold dot per column"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (cw, ch) = grid.dimensions();
if cw == 0 || ch == 0 {
return Ok(());
}
let n_cols = cw;
let lit_cols = (ctx.eased * n_cols as f32).round() as usize;
for col in 0..n_cols {
let kf = (col + 1) as f32;
let freq1 = 0.7 + kf * 0.3;
let freq2 = 1.3 + kf * 0.17;
let raw = ((kf * ctx.time * freq1).sin().abs()
+ (kf * 0.5 * ctx.time * freq2 + 1.0).sin().abs())
* 0.5;
let level = if col < lit_cols { raw } else { raw * 0.12 };
let total_eighths = (level * ch as f32 * 8.0).round() as usize;
let full_cells = total_eighths / 8;
let rem = total_eighths % 8;
for row in 0..full_cells.min(ch) {
let cell_y = ch.saturating_sub(1).saturating_sub(row);
draw::vblock(grid, col, cell_y, 8);
}
if rem > 0 && full_cells < ch {
let cell_y = ch.saturating_sub(1).saturating_sub(full_cells);
draw::vblock(grid, col, cell_y, rem);
}
let peak_eighths = total_eighths.saturating_add(4).min(ch * 8);
let peak_row = ch.saturating_sub(1).saturating_sub(peak_eighths / 8);
if peak_row < ch && full_cells > 0 {
draw::vblock(grid, col, peak_row, 1);
}
if col < lit_cols {
let t = col as f32 / n_cols.max(1) as f32;
let col_color = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, col, col, col_color);
}
}
}
Ok(())
}
}
struct BinaryBus;
impl ProgressStyle for BinaryBus {
fn name(&self) -> &str {
"binary-bus"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"8-bit data bus: parallel bit lines shift bits left→right; word density = 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 n_lines = 8usize.min(h);
let ones_per_word = (ctx.eased * n_lines as f32).round() as usize;
let bit_w = (w / 16).max(2);
let n_cells = w / bit_w;
let scroll = (ctx.time * 4.0) as usize;
for line in 0..n_lines {
let y = if n_lines <= 1 {
h / 2
} else {
line * (h - 1) / (n_lines - 1)
};
let y = y.min(h - 1);
for ci in 0..n_cells {
let slot = ci.wrapping_add(scroll);
let hash = slot
.wrapping_mul(2654435761)
.wrapping_add(line.wrapping_mul(40503));
let bit_pos = line % 8;
let raw_bit = (hash >> bit_pos) & 1;
let bit = raw_bit == 1 && line < ones_per_word;
let x0 = ci * bit_w;
let x1 = (x0 + bit_w).saturating_sub(2).max(x0);
if bit {
draw::hline(grid, x0, x1, y.saturating_sub(1).max(0));
draw::hline(grid, x0, x1, y);
} else {
draw::hline(grid, x0, x1, (y + 1).min(h - 1));
}
if ci + 1 < n_cells {
let next_slot = ci + 1 + scroll;
let next_hash = next_slot
.wrapping_mul(2654435761)
.wrapping_add(line.wrapping_mul(40503));
let next_raw = (next_hash >> (line % 8)) & 1;
let next_bit = next_raw == 1 && line < ones_per_word;
if bit != next_bit && x1 + 1 < w {
let y_lo = y.saturating_sub(1);
let y_hi = (y + 1).min(h - 1);
draw::vline(grid, x1 + 1, y_lo, y_hi);
}
}
}
}
let (cw, ch) = grid.dimensions();
for cx in 0..cw {
let t = cx as f32 / cw.max(1) as f32;
let col = ctx.palette.sample(t * ctx.eased);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct SquareClock;
impl ProgressStyle for SquareClock {
fn name(&self) -> &str {
"square-clock"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"Digital clock signal scrolling left; rising/falling edges sharp; progress = cycles 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 min_period = 8usize;
let max_cycles = (w / min_period).max(1);
let cycles = (ctx.eased * max_cycles as f32).ceil() as usize + 1;
let period = (w / cycles.max(1)).max(4);
let scroll_dots = (ctx.time * 6.0) as usize % (period * 2).max(1);
let hi_y = h / 4; let lo_y = h.saturating_sub(h / 4 + 1);
let lit_x = (ctx.eased * w as f32).round() as usize;
let mut prev_level: Option<bool> = None;
for xi in 0..w {
let phase = (xi + scroll_dots) % (period * 2).max(1);
let high = phase < period;
let y = if high { hi_y } else { lo_y };
let y = y.min(h - 1);
draw::dot(grid, xi, y);
if let Some(prev) = prev_level {
if prev != high {
draw::vline(grid, xi, hi_y, lo_y);
}
}
prev_level = Some(high);
if xi >= lit_x && xi < lit_x + 2 {
draw::vline(grid, xi, 0, h.saturating_sub(1));
}
}
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 col = ctx.palette.sample(cx as f32 / cw.max(1) as f32);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct PwmDuty;
impl ProgressStyle for PwmDuty {
fn name(&self) -> &str {
"pwm-duty"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"PWM signal: fixed-frequency pulses whose on-time duty cycle widens with 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 n_pulses = 8usize;
let period = (w / n_pulses).max(2);
let on_time = (ctx.eased * period as f32).round() as usize;
let hi_y = h / 5;
let lo_y = h.saturating_sub(h / 5 + 1).min(h - 1);
let scroll = (ctx.time * 3.0) as usize % period.max(1);
for xi in 0..w {
let phase = (xi + scroll) % period.max(1);
let is_high = phase < on_time;
let y = if is_high { hi_y } else { lo_y };
draw::dot(grid, xi, y.min(h - 1));
if xi > 0 {
let prev_phase = (xi + scroll - 1) % period.max(1);
let prev_high = prev_phase < on_time;
if is_high != prev_high {
draw::vline(grid, xi, hi_y, lo_y);
}
}
}
let (cw, ch) = grid.dimensions();
for col in 0..cw {
let xi_mid = col * 2 + 1;
let phase = (xi_mid + scroll) % (period * 2 / 1).max(2); let cell_period = period / 2; let cell_on = (on_time / 2).max(if on_time > 0 { 1 } else { 0 });
let cell_phase = (col + scroll / 2) % cell_period.max(1);
if cell_phase < cell_on && cell_period > 0 {
let density = 2usize + (ctx.eased * 2.0) as usize;
for cy in 0..ch {
draw::shade(grid, col, cy, density.min(4));
}
let col_color = ctx.palette.sample(col as f32 / cw.max(1) as f32);
for cy in 0..ch {
draw::tint_row(grid, cy, col, col, col_color);
}
}
let _ = phase; }
Ok(())
}
}
const BAND_T: [f32; 10] = [
0.0, 0.11, 0.22, 0.33, 0.44, 0.55, 0.66, 0.77, 0.88, 1.0, ];
struct ResistorBands;
impl ProgressStyle for ResistorBands {
fn name(&self) -> &str {
"resistor-bands"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"Resistor with IEC colour bands revealing left-to-right as progress increases"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (w, h) = draw::dot_dims(grid);
let (cw, ch) = grid.dimensions();
if w == 0 || h == 0 || cw == 0 || ch == 0 {
return Ok(());
}
let body_top = h / 5;
let body_bot = h.saturating_sub(h / 5).max(body_top);
let lead_y = (body_top + body_bot) / 2;
let body_x0 = w / 6;
let body_x1 = w.saturating_sub(w / 6).max(body_x0 + 1);
draw::hline(grid, 0, body_x0, lead_y);
draw::hline(grid, body_x1, w.saturating_sub(1), lead_y);
draw::rect_outline(
grid,
body_x0,
body_top,
body_x1 - body_x0,
body_bot - body_top + 1,
);
for y in body_top + 1..body_bot {
for x in body_x0 + 1..body_x1 {
if (x + y) % 3 == 0 {
draw::dot(grid, x, y);
}
}
}
let n_bands = 4usize;
let body_len = body_x1.saturating_sub(body_x0 + 2); let band_spacing = body_len / (n_bands + 1);
let band_w = (band_spacing / 2).max(1);
let lit_bands = (ctx.eased * n_bands as f32).ceil() as usize;
for bi in 0..n_bands {
if bi >= lit_bands {
break;
}
let cx_dot = body_x0 + 1 + (bi + 1) * band_spacing;
let bx0 = cx_dot.saturating_sub(band_w / 2);
let bx1 = (bx0 + band_w).min(body_x1.saturating_sub(1));
let digits = [4usize, 7, 2, 5];
let digit = digits[bi % digits.len()];
let band_t = BAND_T[digit];
for bx in bx0..=bx1 {
draw::vline(
grid,
bx,
body_top + 1,
body_bot.saturating_sub(1).max(body_top + 1),
);
}
let band_cell_x0 = bx0 / 2;
let band_cell_x1 = (bx1 / 2 + 1).min(cw.saturating_sub(1));
let col = ctx.palette.sample(band_t);
for cy in 0..ch {
draw::tint_row(grid, cy, band_cell_x0, band_cell_x1, col);
}
}
Ok(())
}
}
#[inline]
fn pseudo_noise(seed: u32) -> f32 {
let h = seed
.wrapping_mul(2246822519)
.wrapping_add(seed.wrapping_mul(3266489917));
let h = h ^ (h >> 13);
let h = h.wrapping_mul(1274126177);
let h = h ^ (h >> 16);
(h as f32 / u32::MAX as f32) * 2.0 - 1.0
}
struct SignalNoise;
impl ProgressStyle for SignalNoise {
fn name(&self) -> &str {
"signal-noise"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"Sine signal rising from noise: SNR improves with progress until the clean wave emerges"
}
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 snr = ctx.eased; let noise_amp = 1.0 - snr * 0.95; let sig_amp = snr;
let freq = 3.0_f32;
let phase = ctx.time * PI * 0.6;
let time_bucket = (ctx.time * 4.0) as u32;
let mid = (h / 2) as i32;
let half_h = (h as f32 * 0.45).max(1.0);
let mut prev_y: Option<i32> = None;
for xi in 0..w {
let xn = xi as f32 / w as f32;
let sine_val = (xn * freq * 2.0 * PI + phase).sin();
let noise_val = pseudo_noise(xi as u32 ^ time_bucket.wrapping_mul(1013904223));
let val = sig_amp * sine_val + noise_amp * noise_val;
let dy = (mid - (val * half_h) as i32).clamp(0, h as i32 - 1);
draw::dot_i(grid, xi as i32, dy);
if let Some(py) = prev_y {
let lo = py.min(dy);
let hi = py.max(dy);
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
prev_y = Some(dy);
}
draw::hline(grid, 0, w.saturating_sub(1), (h / 2).min(h - 1));
let (cw, ch) = grid.dimensions();
for cx in 0..cw {
let t = cx as f32 / cw.max(1) as f32;
let col = ctx.palette.sample(t * 0.5 + snr * 0.5);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
const LISSAJOUS_RATIOS: [(f32, f32); 6] = [
(1.0, 1.0), (2.0, 1.0), (1.0, 2.0), (3.0, 2.0), (3.0, 4.0), (5.0, 4.0), ];
struct LissajousScope;
impl ProgressStyle for LissajousScope {
fn name(&self) -> &str {
"lissajous-scope"
}
fn theme(&self) -> &str {
"electronics"
}
fn describe(&self) -> &str {
"Lissajous figure on a CRT scope: ratio unlocks with progress, phase drifts with 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 n_ratios = LISSAJOUS_RATIOS.len();
let idx = ((ctx.eased * n_ratios as f32) as usize).min(n_ratios - 1);
let (a, b) = LISSAJOUS_RATIOS[idx];
let delta = ctx.time * 0.4;
let cx_f = (w as f32 - 1.0) / 2.0;
let cy_f = (h as f32 - 1.0) / 2.0;
let rx = cx_f * 0.97;
let ry = cy_f * 0.97;
let bezel_pts = (w + h) * 2;
for i in 0..bezel_pts {
let angle = (i as f32 / bezel_pts as f32) * 2.0 * PI;
let px = (cx_f + rx * angle.cos()) as i32;
let py = (cy_f + ry * angle.sin()) as i32;
draw::dot_i(grid, px, py);
}
let mid_x = w / 2;
let mid_y = h / 2;
for x in (0..w).step_by(4) {
draw::dot(grid, x.min(w - 1), mid_y);
if x + 1 < w {
draw::dot(grid, x + 1, mid_y);
}
}
for y in (0..h).step_by(4) {
draw::dot(grid, mid_x, y.min(h - 1));
if y + 1 < h {
draw::dot(grid, mid_x, y + 1);
}
}
let plot_rx = rx * 0.88;
let plot_ry = ry * 0.88;
let steps = (w * h).max(512);
let period = 2.0 * PI;
let mut prev: Option<(i32, i32)> = None;
for si in 0..steps {
let tau = (si as f32 / steps as f32) * period;
let lx = plot_rx * (a * tau + delta).sin();
let ly = plot_ry * (b * tau).sin();
let px = (cx_f + lx) as i32;
let py = (cy_f + ly) as i32;
draw::dot_i(grid, px, py);
if let Some((ox, oy)) = prev {
let gap = (((px - ox).abs() + (py - oy).abs()) as usize).max(1);
for s in 1..gap {
let f = s as f32 / gap as f32;
let ix = (ox as f32 + (px - ox) as f32 * f) as i32;
let iy = (oy as f32 + (py - oy) as f32 * f) as i32;
draw::dot_i(grid, ix, iy);
}
}
prev = Some((px, py));
}
let (cw, ch) = grid.dimensions();
for cell_x in 0..cw {
let t = cell_x as f32 / cw.max(1) as f32;
let col = ctx.palette.sample(t * ctx.eased + (1.0 - ctx.eased) * 0.3);
for cy in 0..ch {
draw::tint_row(grid, cy, cell_x, cell_x, col);
}
}
Ok(())
}
}