Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add blend subcommand #1

Draft
wants to merge 1 commit into
base: feat/transparency
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/blend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::{Color, RGBA};

/// Blend modes that determine how partially transparent source and backdrop colors
/// interact when they overlap. For more information on color blending, see
/// [here](https://www.w3.org/TR/compositing/#blending) and
/// [here](https://en.wikipedia.org/wiki/Blend_modes).
pub trait BlendMode {
fn blend_channel(backdrop: u8, source: u8) -> u8;

fn blend(backdrop: &RGBA<u8>, source: &RGBA<u8>) -> Color {
let r = Self::blend_channel(backdrop.r, source.r);
let g = Self::blend_channel(backdrop.g, source.g);
let b = Self::blend_channel(backdrop.b, source.b);
let a = (backdrop.alpha + source.alpha) / 2.0;

Color::from_rgba(r, g, b, a)
}
}

pub struct Multiply;
pub struct Screen;
pub struct Overlay;

impl BlendMode for Multiply {
fn blend_channel(backdrop: u8, source: u8) -> u8 {
((backdrop as f64 * source as f64) / 255.0).floor() as u8
}
}

impl BlendMode for Screen {
fn blend_channel(backdrop: u8, source: u8) -> u8 {
let backdrop = backdrop as f64 / 255.0;
let source = source as f64 / 255.0;
((1.0 - (1.0 - backdrop) * (1.0 - source)) * 255.0) as u8
}
}

impl BlendMode for Overlay {
fn blend_channel(backdrop: u8, source: u8) -> u8 {
let backdrop = backdrop as f64 / 255.0;
let source = source as f64 / 255.0;
if backdrop < 0.5 {
((2.0 * backdrop * source) * 255.0) as u8
} else {
((1.0 - 2.0 * (1.0 - backdrop) * (1.0 - source)) * 255.0) as u8
}
}
}
11 changes: 11 additions & 0 deletions src/cli/blend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use pastel::blend::*;
use pastel::Color;

