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(Projectile),
Box::new(Pendulum),
Box::new(MassSpring),
Box::new(NewtonsCradle),
Box::new(Orbital),
Box::new(TerminalVelocity),
Box::new(ElasticCollision),
Box::new(GravityWell),
Box::new(LightCone),
Box::new(DoubleSlit),
Box::new(Doppler),
]
}
#[inline]
fn line(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 px = x0 + (x1 - x0) * i / steps;
let py = y0 + (y1 - y0) * i / steps;
draw::dot_i(grid, px, py);
}
}
fn disc(grid: &mut BrailleGrid, cx: i32, cy: i32, r: i32) {
for dy in -r..=r {
for dx in -r..=r {
if dx * dx + dy * dy <= r * r + r {
draw::dot_i(grid, cx + dx, cy + dy);
}
}
}
}
fn tint_around(grid: &mut BrailleGrid, x: i32, y: i32, r: i32, col: crate::Color) {
let lo = (x - r).max(0) as usize / 2;
let hi = (x + r).max(0) as usize / 2;
for yy in (y - r).max(0)..=(y + r).max(0) {
draw::tint_row(grid, yy as usize / 4, lo, hi, col);
}
}
struct Projectile;
impl ProgressStyle for Projectile {
fn name(&self) -> &str {
"projectile"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Projectile motion: a cannonball fired from a cannon traces its parabola toward a target flag as progress rises"
}
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 wf = w as f32;
let hf = h as f32;
let theta: f32 = PI / 4.0; let g: f32 = 1.0; let v: f32 = 1.0; let t_flight = 2.0 * v * theta.sin() / g; let x_range = v * theta.cos() * t_flight;
let arc_steps = ((w * 4).max(32)) as usize;
let t_now = ctx.eased * t_flight;
let ground = h.saturating_sub(1);
for x in (0..w).step_by(3) {
draw::dot(grid, x, ground);
}
let mut prev: Option<(i32, i32)> = None;
for step in 0..=arc_steps {
let frac = step as f32 / arc_steps as f32;
let t = frac * t_now;
let px = (v * theta.cos() * t / x_range.max(1e-6) * wf) as i32;
let py_phys = v * theta.sin() * t - 0.5 * g * t * t;
let max_y = 0.5 * v * v * theta.sin() * theta.sin() / g;
let py = (ground as f32 - py_phys / max_y.max(1e-6) * (hf - 2.0)) as i32;
let py = py.clamp(0, h as i32 - 1);
if let Some((lx, ly)) = prev {
line(grid, lx, ly, px, py);
}
draw::dot_i(grid, px, py);
prev = Some((px, py));
}
let fill_x = (ctx.eased * wf) as usize;
draw::hline(grid, 0, fill_x.min(w.saturating_sub(1)), ground);
if let Some((bx, by)) = prev {
disc(grid, bx, by.min(ground as i32 - 2), 1);
if (ctx.time * 8.0) as i32 % 2 == 0 {
draw::dot_i(grid, bx + 2, by.min(ground as i32 - 2));
}
}
let gi = ground as i32;
if h >= 8 {
disc(grid, 2, gi - 2, 1);
for k in 0..5 {
draw::dot_i(grid, 2 + k, gi - 3 - k);
draw::dot_i(grid, 3 + k, gi - 3 - k);
}
}
let fx = w.saturating_sub(2);
draw::vline(grid, fx, ground.saturating_sub(7), ground);
for k in 0..3usize {
draw::hline(
grid,
fx.saturating_sub(4 - k),
fx,
ground.saturating_sub(7) + k,
);
}
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 as f32;
let col = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct Pendulum;
impl ProgressStyle for Pendulum {
fn name(&self) -> &str {
"pendulum"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Pendulum: pivot rides a ceiling beam to progress; rod and solid bob swing θ₀·cos(ωt) beneath"
}
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 wi = w as i32;
let hi = h as i32;
let bob_r = if hi >= 12 { 2 } else { 1 };
let rod_len = (hi - bob_r - 2).max(1) as f32;
let theta0 = 0.61_f32;
let omega = 2.6_f32;
let theta = theta0 * (omega * ctx.time).cos();
let margin = (rod_len * theta0.sin()) as i32 + bob_r + 1;
let span = (wi - 2 * margin).max(0) as f32;
let pivot_x = margin.min(wi - 1) + (ctx.eased * span) as i32;
let pivot_y = 0_i32;
for x in 0..wi {
if x <= pivot_x || x % 3 == 0 {
draw::dot_i(grid, x, pivot_y);
}
}
for dx in -2_i32..=2 {
draw::dot_i(grid, pivot_x + dx, pivot_y + 1);
}
let steps = ((2.0 * rod_len * theta0) as i32).max(2);
for s in 0..=steps {
if s % 2 == 1 {
continue;
}
let th = theta0 * ((s as f32 / steps as f32) * 2.0 - 1.0);
let ax = pivot_x + (rod_len * th.sin()).round() as i32;
let ay = pivot_y + (rod_len * th.cos()).round() as i32;
draw::dot_i(grid, ax, ay);
}
let bob_x = pivot_x + (rod_len * theta.sin()).round() as i32;
let bob_y = pivot_y + (rod_len * theta.cos()).round() as i32;
line(grid, pivot_x, pivot_y + 1, bob_x, bob_y);
disc(grid, bob_x, bob_y, bob_r);
let (cw, ch) = grid.dimensions();
let pivot_cell = (pivot_x.max(0) as usize) / 2;
for cx in 0..=pivot_cell.min(cw.saturating_sub(1)) {
let t = cx as f32 / cw.max(1) as f32;
draw::tint_row(grid, 0, cx, cx, ctx.palette.sample(t));
}
let lead = ctx.palette.sample(ctx.eased);
let (bx0, bx1) = (
(bob_x - bob_r).min(pivot_x).max(0) as usize / 2,
(bob_x + bob_r).max(pivot_x).max(0) as usize / 2,
);
for cy in 1..ch {
draw::tint_row(grid, cy, bx0, bx1, lead);
}
Ok(())
}
}
struct MassSpring;
impl ProgressStyle for MassSpring {
fn name(&self) -> &str {
"mass-spring"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Mass-spring SHM: x=A·cos(ωt) — coil compresses/extends, amplitude grows 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 wf = w as f32;
let hf = h as f32;
let omega = 3.0_f32; let amp_max = wf * 0.35;
let amp = ctx.eased * amp_max;
let equilibrium_x = wf * 0.55; let mass_x = equilibrium_x + amp * (omega * ctx.time).cos();
let clamp_lo = 2.0_f32;
let clamp_hi = (wf - 3.0).max(clamp_lo);
let mass_x = mass_x.clamp(clamp_lo, clamp_hi);
let mid_y = (hf / 2.0) as i32;
draw::vline(grid, 0, 0, h.saturating_sub(1));
draw::vline(grid, 1, 0, h.saturating_sub(1));
let spring_start = 2_i32;
let spring_end = (mass_x as i32 - 3).max(spring_start + 1);
let spring_len = (spring_end - spring_start).max(1);
let coil_count = 6_i32;
let half_amp = (hf * 0.25).max(1.0) as i32;
for c in 0..coil_count {
let x0 = spring_start + spring_len * c / coil_count;
let x1 = spring_start + spring_len * (c + 1) / coil_count;
let y0 = mid_y + if c % 2 == 0 { half_amp } else { -half_amp };
let y1 = mid_y + if c % 2 == 0 { -half_amp } else { half_amp };
line(grid, x0, y0, x1, y1);
}
let mx = mass_x as i32;
for dy in -2_i32..=2 {
for dx in 0_i32..=3 {
draw::dot_i(grid, mx + dx, mid_y + dy);
}
}
let eq_x = equilibrium_x as usize;
for y in (0..h).step_by(2) {
draw::dot(grid, eq_x.min(w.saturating_sub(1)), y);
}
let (cw, ch) = grid.dimensions();
let mass_cell = (mass_x / 2.0) as usize;
let eq_cell = (equilibrium_x / 2.0) as usize;
let (lo, hi) = if mass_cell < eq_cell {
(mass_cell, eq_cell)
} else {
(eq_cell, mass_cell)
};
for cx in lo..hi.min(cw) {
let t = cx as f32 / cw as f32;
let col = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
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 as f32;
let col = ctx.palette.sample(t * 0.5); for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct NewtonsCradle;
impl ProgressStyle for NewtonsCradle {
fn name(&self) -> &str {
"newtons-cradle"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Newton's cradle: five touching balls in a frame; the end balls trade swings, base fills 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 wf = w as f32;
let hf = h as f32;
let wi = w as i32;
let hi = h as i32;
let n_balls: i32 = 5;
let ball_r = ((hf * 0.16) as i32).max(1);
let pitch = 2 * ball_r + 2;
let rod_top = 0_i32;
let rod_len = (hf - ball_r as f32 - 3.0).max(2.0);
let row_w = pitch * (n_balls - 1);
let x_first = (wf / 2.0) as i32 - row_w / 2;
let omega = 3.5_f32;
let theta0 = 0.35 + ctx.eased * 0.35;
let phase = (omega * ctx.time).cos();
let left_theta = if phase >= 0.0 { -theta0 * phase } else { 0.0 };
let right_theta = if phase < 0.0 { -theta0 * phase } else { 0.0 };
let reach = (rod_len * theta0.sin()) as i32 + ball_r + 4;
let post_l = (x_first - reach).max(0);
let post_r = (x_first + row_w + reach).min(wi - 1);
let base_y = hi - 1;
for x in post_l..=post_r {
draw::dot_i(grid, x, rod_top);
}
line(grid, post_l, rod_top, post_l, base_y);
line(grid, post_r, rod_top, post_r, base_y);
let filled = (ctx.eased * wf) as i32;
for x in 0..wi {
if x < filled || x % 3 == 0 {
draw::dot_i(grid, x, base_y);
}
}
for i in 0..n_balls {
let pivot_x = x_first + i * pitch;
let theta = if i == 0 {
left_theta
} else if i == n_balls - 1 {
right_theta
} else {
0.0
};
let bob_x = pivot_x + (rod_len * theta.sin()).round() as i32;
let bob_y = rod_top + (rod_len * theta.cos()).round() as i32;
line(grid, pivot_x, rod_top, bob_x, bob_y);
disc(grid, bob_x, bob_y, ball_r);
}
let (cw, ch) = grid.dimensions();
let filled_cells = (filled.max(0) as usize + 1) / 2;
for cx in 0..filled_cells.min(cw) {
let t = cx as f32 / cw as f32;
draw::tint_row(grid, ch.saturating_sub(1), cx, cx, ctx.palette.sample(t));
}
let lo = ((x_first - reach).max(0) as usize) / 2 + 1;
let hi_c = ((x_first + row_w + reach).max(0) as usize) / 2;
for cy in 0..ch.saturating_sub(1) {
draw::tint_row(
grid,
cy,
lo,
hi_c.saturating_sub(1),
ctx.palette.sample(ctx.eased),
);
}
Ok(())
}
}
struct Orbital;
impl ProgressStyle for Orbital {
fn name(&self) -> &str {
"orbital"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Kepler orbit: planet with a circling moon sweeps an elliptical path around a pulsing star; orbit traced = 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 wf = w as f32;
let hf = h as f32;
let cx = wf / 2.0;
let cy = (hf - 1.0) / 2.0;
let a = (wf / 2.0 - 4.0).max(1.0);
let b = (hf / 2.0 - 2.0).max(0.5);
let e = 0.3_f32;
let star_x = cx + a * e;
let kepler = |m: f32| -> f32 {
let mut ea = m;
for _ in 0..6 {
ea -= (ea - e * ea.sin() - m) / (1.0 - e * ea.cos());
}
ea
};
let pos = |ea: f32| -> (i32, i32) {
let ang = ea + PI;
(
(cx + a * ang.cos()).round() as i32,
(cy - b * ang.sin()).round() as i32,
)
};
let m_now = ctx.eased * 2.0 * PI;
let e_now = kepler(m_now);
let steps = (w * 2).max(8);
for s in 0..=steps {
let ea = s as f32 / steps as f32 * 2.0 * PI;
if ea <= e_now || s % 4 == 0 {
let (x, y) = pos(ea);
draw::dot_i(grid, x, y);
}
}
let sx = star_x.round() as i32;
let sy = cy.round() as i32;
let star_r = if h >= 12 { 2 } else { 1 };
disc(grid, sx, sy, star_r);
let ray = star_r + 1 + ((ctx.time * 6.0).sin() > 0.0) as i32;
for &(dx, dy) in &[(1, 0), (-1, 0), (0, 1), (0, -1)] {
draw::dot_i(grid, sx + dx * (ray + 1), sy + dy * ray);
}
let (px, py) = pos(e_now);
disc(grid, px, py, 1);
let moon_a = ctx.time * 5.0;
draw::dot_i(
grid,
px + (4.0 * moon_a.cos()).round() as i32,
py + (2.5 * moon_a.sin()).round() as i32,
);
let (cw, ch) = grid.dimensions();
for cx_ in 0..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 * ctx.eased));
}
}
tint_around(grid, sx, sy, star_r + 2, ctx.palette.end);
tint_around(grid, px, py, 2, ctx.palette.start);
Ok(())
}
}
struct TerminalVelocity;
impl ProgressStyle for TerminalVelocity {
fn name(&self) -> &str {
"terminal-velocity"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Terminal velocity: v=v_t·(1−e^(−t/τ)) plots toward its asymptote while a falling body's drag arrow grows to match its weight"
}
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 tau = 1.0_f32;
let t_max = 4.0 * tau;
let ax_x = 1_usize; let ax_y = h.saturating_sub(2); draw::vline(grid, ax_x, 0, ax_y);
draw::hline(grid, ax_x, w.saturating_sub(1), ax_y);
let top_y = 1_usize;
for xi in (ax_x..w).step_by(3) {
if xi + 1 < w {
draw::dot(grid, xi, top_y);
}
}
let t_now = ctx.eased * t_max;
let graph_h = (ax_y as f32 - top_y as f32).max(1.0);
let fbd_w = if w >= 40 { 16 } else { 0 };
let graph_w = w.saturating_sub(ax_x + 1 + fbd_w) as f32;
let mut prev: Option<(i32, i32)> = None;
let steps = w.max(2);
for s in 0..=steps {
let frac = s as f32 / steps as f32;
let t = frac * t_now;
let v = 1.0 - (-t / tau).exp(); let px = (ax_x as f32 + frac * graph_w * (t_now / t_max)) as i32;
let py = (ax_y as f32 - v * graph_h) as i32;
let py = py.clamp(top_y as i32, ax_y as i32);
draw::dot_i(grid, px, py);
if let Some((lx, ly)) = prev {
line(grid, lx, ly, px, py);
}
prev = Some((px, py));
}
if fbd_w > 0 {
let v_now = 1.0 - (-t_now / tau).exp();
let bx = w as i32 - (fbd_w as i32) / 2;
let by = (ax_y as i32) / 2;
disc(grid, bx, by, 2);
let arrow = |grid: &mut BrailleGrid, y0: i32, len: i32, dir: i32| {
if len <= 0 {
return;
}
let tip = y0 + dir * len;
line(grid, bx, y0, bx, tip);
draw::dot_i(grid, bx - 1, tip - dir);
draw::dot_i(grid, bx + 1, tip - dir);
};
let max_len = (by - 3).max(1);
arrow(grid, by + 3, max_len, 1);
arrow(grid, by - 3, (v_now * max_len as f32).round() as i32, -1);
let streaks = (v_now * 4.0).round() as i32;
let scroll = (ctx.time * (4.0 + 20.0 * v_now)) as i32;
for k in 0..streaks {
for side in [-5_i32, 5] {
let y = (ax_y as i32
- 1
- (scroll + k * 4 + side).rem_euclid((ax_y as i32).max(1)))
.max(0);
draw::dot_i(grid, bx + side, y);
draw::dot_i(grid, bx + side, y + 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 t = cx as f32 / cw as f32;
let col = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct ElasticCollision;
impl ProgressStyle for ElasticCollision {
fn name(&self) -> &str {
"elastic-collision"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"1D elastic collision: cart A→B, momentum swaps — three phases: approach, impact, separation"
}
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 wf = w as f32;
let hf = h as f32;
let mid_y = (hf / 2.0) as i32;
let cart_w = (wf / 8.0).max(3.0) as i32;
let cart_h = (hf / 3.0).max(2.0) as i32;
let track_y = (mid_y + cart_h / 2 + 1).min(h as i32 - 1);
draw::hline(grid, 0, w.saturating_sub(1), track_y as usize);
let phase = ctx.eased;
let center_x = (wf / 2.0) as i32;
let (ax, bx) = if phase < 0.4 {
let a_frac = phase / 0.4;
let a_x = (a_frac * (wf * 0.4)) as i32;
let b_x = center_x;
(a_x, b_x)
} else if phase < 0.6 {
let vib = ((ctx.time * 20.0).sin() * 2.0) as i32;
(center_x + vib, center_x + vib)
} else {
let b_frac = (phase - 0.6) / 0.4;
let a_x = center_x;
let b_x = center_x + (b_frac * wf * 0.4) as i32;
(a_x, b_x)
};
let a_left = ax;
let a_top = mid_y - cart_h / 2;
for dy in 0..cart_h {
for dx in 0..cart_w {
draw::dot_i(grid, a_left + dx, a_top + dy);
}
}
if cart_w > 2 && cart_h > 2 {
for dy in 1..cart_h - 1 {
for dx in 1..cart_w - 1 {
let _ = (a_left + dx, a_top + dy); }
}
}
let b_left = bx;
let b_top = mid_y - cart_h / 2;
for dy in 0..cart_h {
draw::dot_i(grid, b_left, b_top + dy);
draw::dot_i(grid, b_left + cart_w - 1, b_top + dy);
}
for dx in 0..cart_w {
draw::dot_i(grid, b_left + dx, b_top);
draw::dot_i(grid, b_left + dx, b_top + cart_h - 1);
}
let arrow_y = (a_top - 2).max(0);
if phase < 0.4 {
let tail = a_left;
let head = (a_left + cart_w + 2).min(w as i32 - 1);
line(grid, tail, arrow_y, head, arrow_y);
draw::dot_i(grid, head - 1, arrow_y - 1);
draw::dot_i(grid, head - 1, arrow_y + 1);
}
if phase > 0.6 {
let tail = b_left + cart_w;
let head = (b_left + cart_w + 4).min(w as i32 - 1);
line(grid, tail, arrow_y, head, arrow_y);
draw::dot_i(grid, head - 1, arrow_y - 1);
draw::dot_i(grid, head - 1, arrow_y + 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 t = cx as f32 / cw as f32;
let col = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}
struct GravityWell;
impl ProgressStyle for GravityWell {
fn name(&self) -> &str {
"gravity-well"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Gravity well: spacetime grid sheet dips into a funnel around a mass; a test particle spirals in 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 wf = w as f32;
let hf = h as f32;
let cx = wf / 2.0;
let bottom = (hf - 3.0).max(0.0);
let s = (wf / 10.0).max(1.0);
let dip = |x: f32| -> f32 {
let u = (x - cx) / s;
1.0 / (1.0 + u * u)
};
let surf = |base: f32, x: f32| -> f32 { base + (bottom - base) * dip(x) };
let bases = [1.0_f32, hf * 0.30, hf * 0.55];
for &base in &bases {
let mut prev: Option<(i32, i32)> = None;
for x in 0..w {
let y = surf(base, x as f32).round() as i32;
if let Some((lx, ly)) = prev {
line(grid, lx, ly, x as i32, y);
}
prev = Some((x as i32, y));
}
}
let nstrut = 4;
for k in 1..=nstrut {
for side in [-1.0_f32, 1.0] {
let x = cx + side * (k as f32) * (wf / 2.0) / (nstrut as f32 + 0.5);
let y0 = surf(bases[0], x).round() as i32;
let y1 = surf(bases[2], x).round() as i32;
let xi = x.round() as i32;
line(grid, xi, y0, xi - side as i32, y1);
}
}
let mass_y = bottom.round() as i32;
disc(grid, cx.round() as i32, mass_y, if h >= 12 { 2 } else { 1 });
let r_max = wf / 2.0 - 2.0;
let r_now = (r_max * (1.0 - ctx.eased)).max(3.0);
let omega = 1.5 * (r_max / r_now).powf(1.5).min(12.0);
let phi = ctx.time * omega;
let at = |r: f32, ph: f32| -> (i32, i32) {
let x = cx + r * ph.cos();
let depth = surf(bases[1], cx + r) + 2.0 * ph.sin();
(x.round() as i32, depth.round() as i32)
};
for i in 1..18 {
let back = i as f32 * 0.16;
let r = (r_now + back * 2.5).min(r_max);
let (x, y) = at(r, phi - back);
if i % 2 == 0 {
draw::dot_i(grid, x, y);
}
}
let (px, py) = at(r_now, phi);
disc(grid, px, py, 1);
let (cw, ch) = grid.dimensions();
for cy_ in 0..ch {
let t = (cy_ as f32 + 0.5) / ch.max(1) as f32;
draw::tint_row(grid, cy_, 0, cw, ctx.palette.sample(t));
}
tint_around(grid, cx.round() as i32, mass_y, 2, ctx.palette.end);
tint_around(grid, px, py, 1, ctx.palette.start);
Ok(())
}
}
struct LightCone;
impl ProgressStyle for LightCone {
fn name(&self) -> &str {
"light-cone"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Light cone: hourglass of past and future cones through the here-now plane; future cone and worldline grow 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 wf = w as f32;
let hi = h as i32;
let cx = (wf / 2.0) as i32;
let ey = (hi - 1) / 2;
let slope = 1.7_f32;
let ellipse = |grid: &mut BrailleGrid, ecx: i32, ecy: i32, a: f32, b: f32, dotted: bool| {
let steps = ((a * 4.0) as i32).max(8);
let shift = (ctx.time * 6.0) as i32;
for s in 0..steps {
if dotted && (s + shift) % 3 != 0 {
continue;
}
let t = s as f32 / steps as f32 * 2.0 * PI;
draw::dot_i(
grid,
ecx + (a * t.cos()).round() as i32,
ecy + (b * t.sin()).round() as i32,
);
}
};
ellipse(grid, cx, ey, wf / 2.0 - 2.0, (hi / 4) as f32, true);
let past_d = (hi - 2 - ey).max(1);
let past_a = past_d as f32 * slope;
line(grid, cx, ey, cx - past_a as i32, ey + past_d);
line(grid, cx, ey, cx + past_a as i32, ey + past_d);
ellipse(grid, cx, ey + past_d, past_a, 1.0, false);
let fut_max = ey.max(1);
let fut_d = (1.0 + ctx.eased * (fut_max - 1) as f32).round() as i32;
let fut_a = fut_d as f32 * slope;
line(grid, cx, ey, cx - fut_a as i32, ey - fut_d);
line(grid, cx, ey, cx + fut_a as i32, ey - fut_d);
ellipse(grid, cx, ey - fut_d, fut_a, 1.0, false);
let top = ey - fut_d + 1;
for y in top..hi {
let dy = (y - ey) as f32;
let x = cx as f32 + (dy * 0.45 + ctx.time * 1.5).sin() * 0.8 * dy.abs().min(4.0);
draw::dot_i(grid, x.round() as i32, y);
}
for d in -1i32..=1 {
draw::dot_i(grid, cx + d, ey);
draw::dot_i(grid, cx, ey + d);
}
let (cw, ch) = grid.dimensions();
let ey_cell = (ey.max(0) as usize) / 4;
for cy in 0..ch {
let col = match cy.cmp(&ey_cell) {
std::cmp::Ordering::Less => ctx.palette.sample(0.5 + 0.5 * ctx.eased),
std::cmp::Ordering::Equal => ctx.palette.sample(0.5),
std::cmp::Ordering::Greater => ctx.palette.start,
};
draw::tint_row(grid, cy, 0, cw, col);
}
Ok(())
}
}
struct DoubleSlit;
impl ProgressStyle for DoubleSlit {
fn name(&self) -> &str {
"double-slit"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Double slit: plane waves hit a two-slit barrier, ripples interfere, and the screen's cos² fringes build 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 wi = w as i32;
let hi = h as i32;
let cy = (hi - 1) as f32 / 2.0;
let barrier_x = (wi * 3 / 10).max(1);
let screen_x = wi - 1;
let hist_len = (wi / 4).max(1);
let ripple_end = screen_x - hist_len - 1;
let slit_off = (hi / 5).max(1);
let slits = [
(cy - slit_off as f32).round() as i32,
(cy + slit_off as f32).round() as i32,
];
for y in 0..hi {
if slits.iter().all(|&sy| (y - sy).abs() > 1) {
draw::dot_i(grid, barrier_x, y);
draw::dot_i(grid, barrier_x + 1, y);
}
}
let lambda = 5.0_f32;
let phase = (ctx.time * 9.0) % lambda;
for y in (cy as i32 - 2)..=(cy as i32 + 2) {
draw::dot_i(grid, 0, y);
draw::dot_i(grid, 1, y);
}
let mut x = 3.0 + phase;
while (x as i32) < barrier_x - 1 {
for y in 0..hi {
if y % 2 == 0 {
draw::dot_i(grid, x as i32, y);
}
}
x += lambda;
}
let max_r = (ripple_end - barrier_x).max(1) as f32;
for ring in 0.. {
let r = phase + 1.0 + ring as f32 * lambda;
if r >= max_r {
break;
}
let steps = (r * 3.0) as i32 + 4;
for &sy in &slits {
for i in 0..=steps {
if i % 2 == 1 {
continue;
}
let a = -PI / 2.0 + PI * i as f32 / steps as f32;
let px = barrier_x + 2 + (r * a.cos()).round() as i32;
let py = sy + (r * a.sin()).round() as i32;
if px <= ripple_end {
draw::dot_i(grid, px, py);
}
}
}
}
for y in 0..hi {
draw::dot_i(grid, screen_x, y);
}
let fringe = (hi as f32 / 3.2).max(1.0);
for y in 0..hi {
let dy = y as f32 - cy;
let env = (1.0 - (dy.abs() / (cy + 2.0))).max(0.0);
let i = (PI * dy / fringe).cos().powi(2) * env;
let len = (i * ctx.eased * hist_len as f32).round() as i32;
for k in 0..len {
draw::dot_i(grid, screen_x - 1 - k, y);
}
}
let (cw, ch) = grid.dimensions();
let bcell = barrier_x.max(0) as usize / 2;
let hcell = (ripple_end + 1).max(0) as usize / 2;
for cy_ in 0..ch {
draw::tint_row(grid, cy_, 0, bcell.saturating_sub(1), ctx.palette.start);
draw::tint_row(grid, cy_, bcell + 1, hcell, ctx.palette.sample(0.4));
draw::tint_row(
grid,
cy_,
hcell + 1,
cw,
ctx.palette.sample(0.5 + ctx.eased * 0.5),
);
}
Ok(())
}
}
struct Doppler;
impl ProgressStyle for Doppler {
fn name(&self) -> &str {
"doppler"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Doppler effect: a source drives across with progress; its circular wavefronts bunch up ahead and stretch out behind"
}
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 wf = w as f32;
let hf = h as f32;
let c_sound = 24.0_f32; let v_src = c_sound * 0.6; let period = 0.26_f32; let n_waves = 8_usize;
let margin = 4.0_f32;
let src_x = margin + ctx.eased * (wf - 2.0 * margin).max(0.0);
let src_y = (hf - 1.0) / 2.0;
let t_phase = ctx.time % period;
for n in 0..n_waves {
let age = t_phase + n as f32 * period; let radius = c_sound * age; let emit_x = src_x - v_src * age;
if radius < 0.5 {
continue;
}
let circ_steps = ((radius * PI * 2.0) as usize + 8).clamp(8, 128);
let mut prev_c: Option<(i32, i32)> = None;
for s in 0..=circ_steps {
let angle = s as f32 / circ_steps as f32 * 2.0 * PI;
let cx = (emit_x + radius * angle.cos()) as i32;
let cy = (src_y + radius * angle.sin()) as i32;
draw::dot_i(grid, cx, cy);
if let Some((lx, ly)) = prev_c {
if (cx - lx).abs() + (cy - ly).abs() > 2 {
line(grid, lx, ly, cx, cy);
}
}
prev_c = Some((cx, cy));
}
}
let src_ix = src_x as i32;
let src_iy = src_y.round() as i32;
disc(grid, src_ix, src_iy, 2);
let arr_y = (src_iy - 5).max(1);
let arr_len = (v_src / c_sound * 6.0 + 1.0) as i32;
line(grid, src_ix, arr_y, src_ix + arr_len, arr_y);
draw::dot_i(grid, src_ix + arr_len - 1, arr_y - 1);
draw::dot_i(grid, src_ix + arr_len - 1, arr_y + 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 t = cx as f32 / cw as f32;
let col = ctx.palette.sample(t);
for cy in 0..ch {
draw::tint_row(grid, cy, cx, cx, col);
}
}
Ok(())
}
}