Skip to main content

steel_utils/
color.rs

1//! Packed color values used by Vanilla codecs and network payloads.
2
3use std::io::{Cursor, Result, Write};
4
5use crate::serial::{ReadFrom, WriteTo};
6
7/// A packed color interpreted through its red, green, and blue channels.
8///
9/// The upper byte is preserved because Vanilla's RGB codecs do not normalize
10/// integer inputs, even though RGB consumers ignore that byte.
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
12#[repr(transparent)]
13pub struct RgbColor(i32);
14
15impl RgbColor {
16    /// Preserves a raw Vanilla packed integer as an RGB-semantic color.
17    #[must_use]
18    pub const fn new(raw: i32) -> Self {
19        Self(raw)
20    }
21
22    /// Returns the unchanged packed integer.
23    #[must_use]
24    pub const fn raw(self) -> i32 {
25        self.0
26    }
27
28    /// Returns the red channel.
29    #[must_use]
30    pub const fn red(self) -> u8 {
31        (self.0 as u32 >> 16) as u8
32    }
33
34    /// Returns the green channel.
35    #[must_use]
36    pub const fn green(self) -> u8 {
37        (self.0 as u32 >> 8) as u8
38    }
39
40    /// Returns the blue channel.
41    #[must_use]
42    pub const fn blue(self) -> u8 {
43        self.0 as u8
44    }
45
46    /// Replaces the ignored upper byte with an alpha channel.
47    #[must_use]
48    pub const fn with_alpha(self, alpha: u8) -> ArgbColor {
49        ArgbColor::new(((alpha as u32) << 24 | (self.0 as u32 & 0x00ff_ffff)) as i32)
50    }
51}
52
53impl WriteTo for RgbColor {
54    fn write(&self, writer: &mut impl Write) -> Result<()> {
55        self.0.write(writer)
56    }
57}
58
59impl ReadFrom for RgbColor {
60    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
61        Ok(Self::new(i32::read(data)?))
62    }
63}
64
65/// A packed color interpreted through its alpha, red, green, and blue channels.
66#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
67#[repr(transparent)]
68pub struct ArgbColor(i32);
69
70impl ArgbColor {
71    /// Preserves a raw Vanilla packed integer as an ARGB-semantic color.
72    #[must_use]
73    pub const fn new(raw: i32) -> Self {
74        Self(raw)
75    }
76
77    /// Returns the unchanged packed integer.
78    #[must_use]
79    pub const fn raw(self) -> i32 {
80        self.0
81    }
82
83    /// Returns the alpha channel.
84    #[must_use]
85    pub const fn alpha(self) -> u8 {
86        (self.0 as u32 >> 24) as u8
87    }
88
89    /// Returns the red channel.
90    #[must_use]
91    pub const fn red(self) -> u8 {
92        (self.0 as u32 >> 16) as u8
93    }
94
95    /// Returns the green channel.
96    #[must_use]
97    pub const fn green(self) -> u8 {
98        (self.0 as u32 >> 8) as u8
99    }
100
101    /// Returns the blue channel.
102    #[must_use]
103    pub const fn blue(self) -> u8 {
104        self.0 as u8
105    }
106
107    /// Returns the same packed bits with RGB semantics.
108    #[must_use]
109    pub const fn rgb(self) -> RgbColor {
110        RgbColor::new(self.0)
111    }
112
113    /// Returns the per-channel mean of two colors.
114    #[must_use]
115    pub const fn average(self, other: Self) -> Self {
116        Self(
117            ((Self::average_channel(self.alpha(), other.alpha()) << 24)
118                | (Self::average_channel(self.red(), other.red()) << 16)
119                | (Self::average_channel(self.green(), other.green()) << 8)
120                | Self::average_channel(self.blue(), other.blue())) as i32,
121        )
122    }
123
124    const fn average_channel(lhs: u8, rhs: u8) -> u32 {
125        u32::midpoint(lhs as u32, rhs as u32)
126    }
127}
128
129impl WriteTo for ArgbColor {
130    fn write(&self, writer: &mut impl Write) -> Result<()> {
131        self.0.write(writer)
132    }
133}
134
135impl ReadFrom for ArgbColor {
136    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
137        Ok(Self::new(i32::read(data)?))
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn argb_average_matches_vanilla_per_channel_mean() {
147        let lhs = ArgbColor::new(0xff00_0000_u32 as i32);
148        let rhs = ArgbColor::new(0x00ff_ffff);
149
150        // Vanilla `ARGB.average` averages each channel independently with integer division.
151        assert_eq!(lhs.average(rhs).raw(), 0x7f7f_7f7f);
152        assert_eq!(
153            ArgbColor::new(0xffff_0000_u32 as i32)
154                .average(ArgbColor::new(0xff00_00ff_u32 as i32))
155                .raw(),
156            0xff7f_007f_u32 as i32
157        );
158    }
159
160    #[test]
161    fn rgb_preserves_ignored_upper_byte() {
162        let color = RgbColor::new(0x7f12_3456);
163
164        assert_eq!(color.raw(), 0x7f12_3456);
165        assert_eq!(
166            (color.red(), color.green(), color.blue()),
167            (0x12, 0x34, 0x56)
168        );
169        assert_eq!(color.with_alpha(0xaa).raw(), 0xaa12_3456_u32 as i32);
170    }
171
172    #[test]
173    fn argb_exposes_all_channels() {
174        let color = ArgbColor::new(0xaabb_ccdd_u32 as i32);
175
176        assert_eq!(
177            (color.alpha(), color.red(), color.green(), color.blue()),
178            (0xaa, 0xbb, 0xcc, 0xdd)
179        );
180    }
181}