use rgb::RGB8;
pub const GAMMA_2_0: [u8; 256] = {
let mut table = [0u8; 256];
let mut i: usize = 0;
while i < 256 {
table[i] = (i * i / 255) as u8;
i += 1;
}
table
};
pub fn apply_brightness_gamma(rgb: RGB8, brightness: u8, gamma: &[u8; 256]) -> RGB8 {
let scale = |v: u8| -> u8 {
let scaled = (v as u16 * brightness as u16 / 255) as u8;
gamma[scaled as usize]
};
RGB8 {
r: scale(rgb.r),
g: scale(rgb.g),
b: scale(rgb.b),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridLayout {
RowMajor,
ColumnMajorBottomUp,
}
impl GridLayout {
pub const fn to_index(self, x: usize, y: usize, width: usize, height: usize) -> usize {
match self {
GridLayout::RowMajor => y * width + x,
GridLayout::ColumnMajorBottomUp => x * height + (height - 1 - y),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GridBuffer<const W: usize, const H: usize, const N: usize> {
pixels: [RGB8; N],
layout: GridLayout,
brightness: u8,
}
impl<const W: usize, const H: usize, const N: usize> GridBuffer<W, H, N> {
pub const fn new(layout: GridLayout) -> Self {
assert!(N == W * H, "N must equal W * H");
Self {
pixels: [RGB8 { r: 0, g: 0, b: 0 }; N],
layout,
brightness: 255,
}
}
pub fn set_brightness(&mut self, level: u8) {
self.brightness = level;
}
pub fn brightness(&self) -> u8 {
self.brightness
}
pub fn fill(&mut self, color: RGB8) {
let c = apply_brightness_gamma(color, self.brightness, &GAMMA_2_0);
self.pixels.fill(c);
}
pub fn set_pixel(&mut self, x: usize, y: usize, color: RGB8) {
if x >= W || y >= H {
return;
}
let idx = self.layout.to_index(x, y, W, H);
self.pixels[idx] = apply_brightness_gamma(color, self.brightness, &GAMMA_2_0);
}
pub fn as_slice(&self) -> &[RGB8] {
&self.pixels
}
pub const fn width(&self) -> usize {
W
}
pub const fn height(&self) -> usize {
H
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gamma_lut_endpoints() {
assert_eq!(GAMMA_2_0[0], 0);
assert_eq!(GAMMA_2_0[255], 255);
}
#[test]
fn gamma_lut_known_midpoints() {
assert_eq!(GAMMA_2_0[128], 64);
assert_eq!(GAMMA_2_0[64], 16);
}
#[test]
fn gamma_lut_monotonic() {
for i in 1..256 {
assert!(
GAMMA_2_0[i] >= GAMMA_2_0[i - 1],
"gamma LUT must be non-decreasing at index {}",
i
);
}
}
#[test]
fn full_brightness_applies_gamma_only() {
let rgb = RGB8::new(255, 128, 64);
let out = apply_brightness_gamma(rgb, 255, &GAMMA_2_0);
assert_eq!(out.r, GAMMA_2_0[255]);
assert_eq!(out.g, GAMMA_2_0[128]);
assert_eq!(out.b, GAMMA_2_0[64]);
}
#[test]
fn zero_brightness_returns_black() {
let rgb = RGB8::new(255, 255, 255);
let out = apply_brightness_gamma(rgb, 0, &GAMMA_2_0);
assert_eq!(out, RGB8::new(0, 0, 0));
}
#[test]
fn identity_lut_halves_at_half_brightness() {
let identity: [u8; 256] = {
let mut lut = [0u8; 256];
let mut i = 0;
while i < 256 {
lut[i] = i as u8;
i += 1;
}
lut
};
let rgb = RGB8::new(200, 100, 50);
let out = apply_brightness_gamma(rgb, 128, &identity);
assert_eq!(out, RGB8::new(100, 50, 25));
}
#[test]
fn row_major_origin() {
assert_eq!(GridLayout::RowMajor.to_index(0, 0, 8, 8), 0);
}
#[test]
fn row_major_end() {
assert_eq!(GridLayout::RowMajor.to_index(7, 7, 8, 8), 63);
}
#[test]
fn row_major_sequential() {
assert_eq!(GridLayout::RowMajor.to_index(1, 0, 8, 8), 1);
assert_eq!(GridLayout::RowMajor.to_index(0, 1, 8, 8), 8);
}
#[test]
fn column_major_bottom_up_bottom_left_is_zero() {
assert_eq!(GridLayout::ColumnMajorBottomUp.to_index(0, 7, 8, 8), 0);
}
#[test]
fn column_major_bottom_up_top_left_is_seven() {
assert_eq!(GridLayout::ColumnMajorBottomUp.to_index(0, 0, 8, 8), 7);
}
#[test]
fn column_major_bottom_up_top_right_is_last() {
assert_eq!(GridLayout::ColumnMajorBottomUp.to_index(7, 0, 8, 8), 63);
}
#[test]
fn column_major_bottom_up_matches_firmware_formula() {
for x in 0..8 {
for y in 0..8 {
let expected = x * 8 + (7 - y);
let actual = GridLayout::ColumnMajorBottomUp.to_index(x, y, 8, 8);
assert_eq!(actual, expected, "mismatch at ({}, {})", x, y);
}
}
}
#[test]
fn new_starts_black() {
let grid: GridBuffer<8, 8, 64> = GridBuffer::new(GridLayout::RowMajor);
assert!(grid.as_slice().iter().all(|p| *p == RGB8::new(0, 0, 0)));
assert_eq!(grid.brightness(), 255);
}
#[test]
fn dimensions_are_compile_time_constants() {
let grid: GridBuffer<8, 8, 64> = GridBuffer::new(GridLayout::RowMajor);
assert_eq!(grid.width(), 8);
assert_eq!(grid.height(), 8);
assert_eq!(grid.as_slice().len(), 64);
}
#[test]
fn brightness_round_trip() {
let mut grid: GridBuffer<8, 8, 64> = GridBuffer::new(GridLayout::RowMajor);
grid.set_brightness(42);
assert_eq!(grid.brightness(), 42);
}
#[test]
fn fill_paints_every_pixel_with_gamma_scaled_colour() {
let mut grid: GridBuffer<8, 8, 64> = GridBuffer::new(GridLayout::RowMajor);
grid.set_brightness(255);
grid.fill(RGB8::new(128, 128, 128));
let expected = RGB8::new(64, 64, 64);
assert!(grid.as_slice().iter().all(|p| *p == expected));
}
#[test]
fn fill_at_zero_brightness_is_black() {
let mut grid: GridBuffer<8, 8, 64> = GridBuffer::new(GridLayout::RowMajor);
grid.set_brightness(0);
grid.fill(RGB8::new(255, 255, 255));
assert!(grid.as_slice().iter().all(|p| *p == RGB8::new(0, 0, 0)));
}
#[test]
fn set_pixel_writes_at_layout_index() {
let mut grid: GridBuffer<8, 8, 64> = GridBuffer::new(GridLayout::ColumnMajorBottomUp);
grid.set_brightness(255);
grid.set_pixel(0, 7, RGB8::new(255, 0, 0));
let expected = RGB8::new(GAMMA_2_0[255], 0, 0);
assert_eq!(grid.as_slice()[0], expected);
assert_eq!(grid.as_slice()[1], RGB8::new(0, 0, 0));
}
#[test]
fn set_pixel_row_major_layout() {
let mut grid: GridBuffer<8, 8, 64> = GridBuffer::new(GridLayout::RowMajor);
grid.set_brightness(255);
grid.set_pixel(3, 2, RGB8::new(0, 255, 0));
let expected = RGB8::new(0, GAMMA_2_0[255], 0);
assert_eq!(grid.as_slice()[19], expected);
}
#[test]
fn set_pixel_out_of_bounds_is_noop() {
let mut grid: GridBuffer<8, 8, 64> = GridBuffer::new(GridLayout::RowMajor);
grid.set_pixel(8, 0, RGB8::new(255, 0, 0));
grid.set_pixel(0, 8, RGB8::new(0, 255, 0));
grid.set_pixel(100, 100, RGB8::new(0, 0, 255));
assert!(grid.as_slice().iter().all(|p| *p == RGB8::new(0, 0, 0)));
}
#[test]
fn non_square_grid() {
let mut grid: GridBuffer<16, 4, 64> = GridBuffer::new(GridLayout::RowMajor);
assert_eq!(grid.width(), 16);
assert_eq!(grid.height(), 4);
grid.set_pixel(15, 3, RGB8::new(255, 0, 0));
let expected = RGB8::new(GAMMA_2_0[255], 0, 0);
assert_eq!(grid.as_slice()[63], expected);
}
}