#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NesPalette {
#[default]
Default,
NesDev,
Smooth,
Classic,
CompositeDirect,
}
impl NesPalette {
pub const ALL: [NesPalette; 5] = [
NesPalette::Default,
NesPalette::NesDev,
NesPalette::Smooth,
NesPalette::Classic,
NesPalette::CompositeDirect,
];
pub fn table(self) -> &'static [(u8, u8, u8); 64] {
match self {
NesPalette::Default => &PALETTE_DEFAULT,
NesPalette::NesDev => &PALETTE_NESDEV,
NesPalette::Smooth => &PALETTE_SMOOTH,
NesPalette::Classic => &PALETTE_CLASSIC,
NesPalette::CompositeDirect => &PALETTE_COMPOSITE_DIRECT,
}
}
pub fn display_name(self) -> &'static str {
match self {
NesPalette::Default => "Default",
NesPalette::NesDev => "NesDev",
NesPalette::Smooth => "Smooth",
NesPalette::Classic => "Classic",
NesPalette::CompositeDirect => "Composite Direct",
}
}
pub fn config_id(self) -> &'static str {
match self {
NesPalette::Default => "default",
NesPalette::NesDev => "nesdev",
NesPalette::Smooth => "smooth",
NesPalette::Classic => "classic",
NesPalette::CompositeDirect => "composite-direct",
}
}
pub fn from_config_id(id: &str) -> Option<NesPalette> {
let id = id.trim().to_ascii_lowercase();
NesPalette::ALL.into_iter().find(|p| p.config_id() == id)
}
pub fn next(self) -> NesPalette {
let idx = NesPalette::ALL.iter().position(|&p| p == self).unwrap_or(0);
NesPalette::ALL[(idx + 1) % NesPalette::ALL.len()]
}
}
#[rustfmt::skip]
pub const PALETTE_DEFAULT: [(u8, u8, u8); 64] = [
(0x54, 0x54, 0x54), (0x00, 0x1E, 0x74), (0x08, 0x10, 0x90), (0x30, 0x00, 0x88),
(0x44, 0x00, 0x64), (0x5C, 0x00, 0x30), (0x54, 0x04, 0x00), (0x3C, 0x18, 0x00),
(0x20, 0x2A, 0x00), (0x08, 0x3A, 0x00), (0x00, 0x40, 0x00), (0x00, 0x3C, 0x00),
(0x00, 0x32, 0x3C), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0x98, 0x96, 0x98), (0x08, 0x4C, 0xC4), (0x30, 0x32, 0xEC), (0x5C, 0x1E, 0xE4),
(0x88, 0x14, 0xB0), (0xA0, 0x14, 0x64), (0x98, 0x22, 0x20), (0x78, 0x3C, 0x00),
(0x54, 0x5A, 0x00), (0x28, 0x72, 0x00), (0x08, 0x7C, 0x00), (0x00, 0x76, 0x28),
(0x00, 0x66, 0x78), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xEC, 0xEE, 0xEC), (0x4C, 0x9A, 0xEC), (0x78, 0x7C, 0xEC), (0xB0, 0x62, 0xEC),
(0xE4, 0x54, 0xEC), (0xEC, 0x58, 0xB4), (0xEC, 0x6A, 0x64), (0xD4, 0x88, 0x20),
(0xA0, 0xAA, 0x00), (0x74, 0xC4, 0x00), (0x4C, 0xD0, 0x20), (0x38, 0xCC, 0x6C),
(0x38, 0xB4, 0xCC), (0x3C, 0x3C, 0x3C), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xEC, 0xEE, 0xEC), (0xA8, 0xCC, 0xEC), (0xBC, 0xBC, 0xEC), (0xD4, 0xB2, 0xEC),
(0xEC, 0xAE, 0xEC), (0xEC, 0xAE, 0xD4), (0xEC, 0xB4, 0xB0), (0xE4, 0xC4, 0x90),
(0xCC, 0xD2, 0x78), (0xB4, 0xDE, 0x78), (0xA8, 0xE2, 0x90), (0x98, 0xE2, 0xB4),
(0xA0, 0xD6, 0xE4), (0xA0, 0xA2, 0xA0), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
];
#[rustfmt::skip]
pub const PALETTE_NESDEV: [(u8, u8, u8); 64] = [
(0x62, 0x62, 0x62), (0x00, 0x1C, 0x95), (0x19, 0x04, 0xAC), (0x42, 0x00, 0x9D),
(0x61, 0x00, 0x6B), (0x6E, 0x00, 0x25), (0x65, 0x05, 0x00), (0x49, 0x1E, 0x00),
(0x22, 0x37, 0x00), (0x00, 0x49, 0x00), (0x00, 0x4F, 0x00), (0x00, 0x48, 0x16),
(0x00, 0x35, 0x5E), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xAB, 0xAB, 0xAB), (0x0C, 0x4E, 0xDB), (0x3D, 0x2E, 0xFF), (0x71, 0x15, 0xF3),
(0x9B, 0x0B, 0xB9), (0xB0, 0x12, 0x62), (0xA9, 0x27, 0x04), (0x89, 0x46, 0x00),
(0x57, 0x66, 0x00), (0x23, 0x7F, 0x00), (0x00, 0x89, 0x00), (0x00, 0x83, 0x32),
(0x00, 0x6D, 0x90), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xFF, 0xFF, 0xFF), (0x57, 0xA5, 0xFF), (0x82, 0x87, 0xFF), (0xB4, 0x6D, 0xFF),
(0xDF, 0x60, 0xFF), (0xF8, 0x63, 0xC6), (0xF8, 0x74, 0x6D), (0xDE, 0x90, 0x20),
(0xB3, 0xAE, 0x00), (0x81, 0xC8, 0x00), (0x56, 0xD5, 0x22), (0x3D, 0xD3, 0x6F),
(0x3E, 0xC1, 0xC8), (0x4E, 0x4E, 0x4E), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xFF, 0xFF, 0xFF), (0xBE, 0xE0, 0xFF), (0xCD, 0xD4, 0xFF), (0xE0, 0xCA, 0xFF),
(0xF1, 0xC4, 0xFF), (0xFC, 0xC4, 0xEF), (0xFD, 0xCA, 0xCE), (0xF5, 0xD4, 0xAF),
(0xE6, 0xDF, 0x9C), (0xD3, 0xE9, 0x9A), (0xC2, 0xEF, 0xA8), (0xB7, 0xEF, 0xC4),
(0xB6, 0xEA, 0xE5), (0xB8, 0xB8, 0xB8), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
];
#[rustfmt::skip]
pub const PALETTE_SMOOTH: [(u8, u8, u8); 64] = [
(0x6A, 0x6A, 0x6A), (0x00, 0x14, 0x8F), (0x1E, 0x02, 0x9B), (0x3F, 0x00, 0x8A),
(0x60, 0x00, 0x60), (0x66, 0x00, 0x17), (0x57, 0x0D, 0x00), (0x3C, 0x1F, 0x00),
(0x1B, 0x33, 0x00), (0x00, 0x42, 0x00), (0x00, 0x45, 0x00), (0x00, 0x3C, 0x1F),
(0x00, 0x31, 0x5C), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xB9, 0xB9, 0xB9), (0x0F, 0x4B, 0xD4), (0x41, 0x2D, 0xEB), (0x6C, 0x1D, 0xD9),
(0x9C, 0x17, 0xAB), (0xA7, 0x1A, 0x4D), (0x99, 0x32, 0x00), (0x7C, 0x4A, 0x00),
(0x54, 0x64, 0x00), (0x1A, 0x78, 0x00), (0x00, 0x7F, 0x00), (0x00, 0x76, 0x3E),
(0x00, 0x67, 0x8F), (0x01, 0x01, 0x01), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xFF, 0xFF, 0xFF), (0x68, 0xA6, 0xFF), (0x8C, 0x9C, 0xFF), (0xB5, 0x86, 0xFF),
(0xD9, 0x75, 0xFD), (0xE3, 0x77, 0xB9), (0xE5, 0x8D, 0x68), (0xD4, 0x9D, 0x29),
(0xB3, 0xAF, 0x0C), (0x7B, 0xC2, 0x11), (0x55, 0xCA, 0x47), (0x46, 0xCB, 0x81),
(0x47, 0xC1, 0xC5), (0x4A, 0x4A, 0x4A), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xFF, 0xFF, 0xFF), (0xCC, 0xEA, 0xFF), (0xDD, 0xDE, 0xFF), (0xEC, 0xDA, 0xFF),
(0xF8, 0xD7, 0xFE), (0xFC, 0xD6, 0xF5), (0xFD, 0xDB, 0xCF), (0xF9, 0xE7, 0xB5),
(0xF1, 0xF0, 0xAA), (0xDA, 0xFA, 0xA9), (0xC9, 0xFF, 0xBC), (0xC3, 0xFB, 0xD7),
(0xC4, 0xF6, 0xF6), (0xBE, 0xBE, 0xBE), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
];
#[rustfmt::skip]
pub const PALETTE_CLASSIC: [(u8, u8, u8); 64] = [
(0x61, 0x61, 0x61), (0x00, 0x00, 0x88), (0x1F, 0x0D, 0x99), (0x37, 0x13, 0x79),
(0x56, 0x12, 0x60), (0x5D, 0x00, 0x10), (0x52, 0x0E, 0x00), (0x3A, 0x23, 0x08),
(0x21, 0x35, 0x0C), (0x0D, 0x41, 0x0E), (0x17, 0x44, 0x17), (0x00, 0x3A, 0x1F),
(0x00, 0x2F, 0x57), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xAA, 0xAA, 0xAA), (0x0D, 0x4D, 0xC4), (0x4B, 0x24, 0xDE), (0x69, 0x12, 0xCF),
(0x90, 0x14, 0xAD), (0x9D, 0x1C, 0x48), (0x92, 0x34, 0x04), (0x73, 0x50, 0x05),
(0x5D, 0x69, 0x13), (0x16, 0x7A, 0x11), (0x13, 0x80, 0x08), (0x12, 0x76, 0x49),
(0x1C, 0x66, 0x91), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xFC, 0xFC, 0xFC), (0x63, 0x9A, 0xFC), (0x8A, 0x7E, 0xFC), (0xB0, 0x6A, 0xFC),
(0xDD, 0x6D, 0xF2), (0xE7, 0x71, 0xAB), (0xE3, 0x86, 0x58), (0xCC, 0x9E, 0x22),
(0xA8, 0xB1, 0x00), (0x72, 0xC1, 0x00), (0x5A, 0xCD, 0x4E), (0x34, 0xC2, 0x8E),
(0x4F, 0xBE, 0xCE), (0x42, 0x42, 0x42), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xFC, 0xFC, 0xFC), (0xBE, 0xD4, 0xFC), (0xCA, 0xCA, 0xFC), (0xD9, 0xC4, 0xFC),
(0xEC, 0xC1, 0xFC), (0xFA, 0xC3, 0xE7), (0xF7, 0xCE, 0xC3), (0xE2, 0xCD, 0xA7),
(0xDA, 0xDB, 0x9C), (0xC8, 0xE3, 0x9E), (0xBF, 0xE5, 0xB8), (0xB2, 0xEB, 0xC8),
(0xB7, 0xE5, 0xEB), (0xAC, 0xAC, 0xAC), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
];
#[rustfmt::skip]
pub const PALETTE_COMPOSITE_DIRECT: [(u8, u8, u8); 64] = [
(0x65, 0x65, 0x65), (0x00, 0x12, 0x7D), (0x18, 0x00, 0x8E), (0x36, 0x00, 0x82),
(0x56, 0x00, 0x5D), (0x5A, 0x00, 0x18), (0x4F, 0x05, 0x00), (0x38, 0x19, 0x00),
(0x1D, 0x31, 0x00), (0x00, 0x3D, 0x00), (0x00, 0x41, 0x00), (0x00, 0x3B, 0x17),
(0x00, 0x2E, 0x55), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xAF, 0xAF, 0xAF), (0x19, 0x4E, 0xC8), (0x47, 0x2F, 0xE3), (0x6B, 0x1F, 0xD7),
(0x93, 0x1B, 0xAE), (0x9E, 0x1A, 0x5E), (0x99, 0x32, 0x00), (0x7B, 0x4B, 0x00),
(0x5B, 0x67, 0x00), (0x26, 0x7A, 0x00), (0x00, 0x82, 0x00), (0x00, 0x7A, 0x3E),
(0x00, 0x6E, 0x8A), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xFF, 0xFF, 0xFF), (0x64, 0xA9, 0xFF), (0x8E, 0x89, 0xFF), (0xB6, 0x76, 0xFF),
(0xE0, 0x6F, 0xFF), (0xEF, 0x6C, 0xC4), (0xF0, 0x80, 0x6A), (0xD8, 0x98, 0x2C),
(0xB9, 0xB4, 0x0A), (0x83, 0xCB, 0x0C), (0x5B, 0xD6, 0x3F), (0x4A, 0xD1, 0x7E),
(0x4D, 0xC7, 0xCB), (0x4C, 0x4C, 0x4C), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
(0xFF, 0xFF, 0xFF), (0xC7, 0xE5, 0xFF), (0xD9, 0xD9, 0xFF), (0xE9, 0xD1, 0xFF),
(0xF9, 0xCE, 0xFF), (0xFF, 0xCC, 0xF1), (0xFF, 0xD4, 0xCB), (0xF8, 0xDF, 0xB1),
(0xED, 0xEA, 0xA4), (0xD6, 0xF4, 0xA4), (0xC5, 0xF8, 0xB8), (0xBE, 0xF6, 0xD3),
(0xBF, 0xF1, 0xF1), (0xB9, 0xB9, 0xB9), (0x00, 0x00, 0x00), (0x00, 0x00, 0x00),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_the_default_variant() {
assert_eq!(NesPalette::default(), NesPalette::Default);
}
#[test]
fn all_contains_every_preset_once() {
assert_eq!(NesPalette::ALL.len(), 5);
for p in NesPalette::ALL {
assert_eq!(NesPalette::ALL.iter().filter(|&&q| q == p).count(), 1);
}
}
#[test]
fn every_table_has_64_entries() {
for p in NesPalette::ALL {
assert_eq!(p.table().len(), 64, "{} has wrong length", p.display_name());
}
}
#[test]
fn config_ids_round_trip_case_insensitively() {
for p in NesPalette::ALL {
let id = p.config_id();
assert_eq!(NesPalette::from_config_id(id), Some(p));
assert_eq!(NesPalette::from_config_id(&id.to_uppercase()), Some(p));
}
}
#[test]
fn from_config_id_trims_whitespace() {
assert_eq!(
NesPalette::from_config_id(" smooth "),
Some(NesPalette::Smooth)
);
}
#[test]
fn from_config_id_rejects_unknown() {
assert_eq!(NesPalette::from_config_id("invalid_name"), None);
assert_eq!(NesPalette::from_config_id(""), None);
}
#[test]
fn next_cycles_through_all_and_wraps() {
assert_eq!(NesPalette::Default.next(), NesPalette::NesDev);
assert_eq!(NesPalette::NesDev.next(), NesPalette::Smooth);
assert_eq!(NesPalette::Smooth.next(), NesPalette::Classic);
assert_eq!(NesPalette::Classic.next(), NesPalette::CompositeDirect);
assert_eq!(NesPalette::CompositeDirect.next(), NesPalette::Default);
}
#[test]
fn presets_render_visibly_different_colors() {
let grays: Vec<(u8, u8, u8)> = NesPalette::ALL.iter().map(|p| p.table()[0]).collect();
for i in 0..grays.len() {
for j in (i + 1)..grays.len() {
assert_ne!(grays[i], grays[j], "presets {i} and {j} share color $00");
}
}
}
#[test]
fn default_table_matches_original_anchor_colors() {
assert_eq!(PALETTE_DEFAULT[0x00], (0x54, 0x54, 0x54));
assert_eq!(PALETTE_DEFAULT[0x21], (0x4C, 0x9A, 0xEC));
assert_eq!(PALETTE_DEFAULT[0x30], (0xEC, 0xEE, 0xEC));
}
#[test]
fn sourced_tables_match_known_anchor_colors() {
assert_eq!(PALETTE_NESDEV[0x00], (0x62, 0x62, 0x62));
assert_eq!(PALETTE_SMOOTH[0x00], (0x6A, 0x6A, 0x6A));
assert_eq!(PALETTE_CLASSIC[0x00], (0x61, 0x61, 0x61));
assert_eq!(PALETTE_COMPOSITE_DIRECT[0x00], (0x65, 0x65, 0x65));
}
}