1use crate::{
2 Alpha, ColorToComponents, Gray, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba,
3 StandardColor, Xyza,
4};
5use bevy_math::{Vec3, Vec4};
6use bevy_reflect::prelude::*;
7
8#[doc = include_str!("../docs/conversion.md")]
11#[doc = include_str!("../docs/diagrams/model_graph.svg")]
13#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
15#[reflect(PartialEq, Default)]
16#[cfg_attr(
17 feature = "serialize",
18 derive(serde::Serialize, serde::Deserialize),
19 reflect(Serialize, Deserialize)
20)]
21pub struct Hsla {
22 pub hue: f32,
24 pub saturation: f32,
26 pub lightness: f32,
28 pub alpha: f32,
30}
31
32impl StandardColor for Hsla {}
33
34impl Hsla {
35 pub const fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
44 Self {
45 hue,
46 saturation,
47 lightness,
48 alpha,
49 }
50 }
51
52 pub const fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self {
60 Self::new(hue, saturation, lightness, 1.0)
61 }
62
63 pub const fn with_saturation(self, saturation: f32) -> Self {
65 Self { saturation, ..self }
66 }
67
68 pub const fn with_lightness(self, lightness: f32) -> Self {
70 Self { lightness, ..self }
71 }
72
73 pub fn sequential_dispersed(index: u32) -> Self {
91 const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; const RATIO_360: f32 = 360.0 / u32::MAX as f32;
93
94 let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360;
99 Self::hsl(hue, 1., 0.5)
100 }
101}
102
103impl Default for Hsla {
104 fn default() -> Self {
105 Self::new(0., 0., 1., 1.)
106 }
107}
108
109impl Mix for Hsla {
110 #[inline]
111 fn mix(&self, other: &Self, factor: f32) -> Self {
112 let n_factor = 1.0 - factor;
113 Self {
114 hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
115 saturation: self.saturation * n_factor + other.saturation * factor,
116 lightness: self.lightness * n_factor + other.lightness * factor,
117 alpha: self.alpha * n_factor + other.alpha * factor,
118 }
119 }
120}
121
122impl Gray for Hsla {
123 const BLACK: Self = Self::new(0., 0., 0., 1.);
124 const WHITE: Self = Self::new(0., 0., 1., 1.);
125}
126
127impl Alpha for Hsla {
128 #[inline]
129 fn with_alpha(&self, alpha: f32) -> Self {
130 Self { alpha, ..*self }
131 }
132
133 #[inline]
134 fn alpha(&self) -> f32 {
135 self.alpha
136 }
137
138 #[inline]
139 fn set_alpha(&mut self, alpha: f32) {
140 self.alpha = alpha;
141 }
142}
143
144impl Hue for Hsla {
145 #[inline]
146 fn with_hue(&self, hue: f32) -> Self {
147 Self { hue, ..*self }
148 }
149
150 #[inline]
151 fn hue(&self) -> f32 {
152 self.hue
153 }
154
155 #[inline]
156 fn set_hue(&mut self, hue: f32) {
157 self.hue = hue;
158 }
159}
160
161impl Luminance for Hsla {
162 #[inline]
163 fn with_luminance(&self, lightness: f32) -> Self {
164 Self { lightness, ..*self }
165 }
166
167 fn luminance(&self) -> f32 {
168 self.lightness
169 }
170
171 fn darker(&self, amount: f32) -> Self {
172 Self {
173 lightness: (self.lightness - amount).clamp(0., 1.),
174 ..*self
175 }
176 }
177
178 fn lighter(&self, amount: f32) -> Self {
179 Self {
180 lightness: (self.lightness + amount).min(1.),
181 ..*self
182 }
183 }
184}
185
186impl ColorToComponents for Hsla {
187 fn to_f32_array(self) -> [f32; 4] {
188 [self.hue, self.saturation, self.lightness, self.alpha]
189 }
190
191 fn to_f32_array_no_alpha(self) -> [f32; 3] {
192 [self.hue, self.saturation, self.lightness]
193 }
194
195 fn to_vec4(self) -> Vec4 {
196 Vec4::new(self.hue, self.saturation, self.lightness, self.alpha)
197 }
198
199 fn to_vec3(self) -> Vec3 {
200 Vec3::new(self.hue, self.saturation, self.lightness)
201 }
202
203 fn from_f32_array(color: [f32; 4]) -> Self {
204 Self {
205 hue: color[0],
206 saturation: color[1],
207 lightness: color[2],
208 alpha: color[3],
209 }
210 }
211
212 fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
213 Self {
214 hue: color[0],
215 saturation: color[1],
216 lightness: color[2],
217 alpha: 1.0,
218 }
219 }
220
221 fn from_vec4(color: Vec4) -> Self {
222 Self {
223 hue: color[0],
224 saturation: color[1],
225 lightness: color[2],
226 alpha: color[3],
227 }
228 }
229
230 fn from_vec3(color: Vec3) -> Self {
231 Self {
232 hue: color[0],
233 saturation: color[1],
234 lightness: color[2],
235 alpha: 1.0,
236 }
237 }
238}
239
240impl From<Hsla> for Hsva {
241 fn from(
242 Hsla {
243 hue,
244 saturation,
245 lightness,
246 alpha,
247 }: Hsla,
248 ) -> Self {
249 let value = lightness + saturation * lightness.min(1. - lightness);
251 let saturation = if value == 0. {
252 0.
253 } else {
254 2. * (1. - (lightness / value))
255 };
256
257 Hsva::new(hue, saturation, value, alpha)
258 }
259}
260
261impl From<Hsva> for Hsla {
262 fn from(
263 Hsva {
264 hue,
265 saturation,
266 value,
267 alpha,
268 }: Hsva,
269 ) -> Self {
270 let lightness = value * (1. - saturation / 2.);
272 let saturation = if lightness == 0. || lightness == 1. {
273 0.
274 } else {
275 (value - lightness) / lightness.min(1. - lightness)
276 };
277
278 Hsla::new(hue, saturation, lightness, alpha)
279 }
280}
281
282impl From<Hwba> for Hsla {
285 fn from(value: Hwba) -> Self {
286 Hsva::from(value).into()
287 }
288}
289
290impl From<Hsla> for Hwba {
291 fn from(value: Hsla) -> Self {
292 Hsva::from(value).into()
293 }
294}
295
296impl From<Srgba> for Hsla {
297 fn from(value: Srgba) -> Self {
298 Hsva::from(value).into()
299 }
300}
301
302impl From<Hsla> for Srgba {
303 fn from(value: Hsla) -> Self {
304 Hsva::from(value).into()
305 }
306}
307
308impl From<LinearRgba> for Hsla {
309 fn from(value: LinearRgba) -> Self {
310 Hsva::from(value).into()
311 }
312}
313
314impl From<Hsla> for LinearRgba {
315 fn from(value: Hsla) -> Self {
316 Hsva::from(value).into()
317 }
318}
319
320impl From<Lcha> for Hsla {
321 fn from(value: Lcha) -> Self {
322 Hsva::from(value).into()
323 }
324}
325
326impl From<Hsla> for Lcha {
327 fn from(value: Hsla) -> Self {
328 Hsva::from(value).into()
329 }
330}
331
332impl From<Xyza> for Hsla {
333 fn from(value: Xyza) -> Self {
334 Hsva::from(value).into()
335 }
336}
337
338impl From<Hsla> for Xyza {
339 fn from(value: Hsla) -> Self {
340 Hsva::from(value).into()
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::{
348 color_difference::EuclideanDistance, test_colors::TEST_COLORS, testing::assert_approx_eq,
349 };
350
351 #[test]
352 fn test_to_from_srgba() {
353 let hsla = Hsla::new(0.5, 0.5, 0.5, 1.0);
354 let srgba: Srgba = hsla.into();
355 let hsla2: Hsla = srgba.into();
356 assert_approx_eq!(hsla.hue, hsla2.hue, 0.001);
357 assert_approx_eq!(hsla.saturation, hsla2.saturation, 0.001);
358 assert_approx_eq!(hsla.lightness, hsla2.lightness, 0.001);
359 assert_approx_eq!(hsla.alpha, hsla2.alpha, 0.001);
360 }
361
362 #[test]
363 fn test_to_from_srgba_2() {
364 for color in TEST_COLORS.iter() {
365 let rgb2: Srgba = (color.hsl).into();
366 let hsl2: Hsla = (color.rgb).into();
367 assert!(
368 color.rgb.distance(&rgb2) < 0.000001,
369 "{}: {:?} != {:?}",
370 color.name,
371 color.rgb,
372 rgb2
373 );
374 assert_approx_eq!(color.hsl.hue, hsl2.hue, 0.001);
375 assert_approx_eq!(color.hsl.saturation, hsl2.saturation, 0.001);
376 assert_approx_eq!(color.hsl.lightness, hsl2.lightness, 0.001);
377 assert_approx_eq!(color.hsl.alpha, hsl2.alpha, 0.001);
378 }
379 }
380
381 #[test]
382 fn test_to_from_linear() {
383 let hsla = Hsla::new(0.5, 0.5, 0.5, 1.0);
384 let linear: LinearRgba = hsla.into();
385 let hsla2: Hsla = linear.into();
386 assert_approx_eq!(hsla.hue, hsla2.hue, 0.001);
387 assert_approx_eq!(hsla.saturation, hsla2.saturation, 0.001);
388 assert_approx_eq!(hsla.lightness, hsla2.lightness, 0.001);
389 assert_approx_eq!(hsla.alpha, hsla2.alpha, 0.001);
390 }
391
392 #[test]
393 fn test_mix_wrap() {
394 let hsla0 = Hsla::new(10., 0.5, 0.5, 1.0);
395 let hsla1 = Hsla::new(20., 0.5, 0.5, 1.0);
396 let hsla2 = Hsla::new(350., 0.5, 0.5, 1.0);
397 assert_approx_eq!(hsla0.mix(&hsla1, 0.25).hue, 12.5, 0.001);
398 assert_approx_eq!(hsla0.mix(&hsla1, 0.5).hue, 15., 0.001);
399 assert_approx_eq!(hsla0.mix(&hsla1, 0.75).hue, 17.5, 0.001);
400
401 assert_approx_eq!(hsla1.mix(&hsla0, 0.25).hue, 17.5, 0.001);
402 assert_approx_eq!(hsla1.mix(&hsla0, 0.5).hue, 15., 0.001);
403 assert_approx_eq!(hsla1.mix(&hsla0, 0.75).hue, 12.5, 0.001);
404
405 assert_approx_eq!(hsla0.mix(&hsla2, 0.25).hue, 5., 0.001);
406 assert_approx_eq!(hsla0.mix(&hsla2, 0.5).hue, 0., 0.001);
407 assert_approx_eq!(hsla0.mix(&hsla2, 0.75).hue, 355., 0.001);
408
409 assert_approx_eq!(hsla2.mix(&hsla0, 0.25).hue, 355., 0.001);
410 assert_approx_eq!(hsla2.mix(&hsla0, 0.5).hue, 0., 0.001);
411 assert_approx_eq!(hsla2.mix(&hsla0, 0.75).hue, 5., 0.001);
412 }
413
414 #[test]
415 fn test_from_index() {
416 let references = [
417 Hsla::hsl(0.0, 1., 0.5),
418 Hsla::hsl(222.49225, 1., 0.5),
419 Hsla::hsl(84.984474, 1., 0.5),
420 Hsla::hsl(307.4767, 1., 0.5),
421 Hsla::hsl(169.96895, 1., 0.5),
422 ];
423
424 for (index, reference) in references.into_iter().enumerate() {
425 let color = Hsla::sequential_dispersed(index as u32);
426
427 assert_approx_eq!(color.hue, reference.hue, 0.001);
428 }
429 }
430}