use crate::buffer::OptimizedBuffer;
use crate::cell::Cell;
use crate::color::Rgba;
use crate::error::Error;
use crate::style::Style;
const QUADRANT_CHARS: [char; 16] = [
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
];
const GRAYSCALE_ASCII: &[char] = &[' ', '.', ':', '-', '=', '+', '*', '#', '%', '@'];
const GRAYSCALE_UNICODE: &[char] = &[' ', '░', '▒', '▓', '█'];
#[derive(Clone, Debug)]
pub struct PixelBuffer {
pub width: u32,
pub height: u32,
pub pixels: Vec<Rgba>,
}
impl PixelBuffer {
#[must_use]
pub fn new(width: u32, height: u32) -> Self {
let size = (width as usize).saturating_mul(height as usize);
Self {
width,
height,
pixels: vec![Rgba::TRANSPARENT; size],
}
}
#[inline]
fn pixel_index(&self, x: u32, y: u32) -> Option<usize> {
if x >= self.width || y >= self.height {
return None;
}
let row_offset = (y as usize).checked_mul(self.width as usize)?;
let idx = row_offset.checked_add(x as usize)?;
if idx < self.pixels.len() {
Some(idx)
} else {
None
}
}
#[must_use]
pub fn from_pixels(width: u32, height: u32, pixels: Vec<Rgba>) -> Self {
Self::try_from_pixels(width, height, pixels).expect("invalid pixel buffer dimensions")
}
pub fn try_from_pixels(width: u32, height: u32, pixels: Vec<Rgba>) -> Result<Self, Error> {
let expected_size = (width as usize)
.checked_mul(height as usize)
.ok_or(Error::DimensionOverflow { width, height })?;
if pixels.len() != expected_size {
return Err(Error::SizeMismatch {
expected: expected_size,
actual: pixels.len(),
});
}
Ok(Self {
width,
height,
pixels,
})
}
#[must_use]
pub fn get(&self, x: u32, y: u32) -> Option<Rgba> {
self.pixel_index(x, y).map(|idx| self.pixels[idx])
}
pub fn set(&mut self, x: u32, y: u32, color: Rgba) {
if let Some(idx) = self.pixel_index(x, y) {
self.pixels[idx] = color;
}
}
pub fn fill(&mut self, color: Rgba) {
self.pixels.fill(color);
}
}
#[derive(Clone, Debug)]
pub struct GrayscaleBuffer {
pub width: u32,
pub height: u32,
pub values: Vec<f32>,
}
impl GrayscaleBuffer {
#[must_use]
pub fn new(width: u32, height: u32) -> Self {
let size = (width as usize).saturating_mul(height as usize);
Self {
width,
height,
values: vec![0.0; size],
}
}
#[inline]
fn pixel_index(&self, x: u32, y: u32) -> Option<usize> {
if x >= self.width || y >= self.height {
return None;
}
let row_offset = (y as usize).checked_mul(self.width as usize)?;
let idx = row_offset.checked_add(x as usize)?;
if idx < self.values.len() {
Some(idx)
} else {
None
}
}
#[must_use]
pub fn get(&self, x: u32, y: u32) -> Option<f32> {
self.pixel_index(x, y).map(|idx| self.values[idx])
}
pub fn set(&mut self, x: u32, y: u32, value: f32) {
if let Some(idx) = self.pixel_index(x, y) {
self.values[idx] = value.clamp(0.0, 1.0);
}
}
}
impl OptimizedBuffer {
#[allow(clippy::similar_names)]
pub fn draw_supersample_buffer(&mut self, x: u32, y: u32, src: &PixelBuffer, threshold: f32) {
let cells_w = src.width / 2;
let cells_h = src.height / 2;
for cy in 0..cells_h {
for cx in 0..cells_w {
let px = cx * 2;
let py = cy * 2;
let tl = src.get(px, py).unwrap_or(Rgba::TRANSPARENT);
let tr = src.get(px + 1, py).unwrap_or(Rgba::TRANSPARENT);
let bl = src.get(px, py + 1).unwrap_or(Rgba::TRANSPARENT);
let br = src.get(px + 1, py + 1).unwrap_or(Rgba::TRANSPARENT);
let tl_bright = tl.luminance();
let tr_bright = tr.luminance();
let bl_bright = bl.luminance();
let br_bright = br.luminance();
let mut mask = 0u8;
if tl_bright >= threshold {
mask |= 0b0001;
}
if tr_bright >= threshold {
mask |= 0b0010;
}
if bl_bright >= threshold {
mask |= 0b0100;
}
if br_bright >= threshold {
mask |= 0b1000;
}
let lit_mask = [
tl_bright >= threshold,
tr_bright >= threshold,
bl_bright >= threshold,
br_bright >= threshold,
];
let (fg, bg) = average_colors(&[tl, tr, bl, br], &lit_mask);
let ch = QUADRANT_CHARS[mask as usize];
let style = Style::builder().fg(fg).bg(bg).build();
let cell = Cell::new(ch, style);
self.set(x + cx, y + cy, cell);
}
}
}
pub fn draw_grayscale_buffer(
&mut self,
x: u32,
y: u32,
src: &GrayscaleBuffer,
fg: Rgba,
bg: Rgba,
) {
self.draw_grayscale_buffer_with_chars(x, y, src, fg, bg, GRAYSCALE_ASCII);
}
pub fn draw_grayscale_buffer_unicode(
&mut self,
x: u32,
y: u32,
src: &GrayscaleBuffer,
fg: Rgba,
bg: Rgba,
) {
self.draw_grayscale_buffer_with_chars(x, y, src, fg, bg, GRAYSCALE_UNICODE);
}
fn draw_grayscale_buffer_with_chars(
&mut self,
x: u32,
y: u32,
src: &GrayscaleBuffer,
fg: Rgba,
bg: Rgba,
chars: &[char],
) {
let num_chars = chars.len();
let style = Style::builder().fg(fg).bg(bg).build();
for py in 0..src.height {
for px in 0..src.width {
let intensity = src.get(px, py).unwrap_or(0.0);
let idx =
((intensity * (num_chars - 1) as f32).round() as usize).min(num_chars - 1);
let ch = chars[idx];
let cell = Cell::new(ch, style);
self.set(x + px, y + py, cell);
}
}
}
pub fn draw_grayscale_buffer_supersampled(
&mut self,
x: u32,
y: u32,
src: &GrayscaleBuffer,
fg: Rgba,
bg: Rgba,
) {
let cells_w = src.width / 2;
let cells_h = src.height / 2;
let num_chars = GRAYSCALE_ASCII.len();
let style = Style::builder().fg(fg).bg(bg).build();
for cy in 0..cells_h {
for cx in 0..cells_w {
let px = cx * 2;
let py = cy * 2;
let tl = src.get(px, py).unwrap_or(0.0);
let tr = src.get(px + 1, py).unwrap_or(0.0);
let bl = src.get(px, py + 1).unwrap_or(0.0);
let br = src.get(px + 1, py + 1).unwrap_or(0.0);
let avg = (tl + tr + bl + br) / 4.0;
let idx = ((avg * (num_chars - 1) as f32).round() as usize).min(num_chars - 1);
let ch = GRAYSCALE_ASCII[idx];
let cell = Cell::new(ch, style);
self.set(x + cx, y + cy, cell);
}
}
}
pub fn draw_packed_buffer(&mut self, x: u32, y: u32, width: u32, height: u32, cells: &[Cell]) {
if cells.len() < (width as usize * height as usize) {
return;
}
for py in 0..height {
for px in 0..width {
let idx = (py * width + px) as usize;
self.set(x + px, y + py, cells[idx]);
}
}
}
}
#[inline]
fn srgb_to_linear(value: f32) -> f32 {
if value <= 0.04045 {
value / 12.92
} else {
((value + 0.055) / 1.055).powf(2.4)
}
}
#[inline]
fn linear_to_srgb(value: f32) -> f32 {
if value <= 0.003_130_8 {
value * 12.92
} else {
1.055 * value.powf(1.0 / 2.4) - 0.055
}
}
fn average_colors(colors: &[Rgba], mask: &[bool]) -> (Rgba, Rgba) {
let mut fg_r = 0.0f32;
let mut fg_g = 0.0f32;
let mut fg_b = 0.0f32;
let mut fg_count = 0u32;
let mut bg_r = 0.0f32;
let mut bg_g = 0.0f32;
let mut bg_b = 0.0f32;
let mut bg_count = 0u32;
for (i, &color) in colors.iter().enumerate() {
let lin_r = srgb_to_linear(color.r);
let lin_g = srgb_to_linear(color.g);
let lin_b = srgb_to_linear(color.b);
if mask[i] {
fg_r += lin_r;
fg_g += lin_g;
fg_b += lin_b;
fg_count += 1;
} else {
bg_r += lin_r;
bg_g += lin_g;
bg_b += lin_b;
bg_count += 1;
}
}
let fg = if fg_count > 0 {
Rgba::rgb(
linear_to_srgb(fg_r / fg_count as f32),
linear_to_srgb(fg_g / fg_count as f32),
linear_to_srgb(fg_b / fg_count as f32),
)
} else {
Rgba::WHITE
};
let bg = if bg_count > 0 {
Rgba::rgb(
linear_to_srgb(bg_r / bg_count as f32),
linear_to_srgb(bg_g / bg_count as f32),
linear_to_srgb(bg_b / bg_count as f32),
)
} else {
Rgba::BLACK
};
(fg, bg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pixel_buffer_creation() {
let buf = PixelBuffer::new(10, 10);
assert_eq!(buf.width, 10);
assert_eq!(buf.height, 10);
assert_eq!(buf.pixels.len(), 100);
}
#[test]
fn test_pixel_buffer_get_set() {
let mut buf = PixelBuffer::new(10, 10);
buf.set(5, 5, Rgba::RED);
assert_eq!(buf.get(5, 5), Some(Rgba::RED));
}
#[test]
fn test_grayscale_buffer_creation() {
let buf = GrayscaleBuffer::new(10, 10);
assert_eq!(buf.width, 10);
assert_eq!(buf.height, 10);
assert_eq!(buf.values.len(), 100);
}
#[test]
fn test_quadrant_chars() {
assert_eq!(QUADRANT_CHARS[0b0000], ' ');
assert_eq!(QUADRANT_CHARS[0b1111], '█');
assert_eq!(QUADRANT_CHARS[0b0011], '▀'); assert_eq!(QUADRANT_CHARS[0b1100], '▄'); }
#[test]
fn test_draw_supersample_buffer() {
let mut dest = OptimizedBuffer::new(10, 10);
let mut src = PixelBuffer::new(4, 4);
src.set(0, 0, Rgba::WHITE);
src.set(1, 0, Rgba::WHITE);
src.set(0, 1, Rgba::WHITE);
src.set(1, 1, Rgba::WHITE);
dest.draw_supersample_buffer(0, 0, &src, 0.5);
let cell = dest.get(0, 0).unwrap();
assert_eq!(cell.content, crate::cell::CellContent::Char('█'));
}
#[test]
fn test_draw_grayscale_buffer() {
let mut dest = OptimizedBuffer::new(10, 10);
let mut src = GrayscaleBuffer::new(5, 5);
src.set(0, 0, 0.0); src.set(1, 0, 1.0);
dest.draw_grayscale_buffer(0, 0, &src, Rgba::WHITE, Rgba::BLACK);
let cell0 = dest.get(0, 0).unwrap();
let cell1 = dest.get(1, 0).unwrap();
assert_eq!(cell0.content, crate::cell::CellContent::Char(' '));
assert_eq!(cell1.content, crate::cell::CellContent::Char('@'));
}
#[test]
fn test_luminance() {
assert!((Rgba::WHITE.luminance() - 1.0).abs() < 0.01);
assert!(Rgba::BLACK.luminance().abs() < 0.01);
assert!((Rgba::RED.luminance() - 0.299).abs() < 0.01);
}
#[test]
fn test_srgb_linear_roundtrip() {
for i in 0..=10 {
let value = i as f32 / 10.0;
let linear = super::srgb_to_linear(value);
let back = super::linear_to_srgb(linear);
assert!(
(value - back).abs() < 0.0001,
"Roundtrip failed for {value}: got {back}"
);
}
}
#[test]
fn test_srgb_linear_boundary() {
let below = super::srgb_to_linear(0.04);
let above = super::srgb_to_linear(0.05);
assert!(below > 0.0);
assert!(above > below);
}
#[test]
fn test_gamma_correct_average_brighter_than_naive() {
let red = Rgba::rgb(1.0, 0.0, 0.0);
let blue = Rgba::rgb(0.0, 0.0, 1.0);
let naive_avg_r = 0.5;
let naive_avg_b = 0.5;
let (fg, _bg) = super::average_colors(&[red, blue], &[true, true]);
assert!(
fg.r > naive_avg_r,
"Gamma-correct red {} should be brighter than naive {}",
fg.r,
naive_avg_r
);
assert!(
fg.b > naive_avg_b,
"Gamma-correct blue {} should be brighter than naive {}",
fg.b,
naive_avg_b
);
}
#[test]
fn test_gamma_correct_average_preserves_extremes() {
let (fg, _) = super::average_colors(&[Rgba::WHITE, Rgba::WHITE], &[true, true]);
assert!((fg.r - 1.0).abs() < 0.001);
assert!((fg.g - 1.0).abs() < 0.001);
assert!((fg.b - 1.0).abs() < 0.001);
let (fg, _) = super::average_colors(&[Rgba::BLACK, Rgba::BLACK], &[true, true]);
assert!(fg.r.abs() < 0.001);
assert!(fg.g.abs() < 0.001);
assert!(fg.b.abs() < 0.001);
}
#[test]
fn test_gamma_correct_fg_bg_separation() {
let red = Rgba::RED;
let green = Rgba::GREEN;
let blue = Rgba::BLUE;
let white = Rgba::WHITE;
let mask = [true, true, false, false];
let (fg, bg) = super::average_colors(&[red, green, blue, white], &mask);
assert!(fg.r > 0.5, "FG should have red component");
assert!(fg.g > 0.5, "FG should have green component");
assert!(fg.b < 0.3, "FG should not have blue component");
assert!(bg.b > 0.5, "BG should have blue component");
}
#[test]
fn test_try_from_pixels_success() {
let pixels = vec![Rgba::RED; 100];
let result = PixelBuffer::try_from_pixels(10, 10, pixels);
assert!(result.is_ok());
let buf = result.unwrap();
assert_eq!(buf.width, 10);
assert_eq!(buf.height, 10);
assert_eq!(buf.pixels.len(), 100);
}
#[test]
fn test_try_from_pixels_size_mismatch() {
let pixels = vec![Rgba::RED; 50]; let result = PixelBuffer::try_from_pixels(10, 10, pixels);
assert!(result.is_err());
match result {
Err(Error::SizeMismatch { expected, actual }) => {
assert_eq!(expected, 100);
assert_eq!(actual, 50);
}
other => {
assert!(
matches!(other, Err(Error::SizeMismatch { .. })),
"expected SizeMismatch error"
);
}
}
}
#[test]
fn test_try_from_pixels_dimension_overflow() {
let pixels = vec![];
let result = PixelBuffer::try_from_pixels(u32::MAX, u32::MAX, pixels);
assert!(result.is_err());
assert!(
matches!(result, Err(Error::DimensionOverflow { .. }))
|| matches!(result, Err(Error::SizeMismatch { .. }))
);
}
#[test]
fn test_from_pixels_delegates_to_try() {
let pixels = vec![Rgba::BLUE; 25];
let buf = PixelBuffer::from_pixels(5, 5, pixels);
assert_eq!(buf.width, 5);
assert_eq!(buf.height, 5);
}
}