Skip to content

Commit 5e3927b

Browse files
authored
UI gradients long hue paths fix (#20010)
# Objective The false and true arguments for the select statement in `lerp_hue_long` are misordered, resulting in it taking the wrong hue path: ![oklch-long-wrong-path](https://github.com/user-attachments/assets/68b733ab-be4b-4280-9346-4fdfccdb053a) ## Solution Swap the arguments around. Also fixed another case I found during testing. The hue was interpolated even when it is undefined for one of the endpoints (for example in a gradient from black to yellow). In those cases it shouldn't interpolate, instead it should return the hue of the other end point. ## Testing I added a `linear_gradient` module to the testbed `ui` example, run with: ``` cargo run --example testbed_ui ``` In the linear gradients screen (press space to switch) it shows a column of red to yellow linear gradients. The last gradient in the column uses the OKLCH long path, which should look like this: ![okchlong-red-yellow](https://github.com/user-attachments/assets/23537ff4-f01a-4a03-8473-9df57b2bfaf1) matching the same gradient in CSS: https://jsfiddle.net/fevshkdy/14/ if the correct hue path is chosen.
1 parent 3aed85a commit 5e3927b

File tree

2 files changed

+114
-24
lines changed

2 files changed

+114
-24
lines changed

crates/bevy_ui_render/src/gradient.wgsl

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -154,29 +154,28 @@ fn mix_linear_rgba_in_oklaba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f
154154
}
155155

156156
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;
157+
let max = max(max(c.r, c.g), c.b);
158+
let min = min(min(c.r, c.g), c.b);
159+
let l = (max + min) * 0.5;
160+
if max == min {
161+
return vec4(0., 0., l, c.a);
162+
} else {
163+
let delta = max - min;
164+
let s = delta / (1. - abs(2. * l - 1.));
165+
var h = 0.;
166+
if max == c.r {
167+
h = ((c.g - c.b) / delta) % 6.;
168+
} else if max == c.g {
169+
h = ((c.b - c.r) / delta) + 2.;
169170
} 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;
171+
h = ((c.r - c.g) / delta) + 4.;
175172
}
173+
h = h / 6.;
174+
return vec4<f32>(h, s, l, c.a);
176175
}
177-
return vec4<f32>(h, s, l, c.a);
178176
}
179177

178+
180179
fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
181180
let h = hsl.x;
182181
let s = hsl.y;
@@ -259,7 +258,7 @@ fn linear_rgba_to_oklcha(c: vec4<f32>) -> vec4<f32> {
259258
let o = linear_rgba_to_oklaba(c);
260259
let chroma = sqrt(o.y * o.y + o.z * o.z);
261260
let hue = atan2(o.z, o.y);
262-
return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.a);
261+
return vec4(o.x, chroma, rem_euclid(hue, TAU), o.a);
263262
}
264263

265264
fn oklcha_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
@@ -279,22 +278,26 @@ fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
279278

280279
fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 {
281280
let diff = rem_euclid(b - a + PI, TAU) - PI;
282-
return rem_euclid(a + select(diff - TAU, diff + TAU, 0. < diff) * t, TAU);
281+
return rem_euclid(a + (diff + select(TAU, -TAU, 0. < diff)) * t, TAU);
283282
}
284283

285284
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
285+
let ah = select(a.z, b.z, a.y == 0.);
286+
let bh = select(b.z, a.z, b.y == 0.);
286287
return vec4(
287288
mix(a.xy, b.xy, t),
288-
lerp_hue(a.z, b.z, t),
289+
lerp_hue(ah, bh, t),
289290
mix(a.a, b.a, t)
290291
);
291292
}
292293

293294
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
295+
let ah = select(a.z, b.z, a.y == 0.);
296+
let bh = select(b.z, a.z, b.y == 0.);
294297
return vec4(
295298
mix(a.xy, b.xy, t),
296-
lerp_hue_long(a.z, b.z, t),
297-
mix(a.a, b.a, t)
299+
lerp_hue_long(ah, bh, t),
300+
mix(a.w, b.w, t)
298301
);
299302
}
300303

