use std::{ cmp::min, ops::{Add, Mul, Range}, time::Duration, }; use surf_n_term::{ view::{ViewContext, ViewLayoutStore}, Color, Error, Image, LinColor, Size, Surface, SurfaceOwned, SystemTerminal, Terminal, TerminalAction, TerminalCommand, TerminalEvent, TerminalSurfaceExt, }; #[derive(Copy, Clone, Default)] struct Complex { x: f64, y: f64, } impl Complex { fn sabs(self) -> f64 { self.x * self.x + self.y * self.y } } impl Mul for Complex { type Output = Self; fn mul(self, other: Self) -> Self::Output { Complex { x: self.x * other.x - self.y * other.y, y: self.x * other.y + self.y * other.x, } } } impl Add for Complex { type Output = Self; fn add(self, other: Self) -> Self::Output { Complex { x: self.x + other.x, y: self.y + other.y, } } } fn mandelbrot_at(c: Complex, max_iter: usize) -> usize { (0..max_iter) .try_fold(Complex::default(), |z, i| { if z.sabs() >= 4.0 { Err(i) } else { Ok(z * z + c) } }) .err() .unwrap_or(max_iter) } impl Default for ColorMap { fn default() -> Self { let colors = vec![ LinColor::new(0.001462, 0.000466, 0.013866, 1.0), LinColor::new(0.039608, 0.03109, 0.133515, 1.0), LinColor::new(0.113094, 0.065492, 0.276784, 1.0), LinColor::new(0.211718, 0.061992, 0.418647, 1.0), LinColor::new(0.316654, 0.07169, 0.48538, 1.0), LinColor::new(0.414709, 0.110431, 0.504662, 1.0), LinColor::new(0.512831, 0.148179, 0.507648, 1.0), LinColor::new(0.613617, 0.181811, 0.498536, 1.0), LinColor::new(0.716387, 0.214982, 0.47529, 1.0), LinColor::new(0.816914, 0.255895, 0.436461, 1.0), LinColor::new(0.904281, 0.31961, 0.388137, 1.0), LinColor::new(0.960949, 0.418323, 0.35963, 1.0), LinColor::new(0.9867, 0.535582, 0.38221, 1.0), LinColor::new(0.996096, 0.653659, 0.446213, 1.0), LinColor::new(0.996898, 0.769591, 0.534892, 1.0), LinColor::new(0.99244, 0.88433, 0.640099, 1.0), ]; Self { colors } } } struct ColorMap { colors: Vec, } impl ColorMap { fn lookup(&self, value: f64) -> LinColor { let offset = value.clamp(0.0, 1.0) * (self.colors.len() - 1) as f64; let index = offset.floor() as usize; let fract = offset.fract(); let start = self.colors[index]; let end = self.colors[min(index + 1, self.colors.len() - 1)]; start.lerp(end, fract as f32) } } fn mandelbrot(xs: Range, ys: Range, size: Size, max_iter: usize) -> SurfaceOwned { let height = (size.height - 1) as f64; let width = (size.width - 1) as f64; SurfaceOwned::new_with(size, |pos| { let x = lerp(&xs, (pos.col as f64 + 0.5) / width); let y = lerp(&ys, (pos.row as f64 + 0.5) / height); mandelbrot_at(Complex { x, y }, max_iter) }) } fn mandlebrot_imgs( mand: impl Surface, colormap: &ColorMap, count: usize, ) -> Vec { let mut imgs = Vec::new(); for iter in 0..count { let surf = SurfaceOwned::new_with(mand.size(), |pos| { let ratio = min(*mand.get(pos).unwrap(), iter) as f64 / count as f64; colormap.lookup(ratio).into() }); imgs.push(Image::from(surf)); } imgs } fn lerp(vs: &Range, t: f64) -> f64 { vs.start * (1.0 - t) + vs.end * t } fn main() -> Result<(), Error> { let f = TerminalEvent::Key("f".parse()?); let q = TerminalEvent::Key("q".parse()?); let ctrl_c = TerminalEvent::Key("ctrl+c".parse()?); let mut term = SystemTerminal::new()?; term.duplicate_output("/tmp/surf_n_term.log")?; // init term.execute_many([ TerminalCommand::CursorSave, TerminalCommand::visible_cursor_set(false), TerminalCommand::altscreen_set(true), ])?; let mut imgs = Vec::new(); let xs = -2.5..1.0; let ys = -1.0..1.0; let colormap = ColorMap::default(); let mut size = Size { height: 58, width: 100, }; let max_iter = 100; // loop let mut index = 0; let mut ascii = true; let delay = Duration::from_millis(16); let mut layout_store = ViewLayoutStore::new(); term.run_render(move |term, event, mut view| -> Result<_, Error> { if imgs.len() < max_iter { let mand = mandelbrot(xs.clone(), ys.clone(), size, max_iter); imgs = mandlebrot_imgs(mand, &colormap, max_iter); }; let ctx = ViewContext::new(term)?; if ascii { view.draw_view(&ctx, Some(&mut layout_store), imgs[index].ascii_view())?; } else { view.draw_view(&ctx, Some(&mut layout_store), &imgs[index])?; } index = (index + 1) % max_iter; // process event if event.as_ref() == Some(&q) || event.as_ref() == Some(&ctrl_c) { Ok(TerminalAction::Quit(())) } else { if event.as_ref() == Some(&f) { ascii = !ascii; size = if ascii { Size { height: 58, width: 100, } } else { Size { height: 486, width: 800, } }; imgs.clear(); } Ok(TerminalAction::Sleep(delay)) } })?; // clean up term.execute_many([ TerminalCommand::altscreen_set(false), TerminalCommand::CursorRestore, ])?; term.poll(Some(Duration::from_millis(50)))?; Ok(()) }