From 3ace97660fde7fe1f0cc07a3925d1114af9a9c2f Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 16 Jul 2024 10:22:03 +0300 Subject: Implement gradient color interpolation option (#548) * Added the better color averaging code (tested & functional) * rustfmt * Make Color f32 0..1, clarify premul/unpremul * Fix imports and test name * Premultiply gradient colors matching CSS * Fix indentation * fixup * Add gradient image --------- Co-authored-by: K's Thinkpad --- niri-config/src/lib.rs | 258 +++++++++++++++++---- niri-visual-tests/src/cases/gradient_angle.rs | 7 +- niri-visual-tests/src/cases/gradient_area.rs | 9 +- niri-visual-tests/src/cases/gradient_oklab.rs | 53 +++++ .../src/cases/gradient_oklab_alpha.rs | 51 ++++ .../src/cases/gradient_oklch_alpha.rs | 53 +++++ .../src/cases/gradient_oklch_decreasing.rs | 53 +++++ .../src/cases/gradient_oklch_increasing.rs | 53 +++++ .../src/cases/gradient_oklch_longer.rs | 53 +++++ .../src/cases/gradient_oklch_shorter.rs | 53 +++++ niri-visual-tests/src/cases/gradient_srgb.rs | 53 +++++ niri-visual-tests/src/cases/gradient_srgb_alpha.rs | 51 ++++ niri-visual-tests/src/cases/gradient_srgblinear.rs | 53 +++++ .../src/cases/gradient_srgblinear_alpha.rs | 51 ++++ niri-visual-tests/src/cases/layout.rs | 4 +- niri-visual-tests/src/cases/mod.rs | 11 + niri-visual-tests/src/cases/tile.rs | 2 +- niri-visual-tests/src/main.rs | 26 ++- resources/default-config.kdl | 1 + src/layout/focus_ring.rs | 15 +- src/layout/tile.rs | 7 +- src/render_helpers/border.rs | 43 +++- src/render_helpers/shaders/border.frag | 174 +++++++++++++- src/render_helpers/shaders/mod.rs | 2 + src/window/mapped.rs | 7 +- wiki/Configuration:-Layout.md | 25 +- wiki/img/gradients-oklch.png | 3 + 27 files changed, 1092 insertions(+), 79 deletions(-) create mode 100644 niri-visual-tests/src/cases/gradient_oklab.rs create mode 100644 niri-visual-tests/src/cases/gradient_oklab_alpha.rs create mode 100644 niri-visual-tests/src/cases/gradient_oklch_alpha.rs create mode 100644 niri-visual-tests/src/cases/gradient_oklch_decreasing.rs create mode 100644 niri-visual-tests/src/cases/gradient_oklch_increasing.rs create mode 100644 niri-visual-tests/src/cases/gradient_oklch_longer.rs create mode 100644 niri-visual-tests/src/cases/gradient_oklch_shorter.rs create mode 100644 niri-visual-tests/src/cases/gradient_srgb.rs create mode 100644 niri-visual-tests/src/cases/gradient_srgb_alpha.rs create mode 100644 niri-visual-tests/src/cases/gradient_srgblinear.rs create mode 100644 niri-visual-tests/src/cases/gradient_srgblinear_alpha.rs create mode 100644 wiki/img/gradients-oklch.png diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index e5998bfb..6396f6dc 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -410,8 +410,8 @@ impl Default for FocusRing { Self { off: false, width: FloatOrInt(4.), - active_color: Color::new(127, 200, 255, 255), - inactive_color: Color::new(80, 80, 80, 255), + active_color: Color::from_rgba8_unpremul(127, 200, 255, 255), + inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255), active_gradient: None, inactive_gradient: None, } @@ -428,6 +428,8 @@ pub struct Gradient { pub angle: i16, #[knuffel(property, default)] pub relative_to: GradientRelativeTo, + #[knuffel(property(name = "in"), str, default)] + pub in_: GradientInterpolation, } #[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -437,6 +439,30 @@ pub enum GradientRelativeTo { WorkspaceView, } +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct GradientInterpolation { + pub color_space: GradientColorSpace, + pub hue_interpolation: HueInterpolation, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum GradientColorSpace { + #[default] + Srgb, + SrgbLinear, + Oklab, + Oklch, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum HueInterpolation { + #[default] + Shorter, + Longer, + Increasing, + Decreasing, +} + #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct Border { #[knuffel(child)] @@ -458,8 +484,8 @@ impl Default for Border { Self { off: true, width: FloatOrInt(4.), - active_color: Color::new(255, 200, 127, 255), - inactive_color: Color::new(80, 80, 80, 255), + active_color: Color::from_rgba8_unpremul(255, 200, 127, 255), + inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255), active_gradient: None, inactive_gradient: None, } @@ -492,23 +518,49 @@ impl From for Border { } } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +/// RGB color in [0, 1] with unpremultiplied alpha. +#[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Color { - pub r: u8, - pub g: u8, - pub b: u8, - pub a: u8, + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, } impl Color { - pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + pub const fn new_unpremul(r: f32, g: f32, b: f32, a: f32) -> Self { Self { r, g, b, a } } -} -impl From for [f32; 4] { - fn from(c: Color) -> Self { - let [r, g, b, a] = [c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.); + pub fn from_rgba8_unpremul(r: u8, g: u8, b: u8, a: u8) -> Self { + Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.)) + } + + pub fn from_array_premul([r, g, b, a]: [f32; 4]) -> Self { + let a = a.clamp(0., 1.); + + if a == 0. { + Self::new_unpremul(0., 0., 0., 0.) + } else { + Self { + r: (r / a).clamp(0., 1.), + g: (g / a).clamp(0., 1.), + b: (b / a).clamp(0., 1.), + a, + } + } + } + + pub fn from_array_unpremul([r, g, b, a]: [f32; 4]) -> Self { + Self { r, g, b, a } + } + + pub fn to_array_unpremul(self) -> [f32; 4] { + [self.r, self.g, self.b, self.a] + } + + pub fn to_array_premul(self) -> [f32; 4] { + let [r, g, b, a] = [self.r, self.g, self.b, self.a]; [r * a, g * a, b * a, a] } } @@ -1429,12 +1481,73 @@ impl CornerRadius { } } +impl FromStr for GradientInterpolation { + type Err = miette::Error; + + fn from_str(s: &str) -> Result { + let mut iter = s.split_whitespace(); + let in_part1 = iter.next(); + let in_part2 = iter.next(); + let in_part3 = iter.next(); + + let Some(in_part1) = in_part1 else { + return Err(miette!("missing color space")); + }; + + let color = match in_part1 { + "srgb" => GradientColorSpace::Srgb, + "srgb-linear" => GradientColorSpace::SrgbLinear, + "oklab" => GradientColorSpace::Oklab, + "oklch" => GradientColorSpace::Oklch, + x => { + return Err(miette!( + "invalid color space {x}; can be srgb, srgb-linear, oklab or oklch" + )) + } + }; + + let interpolation = if let Some(in_part2) = in_part2 { + if color != GradientColorSpace::Oklch { + return Err(miette!("only oklch color space can have hue interpolation")); + } + + if in_part3 != Some("hue") { + return Err(miette!( + "interpolation must end with \"hue\", like \"oklch shorter hue\"" + )); + } else if iter.next().is_some() { + return Err(miette!("unexpected text after hue interpolation")); + } else { + match in_part2 { + "shorter" => HueInterpolation::Shorter, + "longer" => HueInterpolation::Longer, + "increasing" => HueInterpolation::Increasing, + "decreasing" => HueInterpolation::Decreasing, + x => { + return Err(miette!( + "invalid hue interpolation {x}; \ + can be shorter, longer, increasing, decreasing" + )) + } + } + } + } else { + HueInterpolation::default() + }; + + Ok(Self { + color_space: color, + hue_interpolation: interpolation, + }) + } +} + impl FromStr for Color { type Err = miette::Error; fn from_str(s: &str) -> Result { - let [r, g, b, a] = csscolorparser::parse(s).into_diagnostic()?.to_rgba8(); - Ok(Self { r, g, b, a }) + let color = csscolorparser::parse(s).into_diagnostic()?.to_array(); + Ok(Self::from_array_unpremul(color.map(|x| x as f32))) } } @@ -1453,7 +1566,7 @@ struct ColorRgba { impl From for Color { fn from(value: ColorRgba) -> Self { let ColorRgba { r, g, b, a } = value; - Self { r, g, b, a } + Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.)) } } @@ -2771,41 +2884,25 @@ mod tests { focus_ring: FocusRing { off: false, width: FloatOrInt(5.), - active_color: Color { - r: 0, - g: 100, - b: 200, - a: 255, - }, - inactive_color: Color { - r: 255, - g: 200, - b: 100, - a: 0, - }, + active_color: Color::from_rgba8_unpremul(0, 100, 200, 255), + inactive_color: Color::from_rgba8_unpremul(255, 200, 100, 0), active_gradient: Some(Gradient { - from: Color::new(10, 20, 30, 255), - to: Color::new(0, 128, 255, 255), + from: Color::from_rgba8_unpremul(10, 20, 30, 255), + to: Color::from_rgba8_unpremul(0, 128, 255, 255), angle: 180, relative_to: GradientRelativeTo::WorkspaceView, + in_: GradientInterpolation { + color_space: GradientColorSpace::Srgb, + hue_interpolation: HueInterpolation::Shorter, + }, }), inactive_gradient: None, }, border: Border { off: false, width: FloatOrInt(3.), - active_color: Color { - r: 255, - g: 200, - b: 127, - a: 255, - }, - inactive_color: Color { - r: 255, - g: 200, - b: 100, - a: 0, - }, + active_color: Color::from_rgba8_unpremul(255, 200, 127, 255), + inactive_color: Color::from_rgba8_unpremul(255, 200, 100, 0), active_gradient: None, inactive_gradient: None, }, @@ -3095,6 +3192,81 @@ mod tests { assert!("10% ".parse::().is_err()); } + #[test] + fn parse_gradient_interpolation() { + assert_eq!( + "srgb".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Srgb, + ..Default::default() + } + ); + assert_eq!( + "srgb-linear".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::SrgbLinear, + ..Default::default() + } + ); + assert_eq!( + "oklab".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklab, + ..Default::default() + } + ); + assert_eq!( + "oklch".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + ..Default::default() + } + ); + assert_eq!( + "oklch shorter hue" + .parse::() + .unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Shorter, + } + ); + assert_eq!( + "oklch longer hue".parse::().unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Longer, + } + ); + assert_eq!( + "oklch decreasing hue" + .parse::() + .unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Decreasing, + } + ); + assert_eq!( + "oklch increasing hue" + .parse::() + .unwrap(), + GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Increasing, + } + ); + + assert!("".parse::().is_err()); + assert!("srgb shorter hue".parse::().is_err()); + assert!("oklch shorter".parse::().is_err()); + assert!("oklch shorter h".parse::().is_err()); + assert!("oklch a hue".parse::().is_err()); + assert!("oklch shorter hue a" + .parse::() + .is_err()); + } + #[test] fn parse_iso_level3_shift() { assert_eq!( diff --git a/niri-visual-tests/src/cases/gradient_angle.rs b/niri-visual-tests/src/cases/gradient_angle.rs index 7d0b7542..fa70dc73 100644 --- a/niri-visual-tests/src/cases/gradient_angle.rs +++ b/niri-visual-tests/src/cases/gradient_angle.rs @@ -4,7 +4,7 @@ use std::time::Duration; use niri::animation::ANIMATION_SLOWDOWN; use niri::render_helpers::border::BorderRenderElement; -use niri_config::CornerRadius; +use niri_config::{Color, CornerRadius, GradientInterpolation}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Physical, Rectangle, Size}; @@ -64,8 +64,9 @@ impl TestCase for GradientAngle { [BorderRenderElement::new( area.size, Rectangle::from_loc_and_size((0., 0.), area.size), - [1., 0., 0., 1.], - [0., 1., 0., 1.], + GradientInterpolation::default(), + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), self.angle - FRAC_PI_2, Rectangle::from_loc_and_size((0., 0.), area.size), 0., diff --git a/niri-visual-tests/src/cases/gradient_area.rs b/niri-visual-tests/src/cases/gradient_area.rs index 13f50d4c..5463cfc4 100644 --- a/niri-visual-tests/src/cases/gradient_area.rs +++ b/niri-visual-tests/src/cases/gradient_area.rs @@ -5,7 +5,7 @@ use std::time::Duration; use niri::animation::ANIMATION_SLOWDOWN; use niri::layout::focus_ring::FocusRing; use niri::render_helpers::border::BorderRenderElement; -use niri_config::{Color, CornerRadius, FloatOrInt}; +use niri_config::{Color, CornerRadius, FloatOrInt, GradientInterpolation}; use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Physical, Point, Rectangle, Size}; @@ -23,7 +23,7 @@ impl GradientArea { let border = FocusRing::new(niri_config::FocusRing { off: false, width: FloatOrInt(1.), - active_color: Color::new(255, 255, 255, 128), + active_color: Color::from_rgba8_unpremul(255, 255, 255, 128), inactive_color: Color::default(), active_gradient: None, inactive_gradient: None, @@ -104,8 +104,9 @@ impl TestCase for GradientArea { [BorderRenderElement::new( area.size, g_area, - [1., 0., 0., 1.], - [0., 1., 0., 1.], + GradientInterpolation::default(), + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), FRAC_PI_4, Rectangle::from_loc_and_size((0, 0), rect_size).to_f64(), 0., diff --git a/niri-visual-tests/src/cases/gradient_oklab.rs b/niri-visual-tests/src/cases/gradient_oklab.rs new file mode 100644 index 00000000..abebb213 --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklab.rs @@ -0,0 +1,53 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientOklab { + gradient_format: GradientInterpolation, +} + +impl GradientOklab { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Oklab, + hue_interpolation: HueInterpolation::Shorter, + }, + } + } +} + +impl TestCase for GradientOklab { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_oklab_alpha.rs b/niri-visual-tests/src/cases/gradient_oklab_alpha.rs new file mode 100644 index 00000000..31ae59ef --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklab_alpha.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientOklabAlpha { + gradient_format: GradientInterpolation, +} + +impl GradientOklabAlpha { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Oklab, + hue_interpolation: Default::default(), + }, + } + } +} + +impl TestCase for GradientOklabAlpha { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 0.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_oklch_alpha.rs b/niri-visual-tests/src/cases/gradient_oklch_alpha.rs new file mode 100644 index 00000000..022f59ec --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_alpha.rs @@ -0,0 +1,53 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientOklchAlpha { + gradient_format: GradientInterpolation, +} + +impl GradientOklchAlpha { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Longer, + }, + } + } +} + +impl TestCase for GradientOklchAlpha { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 0.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_oklch_decreasing.rs b/niri-visual-tests/src/cases/gradient_oklch_decreasing.rs new file mode 100644 index 00000000..7039f2c8 --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_decreasing.rs @@ -0,0 +1,53 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientOklchDecreasing { + gradient_format: GradientInterpolation, +} + +impl GradientOklchDecreasing { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Decreasing, + }, + } + } +} + +impl TestCase for GradientOklchDecreasing { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_oklch_increasing.rs b/niri-visual-tests/src/cases/gradient_oklch_increasing.rs new file mode 100644 index 00000000..2a020923 --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_increasing.rs @@ -0,0 +1,53 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientOklchIncreasing { + gradient_format: GradientInterpolation, +} + +impl GradientOklchIncreasing { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Increasing, + }, + } + } +} + +impl TestCase for GradientOklchIncreasing { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_oklch_longer.rs b/niri-visual-tests/src/cases/gradient_oklch_longer.rs new file mode 100644 index 00000000..d63259fd --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_longer.rs @@ -0,0 +1,53 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientOklchLonger { + gradient_format: GradientInterpolation, +} + +impl GradientOklchLonger { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Longer, + }, + } + } +} + +impl TestCase for GradientOklchLonger { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_oklch_shorter.rs b/niri-visual-tests/src/cases/gradient_oklch_shorter.rs new file mode 100644 index 00000000..7cd412ab --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_oklch_shorter.rs @@ -0,0 +1,53 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientOklchShorter { + gradient_format: GradientInterpolation, +} + +impl GradientOklchShorter { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Oklch, + hue_interpolation: HueInterpolation::Shorter, + }, + } + } +} + +impl TestCase for GradientOklchShorter { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_srgb.rs b/niri-visual-tests/src/cases/gradient_srgb.rs new file mode 100644 index 00000000..d0b847c0 --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_srgb.rs @@ -0,0 +1,53 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientSrgb { + gradient_format: GradientInterpolation, +} + +impl GradientSrgb { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Srgb, + hue_interpolation: HueInterpolation::Shorter, + }, + } + } +} + +impl TestCase for GradientSrgb { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_srgb_alpha.rs b/niri-visual-tests/src/cases/gradient_srgb_alpha.rs new file mode 100644 index 00000000..c1c3c75c --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_srgb_alpha.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientSrgbAlpha { + gradient_format: GradientInterpolation, +} + +impl GradientSrgbAlpha { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::Srgb, + hue_interpolation: Default::default(), + }, + } + } +} + +impl TestCase for GradientSrgbAlpha { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 0.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_srgblinear.rs b/niri-visual-tests/src/cases/gradient_srgblinear.rs new file mode 100644 index 00000000..b8d0ebba --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_srgblinear.rs @@ -0,0 +1,53 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientSrgbLinear { + gradient_format: GradientInterpolation, +} + +impl GradientSrgbLinear { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::SrgbLinear, + hue_interpolation: HueInterpolation::Shorter, + }, + } + } +} + +impl TestCase for GradientSrgbLinear { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 1.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/gradient_srgblinear_alpha.rs b/niri-visual-tests/src/cases/gradient_srgblinear_alpha.rs new file mode 100644 index 00000000..2a22388f --- /dev/null +++ b/niri-visual-tests/src/cases/gradient_srgblinear_alpha.rs @@ -0,0 +1,51 @@ +use niri::render_helpers::border::BorderRenderElement; +use niri_config::{Color, CornerRadius, GradientColorSpace, GradientInterpolation}; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::utils::{Logical, Physical, Rectangle, Size}; + +use super::TestCase; + +pub struct GradientSrgbLinearAlpha { + gradient_format: GradientInterpolation, +} + +impl GradientSrgbLinearAlpha { + pub fn new(_size: Size) -> Self { + Self { + gradient_format: GradientInterpolation { + color_space: GradientColorSpace::SrgbLinear, + hue_interpolation: Default::default(), + }, + } + } +} + +impl TestCase for GradientSrgbLinearAlpha { + fn render( + &mut self, + _renderer: &mut GlesRenderer, + size: Size, + ) -> Vec>> { + let (a, b) = (size.w / 6, size.h / 3); + let size = (size.w - a * 2, size.h - b * 2); + let area = Rectangle::from_loc_and_size((a, b), size).to_f64(); + + [BorderRenderElement::new( + area.size, + Rectangle::from_loc_and_size((0., 0.), area.size), + self.gradient_format, + Color::new_unpremul(1., 0., 0., 1.), + Color::new_unpremul(0., 1., 0., 0.), + 0., + Rectangle::from_loc_and_size((0., 0.), area.size), + 0., + CornerRadius::default(), + 1., + ) + .with_location(area.loc)] + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs index 2e730dbe..c599ce8c 100644 --- a/niri-visual-tests/src/cases/layout.rs +++ b/niri-visual-tests/src/cases/layout.rs @@ -50,8 +50,8 @@ impl Layout { border: niri_config::Border { off: false, width: FloatOrInt(4.), - active_color: Color::new(255, 163, 72, 255), - inactive_color: Color::new(50, 50, 50, 255), + active_color: Color::from_rgba8_unpremul(255, 163, 72, 255), + inactive_color: Color::from_rgba8_unpremul(50, 50, 50, 255), active_gradient: None, inactive_gradient: None, }, diff --git a/niri-visual-tests/src/cases/mod.rs b/niri-visual-tests/src/cases/mod.rs index 25aa1e30..07a41ba4 100644 --- a/niri-visual-tests/src/cases/mod.rs +++ b/niri-visual-tests/src/cases/mod.rs @@ -6,6 +6,17 @@ use smithay::utils::{Physical, Size}; pub mod gradient_angle; pub mod gradient_area; +pub mod gradient_oklab; +pub mod gradient_oklab_alpha; +pub mod gradient_oklch_alpha; +pub mod gradient_oklch_decreasing; +pub mod gradient_oklch_increasing; +pub mod gradient_oklch_longer; +pub mod gradient_oklch_shorter; +pub mod gradient_srgb; +pub mod gradient_srgb_alpha; +pub mod gradient_srgblinear; +pub mod gradient_srgblinear_alpha; pub mod layout; pub mod tile; pub mod window; diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs index 95261877..7a14111e 100644 --- a/niri-visual-tests/src/cases/tile.rs +++ b/niri-visual-tests/src/cases/tile.rs @@ -72,7 +72,7 @@ impl Tile { border: niri_config::Border { off: false, width: FloatOrInt(32.), - active_color: Color::new(255, 163, 72, 255), + active_color: Color::from_rgba8_unpremul(255, 163, 72, 255), ..Default::default() }, ..Default::default() diff --git a/niri-visual-tests/src/main.rs b/niri-visual-tests/src/main.rs index 771c501c..2b46f4f5 100644 --- a/niri-visual-tests/src/main.rs +++ b/niri-visual-tests/src/main.rs @@ -5,8 +5,6 @@ use std::env; use std::sync::atomic::Ordering; use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt}; -use cases::tile::Tile; -use cases::window::Window; use gtk::prelude::{ AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt, }; @@ -18,7 +16,20 @@ use tracing_subscriber::EnvFilter; use crate::cases::gradient_angle::GradientAngle; use crate::cases::gradient_area::GradientArea; +use crate::cases::gradient_oklab::GradientOklab; +use crate::cases::gradient_oklab_alpha::GradientOklabAlpha; +use crate::cases::gradient_oklch_alpha::GradientOklchAlpha; +use crate::cases::gradient_oklch_decreasing::GradientOklchDecreasing; +use crate::cases::gradient_oklch_increasing::GradientOklchIncreasing; +use crate::cases::gradient_oklch_longer::GradientOklchLonger; +use crate::cases::gradient_oklch_shorter::GradientOklchShorter; +use crate::cases::gradient_srgb::GradientSrgb; +use crate::cases::gradient_srgb_alpha::GradientSrgbAlpha; +use crate::cases::gradient_srgblinear::GradientSrgbLinear; +use crate::cases::gradient_srgblinear_alpha::GradientSrgbLinearAlpha; use crate::cases::layout::Layout; +use crate::cases::tile::Tile; +use crate::cases::window::Window; use crate::cases::TestCase; mod cases; @@ -112,6 +123,17 @@ fn build_ui(app: &adw::Application) { s.add(GradientAngle::new, "Gradient - Angle"); s.add(GradientArea::new, "Gradient - Area"); + s.add(GradientSrgb::new, "Gradient - Srgb"); + s.add(GradientSrgbLinear::new, "Gradient - SrgbLinear"); + s.add(GradientOklab::new, "Gradient - Oklab"); + s.add(GradientOklchShorter::new, "Gradient - Oklch Shorter"); + s.add(GradientOklchLonger::new, "Gradient - Oklch Longer"); + s.add(GradientOklchIncreasing::new, "Gradient - Oklch Increasing"); + s.add(GradientOklchDecreasing::new, "Gradient - Oklch Decreasing"); + s.add(GradientSrgbAlpha::new, "Gradient - Srgb Alpha"); + s.add(GradientSrgbLinearAlpha::new, "Gradient - SrgbLinear Alpha"); + s.add(GradientOklabAlpha::new, "Gradient - Oklab Alpha"); + s.add(GradientOklchAlpha::new, "Gradient - Oklch Alpha"); let content_headerbar = adw::HeaderBar::new(); diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 1d8ebb67..ed2d4f0e 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -153,6 +153,7 @@ layout { // The angle is the same as in linear-gradient, and is optional, // defaulting to 180 (top-to-bottom gradient). // You can use any CSS linear-gradient tool on the web to set these up. + // Changing the color space is also supported, check the wiki for more info. // // active-gradient from="#80c8ff" to="#bbddff" angle=45 diff --git a/src/layout/focus_ring.rs b/src/layout/focus_ring.rs index 97a1b75a..7cd73760 100644 --- a/src/layout/focus_ring.rs +++ b/src/layout/focus_ring.rs @@ -1,7 +1,7 @@ use std::iter::zip; use arrayvec::ArrayVec; -use niri_config::{CornerRadius, Gradient, GradientRelativeTo}; +use niri_config::{CornerRadius, Gradient, GradientInterpolation, GradientRelativeTo}; use smithay::backend::renderer::element::Kind; use smithay::utils::{Logical, Point, Rectangle, Size}; @@ -72,7 +72,7 @@ impl FocusRing { }; for buf in &mut self.buffers { - buf.set_color(color.into()); + buf.set_color(color.to_array_premul()); } let radius = radius.fit_to(self.full_size.w as f32, self.full_size.h as f32); @@ -91,6 +91,7 @@ impl FocusRing { to: color, angle: 0, relative_to: GradientRelativeTo::Window, + in_: GradientInterpolation::default(), }); let full_rect = Rectangle::from_loc_and_size((-width, -width), self.full_size); @@ -178,8 +179,9 @@ impl FocusRing { border.update( size, Rectangle::from_loc_and_size(gradient_area.loc - loc, gradient_area.size), - gradient.from.into(), - gradient.to.into(), + gradient.in_, + gradient.from, + gradient.to, ((gradient.angle as f32) - 90.).to_radians(), Rectangle::from_loc_and_size(full_rect.loc - loc, full_rect.size), rounded_corner_border_width, @@ -198,8 +200,9 @@ impl FocusRing { gradient_area.loc - self.locations[0], gradient_area.size, ), - gradient.from.into(), - gradient.to.into(), + gradient.in_, + gradient.from, + gradient.to, ((gradient.angle as f32) - 90.).to_radians(), Rectangle::from_loc_and_size(full_rect.loc - self.locations[0], full_rect.size), rounded_corner_border_width, diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 710add83..167ff2c6 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use std::time::Duration; -use niri_config::CornerRadius; +use niri_config::{Color, CornerRadius, GradientInterpolation}; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::{Element, Kind}; use smithay::backend::renderer::gles::GlesRenderer; @@ -757,8 +757,9 @@ impl Tile { return BorderRenderElement::new( geo.size, Rectangle::from_loc_and_size((0., 0.), geo.size), - elem.color(), - elem.color(), + GradientInterpolation::default(), + Color::from_array_premul(elem.color()), + Color::from_array_premul(elem.color()), 0., Rectangle::from_loc_and_size((0., 0.), geo.size), 0., diff --git a/src/render_helpers/border.rs b/src/render_helpers/border.rs index c3442425..c0ab6663 100644 --- a/src/render_helpers/border.rs +++ b/src/render_helpers/border.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use glam::{Mat3, Vec2}; -use niri_config::CornerRadius; +use niri_config::{ + Color, CornerRadius, GradientColorSpace, GradientInterpolation, HueInterpolation, +}; use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage}; use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform}; use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions}; @@ -28,8 +30,9 @@ pub struct BorderRenderElement { struct Parameters { size: Size, gradient_area: Rectangle, - color_from: [f32; 4], - color_to: [f32; 4], + gradient_format: GradientInterpolation, + color_from: Color, + color_to: Color, angle: f32, geometry: Rectangle, border_width: f32, @@ -43,8 +46,9 @@ impl BorderRenderElement { pub fn new( size: Size, gradient_area: Rectangle, - color_from: [f32; 4], - color_to: [f32; 4], + gradient_format: GradientInterpolation, + color_from: Color, + color_to: Color, angle: f32, geometry: Rectangle, border_width: f32, @@ -57,6 +61,7 @@ impl BorderRenderElement { params: Parameters { size, gradient_area, + gradient_format, color_from, color_to, angle, @@ -77,6 +82,7 @@ impl BorderRenderElement { params: Parameters { size: Default::default(), gradient_area: Default::default(), + gradient_format: GradientInterpolation::default(), color_from: Default::default(), color_to: Default::default(), angle: 0., @@ -97,8 +103,9 @@ impl BorderRenderElement { &mut self, size: Size, gradient_area: Rectangle, - color_from: [f32; 4], - color_to: [f32; 4], + gradient_format: GradientInterpolation, + color_from: Color, + color_to: Color, angle: f32, geometry: Rectangle, border_width: f32, @@ -108,6 +115,7 @@ impl BorderRenderElement { let params = Parameters { size, gradient_area, + gradient_format, color_from, color_to, angle, @@ -128,6 +136,7 @@ impl BorderRenderElement { let Parameters { size, gradient_area, + gradient_format, color_from, color_to, angle, @@ -162,13 +171,29 @@ impl BorderRenderElement { let input_to_geo = Mat3::from_scale(area_size) * Mat3::from_translation(-geo_loc / area_size); + let colorspace = match gradient_format.color_space { + GradientColorSpace::Srgb => 0., + GradientColorSpace::SrgbLinear => 1., + GradientColorSpace::Oklab => 2., + GradientColorSpace::Oklch => 3., + }; + + let hue_interpolation = match gradient_format.hue_interpolation { + HueInterpolation::Shorter => 0., + HueInterpolation::Longer => 1., + HueInterpolation::Increasing => 2., + HueInterpolation::Decreasing => 3., + }; + self.inner.update( size, None, scale, vec![ - Uniform::new("color_from", color_from), - Uniform::new("color_to", color_to), + Uniform::new("colorspace", colorspace), + Uniform::new("hue_interpolation", hue_interpolation), + Uniform::new("color_from", color_from.to_array_unpremul()), + Uniform::new("color_to", color_to.to_array_unpremul()), Uniform::new("grad_offset", grad_offset.to_array()), Uniform::new("grad_width", w), Uniform::new("grad_vec", grad_vec.to_array()), diff --git a/src/render_helpers/shaders/border.frag b/src/render_helpers/shaders/border.frag index fe121037..80030627 100644 --- a/src/render_helpers/shaders/border.frag +++ b/src/render_helpers/shaders/border.frag @@ -10,6 +10,8 @@ uniform float niri_scale; uniform vec2 niri_size; varying vec2 niri_v_coords; +uniform float colorspace; +uniform float hue_interpolation; uniform vec4 color_from; uniform vec4 color_to; uniform vec2 grad_offset; @@ -21,6 +23,176 @@ uniform vec2 geo_size; uniform vec4 outer_radius; uniform float border_width; +vec4 premul_rect(vec4 color) { + color.rgb *= color.a; + return color; +} + +vec4 premul_lch(vec4 color) { + color.xy *= color.a; + return color; +} + +vec4 unpremul_rect(vec4 color) { + if (color.a == 0.0) + return color; + + color.rgb /= color.a; + return color; +} + +vec4 unpremul_lch(vec4 color) { + if (color.a == 0.0) + return color; + + color.xy /= color.a; + return color; +} + +vec4 premul_mix_unpremul_rect(vec4 color1, vec4 color2, float ratio) { + vec4 mixed = mix(premul_rect(color1), premul_rect(color2), ratio); + return unpremul_rect(mixed); +} + +vec4 premul_mix_unpremul_lch(vec4 color1, vec4 color2, float ratio) { + vec4 mixed = mix(premul_lch(color1), premul_lch(color2), ratio); + return unpremul_lch(mixed); +} + +vec3 srgb_to_linear(vec3 color) { + return pow(color, vec3(2.2)); +} + +vec3 linear_to_srgb(vec3 color) { + return pow(color, vec3(1.0 / 2.2)); +} + +vec3 lab_to_lch(vec3 color) { + float c = sqrt(pow(color.y, 2.0) + pow(color.z, 2.0)); + float h = degrees(atan(color.z, color.y)) ; + h += h <= 0.0 ? + 360.0 : + 0.0 ; + return vec3( + color.x, + c, + h + ); +} + +vec3 lch_to_lab(vec3 color) { + float a = color.y * clamp(cos(radians(color.z)), -1.0, 1.0); + float b = color.y * clamp(sin(radians(color.z)), -1.0, 1.0); + return vec3( + color.x, + a, + b + ); +} + +vec3 linear_to_oklab(vec3 color){ + mat3 rgb_to_lms = mat3( + vec3(0.4122214708, 0.5363325363, 0.0514459929), + vec3(0.2119034982, 0.6806995451, 0.1073969566), + vec3(0.0883024619, 0.2817188376, 0.6299787005) + ); + mat3 lms_to_oklab = mat3( + vec3(0.2104542553, 0.7936177850, -0.0040720468), + vec3(1.9779984951, -2.4285922050, 0.4505937099), + vec3(0.0259040371, 0.7827717662, -0.8086757660) + ); + vec3 lms = color * rgb_to_lms; + lms = pow(lms, vec3(1.0 / 3.0)); + return lms * lms_to_oklab; +} + +vec3 oklab_to_linear(vec3 color){ + mat3 oklab_to_lms = mat3( + vec3(1.0, 0.3963377774, 0.2158037573), + vec3(1.0, -0.1055613458, -0.0638541728), + vec3(1.0, -0.0894841775, -1.2914855480) + ); + mat3 lms_to_rgb = mat3( + vec3(4.0767416621, -3.3077115913, 0.2309699292), + vec3(-1.2684380046, 2.6097574011, -0.3413193965), + vec3(-0.0041960863, -0.7034186147, 1.7076147010) + ); + vec3 lms = color * oklab_to_lms; + lms = pow(lms, vec3(3.0)); + return lms * lms_to_rgb; +} + +vec4 color_mix(vec4 color1, vec4 color2, float color_ratio) { + vec4 color_out; + + // srgb + if (colorspace == 0.0) { + return mix(premul_rect(color1), premul_rect(color2), color_ratio); + } + + color1.rgb = srgb_to_linear(color1.rgb); + color2.rgb = srgb_to_linear(color2.rgb); + + // srgb-linear + if (colorspace == 1.0) { + color_out = premul_mix_unpremul_rect(color1, color2, color_ratio); + // oklab + } else if (colorspace == 2.0) { + color1.xyz = linear_to_oklab(color1.rgb); + color2.xyz = linear_to_oklab(color2.rgb); + color_out = premul_mix_unpremul_rect(color1, color2, color_ratio); + color_out.rgb = oklab_to_linear(color_out.xyz); + // oklch + } else if (colorspace == 3.0) { + color1.xyz = lab_to_lch(linear_to_oklab(color1.rgb)); + color2.xyz = lab_to_lch(linear_to_oklab(color2.rgb)); + color_out = premul_mix_unpremul_lch(color1, color2, color_ratio); + + float min_hue = min(color1.z, color2.z); + float max_hue = max(color1.z, color2.z); + float path_direct_distance = (max_hue - min_hue) * color_ratio; + float path_mod_distance = (360.0 - max_hue + min_hue) * color_ratio; + + float path_mod = + color1.z == min_hue ? + mod(color1.z - path_mod_distance, 360.0) : + mod(color1.z + path_mod_distance, 360.0) ; + float path_direct = + color1.z == min_hue ? + color1.z + path_direct_distance : + color1.z - path_direct_distance ; + + // shorter + if (hue_interpolation == 0.0) { + color_out.z = + max_hue - min_hue > 360.0 - max_hue + min_hue ? + path_mod : + path_direct ; + // longer + } else if (hue_interpolation == 1.0) { + color_out.z = + max_hue - min_hue <= 360.0 - max_hue + min_hue ? + path_mod : + path_direct ; + // increasing + } else if (hue_interpolation == 2.0) { + color_out.z = + color1.z > color2.z ? + path_mod : + path_direct ; + // decreasing + } else if (hue_interpolation == 3.0) { + color_out.z = + color1.z <= color2.z ? + path_mod : + path_direct ; + } + color_out.rgb = clamp(oklab_to_linear(lch_to_lab(color_out.xyz)), 0.0, 1.0); + } + + return premul_rect(vec4(linear_to_srgb(color_out.rgb), color_out.a)); +} + vec4 gradient_color(vec2 coords) { coords = coords + grad_offset; @@ -33,7 +205,7 @@ vec4 gradient_color(vec2 coords) { frac += 1.0; frac = clamp(frac, 0.0, 1.0); - return mix(color_from, color_to, frac); + return color_mix(color_from, color_to, frac); } float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) { diff --git a/src/render_helpers/shaders/mod.rs b/src/render_helpers/shaders/mod.rs index b4824931..91ba32d1 100644 --- a/src/render_helpers/shaders/mod.rs +++ b/src/render_helpers/shaders/mod.rs @@ -34,6 +34,8 @@ impl Shaders { renderer, include_str!("border.frag"), &[ + UniformName::new("colorspace", UniformType::_1f), + UniformName::new("hue_interpolation", UniformType::_1f), UniformName::new("color_from", UniformType::_4f), UniformName::new("color_to", UniformType::_4f), UniformName::new("grad_offset", UniformType::_2f), diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 178b0a30..9963d7b6 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -2,7 +2,7 @@ use std::cell::{Cell, RefCell}; use std::cmp::{max, min}; use std::time::Duration; -use niri_config::{CornerRadius, WindowRule}; +use niri_config::{Color, CornerRadius, GradientInterpolation, WindowRule}; use smithay::backend::renderer::element::surface::render_elements_from_surface_tree; use smithay::backend::renderer::element::{Id, Kind}; use smithay::backend::renderer::gles::GlesRenderer; @@ -289,8 +289,9 @@ impl Mapped { return BorderRenderElement::new( geo.size, Rectangle::from_loc_and_size((0., 0.), geo.size), - elem.color(), - elem.color(), + GradientInterpolation::default(), + Color::from_array_premul(elem.color()), + Color::from_array_premul(elem.color()), 0., Rectangle::from_loc_and_size((0., 0.), geo.size), 0., diff --git a/wiki/Configuration:-Layout.md b/wiki/Configuration:-Layout.md index 04fe04b4..2eac03a5 100644 --- a/wiki/Configuration:-Layout.md +++ b/wiki/Configuration:-Layout.md @@ -32,7 +32,7 @@ layout { active-color "#ffc87f" inactive-color "#505050" // active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view" - // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear" } struts { @@ -169,7 +169,7 @@ layout { inactive-color "#505050" // active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view" - // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" in="srgb-linear" } } ``` @@ -238,6 +238,27 @@ layout { } ``` +Since: 0.1.8 You can set the gradient interpolation color space using syntax like `in="srgb-linear"` or `in="oklch longer hue"`. +Supported color spaces are: + +- `srgb` (the default), +- `srgb-linear`, +- `oklab`, +- `oklch` with `shorter hue` or `longer hue` or `increasing hue` or `decreasing hue`. + +They are rendered the same as CSS. +For example, `active-gradient from="#f00f" to="#0f05" angle=45 in="oklch longer hue"` will look the same as CSS `linear-gradient(45deg in oklch longer hue, #f00f, #0f05)`. + +![](./img/gradients-oklch.png) + +```kdl +layout { + border { + active-gradient from="#f00f" to="#0f05" angle=45 in="oklch longer hue" + } +} +``` + ### `struts` Struts shrink the area occupied by windows, similarly to layer-shell panels. diff --git a/wiki/img/gradients-oklch.png b/wiki/img/gradients-oklch.png new file mode 100644 index 00000000..ab255c51 --- /dev/null +++ b/wiki/img/gradients-oklch.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3e4db583ac849ec3bd6c2312a84a16eaad810ebb7599cad3762a7dfc66e865b +size 23180 -- cgit