Skip to main content

gizmo/
color.rs

1use gizmo_math::Vec4;
2
3/// Bevy-like color type. Holds RGBA float values (0.0 - 1.0).
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct Color(pub Vec4);
6
7impl Color {
8    // ─── Temel Renkler ────────────────────────────────────────────────────────
9    pub const RED: Color = Color(Vec4::new(1.0, 0.0, 0.0, 1.0));
10    pub const GREEN: Color = Color(Vec4::new(0.0, 1.0, 0.0, 1.0));
11    pub const BLUE: Color = Color(Vec4::new(0.0, 0.0, 1.0, 1.0));
12    pub const WHITE: Color = Color(Vec4::new(1.0, 1.0, 1.0, 1.0));
13    pub const BLACK: Color = Color(Vec4::new(0.0, 0.0, 0.0, 1.0));
14    pub const YELLOW: Color = Color(Vec4::new(1.0, 1.0, 0.0, 1.0));
15    pub const CYAN: Color = Color(Vec4::new(0.0, 1.0, 1.0, 1.0));
16    pub const MAGENTA: Color = Color(Vec4::new(1.0, 0.0, 1.0, 1.0));
17    pub const ORANGE: Color = Color(Vec4::new(1.0, 0.5, 0.0, 1.0));
18    pub const GRAY: Color = Color(Vec4::new(0.5, 0.5, 0.5, 1.0));
19    pub const DARK_GRAY: Color = Color(Vec4::new(0.2, 0.2, 0.2, 1.0));
20    pub const TRANSPARENT: Color = Color(Vec4::new(0.0, 0.0, 0.0, 0.0));
21
22    // ─── Yapıcılar ────────────────────────────────────────────────────────────
23
24    /// Construct from RGB float values (between 0.0 - 1.0).
25    #[inline]
26    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
27        Color(Vec4::new(r, g, b, 1.0))
28    }
29
30    /// Construct from RGBA float values (between 0.0 - 1.0).
31    #[inline]
32    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
33        Color(Vec4::new(r, g, b, a))
34    }
35
36    /// Construct from RGB integer values between 0-255.
37    #[inline]
38    pub fn rgb8(r: u8, g: u8, b: u8) -> Self {
39        Color(Vec4::new(
40            r as f32 / 255.0,
41            g as f32 / 255.0,
42            b as f32 / 255.0,
43            1.0,
44        ))
45    }
46
47    /// Construct from a hex string. Example: `Color::hex("#FF5733")`, `Color::hex("FF5733")` or RGBA `#FF5733AA`.
48    pub fn hex(s: &str) -> Self {
49        let s = s.trim_start_matches('#');
50        let bytes = s.as_bytes();
51        let parse = |i: usize| -> f32 {
52            let slice = std::str::from_utf8(&bytes[i..i + 2]).unwrap_or("ff");
53            u8::from_str_radix(slice, 16).unwrap_or(255) as f32 / 255.0
54        };
55        if bytes.len() >= 8 {
56            Color(Vec4::new(parse(0), parse(2), parse(4), parse(6)))
57        } else if bytes.len() >= 6 {
58            Color(Vec4::new(parse(0), parse(2), parse(4), 1.0))
59        } else {
60            Color::WHITE
61        }
62    }
63
64    /// Converts the color to Hex format and returns it. Example: "#FF5733"
65    pub fn to_hex(self) -> String {
66        let r = (self.0.x.clamp(0.0, 1.0) * 255.0).round() as u8;
67        let g = (self.0.y.clamp(0.0, 1.0) * 255.0).round() as u8;
68        let b = (self.0.z.clamp(0.0, 1.0) * 255.0).round() as u8;
69
70        if self.0.w < 0.999 {
71            let a = (self.0.w.clamp(0.0, 1.0) * 255.0).round() as u8;
72            format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
73        } else {
74            format!("#{:02X}{:02X}{:02X}", r, g, b)
75        }
76    }
77
78    /// Performs linear interpolation (blending) between two colors.
79    pub fn lerp(self, other: Color, t: f32) -> Color {
80        let t = t.clamp(0.0, 1.0);
81        Color(Vec4::new(
82            self.0.x + (other.0.x - self.0.x) * t,
83            self.0.y + (other.0.y - self.0.y) * t,
84            self.0.z + (other.0.z - self.0.z) * t,
85            self.0.w + (other.0.w - self.0.w) * t,
86        ))
87    }
88
89    /// Merge with an alpha (transparency).
90    #[inline]
91    pub fn with_alpha(mut self, a: f32) -> Self {
92        self.0.w = a;
93        self
94    }
95
96    /// Return the inner Vec4 value.
97    #[inline]
98    pub fn to_vec4(self) -> Vec4 {
99        self.0
100    }
101}
102
103impl Default for Color {
104    /// Default color: opaque white (`Color::WHITE`).
105    fn default() -> Self {
106        Color::WHITE
107    }
108}
109
110impl From<Color> for Vec4 {
111    fn from(c: Color) -> Vec4 {
112        c.0
113    }
114}
115
116impl From<Vec4> for Color {
117    fn from(v: Vec4) -> Color {
118        Color(v)
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    fn approx(a: f32, b: f32) {
127        assert!((a - b).abs() < 1e-6, "{a} != {b}");
128    }
129
130    #[test]
131    fn rgb_sets_opaque_alpha() {
132        let c = Color::rgb(0.1, 0.2, 0.3);
133        assert_eq!(c.0, Vec4::new(0.1, 0.2, 0.3, 1.0));
134    }
135
136    #[test]
137    fn rgba_passes_alpha_through() {
138        let c = Color::rgba(0.1, 0.2, 0.3, 0.4);
139        assert_eq!(c.0, Vec4::new(0.1, 0.2, 0.3, 0.4));
140    }
141
142    #[test]
143    fn rgb8_maps_255_to_one_and_0_to_zero() {
144        assert_eq!(Color::rgb8(255, 0, 128).0, Vec4::new(1.0, 0.0, 128.0 / 255.0, 1.0));
145        // sınır: 0 ve 255 tam uçlar
146        assert_eq!(Color::rgb8(0, 0, 0), Color::BLACK);
147        assert_eq!(Color::rgb8(255, 255, 255), Color::WHITE);
148    }
149
150    #[test]
151    fn hex_parses_with_and_without_hash() {
152        let a = Color::hex("#FF5733");
153        let b = Color::hex("FF5733");
154        assert_eq!(a, b);
155        approx(a.0.x, 1.0);
156        approx(a.0.y, 0x57 as f32 / 255.0);
157        approx(a.0.z, 0x33 as f32 / 255.0);
158        approx(a.0.w, 1.0);
159    }
160
161    #[test]
162    fn hex_parses_rgba_8_digits() {
163        let c = Color::hex("#FF573380");
164        approx(c.0.x, 1.0);
165        approx(c.0.w, 0x80 as f32 / 255.0);
166    }
167
168    #[test]
169    fn hex_too_short_falls_back_to_white() {
170        assert_eq!(Color::hex("#abc"), Color::WHITE);
171        assert_eq!(Color::hex(""), Color::WHITE);
172        assert_eq!(Color::hex("#"), Color::WHITE);
173    }
174
175    #[test]
176    fn hex_invalid_digits_default_to_full_channel() {
177        // u8::from_str_radix hatası → unwrap_or(255) → 1.0 (panik YOK)
178        let c = Color::hex("ZZZZZZ");
179        assert_eq!(c, Color::WHITE);
180    }
181
182    #[test]
183    fn to_hex_opaque_omits_alpha() {
184        assert_eq!(Color::RED.to_hex(), "#FF0000");
185        assert_eq!(Color::WHITE.to_hex(), "#FFFFFF");
186        assert_eq!(Color::BLACK.to_hex(), "#000000");
187    }
188
189    #[test]
190    fn to_hex_includes_alpha_when_translucent() {
191        // 0.5 * 255 = 127.5 → round = 128 = 0x80
192        assert_eq!(Color::rgba(1.0, 0.0, 0.0, 0.5).to_hex(), "#FF000080");
193    }
194
195    #[test]
196    fn to_hex_clamps_out_of_range_channels() {
197        // negatif/1'in üstü kanallar [0,1]'e kırpılır
198        assert_eq!(Color::rgba(2.0, -1.0, 0.5, 1.0).to_hex(), "#FF0080");
199    }
200
201    #[test]
202    fn hex_round_trips_within_one_lsb() {
203        for c in [Color::RED, Color::ORANGE, Color::rgb8(17, 200, 99), Color::rgba(0.3, 0.6, 0.9, 0.4)] {
204            let back = Color::hex(&c.to_hex());
205            for (a, b) in [(c.0.x, back.0.x), (c.0.y, back.0.y), (c.0.z, back.0.z), (c.0.w, back.0.w)] {
206                assert!((a - b).abs() <= 1.0 / 255.0 + 1e-6, "{a} vs {b}");
207            }
208        }
209    }
210
211    #[test]
212    fn lerp_endpoints_and_midpoint() {
213        assert_eq!(Color::BLACK.lerp(Color::WHITE, 0.0), Color::BLACK);
214        assert_eq!(Color::BLACK.lerp(Color::WHITE, 1.0), Color::WHITE);
215        let mid = Color::BLACK.lerp(Color::WHITE, 0.5);
216        assert_eq!(mid.0, Vec4::new(0.5, 0.5, 0.5, 1.0));
217    }
218
219    #[test]
220    fn lerp_clamps_t_outside_unit_range() {
221        assert_eq!(Color::BLACK.lerp(Color::WHITE, -3.0), Color::BLACK);
222        assert_eq!(Color::BLACK.lerp(Color::WHITE, 5.0), Color::WHITE);
223    }
224
225    #[test]
226    fn with_alpha_only_touches_w() {
227        let c = Color::RED.with_alpha(0.25);
228        assert_eq!(c.0, Vec4::new(1.0, 0.0, 0.0, 0.25));
229    }
230
231    #[test]
232    fn default_is_opaque_white() {
233        assert_eq!(Color::default(), Color::WHITE);
234    }
235
236    #[test]
237    fn vec4_conversions_round_trip() {
238        let v = Vec4::new(0.2, 0.4, 0.6, 0.8);
239        let c: Color = v.into();
240        let back: Vec4 = c.into();
241        assert_eq!(v, back);
242        assert_eq!(c.to_vec4(), v);
243    }
244}