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 {
22 let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
23 Rgba {
24 color: palette::rgb::Rgb::new(r, g, b),
25 alpha: 1.0,
26 }
27}
28
29pub fn rgba(hex: u32) -> Rgba {
40 let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
41 Rgba::new(r, g, b, a)
42}
43
44pub fn rgb_to_hsla(color: Rgba) -> Hsla {
49 color.into_color()
50}
51
52pub fn hsla_to_rgba(color: Hsla) -> Rgba {
54 color.into_color()
55}
56
57pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
66 color.swap(0, 2);
67 if color[3] > 0 {
68 let a = color[3] as f32 / 255.;
69 color[0] = (color[0] as f32 / a) as u8;
70 color[1] = (color[1] as f32 / a) as u8;
71 color[2] = (color[2] as f32 / a) as u8;
72 }
73}
74
75pub const fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
89 Hsla {
90 color: palette::Hsl::new_const(
91 RgbHue::new(h.clamp(0., 1.) * 360.),
94 s.clamp(0., 1.),
95 l.clamp(0., 1.),
96 ),
97 alpha: a.clamp(0., 1.),
98 }
99}
100
101pub fn oklcha<T>(lightness: T, chroma: T, hue: impl Into<OklabHue<T>>, alpha: T) -> Oklcha<T> {
106 Oklcha::new(lightness, chroma, hue, alpha)
107}
108
109pub const fn black() -> Hsla {
111 Hsla::new_const(RgbHue::new(0.), 0., 0., 1.)
112}
113
114pub const fn transparent_black() -> Hsla {
116 Hsla::new_const(RgbHue::new(0.), 0., 0., 0.)
117}
118
119pub const fn transparent_white() -> Hsla {
121 Hsla::new_const(RgbHue::new(0.), 0., 1., 0.)
122}
123
124pub const fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
129 Hsla::new_const(RgbHue::new(0.), 0., lightness, opacity)
130}
131
132pub const fn white() -> Hsla {
134 Hsla::new_const(RgbHue::new(0.), 0., 1., 1.)
135}
136
137pub const fn red() -> Hsla {
139 Hsla::new_const(RgbHue::new(0.), 1., 0.5, 1.)
140}
141
142pub const fn blue() -> Hsla {
144 Hsla::new_const(RgbHue::new(240.), 1., 0.5, 1.)
145}
146
147pub const fn green() -> Hsla {
149 Hsla::new_const(RgbHue::new(120.), 1., 0.25, 1.)
150}
151
152pub const fn yellow() -> Hsla {
154 Hsla::new_const(RgbHue::new(60.), 1., 0.5, 1.)
155}
156
157pub fn hsla_schemar(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
159 json_schema!({
162 "type": "object",
163 "properties": {
164 "hue": {
165 "type": "number",
166 "format": "float"
167 },
168 "saturation": {
169 "type": "number",
170 "format": "float"
171 },
172 "lightness": {
173 "type": "number",
174 "format": "float"
175 },
176 "alpha": {
177 "type": "number",
178 "format": "float"
179 },
180 }
181 })
182}
183
184pub fn rgba_schemar(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
186 json_schema!({
189 "type": "object",
190 "properties": {
191 "red": {
192 "type": "number",
193 "format": "float"
194 },
195 "green": {
196 "type": "number",
197 "format": "float"
198 },
199 "blue": {
200 "type": "number",
201 "format": "float"
202 },
203 "alpha": {
204 "type": "number",
205 "format": "float"
206 },
207 }
208 })
209}
210
211pub trait ColorExt {
216 fn blend(&self, other: &Self) -> Self
218 where
219 Self: Sized;
220 fn fade_out(&mut self, factor: f32);
225 fn opacity(&self, factor: f32) -> Self
250 where
251 Self: Sized;
252}
253impl ColorExt for Rgba {
254 fn blend(&self, other: &Self) -> Self {
255 use palette::blend::Compose;
256 other.over(*self)
257 }
258
259 fn fade_out(&mut self, factor: f32) {
260 self.alpha *= 1.0 - factor.clamp(0., 1.);
261 }
262
263 fn opacity(&self, factor: f32) -> Self {
264 let mut color = *self;
265 color.alpha *= factor.clamp(0., 1.);
266 color
267 }
268}
269impl ColorExt for Hsla {
270 fn blend(&self, other: &Self) -> Self {
271 let this: Rgba = (*self).into_color();
272 let other: Rgba = (*other).into_color();
273 this.blend(&other).into_color()
274 }
275
276 fn fade_out(&mut self, factor: f32) {
277 self.alpha *= 1.0 - factor.clamp(0., 1.);
278 }
279
280 fn opacity(&self, factor: f32) -> Self {
281 let mut color = *self;
282 color.alpha *= factor.clamp(0., 1.);
283 color
284 }
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
289#[repr(u32)]
290pub enum BackgroundTag {
291 Solid = 0,
293 LinearGradient = 1,
295 PatternSlash = 2,
297 Checkerboard = 3,
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
307#[repr(u32)]
308pub enum ColorSpace {
309 #[default]
310 Srgb = 0,
312 Oklab = 1,
314}
315
316impl Display for ColorSpace {
317 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
318 match self {
319 ColorSpace::Srgb => write!(f, "sRGB"),
320 ColorSpace::Oklab => write!(f, "Oklab"),
321 }
322 }
323}
324
325#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
347#[repr(C)]
348pub struct Background {
349 pub(crate) tag: BackgroundTag,
350 pub(crate) color_space: ColorSpace,
351 pub(crate) solid: crate::SceneHsla,
352 pub(crate) gradient_angle_or_pattern_height: f32,
353 pub(crate) colors: [LinearColorStop; 2],
354 pub(crate) padding: u32,
356}
357
358impl std::fmt::Debug for Background {
359 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
360 match self.tag {
361 BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
362 BackgroundTag::LinearGradient => write!(
363 f,
364 "LinearGradient({}, {:?}, {:?})",
365 self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
366 ),
367 BackgroundTag::PatternSlash => write!(
368 f,
369 "PatternSlash({:?}, {})",
370 self.solid, self.gradient_angle_or_pattern_height
371 ),
372 BackgroundTag::Checkerboard => write!(
373 f,
374 "Checkerboard({:?}, {})",
375 self.solid, self.gradient_angle_or_pattern_height
376 ),
377 }
378 }
379}
380
381impl Eq for Background {}
382impl Default for Background {
383 fn default() -> Self {
385 Self {
386 tag: BackgroundTag::Solid,
387 solid: Hsla::default().into(),
388 color_space: ColorSpace::default(),
389 gradient_angle_or_pattern_height: 0.0,
390 colors: [LinearColorStop::default(), LinearColorStop::default()],
391 padding: 0,
392 }
393 }
394}
395
396pub fn pattern_slash(color: impl IntoColor<Hsla>, width: f32, interval: f32) -> Background {
403 const PATTERN_COMPONENT_SCALE: f32 = u8::MAX as f32;
404 const PATTERN_PACKING_RADIX: u32 = u16::MAX as u32;
405
406 let width_scaled = (width * PATTERN_COMPONENT_SCALE) as u32;
407 let interval_scaled = (interval * PATTERN_COMPONENT_SCALE) as u32;
408 let height = (width_scaled * PATTERN_PACKING_RADIX + interval_scaled) as f32;
409
410 Background {
411 tag: BackgroundTag::PatternSlash,
412 solid: color.into_color().into(),
413 gradient_angle_or_pattern_height: height,
414 ..Default::default()
415 }
416}
417
418pub fn checkerboard(color: impl IntoColor<Hsla>, size: f32) -> Background {
422 Background {
423 tag: BackgroundTag::Checkerboard,
424 solid: color.into_color().into(),
425 gradient_angle_or_pattern_height: size,
426 ..Default::default()
427 }
428}
429
430pub fn solid_background(color: impl IntoColor<Hsla>) -> Background {
434 Background {
435 solid: color.into_color().into(),
436 ..Default::default()
437 }
438}
439
440pub fn linear_gradient(
461 angle: f32,
462 from: impl Into<LinearColorStop>,
463 to: impl Into<LinearColorStop>,
464) -> Background {
465 Background {
466 tag: BackgroundTag::LinearGradient,
467 gradient_angle_or_pattern_height: angle,
468 colors: [from.into(), to.into()],
469 color_space: ColorSpace::Oklab,
470 ..Default::default()
471 }
472}
473
474#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
478#[repr(C)]
479pub struct LinearColorStop {
480 pub color: crate::SceneHsla,
482 pub percentage: f32,
484}
485
486pub fn linear_color_stop(color: impl IntoColor<Hsla>, percentage: f32) -> LinearColorStop {
490 LinearColorStop {
491 color: color.into_color().into(),
492 percentage,
493 }
494}
495
496impl LinearColorStop {
497 pub fn opacity(&self, factor: f32) -> Self {
501 let color: Hsla = self.color.into();
502 Self {
503 percentage: self.percentage,
504 color: color.opacity(factor).into(),
505 }
506 }
507}
508
509#[derive(Debug, Clone, Copy, PartialEq)]
511pub enum BackgroundKind {
512 Solid(Hsla),
514 LinearGradient {
516 angle: f32,
518 stops: [LinearColorStop; 2],
520 },
521 PatternSlash {
523 color: Hsla,
525 width: f32,
527 interval: f32,
529 },
530 Checkerboard {
532 color: Hsla,
534 size: f32,
536 },
537}
538
539impl Background {
540 pub fn as_solid(&self) -> Option<Hsla> {
542 if self.tag == BackgroundTag::Solid {
543 Some(self.solid.into())
544 } else {
545 None
546 }
547 }
548
549 pub fn kind(&self) -> BackgroundKind {
551 match self.tag {
552 BackgroundTag::Solid => BackgroundKind::Solid(self.solid.into()),
553 BackgroundTag::LinearGradient => BackgroundKind::LinearGradient {
554 angle: self.gradient_angle_or_pattern_height,
555 stops: self.colors,
556 },
557 BackgroundTag::PatternSlash => {
558 const PATTERN_COMPONENT_SCALE: f32 = u8::MAX as f32;
559 const PATTERN_PACKING_RADIX: f32 = u16::MAX as f32;
560
561 let packed = self.gradient_angle_or_pattern_height;
564 BackgroundKind::PatternSlash {
565 color: self.solid.into(),
566 width: (packed / PATTERN_PACKING_RADIX).floor() / PATTERN_COMPONENT_SCALE,
567 interval: (packed.rem_euclid(PATTERN_PACKING_RADIX)) / PATTERN_COMPONENT_SCALE,
568 }
569 }
570 BackgroundTag::Checkerboard => BackgroundKind::Checkerboard {
571 color: self.solid.into(),
572 size: self.gradient_angle_or_pattern_height,
573 },
574 }
575 }
576
577 pub fn color_space(mut self, color_space: ColorSpace) -> Self {
584 self.color_space = color_space;
585 self
586 }
587
588 pub fn interpolation_space(&self) -> ColorSpace {
590 self.color_space
591 }
592
593 pub fn opacity(&self, factor: f32) -> Self {
597 let mut background = *self;
598 let solid: Hsla = background.solid.into();
599 background.solid = solid.opacity(factor).into();
600 background.colors = [
601 self.colors[0].opacity(factor),
602 self.colors[1].opacity(factor),
603 ];
604 background
605 }
606
607 pub fn is_transparent(&self) -> bool {
612 match self.tag {
613 BackgroundTag::Solid => self.solid.a == 0.,
614 BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.a == 0.),
615 BackgroundTag::PatternSlash => self.solid.a == 0.,
616 BackgroundTag::Checkerboard => self.solid.a == 0.,
617 }
618 }
619}
620
621impl<T: IntoColor<Hsla>> From<T> for Background {
622 fn from(value: T) -> Self {
624 Self {
625 tag: BackgroundTag::Solid,
626 solid: value.into_color().into(),
627 ..Default::default()
628 }
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635
636 #[test]
637 fn color_blend_composites_the_overlay_over_the_base() {
638 let assert_close = |actual: Rgba, expected: Rgba| {
639 for (actual, expected) in [
640 (actual.red, expected.red),
641 (actual.green, expected.green),
642 (actual.blue, expected.blue),
643 (actual.alpha, expected.alpha),
644 ] {
645 assert!(
646 (actual - expected).abs() < 1e-6,
647 "{actual:?} != {expected:?}"
648 );
649 }
650 };
651
652 let base = Rgba::new(1.0, 0.0, 0.0, 1.0);
653 assert_close(base.blend(&Rgba::new(0.0, 0.0, 1.0, 0.0)), base);
654 assert_close(
655 base.blend(&Rgba::new(0.0, 0.0, 1.0, 1.0)),
656 Rgba::new(0.0, 0.0, 1.0, 1.0),
657 );
658 assert_close(
659 base.blend(&Rgba::new(0.0, 0.0, 1.0, 0.25)),
660 Rgba::new(0.75, 0.0, 0.25, 1.0),
661 );
662 }
663
664 #[test]
665 fn highlight_keeps_the_base_color_under_a_transparent_overlay() {
666 let base = crate::TextStyle {
667 color: red(),
668 ..Default::default()
669 };
670 let transparent_blue = Hsla::new_const(RgbHue::new(240.), 1., 0.5, 0.);
671 let highlighted = base
672 .clone()
673 .highlight(crate::HighlightStyle::color(transparent_blue));
674 assert_eq!(highlighted.color, base.color);
675 }
676
677 #[test]
678 fn test_background_solid() {
679 let color: Hsla = rgba(0xff0099ff).into_color();
680 let mut background = Background::from(color);
681 assert_eq!(background.tag, BackgroundTag::Solid);
682 assert_eq!(background.solid, color.into());
683
684 assert_eq!(background.opacity(0.5).solid, color.opacity(0.5).into());
685 assert!(!background.is_transparent());
686 background.solid = hsla(0.0, 0.0, 0.0, 0.0).into();
687 assert!(background.is_transparent());
688 }
689
690 #[test]
691 fn test_background_linear_gradient() {
692 let from = linear_color_stop(rgba(0xff0099ff), 0.0);
693 let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
694 let background = linear_gradient(90.0, from, to);
695 assert_eq!(background.tag, BackgroundTag::LinearGradient);
696 assert_eq!(background.colors[0], from);
697 assert_eq!(background.colors[1], to);
698
699 assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
700 assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
701 assert!(!background.is_transparent());
702 assert!(background.opacity(0.0).is_transparent());
703 }
704
705 #[test]
706 fn test_background_kind() {
707 let color: Hsla = rgba(0xff0099ff).into_color();
708 assert_eq!(Background::from(color).kind(), BackgroundKind::Solid(color));
709
710 let from = linear_color_stop(rgba(0xff0099ff), 0.0);
711 let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
712 assert_eq!(
713 linear_gradient(90.0, from, to).kind(),
714 BackgroundKind::LinearGradient {
715 angle: 90.0,
716 stops: [from, to],
717 }
718 );
719
720 assert_eq!(
721 checkerboard(color, 12.0).kind(),
722 BackgroundKind::Checkerboard { color, size: 12.0 }
723 );
724 }
725
726 #[test]
727 fn test_background_kind_unpacks_pattern_slash() {
728 let color: Hsla = rgba(0xff0099ff).into_color();
729 for (width, interval) in [(1.0, 3.0), (0.5, 0.25), (2.0, 10.0)] {
731 let BackgroundKind::PatternSlash {
732 width: got_width,
733 interval: got_interval,
734 ..
735 } = pattern_slash(color, width, interval).kind()
736 else {
737 panic!("pattern_slash did not produce a PatternSlash");
738 };
739 assert!((got_width - width).abs() <= 1.0 / 255.0);
740 assert!((got_interval - interval).abs() <= 1.0 / 255.0);
741 }
742 }
743
744 #[test]
745 fn test_rgba_alpha() {
746 use palette::WithAlpha;
747 let color = Rgba::<palette::Srgb>::new(0.2, 0.6, 1.0, 0.8);
748 assert_eq!(color.with_alpha(0.25).alpha, 0.25);
749 assert_eq!(color.with_alpha(1.5).alpha, 1.5);
751 }
752
753 #[test]
754 fn test_rgba_opacity() {
755 use super::ColorExt;
756 let color = Rgba::new(0.2, 0.6, 1.0, 0.8);
757 assert!((color.opacity(0.5).alpha - 0.4).abs() < 1e-6);
758 assert_eq!(color.opacity(2.0).alpha, 0.8);
759 }
760}