pub fn get_blending_function(blend_mode: &str) -> Box<dyn Fn(&Color, &Color) -> Color> {
match blend_mode.to_lowercase().as_ref() {
"multiply" => Box::new(|b: &Color, s: &Color| b.blend::<Multiply>(s)),
"screen" => Box::new(|b: &Color, s: &Color| b.blend::<Screen>(s)),
"overlay" => Box::new(|b: &Color, s: &Color| b.blend::<Overlay>(s)),
_ => unreachable!("Unknown blend mode"),
}
}
26 changes: 26 additions & 0 deletions src/cli/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,32 @@ pub fn build_cli() -> App<'static, 'static> {
)
.arg(color_arg.clone()),
)
.subcommand(
SubCommand::with_name("blend")
.about("Blend two colors using the given blend mode")
.long_about(
"Create new colors by blending two colors using the given blend mode.\n\n\
Example:\n \
pastel blend --blend_mode=multiply red blue")
.arg(
Arg::with_name("blend_mode")
.long("blend_mode")
.short("b")
.value_name("name")
.help("The blend mode to use")
.possible_values(&["multiply", "screen", "overlay"])
.case_insensitive(true)
.default_value("multiply")
.required(true)
)
.arg(
Arg::with_name("base")
.value_name("backdrop")
.help("The backdrop color on which the other color will be blended.")
.required(true),
)
.arg(color_arg.clone().value_name("source").multiple(false)),
)
.subcommand(
SubCommand::with_name("colorblind")
.about("Simulate a color under a certain colorblindness profile")
Expand Down
21 changes: 21 additions & 0 deletions src/cli/commands/color_commands.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::blend::get_blending_function;
use crate::colorspace::get_mixing_function;
use crate::commands::prelude::*;
use crate::output::Blend;

use pastel::ColorblindnessType;
use pastel::Fraction;
Expand Down Expand Up @@ -77,6 +79,25 @@ color_command!(MixCommand, config, matches, color, {
mix(&base, color, fraction)
});

color_command!(BlendCommand, config, matches, color, {
let mut print_spectrum = PrintSpectrum::Yes;

let base = ColorArgIterator::from_color_arg(
config,
matches.value_of("base").expect("required argument"),
&mut print_spectrum,
)?;

let blend = get_blending_function(matches.value_of("blend_mode").expect("required argument"));

// FIXME: maybe get rid of clones?
Blend {
backdrop: base.clone(),
source: color.clone(),
output: blend(&base, color),
}
});

color_command!(ColorblindCommand, config, matches, color, {
// The type of colorblindness selected (protanopia, deuteranopia, tritanopia)
let cb_ty = matches.value_of("type").expect("required argument");
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl Command {
"paint" => Command::Generic(Box::new(PaintCommand)),
"format" => Command::WithColor(Box::new(FormatCommand)),
"colorcheck" => Command::Generic(Box::new(ColorCheckCommand)),
"blend" => Command::WithColor(Box::new(color_commands::BlendCommand)),
_ => unreachable!("Unknown subcommand"),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::io::{self, Write};

use atty::Stream;

mod blend;
mod cli;
mod colorpicker;
mod colorpicker_tools;
Expand Down
61 changes: 55 additions & 6 deletions src/cli/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,46 @@ pub struct Output<'a> {
colors_shown: usize,
}

pub trait ColorBlock {
fn primary_color(&self) -> &Color;
fn paint(&self, canvas: &mut Canvas, row: usize, col: usize, height: usize, width: usize);
}

pub struct Blend {
pub backdrop: Color,
pub source: Color,
pub output: Color,
}

impl ColorBlock for Color {
fn primary_color(&self) -> &Color {
self
}

fn paint(&self, canvas: &mut Canvas, row: usize, col: usize, height: usize, width: usize) {
canvas.draw_rect(row, col, height, width, self);
}
}

impl ColorBlock for Blend {
fn primary_color(&self) -> &Color {
&self.output
}

fn paint(&self, canvas: &mut Canvas, row: usize, col: usize, height: usize, width: usize) {
// draw backdrop
canvas.draw_rect(row, col, 1, width - 1, &self.backdrop);
canvas.draw_rect(row + 1, col, height - 2, 1, &self.backdrop);

// draw source
canvas.draw_rect(height + 1, col + 1, 1, width - 1, &self.source);
canvas.draw_rect(row + 1, col + width - 1, height - 2, 1, &self.source);

// draw overlap
canvas.draw_rect(row + 1, col + 1, height - 2, width - 2, &self.output);
}
}

impl Output<'_> {
pub fn new(handle: &mut dyn Write) -> Output {
Output {
Expand All @@ -22,7 +62,12 @@ impl Output<'_> {
}
}

pub fn show_color_tty(&mut self, config: &Config, color: &Color) -> Result<()> {
pub fn show_color_tty<C: ColorBlock>(
&mut self,
config: &Config,
color_block: &C,
) -> Result<()> {
let color = color_block.primary_color();
let checkerboard_size: usize = 16;
let color_panel_size: usize = 12;

Expand All @@ -44,12 +89,12 @@ impl Output<'_> {
&Color::graytone(0.94),
&Color::graytone(0.71),
);
canvas.draw_rect(
color_block.paint(
&mut canvas,
color_panel_position_y,
color_panel_position_x,
color_panel_size,
color_panel_size,
color,
);

let mut text_y_offset = 0;
Expand Down Expand Up @@ -102,15 +147,19 @@ impl Output<'_> {
canvas.print(self.handle)
}

pub fn show_color(&mut self, config: &Config, color: &Color) -> Result<()> {
pub fn show_color<C: ColorBlock>(&mut self, config: &Config, color_block: &C) -> Result<()> {
if config.interactive_mode {
if self.colors_shown < 1 {
writeln!(self.handle)?
};
self.show_color_tty(config, color)?;
self.show_color_tty(config, color_block)?;
writeln!(self.handle)?;
} else {
writeln!(self.handle, "{}", color.to_hsl_string(Format::NoSpaces))?;
writeln!(
self.handle,
"{}",
color_block.primary_color().to_hsl_string(Format::NoSpaces)
)?;
}
self.colors_shown += 1;

Expand Down
13 changes: 13 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod ansi;
pub mod blend;
pub mod colorspace;
pub mod delta_e;
pub mod distinct;
Expand All @@ -10,6 +11,7 @@ mod types;

use std::fmt;

use blend::BlendMode;
use colorspace::ColorSpace;
pub use helper::Fraction;
use helper::{clamp, interpolate, interpolate_angle, mod_positive};
Expand Down Expand Up @@ -620,6 +622,17 @@ impl Color {

Color::from_rgba(r, g, b, a)
}

/// Blend two colors, using the first as the backdrop and the second as the source.
/// The result is modulated by the backdrop alpha. A fully opaque backdrop will use
/// the blending function exclusively, while a partially or fully transparent
/// backdrop causes the result to be a weighted average between the source color
/// and the blended color.
pub fn blend<B: BlendMode>(&self, source: &Color) -> Color {
let backdrop = self.to_rgba();
let source = source.to_rgba();
B::blend(&backdrop, &source)
}
}

// by default Colors will be printed into HSLA fromat
Expand Down