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, PartialEq)]
100pub struct TextureLodConfig {
101 pub near_distance: f32,
103 pub far_distance: f32,
105 pub fallback_color: Rgb565,
107}
108
109impl TextureLodConfig {
110 pub const fn new(near_distance: f32, far_distance: f32, fallback_color: Rgb565) -> Self {
112 Self {
113 near_distance,
114 far_distance,
115 fallback_color,
116 }
117 }
118
119 #[inline]
121 pub fn should_drop_texture(&self, z: f32) -> bool {
122 z >= self.far_distance
123 }
124
125 #[inline]
127 pub fn is_in_transition(&self, z: f32) -> bool {
128 z >= self.near_distance && z < self.far_distance
129 }
130
131 #[inline]
133 pub fn flat_blend_factor(&self, z: f32) -> f32 {
134 if z <= self.near_distance {
135 0.0
136 } else if z >= self.far_distance {
137 1.0
138 } else {
139 (z - self.near_distance) / (self.far_distance - self.near_distance)
140 }
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct AnimatedPalette<const N: usize> {
147 pub colors: [Rgb565; N],
149 pub cycle_start: usize,
151 pub cycle_end: usize,
153 pub offset: usize,
155}
156
157impl<const N: usize> AnimatedPalette<N> {
158 pub const fn new(colors: [Rgb565; N]) -> Self {
160 Self {
161 colors,
162 cycle_start: 0,
163 cycle_end: N,
164 offset: 0,
165 }
166 }
167
168 pub const fn with_cycle_range(mut self, start: usize, end: usize) -> Self {
170 self.cycle_start = start;
171 self.cycle_end = if end <= N { end } else { N };
172 self
173 }
174
175 pub fn step(&mut self, steps: usize) {
177 let range_len = self.cycle_end.saturating_sub(self.cycle_start);
178 if range_len > 1 {
179 self.offset = (self.offset + steps) % range_len;
180 }
181 }
182
183 #[inline]
185 pub fn get_color(&self, index: usize) -> Rgb565 {
186 if index >= N {
187 return Rgb565::BLACK;
188 }
189 if index >= self.cycle_start && index < self.cycle_end {
190 let range_len = self.cycle_end - self.cycle_start;
191 let cycled_idx =
192 self.cycle_start + (index - self.cycle_start + self.offset) % range_len;
193 self.colors[cycled_idx]
194 } else {
195 self.colors[index]
196 }
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
202pub enum CycleDirection {
203 #[default]
205 Forward,
206 Reverse,
208 PingPong,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq)]
214pub struct PaletteSlice {
215 pub start: usize,
217 pub end: usize,
219 pub rate_hz: f32,
221 pub direction: CycleDirection,
223 pub timer: f32,
225 pub current_step: usize,
227 pub ping_pong_forward: bool,
229}
230
231impl PaletteSlice {
232 pub const fn new(start: usize, end: usize, rate_hz: f32, direction: CycleDirection) -> Self {
234 Self {
235 start,
236 end,
237 rate_hz,
238 direction,
239 timer: 0.0,
240 current_step: 0,
241 ping_pong_forward: true,
242 }
243 }
244
245 pub fn advance(&mut self, dt: f32) -> bool {
247 let span = self.end.saturating_sub(self.start);
248 if span <= 1 || self.rate_hz <= 0.0 {
249 return false;
250 }
251
252 self.timer += dt;
253 let period = 1.0 / self.rate_hz;
254 let mut changed = false;
255
256 while self.timer >= period {
257 self.timer -= period;
258 changed = true;
259
260 match self.direction {
261 CycleDirection::Forward => {
262 self.current_step = (self.current_step + 1) % span;
263 }
264 CycleDirection::Reverse => {
265 self.current_step = if self.current_step == 0 {
266 span - 1
267 } else {
268 self.current_step - 1
269 };
270 }
271 CycleDirection::PingPong => {
272 if self.ping_pong_forward {
273 if self.current_step + 1 >= span {
274 self.ping_pong_forward = false;
275 self.current_step = self.current_step.saturating_sub(1);
276 } else {
277 self.current_step += 1;
278 }
279 } else if self.current_step == 0 {
280 self.ping_pong_forward = true;
281 self.current_step = 1.min(span - 1);
282 } else {
283 self.current_step -= 1;
284 }
285 }
286 }
287 }
288
289 changed
290 }
291
292 #[inline]
294 pub fn map_index(&self, index: usize) -> usize {
295 let span = self.end.saturating_sub(self.start);
296 if span <= 1 || index < self.start || index >= self.end {
297 return index;
298 }
299 self.start + (index - self.start + self.current_step) % span
300 }
301}
302
303#[derive(Debug, Clone, Copy)]
308pub struct PaletteCycler<const MAX_SLICES: usize = 4> {
309 pub slices: [Option<PaletteSlice>; MAX_SLICES],
311 pub slice_count: usize,
313}
314
315impl<const MAX_SLICES: usize> Default for PaletteCycler<MAX_SLICES> {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321impl<const MAX_SLICES: usize> PaletteCycler<MAX_SLICES> {
322 pub const fn new() -> Self {
324 Self {
325 slices: [None; MAX_SLICES],
326 slice_count: 0,
327 }
328 }
329
330 pub fn add_slice(&mut self, slice: PaletteSlice) -> bool {
332 if self.slice_count < MAX_SLICES {
333 self.slices[self.slice_count] = Some(slice);
334 self.slice_count += 1;
335 true
336 } else {
337 false
338 }
339 }
340
341 pub fn add_range(
343 &mut self,
344 start: usize,
345 end: usize,
346 rate_hz: f32,
347 direction: CycleDirection,
348 ) -> bool {
349 self.add_slice(PaletteSlice::new(start, end, rate_hz, direction))
350 }
351
352 pub fn advance(&mut self, dt: f32) -> bool {
356 let mut any_changed = false;
357 for slice in self.slices.iter_mut().take(self.slice_count).flatten() {
358 if slice.advance(dt) {
359 any_changed = true;
360 }
361 }
362 any_changed
363 }
364
365 #[inline]
367 pub fn map_index(&self, index: usize) -> usize {
368 let mut mapped = index;
369 for slice in self.slices.iter().take(self.slice_count).flatten() {
370 if index >= slice.start && index < slice.end {
371 mapped = slice.map_index(index);
372 break;
373 }
374 }
375 mapped
376 }
377
378 pub fn cycle_palette<const N: usize>(&self, base: &[Rgb565; N], dest: &mut [Rgb565; N]) {
380 for i in 0..N {
381 dest[i] = base[self.map_index(i)];
382 }
383 }
384
385 pub fn cycle_slice(&self, base: &[Rgb565], dest: &mut [Rgb565]) {
387 let count = base.len().min(dest.len());
388 for i in 0..count {
389 dest[i] = base[self.map_index(i)];
390 }
391 }
392}
393
394#[derive(Debug, Clone, Copy)]
396pub struct RetroStyle {
397 pub fog: Option<FogConfig>,
399 pub dither: Option<DitherConfig>,
401 pub vertex_snap_bits: u8,
403 pub texture_mapping: TextureMapping,
405 pub light_levels: LightLevels,
407 pub stipple_mode: StippleMode,
409 pub screen_tint: Option<ScreenTint>,
411 pub palette_mode: PaletteMode,
413 pub sky: Option<SkyConfig>,
415}
416
417impl Default for RetroStyle {
418 fn default() -> Self {
419 Self::modern()
420 }
421}
422
423impl RetroStyle {
424 pub const fn modern() -> Self {
426 Self {
427 fog: None,
428 dither: None,
429 vertex_snap_bits: 0,
430 texture_mapping: TextureMapping::PerspectiveCorrect,
431 light_levels: LightLevels::Linear,
432 stipple_mode: StippleMode::Off,
433 screen_tint: None,
434 palette_mode: PaletteMode::Off,
435 sky: None,
436 }
437 }
438
439 pub const fn doom_walkable() -> Self {
441 Self {
442 fog: None,
443 dither: Some(DitherConfig { intensity: 20 }),
444 vertex_snap_bits: 0,
445 texture_mapping: TextureMapping::Affine,
446 light_levels: LightLevels::Doom32,
447 stipple_mode: StippleMode::Off,
448 screen_tint: None,
449 palette_mode: PaletteMode::Rgb332,
450 sky: Some(SkyConfig::retro_blue()),
451 }
452 }
453
454 pub fn psx() -> Self {
456 Self {
457 fog: Some(FogConfig::new(Rgb565::new(2, 2, 4), 6.0, 20.0)),
458 dither: Some(DitherConfig::new(72)),
459 vertex_snap_bits: 6,
460 texture_mapping: TextureMapping::Affine,
461 light_levels: LightLevels::Linear,
462 stipple_mode: StippleMode::Off,
463 screen_tint: None,
464 palette_mode: PaletteMode::Rgb332,
465 sky: Some(SkyConfig::retro_blue()),
466 }
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn palette_off_is_identity() {
476 let c = Rgb565::new(17, 45, 23);
477 assert_eq!(PaletteMode::Off.apply(c), c);
478 }
479
480 #[test]
481 fn palette_rgb332_quantizes_channels() {
482 let c = Rgb565::new(17, 45, 23);
483 let q = PaletteMode::Rgb332.apply(c);
484 assert_eq!(q, Rgb565::new(17, 45, 20));
486 }
487
488 #[test]
489 fn screen_tint_strength_extremes() {
490 let base = Rgb565::new(7, 20, 5);
491 let tint = Rgb565::new(31, 0, 31);
492
493 let off = ScreenTint {
494 color: tint,
495 strength: 0,
496 };
497 assert_eq!(off.apply(base), base);
498
499 let full = ScreenTint {
500 color: tint,
501 strength: 255,
502 };
503 assert_eq!(full.apply(base), tint);
504 }
505
506 #[test]
507 fn doom_walkable_preset_matches_expected_profile() {
508 let s = RetroStyle::doom_walkable();
509 assert!(s.fog.is_none());
510 assert_eq!(s.vertex_snap_bits, 0);
511 assert_eq!(s.texture_mapping, TextureMapping::Affine);
512 assert_eq!(s.light_levels, LightLevels::Doom32);
513 assert_eq!(s.stipple_mode, StippleMode::Off);
514 assert_eq!(s.palette_mode, PaletteMode::Rgb332);
515 assert!(s.sky.is_some());
516 assert!(s.dither.is_some());
517 }
518
519 #[test]
520 fn psx_preset_enables_snap_and_fog() {
521 let s = RetroStyle::psx();
522 assert_eq!(s.vertex_snap_bits, 6);
523 assert_eq!(s.texture_mapping, TextureMapping::Affine);
524 assert_eq!(s.light_levels, LightLevels::Linear);
525 assert_eq!(s.palette_mode, PaletteMode::Rgb332);
526 assert!(s.fog.is_some());
527 assert!(s.dither.is_some());
528 assert!(s.sky.is_some());
529 }
530
531 #[test]
532 fn test_texture_lod_config() {
533 let lod = TextureLodConfig::new(50.0, 150.0, Rgb565::new(16, 32, 16));
534 assert!(!lod.should_drop_texture(40.0));
535 assert!(!lod.should_drop_texture(100.0));
536 assert!(lod.should_drop_texture(150.0));
537 assert!(lod.should_drop_texture(200.0));
538
539 assert!(!lod.is_in_transition(40.0));
540 assert!(lod.is_in_transition(100.0));
541 assert!(!lod.is_in_transition(150.0));
542
543 assert_eq!(lod.flat_blend_factor(50.0), 0.0);
544 assert_eq!(lod.flat_blend_factor(100.0), 0.5);
545 assert_eq!(lod.flat_blend_factor(150.0), 1.0);
546 }
547
548 #[test]
549 fn test_animated_palette() {
550 let colors = [Rgb565::RED, Rgb565::GREEN, Rgb565::BLUE, Rgb565::WHITE];
551 let mut pal = AnimatedPalette::new(colors).with_cycle_range(1, 3);
552
553 assert_eq!(pal.get_color(0), Rgb565::RED);
554 assert_eq!(pal.get_color(1), Rgb565::GREEN);
555 assert_eq!(pal.get_color(2), Rgb565::BLUE);
556 assert_eq!(pal.get_color(3), Rgb565::WHITE);
557
558 pal.step(1);
559 assert_eq!(pal.get_color(0), Rgb565::RED); assert_eq!(pal.get_color(1), Rgb565::BLUE); assert_eq!(pal.get_color(2), Rgb565::GREEN); assert_eq!(pal.get_color(3), Rgb565::WHITE); }
564
565 #[test]
566 fn test_palette_cycler_forward_and_reverse() {
567 let mut cycler = PaletteCycler::<2>::new();
568 cycler.add_range(2, 5, 10.0, CycleDirection::Forward);
570
571 assert_eq!(cycler.map_index(0), 0);
572 assert_eq!(cycler.map_index(2), 2);
573 assert_eq!(cycler.map_index(3), 3);
574 assert_eq!(cycler.map_index(4), 4);
575 assert_eq!(cycler.map_index(5), 5);
576
577 assert!(cycler.advance(0.1));
579 assert_eq!(cycler.map_index(2), 3);
580 assert_eq!(cycler.map_index(3), 4);
581 assert_eq!(cycler.map_index(4), 2);
582
583 let mut rev_slice = PaletteSlice::new(0, 3, 5.0, CycleDirection::Reverse);
585 assert_eq!(rev_slice.current_step, 0);
586 rev_slice.advance(0.2); assert_eq!(rev_slice.current_step, 2);
588 }
589
590 #[test]
591 fn test_palette_cycler_ping_pong() {
592 let mut slice = PaletteSlice::new(0, 4, 10.0, CycleDirection::PingPong);
593 assert_eq!(slice.current_step, 0);
594 slice.advance(0.1);
595 assert_eq!(slice.current_step, 1);
596 slice.advance(0.1);
597 assert_eq!(slice.current_step, 2);
598 slice.advance(0.1);
599 assert_eq!(slice.current_step, 3);
600 slice.advance(0.1);
601 assert_eq!(slice.current_step, 2); slice.advance(0.1);
603 assert_eq!(slice.current_step, 1);
604 slice.advance(0.1);
605 assert_eq!(slice.current_step, 0); slice.advance(0.1);
607 assert_eq!(slice.current_step, 1);
608 }
609
610 #[test]
611 fn test_palette_cycler_cycle_palette() {
612 let mut cycler = PaletteCycler::<1>::new();
613 cycler.add_range(1, 3, 20.0, CycleDirection::Forward);
614
615 let base = [Rgb565::RED, Rgb565::GREEN, Rgb565::BLUE, Rgb565::WHITE];
616 let mut dest = [Rgb565::BLACK; 4];
617
618 cycler.cycle_palette(&base, &mut dest);
619 assert_eq!(dest[0], Rgb565::RED);
620 assert_eq!(dest[1], Rgb565::GREEN);
621 assert_eq!(dest[2], Rgb565::BLUE);
622 assert_eq!(dest[3], Rgb565::WHITE);
623
624 cycler.advance(0.05); cycler.cycle_palette(&base, &mut dest);
626 assert_eq!(dest[1], Rgb565::BLUE);
627 assert_eq!(dest[2], Rgb565::GREEN);
628 }
629}