Adding draw functions

Part 1 - Part 2

Rendering - Part 2

Following on from creating our 2d pixel buffer in Part 1, we start by splitting the drawing functions into a separate seperate source code file called render.rs - main.rs will now contain our test and exploration code to illustrate the wrok we are doing.

In part 01 we added functions for handling the new pixel buffer and drawing a rudimentory square. In this part we add draw_pixel to draw a singel pixel at an x,y location for a given RGBA colour.
draw_line uses draw_pixel to draw a line between two x,y points.
draw_triangel and draw_rect use draw_line to draw a triangle or rectangle from provided points.

The draw_square function will probably get depricated because it does not follow the principles of the other draw functions.

The Rust code render.rs

// Adding render functions to minifb
// by Rich of maths.earth 202500308
/// A struct to represent an RGBA pixel.
#[derive(Clone, Copy)]
pub struct Pixel {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

impl Pixel {
    /// Create a new Pixel with the given RGBA values.
    pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self { r, g, b, a }
    }

    /// Convert this pixel into a 32-bit colour in 0xAARRGGBB format.
    pub fn to_u32(self) -> u32 {
        ((self.a as u32) << 24) | ((self.r as u32) << 16) | ((self.g as u32) << 8) | (self.b as u32)
    }
}

/// Clears the given 2D pixel buffer by filling every pixel with black.
pub fn clear_buffer(buffer: &mut Vec>) {
    for row in buffer.iter_mut() {
        for pixel in row.iter_mut() {
            *pixel = Pixel::new(0, 0, 0, 255);
        }
    }
}

/// Draw a square into the provided 2D pixel buffer.
/// 
/// * `x` and `y` are the top-left coordinates of the square.
/// * `square_width` and `square_height` specify its dimensions.
/// * `color` is the colour to draw.
pub fn draw_square(
    buffer: &mut Vec>,
    x: usize,
    y: usize,
    square_width: usize,
    square_height: usize,
    color: Pixel,
) {
    for j in y..(y + square_height) {
        for i in x..(x + square_width) {
            // Ensure we remain within bounds.
            if j < buffer.len() && i < buffer[j].len() {
                buffer[j][i] = color;
            }
        }
    }
}

/// Draw a single pixel into the provided 2D pixel buffer.
pub fn draw_pixel(
    buffer: &mut Vec>, 
    x: usize, 
    y: usize, 
    color: 
    Pixel
) {
    if y < buffer.len() && x < buffer[y].len() {
        buffer[y][x] = color;
    }
}

/// Draw a line in to the provided 2D pixel buffer.
pub fn draw_line(
    buffer: &mut Vec>,
    x0: i32,
    y0: i32,
    x1: i32,
    y1: i32,
    color: Pixel,
) {
    // Calculate the differences.
    let delta_x = x1 - x0;
    let delta_y = y1 - y0;
    
    // Determine the number of steps needed based on the longest side.
    let longest_side_length = if delta_x.abs() >= delta_y.abs() {
        delta_x.abs()
    } else {
        delta_y.abs()
    };

    // If the line is just a point, draw that pixel.
    if longest_side_length == 0 {
        draw_pixel(buffer, x0 as usize, y0 as usize, color);
        return;
    }
    
    // Calculate the increments for each step.
    let x_inc = delta_x as f32 / longest_side_length as f32;
    let y_inc = delta_y as f32 / longest_side_length as f32;
    
    // Initialise the current position.
    let mut current_x = x0 as f32;
    let mut current_y = y0 as f32;
    
    // Draw pixels along the line.
    for _ in 0..=longest_side_length {
        let ix = current_x.round() as usize;
        let iy = current_y.round() as usize;
        draw_pixel(buffer, ix, iy, color);
        current_x += x_inc;
        current_y += y_inc;
    }
}

/// Draw a triangle in to the provided 2D pixel buffer.
pub fn draw_triangle(
    buffer: &mut Vec>,
    x0: i32,
    y0: i32,
    x1: i32,
    y1: i32,
    x2: i32,
    y2: i32,
    color: Pixel,
) {
    draw_line(buffer, x0, y0, x1, y1, color);
    draw_line(buffer, x1, y1, x2, y2, color);
    draw_line(buffer, x2, y2, x0, y0, color);
}

/// Draw a rectangle in to the provided 2D pixel buffer.

pub fn draw_rect(
    buffer: &mut Vec>,
    x: i32,
    y: i32,
    width: i32,
    height: i32,
    color: Pixel 
) {
    draw_line(buffer, x, y, x + width, y, color);
    draw_line(buffer, x + width, y, x + width, y + height, color);
    draw_line(buffer, x + width, y + height, x, y + height, color);
    draw_line(buffer, x, y + height, x, y, color); 
}

