diff --git a/crates/bevy_ui/src/gradients.rs b/crates/bevy_ui/src/gradients.rs index 969e062cd7..eb1d255cc7 100644 --- a/crates/bevy_ui/src/gradients.rs +++ b/crates/bevy_ui/src/gradients.rs @@ -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 /// /// -#[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) -> 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) -> 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 /// /// -#[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) -> 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 + } +} diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index bd818c7d5b..e1c845d481 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -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, }, ); diff --git a/crates/bevy_ui/src/render/gradient.wgsl b/crates/bevy_ui/src/render/gradient.wgsl index 0223836f2d..074cf35a35 100644 --- a/crates/bevy_ui/src/render/gradient.wgsl +++ b/crates/bevy_ui/src/render/gradient.wgsl @@ -122,6 +122,89 @@ fn mix_linear_rgb_in_srgb_space(a: vec4, b: vec4, t: f32) -> vec4 return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t)); } +fn linear_rgb_to_oklab(c: vec4) -> vec4 { + 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) -> vec4 { + 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, b: vec4, t: f32) -> vec4 { + 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) -> vec4 { + 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) -> vec4 { + 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, b: vec4, t: f32) -> vec4 { + 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, b: vec4, t: f32) -> vec4 { + 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, b: vec4, t: f32) -> vec4 { + 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, b: vec4, t: f32) -> vec4 { + 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 } diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 6538840575..10e4e8dc8f 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -618,6 +618,7 @@ mod radial_gradient { stops: color_stops.clone(), position, shape, + ..default() }), )); }); diff --git a/examples/ui/button.rs b/examples/ui/button.rs index e533a84867..a402b5e7da 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -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 { diff --git a/examples/ui/gradients.rs b/examples/ui/gradients.rs index a354903708..0adc930a34 100644 --- a/examples/ui/gradients.rs +++ b/examples/ui/gradients.rs @@ -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>, mut border_query: Query<&mut BorderColor, With