
# 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```
299 lines
14 KiB
Rust
299 lines
14 KiB
Rust
//! Simple example demonstrating linear gradients.
|
|
|
|
use bevy::color::palettes::css::BLUE;
|
|
use bevy::color::palettes::css::GREEN;
|
|
use bevy::color::palettes::css::INDIGO;
|
|
use bevy::color::palettes::css::LIME;
|
|
use bevy::color::palettes::css::ORANGE;
|
|
use bevy::color::palettes::css::RED;
|
|
use bevy::color::palettes::css::VIOLET;
|
|
use bevy::color::palettes::css::YELLOW;
|
|
use bevy::prelude::*;
|
|
use bevy::ui::ColorStop;
|
|
use std::f32::consts::TAU;
|
|
|
|
#[derive(Component)]
|
|
struct CurrentColorSpaceLabel;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, update)
|
|
.run();
|
|
}
|
|
|
|
fn setup(mut commands: Commands) {
|
|
commands.spawn(Camera2d);
|
|
|
|
commands
|
|
.spawn(Node {
|
|
flex_direction: FlexDirection::Column,
|
|
row_gap: Val::Px(20.),
|
|
margin: UiRect::all(Val::Px(20.)),
|
|
..Default::default()
|
|
})
|
|
.with_children(|commands| {
|
|
for (b, stops) in [
|
|
(
|
|
4.,
|
|
vec![
|
|
ColorStop::new(Color::WHITE, Val::Percent(15.)),
|
|
ColorStop::new(Color::BLACK, Val::Percent(85.)),
|
|
],
|
|
),
|
|
(4., vec![RED.into(), BLUE.into(), LIME.into()]),
|
|
(
|
|
0.,
|
|
vec![
|
|
RED.into(),
|
|
ColorStop::new(RED, Val::Percent(100. / 7.)),
|
|
ColorStop::new(ORANGE, Val::Percent(100. / 7.)),
|
|
ColorStop::new(ORANGE, Val::Percent(200. / 7.)),
|
|
ColorStop::new(YELLOW, Val::Percent(200. / 7.)),
|
|
ColorStop::new(YELLOW, Val::Percent(300. / 7.)),
|
|
ColorStop::new(GREEN, Val::Percent(300. / 7.)),
|
|
ColorStop::new(GREEN, Val::Percent(400. / 7.)),
|
|
ColorStop::new(BLUE, Val::Percent(400. / 7.)),
|
|
ColorStop::new(BLUE, Val::Percent(500. / 7.)),
|
|
ColorStop::new(INDIGO, Val::Percent(500. / 7.)),
|
|
ColorStop::new(INDIGO, Val::Percent(600. / 7.)),
|
|
ColorStop::new(VIOLET, Val::Percent(600. / 7.)),
|
|
VIOLET.into(),
|
|
],
|
|
),
|
|
] {
|
|
commands.spawn(Node::default()).with_children(|commands| {
|
|
commands
|
|
.spawn(Node {
|
|
flex_direction: FlexDirection::Column,
|
|
row_gap: Val::Px(5.),
|
|
..Default::default()
|
|
})
|
|
.with_children(|commands| {
|
|
for (w, h) in [(70., 70.), (35., 70.), (70., 35.)] {
|
|
commands
|
|
.spawn(Node {
|
|
column_gap: Val::Px(10.),
|
|
..Default::default()
|
|
})
|
|
.with_children(|commands| {
|
|
for angle in (0..8).map(|i| i as f32 * TAU / 8.) {
|
|
commands.spawn((
|
|
Node {
|
|
width: Val::Px(w),
|
|
height: Val::Px(h),
|
|
border: UiRect::all(Val::Px(b)),
|
|
..default()
|
|
},
|
|
BorderRadius::all(Val::Px(20.)),
|
|
BackgroundGradient::from(LinearGradient {
|
|
angle,
|
|
stops: stops.clone(),
|
|
..default()
|
|
}),
|
|
BorderGradient::from(LinearGradient {
|
|
angle: 3. * TAU / 8.,
|
|
stops: vec![
|
|
YELLOW.into(),
|
|
Color::WHITE.into(),
|
|
ORANGE.into(),
|
|
],
|
|
..default()
|
|
}),
|
|
));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
commands.spawn(Node::default()).with_children(|commands| {
|
|
commands.spawn((
|
|
Node {
|
|
aspect_ratio: Some(1.),
|
|
height: Val::Percent(100.),
|
|
border: UiRect::all(Val::Px(b)),
|
|
margin: UiRect::left(Val::Px(20.)),
|
|
..default()
|
|
},
|
|
BorderRadius::all(Val::Px(20.)),
|
|
BackgroundGradient::from(LinearGradient {
|
|
angle: 0.,
|
|
stops: stops.clone(),
|
|
..default()
|
|
}),
|
|
BorderGradient::from(LinearGradient {
|
|
angle: 3. * TAU / 8.,
|
|
stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
|
|
..default()
|
|
}),
|
|
AnimateMarker,
|
|
));
|
|
|
|
commands.spawn((
|
|
Node {
|
|
aspect_ratio: Some(1.),
|
|
height: Val::Percent(100.),
|
|
border: UiRect::all(Val::Px(b)),
|
|
margin: UiRect::left(Val::Px(20.)),
|
|
..default()
|
|
},
|
|
BorderRadius::all(Val::Px(20.)),
|
|
BackgroundGradient::from(RadialGradient {
|
|
stops: stops.clone(),
|
|
shape: RadialGradientShape::ClosestSide,
|
|
position: UiPosition::CENTER,
|
|
..default()
|
|
}),
|
|
BorderGradient::from(LinearGradient {
|
|
angle: 3. * TAU / 8.,
|
|
stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
|
|
..default()
|
|
}),
|
|
AnimateMarker,
|
|
));
|
|
commands.spawn((
|
|
Node {
|
|
aspect_ratio: Some(1.),
|
|
height: Val::Percent(100.),
|
|
border: UiRect::all(Val::Px(b)),
|
|
margin: UiRect::left(Val::Px(20.)),
|
|
..default()
|
|
},
|
|
BorderRadius::all(Val::Px(20.)),
|
|
BackgroundGradient::from(ConicGradient {
|
|
start: 0.,
|
|
stops: stops
|
|
.iter()
|
|
.map(|stop| AngularColorStop::auto(stop.color))
|
|
.collect(),
|
|
position: UiPosition::CENTER,
|
|
..default()
|
|
}),
|
|
BorderGradient::from(LinearGradient {
|
|
angle: 3. * TAU / 8.,
|
|
stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
|
|
..default()
|
|
}),
|
|
AnimateMarker,
|
|
));
|
|
});
|
|
});
|
|
}
|
|
|
|
let button = commands.spawn((
|
|
Button,
|
|
Node {
|
|
border: UiRect::all(Val::Px(2.0)),
|
|
padding: UiRect::axes(Val::Px(8.0), Val::Px(4.0)),
|
|
// horizontally center child text
|
|
justify_content: JustifyContent::Center,
|
|
// vertically center child text
|
|
align_items: AlignItems::Center,
|
|
..default()
|
|
},
|
|
BorderColor::all(Color::WHITE),
|
|
BorderRadius::MAX,
|
|
BackgroundColor(Color::BLACK),
|
|
children![(
|
|
Text::new("next color space"),
|
|
TextColor(Color::srgb(0.9, 0.9, 0.9)),
|
|
TextShadow::default(),
|
|
)]
|
|
)).observe(
|
|
|_trigger: On<Pointer<Over>>, mut border_query: Query<&mut BorderColor, With<Button>>| {
|
|
*border_query.single_mut().unwrap() = BorderColor::all(RED.into());
|
|
|
|
|
|
})
|
|
.observe(
|
|
|_trigger: On<Pointer<Out>>, mut border_query: Query<&mut BorderColor, With<Button>>| {
|
|
*border_query.single_mut().unwrap() = BorderColor::all(Color::WHITE);
|
|
})
|
|
.observe(
|
|
|_trigger: On<Pointer<Click>>,
|
|
mut gradients_query: Query<&mut BackgroundGradient>,
|
|
mut label_query: Query<
|
|
&mut Text,
|
|
With<CurrentColorSpaceLabel>,
|
|
>| {
|
|
let mut current_space = InterpolationColorSpace::default();
|
|
for mut gradients in gradients_query.iter_mut() {
|
|
for gradient in gradients.0.iter_mut() {
|
|
let space = match gradient {
|
|
Gradient::Linear(linear_gradient) => {
|
|
&mut linear_gradient.color_space
|
|
}
|
|
Gradient::Radial(radial_gradient) => {
|
|
&mut radial_gradient.color_space
|
|
}
|
|
Gradient::Conic(conic_gradient) => {
|
|
&mut conic_gradient.color_space
|
|
}
|
|
};
|
|
*space = match *space {
|
|
InterpolationColorSpace::OkLab => {
|
|
InterpolationColorSpace::OkLch
|
|
}
|
|
InterpolationColorSpace::OkLch => {
|
|
InterpolationColorSpace::OkLchLong
|
|
}
|
|
InterpolationColorSpace::OkLchLong => {
|
|
InterpolationColorSpace::Srgb
|
|
}
|
|
InterpolationColorSpace::Srgb => {
|
|
InterpolationColorSpace::LinearRgb
|
|
}
|
|
InterpolationColorSpace::LinearRgb => {
|
|
InterpolationColorSpace::Hsl
|
|
}
|
|
InterpolationColorSpace::Hsl => {
|
|
InterpolationColorSpace::HslLong
|
|
}
|
|
InterpolationColorSpace::HslLong => {
|
|
InterpolationColorSpace::Hsv
|
|
}
|
|
InterpolationColorSpace::Hsv => {
|
|
InterpolationColorSpace::HsvLong
|
|
}
|
|
InterpolationColorSpace::HsvLong => {
|
|
InterpolationColorSpace::OkLab
|
|
}
|
|
};
|
|
current_space = *space;
|
|
}
|
|
}
|
|
for mut label in label_query.iter_mut() {
|
|
label.0 = format!("{current_space:?}");
|
|
}
|
|
}
|
|
).id();
|
|
|
|
commands.spawn(
|
|
Node {
|
|
flex_direction: FlexDirection::Column,
|
|
row_gap: Val::Px(10.),
|
|
align_items: AlignItems::Center,
|
|
..Default::default()
|
|
}
|
|
).with_children(|commands| {
|
|
commands.spawn((Text::new(format!("{:?}", InterpolationColorSpace::default())), TextFont { font_size: 25., ..default() }, CurrentColorSpaceLabel));
|
|
|
|
})
|
|
.add_child(button);
|
|
});
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct AnimateMarker;
|
|
|
|
fn update(time: Res<Time>, mut query: Query<&mut BackgroundGradient, With<AnimateMarker>>) {
|
|
for mut gradients in query.iter_mut() {
|
|
for gradient in gradients.0.iter_mut() {
|
|
if let Gradient::Linear(LinearGradient { angle, .. }) = gradient {
|
|
*angle += 0.5 * time.delta_secs();
|
|
}
|
|
}
|
|
}
|
|
}
|