1use palette::{IntoColor, OklabHue, Oklcha, RgbHue};
2use schemars::{JsonSchema, json_schema};
3use serde::{Deserialize, Serialize};
4use std::fmt::{self, Display, Formatter};
5
6pub use palette::{Hsla, rgb::Rgba};
8
9pub fn rgb(hex: u32) -> Rgba {
11 let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
12 Rgba {
13 color: palette::rgb::Rgb::new(r, g, b),
14 alpha: 1.0,
15 }
16}
17
18pub fn rgba(hex: u32) -> Rgba {
20 let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
21 Rgba::new(r, g, b, a)
22}
23
24pub fn rgb_to_hsla(color: Rgba) -> Hsla {
29 color.into_color()
30}
31
32pub fn hsla_to_rgba(color: Hsla) -> Rgba {
34 color.into_color()
35}
36
37pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
39 color.swap(0, 2);
40 if color[3] > 0 {
41 let a = color[3] as f32 / 255.;
42 color[0] = (color[0] as f32 / a) as u8;
43 color[1] = (color[1] as f32 / a) as u8;
44 color[2] = (color[2] as f32 / a) as u8;
45 }
46}
47
48pub const fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
50 Hsla {
51 color: palette::Hsl::new_const(
52 RgbHue::new(h.clamp(0., 1.) * 360.),
55 s.clamp(0., 1.),
56 l.clamp(0., 1.),
57 ),
58 alpha: a.clamp(0., 1.),
59 }
60}
61
62pub fn oklcha<T>(lightness: T, chroma: T, hue: impl Into<OklabHue<T>>, alpha: T) -> Oklcha<T> {
64 Oklcha::new(lightness, chroma, hue, alpha)
65}
66
67pub const fn black() -> Hsla {
69 Hsla::new_const(RgbHue::new(0.), 0., 0., 1.)
70}
71
72pub const fn transparent_black() -> Hsla {
74 Hsla::new_const(RgbHue::new(0.), 0., 0., 0.)
75}
76
77pub const fn transparent_white() -> Hsla {
79 Hsla::new_const(RgbHue::new(0.), 0., 1., 0.)
80}
81
82pub const fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
84 Hsla::new_const(RgbHue::new(0.), 0., lightness, opacity)
85}
86
87pub const fn white() -> Hsla {
89 Hsla::new_const(RgbHue::new(0.), 0., 1., 1.)
90}
91
92pub const fn red() -> Hsla {
94 Hsla::new_const(RgbHue::new(0.), 1., 0.5, 1.)
95}
96
97pub const fn blue() -> Hsla {
99 Hsla::new_const(RgbHue::new(240.), 1., 0.5, 1.)
100}
101
102pub const fn green() -> Hsla {
104 Hsla::new_const(RgbHue::new(120.), 1., 0.25, 1.)
105}
106
107pub const fn yellow() -> Hsla {
109 Hsla::new_const(RgbHue::new(60.), 1., 0.5, 1.)
110}
111
112pub fn hsla_schemar(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
114 json_schema!({
117 "type": "object",
118 "properties": {
119 "hue": {
120 "type": "number",
121 "format": "float"
122 },
123 "saturation": {
124 "type": "number",
125 "format": "float"
126 },
127 "lightness": {
128 "type": "number",
129 "format": "float"
130 },
131 "alpha": {
132 "type": "number",
133 "format": "float"
134 },
135 }
136 })
137}
138
139pub fn rgba_schemar(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
141 json_schema!({
144 "type": "object",
145 "properties": {
146 "red": {
147 "type": "number",
148 "format": "float"
149 },
150 "green": {
151 "type": "number",
152 "format": "float"
153 },
154 "blue": {
155 "type": "number",
156 "format": "float"
157 },
158 "alpha": {
159 "type": "number",
160 "format": "float"
161 },
162 }
163 })
164}
165
166pub trait ColorExt {
168 fn blend(&self, other: &Self) -> Self
170 where
171 Self: Sized;
172
173 fn fade_out(&mut self, factor: f32);
176
177 fn opacity(&self, factor: f32) -> Self
206 where
207 Self: Sized;
208}
209impl ColorExt for Rgba {
210 fn blend(&self, other: &Self) -> Self {
211 use palette::blend::{BlendWith, Equations, Parameter};
212 let blend_mode =
213 Equations::from_parameters(Parameter::OneMinusSourceAlpha, Parameter::SourceAlpha);
214 self.blend_with(*other, blend_mode)
215 }
216
217 fn fade_out(&mut self, factor: f32) {
218 self.alpha *= 1.0 - factor.clamp(0., 1.);
219 }
220
221 fn opacity(&self, factor: f32) -> Self {
222 let mut color = *self;
223 color.alpha *= factor.clamp(0., 1.);
224 color
225 }
226}
227impl ColorExt for Hsla {
228 fn blend(&self, other: &Self) -> Self {
229 let this: Rgba = (*self).into_color();
230 let other: Rgba = (*other).into_color();
231 this.blend(&other).into_color()
232 }
233
234 fn fade_out(&mut self, factor: f32) {
235 self.alpha *= 1.0 - factor.clamp(0., 1.);
236 }
237
238 fn opacity(&self, factor: f32) -> Self {
239 let mut color = *self;
240 color.alpha *= factor.clamp(0., 1.);
241 color
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
246#[repr(C)]
247pub(crate) enum BackgroundTag {
248 Solid = 0,
249 LinearGradient = 1,
250 PatternSlash = 2,
251 Checkerboard = 3,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
260#[repr(C)]
261pub enum ColorSpace {
262 #[default]
263 Srgb = 0,
265 Oklab = 1,
267}
268
269impl Display for ColorSpace {
270 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
271 match self {
272 ColorSpace::Srgb => write!(f, "sRGB"),
273 ColorSpace::Oklab => write!(f, "Oklab"),
274 }
275 }
276}
277
278#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
280#[repr(C)]
281pub struct Background {
282 pub(crate) tag: BackgroundTag,
283 pub(crate) color_space: ColorSpace,
284 pub(crate) solid: crate::SceneHsla,
285 pub(crate) gradient_angle_or_pattern_height: f32,
286 pub(crate) colors: [LinearColorStop; 2],
287 pad: u32,
289}
290
291impl std::fmt::Debug for Background {
292 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
293 match self.tag {
294 BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
295 BackgroundTag::LinearGradient => write!(
296 f,
297 "LinearGradient({}, {:?}, {:?})",
298 self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
299 ),
300 BackgroundTag::PatternSlash => write!(
301 f,
302 "PatternSlash({:?}, {})",
303 self.solid, self.gradient_angle_or_pattern_height
304 ),
305 BackgroundTag::Checkerboard => write!(
306 f,
307 "Checkerboard({:?}, {})",
308 self.solid, self.gradient_angle_or_pattern_height
309 ),
310 }
311 }
312}
313
314impl Eq for Background {}
315impl Default for Background {
316 fn default() -> Self {
317 Self {
318 tag: BackgroundTag::Solid,
319 solid: Hsla::default().into(),
320 color_space: ColorSpace::default(),
321 gradient_angle_or_pattern_height: 0.0,
322 colors: [LinearColorStop::default(), LinearColorStop::default()],
323 pad: 0,
324 }
325 }
326}
327
328pub fn pattern_slash(color: impl IntoColor<Hsla>, width: f32, interval: f32) -> Background {
330 let width_scaled = (width * 255.0) as u32;
331 let interval_scaled = (interval * 255.0) as u32;
332 let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
333
334 Background {
335 tag: BackgroundTag::PatternSlash,
336 solid: color.into_color().into(),
337 gradient_angle_or_pattern_height: height,
338 ..Default::default()
339 }
340}
341
342pub fn checkerboard(color: impl IntoColor<Hsla>, size: f32) -> Background {
344 Background {
345 tag: BackgroundTag::Checkerboard,
346 solid: color.into_color().into(),
347 gradient_angle_or_pattern_height: size,
348 ..Default::default()
349 }
350}
351
352pub fn solid_background(color: impl IntoColor<Hsla>) -> Background {
354 Background {
355 solid: color.into_color().into(),
356 ..Default::default()
357 }
358}
359
360pub fn linear_gradient(
368 angle: f32,
369 from: impl Into<LinearColorStop>,
370 to: impl Into<LinearColorStop>,
371) -> Background {
372 Background {
373 tag: BackgroundTag::LinearGradient,
374 gradient_angle_or_pattern_height: angle,
375 colors: [from.into(), to.into()],
376 color_space: ColorSpace::Oklab,
377 ..Default::default()
378 }
379}
380
381#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
385#[repr(C)]
386pub struct LinearColorStop {
387 pub color: crate::SceneHsla,
389 pub percentage: f32,
391}
392
393pub fn linear_color_stop(color: impl IntoColor<Hsla>, percentage: f32) -> LinearColorStop {
397 LinearColorStop {
398 color: color.into_color().into(),
399 percentage,
400 }
401}
402
403impl LinearColorStop {
404 pub fn opacity(&self, factor: f32) -> Self {
406 let color: Hsla = self.color.into();
407 Self {
408 percentage: self.percentage,
409 color: color.opacity(factor).into(),
410 }
411 }
412}
413
414#[derive(Debug, Clone, Copy, PartialEq)]
416pub enum BackgroundKind {
417 Solid(Hsla),
419 LinearGradient {
421 angle: f32,
423 stops: [LinearColorStop; 2],
425 },
426 PatternSlash {
428 color: Hsla,
430 width: f32,
432 interval: f32,
434 },
435 Checkerboard {
437 color: Hsla,
439 size: f32,
441 },
442}
443
444impl Background {
445 pub fn as_solid(&self) -> Option<Hsla> {
447 if self.tag == BackgroundTag::Solid {
448 Some(self.solid.into())
449 } else {
450 None
451 }
452 }
453
454 pub fn kind(&self) -> BackgroundKind {
456 match self.tag {
457 BackgroundTag::Solid => BackgroundKind::Solid(self.solid.into()),
458 BackgroundTag::LinearGradient => BackgroundKind::LinearGradient {
459 angle: self.gradient_angle_or_pattern_height,
460 stops: self.colors,
461 },
462 BackgroundTag::PatternSlash => {
463 let packed = self.gradient_angle_or_pattern_height;
467 BackgroundKind::PatternSlash {
468 color: self.solid.into(),
469 width: (packed / 0xFFFF as f32).floor() / 255.0,
470 interval: (packed.rem_euclid(0xFFFF as f32)) / 255.0,
471 }
472 }
473 BackgroundTag::Checkerboard => BackgroundKind::Checkerboard {
474 color: self.solid.into(),
475 size: self.gradient_angle_or_pattern_height,
476 },
477 }
478 }
479
480 pub fn color_space(mut self, color_space: ColorSpace) -> Self {
484 self.color_space = color_space;
485 self
486 }
487
488 pub fn interpolation_space(&self) -> ColorSpace {
490 self.color_space
491 }
492
493 pub fn opacity(&self, factor: f32) -> Self {
495 let mut background = *self;
496 let solid: Hsla = background.solid.into();
497 background.solid = solid.opacity(factor).into();
498 background.colors = [
499 self.colors[0].opacity(factor),
500 self.colors[1].opacity(factor),
501 ];
502 background
503 }
504
505 pub fn is_transparent(&self) -> bool {
507 match self.tag {
508 BackgroundTag::Solid => self.solid.a == 0.,
509 BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.a == 0.),
510 BackgroundTag::PatternSlash => self.solid.a == 0.,
511 BackgroundTag::Checkerboard => self.solid.a == 0.,
512 }
513 }
514}
515
516impl<T: IntoColor<Hsla>> From<T> for Background {
517 fn from(value: T) -> Self {
518 Self {
519 tag: BackgroundTag::Solid,
520 solid: value.into_color().into(),
521 ..Default::default()
522 }
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn test_background_solid() {
532 let color: Hsla = rgba(0xff0099ff).into_color();
533 let mut background = Background::from(color);
534 assert_eq!(background.tag, BackgroundTag::Solid);
535 assert_eq!(background.solid, color.into());
536
537 assert_eq!(background.opacity(0.5).solid, color.opacity(0.5).into());
538 assert!(!background.is_transparent());
539 background.solid = hsla(0.0, 0.0, 0.0, 0.0).into();
540 assert!(background.is_transparent());
541 }
542
543 #[test]
544 fn test_background_linear_gradient() {
545 let from = linear_color_stop(rgba(0xff0099ff), 0.0);
546 let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
547 let background = linear_gradient(90.0, from, to);
548 assert_eq!(background.tag, BackgroundTag::LinearGradient);
549 assert_eq!(background.colors[0], from);
550 assert_eq!(background.colors[1], to);
551
552 assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
553 assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
554 assert!(!background.is_transparent());
555 assert!(background.opacity(0.0).is_transparent());
556 }
557
558 #[test]
559 fn test_background_kind() {
560 let color: Hsla = rgba(0xff0099ff).into_color();
561 assert_eq!(Background::from(color).kind(), BackgroundKind::Solid(color));
562
563 let from = linear_color_stop(rgba(0xff0099ff), 0.0);
564 let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
565 assert_eq!(
566 linear_gradient(90.0, from, to).kind(),
567 BackgroundKind::LinearGradient {
568 angle: 90.0,
569 stops: [from, to],
570 }
571 );
572
573 assert_eq!(
574 checkerboard(color, 12.0).kind(),
575 BackgroundKind::Checkerboard { color, size: 12.0 }
576 );
577 }
578
579 #[test]
580 fn test_background_kind_unpacks_pattern_slash() {
581 let color: Hsla = rgba(0xff0099ff).into_color();
582 for (width, interval) in [(1.0, 3.0), (0.5, 0.25), (2.0, 10.0)] {
584 let BackgroundKind::PatternSlash {
585 width: got_width,
586 interval: got_interval,
587 ..
588 } = pattern_slash(color, width, interval).kind()
589 else {
590 panic!("pattern_slash did not produce a PatternSlash");
591 };
592 assert!((got_width - width).abs() <= 1.0 / 255.0);
593 assert!((got_interval - interval).abs() <= 1.0 / 255.0);
594 }
595 }
596
597 #[test]
598 fn test_rgba_alpha() {
599 use palette::WithAlpha;
600 let color = Rgba::<palette::Srgb>::new(0.2, 0.6, 1.0, 0.8);
601 assert_eq!(color.with_alpha(0.25).alpha, 0.25);
602 assert_eq!(color.with_alpha(1.5).alpha, 1.5);
604 }
605
606 #[test]
607 fn test_rgba_opacity() {
608 use super::ColorExt;
609 let color = Rgba::new(0.2, 0.6, 1.0, 0.8);
610 assert!((color.opacity(0.5).alpha - 0.4).abs() < 1e-6);
611 assert_eq!(color.opacity(2.0).alpha, 0.8);
612 }
613}