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<()> | 
