Preconvert colors before sending to shader (#20074)

# Objective

- Fixes #20008 - Preconvert colors before sending them to the UI
gradients shader for better performance

  ## Solution

- Modified `prepare_gradient` in `gradient.rs` to convert colors from
`LinearRgba` to `Srgba` on the CPU before sending to the GPU
- Updated the gradient shader to remove per-pixel color space
conversions since colors are now pre-converted
  - Added documentation to clarify that vertex colors are in sRGB space

This optimization reduces the number of power operations per pixel from
3 to 1:
- **Before**: Convert start color to sRGB, convert end color to sRGB,
mix, convert back to linear (3 pow operations per pixel)
- **After**: Colors pre-converted on CPU, mix in sRGB space, convert
back to linear (1 pow operation per pixel)

  ## Testing

- Verified that the UI gradient examples (`cargo run --example
gradients`) compile and render correctly
- The visual output should remain identical while performance improves,
especially for large gradient areas
- Changes maintain the same color interpolation behavior (mixing in sRGB
space)

  To test:
1. Run `cargo run --example gradients` or `cargo run --example
stacked_gradients`
  2. Verify gradients render correctly

---------

Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
This commit is contained in:
Tyler Critchlow 2025-07-16 12:06:45 -04:00 committed by GitHub
parent 8e14d99fb9
commit 3fc49f00c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 381 additions and 182 deletions

View File

@ -3068,6 +3068,17 @@ description = "Test rendering of many UI elements"
category = "Stress Tests"
wasm = true
[[example]]
name = "many_gradients"
path = "examples/stress_tests/many_gradients.rs"
doc-scrape-examples = true
[package.metadata.example.many_gradients]
name = "Many Gradients"
description = "Stress test for gradient rendering performance"
category = "Stress Tests"
wasm = true
[[example]]
name = "many_cameras_lights"
path = "examples/stress_tests/many_cameras_lights.rs"

View File

@ -7,7 +7,7 @@ use core::{
use super::shader_flags::BORDER_ALL;
use crate::*;
use bevy_asset::*;
use bevy_color::{ColorToComponents, LinearRgba};
use bevy_color::{ColorToComponents, Hsla, Hsva, LinearRgba, Oklaba, Oklcha, Srgba};
use bevy_ecs::{
prelude::Component,
system::{
@ -654,6 +654,44 @@ struct UiGradientVertex {
hint: f32,
}
fn convert_color_to_space(color: LinearRgba, space: InterpolationColorSpace) -> [f32; 4] {
match space {
InterpolationColorSpace::Oklaba => {
let oklaba: Oklaba = color.into();
[oklaba.lightness, oklaba.a, oklaba.b, oklaba.alpha]
}
InterpolationColorSpace::Oklcha | InterpolationColorSpace::OklchaLong => {
let oklcha: Oklcha = color.into();
[
oklcha.lightness,
oklcha.chroma,
oklcha.hue.to_radians(),
oklcha.alpha,
]
}
InterpolationColorSpace::Srgba => {
let srgba: Srgba = color.into();
[srgba.red, srgba.green, srgba.blue, srgba.alpha]
}
InterpolationColorSpace::LinearRgba => color.to_f32_array(),
InterpolationColorSpace::Hsla | InterpolationColorSpace::HslaLong => {
let hsla: Hsla = color.into();
// Normalize hue to 0..1 range for shader
[
hsla.hue / 360.0,
hsla.saturation,
hsla.lightness,
hsla.alpha,
]
}
InterpolationColorSpace::Hsva | InterpolationColorSpace::HsvaLong => {
let hsva: Hsva = color.into();
// Normalize hue to 0..1 range for shader
[hsva.hue / 360.0, hsva.saturation, hsva.value, hsva.alpha]
}
}
}
pub fn prepare_gradient(
mut commands: Commands,
render_device: Res<RenderDevice>,
@ -804,8 +842,9 @@ pub fn prepare_gradient(
continue;
}
}
let start_color = start_stop.0.to_f32_array();
let end_color = end_stop.0.to_f32_array();
let start_color =
convert_color_to_space(start_stop.0, gradient.color_space);
let end_color = convert_color_to_space(end_stop.0, gradient.color_space);
let mut stop_flags = flags;
if 0. < start_stop.1
&& (stop_index == gradient.stops_range.start || segment_count == 0)

View File

@ -114,26 +114,6 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4<f32> {
}
}
// This function converts two linear rgba colors to srgba space, mixes them, and then converts the result back to linear rgb space.
fn mix_linear_rgba_in_srgba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let a_srgb = pow(a.rgb, vec3(1. / 2.2));
let b_srgb = pow(b.rgb, vec3(1. / 2.2));
let mixed_srgb = mix(a_srgb, b_srgb, t);
return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t));
}
fn linear_rgba_to_oklaba(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.a
);
}
fn oklaba_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;
@ -149,33 +129,6 @@ fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
);
}
fn mix_linear_rgba_in_oklaba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t));
}
fn linear_rgba_to_hsla(c: vec4<f32>) -> vec4<f32> {
let max = max(max(c.r, c.g), c.b);
let min = min(min(c.r, c.g), c.b);
let l = (max + min) * 0.5;
if max == min {
return vec4(0., 0., l, c.a);
} else {
let delta = max - min;
let s = delta / (1. - abs(2. * l - 1.));
var h = 0.;
if max == c.r {
h = ((c.g - c.b) / delta) % 6.;
} else if max == c.g {
h = ((c.b - c.r) / delta) + 2.;
} else {
h = ((c.r - c.g) / delta) + 4.;
}
h = h / 6.;
return vec4<f32>(h, s, l, c.a);
}
}
fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
let h = hsl.x;
let s = hsl.y;
@ -203,30 +156,6 @@ fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
return vec4<f32>(r + m, g + m, b + m, hsl.a);
}
fn linear_rgba_to_hsva(c: vec4<f32>) -> vec4<f32> {
let maxc = max(max(c.r, c.g), c.b);
let minc = min(min(c.r, c.g), c.b);
let delta = maxc - minc;
var h: f32 = 0.0;
var s: f32 = 0.0;
let v: f32 = maxc;
if delta != 0.0 {
s = delta / maxc;
if maxc == c.r {
h = ((c.g - c.b) / delta) % 6.0;
} else if maxc == c.g {
h = ((c.b - c.r) / delta) + 2.0;
} else {
h = ((c.r - c.g) / delta) + 4.0;
}
h = h / 6.0;
if h < 0.0 {
h = h + 1.0;
}
}
return vec4<f32>(h, s, v, c.a);
}
fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
let h = hsva.x * 6.0;
let s = hsva.y;
@ -253,14 +182,6 @@ fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
return vec4<f32>(r + m, g + m, b + m, hsva.a);
}
/// hue is left in radians and not converted to degrees
fn linear_rgba_to_oklcha(c: vec4<f32>) -> vec4<f32> {
let o = linear_rgba_to_oklaba(c);
let chroma = sqrt(o.y * o.y + o.z * o.z);
let hue = atan2(o.z, o.y);
return vec4(o.x, chroma, rem_euclid(hue, TAU), o.a);
}
fn oklcha_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
let a = c.y * cos(c.z);
let b = c.y * sin(c.z);
@ -271,90 +192,6 @@ 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 + (diff + select(TAU, -TAU, 0. < diff)) * t, TAU);
}
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ah = select(a.z, b.z, a.y == 0.);
let bh = select(b.z, a.z, b.y == 0.);
return vec4(
mix(a.xy, b.xy, t),
lerp_hue(ah, bh, t),
mix(a.a, b.a, t)
);
}
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ah = select(a.z, b.z, a.y == 0.);
let bh = select(b.z, a.z, b.y == 0.);
return vec4(
mix(a.xy, b.xy, t),
lerp_hue_long(ah, bh, t),
mix(a.w, b.w, t)
);
}
fn mix_linear_rgba_in_oklcha_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklcha_to_linear_rgba(mix_oklcha(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
}
fn mix_linear_rgba_in_oklcha_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklcha_to_linear_rgba(mix_oklcha_long(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
}
fn mix_linear_rgba_in_hsva_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsva(a);
let hb = linear_rgba_to_hsva(b);
var h: f32;
if ha.y == 0. {
h = hb.x;
} else if hb.y == 0. {
h = ha.x;
} else {
h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
}
let s = mix(ha.y, hb.y, t);
let v = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
}
fn mix_linear_rgba_in_hsva_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsva(a);
let hb = linear_rgba_to_hsva(b);
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let v = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
}
fn mix_linear_rgba_in_hsla_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsla(a);
let hb = linear_rgba_to_hsla(b);
let h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let l = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
}
fn mix_linear_rgba_in_hsla_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsla(a);
let hb = linear_rgba_to_hsla(b);
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let l = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
}
// 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.
@ -386,6 +223,105 @@ fn conic_distance(
return (((angle - start) % TAU) + TAU) % TAU;
}
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.z - a.z;
var adjusted_hue = a.z;
if abs(hue_diff) > PI {
if hue_diff > 0.0 {
adjusted_hue = a.z + (hue_diff - TAU) * t;
} else {
adjusted_hue = a.z + (hue_diff + TAU) * t;
}
} else {
adjusted_hue = a.z + hue_diff * t;
}
return vec4(
mix(a.x, b.x, t),
mix(a.y, b.y, t),
rem_euclid(adjusted_hue, TAU),
mix(a.w, b.w, t)
);
}
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.z - a.z;
var adjusted_hue = a.z;
if abs(hue_diff) < PI {
if hue_diff >= 0.0 {
adjusted_hue = a.z + (hue_diff - TAU) * t;
} else {
adjusted_hue = a.z + (hue_diff + TAU) * t;
}
} else {
adjusted_hue = a.z + hue_diff * t;
}
return vec4(
mix(a.x, b.x, t),
mix(a.y, b.y, t),
rem_euclid(adjusted_hue, TAU),
mix(a.w, b.w, t)
);
}
fn mix_hsla(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return vec4(
fract(a.x + (fract(b.x - a.x + 0.5) - 0.5) * t),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsla_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let d = fract(b.x - a.x + 0.5) - 0.5;
return vec4(
fract(a.x + (d + select(1., -1., 0. < d)) * t),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsva(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.x - a.x;
var adjusted_hue = a.x;
if abs(hue_diff) > 0.5 {
if hue_diff > 0.0 {
adjusted_hue = a.x + (hue_diff - 1.0) * t;
} else {
adjusted_hue = a.x + (hue_diff + 1.0) * t;
}
} else {
adjusted_hue = a.x + hue_diff * t;
}
return vec4(
fract(adjusted_hue),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsva_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.x - a.x;
var adjusted_hue = a.x;
if abs(hue_diff) < 0.5 {
if hue_diff >= 0.0 {
adjusted_hue = a.x + (hue_diff - 1.0) * t;
} else {
adjusted_hue = a.x + (hue_diff + 1.0) * t;
}
} else {
adjusted_hue = a.x + hue_diff * t;
}
return vec4(
fract(adjusted_hue),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn interpolate_gradient(
distance: f32,
start_color: vec4<f32>,
@ -397,10 +333,10 @@ fn interpolate_gradient(
) -> vec4<f32> {
if start_distance == end_distance {
if distance <= start_distance && enabled(flags, FILL_START) {
return start_color;
return convert_to_linear_rgba(start_color);
}
if start_distance <= distance && enabled(flags, FILL_END) {
return end_color;
return convert_to_linear_rgba(end_color);
}
return vec4(0.);
}
@ -409,14 +345,14 @@ fn interpolate_gradient(
if t < 0.0 {
if enabled(flags, FILL_START) {
return start_color;
return convert_to_linear_rgba(start_color);
}
return vec4(0.0);
}
if 1. < t {
if enabled(flags, FILL_END) {
return end_color;
return convert_to_linear_rgba(end_color);
}
return vec4(0.0);
}
@ -426,24 +362,56 @@ fn interpolate_gradient(
} else {
t = 0.5 * (1 + (t - hint) / (1.0 - hint));
}
#ifdef IN_SRGB
return mix_linear_rgba_in_srgba_space(start_color, end_color, t);
#else ifdef IN_OKLAB
return mix_linear_rgba_in_oklaba_space(start_color, end_color, t);
#else ifdef IN_OKLCH
return mix_linear_rgba_in_oklcha_space(start_color, end_color, t);
return convert_to_linear_rgba(mix_colors(start_color, end_color, t));
}
// Mix the colors, choosing the appropriate interpolation method for the given color space
fn mix_colors(
start_color: vec4<f32>,
end_color: vec4<f32>,
t: f32,
) -> vec4<f32> {
#ifdef IN_OKLCH
return mix_oklcha(start_color, end_color, t);
#else ifdef IN_OKLCH_LONG
return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t);
return mix_oklcha_long(start_color, end_color, t);
#else ifdef IN_HSV
return mix_linear_rgba_in_hsva_space(start_color, end_color, t);
return mix_hsva(start_color, end_color, t);
#else ifdef IN_HSV_LONG
return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t);
return mix_hsva_long(start_color, end_color, t);
#else ifdef IN_HSL
return mix_linear_rgba_in_hsla_space(start_color, end_color, t);
return mix_hsla(start_color, end_color, t);
#else ifdef IN_HSL_LONG
return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t);
return mix_hsla_long(start_color, end_color, t);
#else
// Just lerp in linear RGBA, OkLab and SRGBA spaces
return mix(start_color, end_color, t);
#endif
}
// Convert a color from the interpolation color space to linear rgba
fn convert_to_linear_rgba(
color: vec4<f32>,
) -> vec4<f32> {
#ifdef IN_OKLCH
return oklcha_to_linear_rgba(color);
#else ifdef IN_OKLCH_LONG
return oklcha_to_linear_rgba(color);
#else ifdef IN_HSV
return hsva_to_linear_rgba(color);
#else ifdef IN_HSV_LONG
return hsva_to_linear_rgba(color);
#else ifdef IN_HSL
return hsla_to_linear_rgba(color);
#else ifdef IN_HSL_LONG
return hsla_to_linear_rgba(color);
#else ifdef IN_OKLAB
return oklaba_to_linear_rgba(color);
#else ifdef IN_SRGB
return vec4(pow(color.rgb, vec3(2.2)), color.a);
#else
// Color is already in linear rgba space
return color;
#endif
}

View File

@ -510,6 +510,7 @@ Example | Description
[Many Foxes](../examples/stress_tests/many_foxes.rs) | Loads an animated fox model and spawns lots of them. Good for testing skinned mesh performance. Takes an unsigned integer argument for the number of foxes to spawn. Defaults to 1000
[Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos
[Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering.
[Many Gradients](../examples/stress_tests/many_gradients.rs) | Stress test for gradient rendering performance
[Many Lights](../examples/stress_tests/many_lights.rs) | Simple benchmark to test rendering many point lights. Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights
[Many Sprites](../examples/stress_tests/many_sprites.rs) | Displays many sprites in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites.
[Many Text2d](../examples/stress_tests/many_text2d.rs) | Displays many Text2d! Used for performance testing.

View File

@ -0,0 +1,180 @@
//! Stress test demonstrating gradient performance improvements.
//!
//! This example creates many UI nodes with gradients to measure the performance
//! impact of pre-converting colors to the target color space on the CPU.
use argh::FromArgs;
use bevy::{
color::palettes::css::*,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
math::ops::sin,
prelude::*,
ui::{
BackgroundGradient, ColorStop, Display, Gradient, InterpolationColorSpace, LinearGradient,
RepeatedGridTrack,
},
window::{PresentMode, WindowResolution},
};
const COLS: usize = 30;
#[derive(FromArgs, Resource, Debug)]
/// Gradient stress test
struct Args {
/// how many gradients per group (default: 900)
#[argh(option, default = "900")]
gradient_count: usize,
/// whether to animate gradients by changing colors
#[argh(switch)]
animate: bool,
/// use sRGB interpolation
#[argh(switch)]
srgb: bool,
/// use HSL interpolation
#[argh(switch)]
hsl: bool,
}
fn main() {
let args: Args = argh::from_env();
let total_gradients = args.gradient_count;
println!("Gradient stress test with {total_gradients} gradients");
println!(
"Color space: {}",
if args.srgb {
"sRGB"
} else if args.hsl {
"HSL"
} else {
"OkLab (default)"
}
);
App::new()
.add_plugins((
LogDiagnosticsPlugin::default(),
FrameTimeDiagnosticsPlugin::default(),
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Gradient Stress Test".to_string(),
resolution: WindowResolution::new(1920.0, 1080.0),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
))
.insert_resource(args)
.add_systems(Startup, setup)
.add_systems(Update, animate_gradients)
.run();
}
fn setup(mut commands: Commands, args: Res<Args>) {
commands.spawn(Camera2d);
let rows_to_spawn = args.gradient_count.div_ceil(COLS);
// Create a grid of gradients
commands
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
display: Display::Grid,
grid_template_columns: RepeatedGridTrack::flex(COLS as u16, 1.0),
grid_template_rows: RepeatedGridTrack::flex(rows_to_spawn as u16, 1.0),
..default()
})
.with_children(|parent| {
for i in 0..args.gradient_count {
let angle = (i as f32 * 10.0) % 360.0;
let mut gradient = LinearGradient::new(
angle,
vec![
ColorStop::new(RED, Val::Percent(0.0)),
ColorStop::new(BLUE, Val::Percent(100.0)),
ColorStop::new(GREEN, Val::Percent(20.0)),
ColorStop::new(YELLOW, Val::Percent(40.0)),
ColorStop::new(ORANGE, Val::Percent(60.0)),
ColorStop::new(LIME, Val::Percent(80.0)),
ColorStop::new(DARK_CYAN, Val::Percent(90.0)),
],
);
gradient.color_space = if args.srgb {
InterpolationColorSpace::Srgba
} else if args.hsl {
InterpolationColorSpace::Hsla
} else {
InterpolationColorSpace::Oklaba
};
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
BackgroundGradient(vec![Gradient::Linear(gradient)]),
GradientNode { index: i },
));
}
});
}
#[derive(Component)]
struct GradientNode {
index: usize,
}
fn animate_gradients(
mut gradients: Query<(&mut BackgroundGradient, &GradientNode)>,
args: Res<Args>,
time: Res<Time>,
) {
if !args.animate {
return;
}
let t = time.elapsed_secs();
for (mut bg_gradient, node) in &mut gradients {
let offset = node.index as f32 * 0.01;
let hue_shift = sin(t + offset) * 0.5 + 0.5;
if let Some(Gradient::Linear(gradient)) = bg_gradient.0.get_mut(0) {
let color1 = Color::hsl(hue_shift * 360.0, 1.0, 0.5);
let color2 = Color::hsl((hue_shift + 0.3) * 360.0 % 360.0, 1.0, 0.5);
gradient.stops = vec![
ColorStop::new(color1, Val::Percent(0.0)),
ColorStop::new(color2, Val::Percent(100.0)),
ColorStop::new(
Color::hsl((hue_shift + 0.1) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(20.0),
),
ColorStop::new(
Color::hsl((hue_shift + 0.15) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(40.0),
),
ColorStop::new(
Color::hsl((hue_shift + 0.2) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(60.0),
),
ColorStop::new(
Color::hsl((hue_shift + 0.25) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(80.0),
),
ColorStop::new(
Color::hsl((hue_shift + 0.28) * 360.0 % 360.0, 1.0, 0.5),
Val::Percent(90.0),
),
];
}
}
}