examples/testbed/ui.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ fn main() {
2020
.add_systems(OnEnter(Scene::Overflow), overflow::setup)
2121
.add_systems(OnEnter(Scene::Slice), slice::setup)
2222
.add_systems(OnEnter(Scene::LayoutRounding), layout_rounding::setup)
23+
.add_systems(OnEnter(Scene::LinearGradient), linear_gradient::setup)
2324
.add_systems(OnEnter(Scene::RadialGradient), radial_gradient::setup)
2425
.add_systems(Update, switch_scene);
2526

@@ -42,6 +43,7 @@ enum Scene {
4243
Overflow,
4344
Slice,
4445
LayoutRounding,
46+
LinearGradient,
4547
RadialGradient,
4648
}
4749

@@ -56,7 +58,8 @@ impl Next for Scene {
5658
Scene::TextWrap => Scene::Overflow,
5759
Scene::Overflow => Scene::Slice,
5860
Scene::Slice => Scene::LayoutRounding,
59-
Scene::LayoutRounding => Scene::RadialGradient,
61+
Scene::LayoutRounding => Scene::LinearGradient,
62+
Scene::LinearGradient => Scene::RadialGradient,
6063
Scene::RadialGradient => Scene::Image,
6164
}
6265
}
@@ -551,6 +554,90 @@ mod layout_rounding {
551554
}
552555
}
553556

557+
mod linear_gradient {
558+
use bevy::color::palettes::css::RED;
559+
use bevy::color::palettes::css::YELLOW;
560+
use bevy::color::Color;
561+
use bevy::ecs::prelude::*;
562+
use bevy::render::camera::Camera2d;
563+
use bevy::state::state_scoped::DespawnOnExitState;
564+
use bevy::ui::AlignItems;
565+
use bevy::ui::BackgroundGradient;
566+
use bevy::ui::ColorStop;
567+
use bevy::ui::InterpolationColorSpace;
568+
use bevy::ui::JustifyContent;
569+
use bevy::ui::LinearGradient;
570+
use bevy::ui::Node;
571+
use bevy::ui::PositionType;
572+
use bevy::ui::Val;
573+
use bevy::utils::default;
574+
575+
pub fn setup(mut commands: Commands) {
576+
commands.spawn((Camera2d, DespawnOnExitState(super::Scene::LinearGradient)));
577+
commands
578+
.spawn((
579+
Node {
580+
flex_direction: bevy::ui::FlexDirection::Column,
581+
width: Val::Percent(100.),
582+
height: Val::Percent(100.),
583+
justify_content: JustifyContent::Center,
584+
align_items: AlignItems::Center,
585+
row_gap: Val::Px(5.),
586+
..default()
587+
},
588+
DespawnOnExitState(super::Scene::LinearGradient),
589+
))
590+
.with_children(|commands| {
591+
for stops in [
592+
vec![ColorStop::auto(RED), ColorStop::auto(YELLOW)],
593+
vec![
594+
ColorStop::auto(Color::BLACK),
595+
ColorStop::auto(RED),
596+
ColorStop::auto(Color::WHITE),
597+
],
598+
] {
599+
for color_space in [
600+
InterpolationColorSpace::LinearRgb,
601+
InterpolationColorSpace::Srgb,
602+
InterpolationColorSpace::OkLab,
603+
InterpolationColorSpace::OkLch,
604+
InterpolationColorSpace::OkLchLong,
605+
InterpolationColorSpace::Hsl,
606+
InterpolationColorSpace::HslLong,
607+
InterpolationColorSpace::Hsv,
608+
InterpolationColorSpace::HsvLong,
609+
] {
610+
commands.spawn((
611+
Node {
612+
justify_content: JustifyContent::SpaceEvenly,
613+
..Default::default()
614+
},
615+
children![(
616+
Node {
617+
height: Val::Px(30.),
618+
width: Val::Px(300.),
619+
..Default::default()
620+
},
621+
BackgroundGradient::from(LinearGradient {
622+
color_space,
623+
angle: LinearGradient::TO_RIGHT,
624+
stops: stops.clone(),
625+
}),
626+
children![
627+
Node {
628+
position_type: PositionType::Absolute,
629+
..default()
630+
},
631+
bevy::ui::widget::Text(format!("{color_space:?}")),
632+
]
633+
)],
634+
));
635+
}
636+
}
637+
});
638+
}
639+
}
640+
554641
mod radial_gradient {
555642
use bevy::color::palettes::css::RED;
556643
use bevy::color::palettes::tailwind::GRAY_700;

0 commit comments

Comments
 (0)