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(PeriodicTable),
Box::new(ElectronOrbitals),
Box::new(TitrationCurve),
Box::new(ReactionKinetics),
Box::new(Crystallization),
Box::new(PhScale),
Box::new(GasDiffusion),
Box::new(Distillation),
Box::new(BondVibration),
Box::new(BoltzmannDistribution),
Box::new(FlameSpectralLines),
]
}
#[inline]
fn hash2(a: usize, b: usize) -> u32 {
let mut x = (a as u32)
.wrapping_mul(2654435761)
.wrapping_add(b as u32)
.wrapping_mul(2246822519);
x ^= x >> 15;
x = x.wrapping_mul(2246822519);
x ^= x >> 13;
x
}
struct PeriodicTable;
impl ProgressStyle for PeriodicTable {
fn name(&self) -> &str {
"periodic-table"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Periodic table: element cells fill one-by-one left-to-right, top-to-bottom as progress advances"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (cw, ch) = grid.dimensions();
if cw == 0 || ch == 0 {
return Ok(());
}
let total_cells = cw * ch;
let lit_f = ctx.eased * total_cells as f32;
let full = lit_f.floor() as usize;
let frac = lit_f - full as f32;
for idx in 0..total_cells {
let cx = idx % cw;
let cy = idx / cw;
if idx < full {
draw::glyph(grid, cx, cy, '█');
} else if idx == full && frac > 0.0 {
let level = (frac * 8.0).round() as usize;
draw::vblock(grid, cx, cy, level.max(1));
}
}
for idx in 0..full.min(total_cells) {
let cx = idx % cw;
let cy = idx / cw;
let t = idx as f32 / total_cells as f32;
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
Ok(())
}
}
struct ElectronOrbitals;
impl ProgressStyle for ElectronOrbitals {
fn name(&self) -> &str {
"electron-orbitals"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Electron orbitals: s-sphere, p-dumbbell, d-cloverleaf shapes grow as progress fills the subshell"
}
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 shape = ((ctx.eased * 3.0).floor() as usize).min(2);
let local = (ctx.eased * 3.0).fract();
let cx = (w / 2) as f32;
let cy = (h / 2) as f32;
let rx = (w as f32 * 0.48).max(1.0);
let ry = (h as f32 * 0.48).max(1.0);
let phase = ctx.time * 0.4;
let threshold = 1.0 - local;
for yi in 0..h {
for xi in 0..w {
let nx = (xi as f32 - cx) / rx; let ny = (yi as f32 - cy) / ry;
let r = (nx * nx + ny * ny).sqrt();
if r > 1.1 {
continue;
}
let density = match shape {
0 => {
let sigma = 0.5_f32;
(-(r * r) / (2.0 * sigma * sigma)).exp()
}
1 => {
let angle = ny.atan2(nx) - phase;
let lobe = angle.cos().powi(2);
let radial = (-(r * r) / 0.4).exp();
lobe * radial
}
_ => {
let angle = ny.atan2(nx) - phase * 0.5;
let lobe = (2.0 * angle).cos().powi(2);
let radial = (-(r * r) / 0.35).exp();
lobe * radial
}
};
if density > threshold {
draw::dot(grid, xi, yi);
}
}
}
let (cw, ch) = grid.dimensions();
for cx2 in 0..cw {
let t = ctx.eased;
for cy2 in 0..ch {
draw::tint_row(grid, cy2, cx2, cx2, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct TitrationCurve;
impl ProgressStyle for TitrationCurve {
fn name(&self) -> &str {
"titration-curve"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Titration: a sigmoid pH–volume curve draws out while drips fall and the equivalence point appears"
}
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 drawn_x = (ctx.eased * w as f32).round() as usize;
let mut prev_y: Option<i32> = None;
for xi in 0..drawn_x.min(w) {
let v = xi as f32 / w as f32; let ph = 7.0 + 4.0 * (10.0 * (v - 0.5)).tanh();
let norm = (ph - 3.0) / 8.0; let row = ((1.0 - norm) * (h - 1) as f32).round() as i32;
let row = row.clamp(0, h as i32 - 1);
draw::dot_i(grid, xi as i32, row);
if let Some(py) = prev_y {
let lo = py.min(row);
let hi = py.max(row);
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
prev_y = Some(row);
}
let eq_x = (w / 2) as i32;
let eq_ph = 7.0_f32;
let eq_norm = (eq_ph - 3.0) / 8.0;
let eq_y = ((1.0 - eq_norm) * (h - 1) as f32).round() as i32;
let tick = (h / 6).max(1) as i32;
for yy in (eq_y - tick).max(0)..=(eq_y + tick).min(h as i32 - 1) {
draw::dot_i(grid, eq_x, yy);
}
let drip_interval = 0.4_f32;
let drip_speed = 6.0_f32; let n_drips = 5usize;
for di in 0..n_drips {
let t_offset = di as f32 * drip_interval;
let t_local = (ctx.time - t_offset).rem_euclid(drip_interval * n_drips as f32);
if t_local < 0.0 {
continue;
}
let drop_y = (t_local * drip_speed) as i32;
let drip_x = if drawn_x > 0 {
(di * drawn_x / n_drips.max(1)) as i32
} else {
0
};
if drop_y < h as i32 {
draw::dot_i(grid, drip_x, drop_y);
}
}
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 as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct ReactionKinetics;
impl ProgressStyle for ReactionKinetics {
fn name(&self) -> &str {
"reaction-kinetics"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Reaction kinetics: [A] decays exponentially while [B] rises to equilibrium — both curves fill"
}
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 drawn_x = (ctx.eased * w as f32).round() as usize;
let k = 3.0_f32;
let mut prev_a: Option<i32> = None;
let mut prev_b: Option<i32> = None;
for xi in 0..drawn_x.min(w) {
let t_norm = xi as f32 / w as f32; let a = (-k * t_norm).exp(); let b = 1.0 - a;
let row_a = ((1.0 - a) * (h - 1) as f32).round() as i32;
let row_a = row_a.clamp(0, h as i32 - 1);
let row_b = ((1.0 - b) * (h - 1) as f32).round() as i32;
let row_b = row_b.clamp(0, h as i32 - 1);
draw::dot_i(grid, xi as i32, row_a);
draw::dot_i(grid, xi as i32, row_b);
if let Some(pa) = prev_a {
let (lo, hi) = (pa.min(row_a), pa.max(row_a));
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
if let Some(pb) = prev_b {
let (lo, hi) = (pb.min(row_b), pb.max(row_b));
for yy in lo..=hi {
draw::dot_i(grid, xi as i32, yy);
}
}
for y in (row_b as usize)..h {
if (xi + y) % 2 == 0 {
draw::dot(grid, xi, y);
}
}
prev_a = Some(row_a);
prev_b = Some(row_b);
}
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 as f32;
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct Crystallization;
impl ProgressStyle for Crystallization {
fn name(&self) -> &str {
"crystallization"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Crystallization: a lattice nucleates at the seed and grows outward — atoms snap onto grid positions"
}
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 cx = (w / 2) as i32;
let cy = (h / 2) as i32;
let spacing = (w / 10).max(2) as i32;
let max_r = (((cx * cx + cy * cy) as f32).sqrt()).max(1.0);
let r = ctx.eased * max_r;
let mut lx = -(cx / spacing) * spacing - spacing;
while lx <= cx + cx {
let mut ly = -(cy / spacing) * spacing - spacing;
while ly <= cy + cy {
let dx = lx as f32;
let dy = ly as f32;
let dist = (dx * dx + dy * dy).sqrt();
if dist <= r {
let px = cx + lx;
let py = cy + ly;
draw::dot_i(grid, px, py);
let nx = lx + spacing;
let ny = ly + spacing;
let dist_r = ((nx as f32).powi(2) + (dy).powi(2)).sqrt();
let dist_d = ((dx).powi(2) + (ny as f32).powi(2)).sqrt();
if dist_r <= r && px >= 0 && (cx + nx) < w as i32 {
draw::hline(
grid,
px.max(0) as usize,
(cx + nx).max(0).min(w as i32 - 1) as usize,
py.max(0).min(h as i32 - 1) as usize,
);
}
if dist_d <= r && py >= 0 && (cy + ny) < h as i32 {
draw::vline(
grid,
px.max(0).min(w as i32 - 1) as usize,
py.max(0) as usize,
(cy + ny).max(0).min(h as i32 - 1) as usize,
);
}
}
ly += spacing;
}
lx += spacing;
}
let (cw, ch) = grid.dimensions();
for cx2 in 0..cw {
for cy2 in 0..ch {
let dx = cx2 as i32 - cw as i32 / 2;
let dy = cy2 as i32 - ch as i32 / 2;
let d = ((dx * dx + dy * dy) as f32).sqrt();
let r_cells = r / 2.0; if d <= r_cells {
let t = d / r_cells.max(1.0);
draw::tint_row(grid, cy2, cx2, cx2, ctx.palette.sample(1.0 - t * 0.5));
}
}
}
Ok(())
}
}
struct PhScale;
impl ProgressStyle for PhScale {
fn name(&self) -> &str {
"ph-scale"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"pH scale: a marker slides left (acid) to right (base) along a shaded gradient track"
}
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(());
}
for cx in 0..cw {
let t = cx as f32 / cw.saturating_sub(1).max(1) as f32;
let level = (t * 4.0).round() as usize;
for cy in 0..ch {
let adj = if ch > 2 && (cy == 0 || cy == ch - 1) {
level.saturating_sub(1)
} else {
level
};
draw::shade(grid, cx, cy, adj.min(4));
}
let col = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
let indicator_x = (ctx.eased * (w - 1) as f32).round() as usize;
for y in 0..h {
draw::dot(grid, indicator_x.min(w - 1), y);
}
if indicator_x > 0 {
for y in 1..h.saturating_sub(1) {
draw::dot(grid, indicator_x - 1, y);
}
}
if indicator_x + 1 < w {
for y in 1..h.saturating_sub(1) {
draw::dot(grid, indicator_x + 1, y);
}
}
Ok(())
}
}
struct GasDiffusion;
impl ProgressStyle for GasDiffusion {
fn name(&self) -> &str {
"gas-diffusion"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Gas diffusion: molecules spread from a point source — cloud density is a Gaussian growing 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 sigma_x = (ctx.eased * w as f32 * 0.5).max(0.5);
let sigma_y = (ctx.eased * h as f32 * 0.5).max(0.5);
let src_x = 0.0_f32;
let src_y = (h as f32) / 2.0;
let drift = (ctx.time * 2.0).min(w as f32 * 0.5);
let cx = src_x + drift;
for yi in 0..h {
for xi in 0..w {
let dx = xi as f32 - cx;
let dy = yi as f32 - src_y;
let exponent =
(dx * dx) / (2.0 * sigma_x * sigma_x) + (dy * dy) / (2.0 * sigma_y * sigma_y);
let density = (-exponent).exp();
let h_val = hash2(xi, yi) as f32 / u32::MAX as f32;
if density > h_val * 0.9 + 0.05 {
draw::dot(grid, xi, yi);
}
}
}
let (cw, ch) = grid.dimensions();
let spread_cells = (ctx.eased * cw as f32).round() as usize;
for cx2 in 0..spread_cells.min(cw) {
let t = cx2 as f32 / cw as f32;
for cy2 in 0..ch {
draw::tint_row(grid, cy2, cx2, cx2, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct Distillation;
impl ProgressStyle for Distillation {
fn name(&self) -> &str {
"distillation"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Distillation: vapor rises in a column, condenses at the top, drips down into a filling receiver"
}
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_x = w / 2;
if mid_x > 0 {
draw::vline(grid, 0, 0, h - 1);
}
draw::vline(grid, mid_x, 0, h - 1);
if w > 1 {
draw::vline(grid, w - 1, 0, h - 1);
}
draw::hline(grid, 0, w - 1, 0);
let n_vapour = (mid_x / 2).max(1);
for vi in 0..n_vapour {
let speed = 0.7 + (vi as f32 * 0.13).sin().abs() * 0.4;
let phase = vi as f32 * 0.6;
let y_frac = 1.0 - ((ctx.time * speed + phase).rem_euclid(1.8) / 1.8);
let y = (y_frac * (h - 1) as f32).round() as usize;
let x_wobble = (((ctx.time * 1.1 + vi as f32 * 0.4) * PI).sin() * 1.5) as i32;
let vx = (mid_x / 4) as i32 + x_wobble;
draw::dot_i(grid, vx.max(1).min(mid_x as i32 - 1), y as i32);
}
let drip_period = 0.6_f32;
let drip_y = ((ctx.time.rem_euclid(drip_period) / drip_period) * h as f32).round() as usize;
let drip_x = mid_x + (w - mid_x) / 2;
if drip_x < w && drip_y < h {
draw::dot(grid, drip_x.min(w - 2), drip_y);
}
let receiver_x0 = mid_x + 1;
let receiver_w = w.saturating_sub(receiver_x0 + 1);
if receiver_w > 0 {
let fill_h = (ctx.eased * (h - 2) as f32).round() as usize;
if fill_h > 0 {
let fill_y0 = (h - 1).saturating_sub(fill_h);
draw::fill_rect(grid, receiver_x0, fill_y0, receiver_w, fill_h);
}
}
let (cw, ch) = grid.dimensions();
let recv_cx0 = cw / 2 + 1;
let recv_filled = (ctx.eased * ch as f32).round() as usize;
for cy in (ch.saturating_sub(recv_filled))..ch {
let t = 1.0 - cy as f32 / ch as f32;
draw::tint_row(
grid,
cy,
recv_cx0,
cw.saturating_sub(1),
ctx.palette.sample(t),
);
}
Ok(())
}
}
struct BondVibration;
impl ProgressStyle for BondVibration {
fn name(&self) -> &str {
"bond-vibration"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Bond vibration: two atoms oscillate on a spring — frequency and bond length vary 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_y = (h / 2) as i32;
let d0 = ctx.eased * (w as f32 * 0.6) + w as f32 * 0.1;
let amp = (h as f32 * 0.15).max(1.0);
let omega = 4.0 * PI * (1.0 + (1.0 - ctx.eased) * 2.0);
let disp = amp * (omega * ctx.time).cos();
let cx = (w / 2) as f32;
let ax = (cx - d0 / 2.0 - disp / 2.0).round() as i32;
let bx = (cx + d0 / 2.0 + disp / 2.0).round() as i32;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
if dx * dx + dy * dy <= 1 {
draw::dot_i(grid, ax + dx, mid_y + dy);
draw::dot_i(grid, bx + dx, mid_y + dy);
}
}
}
let left_x = ax.max(0).min(bx);
let right_x = bx.max(0).max(ax);
let bond_len = (right_x - left_x).max(1);
let n_teeth = ((bond_len / 3).max(2) as usize).min(20);
let tooth_h = (h as i32 / 4).max(1);
for ti in 0..n_teeth {
let x0 = left_x + (ti as i32 * bond_len) / n_teeth as i32;
let x1 = left_x + ((ti + 1) as i32 * bond_len) / n_teeth as i32;
let y0 = mid_y + if ti % 2 == 0 { -tooth_h } else { tooth_h };
let y1 = mid_y + if ti % 2 == 0 { tooth_h } else { -tooth_h };
let steps = ((x1 - x0).abs() + (y1 - y0).abs()).max(1) as usize;
for s in 0..=steps {
let f = s as f32 / steps as f32;
let ix = (x0 as f32 + (x1 - x0) as f32 * f).round() as i32;
let iy = (y0 as f32 + (y1 - y0) as f32 * f).round() as i32;
draw::dot_i(grid, ix, iy);
}
}
let (cw, ch) = grid.dimensions();
for cx2 in 0..cw {
let t = cx2 as f32 / cw as f32;
for cy2 in 0..ch {
draw::tint_row(grid, cy2, cx2, cx2, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct BoltzmannDistribution;
impl ProgressStyle for BoltzmannDistribution {
fn name(&self) -> &str {
"boltzmann-distribution"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Maxwell-Boltzmann: velocity histogram builds up — temperature rises with progress, broadening the peak"
}
fn render(&self, grid: &mut BrailleGrid, ctx: &BarContext) -> Result<(), DotmaxError> {
let (cw, ch) = grid.dimensions();
if cw == 0 || ch == 0 {
return Ok(());
}
let temp = 0.3 + ctx.eased * 2.7;
let v_max = 4.0 * temp.sqrt();
let mut heights: Vec<f32> = Vec::with_capacity(cw);
let mut max_h = 0.0_f32;
for col in 0..cw {
let v = (col as f32 + 0.5) / cw as f32 * v_max;
let fv = v * v * (-(v * v) / (2.0 * temp)).exp();
heights.push(fv);
if fv > max_h {
max_h = fv;
}
}
for (col, h_val) in heights.iter_mut().enumerate() {
let ripple = 1.0 + 0.05 * (ctx.time * 3.0 + col as f32 * 0.3).sin();
*h_val *= ripple;
}
let max_h = heights.iter().cloned().fold(0.0_f32, f32::max).max(0.001);
for col in 0..cw {
let norm = heights[col] / max_h; let total_eighths = (norm * ch as f32 * 8.0).round() as usize;
let full_cells = total_eighths / 8;
let partial = total_eighths % 8;
for row in 0..full_cells.min(ch) {
let cy = ch - 1 - row;
draw::vblock(grid, col, cy, 8);
}
if partial > 0 && full_cells < ch {
let cy = ch - 1 - full_cells;
draw::vblock(grid, col, cy, partial);
}
let t = heights[col] / max_h;
for cy in 0..ch {
draw::tint_row(grid, cy, col, col, ctx.palette.sample(t));
}
}
Ok(())
}
}
struct FlameSpectralLines;
impl ProgressStyle for FlameSpectralLines {
fn name(&self) -> &str {
"flame-spectral-lines"
}
fn theme(&self) -> &str {
"chemistry"
}
fn describe(&self) -> &str {
"Flame test: discrete emission lines flicker into existence as progress reveals each spectral line"
}
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 lines: &[(f32, f32, bool)] = &[
(0.08, 0.60, false), (0.20, 0.95, false), (0.38, 0.60, true), (0.44, 0.45, false), (0.52, 0.75, false), (0.60, 0.50, false), (0.65, 0.40, false), (0.72, 0.65, false), (0.80, 0.35, false), (0.88, 0.50, false), (0.94, 0.30, false), ];
let n_lines = lines.len();
draw::hline(grid, 0, w - 1, h - 1);
let visible = (ctx.eased * n_lines as f32).round() as usize;
for (li, &(pos_frac, intensity, doublet)) in lines.iter().enumerate() {
if li >= visible {
break;
}
let flicker = 0.7 + 0.3 * (ctx.time * (3.0 + li as f32 * 0.7)).sin().abs();
let final_intensity = (intensity * flicker).min(1.0);
let xd = (pos_frac * (w - 1) as f32).round() as usize;
let line_h = (final_intensity * (h - 2) as f32).round() as usize;
let y0 = (h - 1).saturating_sub(line_h);
draw::vline(grid, xd.min(w - 1), y0, h - 2);
if doublet && xd + 1 < w {
let doublet_h = (line_h as f32 * 0.85).round() as usize;
let dy0 = (h - 1).saturating_sub(doublet_h);
draw::vline(grid, xd + 1, dy0, h - 2);
}
let (cw, ch) = grid.dimensions();
let cx = (pos_frac * (cw - 1) as f32).round() as usize;
let col = ctx.palette.sample(pos_frac);
for cy in 0..ch {
draw::tint_row(grid, cy, cx.min(cw - 1), cx.min(cw - 1), col);
}
if doublet {
let cx2 = (cx + 1).min(cw - 1);
for cy in 0..ch {
draw::tint_row(grid, cy, cx2, cx2, ctx.palette.sample(pos_frac + 0.01));
}
}
}
Ok(())
}
}