1#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct Color(pub f32, pub f32, pub f32, pub f32);
5
6impl Color {
7 pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
8 Self(r, g, b, 1.0)
9 }
10
11 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
12 Self(r, g, b, a)
13 }
14
15 pub const fn from_rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
16 Self(
17 r as f32 / 255.0,
18 g as f32 / 255.0,
19 b as f32 / 255.0,
20 a as f32 / 255.0,
21 )
22 }
23
24 pub const fn from_rgb_u8(r: u8, g: u8, b: u8) -> Self {
25 Self::from_rgba_u8(r, g, b, 255)
26 }
27
28 pub fn r(&self) -> f32 {
29 self.0
30 }
31
32 pub fn g(&self) -> f32 {
33 self.1
34 }
35
36 pub fn b(&self) -> f32 {
37 self.2
38 }
39
40 pub fn a(&self) -> f32 {
41 self.3
42 }
43
44 pub fn with_alpha(&self, alpha: f32) -> Self {
45 Self(self.0, self.1, self.2, alpha)
46 }
47
48 pub fn srgb_8bit(self) -> Self {
72 Self(
73 srgb_channel_8bit(self.0),
74 srgb_channel_8bit(self.1),
75 srgb_channel_8bit(self.2),
76 srgb_channel_8bit(self.3),
77 )
78 }
79
80 pub const BLACK: Color = Color(0.0, 0.0, 0.0, 1.0);
82 pub const WHITE: Color = Color(1.0, 1.0, 1.0, 1.0);
83 pub const RED: Color = Color(1.0, 0.0, 0.0, 1.0);
84 pub const GREEN: Color = Color(0.0, 1.0, 0.0, 1.0);
85 pub const BLUE: Color = Color(0.0, 0.0, 1.0, 1.0);
86 pub const TRANSPARENT: Color = Color(0.0, 0.0, 0.0, 0.0);
87}
88
89fn srgb_channel_8bit(channel: f32) -> f32 {
91 (channel.clamp(0.0, 1.0) * 255.0).round() / 255.0
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn an_srgb_colour_is_eight_bits_and_a_half_rounds_up() {
100 let mixed = Color(9.9 / 255.0, 22.5 / 255.0, 34.2 / 255.0, 1.0);
103 let snapped = mixed.srgb_8bit();
104 assert_eq!(
105 [
106 (snapped.0 * 255.0).round() as u8,
107 (snapped.1 * 255.0).round() as u8,
108 (snapped.2 * 255.0).round() as u8,
109 ],
110 [10, 23, 34]
111 );
112 for channel in [snapped.0, snapped.1, snapped.2, snapped.3] {
116 let scaled = channel * 255.0;
117 assert!(
118 (scaled - scaled.round()).abs() < 1e-3,
119 "{scaled} is not a whole channel value"
120 );
121 }
122 }
123
124 #[test]
125 fn snapping_matches_the_platforms_own_expression_over_the_whole_range() {
126 for step in 0..=100_000u32 {
130 let channel = step as f32 / 100_000.0;
131 let platform = (channel * 255.0 + 0.5) as u32;
132 let ours = (srgb_channel_8bit(channel) * 255.0).round() as u32;
133 assert_eq!(platform, ours, "channel {channel}");
134 }
135 }
136
137 #[test]
138 fn snapping_is_idempotent_and_leaves_exact_bytes_alone() {
139 for byte in 0..=255u8 {
140 let colour = Color::from_rgba_u8(byte, byte, byte, byte);
141 assert_eq!(colour.srgb_8bit(), colour);
142 }
143 let odd = Color(0.123_456, 0.789_012, 0.5, 0.25);
144 assert_eq!(odd.srgb_8bit().srgb_8bit(), odd.srgb_8bit());
145 }
146
147 #[test]
148 fn snapping_clamps_out_of_range_channels() {
149 let wild = Color(-2.0, 1.5, 0.0, 1.0).srgb_8bit();
150 assert_eq!(wild, Color(0.0, 1.0, 0.0, 1.0));
151 }
152}