Color interpolation in OKLab, OKLCH spaces for UI gradients (#19330)
# Objective Add support for interpolation in OKLab and OKLCH color spaces for UI gradients. ## Solution * New `InterpolationColorSpace` enum with `OkLab`, `OkLch`, `OkLchLong`, `Srgb` and `LinearRgb` variants. * Added a color space specialization to the gradients pipeline. * Added support for interpolation in OkLCH and OkLAB color spaces to the gradients shader. OKLCH interpolation supports both short and long hue paths. This is mostly based on the conversion functions from `bevy_color` except that interpolation in polar space uses radians. * Added `color_space` fields to each gradient type. ## Testing The `gradients` example has been updated to demonstrate the different color interpolation methods. Press space to cycle through the different options. --- ## Showcase 
This commit is contained in:
parent
5d5a95fa6e
commit
45a3f3d138
@ -3,6 +3,7 @@ use bevy_color::{Color, Srgba};
|
||||
use bevy_ecs::component::Component;
|
||||
use bevy_math::Vec2;
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_utils::default;
|
||||
use core::{f32, f32::consts::TAU};
|
||||
|
||||
/// A color stop for a gradient
|
||||
@ -205,7 +206,7 @@ impl Default for AngularColorStop {
|
||||
/// A linear gradient
|
||||
///
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
|
||||
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||
#[derive(Default, Clone, PartialEq, Debug, Reflect)]
|
||||
#[reflect(PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
@ -213,6 +214,8 @@ impl Default for AngularColorStop {
|
||||
reflect(Serialize, Deserialize)
|
||||
)]
|
||||
pub struct LinearGradient {
|
||||
/// The color space used for interpolation.
|
||||
pub color_space: InterpolationColorSpace,
|
||||
/// The direction of the gradient in radians.
|
||||
/// An angle of `0.` points upward, with the value increasing in the clockwise direction.
|
||||
pub angle: f32,
|
||||
@ -240,7 +243,11 @@ impl LinearGradient {
|
||||
|
||||
/// Create a new linear gradient
|
||||
pub fn new(angle: f32, stops: Vec<ColorStop>) -> Self {
|
||||
Self { angle, stops }
|
||||
Self {
|
||||
angle,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A linear gradient transitioning from bottom to top
|
||||
@ -248,6 +255,7 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: Self::TO_TOP,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,6 +264,7 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: Self::TO_TOP_RIGHT,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,6 +273,7 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: Self::TO_RIGHT,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,6 +282,7 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: Self::TO_BOTTOM_RIGHT,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,6 +291,7 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: Self::TO_BOTTOM,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,6 +300,7 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: Self::TO_BOTTOM_LEFT,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -296,6 +309,7 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: Self::TO_LEFT,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,6 +318,7 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: Self::TO_TOP_LEFT,
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -312,8 +327,14 @@ impl LinearGradient {
|
||||
Self {
|
||||
angle: degrees.to_radians(),
|
||||
stops,
|
||||
color_space: InterpolationColorSpace::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A radial gradient
|
||||
@ -327,6 +348,8 @@ impl LinearGradient {
|
||||
reflect(Serialize, Deserialize)
|
||||
)]
|
||||
pub struct RadialGradient {
|
||||
/// The color space used for interpolation.
|
||||
pub color_space: InterpolationColorSpace,
|
||||
/// The center of the radial gradient
|
||||
pub position: UiPosition,
|
||||
/// Defines the end shape of the radial gradient
|
||||
@ -339,11 +362,17 @@ impl RadialGradient {
|
||||
/// Create a new radial gradient
|
||||
pub fn new(position: UiPosition, shape: RadialGradientShape, stops: Vec<ColorStop>) -> Self {
|
||||
Self {
|
||||
color_space: default(),
|
||||
position,
|
||||
shape,
|
||||
stops,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RadialGradient {
|
||||
@ -352,6 +381,7 @@ impl Default for RadialGradient {
|
||||
position: UiPosition::CENTER,
|
||||
shape: RadialGradientShape::ClosestCorner,
|
||||
stops: Vec::new(),
|
||||
color_space: default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -359,7 +389,7 @@ impl Default for RadialGradient {
|
||||
/// A conic gradient
|
||||
///
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/conic-gradient>
|
||||
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||
#[derive(Default, Clone, PartialEq, Debug, Reflect)]
|
||||
#[reflect(PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
@ -367,6 +397,8 @@ impl Default for RadialGradient {
|
||||
reflect(Serialize, Deserialize)
|
||||
)]
|
||||
pub struct ConicGradient {
|
||||
/// The color space used for interpolation.
|
||||
pub color_space: InterpolationColorSpace,
|
||||
/// The starting angle of the gradient in radians
|
||||
pub start: f32,
|
||||
/// The center of the conic gradient
|
||||
@ -379,6 +411,7 @@ impl ConicGradient {
|
||||
/// Create a new conic gradient
|
||||
pub fn new(position: UiPosition, stops: Vec<AngularColorStop>) -> Self {
|
||||
Self {
|
||||
color_space: default(),
|
||||
start: 0.,
|
||||
position,
|
||||
stops,
|
||||
@ -396,6 +429,11 @@ impl ConicGradient {
|
||||
self.position = position;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Reflect)]
|
||||
@ -573,3 +611,79 @@ impl RadialGradientShape {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The color space used for interpolation.
|
||||
#[derive(Default, Copy, Clone, Hash, Debug, PartialEq, Eq, Reflect)]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
reflect(Serialize, Deserialize)
|
||||
)]
|
||||
pub enum InterpolationColorSpace {
|
||||
/// Interpolates in `OKLab` space.
|
||||
#[default]
|
||||
OkLab,
|
||||
/// Interpolates in OKLCH space, taking the shortest hue path.
|
||||
OkLch,
|
||||
/// Interpolates in OKLCH space, taking the longest hue path.
|
||||
OkLchLong,
|
||||
/// Interpolates in sRGB space.
|
||||
Srgb,
|
||||
/// Interpolates in linear sRGB space.
|
||||
LinearRgb,
|
||||
}
|
||||
|
||||
/// Set the color space used for interpolation.
|
||||
pub trait InColorSpace: Sized {
|
||||
/// Interpolate in the given `color_space`.
|
||||
fn in_color_space(self, color_space: InterpolationColorSpace) -> Self;
|
||||
|
||||
/// Interpolate in `OKLab` space.
|
||||
fn in_oklab(self) -> Self {
|
||||
self.in_color_space(InterpolationColorSpace::OkLab)
|
||||
}
|
||||
|
||||
/// Interpolate in OKLCH space (short hue path).
|
||||
fn in_oklch(self) -> Self {
|
||||
self.in_color_space(InterpolationColorSpace::OkLch)
|
||||
}
|
||||
|
||||
/// Interpolate in OKLCH space (long hue path).
|
||||
fn in_oklch_long(self) -> Self {
|
||||
self.in_color_space(InterpolationColorSpace::OkLchLong)
|
||||
}
|
||||
|
||||
/// Interpolate in sRGB space.
|
||||
fn in_srgb(self) -> Self {
|
||||
self.in_color_space(InterpolationColorSpace::Srgb)
|
||||
}
|
||||
|
||||
/// Interpolate in linear sRGB space.
|
||||
fn in_linear_rgb(self) -> Self {
|
||||
self.in_color_space(InterpolationColorSpace::LinearRgb)
|
||||
}
|
||||
}
|
||||
|
||||
impl InColorSpace for LinearGradient {
|
||||
/// Interpolate in the given `color_space`.
|
||||
fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InColorSpace for RadialGradient {
|
||||
/// Interpolate in the given `color_space`.
|
||||
fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl InColorSpace for ConicGradient {
|
||||
/// Interpolate in the given `color_space`.
|
||||
fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +140,7 @@ pub fn compute_gradient_line_length(angle: f32, size: Vec2) -> f32 {
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct UiGradientPipelineKey {
|
||||
anti_alias: bool,
|
||||
color_space: InterpolationColorSpace,
|
||||
pub hdr: bool,
|
||||
}
|
||||
|
||||
@ -180,10 +181,18 @@ impl SpecializedRenderPipeline for GradientPipeline {
|
||||
VertexFormat::Float32,
|
||||
],
|
||||
);
|
||||
let color_space = match key.color_space {
|
||||
InterpolationColorSpace::OkLab => "IN_OKLAB",
|
||||
InterpolationColorSpace::OkLch => "IN_OKLCH",
|
||||
InterpolationColorSpace::OkLchLong => "IN_OKLCH_LONG",
|
||||
InterpolationColorSpace::Srgb => "IN_SRGB",
|
||||
InterpolationColorSpace::LinearRgb => "IN_LINEAR_RGB",
|
||||
};
|
||||
|
||||
let shader_defs = if key.anti_alias {
|
||||
vec!["ANTI_ALIAS".into()]
|
||||
vec![color_space.into(), "ANTI_ALIAS".into()]
|
||||
} else {
|
||||
Vec::new()
|
||||
vec![color_space.into()]
|
||||
};
|
||||
|
||||
RenderPipelineDescriptor {
|
||||
@ -254,6 +263,7 @@ pub struct ExtractedGradient {
|
||||
/// Ordering: left, top, right, bottom.
|
||||
pub border: BorderRect,
|
||||
pub resolved_gradient: ResolvedGradient,
|
||||
pub color_space: InterpolationColorSpace,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
@ -422,7 +432,11 @@ pub fn extract_gradients(
|
||||
continue;
|
||||
}
|
||||
match gradient {
|
||||
Gradient::Linear(LinearGradient { angle, stops }) => {
|
||||
Gradient::Linear(LinearGradient {
|
||||
color_space,
|
||||
angle,
|
||||
stops,
|
||||
}) => {
|
||||
let length = compute_gradient_line_length(*angle, uinode.size);
|
||||
|
||||
let range_start = extracted_color_stops.0.len();
|
||||
@ -452,9 +466,11 @@ pub fn extract_gradients(
|
||||
border_radius: uinode.border_radius,
|
||||
border: uinode.border,
|
||||
resolved_gradient: ResolvedGradient::Linear { angle: *angle },
|
||||
color_space: *color_space,
|
||||
});
|
||||
}
|
||||
Gradient::Radial(RadialGradient {
|
||||
color_space,
|
||||
position: center,
|
||||
shape,
|
||||
stops,
|
||||
@ -500,9 +516,11 @@ pub fn extract_gradients(
|
||||
border_radius: uinode.border_radius,
|
||||
border: uinode.border,
|
||||
resolved_gradient: ResolvedGradient::Radial { center: c, size },
|
||||
color_space: *color_space,
|
||||
});
|
||||
}
|
||||
Gradient::Conic(ConicGradient {
|
||||
color_space,
|
||||
start,
|
||||
position: center,
|
||||
stops,
|
||||
@ -557,6 +575,7 @@ pub fn extract_gradients(
|
||||
start: *start,
|
||||
center: g_start,
|
||||
},
|
||||
color_space: *color_space,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -601,6 +620,7 @@ pub fn queue_gradient(
|
||||
&gradients_pipeline,
|
||||
UiGradientPipelineKey {
|
||||
anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)),
|
||||
color_space: gradient.color_space,
|
||||
hdr: view.hdr,
|
||||
},
|
||||
);
|
||||
|
@ -122,6 +122,89 @@ fn mix_linear_rgb_in_srgb_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32>
|
||||
return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t));
|
||||
}
|
||||
|
||||
fn linear_rgb_to_oklab(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
|
||||
);
|
||||
}
|
||||
|
||||
fn oklab_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;
|
||||
let l = l_ * l_ * l_;
|
||||
let m = m_ * m_ * m_;
|
||||
let s = s_ * s_ * s_;
|
||||
return vec4(
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// 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);
|
||||
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);
|
||||
}
|
||||
|
||||
fn oklch_to_linear_rgb(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));
|
||||
}
|
||||
|
||||
fn rem_euclid(a: f32, b: f32) -> f32 {
|
||||
return ((a % b) + b) % b;
|
||||
}
|
||||
|
||||
fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
|
||||
let diff = rem_euclid(b - a + PI, TAU) - PI;
|
||||
return rem_euclid(a + diff * t, TAU);
|
||||
}
|
||||
|
||||
fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 {
|
||||
let diff = rem_euclid(b - a + PI, TAU) - PI;
|
||||
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> {
|
||||
return vec4(
|
||||
mix(a.xy, b.xy, t),
|
||||
lerp_hue(a.z, b.z, t),
|
||||
mix(a.w, b.w, t)
|
||||
);
|
||||
}
|
||||
|
||||
fn mix_oklch_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)
|
||||
);
|
||||
}
|
||||
|
||||
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_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));
|
||||
}
|
||||
|
||||
// These functions are used to calculate the distance in gradient space from the start of the gradient to the point.
|
||||
// The distance in gradient space is then used to interpolate between the start and end colors.
|
||||
|
||||
@ -192,7 +275,16 @@ fn interpolate_gradient(
|
||||
} else {
|
||||
t = 0.5 * (1 + (t - hint) / (1.0 - hint));
|
||||
}
|
||||
|
||||
// Only color interpolation in SRGB space is supported atm.
|
||||
|
||||
#ifdef IN_SRGB
|
||||
return mix_linear_rgb_in_srgb_space(start_color, end_color, t);
|
||||
#else ifdef IN_OKLAB
|
||||
return mix_linear_rgb_in_oklab_space(start_color, end_color, t);
|
||||
#else ifdef IN_OKLCH
|
||||
return mix_linear_rgb_in_oklch_space(start_color, end_color, t);
|
||||
#else ifdef IN_OKLCH_LONG
|
||||
return mix_linear_rgb_in_oklch_space_long(start_color, end_color, t);
|
||||
#else
|
||||
return mix(start_color, end_color, t);
|
||||
#endif
|
||||
}
|
||||
|
@ -618,6 +618,7 @@ mod radial_gradient {
|
||||
stops: color_stops.clone(),
|
||||
position,
|
||||
shape,
|
||||
..default()
|
||||
}),
|
||||
));
|
||||
});
|
||||
|
@ -93,9 +93,9 @@ fn button(asset_server: &AssetServer) -> impl Bundle + use<> {
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
BorderColor::all(Color::BLACK),
|
||||
BorderColor::all(Color::WHITE),
|
||||
BorderRadius::MAX,
|
||||
BackgroundColor(NORMAL_BUTTON),
|
||||
BackgroundColor(Color::BLACK),
|
||||
children![(
|
||||
Text::new("Button"),
|
||||
TextFont {
|
||||
|
@ -12,6 +12,9 @@ use bevy::prelude::*;
|
||||
use bevy::ui::ColorStop;
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
#[derive(Component)]
|
||||
struct CurrentColorSpaceLabel;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
@ -87,6 +90,7 @@ fn setup(mut commands: Commands) {
|
||||
BackgroundGradient::from(LinearGradient {
|
||||
angle,
|
||||
stops: stops.clone(),
|
||||
..default()
|
||||
}),
|
||||
BorderGradient::from(LinearGradient {
|
||||
angle: 3. * TAU / 8.,
|
||||
@ -95,6 +99,7 @@ fn setup(mut commands: Commands) {
|
||||
Color::WHITE.into(),
|
||||
ORANGE.into(),
|
||||
],
|
||||
..default()
|
||||
}),
|
||||
));
|
||||
}
|
||||
@ -115,10 +120,12 @@ fn setup(mut commands: Commands) {
|
||||
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,
|
||||
));
|
||||
@ -136,10 +143,12 @@ fn setup(mut commands: Commands) {
|
||||
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,
|
||||
));
|
||||
@ -159,16 +168,107 @@ fn setup(mut commands: Commands) {
|
||||
.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::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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,7 @@ fn setup(mut commands: Commands) {
|
||||
AngularColorStop::auto(YELLOW.with_alpha(0.)),
|
||||
AngularColorStop::auto(YELLOW.with_alpha(0.)),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
RadialGradient {
|
||||
@ -63,6 +64,7 @@ fn setup(mut commands: Commands) {
|
||||
ColorStop::auto(YELLOW.with_alpha(0.1)),
|
||||
ColorStop::auto(YELLOW.with_alpha(0.)),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
LinearGradient {
|
||||
@ -71,6 +73,7 @@ fn setup(mut commands: Commands) {
|
||||
ColorStop::auto(Color::BLACK),
|
||||
ColorStop::auto(Color::BLACK.with_alpha(0.)),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
LinearGradient {
|
||||
@ -79,6 +82,7 @@ fn setup(mut commands: Commands) {
|
||||
ColorStop::auto(Color::BLACK),
|
||||
ColorStop::auto(Color::BLACK.with_alpha(0.)),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
]),
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: UI Gradients
|
||||
authors: ["@Ickshonpe"]
|
||||
pull_requests: [18139]
|
||||
pull_requests: [18139, 19330]
|
||||
---
|
||||
|
||||
Support for UI node's that display a gradient that transitions smoothly between two or more colors.
|
||||
@ -10,7 +10,7 @@ To draw a UI node with a gradient insert the components `BackgroundGradient` and
|
||||
|
||||
The are three gradient structs corresponding to the three types of gradients supported: `LinearGradient`, `ConicGradient` and `RadialGradient`. These are then wrapped by the `Gradient` enum discriminator which has `Linear`, `Conic` and `Radial` variants.
|
||||
|
||||
Each gradient type consists of the geometric properties for that gradient and a list of color stops.
|
||||
Each gradient type consists of the geometric properties for that gradient, a list of color stops and the color space used for interpolation.
|
||||
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
|
||||
@ -19,7 +19,7 @@ vec![vec![ColorStop::new(RED, Val::Percent(90.), ColorStop::new(Color::GREEN, Va
|
||||
|
||||
the colors will be reordered and the gradient will transition from green at 10% to red at 90%.
|
||||
|
||||
Colors are interpolated between the stops in SRGB space. 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 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.
|
||||
|
||||
For sharp stops with no interpolated transition, place two stops at the same point.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user