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 --- src/render_helpers/border.rs | 43 ++++++-- src/render_helpers/shaders/border.frag | 174 ++++++++++++++++++++++++++++++++- src/render_helpers/shaders/mod.rs | 2 + 3 files changed, 209 insertions(+), 10 deletions(-) (limited to 'src/render_helpers') 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), -- cgit