use font8x8::{BASIC_FONTS, UnicodeFonts};
pub type Color = [u8; 4];
pub struct Canvas {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
depth: Vec<f32>,
}
impl Canvas {
pub fn new(width: u32, height: u32, background: Color) -> Self {
let mut pixels = vec![0; width as usize * height as usize * 4];
for pixel in pixels.chunks_exact_mut(4) {
pixel.copy_from_slice(&background);
}
Self {
width,
height,
pixels,
depth: vec![f32::NEG_INFINITY; width as usize * height as usize],
}
}
pub fn vertical_gradient(&mut self, top: Color, bottom: Color) {
let denominator = self.height.saturating_sub(1).max(1) as f32;
for y in 0..self.height {
let amount = y as f32 / denominator;
let color = mix(top, bottom, amount);
let start = y as usize * self.width as usize * 4;
let end = start + self.width as usize * 4;
for pixel in self.pixels[start..end].chunks_exact_mut(4) {
pixel.copy_from_slice(&color);
}
}
}
pub fn line_3d(
&mut self,
from: [f32; 3],
to: [f32; 3],
thickness: f32,
from_color: Color,
to_color: Color,
) {
let dx = to[0] - from[0];
let dy = to[1] - from[1];
let distance = (dx * dx + dy * dy).sqrt();
let steps = distance.ceil().max(1.0) as usize;
for step in 0..=steps {
let amount = step as f32 / steps as f32;
let x = from[0] + dx * amount;
let y = from[1] + dy * amount;
let z = from[2] + (to[2] - from[2]) * amount;
self.disc_3d(x, y, z, thickness * 0.5, mix(from_color, to_color, amount));
}
}
pub fn sphere(&mut self, x: f32, y: f32, z: f32, radius: f32, color: Color) {
let radius = radius.max(1.0);
let min_x = (x - radius).floor() as i32;
let max_x = (x + radius).ceil() as i32;
let min_y = (y - radius).floor() as i32;
let max_y = (y + radius).ceil() as i32;
let light = [-0.42_f32, -0.55_f32, 0.72_f32];
for py in min_y..=max_y {
for px in min_x..=max_x {
let nx = (px as f32 + 0.5 - x) / radius;
let ny = (py as f32 + 0.5 - y) / radius;
let radial = nx * nx + ny * ny;
if radial > 1.0 {
continue;
}
let nz = (1.0 - radial).sqrt();
let surface_z = z + nz * radius * 0.0025;
let diffuse = (nx * light[0] + ny * light[1] + nz * light[2]).max(0.0);
let rim = (1.0 - nz).powi(2) * 0.12;
let shade = (0.48 + diffuse * 0.56 - rim).clamp(0.28, 1.12);
self.pixel_3d(px, py, surface_z, scale(color, shade));
}
}
}
pub fn disc_3d(&mut self, x: f32, y: f32, z: f32, radius: f32, color: Color) {
let radius = radius.max(0.8);
let min_x = (x - radius).floor() as i32;
let max_x = (x + radius).ceil() as i32;
let min_y = (y - radius).floor() as i32;
let max_y = (y + radius).ceil() as i32;
let squared = radius * radius;
for py in min_y..=max_y {
for px in min_x..=max_x {
let dx = px as f32 + 0.5 - x;
let dy = py as f32 + 0.5 - y;
if dx * dx + dy * dy <= squared {
self.pixel_3d(px, py, z, color);
}
}
}
}
pub fn fill_rect(&mut self, x: u32, y: u32, width: u32, height: u32, color: Color) {
for py in y..y.saturating_add(height).min(self.height) {
for px in x..x.saturating_add(width).min(self.width) {
self.pixel(px as i32, py as i32, color);
}
}
}
pub fn horizontal_line(&mut self, y: u32, color: Color) {
if y >= self.height {
return;
}
for x in 0..self.width {
self.pixel(x as i32, y as i32, color);
}
}
pub fn text(&mut self, x: u32, y: u32, value: &str, color: Color) {
let mut cursor = x;
for character in value.chars() {
if cursor + 8 >= self.width {
break;
}
if let Some(glyph) = BASIC_FONTS.get(character) {
for (row, bits) in glyph.iter().enumerate() {
for column in 0..8 {
if bits & (1 << column) != 0 {
self.pixel((cursor + column) as i32, (y + row as u32) as i32, color);
}
}
}
}
cursor += 8;
}
}
fn pixel_3d(&mut self, x: i32, y: i32, z: f32, color: Color) {
if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
return;
}
let index = y as usize * self.width as usize + x as usize;
if z < self.depth[index] {
return;
}
self.depth[index] = z;
let offset = index * 4;
self.pixels[offset..offset + 4].copy_from_slice(&color);
}
fn pixel(&mut self, x: i32, y: i32, color: Color) {
if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
return;
}
let offset = (y as usize * self.width as usize + x as usize) * 4;
if color[3] == 255 {
self.pixels[offset..offset + 4].copy_from_slice(&color);
return;
}
let alpha = color[3] as f32 / 255.0;
for (channel, source) in color.iter().take(3).enumerate() {
self.pixels[offset + channel] = (self.pixels[offset + channel] as f32 * (1.0 - alpha)
+ *source as f32 * alpha)
.round() as u8;
}
self.pixels[offset + 3] = 255;
}
}
pub fn mix(from: Color, to: Color, amount: f32) -> Color {
let amount = amount.clamp(0.0, 1.0);
[
(from[0] as f32 + (to[0] as f32 - from[0] as f32) * amount).round() as u8,
(from[1] as f32 + (to[1] as f32 - from[1] as f32) * amount).round() as u8,
(from[2] as f32 + (to[2] as f32 - from[2] as f32) * amount).round() as u8,
255,
]
}
pub fn scale(color: Color, amount: f32) -> Color {
[
(color[0] as f32 * amount).clamp(0.0, 255.0) as u8,
(color[1] as f32 * amount).clamp(0.0, 255.0) as u8,
(color[2] as f32 * amount).clamp(0.0, 255.0) as u8,
color[3],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nearer_pixels_win_depth_test() {
let mut canvas = Canvas::new(2, 2, [0, 0, 0, 255]);
canvas.pixel_3d(0, 0, 1.0, [255, 0, 0, 255]);
canvas.pixel_3d(0, 0, 0.0, [0, 255, 0, 255]);
assert_eq!(&canvas.pixels[..4], &[255, 0, 0, 255]);
}
}