diff options
-rw-r--r-- | src/lib.rs | 99 | ||||
-rw-r--r-- | src/vtcol.rs | 99 |
2 files changed, 195 insertions, 3 deletions
@@ -2,7 +2,8 @@ use std::{convert::TryFrom, fmt, io::{self, BufWriter, Error, Write}, os::unix::io::{AsRawFd, RawFd}, - path::{Path, PathBuf}}; + path::{Path, PathBuf}, + time::{Duration, Instant}}; trait IsMinusOne { @@ -776,3 +777,99 @@ impl fmt::Display for Console write!(f, "Console(fd={})", self.0) } } + +/* Use doubles for fractional scaling. */ +struct FadePalette([f64; PALETTE_SIZE * 3]); + +impl FadePalette +{ + fn new() -> Self { Self([0f64; PALETTE_SIZE * 3]) } + + /** Linear interpolation between self and ``goal``. + + We scale each byte individually so there can be a maximum + of 256 interpolation steps. + */ + fn towards(&self, goal: &FadePalette, progress: f64) -> FadePalette + { + let mut res = FadePalette::new(); + + for (i, (a, b)) in self.0.iter().zip(goal.0.iter()).enumerate() { + res.0[i] = a + (b - a) * progress; + } + + res + } +} + +impl From<&Palette> for FadePalette +{ + fn from(pal: &Palette) -> Self + { + let mut fpal = Self::new(); + pal.0.iter().enumerate().for_each(|(i, &b)| { + fpal.0[i] = b as f64; + }); + fpal + } +} + +impl From<&FadePalette> for Palette +{ + fn from(fpal: &FadePalette) -> Self + { + let mut pal = Self::new(); + fpal.0.iter().enumerate().for_each(|(i, &b)| { + let b = if b < 0f64 { + 0 + } else if 256f64 <= b { + 255 + } else { + b.round() as u8 + }; + pal.0[i] = b; + }); + pal + } +} + +pub struct Fade +{ + from: Palette, + to: Palette, + hz: u8, + duration: Duration, +} + +impl Fade +{ + pub fn new(from: Palette, to: Palette, duration: Duration, hz: u8) -> Self + { + let hz = if hz == 0 { 1 } else { hz }; + Self { from, to, hz, duration } + } + + pub fn commence(self, con: &Console) -> io::Result<()> + { + let Self { from, to, hz, duration } = self; + con.apply_palette(&from)?; + + let fade = FadePalette::from(&con.current_palette()?); + let fade_to = FadePalette::from(&to); + let t_0 = Instant::now(); + let tick = Duration::from_millis(1_000u64 / hz as u64); + let iters = (duration.as_millis() / tick.as_millis()) as u32; + + let mut i = 0; + while i < iters { + i += 1; + let progress = f64::from(i) / f64::from(iters); + let pal = Palette::from(&fade.towards(&fade_to, progress)); + con.apply_palette(&pal)?; + let next = i * tick; + std::thread::sleep(next.saturating_sub(t_0.elapsed())); + } + + Ok(()) + } +} diff --git a/src/vtcol.rs b/src/vtcol.rs index b0c4b85..3694753 100644 --- a/src/vtcol.rs +++ b/src/vtcol.rs @@ -1,13 +1,17 @@ pub mod lib; -use vtcol::{Console, Palette, Scheme}; +use vtcol::{Console, Fade, Palette, Scheme}; use anyhow::{anyhow, Result}; use std::{io::{self, BufWriter}, - sync::atomic::{AtomicBool, Ordering}}; + sync::atomic::{AtomicBool, Ordering}, + time::Duration}; static VERBOSITY: AtomicBool = AtomicBool::new(false); +const DEFAULT_FADE_DURATION_MS: u64 = 500; +const DEFAULT_FADE_UPDATE_HZ: u8 = 25; + macro_rules! vrb { ( $( $e:expr ),* ) => {( if VERBOSITY.load(Ordering::SeqCst) { println!( $( $e ),* ) } @@ -29,6 +33,8 @@ enum Job Get, /** Toggle between two schemes. */ Toggle(Scheme, Scheme), + /** Fade from current scheme to another. */ + Fade(Option<Scheme>, Scheme, Duration, u8), } impl<'a> Job @@ -91,6 +97,43 @@ impl<'a> Job .takes_value(true), ), ) + .subcommand( + SubCommand::with_name("fade") + .about("fade from one scheme to another") + .arg( + Arg::with_name("from") + .short("f") + .long("from") + .value_name("NAME1") + .help("initial color scheme (default: current)") + .takes_value(true), + ) + .arg( + Arg::with_name("to") + .short("t") + .long("to") + .value_name("NAME2") + .help("final color scheme") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("ms") + .value_name("MS") + .short("m") + .long("ms") + .help("how long (in ms) the fade should take") + .takes_value(true), + ) + .arg( + Arg::with_name("frequency") + .value_name("HZ") + .short("h") + .long("frequency") + .help("rate (HZ/s) of intermediate scheme changes") + .takes_value(true), + ), + ) .arg( Arg::with_name("verbose") .short("v") @@ -148,6 +191,31 @@ impl<'a> Job )), } }, + ("fade", Some(subm)) => { + let dur: u64 = if let Some(ms) = subm.value_of("ms") { + ms.parse()? + } else { + DEFAULT_FADE_DURATION_MS + }; + let hz: u8 = if let Some(ms) = subm.value_of("frequency") { + ms.parse()? + } else { + DEFAULT_FADE_UPDATE_HZ + }; + let dur = Duration::from_millis(dur); + + match (subm.value_of("from"), subm.value_of("to")) { + (_, None) => + Err(anyhow!("please supply color scheme to fade to")), + (from, Some(to)) => + Ok(Self::Fade( + from.map(Scheme::from), + Scheme::from(to), + dur, + hz, + )), + } + }, (junk, _) => Err(anyhow!( "invalid subcommand [{}]; try ``{} --help``", @@ -212,6 +280,7 @@ impl<'a> Job Self::Set(scm) => Self::set_scheme(scm)?, Self::Get => Self::get_scheme()?, Self::Toggle(one, two) => Self::toggle_scheme(one, two)?, + Self::Fade(from, to, ms, hz) => Self::fade(from, to, ms, hz)?, } Ok(()) @@ -257,6 +326,32 @@ impl<'a> Job Self::set_scheme(one) } } + + /** Fade from one scheme to another. + + If ``from`` is ``None``, the current palette is used as starting point. */ + fn fade( + from: Option<Scheme>, + to: Scheme, + dur: Duration, + hz: u8, + ) -> Result<()> + { + let fd = Console::current()?; + vrb!("console fd: {}", fd); + + let from = if let Some(from) = from { + Palette::try_from(&from)? + } else { + fd.current_palette()? + }; + let to = Palette::try_from(&to)?; + + let fade = Fade::new(from, to, dur, hz); + + fade.commence(&fd)?; + Ok(()) + } } /* [impl Job] */ fn main() -> Result<()> |