HSL and HSV interpolation for UI gradients (#19992)
# Objective Add interpolation in HSL and HSV colour spaces for UI gradients. ## Solution Added new variants to `InterpolationColorSpace`: `Hsl`, `HslLong`, `Hsv`, and `HsvLong`, along with mix functions to the `gradients` shader for each of them. #### Limitations * Didn't include increasing and decreasing path support, it's not essential and can be done in a follow up if someone feels like it. * The colour conversions should really be performed before the colours are sent to the shader but it would need more changes and performance is good enough for now. ## Testing ```cargo run --example gradients```
This commit is contained in:
parent
12da4e482a
commit
5ea0e4004f
@ -631,6 +631,14 @@ pub enum InterpolationColorSpace {
|
||||
Srgb,
|
||||
/// Interpolates in linear sRGB space.
|
||||
LinearRgb,
|
||||
/// Interpolates in HSL space, taking the shortest hue path.
|
||||
Hsl,
|
||||
/// Interpolates in HSL space, taking the longest hue path.
|
||||
HslLong,
|
||||
/// Interpolates in HSV space, taking the shortest hue path.
|
||||
Hsv,
|
||||
/// Interpolates in HSV space, taking the longest hue path.
|
||||
HsvLong,
|
||||
}
|
||||
|
||||
/// Set the color space used for interpolation.
|
||||
|
@ -186,6 +186,10 @@ impl SpecializedRenderPipeline for GradientPipeline {
|
||||
InterpolationColorSpace::OkLchLong => "IN_OKLCH_LONG",
|
||||
InterpolationColorSpace::Srgb => "IN_SRGB",
|
||||
InterpolationColorSpace::LinearRgb => "IN_LINEAR_RGB",
|
||||
InterpolationColorSpace::Hsl => "IN_HSL",
|
||||
InterpolationColorSpace::HslLong => "IN_HSL_LONG",
|
||||
InterpolationColorSpace::Hsv => "IN_HSV",
|
||||
InterpolationColorSpace::HsvLong => "IN_HSV_LONG",
|
||||
};
|
||||
|
||||
let shader_defs = if key.anti_alias {
|
||||
|
@ -31,7 +31,7 @@ struct GradientVertexOutput {
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) @interpolate(flat) size: vec2<f32>,
|
||||
@location(2) @interpolate(flat) flags: u32,
|
||||
@location(3) @interpolate(flat) radius: vec4<f32>,
|
||||
@location(3) @interpolate(flat) radius: vec4<f32>,
|
||||
@location(4) @interpolate(flat) border: vec4<f32>,
|
||||
|
||||
// Position relative to the center of the rectangle.
|
||||
@ -114,27 +114,27 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4<f32> {
|
||||
}
|
||||
}
|
||||
|
||||
// This function converts two linear rgb colors to srgb space, mixes them, and then converts the result back to linear rgb space.
|
||||
fn mix_linear_rgb_in_srgb_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
// This function converts two linear rgba colors to srgba space, mixes them, and then converts the result back to linear rgb space.
|
||||
fn mix_linear_rgba_in_srgba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
let a_srgb = pow(a.rgb, vec3(1. / 2.2));
|
||||
let b_srgb = pow(b.rgb, vec3(1. / 2.2));
|
||||
let mixed_srgb = mix(a_srgb, b_srgb, t);
|
||||
return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t));
|
||||
}
|
||||
|
||||
fn linear_rgb_to_oklab(c: vec4<f32>) -> vec4<f32> {
|
||||
fn linear_rgba_to_oklaba(c: vec4<f32>) -> vec4<f32> {
|
||||
let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.);
|
||||
let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.);
|
||||
let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.);
|
||||
return vec4(
|
||||
0.21045426 * l + 0.7936178 * m - 0.004072047 * s,
|
||||
1.9779985 * l - 2.4285922 * m + 0.4505937 * s,
|
||||
0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
|
||||
c.w
|
||||
0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
|
||||
c.a
|
||||
);
|
||||
}
|
||||
|
||||
fn oklab_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
|
||||
fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
|
||||
let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z;
|
||||
let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z;
|
||||
let s_ = c.x - 0.08948418 * c.y - 1.2914855 * c.z;
|
||||
@ -145,26 +145,127 @@ fn oklab_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
|
||||
4.0767417 * l - 3.3077116 * m + 0.23096994 * s,
|
||||
-1.268438 * l + 2.6097574 * m - 0.34131938 * s,
|
||||
-0.0041960863 * l - 0.7034186 * m + 1.7076147 * s,
|
||||
c.w
|
||||
c.a
|
||||
);
|
||||
}
|
||||
|
||||
fn mix_linear_rgb_in_oklab_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
return oklab_to_linear_rgba(mix(linear_rgb_to_oklab(a), linear_rgb_to_oklab(b), t));
|
||||
fn mix_linear_rgba_in_oklaba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t));
|
||||
}
|
||||
|
||||
fn linear_rgba_to_hsla(c: vec4<f32>) -> vec4<f32> {
|
||||
let maxc = max(max(c.r, c.g), c.b);
|
||||
let minc = min(min(c.r, c.g), c.b);
|
||||
let delta = maxc - minc;
|
||||
let l = (maxc + minc) * 0.5;
|
||||
var h: f32 = 0.0;
|
||||
var s: f32 = 0.0;
|
||||
if delta != 0.0 {
|
||||
s = delta / (1.0 - abs(2.0 * l - 1.0));
|
||||
if maxc == c.r {
|
||||
h = ((c.g - c.b) / delta) % 6.0;
|
||||
} else if maxc == c.g {
|
||||
h = ((c.b - c.r) / delta) + 2.0;
|
||||
} else {
|
||||
h = ((c.r - c.g) / delta) + 4.0;
|
||||
}
|
||||
h = h / 6.0;
|
||||
if h < 0.0 {
|
||||
h = h + 1.0;
|
||||
}
|
||||
}
|
||||
return vec4<f32>(h, s, l, c.a);
|
||||
}
|
||||
|
||||
fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
|
||||
let h = hsl.x;
|
||||
let s = hsl.y;
|
||||
let l = hsl.z;
|
||||
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
|
||||
let hp = h * 6.0;
|
||||
let x = c * (1.0 - abs(hp % 2.0 - 1.0));
|
||||
var r: f32 = 0.0;
|
||||
var g: f32 = 0.0;
|
||||
var b: f32 = 0.0;
|
||||
if 0.0 <= hp && hp < 1.0 {
|
||||
r = c; g = x; b = 0.0;
|
||||
} else if 1.0 <= hp && hp < 2.0 {
|
||||
r = x; g = c; b = 0.0;
|
||||
} else if 2.0 <= hp && hp < 3.0 {
|
||||
r = 0.0; g = c; b = x;
|
||||
} else if 3.0 <= hp && hp < 4.0 {
|
||||
r = 0.0; g = x; b = c;
|
||||
} else if 4.0 <= hp && hp < 5.0 {
|
||||
r = x; g = 0.0; b = c;
|
||||
} else if 5.0 <= hp && hp < 6.0 {
|
||||
r = c; g = 0.0; b = x;
|
||||
}
|
||||
let m = l - 0.5 * c;
|
||||
return vec4<f32>(r + m, g + m, b + m, hsl.a);
|
||||
}
|
||||
|
||||
fn linear_rgba_to_hsva(c: vec4<f32>) -> vec4<f32> {
|
||||
let maxc = max(max(c.r, c.g), c.b);
|
||||
let minc = min(min(c.r, c.g), c.b);
|
||||
let delta = maxc - minc;
|
||||
var h: f32 = 0.0;
|
||||
var s: f32 = 0.0;
|
||||
let v: f32 = maxc;
|
||||
if delta != 0.0 {
|
||||
s = delta / maxc;
|
||||
if maxc == c.r {
|
||||
h = ((c.g - c.b) / delta) % 6.0;
|
||||
} else if maxc == c.g {
|
||||
h = ((c.b - c.r) / delta) + 2.0;
|
||||
} else {
|
||||
h = ((c.r - c.g) / delta) + 4.0;
|
||||
}
|
||||
h = h / 6.0;
|
||||
if h < 0.0 {
|
||||
h = h + 1.0;
|
||||
}
|
||||
}
|
||||
return vec4<f32>(h, s, v, c.a);
|
||||
}
|
||||
|
||||
fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
|
||||
let h = hsva.x * 6.0;
|
||||
let s = hsva.y;
|
||||
let v = hsva.z;
|
||||
let c = v * s;
|
||||
let x = c * (1.0 - abs(h % 2.0 - 1.0));
|
||||
let m = v - c;
|
||||
var r: f32 = 0.0;
|
||||
var g: f32 = 0.0;
|
||||
var b: f32 = 0.0;
|
||||
if 0.0 <= h && h < 1.0 {
|
||||
r = c; g = x; b = 0.0;
|
||||
} else if 1.0 <= h && h < 2.0 {
|
||||
r = x; g = c; b = 0.0;
|
||||
} else if 2.0 <= h && h < 3.0 {
|
||||
r = 0.0; g = c; b = x;
|
||||
} else if 3.0 <= h && h < 4.0 {
|
||||
r = 0.0; g = x; b = c;
|
||||
} else if 4.0 <= h && h < 5.0 {
|
||||
r = x; g = 0.0; b = c;
|
||||
} else if 5.0 <= h && h < 6.0 {
|
||||
r = c; g = 0.0; b = x;
|
||||
}
|
||||
return vec4<f32>(r + m, g + m, b + m, hsva.a);
|
||||
}
|
||||
|
||||
/// hue is left in radians and not converted to degrees
|
||||
fn linear_rgb_to_oklch(c: vec4<f32>) -> vec4<f32> {
|
||||
let o = linear_rgb_to_oklab(c);
|
||||
fn linear_rgba_to_oklcha(c: vec4<f32>) -> vec4<f32> {
|
||||
let o = linear_rgba_to_oklaba(c);
|
||||
let chroma = sqrt(o.y * o.y + o.z * o.z);
|
||||
let hue = atan2(o.z, o.y);
|
||||
return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.w);
|
||||
return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.a);
|
||||
}
|
||||
|
||||
fn oklch_to_linear_rgb(c: vec4<f32>) -> vec4<f32> {
|
||||
fn oklcha_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
|
||||
let a = c.y * cos(c.z);
|
||||
let b = c.y * sin(c.z);
|
||||
return oklab_to_linear_rgba(vec4(c.x, a, b, c.w));
|
||||
return oklaba_to_linear_rgba(vec4(c.x, a, b, c.a));
|
||||
}
|
||||
|
||||
fn rem_euclid(a: f32, b: f32) -> f32 {
|
||||
@ -181,28 +282,75 @@ fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 {
|
||||
return rem_euclid(a + select(diff - TAU, diff + TAU, 0. < diff) * t, TAU);
|
||||
}
|
||||
|
||||
fn mix_oklch(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
return vec4(
|
||||
mix(a.xy, b.xy, t),
|
||||
lerp_hue(a.z, b.z, t),
|
||||
mix(a.w, b.w, t)
|
||||
mix(a.a, b.a, t)
|
||||
);
|
||||
}
|
||||
|
||||
fn mix_oklch_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
return vec4(
|
||||
mix(a.xy, b.xy, t),
|
||||
lerp_hue_long(a.z, b.z, t),
|
||||
mix(a.w, b.w, t)
|
||||
mix(a.a, b.a, t)
|
||||
);
|
||||
}
|
||||
|
||||
fn mix_linear_rgb_in_oklch_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
return oklch_to_linear_rgb(mix_oklch(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t));
|
||||
fn mix_linear_rgba_in_oklcha_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
return oklcha_to_linear_rgba(mix_oklcha(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
|
||||
}
|
||||
|
||||
fn mix_linear_rgb_in_oklch_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t));
|
||||
fn mix_linear_rgba_in_oklcha_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
return oklcha_to_linear_rgba(mix_oklcha_long(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
|
||||
}
|
||||
|
||||
fn mix_linear_rgba_in_hsva_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
let ha = linear_rgba_to_hsva(a);
|
||||
let hb = linear_rgba_to_hsva(b);
|
||||
var h: f32;
|
||||
if ha.y == 0. {
|
||||
h = hb.x;
|
||||
} else if hb.y == 0. {
|
||||
h = ha.x;
|
||||
} else {
|
||||
h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
|
||||
}
|
||||
let s = mix(ha.y, hb.y, t);
|
||||
let v = mix(ha.z, hb.z, t);
|
||||
let a_alpha = mix(ha.a, hb.a, t);
|
||||
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
|
||||
}
|
||||
|
||||
fn mix_linear_rgba_in_hsva_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
let ha = linear_rgba_to_hsva(a);
|
||||
let hb = linear_rgba_to_hsva(b);
|
||||
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
|
||||
let s = mix(ha.y, hb.y, t);
|
||||
let v = mix(ha.z, hb.z, t);
|
||||
let a_alpha = mix(ha.a, hb.a, t);
|
||||
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
|
||||
}
|
||||
|
||||
fn mix_linear_rgba_in_hsla_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
let ha = linear_rgba_to_hsla(a);
|
||||
let hb = linear_rgba_to_hsla(b);
|
||||
let h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
|
||||
let s = mix(ha.y, hb.y, t);
|
||||
let l = mix(ha.z, hb.z, t);
|
||||
let a_alpha = mix(ha.a, hb.a, t);
|
||||
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
|
||||
}
|
||||
|
||||
fn mix_linear_rgba_in_hsla_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
|
||||
let ha = linear_rgba_to_hsla(a);
|
||||
let hb = linear_rgba_to_hsla(b);
|
||||
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
|
||||
let s = mix(ha.y, hb.y, t);
|
||||
let l = mix(ha.z, hb.z, t);
|
||||
let a_alpha = mix(ha.a, hb.a, t);
|
||||
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
|
||||
}
|
||||
|
||||
// These functions are used to calculate the distance in gradient space from the start of the gradient to the point.
|
||||
@ -277,13 +425,21 @@ fn interpolate_gradient(
|
||||
}
|
||||
|
||||
#ifdef IN_SRGB
|
||||
return mix_linear_rgb_in_srgb_space(start_color, end_color, t);
|
||||
return mix_linear_rgba_in_srgba_space(start_color, end_color, t);
|
||||
#else ifdef IN_OKLAB
|
||||
return mix_linear_rgb_in_oklab_space(start_color, end_color, t);
|
||||
return mix_linear_rgba_in_oklaba_space(start_color, end_color, t);
|
||||
#else ifdef IN_OKLCH
|
||||
return mix_linear_rgb_in_oklch_space(start_color, end_color, t);
|
||||
return mix_linear_rgba_in_oklcha_space(start_color, end_color, t);
|
||||
#else ifdef IN_OKLCH_LONG
|
||||
return mix_linear_rgb_in_oklch_space_long(start_color, end_color, t);
|
||||
return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t);
|
||||
#else ifdef IN_HSV
|
||||
return mix_linear_rgba_in_hsva_space(start_color, end_color, t);
|
||||
#else ifdef IN_HSV_LONG
|
||||
return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t);
|
||||
#else ifdef IN_HSL
|
||||
return mix_linear_rgba_in_hsla_space(start_color, end_color, t);
|
||||
#else ifdef IN_HSL_LONG
|
||||
return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t);
|
||||
#else
|
||||
return mix(start_color, end_color, t);
|
||||
#endif
|
||||
|
@ -245,6 +245,18 @@ fn setup(mut commands: Commands) {
|
||||
InterpolationColorSpace::LinearRgb
|
||||
}
|
||||
InterpolationColorSpace::LinearRgb => {
|
||||
InterpolationColorSpace::Hsl
|
||||
}
|
||||
InterpolationColorSpace::Hsl => {
|
||||
InterpolationColorSpace::HslLong
|
||||
}
|
||||
InterpolationColorSpace::HslLong => {
|
||||
InterpolationColorSpace::Hsv
|
||||
}
|
||||
InterpolationColorSpace::Hsv => {
|
||||
InterpolationColorSpace::HsvLong
|
||||
}
|
||||
InterpolationColorSpace::HsvLong => {
|
||||
InterpolationColorSpace::OkLab
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: UI Gradients
|
||||
authors: ["@Ickshonpe"]
|
||||
pull_requests: [18139, 19330]
|
||||
pull_requests: [18139, 19330, 19992]
|
||||
---
|
||||
|
||||
Support for UI node's that display a gradient that transitions smoothly between two or more colors.
|
||||
@ -14,12 +14,12 @@ Each gradient type consists of the geometric properties for that gradient, a lis
|
||||
Color stops consist of a color, a position or angle and an optional hint. If no position is specified for a stop, it's evenly spaced between the previous and following stops. Color stop positions are absolute. With the list of stops:
|
||||
|
||||
```rust
|
||||
vec![vec![ColorStop::new(RED, Val::Percent(90.), ColorStop::new(Color::GREEN, Val::Percent(10.))
|
||||
vec![ColorStop::new(RED, Val::Percent(90.), ColorStop::new(GREEN), Val::Percent(10.))]
|
||||
```
|
||||
|
||||
the colors will be reordered and the gradient will transition from green at 10% to red at 90%.
|
||||
|
||||
Colors can be interpolated between the stops in OKLab, OKLCH, SRGB and linear RGB color spaces. The hint is a normalized value that can be used to shift the mid-point where the colors are mixed 50-50 between the stop with the hint and the following stop.
|
||||
Colors can be interpolated between the stops in OKLab, OKLCH, SRGB, HSL, HSV and linear RGB color spaces. The hint is a normalized value that can be used to shift the mid-point where the colors are mixed 50-50 between the stop with the hint and the following stop. Cylindrical color spaces support interpolation along both short and long hue paths.
|
||||
|
||||
For sharp stops with no interpolated transition, place two stops at the same point.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user