diff options
author | metamuffin <yvchraiqi@protonmail.com> | 2022-06-05 14:52:50 +0200 |
---|---|---|
committer | metamuffin <yvchraiqi@protonmail.com> | 2022-06-05 14:52:50 +0200 |
commit | ca3536ce691e8726ceebbf6c058bd009ebab62ab (patch) | |
tree | 0708344fd2fbf048226e875570c6c2c5a273f87a | |
download | blubcat-ca3536ce691e8726ceebbf6c058bd009ebab62ab.tar blubcat-ca3536ce691e8726ceebbf6c058bd009ebab62ab.tar.bz2 blubcat-ca3536ce691e8726ceebbf6c058bd009ebab62ab.tar.zst |
initial
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 16 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | src/color.rs | 101 | ||||
-rw-r--r-- | src/main.rs | 225 |
5 files changed, 351 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..204d819 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" + +[[package]] +name = "blubcat" +version = "0.1.0" +dependencies = [ + "anyhow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1ac9ea0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "blubcat" +version = "0.1.0" +edition = "2021" + + +[dependencies] +anyhow = "1.0.57" diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..6ade373 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,101 @@ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Color { + pub r: f64, + pub g: f64, + pub b: f64, +} + +impl Color { + pub fn rgb(r: f64, g: f64, b: f64) -> Self { + Self { r, g, b } + } + + pub fn parse(c: &str) -> Option<Self> { + match c { + "red" => Some(Color::RED), + "green" => Some(Color::GREEN), + "blue" => Some(Color::BLUE), + "yellow" => Some(Color::YELLOW), + "cyan" => Some(Color::CYAN), + "magenta" => Some(Color::MAGENTA), + "black" => Some(Color::BLACK), + "white" => Some(Color::WHITE), + _ => { + if c.len() == 6 { + Some(Self::from_rgb888( + u8::from_str_radix(&c[0..2], 16).ok()?, + u8::from_str_radix(&c[2..4], 16).ok()?, + u8::from_str_radix(&c[4..6], 16).ok()?, + )) + } else { + None + } + } + } + } + + pub fn as_rgb888(&self) -> (u8, u8, u8) { + ( + (self.r * 255.0) as u8, + (self.g * 255.0) as u8, + (self.b * 255.0) as u8, + ) + } + pub fn from_rgb888(r: u8, g: u8, b: u8) -> Self { + Color { + r: r as f64 / 255.0, + g: g as f64 / 255.0, + b: b as f64 / 255.0, + } + } + pub const RED: Color = Color { + r: 1.0, + g: 0.0, + b: 0.0, + }; + pub const GREEN: Color = Color { + r: 0.0, + g: 1.0, + b: 0.0, + }; + pub const BLUE: Color = Color { + r: 0.0, + g: 0.0, + b: 1.0, + }; + pub const YELLOW: Color = Color { + r: 1.0, + g: 1.0, + b: 0.0, + }; + pub const CYAN: Color = Color { + r: 0.0, + g: 1.0, + b: 1.0, + }; + pub const MAGENTA: Color = Color { + r: 1.0, + g: 0.0, + b: 1.0, + }; + + pub const BLACK: Color = Color { + r: 0.0, + g: 0.0, + b: 0.0, + }; + pub const WHITE: Color = Color { + r: 1.0, + g: 1.0, + b: 1.0, + }; + + pub fn mix(a: Color, b: Color, x: f64) -> Color { + let y = 1.0 - x; + Color { + r: a.r * y + b.r * x, + g: a.g * y + b.g * x, + b: a.b * y + b.b * x, + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..757b842 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,225 @@ +#![feature(box_syntax)] +pub mod color; + +use anyhow::Result; +use color::Color; +use std::{ + env, + f64::consts::PI, + fs::File, + io::{stdin, BufRead, BufReader, Read}, + process::exit, +}; + +static HELP: &str = " +Usage: blubcat PATTERN [FILE]... +Concatenate FILE(s) to standard output, colorizing the output according to OPTION(s). + +With no FILE, or when FILE is -, the standard input is read. +If PATTERN is repeated only the last one will be used. + +PATTERN: + -R --rainbow Rainbow + -S --sequence <c1,c2,c3,...> Sequences colors on the X-axis + -G --gradient <c1,c2,c3,...> Interpolates the colors on the X-axis + <PATTERN> <TRANSFORM> Apply TRANSFORM to PATTERN + <PRESET> + +TRANSFORM: + -r --rotate <angle> + -s --scale <factor> + -m --matrix <x> <y> <z> <w> + +PRESET: + -P --pride Pride flag + +COLOR: + <rrggbb> + black white red green blue yellow cyan magenta + +"; + +fn main() -> Result<()> { + let mut args = env::args().skip(1); + + let mut pat: Box<dyn Sample2D> = box Solid(Color::WHITE); + + loop { + let a = args.next(); + let mut arg_next = || args.next(); + let mut arg_num = || { + arg_next() + .expect("value expected") + .parse::<f64>() + .expect("not a number") + }; + + if let Some(a) = a { + if !a.starts_with("-") || a == "--" || a == "-" { + break; + } + match a.as_str() { + "-?" | "--help" => { + println!("{}", HELP); + exit(0) + } + + /* PRESETS */ + "-P" | "--pride" => { + pat = box Transform { + matrix: ((1.0 / 12.0, 1.0 / 12.0), (0.0, 0.0)), + inner: box Extend(box Sequence(vec![ + Color::parse("e50000").unwrap(), + Color::parse("ff8d00").unwrap(), + Color::parse("ffee00").unwrap(), + Color::parse("028121").unwrap(), + Color::parse("004cff").unwrap(), + Color::parse("770088").unwrap(), + ])), + } + } + + /* PATTERNS */ + "-R" | "--rainbow" => pat = box Extend(box Rainbow), + "-S" | "--sequence" => { + pat = box Extend(box Sequence( + arg_next() + .unwrap() + .split(",") + .map(|e| Color::parse(e).expect("color invalid")) + .collect::<Vec<_>>(), + )) + } + "-G" | "--gradient" => { + pat = box Extend(box Gradient( + arg_next() + .unwrap() + .split(",") + .map(|e| Color::parse(e).expect("color invalid")) + .collect::<Vec<_>>(), + )) + } + + /* TRANSFORMS */ + "-s" | "--scale" => { + let fac = arg_num(); + pat = box Transform { + inner: pat, + matrix: ((fac, 0.0), (0.0, fac)), + } + } + "-r" | "--rotate" => { + let angle = arg_num() * PI * 2.0; + pat = box Transform { + inner: pat, + matrix: ((angle.cos(), -angle.sin()), (angle.sin(), angle.cos())), + } + } + "-m" | "--matrix" => { + pat = box Transform { + inner: pat, + matrix: ((arg_num(), arg_num()), (arg_num(), arg_num())), + } + } + _ => panic!("unknown option {}", &a), + } + } else { + break; + } + } + + let inputs = args.collect::<Vec<String>>(); + + if inputs.len() == 0 { + colorize(stdin(), &mut pat)? + } else { + for f in inputs { + let file = File::open(f)?; + colorize(file, &mut pat)?; + } + } + Ok(()) +} + +fn colorize(file: impl Read, sampler: &mut Box<dyn Sample2D>) -> Result<()> { + let mut file = BufReader::new(file); + let mut line = String::new(); + for y in 0.. { + if file.read_line(&mut line)? == 0 { + break; + } + for (x, c) in line.chars().enumerate() { + let (r, g, b) = sampler.sample(x as f64, y as f64).as_rgb888(); + print!("\x1b[38;2;{r};{g};{b}m{c}") + } + line.clear(); + } + Ok(()) +} + +trait Sample2D { + fn sample(&mut self, x: f64, y: f64) -> Color; +} +trait Sample1D { + fn sample(&mut self, x: f64) -> Color; +} + +struct Extend(Box<dyn Sample1D>); +impl Sample2D for Extend { + fn sample(&mut self, x: f64, _y: f64) -> Color { + self.0.sample(x) + } +} + +struct Transform { + pub inner: Box<dyn Sample2D>, + pub matrix: ((f64, f64), (f64, f64)), +} +impl Sample2D for Transform { + fn sample(&mut self, x: f64, y: f64) -> Color { + let (x, y) = ( + x * self.matrix.0 .0 + y * self.matrix.0 .1, + x * self.matrix.1 .0 + y * self.matrix.1 .1, + ); + self.inner.sample(x, y) + } +} + +struct Rainbow; +impl Sample1D for Rainbow { + fn sample(&mut self, x: f64) -> Color { + static PI: f64 = 3.14; + let x = x * PI * 2.0; + return Color { + r: (x + PI / 3.0 * 0.0).sin() * 0.5 + 0.5, + g: (x + PI / 3.0 * 2.0).sin() * 0.5 + 0.5, + b: (x + PI / 3.0 * 4.0).sin() * 0.5 + 0.5, + }; + } +} + +struct Sequence(pub Vec<Color>); +impl Sample1D for Sequence { + fn sample(&mut self, x: f64) -> Color { + self.0[(x * self.0.len() as f64).floor() as usize % self.0.len()] + } +} + +struct Gradient(pub Vec<Color>); +impl Sample1D for Gradient { + fn sample(&mut self, x: f64) -> Color { + let index = x * self.0.len() as f64; + let index_int = index.floor() as usize; + let index_error = index % 1.0; + let a = self.0[index_int % self.0.len()]; + let b = self.0[(index_int + 1) % self.0.len()]; + Color::mix(a, b, index_error) + } +} + +struct Solid(Color); +impl Sample2D for Solid { + fn sample(&mut self, _x: f64, _y: f64) -> Color { + self.0 + } +} |