117 lines
4.2 KiB
Rust
117 lines
4.2 KiB
Rust
//! Full-color menu illustration; indexed gameplay and movie palettes stay separate.
|
|
use crate::{assets::TRANSPARENT, render::Canvas};
|
|
use image::{DynamicImage, RgbaImage, imageops::FilterType};
|
|
use std::collections::BTreeMap;
|
|
|
|
pub const MENU_PANEL: u16 = 257;
|
|
pub const MENU_CARD: u16 = 258;
|
|
pub const MENU_FOCUS: u16 = 259;
|
|
pub const MENU_TEXT: u16 = 260;
|
|
pub const MENU_MUTED: u16 = 261;
|
|
pub const MENU_ACCENT: u16 = 262;
|
|
pub const MENU_RULE: u16 = 263;
|
|
|
|
pub struct MenuArt {
|
|
original: RgbaImage,
|
|
resized: BTreeMap<(usize, usize, usize), RgbaImage>,
|
|
}
|
|
impl MenuArt {
|
|
pub fn bundled() -> Self {
|
|
let original = image::load_from_memory(include_bytes!("../art/menu-background-v1.png"))
|
|
.expect("bundled menu artwork is a checked PNG")
|
|
.into_rgba8();
|
|
Self {
|
|
original,
|
|
resized: BTreeMap::new(),
|
|
}
|
|
}
|
|
pub fn compose(
|
|
&mut self,
|
|
canvas: &Canvas,
|
|
palette: &[[u8; 3]; 256],
|
|
scanlines: bool,
|
|
) -> Vec<u8> {
|
|
let background = self
|
|
.resized
|
|
.entry((canvas.scale, canvas.image.width, canvas.image.height))
|
|
.or_insert_with(|| {
|
|
DynamicImage::ImageRgba8(self.original.clone())
|
|
.resize_to_fill(
|
|
canvas.image.width as u32,
|
|
canvas.image.height as u32,
|
|
FilterType::Lanczos3,
|
|
)
|
|
.into_rgba8()
|
|
});
|
|
let mut rgba = background.as_raw().clone();
|
|
for (i, index) in canvas.image.pixels.iter().enumerate() {
|
|
let color = if matches!(*index, MENU_PANEL | MENU_CARD | MENU_FOCUS) {
|
|
let (tint, opacity) = match *index {
|
|
MENU_CARD => ([10u16, 17, 23], 72),
|
|
MENU_FOCUS => ([49u16, 38, 27], 82),
|
|
_ => ([12u16, 18, 29], 85),
|
|
};
|
|
[0, 1, 2].map(|channel| {
|
|
((tint[channel] * opacity + rgba[i * 4 + channel] as u16 * (100 - opacity))
|
|
/ 100) as u8
|
|
})
|
|
} else if *index == MENU_TEXT {
|
|
[221, 225, 224]
|
|
} else if *index == MENU_MUTED {
|
|
[147, 163, 171]
|
|
} else if *index == MENU_ACCENT {
|
|
[237, 180, 105]
|
|
} else if *index == MENU_RULE {
|
|
[65, 79, 85]
|
|
} else if *index == TRANSPARENT {
|
|
[rgba[i * 4], rgba[i * 4 + 1], rgba[i * 4 + 2]]
|
|
} else {
|
|
palette[(*index).min(255) as usize]
|
|
};
|
|
let dark =
|
|
scanlines && (i / canvas.image.width) % (canvas.scale * 3) == canvas.scale * 2;
|
|
for j in 0..3 {
|
|
rgba[i * 4 + j] = if dark { color[j] / 2 } else { color[j] };
|
|
}
|
|
rgba[i * 4 + 3] = 255;
|
|
}
|
|
rgba
|
|
}
|
|
pub fn sidebars(&self) -> RgbaImage {
|
|
let mut image = DynamicImage::ImageRgba8(self.original.clone())
|
|
.resize_to_fill(800, 450, FilterType::Lanczos3)
|
|
.into_rgba8();
|
|
for pixel in image.pixels_mut() {
|
|
for channel in &mut pixel.0[..3] {
|
|
*channel = (*channel as u16 * 23 / 100) as u8;
|
|
}
|
|
pixel[3] = 255;
|
|
}
|
|
image
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
#[test]
|
|
fn menu_overlay_preserves_full_color_background_and_opaque_zero() {
|
|
let mut art = MenuArt::bundled();
|
|
let mut canvas = Canvas::new(2);
|
|
canvas.clear(TRANSPARENT);
|
|
let mut palette = [[0; 3]; 256];
|
|
palette[7] = [240, 210, 160];
|
|
let background = art.compose(&canvas, &palette, false);
|
|
assert!(background.chunks_exact(4).any(|pixel| pixel[0] != pixel[1]));
|
|
canvas.rect(10, 10, 3, 3, 7);
|
|
canvas.rect(20, 20, 3, 3, 0);
|
|
let overlay = art.compose(&canvas, &palette, false);
|
|
let amber = (20 * canvas.image.width + 20) * 4;
|
|
assert_eq!(&overlay[amber..amber + 4], &[240, 210, 160, 255]);
|
|
let black = (40 * canvas.image.width + 40) * 4;
|
|
assert_eq!(&overlay[black..black + 4], &[0, 0, 0, 255]);
|
|
assert_eq!(&background[..4], &overlay[..4]);
|
|
assert_eq!(art.resized.len(), 1);
|
|
}
|
|
}
|