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(DnaHelix),
Box::new(Mitosis),
Box::new(ActionPotential),
Box::new(LogisticGrowth),
Box::new(EcgHeartbeat),
Box::new(ProteinFolding),
Box::new(Photosynthesis),
Box::new(PhylogeneticTree),
Box::new(BloodFlow),
Box::new(EnzymeKinetics),
Box::new(VirusSpread),
Box::new(IonChannels),
]
}
fn line_i(grid: &mut BrailleGrid, x0: i32, y0: i32, x1: i32, y1: i32) {
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let steps = dx.max(dy).max(1);
for i in 0..=steps {
let x = x0 + (x1 - x0) * i / steps;
let y = y0 + (y1 - y0) * i / steps;
draw::dot_i(grid, x, y);
}
}
struct DnaHelix;
impl ProgressStyle for DnaHelix {
fn name(&self) -> &str {
"dna-helix"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"DNA double helix: two counter-rotating sine strands with base-pair rungs; unzips 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 mid = (h / 2) as f32;
let amp = (h as f32 * 0.42).max(1.0);
let phase_offset = ctx.time * 1.4;
let unzip_x = (ctx.eased * w as f32) as usize;
let mut prev_a: Option<i32> = None;
let mut prev_b: Option<i32> = None;
for xi in 0..w {
let xf = xi as f32 / w as f32;
let angle = xf * 4.0 * PI + phase_offset;
let ya = (mid + amp * angle.sin()) as i32;
let yb = (mid + amp * (angle + PI).sin()) as i32;
draw::dot_i(grid, xi as i32, ya);
if let Some(py) = prev_a {
let (lo, hi) = (py.min(ya), py.max(ya));
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
prev_a = Some(ya);
draw::dot_i(grid, xi as i32, yb);
if let Some(py) = prev_b {
let (lo, hi) = (py.min(yb), py.max(yb));
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
prev_b = Some(yb);
if xi >= unzip_x && xi % 8 == 0 {
let rung_lo = ya.min(yb).max(0) as usize;
let rung_hi = ya.max(yb).min(h as i32 - 1) as usize;
draw::vline(grid, xi, rung_lo, rung_hi);
}
}
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 Mitosis;
impl ProgressStyle for Mitosis {
fn name(&self) -> &str {
"mitosis"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Cell mitosis: a circle pinches at its equator and divides into two daughter cells"
}
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_y = (h / 2) as f32;
let base_r = ((w.min(h) as f32) * 0.30).max(2.0);
let e = ctx.eased;
let pinch = (e * 2.0).min(1.0); let split = ((e - 0.5) * 2.0).max(0.0);
let h_scale = 1.0 - pinch * 0.45;
let v_scale = 1.0 + pinch * 0.2;
let sep = split * base_r * 1.8;
let draw_cell = |grid: &mut BrailleGrid, cx: f32, cy: f32, rx: f32, ry: f32| {
let steps = ((rx + ry) as usize * 8).max(16);
for s in 0..=steps {
let angle = s as f32 / steps as f32 * 2.0 * PI;
let px = (cx + rx * angle.cos()) as i32;
let py = (cy + ry * angle.sin()) as i32;
draw::dot_i(grid, px, py);
}
};
if split < 0.01 {
let rx = base_r * h_scale;
let ry = base_r * v_scale;
let cx = (w / 2) as f32;
draw_cell(grid, cx, mid_y, rx, ry);
let furrow_depth = (pinch * ry * 0.5) as i32;
let fx = cx as i32;
for d in 0..furrow_depth {
draw::dot_i(grid, fx - d, mid_y as i32);
draw::dot_i(grid, fx + d, mid_y as i32);
}
} else {
let r = base_r * 0.8;
let cx1 = (w as f32 / 2.0) - sep;
let cx2 = (w as f32 / 2.0) + sep;
draw_cell(grid, cx1, mid_y, r, r);
draw_cell(grid, cx2, mid_y, r, r);
draw::dot_i(grid, cx1 as i32, mid_y as i32);
draw::dot_i(grid, cx2 as i32, mid_y as i32);
}
let (cw, ch) = grid.dimensions();
for cy in 0..ch {
let col = ctx.palette.sample(ctx.eased);
draw::tint_row(grid, cy, 0, cw.saturating_sub(1), col);
}
Ok(())
}
}
struct ActionPotential;
impl ProgressStyle for ActionPotential {
fn name(&self) -> &str {
"action-potential"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Neuron action potential: a travelling voltage spike scrolls along the axon; spikes fired = 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 baseline_y = (h as f32 * 0.72) as usize; let spike_amp = (h as f32 * 0.70).max(1.0);
draw::hline(grid, 0, w.saturating_sub(1), baseline_y);
let soma_r = (h as f32 * 0.25).max(1.0) as i32;
for dy in -soma_r..=soma_r {
let dist = (dy.abs() as f32 / soma_r as f32).powi(2);
let dx = ((1.0 - dist).max(0.0).sqrt() * soma_r as f32) as i32;
for ddx in -dx..=dx {
draw::dot_i(grid, ddx, baseline_y as i32 + dy);
}
}
let spike_shape = |phase: f32| -> f32 {
if phase < 0.15 {
(phase / 0.15) * 1.0
} else if phase < 0.30 {
1.0 - ((phase - 0.15) / 0.15) * 1.2
} else if phase < 0.45 {
-0.2 * (1.0 - ((phase - 0.30) / 0.15))
} else {
0.0
}
};
let spike_period = 1.2_f32 - ctx.eased * 0.7; let spike_period = spike_period.max(0.4);
let spike_w = (w as f32 * 0.22).max(6.0);
let scroll = (ctx.time / spike_period).fract(); for pass in 0..5i32 {
let spike_center = w as f32 - (scroll + pass as f32) * (w as f32 * 0.5);
for xi in 0..w {
let xf = xi as f32;
let dist = xf - spike_center;
if dist.abs() < spike_w {
let local_phase = (dist / spike_w * 0.5 + 0.5).clamp(0.0, 1.0);
let v = spike_shape(local_phase);
let dy = (v * spike_amp) as i32;
let sy = baseline_y as i32 - dy;
draw::dot_i(grid, xi as i32, sy.clamp(0, h as i32 - 1));
}
}
}
let (cw, ch) = grid.dimensions();
let filled = (ctx.eased * cw as f32).round() as usize;
for cx in 0..filled.min(cw) {
let t = cx as f32 / cw.max(1) as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct LogisticGrowth;
impl ProgressStyle for LogisticGrowth {
fn name(&self) -> &str {
"logistic-growth"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Logistic population growth: S-curve fills the bar with carrying-capacity ceiling"
}
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 k_y = 2usize;
for x in (0..w).step_by(4) {
draw::hline(grid, x, (x + 2).min(w - 1), k_y);
}
let r = 8.0_f32; let midpoint = (1.0 - ctx.eased) * w as f32;
let mut prev_pop_y: Option<i32> = None;
for xi in 0..w {
let xf = xi as f32;
let t = (xf - midpoint) * r / w as f32;
let n = 1.0 / (1.0 + (-t).exp()); let pop_y = (k_y + 1 + ((1.0 - n) * (h - k_y - 2) as f32) as usize).min(h - 1);
let py = pop_y as i32;
draw::dot_i(grid, xi as i32, py);
if let Some(prev) = prev_pop_y {
let (lo, hi) = (prev.min(py), prev.max(py));
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
prev_pop_y = Some(py);
for y in (pop_y + 1)..h {
if y % 2 == xi % 2 {
draw::dot(grid, xi, y);
}
}
}
for &frac in &[0.25_f32, 0.5, 0.75] {
let ty = (k_y + 1 + ((1.0 - frac) * (h - k_y - 2) as f32) as usize).min(h - 1);
draw::hline(grid, 0, 3.min(w - 1), ty);
}
let (cw, ch) = grid.dimensions();
let filled = (ctx.eased * cw as f32).round() as usize;
for cx in 0..filled.min(cw) {
let t = cx as f32 / cw.max(1) as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct EcgHeartbeat;
impl ProgressStyle for EcgHeartbeat {
fn name(&self) -> &str {
"ecg-heartbeat"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Scrolling ECG / PQRST heartbeat waveform; beats counted = 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 baseline_y = (h as f32 * 0.65) as usize;
draw::hline(grid, 0, w.saturating_sub(1), baseline_y);
let pqrst = |phase: f32| -> f32 {
let p = if (0.05..0.20).contains(&phase) {
((phase - 0.05) / 0.075 * PI).sin() * 0.15
} else {
0.0
};
let q = if (0.25..0.30).contains(&phase) {
-((phase - 0.25) / 0.05 * PI).sin() * 0.12
} else {
0.0
};
let r = if (0.30..0.40).contains(&phase) {
((phase - 0.30) / 0.05 * PI).sin() * 1.0
} else {
0.0
};
let s = if (0.40..0.46).contains(&phase) {
-((phase - 0.40) / 0.03 * PI).sin() * 0.25
} else {
0.0
};
let t = if (0.50..0.70).contains(&phase) {
((phase - 0.50) / 0.10 * PI).sin() * 0.30
} else {
0.0
};
p + q + r + s + t
};
let bpm = 60.0 + ctx.eased * 60.0;
let period = 60.0 / bpm; let scroll_phase = (ctx.time / period) % 1.0;
let amp = (h as f32 * 0.55).max(1.0);
let mut prev_y: Option<i32> = None;
for xi in 0..w {
let phase = ((xi as f32 / w as f32) + scroll_phase) % 1.0;
let v = pqrst(phase);
let dy = baseline_y as i32 - (v * amp) as i32;
let dy = dy.clamp(0, h as i32 - 1);
draw::dot_i(grid, xi as i32, dy);
if let Some(py) = prev_y {
let (lo, hi) = (py.min(dy), 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();
let filled = (ctx.eased * cw as f32).round() as usize;
for cx in 0..filled.min(cw) {
let t = cx as f32 / cw.max(1) as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct ProteinFolding;
impl ProgressStyle for ProteinFolding {
fn name(&self) -> &str {
"protein-folding"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Protein folding: amino-acid chain collapses from extended strand to a compact folded blob"
}
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_residues = (w / 4).max(4).min(32);
let mid_y = (h / 2) as f32;
let blob_r = (w.min(h) as f32 * 0.30).max(2.0);
let cx = (w / 2) as f32;
let e = ctx.eased;
let fold_rot = ctx.time * 0.4;
let mut prev_x: Option<i32> = None;
let mut prev_y: Option<i32> = None;
for i in 0..n_residues {
let frac = if n_residues <= 1 {
0.5
} else {
i as f32 / (n_residues - 1) as f32
};
let ex = frac * (w - 1) as f32;
let ey = mid_y + (i as f32 * 1.3).sin() * (h as f32 * 0.05);
let spiral_angle = frac * 4.0 * PI + fold_rot;
let spiral_r = blob_r * (0.3 + frac * 0.7);
let fx = cx + spiral_r * spiral_angle.cos();
let fy = mid_y + spiral_r * 0.55 * spiral_angle.sin();
let px = (ex + (fx - ex) * e) as i32;
let py = (ey + (fy - ey) * e) as i32;
draw::dot_i(grid, px, py);
if let (Some(lx), Some(ly)) = (prev_x, prev_y) {
line_i(grid, lx, ly, px, py);
}
prev_x = Some(px);
prev_y = Some(py);
}
let (cw, ch) = grid.dimensions();
let blob_cells = (e * cw as f32).round() as usize;
for cx2 in 0..blob_cells.min(cw) {
let t = cx2 as f32 / cw.max(1) as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx2, cx2, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct Photosynthesis;
impl ProgressStyle for Photosynthesis {
fn name(&self) -> &str {
"photosynthesis"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Photosynthesis: sun rays hit a leaf; glucose store fills with 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 mid_y = (h / 2) as i32;
let sun_cx = (w as f32 * 0.08) as i32;
let sun_r = (h as f32 * 0.30).max(2.0) as i32;
let sun_steps = 24usize;
for s in 0..sun_steps {
let angle = s as f32 / sun_steps as f32 * PI * 1.2 - PI * 0.1; let ax = sun_cx + (angle.cos() * sun_r as f32) as i32;
let ay = mid_y + (angle.sin() * sun_r as f32) as i32;
draw::dot_i(grid, ax, ay);
}
let ray_count = 5usize;
let leaf_cx = (w as f32 * 0.50) as i32;
let ray_phase = ctx.time * 1.8;
for ri in 0..ray_count {
let angle_spread = PI * 0.55;
let angle =
-angle_spread / 2.0 + (ri as f32 / (ray_count - 1).max(1) as f32) * angle_spread;
let pulse = (ray_phase + ri as f32 * 0.7).sin() * 0.5 + 0.5;
let ray_len = ((leaf_cx - sun_cx) as f32 * (0.6 + pulse * 0.4)) as i32;
let rx_end = sun_cx + (angle.cos() * ray_len as f32) as i32;
let ry_end = mid_y + (angle.sin() * ray_len as f32) as i32;
let rx_start = sun_cx + (angle.cos() * sun_r as f32) as i32;
let ry_start = mid_y + (angle.sin() * sun_r as f32) as i32;
line_i(grid, rx_start, ry_start, rx_end, ry_end);
}
let leaf_h = (h as f32 * 0.70).max(2.0);
let leaf_w = (w as f32 * 0.12).max(2.0);
let leaf_steps = 20usize;
for s in 0..=leaf_steps {
let t = s as f32 / leaf_steps as f32;
let angle = t * PI;
let lx = leaf_cx + (leaf_w * (angle - PI / 2.0).cos()) as i32;
let ly = mid_y - (leaf_h / 2.0 * angle.sin()) as i32;
draw::dot_i(grid, lx, ly);
draw::dot_i(grid, lx, mid_y + (mid_y - ly));
}
draw::vline(
grid,
leaf_cx as usize,
(mid_y - (leaf_h / 2.0) as i32).max(0) as usize,
(mid_y + (leaf_h / 2.0) as i32).min(h as i32 - 1) as usize,
);
let store_x = (w as f32 * 0.80) as usize;
let store_w = (w as f32 * 0.15).max(2.0) as usize;
let store_h = (h as f32 * 0.70).max(2.0) as usize;
let store_y = (h - store_h) / 2;
draw::rect_outline(grid, store_x, store_y, store_w, store_h);
let fill_h = (ctx.eased * store_h as f32) as usize;
let fill_y = store_y + store_h.saturating_sub(fill_h);
if fill_h > 0 && store_w > 2 {
draw::fill_rect(grid, store_x + 1, fill_y, store_w.saturating_sub(2), fill_h);
}
let (cw, ch) = grid.dimensions();
let filled = (ctx.eased * cw as f32).round() as usize;
for cx in 0..filled.min(cw) {
let t = cx as f32 / cw.max(1) as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct PhylogeneticTree;
impl ProgressStyle for PhylogeneticTree {
fn name(&self) -> &str {
"phylogenetic-tree"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Phylogenetic tree: branches bifurcate outward; depth unlocks with 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 max_depth = 1 + (ctx.eased * 4.0).floor() as usize;
let root_x = 2i32;
let root_y = (h / 2) as i32;
let root_len = (w as f32 * 0.30).max(4.0);
struct Branch {
x: i32,
y: i32,
angle: f32,
len: f32,
depth: usize,
}
let mut stack: Vec<Branch> = vec![Branch {
x: root_x,
y: root_y,
angle: 0.0,
len: root_len,
depth: 0,
}];
let flutter_amp = 0.05_f32;
let flutter = (ctx.time * 2.5).sin() * flutter_amp;
while let Some(b) = stack.pop() {
if b.depth > max_depth {
continue;
}
let end_x = b.x + (b.angle.cos() * b.len) as i32;
let end_y = b.y + (b.angle.sin() * b.len) as i32;
line_i(grid, b.x, b.y, end_x, end_y);
if b.depth < max_depth {
let new_len = b.len * 0.55;
if new_len < 1.5 {
continue;
}
let spread = PI * 0.38 + flutter * (b.depth as f32 + 1.0);
stack.push(Branch {
x: end_x,
y: end_y,
angle: b.angle - spread,
len: new_len,
depth: b.depth + 1,
});
stack.push(Branch {
x: end_x,
y: end_y,
angle: b.angle + spread,
len: new_len,
depth: b.depth + 1,
});
}
}
draw::dot_i(grid, root_x, root_y);
let (cw, ch) = grid.dimensions();
for cx in 0..cw {
let t = cx as f32 / cw.max(1) as f32;
if t <= ctx.eased {
let col = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
}
Ok(())
}
}
struct BloodFlow;
impl ProgressStyle for BloodFlow {
fn name(&self) -> &str {
"blood-flow"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Blood flow: red cells stream through a vessel at pulsatile speed; perfused fraction = 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 mid = h / 2;
let vessel_r = (h as f32 * 0.25).max(1.0) as usize;
let top_wall = mid.saturating_sub(vessel_r);
let bot_wall = (mid + vessel_r).min(h - 1);
draw::hline(grid, 0, w.saturating_sub(1), top_wall);
draw::hline(grid, 0, w.saturating_sub(1), bot_wall);
let perfused_w = (ctx.eased * w as f32) as usize;
let cardiac_freq = 1.1_f32; let cardiac_phase = (ctx.time * cardiac_freq).fract();
let pulse_v = if cardiac_phase < 0.3 {
1.0 + 2.0 * (cardiac_phase / 0.15 * PI).sin().powi(2)
} else {
0.4 + 0.1 * (cardiac_phase * 4.0 * PI).sin()
};
let n_cells = 12usize;
for ci in 0..n_cells {
let base_x = (ci as f32 / n_cells as f32) * w as f32;
let scroll = (ctx.time * pulse_v * 8.0 + base_x) % w as f32;
let cx = scroll as i32;
let cy = mid as i32;
if (scroll as usize) < perfused_w {
draw::dot_i(grid, cx - 1, cy);
draw::dot_i(grid, cx, cy);
draw::dot_i(grid, cx + 1, cy);
draw::dot_i(grid, cx, cy - 1);
draw::dot_i(grid, cx, cy + 1);
}
}
let (cw, ch) = grid.dimensions();
let perfused_cells = (ctx.eased * cw as f32).round() as usize;
for cx in 0..perfused_cells.min(cw) {
let t = cx as f32 / cw.max(1) as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct EnzymeKinetics;
impl ProgressStyle for EnzymeKinetics {
fn name(&self) -> &str {
"enzyme-kinetics"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Michaelis-Menten kinetics: substrates bind the active site; products counted = 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 mid = (h / 2) as i32;
let enzyme_cx = (w as f32 * 0.20) as i32;
let pocket_r = (h as f32 * 0.30).max(2.0) as i32;
let steps = 24usize;
for s in 0..=steps {
let angle = s as f32 / steps as f32 * PI; let ax = enzyme_cx + (angle.cos() * pocket_r as f32) as i32;
let ay = mid + (angle.sin() * pocket_r as f32) as i32;
draw::dot_i(grid, ax, ay);
}
draw::vline(
grid,
(enzyme_cx - pocket_r).max(0) as usize,
(mid - pocket_r).max(0) as usize,
mid as usize,
);
draw::vline(
grid,
(enzyme_cx + pocket_r).min(w as i32 - 1) as usize,
(mid - pocket_r).max(0) as usize,
mid as usize,
);
let s_conc = 1.0 - ctx.eased; let km = 0.3_f32;
let v_max = 1.0_f32;
let rate = v_max * s_conc / (km + s_conc);
let n_substrates = 4usize;
for si in 0..n_substrates {
let phase = (ctx.time * rate * 1.5 + si as f32 * 0.25) % 1.0;
let sx = (w as f32 - (enzyme_cx + pocket_r + 2) as f32) * (1.0 - phase)
+ (enzyme_cx + pocket_r + 2) as f32;
let sy = mid - 1 + (si % 2) as i32 * 2;
if phase < 0.85 {
draw::dot_i(grid, sx as i32, sy);
draw::dot_i(grid, sx as i32 + 1, sy);
draw::dot_i(grid, sx as i32, sy + 1);
draw::dot_i(grid, sx as i32 + 1, sy + 1);
}
}
let n_products = (ctx.eased * 6.0) as usize;
for pi in 0..n_products {
let px = enzyme_cx + pocket_r + 2 + (pi as i32 * 5);
let py = mid + pocket_r / 2;
draw::dot_i(grid, px, py);
draw::dot_i(grid, px + 1, py);
}
let graph_y_base = (h as f32 * 0.88) as usize;
let graph_h = (h as f32 * 0.10).max(1.0) as usize;
for xi in 0..w {
let s = 1.0 - xi as f32 / w as f32; let v = v_max * s / (km + s);
let bar_h = (v * graph_h as f32) as usize;
for y in graph_y_base.saturating_sub(bar_h)..graph_y_base {
draw::dot(grid, xi, y);
}
}
let km_x = ((1.0 - km) * w as f32) as usize;
if km_x < w {
draw::dot(grid, km_x, graph_y_base);
}
let (cw, ch) = grid.dimensions();
let filled = (ctx.eased * cw as f32).round() as usize;
for cx in 0..filled.min(cw) {
let t = cx as f32 / cw.max(1) as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct VirusSpread;
impl ProgressStyle for VirusSpread {
fn name(&self) -> &str {
"virus-spread"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Viral spread: infection sweeps a tissue grid cell by cell; infected fraction = progress"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (cw, ch) = grid.dimensions();
if cw == 0 || ch == 0 {
return Ok(());
}
let _total = cw * ch;
for cy in 0..ch {
for cx in 0..cw {
let dist = (cx + cy) as f32 / ((cw + ch).saturating_sub(2).max(1)) as f32;
let at_front = (dist - ctx.eased).abs() < 0.08;
let shimmer = (ctx.time * 6.0 + dist * 4.0).sin() * 0.5 + 0.5;
if dist <= ctx.eased {
let density = if at_front && shimmer > 0.5 { 3usize } else { 4 };
draw::shade(grid, cx, cy, density);
let col = ctx.palette.sample(dist);
draw::tint_row(grid, cy, cx, cx, col);
} else if dist <= ctx.eased + 0.06 {
draw::shade(grid, cx, cy, 2);
}
}
}
Ok(())
}
}
struct IonChannels;
impl ProgressStyle for IonChannels {
fn name(&self) -> &str {
"ion-channels"
}
fn theme(&self) -> &str {
"biology"
}
fn describe(&self) -> &str {
"Cell membrane ion channels: pumps drive ions through the bilayer; gradient builds 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 outer_y = (h as f32 * 0.35) as usize;
let inner_y = (h as f32 * 0.65) as usize;
draw::hline(grid, 0, w.saturating_sub(1), outer_y);
draw::hline(grid, 0, w.saturating_sub(1), inner_y);
let n_channels = ((w / 10).max(1)).min(8);
for ci in 0..n_channels {
let ch_x = (ci * w / n_channels + w / (n_channels * 2).max(1)).min(w - 1);
let gate_phase = ctx.time * 2.0 + ci as f32 * 0.9;
let open = (gate_phase.sin() * 0.5 + 0.5) * ctx.eased;
let gap_half = 2usize;
if ch_x >= gap_half + 1 {
draw::dot(grid, ch_x - gap_half - 1, outer_y);
draw::dot(grid, ch_x - gap_half - 1, inner_y);
}
if ch_x + gap_half + 1 < w {
draw::dot(grid, ch_x + gap_half + 1, outer_y);
draw::dot(grid, ch_x + gap_half + 1, inner_y);
}
let gate_y_outer = outer_y as i32;
let gate_y_inner = inner_y as i32;
let gate_midpoint = gate_y_outer + ((gate_y_inner - gate_y_outer) as f32 * 0.5) as i32;
let gate_len = ((gate_y_inner - gate_y_outer) as f32 * (1.0 - open) * 0.4) as i32;
draw::dot_i(grid, ch_x as i32, gate_midpoint - gate_len / 2);
draw::dot_i(grid, ch_x as i32, gate_midpoint + gate_len / 2);
let ion_progress = (ctx.time * 1.5 + ci as f32 * 0.6).fract();
let ion_y = (outer_y as f32 + ion_progress * (inner_y - outer_y) as f32) as i32;
if open > 0.2 {
draw::dot_i(grid, ch_x as i32, ion_y);
draw::dot_i(grid, ch_x as i32 - 1, ion_y);
}
}
let outside_density = 1.0 - ctx.eased; let n_outside = (outside_density * w as f32 * 0.3) as usize;
for oi in 0..n_outside {
let ox = (oi * w / n_outside.max(1)) as i32;
let oy_base = outer_y.saturating_sub(1) as i32;
let oy = oy_base
- ((ctx.time * 1.3 + oi as f32 * 0.5).sin() * (outer_y as f32 * 0.5)).abs() as i32;
draw::dot_i(grid, ox, oy.max(0));
}
let inside_density = ctx.eased;
let n_inside = (inside_density * w as f32 * 0.3) as usize;
for ii in 0..n_inside {
let ix = (ii * w / n_inside.max(1)) as i32;
let iy_base = (inner_y + 1).min(h - 1) as i32;
let iy = iy_base
+ ((ctx.time * 1.1 + ii as f32 * 0.6).sin()
* (h.saturating_sub(inner_y) as f32 * 0.4))
.abs() as i32;
draw::dot_i(grid, ix, iy.min(h as i32 - 1));
}
let (cw, ch_cells) = grid.dimensions();
let inner_cell_y = inner_y / 4;
for cy in inner_cell_y.min(ch_cells.saturating_sub(1))..ch_cells {
let t = ctx.eased;
let col = ctx.palette.sample(t);
draw::tint_row(grid, cy, 0, cw.saturating_sub(1), col);
}
for cy in 0..outer_y / 4 {
let col = ctx.palette.sample(1.0 - ctx.eased);
draw::tint_row(grid, cy, 0, cw.saturating_sub(1), col);
}
Ok(())
}
}