Skip to content

Commit 5ea0e40

Browse files
authored
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```
1 parent 12da4e4 commit 5ea0e40

File tree

5 files changed

+210
-30
lines changed

5 files changed

+210
-30
lines changed

crates/bevy_ui/src/gradients.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,14 @@ pub enum InterpolationColorSpace {
631631
Srgb,
632632
/// Interpolates in linear sRGB space.
633633
LinearRgb,
634+
/// Interpolates in HSL space, taking the shortest hue path.
635+
Hsl,
636+
/// Interpolates in HSL space, taking the longest hue path.
637+
HslLong,
638+
/// Interpolates in HSV space, taking the shortest hue path.
639+
Hsv,
640+
/// Interpolates in HSV space, taking the longest hue path.
641+
HsvLong,
634642
}
635643

636644
/// Set the color space used for interpolation.

crates/bevy_ui_render/src/gradient.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ impl SpecializedRenderPipeline for GradientPipeline {
186186
InterpolationColorSpace::OkLchLong => "IN_OKLCH_LONG",
187187
InterpolationColorSpace::Srgb => "IN_SRGB",
188188
InterpolationColorSpace::LinearRgb => "IN_LINEAR_RGB",
189+
InterpolationColorSpace::Hsl => "IN_HSL",
190+
InterpolationColorSpace::HslLong => "IN_HSL_LONG",
191+
InterpolationColorSpace::Hsv => "IN_HSV",
192+
InterpolationColorSpace::HsvLong => "IN_HSV_LONG",
189193
};
190194

191195
let shader_defs = if key.anti_alias {

crates/bevy_ui_render/src/gradient.wgsl

Lines changed: 183 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct GradientVertexOutput {
3131
@location(0) uv: vec2<f32>,
3232
@location(1) @interpolate(flat) size: vec2<f32>,
3333
@location(2) @interpolate(flat) flags: u32,
34-
@location(3) @interpolate(flat) radius: vec4<f32>,
34+
@location(3) @interpolate(flat) radius: vec4<f32>,
3535
@location(4) @interpolate(flat) border: vec4<f32>,
3636

3737
// Position relative to the center of the rectangle.
@@ -114,27 +114,27 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4<f32> {
114114
}
115115
}
116116

117-
// This function converts two linear rgb colors to srgb space, mixes them, and then converts the result back to linear rgb space.
118-
fn mix_linear_rgb_in_srgb_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
117+
// This function converts two linear rgba colors to srgba space, mixes them, and then converts the result back to linear rgb space.
118+
fn mix_linear_rgba_in_srgba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
119119
let a_srgb = pow(a.rgb, vec3(1. / 2.2));
120120
let b_srgb = pow(b.rgb, vec3(1. / 2.2));
121121
let mixed_srgb = mix(a_srgb, b_srgb, t);
122122
return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t));
123123
}
124124

125-
fn linear_rgb_to_oklab(c: vec4<f32>) -> vec4<f32> {
125+
fn linear_rgba_to_oklaba(c: vec4<f32>) -> vec4<f32> {
126126
let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.);
127127
let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.);
128128
let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.);
129129
return vec4(
130130
0.21045426 * l + 0.7936178 * m - 0.004072047 * s,
131131
1.9779985 * l - 2.4285922 * m + 0.4505937 * s,
132-
0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
133-
c.w
132+
0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
133+
c.a
134134
);
135135
}
136136

137-
fn oklab_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
137+
fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
138138
let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z;
139139
let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z;
140140
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> {
145145
4.0767417 * l - 3.3077116 * m + 0.23096994 * s,
146146
-1.268438 * l + 2.6097574 * m - 0.34131938 * s,
147147
-0.0041960863 * l - 0.7034186 * m + 1.7076147 * s,
148-
c.w
148+
c.a
149149
);
150150
}
151151

152-
fn mix_linear_rgb_in_oklab_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
153-
return oklab_to_linear_rgba(mix(linear_rgb_to_oklab(a), linear_rgb_to_oklab(b), t));
152+
fn mix_linear_rgba_in_oklaba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
153+
return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t));
154+
}
155+
156+
fn linear_rgba_to_hsla(c: vec4<f32>) -> vec4<f32> {
157+
let maxc = max(max(c.r, c.g), c.b);
158+
let minc = min(min(c.r, c.g), c.b);
159+
let delta = maxc - minc;
160+
let l = (maxc + minc) * 0.5;
161+
var h: f32 = 0.0;
162+
var s: f32 = 0.0;
163+
if delta != 0.0 {
164+
s = delta / (1.0 - abs(2.0 * l - 1.0));
165+
if maxc == c.r {
166+
h = ((c.g - c.b) / delta) % 6.0;
167+
} else if maxc == c.g {
168+
h = ((c.b - c.r) / delta) + 2.0;
169+
} else {
170+
h = ((c.r - c.g) / delta) + 4.0;
171+
}
172+
h = h / 6.0;
173+
if h < 0.0 {
174+
h = h + 1.0;
175+
}
176+
}
177+
return vec4<f32>(h, s, l, c.a);
178+
}
179+
180+
fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
181+
let h = hsl.x;
182+
let s = hsl.y;
183+
let l = hsl.z;
184+
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
185+
let hp = h * 6.0;
186+
let x = c * (1.0 - abs(hp % 2.0 - 1.0));
187+
var r: f32 = 0.0;
188+
var g: f32 = 0.0;
189+
var b: f32 = 0.0;
190+
if 0.0 <= hp && hp < 1.0 {
191+
r = c; g = x; b = 0.0;
192+
} else if 1.0 <= hp && hp < 2.0 {
193+
r = x; g = c; b = 0.0;
194+
} else if 2.0 <= hp && hp < 3.0 {
195+
r = 0.0; g = c; b = x;
196+
} else if 3.0 <= hp && hp < 4.0 {
197+
r = 0.0; g = x; b = c;
198+
} else if 4.0 <= hp && hp < 5.0 {
199+
r = x; g = 0.0; b = c;
200+
} else if 5.0 <= hp && hp < 6.0 {
201+
r = c; g = 0.0; b = x;
202+
}
203+
let m = l - 0.5 * c;
204+
return vec4<f32>(r + m, g + m, b + m, hsl.a);
205+
}
206+
207+
fn linear_rgba_to_hsva(c: vec4<f32>) -> vec4<f32> {
208+
let maxc = max(max(c.r, c.g), c.b);
209+
let minc = min(min(c.r, c.g), c.b);
210+
let delta = maxc - minc;
211+
var h: f32 = 0.0;
212+
var s: f32 = 0.0;
213+
let v: f32 = maxc;
214+
if delta != 0.0 {
215+
s = delta / maxc;
216+
if maxc == c.r {
217+
h = ((c.g - c.b) / delta) % 6.0;
218+
} else if maxc == c.g {
219+
h = ((c.b - c.r) / delta) + 2.0;
220+
} else {
221+
h = ((c.r - c.g) / delta) + 4.0;
222+
}
223+
h = h / 6.0;
224+
if h < 0.0 {
225+
h = h + 1.0;
226+
}
227+
}
228+
return vec4<f32>(h, s, v, c.a);
229+
}
230+
231+
fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
232+
let h = hsva.x * 6.0;
233+
let s = hsva.y;
234+
let v = hsva.z;
235+
let c = v * s;
236+
let x = c * (1.0 - abs(h % 2.0 - 1.0));
237+
let m = v - c;
238+
var r: f32 = 0.0;
239+
var g: f32 = 0.0;
240+
var b: f32 = 0.0;
241+
if 0.0 <= h && h < 1.0 {
242+
r = c; g = x; b = 0.0;
243+
} else if 1.0 <= h && h < 2.0 {
244+
r = x; g = c; b = 0.0;
245+
} else if 2.0 <= h && h < 3.0 {
246+
r = 0.0; g = c; b = x;
247+
} else if 3.0 <= h && h < 4.0 {
248+
r = 0.0; g = x; b = c;
249+
} else if 4.0 <= h && h < 5.0 {
250+
r = x; g = 0.0; b = c;
251+
} else if 5.0 <= h && h < 6.0 {
252+
r = c; g = 0.0; b = x;
253+
}
254+
return vec4<f32>(r + m, g + m, b + m, hsva.a);
154255
}
155256

156257
/// hue is left in radians and not converted to degrees
157-
fn linear_rgb_to_oklch(c: vec4<f32>) -> vec4<f32> {
158-
let o = linear_rgb_to_oklab(c);
258+
fn linear_rgba_to_oklcha(c: vec4<f32>) -> vec4<f32> {
259+
let o = linear_rgba_to_oklaba(c);
159260
let chroma = sqrt(o.y * o.y + o.z * o.z);
160261
let hue = atan2(o.z, o.y);
161-
return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.w);
262+
return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.a);
162263
}
163264

164-
fn oklch_to_linear_rgb(c: vec4<f32>) -> vec4<f32> {
265+
fn oklcha_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
165266
let a = c.y * cos(c.z);
166267
let b = c.y * sin(c.z);
167-
return oklab_to_linear_rgba(vec4(c.x, a, b, c.w));
268+
return oklaba_to_linear_rgba(vec4(c.x, a, b, c.a));
168269
}
169270

170271
fn rem_euclid(a: f32, b: f32) -> f32 {
@@ -181,28 +282,75 @@ fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 {
181282
return rem_euclid(a + select(diff - TAU, diff + TAU, 0. < diff) * t, TAU);
182283
}
183284

184-
fn mix_oklch(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
285+
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
185286
return vec4(
186287
mix(a.xy, b.xy, t),
187288
lerp_hue(a.z, b.z, t),
188-
mix(a.w, b.w, t)
289+
mix(a.a, b.a, t)
189290
);
190291
}
191292

192-
fn mix_oklch_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
293+
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
193294
return vec4(
194295
mix(a.xy, b.xy, t),
195296
lerp_hue_long(a.z, b.z, t),
196-
mix(a.w, b.w, t)
297+
mix(a.a, b.a, t)
197298
);
198299
}
199300

200-
fn mix_linear_rgb_in_oklch_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
201-
return oklch_to_linear_rgb(mix_oklch(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t));
301+
fn mix_linear_rgba_in_oklcha_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
302+
return oklcha_to_linear_rgba(mix_oklcha(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
303+
}
304+
305+
fn mix_linear_rgba_in_oklcha_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
306+
return oklcha_to_linear_rgba(mix_oklcha_long(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
307+
}
308+
309+
fn mix_linear_rgba_in_hsva_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
310+
let ha = linear_rgba_to_hsva(a);
311+
let hb = linear_rgba_to_hsva(b);
312+
var h: f32;
313+
if ha.y == 0. {
314+
h = hb.x;
315+
} else if hb.y == 0. {
316+
h = ha.x;
317+
} else {
318+
h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
319+
}
320+
let s = mix(ha.y, hb.y, t);
321+
let v = mix(ha.z, hb.z, t);
322+
let a_alpha = mix(ha.a, hb.a, t);
323+
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
324+
}
325+
326+
fn mix_linear_rgba_in_hsva_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
327+
let ha = linear_rgba_to_hsva(a);
328+
let hb = linear_rgba_to_hsva(b);
329+
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
330+
let s = mix(ha.y, hb.y, t);
331+
let v = mix(ha.z, hb.z, t);
332+
let a_alpha = mix(ha.a, hb.a, t);
333+
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
334+
}
335+
336+
fn mix_linear_rgba_in_hsla_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
337+
let ha = linear_rgba_to_hsla(a);
338+
let hb = linear_rgba_to_hsla(b);
339+
let h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
340+
let s = mix(ha.y, hb.y, t);
341+
let l = mix(ha.z, hb.z, t);
342+
let a_alpha = mix(ha.a, hb.a, t);
343+
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
202344
}
203345

204-
fn mix_linear_rgb_in_oklch_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
205-
return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t));
346+
fn mix_linear_rgba_in_hsla_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
347+
let ha = linear_rgba_to_hsla(a);
348+
let hb = linear_rgba_to_hsla(b);
349+
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
350+
let s = mix(ha.y, hb.y, t);
351+
let l = mix(ha.z, hb.z, t);
352+
let a_alpha = mix(ha.a, hb.a, t);
353+
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
206354
}
207355

208356
// 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(
277425
}
278426

279427
#ifdef IN_SRGB
280-
return mix_linear_rgb_in_srgb_space(start_color, end_color, t);
428+
return mix_linear_rgba_in_srgba_space(start_color, end_color, t);
281429
#else ifdef IN_OKLAB
282-
return mix_linear_rgb_in_oklab_space(start_color, end_color, t);
430+
return mix_linear_rgba_in_oklaba_space(start_color, end_color, t);
283431
#else ifdef IN_OKLCH
284-
return mix_linear_rgb_in_oklch_space(start_color, end_color, t);
432+
return mix_linear_rgba_in_oklcha_space(start_color, end_color, t);
285433
#else ifdef IN_OKLCH_LONG
286-
return mix_linear_rgb_in_oklch_space_long(start_color, end_color, t);
434+
return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t);
435+
#else ifdef IN_HSV
436+
return mix_linear_rgba_in_hsva_space(start_color, end_color, t);
437+
#else ifdef IN_HSV_LONG
438+
return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t);
439+
#else ifdef IN_HSL
440+
return mix_linear_rgba_in_hsla_space(start_color, end_color, t);
441+
#else ifdef IN_HSL_LONG
442+
return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t);
287443
#else
288444
return mix(start_color, end_color, t);
289445
#endif

examples/ui/gradients.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,18 @@ fn setup(mut commands: Commands) {
245245
InterpolationColorSpace::LinearRgb
246246
}
247247
InterpolationColorSpace::LinearRgb => {
248+
InterpolationColorSpace::Hsl
249+
}
250+
InterpolationColorSpace::Hsl => {
251+
InterpolationColorSpace::HslLong
252+
}
253+
InterpolationColorSpace::HslLong => {
254+
InterpolationColorSpace::Hsv
255+
}
256+
InterpolationColorSpace::Hsv => {
257+
InterpolationColorSpace::HsvLong
258+
}
259+
InterpolationColorSpace::HsvLong => {
248260
InterpolationColorSpace::OkLab
249261
}
250262
};

release-content/release-notes/ui_gradients.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: UI Gradients
33
authors: ["@Ickshonpe"]
4-
pull_requests: [18139, 19330]
4+
pull_requests: [18139, 19330, 19992]
55
---
66

77
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
1414
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:
1515

1616
```rust
17-
vec![vec![ColorStop::new(RED, Val::Percent(90.), ColorStop::new(Color::GREEN, Val::Percent(10.))
17+
vec![ColorStop::new(RED, Val::Percent(90.), ColorStop::new(GREEN), Val::Percent(10.))]
1818
```
1919

2020
the colors will be reordered and the gradient will transition from green at 10% to red at 90%.
2121

22-
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.
22+
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.
2323

2424
For sharp stops with no interpolated transition, place two stops at the same point.
2525

0 commit comments

Comments
 (0)