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>,
text_scale: u32,
}
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],
text_scale: 1,
}
}
pub fn set_text_scale(&mut self, scale: u32) {
self.text_scale = scale.clamp(1, 3);
}
pub fn text_cell_width(&self) -> u32 {
8 * self.text_scale
}
pub fn text_cell_height(&self) -> u32 {
8 * self.text_scale
}
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 triangle_3d(
&mut self,
first: [f32; 3],
second: [f32; 3],
third: [f32; 3],
color: Color,
) {
self.triangle_3d_gradient(first, second, third, color, color, color);
}
pub fn triangle_3d_gradient(
&mut self,
first: [f32; 3],
second: [f32; 3],
third: [f32; 3],
first_color: Color,
second_color: Color,
third_color: Color,
) {
let area = edge(first, second, third[0], third[1]);
if area.abs() < 0.001 {
return;
}
let sign = area.signum();
let min_x = first[0].min(second[0]).min(third[0]).floor().max(0.0) as i32;
let max_x = first[0]
.max(second[0])
.max(third[0])
.ceil()
.min(self.width.saturating_sub(1) as f32) as i32;
let min_y = first[1].min(second[1]).min(third[1]).floor().max(0.0) as i32;
let max_y = first[1]
.max(second[1])
.max(third[1])
.ceil()
.min(self.height.saturating_sub(1) as f32) as i32;
let inverse_area = 1.0 / area.abs();
for y in min_y..=max_y {
for x in min_x..=max_x {
let px = x as f32 + 0.5;
let py = y as f32 + 0.5;
let w0 = edge(second, third, px, py) * sign;
let w1 = edge(third, first, px, py) * sign;
let w2 = edge(first, second, px, py) * sign;
if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
continue;
}
let z = (w0 * first[2] + w1 * second[2] + w2 * third[2]) * inverse_area;
let weights = [w0 * inverse_area, w1 * inverse_area, w2 * inverse_area];
let mut color = [0_u8; 4];
for channel in 0..4 {
color[channel] = (first_color[channel] as f32 * weights[0]
+ second_color[channel] as f32 * weights[1]
+ third_color[channel] as f32 * weights[2])
.round()
.clamp(0.0, 255.0) as u8;
}
self.pixel_3d(x, y, z, color);
}
}
}
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 stroke_rect(&mut self, x: u32, y: u32, width: u32, height: u32, color: Color) {
if width == 0 || height == 0 {
return;
}
for px in x..x.saturating_add(width).min(self.width) {
self.pixel(px as i32, y as i32, color);
self.pixel(
px as i32,
y.saturating_add(height.saturating_sub(1)) as i32,
color,
);
}
for py in y..y.saturating_add(height).min(self.height) {
self.pixel(x as i32, py as i32, color);
self.pixel(
x.saturating_add(width.saturating_sub(1)) as i32,
py as i32,
color,
);
}
}
pub fn set_pixel(&mut self, x: u32, y: u32, color: Color) {
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;
let scale = self.text_scale;
let cell_width = self.text_cell_width();
for character in value.chars() {
if cursor + cell_width > 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 {
let pixel_x = cursor + column * scale;
let pixel_y = y + row as u32 * scale;
for offset_y in 0..scale {
for offset_x in 0..scale {
self.pixel(
(pixel_x + offset_x) as i32,
(pixel_y + offset_y) as i32,
color,
);
}
}
}
}
}
}
cursor += cell_width;
}
}
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;
}
}
fn edge(first: [f32; 3], second: [f32; 3], x: f32, y: f32) -> f32 {
(x - first[0]) * (second[1] - first[1]) - (y - first[1]) * (second[0] - first[0])
}
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]);
}
#[test]
fn triangle_rasterizes_its_interior() {
let mut canvas = Canvas::new(8, 8, [0, 0, 0, 255]);
canvas.triangle_3d(
[1.0, 1.0, 0.5],
[6.0, 1.0, 0.5],
[1.0, 6.0, 0.5],
[10, 20, 30, 255],
);
let offset = (2 * 8 + 2) * 4;
assert_eq!(&canvas.pixels[offset..offset + 4], &[10, 20, 30, 255]);
}
#[test]
fn triangle_interpolates_vertex_colors() {
let mut canvas = Canvas::new(8, 8, [0, 0, 0, 255]);
canvas.triangle_3d_gradient(
[1.0, 1.0, 0.5],
[6.0, 1.0, 0.5],
[1.0, 6.0, 0.5],
[255, 0, 0, 255],
[0, 255, 0, 255],
[0, 0, 255, 255],
);
let offset = (2 * 8 + 2) * 4;
let color = &canvas.pixels[offset..offset + 4];
assert!(color[0] > 0 && color[1] > 0 && color[2] > 0);
}
}