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);
}
}
struct Projectile;
impl ProgressStyle for Projectile {
fn name(&self) -> &str {
"projectile"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Projectile motion: parabolic arc y=v·sinθ·t−½g·t² draws out 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);
draw::hline(grid, 0, w.saturating_sub(1), 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);
draw::dot_i(grid, 0, ground as i32);
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 SHM: θ(t)=θ₀·cos(√(g/L)·t) — amplitude grows with progress, bob swings 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 wf = w as f32;
let hf = h as f32;
let pivot_x = (wf / 2.0) as i32;
let pivot_y = 0_i32;
let rod_len = (hf * 0.80).max(2.0);
let theta0 = ctx.eased * (PI / 3.0); let omega = (9.8_f32 / rod_len).sqrt() * 4.0;
let theta = theta0 * (omega * ctx.time).cos();
let bob_x = pivot_x + (rod_len * theta.sin()) as i32;
let bob_y = pivot_y + (rod_len * theta.cos()) as i32;
line(grid, pivot_x, pivot_y, bob_x, bob_y);
draw::dot_i(grid, pivot_x, pivot_y);
for dy in -1_i32..=1 {
for dx in -1_i32..=1 {
draw::dot_i(grid, bob_x + dx, bob_y + dy);
}
}
let arc_steps = w.max(2);
let mut prev: Option<(i32, i32)> = None;
for s in 0..=arc_steps {
let th = if arc_steps == 0 {
0.0
} else {
theta0 * ((s as f32 / arc_steps as f32) * 2.0 - 1.0)
};
let ax = pivot_x + (rod_len * th.sin()) as i32;
let ay = pivot_y + (rod_len * th.cos()) as i32;
draw::dot_i(grid, ax, ay);
if let Some((lx, ly)) = prev {
line(grid, lx, ly, ax, ay);
}
prev = Some((ax, ay));
}
let eq_y = (pivot_y + rod_len as i32).min(h as i32 - 1);
draw::hline(grid, 0, w.saturating_sub(1), eq_y as usize);
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 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: elastic momentum transfer — end balls trade arcs every half-period"
}
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 n_balls: usize = 5;
let ball_r = (wf / (n_balls as f32 * 3.0)).max(1.0) as i32;
let spacing = wf / (n_balls as f32 + 1.0);
let rod_top = (hf * 0.05) as i32;
let rod_len = (hf * 0.65).max(4.0);
let bob_y_eq = rod_top + rod_len as i32;
let omega = 3.5_f32;
let theta0 = ctx.eased * (PI / 5.0);
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 bar_y = rod_top;
draw::hline(grid, 0, w.saturating_sub(1), bar_y as usize);
for i in 0..n_balls {
let pivot_x = (spacing * (i as f32 + 1.0)) as i32;
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()) as i32;
let bob_y = rod_top + (rod_len * theta.cos()) as i32;
line(grid, pivot_x, rod_top, bob_x, bob_y);
for step in 0..16_i32 {
let a = step as f32 / 16.0 * 2.0 * PI;
let bx = bob_x + (ball_r as f32 * a.cos()) as i32;
let by = bob_y + (ball_r as f32 * a.sin()) as i32;
draw::dot_i(grid, bx, by);
}
draw::dot_i(grid, bob_x, bob_y);
}
let ground = (bob_y_eq + ball_r + 1).min(h as i32 - 1);
draw::hline(grid, 0, w.saturating_sub(1), ground as usize);
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 Orbital;
impl ProgressStyle for Orbital {
fn name(&self) -> &str {
"orbital"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Kepler orbit: elliptical path, equal-area sweeping — planet position from mean anomaly"
}
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 / 2.0;
let a = (wf / 2.0 - 2.0).max(2.0); let ecc = 0.60_f32; let b = a * (1.0 - ecc * ecc).sqrt(); let focus_offset = a * ecc;
let orbit_steps = (w * 4).max(64);
let mut prev: Option<(i32, i32)> = None;
for s in 0..=orbit_steps {
let e_ang = s as f32 / orbit_steps as f32 * 2.0 * PI;
let px = (cx - focus_offset + a * e_ang.cos()) as i32;
let py = (cy + b * e_ang.sin()) as i32;
draw::dot_i(grid, px, py);
if let Some((lx, ly)) = prev {
line(grid, lx, ly, px, py);
}
prev = Some((px, py));
}
let star_x = (cx - focus_offset) as i32;
let star_y = cy as i32;
draw::dot_i(grid, star_x, star_y);
draw::dot_i(grid, star_x - 1, star_y);
draw::dot_i(grid, star_x + 1, star_y);
draw::dot_i(grid, star_x, star_y - 1);
draw::dot_i(grid, star_x, star_y + 1);
let period = 6.0_f32; let m_anim = 2.0 * PI * (ctx.time % period.max(0.001)) / period.max(0.001);
let e_anim = {
let mut e = m_anim;
for _ in 0..8 {
e = e - (e - ecc * e.sin() - m_anim) / (1.0 - ecc * e.cos());
}
e
};
let planet_x = (cx - focus_offset + a * e_anim.cos()) as i32;
let planet_y = (cy + b * e_anim.sin()) as i32;
line(grid, star_x, star_y, planet_x, planet_y);
for dy in -1_i32..=1 {
for dx in -1_i32..=1 {
draw::dot_i(grid, planet_x + dx, planet_y + dy);
}
}
let (cw, ch) = grid.dimensions();
let filled_cells = (ctx.eased * cw as f32).round() as usize;
for cell_x in 0..filled_cells.min(cw) {
let t = cell_x as f32 / cw as f32;
let col = ctx.palette.sample(t);
for cell_y in 0..ch {
draw::tint_row(grid, cell_y, cell_x, cell_x, col);
}
}
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/τ)) — speed curve fills as object approaches drag limit"
}
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 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 graph_w = (w - ax_x - 1) 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));
}
let t_now_anim = (ctx.time % t_max.max(0.001)) * 0.8;
let y_pos_norm = (t_now_anim / tau - (1.0 - (-t_now_anim / tau).exp()))
/ (t_max / tau - 1.0 + (-t_max / tau).exp()).max(1e-6);
let obj_x = (wf * 0.92) as i32;
let obj_y = (y_pos_norm.clamp(0.0, 1.0) * (ax_y as f32 - 2.0) + 1.0) as i32;
for dy in 0_i32..=2 {
for dx in -1_i32..=1 {
draw::dot_i(grid, obj_x + dx, obj_y + dy);
}
}
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: 1/r potential funnel in dot-lines, test particle spirals inward 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) as i32;
let top = 0_i32;
let depth_max = (hf * 0.85).max(2.0);
let k = depth_max * (wf / 2.0 - 2.0);
let mut prev_l: Option<(i32, i32)> = None;
let mut prev_r: Option<(i32, i32)> = None;
for xi in 1..=(w / 2).max(1) {
let xf = xi as f32;
let depth = (k / xf).min(depth_max);
let py = (top as f32 + hf - 1.0 - depth) as i32;
let py = py.clamp(top, (h as i32) - 1);
let left_x = cx - xi as i32;
let right_x = cx + xi as i32;
draw::dot_i(grid, right_x, py);
draw::dot_i(grid, left_x, py);
if let Some((lx, ly)) = prev_r {
line(grid, lx, ly, right_x, py);
}
if let Some((lx, ly)) = prev_l {
line(grid, lx, ly, left_x, py);
}
prev_r = Some((right_x, py));
prev_l = Some((left_x, py));
}
let well_bottom = (top as f32 + hf - 1.0) as i32;
draw::dot_i(grid, cx - 1, well_bottom);
draw::dot_i(grid, cx, well_bottom);
draw::dot_i(grid, cx + 1, well_bottom);
let r_max = wf / 2.0 - 2.0;
let r_now = r_max * (1.0 - ctx.eased);
let azimuth = ctx.time * 4.0 * PI;
let spiral_steps = 32_usize;
let mut prev_sp: Option<(i32, i32)> = None;
for s in 0..spiral_steps {
let frac = s as f32 / spiral_steps as f32;
let angle = azimuth - frac * PI * 0.5; let r_s = r_now + frac * r_max * 0.25;
let depth = (k / r_s.max(1.0)).min(depth_max);
let py_s = (top as f32 + hf - 1.0 - depth) as i32;
let px_s = cx + (r_s * angle.cos()) as i32;
let py_s = py_s.clamp(top, h as i32 - 1);
draw::dot_i(grid, px_s, py_s);
if let Some((lx, ly)) = prev_sp {
line(grid, lx, ly, px_s, py_s);
}
prev_sp = Some((px_s, py_s));
}
let px_now = cx + (r_now * azimuth.cos()) as i32;
let depth_now = (k / r_now.max(1.0)).min(depth_max);
let py_now = (top as f32 + hf - 1.0 - depth_now) as i32;
let py_now = py_now.clamp(top, h as i32 - 1);
for dy in -1_i32..=1 {
for dx in -1_i32..=1 {
draw::dot_i(grid, px_now + dx, py_now + dy);
}
}
let (cw, ch) = grid.dimensions();
let filled_cells = (ctx.eased * cw as f32).round() as usize;
for cell_x in 0..filled_cells.min(cw) {
let t = cell_x as f32 / cw as f32;
let col = ctx.palette.sample(t);
for cell_y in 0..ch {
draw::tint_row(grid, cell_y, cell_x, cell_x, col);
}
}
Ok(())
}
}
struct LightCone;
impl ProgressStyle for LightCone {
fn name(&self) -> &str {
"light-cone"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Spacetime light cone: 45° null worldlines + timelike particle path drawn by 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 origin_x = (wf / 2.0) as i32;
let origin_y = (hf - 2.0) as i32;
let scale_x = 1_i32; for step in 0..h as i32 {
let dt = step;
let right_x = origin_x + dt * scale_x;
let left_x = origin_x - dt * scale_x;
let cone_y = origin_y - dt;
if cone_y >= 0 {
draw::dot_i(grid, right_x, cone_y);
draw::dot_i(grid, left_x, cone_y);
}
}
for step in 1..4_i32 {
let dt = step;
let right_x = origin_x + dt * scale_x;
let left_x = origin_x - dt * scale_x;
let cone_y = origin_y + dt;
draw::dot_i(grid, right_x, cone_y);
draw::dot_i(grid, left_x, cone_y);
}
draw::hline(grid, 0, w.saturating_sub(1), origin_y as usize);
draw::vline(grid, origin_x as usize, 0, h.saturating_sub(1));
let amp_p = (wf / 6.0).max(1.0);
let omega_p = 2.0 * PI / (hf.max(1.0));
let tau_max = (ctx.eased * (h as f32 - 2.0)) as i32;
let drift = ctx.time * 0.3;
let mut prev_wp: Option<(i32, i32)> = None;
for tau in 0..=tau_max {
let tauf = tau as f32;
let px = (origin_x as f32 + amp_p * (omega_p * tauf + drift).sin()) as i32;
let py = origin_y as i32 - tau;
if py >= 0 {
draw::dot_i(grid, px, py);
if let Some((lx, ly)) = prev_wp {
line(grid, lx, ly, px, py);
}
prev_wp = Some((px, py));
}
}
if let Some((lx, ly)) = prev_wp {
for dy in -1_i32..=1 {
for dx in -1_i32..=1 {
draw::dot_i(grid, lx + dx, ly + dy);
}
}
}
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 DoubleSlit;
impl ProgressStyle for DoubleSlit {
fn name(&self) -> &str {
"double-slit"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Double-slit interference: fringe pattern accumulates as particle count 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 barrier_x = (wf * 0.30) as usize;
let screen_x = (wf * 0.95) as usize;
let slit1_y = (hf * 0.33) as usize;
let slit2_y = (hf * 0.67) as usize;
let slit_h = (hf * 0.08).max(1.0) as usize;
for y in 0..h {
let in_slit1 = y >= slit1_y && y < slit1_y + slit_h;
let in_slit2 = y >= slit2_y && y < slit2_y + slit_h;
if !in_slit1 && !in_slit2 {
draw::dot(grid, barrier_x.min(w.saturating_sub(1)), y);
}
}
let slit_sep = (slit2_y as f32 - slit1_y as f32 + slit_h as f32).max(1.0);
let lambda = slit_sep * 0.6; let dist_l = (screen_x as f32 - barrier_x as f32).max(1.0);
let y_c = hf / 2.0;
for yi in 0..h {
let yf = yi as f32;
let sin_t = (yf - y_c) / (dist_l * dist_l + (yf - y_c) * (yf - y_c)).sqrt();
let phase = PI * slit_sep * sin_t / lambda;
let intensity = phase.cos() * phase.cos(); let threshold = (1.0 - ctx.eased) * 0.98;
if intensity > threshold {
let sx = screen_x.min(w.saturating_sub(1));
draw::dot(grid, sx, yi);
if intensity > 0.7 && sx > 0 {
draw::dot(grid, sx.saturating_sub(1), yi);
}
}
}
let flight = (ctx.time * 0.7).fract();
let part_x = (flight * screen_x as f32) as i32;
let slit_cy = hf / 2.0;
let part_y = slit_cy as i32;
draw::dot_i(grid, part_x, part_y);
draw::dot_i(grid, part_x, part_y - 1);
draw::dot_i(grid, part_x, part_y + 1);
draw::vline(grid, 0, h / 3, 2 * h / 3);
let (cw, ch) = grid.dimensions();
let screen_cell = screen_x / 2;
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);
}
}
let _ = screen_cell;
Ok(())
}
}
struct Doppler;
impl ProgressStyle for Doppler {
fn name(&self) -> &str {
"doppler"
}
fn theme(&self) -> &str {
"physics"
}
fn describe(&self) -> &str {
"Doppler effect: expanding wavefronts compress ahead/stretch behind a moving source"
}
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 = wf / 3.0; let v_src = ctx.eased * c_sound * 0.85; let lambda = c_sound * 0.15; let period = lambda / c_sound; let n_waves = 7_usize;
let loop_dur = (n_waves as f32 * period).max(0.001);
let t_phase = ctx.time % loop_dur;
let src_x = v_src * t_phase;
let src_y = hf / 2.0;
for n in 0..n_waves {
let nf = n as f32;
let t_emit = nf * period; let age = t_phase - t_emit;
if age <= 0.0 {
continue;
}
let radius = c_sound * age; let emit_x = v_src * t_emit;
if radius < 0.5 {
continue;
}
let circ_steps = ((radius * PI * 2.0) as usize + 8).max(8).min(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 as i32;
for dy in -1_i32..=1 {
for dx in -1_i32..=1 {
draw::dot_i(grid, src_ix + dx, src_iy + dy);
}
}
let arr_y = (src_iy - 3).max(0);
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(())
}
}