use crate::draw::{DitherConfig, FogConfig};
use embedded_graphics_core::pixelcolor::Rgb565;
use embedded_graphics_core::pixelcolor::RgbColor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LightLevels {
Linear,
Doom32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StippleMode {
Off,
Checkerboard,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaletteMode {
Off,
Rgb332,
}
impl PaletteMode {
#[inline]
pub fn apply(self, color: Rgb565) -> Rgb565 {
match self {
PaletteMode::Off => color,
PaletteMode::Rgb332 => {
let r3 = ((color.r() as u16 * 7 + 15) / 31) as u8;
let g3 = ((color.g() as u16 * 7 + 31) / 63) as u8;
let b2 = ((color.b() as u16 * 3 + 15) / 31) as u8;
let r = (r3 as u16 * 31 / 7) as u8;
let g = (g3 as u16 * 63 / 7) as u8;
let b = (b2 as u16 * 31 / 3) as u8;
Rgb565::new(r, g, b)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SkyConfig {
pub top_color: Rgb565,
pub bottom_color: Rgb565,
pub stripe_color: Rgb565,
pub stripe_strength: u8,
pub stripe_width: u8,
}
impl SkyConfig {
pub const fn retro_blue() -> Self {
Self {
top_color: Rgb565::new(6, 16, 31),
bottom_color: Rgb565::new(1, 4, 12),
stripe_color: Rgb565::new(18, 30, 31),
stripe_strength: 18,
stripe_width: 16,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScreenTint {
pub color: Rgb565,
pub strength: u8,
}
impl ScreenTint {
#[inline]
pub fn apply(&self, base: Rgb565) -> Rgb565 {
let a = self.strength as u16;
let inv = 255u16.saturating_sub(a);
let r = ((base.r() as u16 * inv + self.color.r() as u16 * a) / 255) as u8;
let g = ((base.g() as u16 * inv + self.color.g() as u16 * a) / 255) as u8;
let b = ((base.b() as u16 * inv + self.color.b() as u16 * a) / 255) as u8;
Rgb565::new(r, g, b)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureMapping {
PerspectiveCorrect,
Affine,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextureLodConfig {
pub near_distance: f32,
pub far_distance: f32,
pub fallback_color: Rgb565,
}
impl TextureLodConfig {
pub const fn new(near_distance: f32, far_distance: f32, fallback_color: Rgb565) -> Self {
Self {
near_distance,
far_distance,
fallback_color,
}
}
#[inline]
pub fn should_drop_texture(&self, z: f32) -> bool {
z >= self.far_distance
}
#[inline]
pub fn is_in_transition(&self, z: f32) -> bool {
z >= self.near_distance && z < self.far_distance
}
#[inline]
pub fn flat_blend_factor(&self, z: f32) -> f32 {
if z <= self.near_distance {
0.0
} else if z >= self.far_distance {
1.0
} else {
(z - self.near_distance) / (self.far_distance - self.near_distance)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnimatedPalette<const N: usize> {
pub colors: [Rgb565; N],
pub cycle_start: usize,
pub cycle_end: usize,
pub offset: usize,
}
impl<const N: usize> AnimatedPalette<N> {
pub const fn new(colors: [Rgb565; N]) -> Self {
Self {
colors,
cycle_start: 0,
cycle_end: N,
offset: 0,
}
}
pub const fn with_cycle_range(mut self, start: usize, end: usize) -> Self {
self.cycle_start = start;
self.cycle_end = if end <= N { end } else { N };
self
}
pub fn step(&mut self, steps: usize) {
let range_len = self.cycle_end.saturating_sub(self.cycle_start);
if range_len > 1 {
self.offset = (self.offset + steps) % range_len;
}
}
#[inline]
pub fn get_color(&self, index: usize) -> Rgb565 {
if index >= N {
return Rgb565::BLACK;
}
if index >= self.cycle_start && index < self.cycle_end {
let range_len = self.cycle_end - self.cycle_start;
let cycled_idx =
self.cycle_start + (index - self.cycle_start + self.offset) % range_len;
self.colors[cycled_idx]
} else {
self.colors[index]
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RetroStyle {
pub fog: Option<FogConfig>,
pub dither: Option<DitherConfig>,
pub vertex_snap_bits: u8,
pub texture_mapping: TextureMapping,
pub light_levels: LightLevels,
pub stipple_mode: StippleMode,
pub screen_tint: Option<ScreenTint>,
pub palette_mode: PaletteMode,
pub sky: Option<SkyConfig>,
}
impl Default for RetroStyle {
fn default() -> Self {
Self::modern()
}
}
impl RetroStyle {
pub const fn modern() -> Self {
Self {
fog: None,
dither: None,
vertex_snap_bits: 0,
texture_mapping: TextureMapping::PerspectiveCorrect,
light_levels: LightLevels::Linear,
stipple_mode: StippleMode::Off,
screen_tint: None,
palette_mode: PaletteMode::Off,
sky: None,
}
}
pub const fn doom_walkable() -> Self {
Self {
fog: None,
dither: Some(DitherConfig { intensity: 20 }),
vertex_snap_bits: 0,
texture_mapping: TextureMapping::Affine,
light_levels: LightLevels::Doom32,
stipple_mode: StippleMode::Off,
screen_tint: None,
palette_mode: PaletteMode::Rgb332,
sky: Some(SkyConfig::retro_blue()),
}
}
pub fn psx() -> Self {
Self {
fog: Some(FogConfig::new(Rgb565::new(2, 2, 4), 6.0, 20.0)),
dither: Some(DitherConfig::new(72)),
vertex_snap_bits: 6,
texture_mapping: TextureMapping::Affine,
light_levels: LightLevels::Linear,
stipple_mode: StippleMode::Off,
screen_tint: None,
palette_mode: PaletteMode::Rgb332,
sky: Some(SkyConfig::retro_blue()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn palette_off_is_identity() {
let c = Rgb565::new(17, 45, 23);
assert_eq!(PaletteMode::Off.apply(c), c);
}
#[test]
fn palette_rgb332_quantizes_channels() {
let c = Rgb565::new(17, 45, 23);
let q = PaletteMode::Rgb332.apply(c);
assert_eq!(q, Rgb565::new(17, 45, 20));
}
#[test]
fn screen_tint_strength_extremes() {
let base = Rgb565::new(7, 20, 5);
let tint = Rgb565::new(31, 0, 31);
let off = ScreenTint {
color: tint,
strength: 0,
};
assert_eq!(off.apply(base), base);
let full = ScreenTint {
color: tint,
strength: 255,
};
assert_eq!(full.apply(base), tint);
}
#[test]
fn doom_walkable_preset_matches_expected_profile() {
let s = RetroStyle::doom_walkable();
assert!(s.fog.is_none());
assert_eq!(s.vertex_snap_bits, 0);
assert_eq!(s.texture_mapping, TextureMapping::Affine);
assert_eq!(s.light_levels, LightLevels::Doom32);
assert_eq!(s.stipple_mode, StippleMode::Off);
assert_eq!(s.palette_mode, PaletteMode::Rgb332);
assert!(s.sky.is_some());
assert!(s.dither.is_some());
}
#[test]
fn psx_preset_enables_snap_and_fog() {
let s = RetroStyle::psx();
assert_eq!(s.vertex_snap_bits, 6);
assert_eq!(s.texture_mapping, TextureMapping::Affine);
assert_eq!(s.light_levels, LightLevels::Linear);
assert_eq!(s.palette_mode, PaletteMode::Rgb332);
assert!(s.fog.is_some());
assert!(s.dither.is_some());
assert!(s.sky.is_some());
}
#[test]
fn test_texture_lod_config() {
let lod = TextureLodConfig::new(50.0, 150.0, Rgb565::new(16, 32, 16));
assert!(!lod.should_drop_texture(40.0));
assert!(!lod.should_drop_texture(100.0));
assert!(lod.should_drop_texture(150.0));
assert!(lod.should_drop_texture(200.0));
assert!(!lod.is_in_transition(40.0));
assert!(lod.is_in_transition(100.0));
assert!(!lod.is_in_transition(150.0));
assert_eq!(lod.flat_blend_factor(50.0), 0.0);
assert_eq!(lod.flat_blend_factor(100.0), 0.5);
assert_eq!(lod.flat_blend_factor(150.0), 1.0);
}
#[test]
fn test_animated_palette() {
let colors = [Rgb565::RED, Rgb565::GREEN, Rgb565::BLUE, Rgb565::WHITE];
let mut pal = AnimatedPalette::new(colors).with_cycle_range(1, 3);
assert_eq!(pal.get_color(0), Rgb565::RED);
assert_eq!(pal.get_color(1), Rgb565::GREEN);
assert_eq!(pal.get_color(2), Rgb565::BLUE);
assert_eq!(pal.get_color(3), Rgb565::WHITE);
pal.step(1);
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); }
}