Demilade Sonuga's blog

All posts

Animating Bitmaps

2022-12-27 · 13 min read

Before we can get on to animating bitmaps, we first need to ask ourselves: what is animation?

Animation is just a process of manipulating images to make them look like they're moving on a screen. It's just a matter of drawing pictures so fast, that the objects the pictures display appear as if they were moving.

The key thing here is getting the image on the screen, removing it, then getting the next image on the screen, blazingly fast.

We're going to get our feet wet here by animating the block we've drawn.

For now, our aim is to move the block from the upper left corner of the screen to the upper right corner. Before continuing, think about how you'll do this yourself.

The steps to take to get the block across the screen:

  1. Initialize the block position to the top left corner of the screen
  2. Draw a block at the block position
  3. Erase the block from the screen
  4. Increment the block position to the right
  5. If the block position is still within screen bounds, go to step 2

The computer executing these steps very quickly gives the illusion of movement: hence animation.

In Rust code, this will translate to:

let mut block_position = (0, 0); // (row, column)
while block_position.1 < NO_OF_PIXELS_IN_A_ROW {
draw_bitmap(screen, &block, block_position);
erase_bitmap(screen, &block, block_position);
block_position.1 += 1;
}

Now, we see that we need an erase_bitmap function. Before moving on, implement your own.

At this point, the only thing we've drawn on screen is our bitmap. Everything else is pure black. By default, a black rectangle is our background image. Since our block is drawn on top of this background, erasing it is simply a matter of coloring it black.

In bitmap.rs, this will be:

// Erase a bitmap from the screen
pub fn erase_bitmap(screen: &mut Screen, bitmap: &Bitmap, pos: (usize, usize)) {
for row in 0..bitmap.width() {
for col in 0..bitmap.height() {
// Blacking out the bitmap on screen
// A color value of all 0s is black
screen.pixels[row + pos.0][col + pos.1] = Pixel {
red: 0,
green: 0,
blue: 0,
reserved: 0
};
}
}
}

The above fills the area where a bitmap at position (row, column) is located with the screen background color (which is black at the moment).

To get on with our animation, we also need to modify our draw_bitmap function:

// DELETED: pub fn draw_bitmap(screen: &mut Screen, bitmap: &Bitmap) {
pub fn draw_bitmap(screen: &mut Screen, bitmap: &Bitmap, pos: (usize, usize)) { // NEW
for row in 0..bitmap.height() {
for col in 0..bitmap.width() {
let inverted_row = bitmap.height() - row - 1;
let color_table_index = bitmap.pixel_array[inverted_row * bitmap.width() + col];
let color = bitmap.color_table.0[color_table_index as usize];
// DELETED: screen.pixels[row][col] = Pixel {
screen.pixels[row + pos.0][col + pos.1] = Pixel {
red: color.red,
green: color.green,
blue: color.blue,
reserved: 0
};
}
}
}

The previous draw_bitmap could only draw a bitmap at the upper left corner of the screen. This modified one can now draw at any position on the screen.

Putting all this together in our efi_main:

#![no_std]
#![no_main]
#![feature(abi_efiapi)]

mod font;
use font::FONT;

mod uefi;
// DELETED: use uefi::{SystemTable, Screen, PixelFormat, Pixel, pre_graphics_print_str, print_str, printint};
use uefi::{SystemTable, Screen, PixelFormat, Pixel, pre_graphics_print_str, print_str,
printint, NO_OF_PIXELS_IN_A_ROW}; // NEW

mod bitmap;
// DELETED: use bitmap::{FileHeader, DIBHeader, ColorTable, Color, Bitmap, draw_bitmap};
use bitmap::{FileHeader, DIBHeader, ColorTable, Color, Bitmap, draw_bitmap, erase_bitmap}; // NEW

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ... Others

let screen = unsafe { &mut *screen };

let block_bytes = include_bytes!("./block.bmp");

let block = Bitmap::new(block_bytes);
// DELETED: draw_bitmap(screen, &block);

// NEW
// Initializing the block's position to the upper left corner
// of the screen
let mut block_position = (0, 0); // (row, column)
// Keep drawing while the block is still within screen boundaries
while block_position.1 < NO_OF_PIXELS_IN_A_ROW {
draw_bitmap(screen, &block, block_position);
erase_bitmap(screen, &block, block_position);
// Increase block position to the right
block_position.1 += 1;
}

0
}

For our code to compile now, we need to export NO_OF_PIXELS_IN_A_ROW from the uefi module:

// DELETED: const NO_OF_PIXELS_IN_A_ROW: usize = 640;
pub const NO_OF_PIXELS_IN_A_ROW: usize = 640; // NEW
// DELETED: const NO_OF_PIXELS_IN_A_COLUMN: usize = 480;
pub const NO_OF_PIXELS_IN_A_COLUMN: usize = 480; // NEW

Running the game is now as easy as hitting cargo runner.

If after running, the output is not the expected animation, check your runner's main function for this:

fn main() {
// ... Others

let status = Command::new("sudo")
.arg("qemu-system-x86_64")
.arg("-drive")
.arg(&format!("if=pflash,format=raw,unit=0,file={}/OVMF_CODE.fd,readonly=on", OVMF_PATH))
.arg("-drive")
.arg(&format!("if=pflash,unit=1,format=raw,file={}/OVMF_VARS.fd", OVMF_PATH))
.arg("-drive")
// Verify that the directory is target/x86_64-unknown-uefi/debug/
// and not blasterball/target/x86_64-unknown-uefi/debug/
.arg("format=raw,file=fat:rw:target/x86_64-unknown-uefi/debug/")
.status()
.unwrap();

// ... Others
}

Compiling your code now should result in the appearance of the block moving across the screen.

Animate Block

Take Away

  • Animation is drawing and erasing images in sequence, very quickly

For the full code, go to the repo

In The Next Post

We'll be panicking and figuring out what to do from there