/// Converts a 2D pixel buffer into a 1D vector of u32 values (0xAARRGGBB).
pub fn buffer_to_u32(buffer: &Vec>) -> Vec {
    let mut flat: Vec = Vec::with_capacity(buffer.len() * buffer[0].len());
    for row in buffer {
        for &pixel in row {
            flat.push(pixel.to_u32());
        }
    }
    flat
}

The Rust code main.rs

// testing our render functions with minifb
// by Rich of maths.earth 202500308

extern crate minifb;

use minifb::{Key, Window, WindowOptions};

// Import our rendering module.
mod render;
use render::{buffer_to_u32, clear_buffer, draw_square, draw_pixel, draw_line, draw_triangle, draw_rect, Pixel};

// Public constants for the window dimensions.
pub const WIDTH: usize = 800;
pub const HEIGHT: usize = 600;

fn main() {
    // Create a new window.
    let mut window = Window::new(
        "Lesson One: 2D Pixel Buffer Rendering with minifb (Library Example)",
        WIDTH,
        HEIGHT,
        WindowOptions::default(),
    )
    .expect("Unable to create window");

    // Create a 2D pixel buffer initialised to black.
    // Each pixel is stored as a Pixel struct.
    let mut pixel_buffer: Vec> = vec![vec![Pixel::new(0, 0, 0, 255); WIDTH]; HEIGHT];

    // Define the dimensions of the square.
    let square_width = 100;
    let square_height = 100;
    // Calculate the top-left corner so that the square is centred.
    let square_x = (WIDTH - square_width) / 2;
    let square_y = (HEIGHT - square_height) / 2;

    // Set the initial square colour to red.
    let mut square_color = Pixel::new(255, 0, 0, 255);
    // Colour state: 0 = red, 1 = green, 2 = blue.
    let mut color_state: u32 = 0;

    // Variable to detect key transitions for the space bar.
    let mut prev_space_down = false;

    // Main loop.
    while window.is_open() && !window.is_key_down(Key::Escape) {
        // Check the current state of the space bar.
        let current_space = window.is_key_down(Key::Space);
        // If space has just been pressed (transition from not pressed to pressed)...
        if current_space && !prev_space_down {
            // Cycle the square colour.
            color_state = (color_state + 1) % 3;
            square_color = match color_state {
                0 => Pixel::new(255, 0, 0, 255),   // Red
                1 => Pixel::new(0, 255, 0, 255),   // Green
                2 => Pixel::new(0, 0, 255, 255),   // Blue
                _ => Pixel::new(255, 0, 0, 255),   // Fallback to red (should not occur)
            };
        }
        // Save the current space state for the next iteration.
        prev_space_down = current_space;

        // Clear the pixel buffer (fill with black).
        clear_buffer(&mut pixel_buffer);

        // Draw the square into the pixel buffer using the current square colour.
        draw_square(&mut pixel_buffer, square_x, square_y, square_width, square_height, square_color);

        // Draw a single pixel into the pixel buffer.
        draw_pixel(&mut pixel_buffer, 10, 10, Pixel::new(255, 255, 255, 255));

        // Draw a line into the pixel buffer.
        draw_line(&mut pixel_buffer, 100, 100, 200, 200, Pixel::new(255, 255, 255, 255));
        draw_line(&mut pixel_buffer, 180, 100, 280, 280, Pixel::new(255, 0, 0, 255));

        // Draw a triangle into the pixel buffer.
        draw_triangle(&mut pixel_buffer, 400, 400, 500, 400, 450, 500, Pixel::new(0, 255, 0, 255));

        // Draw a rectangle into the pixel buffer.
        draw_rect(&mut pixel_buffer, 600, 400, 100, 100, Pixel::new(0, 0, 255, 255));
        
        // Convert the 2D pixel buffer to a 1D u32 buffer.
        let buffer = buffer_to_u32(&pixel_buffer);

        // Update the window with the new 1D pixel buffer.
        // minifb handles double buffering internally.
        window
            .update_with_buffer(&buffer, WIDTH, HEIGHT)
            .expect("Failed to update window");
    }
}

Cargo.toml

[package]
name = "minifb-rgb"
version = "0.1.0"
edition = "2024"

[dependencies]
minifb = "0.28.0"

The image produced:

Mission Accomplished

And there we have it, we can draw pixels, lines, triangles and rectangles.