1use crate::draw::{DitherConfig, FogConfig};
2use embedded_graphics_core::pixelcolor::Rgb565;
3use embedded_graphics_core::pixelcolor::RgbColor;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum LightLevels {
8 Linear,
10 Doom32,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum StippleMode {
17 Off,
18 Checkerboard,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum PaletteMode {
24 Off,
25 Rgb332,
27}
28
29impl PaletteMode {
30 #[inline]
31 pub fn apply(self, color: Rgb565) -> Rgb565 {
32 match self {
33 PaletteMode::Off => color,
34 PaletteMode::Rgb332 => {
35 let r3 = ((color.r() as u16 * 7 + 15) / 31) as u8;
36 let g3 = ((color.g() as u16 * 7 + 31) / 63) as u8;
37 let b2 = ((color.b() as u16 * 3 + 15) / 31) as u8;
38 let r = (r3 as u16 * 31 / 7) as u8;
39 let g = (g3 as u16 * 63 / 7) as u8;
40 let b = (b2 as u16 * 31 / 3) as u8;
41 Rgb565::new(r, g, b)
42 }
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct SkyConfig {
50 pub top_color: Rgb565,
51 pub bottom_color: Rgb565,
52 pub stripe_color: Rgb565,
53 pub stripe_strength: u8,
54 pub stripe_width: u8,
55}
56
57impl SkyConfig {
58 pub const fn retro_blue() -> Self {
59 Self {
60 top_color: Rgb565::new(6, 16, 31),
61 bottom_color: Rgb565::new(1, 4, 12),
62 stripe_color: Rgb565::new(18, 30, 31),
63 stripe_strength: 18,
64 stripe_width: 16,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct ScreenTint {
72 pub color: Rgb565,
73 pub strength: u8,
75}
76
77impl ScreenTint {
78 #[inline]
79 pub fn apply(&self, base: Rgb565) -> Rgb565 {
80 let a = self.strength as u16;
81 let inv = 255u16.saturating_sub(a);
82 let r = ((base.r() as u16 * inv + self.color.r() as u16 * a) / 255) as u8;
83 let g = ((base.g() as u16 * inv + self.color.g() as u16 * a) / 255) as u8;
84 let b = ((base.b() as u16 * inv + self.color.b() as u16 * a) / 255) as u8;
85 Rgb565::new(r, g, b)
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum TextureMapping {
92 PerspectiveCorrect,
94 Affine,
96}
97
98#[derive(Debug, Clone, Copy)]
100pub struct RetroStyle {
101 pub fog: Option<FogConfig>,
103 pub dither: Option<DitherConfig>,
105 pub vertex_snap_bits: u8,
107 pub texture_mapping: TextureMapping,
109 pub light_levels: LightLevels,
111 pub stipple_mode: StippleMode,
113 pub screen_tint: Option<ScreenTint>,
115 pub palette_mode: PaletteMode,
117 pub sky: Option<SkyConfig>,
119}
120
121impl Default for RetroStyle {
122 fn default() -> Self {
123 Self::modern()
124 }
125}
126
127impl RetroStyle {
128 pub const fn modern() -> Self {
130 Self {
131 fog: None,
132 dither: None,
133 vertex_snap_bits: 0,
134 texture_mapping: TextureMapping::PerspectiveCorrect,
135 light_levels: LightLevels::Linear,
136 stipple_mode: StippleMode::Off,
137 screen_tint: None,
138 palette_mode: PaletteMode::Off,
139 sky: None,
140 }
141 }
142
143 pub const fn doom_walkable() -> Self {
145 Self {
146 fog: None,
147 dither: Some(DitherConfig { intensity: 20 }),
148 vertex_snap_bits: 0,
149 texture_mapping: TextureMapping::Affine,
150 light_levels: LightLevels::Doom32,
151 stipple_mode: StippleMode::Off,
152 screen_tint: None,
153 palette_mode: PaletteMode::Rgb332,
154 sky: Some(SkyConfig::retro_blue()),
155 }
156 }
157
158 pub fn psx() -> Self {
160 Self {
161 fog: Some(FogConfig::new(Rgb565::new(2, 2, 4), 6.0, 20.0)),
162 dither: Some(DitherConfig::new(72)),
163 vertex_snap_bits: 6,
164 texture_mapping: TextureMapping::Affine,
165 light_levels: LightLevels::Linear,
166 stipple_mode: StippleMode::Off,
167 screen_tint: None,
168 palette_mode: PaletteMode::Rgb332,
169 sky: Some(SkyConfig::retro_blue()),
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn palette_off_is_identity() {
180 let c = Rgb565::new(17, 45, 23);
181 assert_eq!(PaletteMode::Off.apply(c), c);
182 }
183
184 #[test]
185 fn palette_rgb332_quantizes_channels() {
186 let c = Rgb565::new(17, 45, 23);
187 let q = PaletteMode::Rgb332.apply(c);
188 assert_eq!(q, Rgb565::new(17, 45, 20));
190 }
191
192 #[test]
193 fn screen_tint_strength_extremes() {
194 let base = Rgb565::new(7, 20, 5);
195 let tint = Rgb565::new(31, 0, 31);
196
197 let off = ScreenTint {
198 color: tint,
199 strength: 0,
200 };
201 assert_eq!(off.apply(base), base);
202
203 let full = ScreenTint {
204 color: tint,
205 strength: 255,
206 };
207 assert_eq!(full.apply(base), tint);
208 }
209
210 #[test]
211 fn doom_walkable_preset_matches_expected_profile() {
212 let s = RetroStyle::doom_walkable();
213 assert!(s.fog.is_none());
214 assert_eq!(s.vertex_snap_bits, 0);
215 assert_eq!(s.texture_mapping, TextureMapping::Affine);
216 assert_eq!(s.light_levels, LightLevels::Doom32);
217 assert_eq!(s.stipple_mode, StippleMode::Off);
218 assert_eq!(s.palette_mode, PaletteMode::Rgb332);
219 assert!(s.sky.is_some());
220 assert!(s.dither.is_some());
221 }
222
223 #[test]
224 fn psx_preset_enables_snap_and_fog() {
225 let s = RetroStyle::psx();
226 assert_eq!(s.vertex_snap_bits, 6);
227 assert_eq!(s.texture_mapping, TextureMapping::Affine);
228 assert_eq!(s.light_levels, LightLevels::Linear);
229 assert_eq!(s.palette_mode, PaletteMode::Rgb332);
230 assert!(s.fog.is_some());
231 assert!(s.dither.is_some());
232 assert!(s.sky.is_some());
233 }
234}