Skip to content
8 changes: 8 additions & 0 deletions crates/bevy_ui/src/gradients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_ui_render/src/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
210 changes: 183 additions & 27 deletions crates/bevy_ui_render/src/gradient.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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_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_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_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.
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions examples/ui/gradients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
};
Expand Down
6 changes: 3 additions & 3 deletions release-content/release-notes/ui_gradients.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.

Expand Down