1use bevy::{
4 color::palettes::css::{BLUE, GREEN, INDIGO, LIME, ORANGE, RED, VIOLET, YELLOW},
5 prelude::*,
6 ui::ColorStop,
7 ui_widgets::{Activate, Button},
8};
9use std::f32::consts::TAU;
10
11const COLOR_SPACES: [InterpolationColorSpace; 11] = [
12 InterpolationColorSpace::Oklaba,
13 InterpolationColorSpace::Oklcha,
14 InterpolationColorSpace::OklchaLong,
15 InterpolationColorSpace::Srgba,
16 InterpolationColorSpace::LinearRgba,
17 InterpolationColorSpace::Hsla,
18 InterpolationColorSpace::HslaLong,
19 InterpolationColorSpace::Hsva,
20 InterpolationColorSpace::HsvaLong,
21 InterpolationColorSpace::Okhsla,
22 InterpolationColorSpace::OkhslaLong,
23];
24
25#[derive(Component, Clone, Default)]
27struct PreviousButton;
28
29#[derive(Component, Clone, Default)]
31struct NextButton;
32
33#[derive(Component, Clone, Default)]
35struct CurrentColorSpaceLabel;
36
37#[derive(Resource, Default)]
39struct AppSettings {
40 color_space_current_index: usize,
42}
43
44fn main() {
45 App::new()
46 .add_plugins(DefaultPlugins)
47 .init_resource::<AppSettings>()
48 .add_systems(Startup, setup)
49 .add_systems(Update, update)
50 .add_observer(on_activate_change_space)
51 .run();
52}
53
54fn setup(mut commands: Commands) {
55 commands.spawn(Camera2d);
56
57 let buttons_id = commands.spawn_scene(buttons_scene()).id();
58
59 commands
60 .spawn(Node {
61 flex_direction: FlexDirection::Column,
62 row_gap: px(20),
63 margin: UiRect::all(px(20)),
64 ..Default::default()
65 })
66 .with_children(|commands| {
67 for (b, stops) in [
68 (
69 4.,
70 vec![
71 ColorStop::new(Color::WHITE, percent(15)),
72 ColorStop::new(Color::BLACK, percent(85)),
73 ],
74 ),
75 (4., vec![RED.into(), BLUE.into(), LIME.into()]),
76 (
77 0.,
78 vec![
79 RED.into(),
80 ColorStop::new(RED, percent(100. / 7.)),
81 ColorStop::new(ORANGE, percent(100. / 7.)),
82 ColorStop::new(ORANGE, percent(200. / 7.)),
83 ColorStop::new(YELLOW, percent(200. / 7.)),
84 ColorStop::new(YELLOW, percent(300. / 7.)),
85 ColorStop::new(GREEN, percent(300. / 7.)),
86 ColorStop::new(GREEN, percent(400. / 7.)),
87 ColorStop::new(BLUE, percent(400. / 7.)),
88 ColorStop::new(BLUE, percent(500. / 7.)),
89 ColorStop::new(INDIGO, percent(500. / 7.)),
90 ColorStop::new(INDIGO, percent(600. / 7.)),
91 ColorStop::new(VIOLET, percent(600. / 7.)),
92 VIOLET.into(),
93 ],
94 ),
95 ] {
96 commands.spawn(Node::default()).with_children(|commands| {
97 commands
98 .spawn(Node {
99 flex_direction: FlexDirection::Column,
100 row_gap: px(5),
101 ..Default::default()
102 })
103 .with_children(|commands| {
104 for (w, h) in [(70., 70.), (35., 70.), (70., 35.)] {
105 commands
106 .spawn(Node {
107 column_gap: px(10),
108 ..Default::default()
109 })
110 .with_children(|commands| {
111 for angle in (0..8).map(|i| i as f32 * TAU / 8.) {
112 commands.spawn((
113 Node {
114 width: px(w),
115 height: px(h),
116 border: UiRect::all(px(b)),
117 border_radius: BorderRadius::all(px(20)),
118 ..default()
119 },
120 BackgroundGradient::from(LinearGradient {
121 angle,
122 stops: stops.clone(),
123 ..default()
124 }),
125 BorderGradient::from(LinearGradient {
126 angle: 3. * TAU / 8.,
127 stops: vec![
128 YELLOW.into(),
129 Color::WHITE.into(),
130 ORANGE.into(),
131 ],
132 ..default()
133 }),
134 ));
135 }
136 });
137 }
138 });
139
140 commands.spawn(Node::default()).with_children(|commands| {
141 commands.spawn((
142 Node {
143 aspect_ratio: Some(1.),
144 height: percent(100),
145 border: UiRect::all(px(b)),
146 margin: UiRect::left(px(20)),
147 border_radius: BorderRadius::all(px(20)),
148 ..default()
149 },
150 BackgroundGradient::from(LinearGradient {
151 angle: 0.,
152 stops: stops.clone(),
153 ..default()
154 }),
155 BorderGradient::from(LinearGradient {
156 angle: 3. * TAU / 8.,
157 stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
158 ..default()
159 }),
160 AnimateMarker,
161 ));
162
163 commands.spawn((
164 Node {
165 aspect_ratio: Some(1.),
166 height: percent(100),
167 border: UiRect::all(px(b)),
168 margin: UiRect::left(px(20)),
169 border_radius: BorderRadius::all(px(20)),
170 ..default()
171 },
172 BackgroundGradient::from(RadialGradient {
173 stops: stops.clone(),
174 shape: RadialGradientShape::ClosestSide,
175 position: UiPosition::CENTER,
176 ..default()
177 }),
178 BorderGradient::from(LinearGradient {
179 angle: 3. * TAU / 8.,
180 stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
181 ..default()
182 }),
183 AnimateMarker,
184 ));
185 commands.spawn((
186 Node {
187 aspect_ratio: Some(1.),
188 height: percent(100),
189 border: UiRect::all(px(b)),
190 margin: UiRect::left(px(20)),
191 border_radius: BorderRadius::all(px(20)),
192 ..default()
193 },
194 BackgroundGradient::from(ConicGradient {
195 start: 0.,
196 stops: stops
197 .iter()
198 .map(|stop| AngularColorStop::auto(stop.color))
199 .collect(),
200 position: UiPosition::CENTER,
201 ..default()
202 }),
203 BorderGradient::from(LinearGradient {
204 angle: 3. * TAU / 8.,
205 stops: vec![YELLOW.into(), Color::WHITE.into(), ORANGE.into()],
206 ..default()
207 }),
208 AnimateMarker,
209 ));
210 });
211 });
212 }
213 })
214 .add_child(buttons_id);
215}
216
217fn buttons_scene() -> impl Scene {
220 bsn! {
221 Node {
222 flex_direction: FlexDirection::Row,
223 align_items: AlignItems::Center,
224 justify_content: JustifyContent::Center,
225 column_gap: px(20),
226 }
227 Children [
228 Node {
229 align_items: AlignItems::Center,
230 justify_content: JustifyContent::Center,
231 }
232 Children [
233 CurrentColorSpaceLabel
234 template(|ctx| {
235 let current_index = ctx.resource::<AppSettings>().color_space_current_index;
236 Ok(Text(format!("Current Space\n{:?}", COLOR_SPACES[current_index])))
237 })
238 ]
239 --
240 PreviousButton
241 @button_node_scene("Previous")
242 --
243 NextButton
244 @button_node_scene("Next")
245 ]
246 }
247}
248
249fn button_node_scene(caption: &'static str) -> impl Scene {
251 bsn! {
252 Button
253 Node {
254 border: UiRect::all(px(2)),
255 padding: UiRect::axes(px(8), px(4)),
256 justify_content: JustifyContent::Center,
258 align_items: AlignItems::Center,
260 border_radius: BorderRadius::MAX,
261 }
262 BorderColor::all(Color::WHITE)
263 BackgroundColor(Color::BLACK)
264 on(|event: On<PointerOver>, mut border_query: Query<&mut BorderColor, With<Button>>| {
265 *border_query.get_mut(event.entity).unwrap() = BorderColor::all(RED);
266 })
267 on(|event: On<PointerOut>, mut border_query: Query<&mut BorderColor, With<Button>>| {
268 *border_query.get_mut(event.entity).unwrap() = BorderColor::all(Color::WHITE);
269 })
270 Children [
271 Text(caption)
272 ]
273 }
274}
275
276fn on_activate_change_space(
278 event: On<Activate>,
279 mut app_settings: ResMut<AppSettings>,
280 button_type_q: Query<(Has<PreviousButton>, Has<NextButton>), With<Button>>,
281 mut gradients_query: Query<&mut BackgroundGradient>,
282 mut label_q: Query<&mut Text, With<CurrentColorSpaceLabel>>,
283) {
284 let Ok((has_previous, has_next)) = button_type_q.get(event.entity) else {
285 return;
286 };
287 let next_index = match (has_previous, has_next) {
288 (true, true) | (false, false) => return,
289 (true, false) => {
290 if app_settings.color_space_current_index == 0 {
291 COLOR_SPACES.len() - 1
292 } else {
293 app_settings.color_space_current_index - 1
294 }
295 }
296 (false, true) => (app_settings.color_space_current_index + 1) % COLOR_SPACES.len(),
297 };
298 app_settings.color_space_current_index = next_index;
299
300 let next_space = COLOR_SPACES[app_settings.color_space_current_index];
302 for mut label in label_q.iter_mut() {
303 label.0 = format!("Current Space\n{next_space:?}");
304 }
305 for mut gradients in gradients_query.iter_mut() {
306 for gradient in gradients.0.iter_mut() {
307 let space = match gradient {
308 Gradient::Linear(linear_gradient) => &mut linear_gradient.color_space,
309 Gradient::Radial(radial_gradient) => &mut radial_gradient.color_space,
310 Gradient::Conic(conic_gradient) => &mut conic_gradient.color_space,
311 };
312 *space = next_space;
313 }
314 }
315}
316
317#[derive(Component)]
318struct AnimateMarker;
319
320fn update(time: Res<Time>, mut query: Query<&mut BackgroundGradient, With<AnimateMarker>>) {
321 for mut gradients in query.iter_mut() {
322 for gradient in gradients.0.iter_mut() {
323 if let Gradient::Linear(LinearGradient { angle, .. }) = gradient {
324 *angle += 0.5 * time.delta_secs();
325 }
326 }
327 }
328}