1use crate::{
2 color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents,
3 ColorToPacked, Gray, LinearRgba, Luminance, Mix, StandardColor, Xyza,
4};
5#[cfg(feature = "alloc")]
6use alloc::{format, string::String};
7use bevy_math::{ops, Vec3, Vec4};
8#[cfg(feature = "bevy_reflect")]
9use bevy_reflect::prelude::*;
10use thiserror::Error;
11
12#[doc = include_str!("../docs/conversion.md")]
16#[doc = include_str!("../docs/diagrams/model_graph.svg")]
18#[derive(Debug, Clone, Copy, PartialEq)]
20#[cfg_attr(
21 feature = "bevy_reflect",
22 derive(Reflect),
23 reflect(Clone, PartialEq, Default)
24)]
25#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
26#[cfg_attr(
27 all(feature = "serialize", feature = "bevy_reflect"),
28 reflect(Serialize, Deserialize)
29)]
30pub struct Srgba {
31 pub red: f32,
33 pub green: f32,
35 pub blue: f32,
37 pub alpha: f32,
39}
40
41impl StandardColor for Srgba {}
42
43impl_componentwise_vector_space!(Srgba, [red, green, blue, alpha]);
44
45impl Srgba {
46 pub const BLACK: Srgba = Srgba::new(0.0, 0.0, 0.0, 1.0);
52 #[doc(alias = "transparent")]
55 pub const NONE: Srgba = Srgba::new(0.0, 0.0, 0.0, 0.0);
56 pub const WHITE: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0);
59 pub const RED: Srgba = Srgba::new(1.0, 0.0, 0.0, 1.0);
62 pub const GREEN: Srgba = Srgba::new(0.0, 1.0, 0.0, 1.0);
65 pub const BLUE: Srgba = Srgba::new(0.0, 0.0, 1.0, 1.0);
68
69 pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
78 Self {
79 red,
80 green,
81 blue,
82 alpha,
83 }
84 }
85
86 pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
94 Self {
95 red,
96 green,
97 blue,
98 alpha: 1.0,
99 }
100 }
101
102 pub const fn with_red(self, red: f32) -> Self {
104 Self { red, ..self }
105 }
106
107 pub const fn with_green(self, green: f32) -> Self {
109 Self { green, ..self }
110 }
111
112 pub const fn with_blue(self, blue: f32) -> Self {
114 Self { blue, ..self }
115 }
116
117 pub fn hex<T: AsRef<str>>(hex: T) -> Result<Self, HexColorError> {
130 let hex = hex.as_ref();
131 let hex = hex.strip_prefix('#').unwrap_or(hex);
132
133 match hex.len() {
134 3 => {
136 let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes();
137 let (r, g, b) = (l & 0x0F, (b & 0xF0) >> 4, b & 0x0F);
138 Ok(Self::rgb_u8((r << 4) | r, (g << 4) | g, (b << 4) | b))
139 }
140 4 => {
142 let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes();
143 let (r, g, b, a) = ((l & 0xF0) >> 4, l & 0xF, (b & 0xF0) >> 4, b & 0x0F);
144 Ok(Self::rgba_u8(
145 (r << 4) | r,
146 (g << 4) | g,
147 (b << 4) | b,
148 (a << 4) | a,
149 ))
150 }
151 6 => {
153 let [_, r, g, b] = u32::from_str_radix(hex, 16)?.to_be_bytes();
154 Ok(Self::rgb_u8(r, g, b))
155 }
156 8 => {
158 let [r, g, b, a] = u32::from_str_radix(hex, 16)?.to_be_bytes();
159 Ok(Self::rgba_u8(r, g, b, a))
160 }
161 _ => Err(HexColorError::Length),
162 }
163 }
164
165 #[cfg(feature = "alloc")]
167 pub fn to_hex(&self) -> String {
168 let [r, g, b, a] = self.to_u8_array();
169 match a {
170 255 => format!("#{r:02X}{g:02X}{b:02X}"),
171 _ => format!("#{r:02X}{g:02X}{b:02X}{a:02X}"),
172 }
173 }
174
175 pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
185 Self::from_u8_array_no_alpha([r, g, b])
186 }
187
188 pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
201 Self::from_u8_array([r, g, b, a])
202 }
203
204 pub fn gamma_function(value: f32) -> f32 {
206 if value <= 0.0 {
207 return value;
208 }
209 if value <= 0.04045 {
210 value / 12.92 } else {
212 ops::powf((value + 0.055) / 1.055, 2.4) }
214 }
215
216 pub fn gamma_function_inverse(value: f32) -> f32 {
218 if value <= 0.0 {
219 return value;
220 }
221
222 if value <= 0.0031308 {
223 value * 12.92 } else {
225 (1.055 * ops::powf(value, 1.0 / 2.4)) - 0.055 }
227 }
228}
229
230impl Default for Srgba {
231 fn default() -> Self {
232 Self::WHITE
233 }
234}
235
236impl Luminance for Srgba {
237 #[inline]
238 fn luminance(&self) -> f32 {
239 let linear: LinearRgba = (*self).into();
240 linear.luminance()
241 }
242
243 #[inline]
244 fn with_luminance(&self, luminance: f32) -> Self {
245 let linear: LinearRgba = (*self).into();
246 linear
247 .with_luminance(Srgba::gamma_function(luminance))
248 .into()
249 }
250
251 #[inline]
252 fn darker(&self, amount: f32) -> Self {
253 let linear: LinearRgba = (*self).into();
254 linear.darker(amount).into()
255 }
256
257 #[inline]
258 fn lighter(&self, amount: f32) -> Self {
259 let linear: LinearRgba = (*self).into();
260 linear.lighter(amount).into()
261 }
262}
263
264impl Mix for Srgba {
265 #[inline]
266 fn mix(&self, other: &Self, factor: f32) -> Self {
267 let n_factor = 1.0 - factor;
268 Self {
269 red: self.red * n_factor + other.red * factor,
270 green: self.green * n_factor + other.green * factor,
271 blue: self.blue * n_factor + other.blue * factor,
272 alpha: self.alpha * n_factor + other.alpha * factor,
273 }
274 }
275}
276
277impl Alpha for Srgba {
278 #[inline]
279 fn with_alpha(&self, alpha: f32) -> Self {
280 Self { alpha, ..*self }
281 }
282
283 #[inline]
284 fn alpha(&self) -> f32 {
285 self.alpha
286 }
287
288 #[inline]
289 fn set_alpha(&mut self, alpha: f32) {
290 self.alpha = alpha;
291 }
292}
293
294impl EuclideanDistance for Srgba {
295 #[inline]
296 fn distance_squared(&self, other: &Self) -> f32 {
297 let dr = self.red - other.red;
298 let dg = self.green - other.green;
299 let db = self.blue - other.blue;
300 dr * dr + dg * dg + db * db
301 }
302}
303
304impl Gray for Srgba {
305 const BLACK: Self = Self::BLACK;
306 const WHITE: Self = Self::WHITE;
307}
308
309impl ColorToComponents for Srgba {
310 fn to_f32_array(self) -> [f32; 4] {
311 [self.red, self.green, self.blue, self.alpha]
312 }
313
314 fn to_f32_array_no_alpha(self) -> [f32; 3] {
315 [self.red, self.green, self.blue]
316 }
317
318 fn to_vec4(self) -> Vec4 {
319 Vec4::new(self.red, self.green, self.blue, self.alpha)
320 }
321
322 fn to_vec3(self) -> Vec3 {
323 Vec3::new(self.red, self.green, self.blue)
324 }
325
326 fn from_f32_array(color: [f32; 4]) -> Self {
327 Self {
328 red: color[0],
329 green: color[1],
330 blue: color[2],
331 alpha: color[3],
332 }
333 }
334
335 fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
336 Self {
337 red: color[0],
338 green: color[1],
339 blue: color[2],
340 alpha: 1.0,
341 }
342 }
343
344 fn from_vec4(color: Vec4) -> Self {
345 Self {
346 red: color[0],
347 green: color[1],
348 blue: color[2],
349 alpha: color[3],
350 }
351 }
352
353 fn from_vec3(color: Vec3) -> Self {
354 Self {
355 red: color[0],
356 green: color[1],
357 blue: color[2],
358 alpha: 1.0,
359 }
360 }
361}
362
363impl ColorToPacked for Srgba {
364 fn to_u8_array(self) -> [u8; 4] {
365 [self.red, self.green, self.blue, self.alpha]
366 .map(|v| ops::round(v.clamp(0.0, 1.0) * 255.0) as u8)
367 }
368
369 fn to_u8_array_no_alpha(self) -> [u8; 3] {
370 [self.red, self.green, self.blue].map(|v| ops::round(v.clamp(0.0, 1.0) * 255.0) as u8)
371 }
372
373 fn from_u8_array(color: [u8; 4]) -> Self {
374 Self::from_f32_array(color.map(|u| u as f32 / 255.0))
375 }
376
377 fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
378 Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
379 }
380}
381
382impl From<LinearRgba> for Srgba {
383 #[inline]
384 fn from(value: LinearRgba) -> Self {
385 Self {
386 red: Srgba::gamma_function_inverse(value.red),
387 green: Srgba::gamma_function_inverse(value.green),
388 blue: Srgba::gamma_function_inverse(value.blue),
389 alpha: value.alpha,
390 }
391 }
392}
393
394impl From<Srgba> for LinearRgba {
395 #[inline]
396 fn from(value: Srgba) -> Self {
397 Self {
398 red: Srgba::gamma_function(value.red),
399 green: Srgba::gamma_function(value.green),
400 blue: Srgba::gamma_function(value.blue),
401 alpha: value.alpha,
402 }
403 }
404}
405
406impl From<Xyza> for Srgba {
409 fn from(value: Xyza) -> Self {
410 LinearRgba::from(value).into()
411 }
412}
413
414impl From<Srgba> for Xyza {
415 fn from(value: Srgba) -> Self {
416 LinearRgba::from(value).into()
417 }
418}
419
420#[cfg(feature = "wgpu-types")]
421impl From<Srgba> for wgpu_types::Color {
422 fn from(color: Srgba) -> Self {
423 wgpu_types::Color {
424 r: color.red as f64,
425 g: color.green as f64,
426 b: color.blue as f64,
427 a: color.alpha as f64,
428 }
429 }
430}
431
432#[derive(Debug, Error, PartialEq, Eq)]
434pub enum HexColorError {
435 #[error("Invalid hex string")]
437 Parse(#[from] core::num::ParseIntError),
438 #[error("Unexpected length of hex string")]
440 Length,
441 #[error("Invalid hex char")]
443 Char(char),
444}
445
446#[cfg(test)]
447mod tests {
448 use crate::testing::assert_approx_eq;
449
450 use super::*;
451
452 #[test]
453 fn test_to_from_linear() {
454 let srgba = Srgba::new(0.0, 0.5, 1.0, 1.0);
455 let linear_rgba: LinearRgba = srgba.into();
456 assert_eq!(linear_rgba.red, 0.0);
457 assert_approx_eq!(linear_rgba.green, 0.2140, 0.0001);
458 assert_approx_eq!(linear_rgba.blue, 1.0, 0.0001);
459 assert_eq!(linear_rgba.alpha, 1.0);
460 let srgba2: Srgba = linear_rgba.into();
461 assert_eq!(srgba2.red, 0.0);
462 assert_approx_eq!(srgba2.green, 0.5, 0.0001);
463 assert_approx_eq!(srgba2.blue, 1.0, 0.0001);
464 assert_eq!(srgba2.alpha, 1.0);
465 }
466
467 #[test]
468 fn euclidean_distance() {
469 let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
471 let b = Srgba::new(1.0, 1.0, 1.0, 1.0);
472 assert_eq!(a.distance_squared(&b), 3.0);
473
474 let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
476 let b = Srgba::new(1.0, 1.0, 1.0, 0.0);
477 assert_eq!(a.distance_squared(&b), 3.0);
478
479 let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
481 let b = Srgba::new(1.0, 0.0, 0.0, 1.0);
482 assert_eq!(a.distance_squared(&b), 1.0);
483 }
484
485 #[test]
486 fn darker_lighter() {
487 let color = Srgba::new(0.4, 0.5, 0.6, 1.0);
489 let darker1 = color.darker(0.1);
490 let darker2 = darker1.darker(0.1);
491 let twice_as_dark = color.darker(0.2);
492 assert!(darker2.distance_squared(&twice_as_dark) < 0.0001);
493
494 let lighter1 = color.lighter(0.1);
495 let lighter2 = lighter1.lighter(0.1);
496 let twice_as_light = color.lighter(0.2);
497 assert!(lighter2.distance_squared(&twice_as_light) < 0.0001);
498 }
499
500 #[test]
501 fn hex_color() {
502 assert_eq!(Srgba::hex("FFF"), Ok(Srgba::WHITE));
503 assert_eq!(Srgba::hex("FFFF"), Ok(Srgba::WHITE));
504 assert_eq!(Srgba::hex("FFFFFF"), Ok(Srgba::WHITE));
505 assert_eq!(Srgba::hex("FFFFFFFF"), Ok(Srgba::WHITE));
506 assert_eq!(Srgba::hex("000"), Ok(Srgba::BLACK));
507 assert_eq!(Srgba::hex("000F"), Ok(Srgba::BLACK));
508 assert_eq!(Srgba::hex("000000"), Ok(Srgba::BLACK));
509 assert_eq!(Srgba::hex("000000FF"), Ok(Srgba::BLACK));
510 assert_eq!(Srgba::hex("03a9f4"), Ok(Srgba::rgb_u8(3, 169, 244)));
511 assert_eq!(Srgba::hex("yy"), Err(HexColorError::Length));
512 assert_eq!(Srgba::hex("#f2a"), Ok(Srgba::rgb_u8(255, 34, 170)));
513 assert_eq!(Srgba::hex("#e23030"), Ok(Srgba::rgb_u8(226, 48, 48)));
514 assert_eq!(Srgba::hex("#ff"), Err(HexColorError::Length));
515 assert_eq!(Srgba::hex("11223344"), Ok(Srgba::rgba_u8(17, 34, 51, 68)));
516 assert_eq!(Srgba::hex("1234"), Ok(Srgba::rgba_u8(17, 34, 51, 68)));
517 assert_eq!(Srgba::hex("12345678"), Ok(Srgba::rgba_u8(18, 52, 86, 120)));
518 assert_eq!(Srgba::hex("4321"), Ok(Srgba::rgba_u8(68, 51, 34, 17)));
519
520 assert!(matches!(Srgba::hex("yyy"), Err(HexColorError::Parse(_))));
521 assert!(matches!(Srgba::hex("##fff"), Err(HexColorError::Parse(_))));
522 }
523}