A quick guide on creating your very own graphics library for your custom operating system in C++. Before you jump into implementing this, you will need a way to draw graphics on the screen. If you don’t already, make sure that you have a VGA/VBE driver, so that way you can easily interact with the framebuffer.
A complete working example of this is available here: ChoacuryOS/src/gui/window/gui.cpp at basic_window · MrBisquit/ChoacuryOS
And an example VBE driver is available here: ChoacuryOS/src/drivers/vbe.c at main · Pineconium/ChoacuryOS
You’ll need to create 2 files for this: GUI.cpp
, and GUI.hpp
. I’ll explain everything, what it does, and how it works. In this guide, you’ll create:
- Lines
- Rectangles (Outline & Filled)
- Circles (Outline & Filled)
- Custom buffers
Getting the header set up
First of all, let’s create a namespace called GUI
. Inside of this we will create structures for things like points, rectangles, circles, and buffers. Yours should look a little like this:
#pragma once // Include the header file only once
#ifndef GUI_HPP // Header guard
namespace GUI {
struct uRect32 {
uRect32(uint32_t x, uint32_t y, uint32_t width, uint32_t height) :
x(x), y(y), width(width), height(height) { }
public:
uint32_t x;
uint32_t y;
uint32_t width;
uint32_t height;
};
struct uPoint32 {
uRect32(uint32_t x, uint32_t y) :
x(x), y(y) { }
public:
uint32_t x;
uint32_t y;
};
struct uCircle32 {
uCircle32(uint32_t x, uint32_t y, uint32_t radius) :
x(x), y(y), radius(radius) { }
public:
uint32_t x;
uint32_t y;
uint32_t radius;
};
struct uBuffer32 {
uBuffer32(uint32_t* buffer, uint32_t width, uint32_t height) :
buffer(buffer), width(width), height(height) { }
public:
uint32_t* buffer;
uint32_t width;
uint32_t height;
};
}
#endif // GUI_HPP // End of header guard
We’ll leave it at that for now, but I suggest adding little comments so that your IDE can suggest how to use the functions. For example,
/// @brief Defines a uRect32
struct uRect32 {
...
}
These kinds of comments are mainly useful for things like functions rather than structures. Now, the structures we have defined above are the basics, you may wish to add things such as text, or custom polygons later on.
Now we need to do a little set up in the GUI.cpp file so we can add things later.
#include "GUI.hpp"
Functionality
Let’s get the functionality of these set up. There’s a few things that don’t need much explaining, like the clear
, get_pixel
, and put_pixel
functions.
// GUI.hpp
// In the GUI namespace
/// @brief Clears a buffer with a certain colour
/// @param buffer The buffer
/// @param color The colour to clear it with
void clear(uBuffer32 buffer, uint32_t color);
/// @brief Gets the pixel from a buffer at a certain position
/// @param buffer The buffer
/// @param point The point on the buffer
/// @return The colour of the pixel
uint32_t get_pixel(uBuffer32 buffer, uPoint32 point);
/// @brief Draws a pixel on a buffer at a certain position
/// @param buffer The buffer
/// @param point The point on the buffer
/// @param color The colour
void put_pixel(uBuffer32 buffer, uPoint32 point, uint32_t color);
// GUI.cpp
void GUI::clear(uBuffer32 buffer, uint32_t color) {
for (uint32_t y = 0; y < buffer.height; y++) {
for (uint32_t x = 0; x < buffer.width; x++) {
GUI::put_pixel(buffer, GUI::uPoint32(x, y), color);
}
}
}
uint32_t GUI::get_pixel(uBuffer32 buffer, uPoint32 point) {
return buffer.buffer[point.y * buffer.width + point.x];
}
void GUI::put_pixel(uBuffer32 buffer, uPoint32 point, uint32_t color) {
uint32_t *pixel = &buffer.buffer[point.y * buffer.width + point.x];
uint32_t existing_color = *pixel;
uint8_t alpha = (color >> 24) & 0xFF;
if (alpha == 0xFF) {
return;
} else if (alpha == 0x00) {
*pixel = color & 0xFFFFFF;
} else {
uint8_t src_r = (color >> 16) & 0xFF;
uint8_t src_g = (color >> 8) & 0xFF;
uint8_t src_b = color & 0xFF;
uint8_t dst_r = (existing_color >> 16) & 0xFF;
uint8_t dst_g = (existing_color >> 8) & 0xFF;
uint8_t dst_b = existing_color & 0xFF;
uint8_t blended_r = ((src_r * (255 - alpha)) + (dst_r * alpha)) / 255;
uint8_t blended_g = ((src_g * (255 - alpha)) + (dst_g * alpha)) / 255;
uint8_t blended_b = ((src_b * (255 - alpha)) + (dst_b * alpha)) / 255;
*pixel = (blended_r << 16) | (blended_g << 8) | blended_b;
}
}
Lines
Before getting anything else set up, I personally think that lines are important. So first let’s get the definition set up. In the namespace that we just created (GUI
), create a definition for this, like below.
/// @brief Draws a line from point a to point b
/// @param buffer The buffer
/// @param a Point A
/// @param b Point B
/// @param color The colour
void draw_line(uBuffer32 buffer, uPoint32 a, uPoint32 b, uint32_t color);
Now we can move to the GUI.cpp
file that we created earlier. We’ll be using Bresenham’s Line Algorithm, as it’s the most popular and as far as I know, the most efficient.
void GUI::draw_line(uBuffer32 buffer, uPoint32 a, uPoint32 b, uint32_t color) {
uint32_t x1 = a.x;
uint32_t y1 = a.y;
uint32_t x2 = b.x;
uint32_t y2 = b.y;
int dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
int dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
int err = dx + dy, e2;
while (1) {
GUI::put_pixel(buffer, uPoint32(x1, y1), color);
if (x1 == x2 && y1 == y2) break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; x1 += sx; }
if (e2 <= dx) { err += dx; y1 += sy; }
}
}
The Wikipedia page I’ve linked can explain this algorithm far better than what I could, so I do suggest checking that out if you’re interested.
Rectangles
This is one of the easiest to implement. For rectangles, we need two definitions. Filled and outlined. We will define these in the header first.
/// @brief Draws a rectangle on the buffer
/// @param buffer The buffer
/// @param rect The rectangle
/// @param color The colour
void draw_rect(uBuffer32 buffer, uRect32 rect, uint32_t color);
/// @brief Draws a filled rectangle on the buffer
/// @param buffer The buffer
/// @param rect The rectangle
/// @param color The colour
void draw_fillled_rect(uBuffer32 buffer, uRect32 rect, uint32_t color);
And now we can add the functionality to the definitions.
void GUI::draw_rect(uBuffer32 buffer, uRect32 rect, uint32_t color) {
uint32_t sx = rect.x;
uint32_t sy = rect.y;
uint32_t ex = rect.x + rect.width;
uint32_t ey = rect.y + rect.height;
draw_line(buffer, uPoint32(sx, sy), uPoint32(ex, sy), color); // Top line
draw_line(buffer, uPoint32(ex, sy), uPoint32(ex, ey), color); // Right line
draw_line(buffer, uPoint32(sx, ey), uPoint32(ex, ey), color); // Bottom line
draw_line(buffer, uPoint32(sx, ey), uPoint32(sx, sy), color); // Left line
}
void GUI::fill_rect(uBuffer32 buffer, uRect32 rect, uint32_t color) {
uint32_t sx = rect.x;
uint32_t sy = rect.y;
uint32_t ex = rect.x + rect.width;
uint32_t ey = rect.y + rect.height;
for (uint32_t y = sy; y < ey; y++) {
for (uint32_t x = sx; x < ex; x++) {
GUI::put_pixel(buffer, uPoint32(x, y), color);
}
}
}
Circles
Circles are arguable the most difficult out of all of them. We’ll be using the Midpoint Circle Algorithm, sometimes known as Bresenham’s Circle Algorithm. And again, Wikipedia does a better job at explaining it than I do, so I suggest checking that out.
First of all, we need to create two definitions in our header file.
/// @brief Draws a circle on the buffer
/// @param buffer The buffer
/// @param circle The circle
void draw_circle(uBuffer32 buffer, uCircle32 circle, uint32_t color);
/// @brief Draws a filled circle on the buffer
/// @param buffer The buffer
/// @param circle The circle
void draw_filled_circle(uBuffer32 buffer, uCircle32 circle, uint32_t color);
Then, we can actually write the functionality for the definitions.
void GUI::draw_circle(uBuffer32 buffer, uCircle32 circle, uint32_t color) {
uint32_t x = 0;
uint32_t y = circle.radius;
int32_t d = 3 - 2 * circle.radius;
int ex = circle.radius;
int ey = circle.radius;
while (x <= y) {
put_pixel(buffer, uPoint32(circle.x + x + ex, circle.y + y + ey), color);
put_pixel(buffer, uPoint32(circle.x - x + ex, circle.y + y + ey), color);
put_pixel(buffer, uPoint32(circle.x + x + ex, circle.y - y + ey), color);
put_pixel(buffer, uPoint32(circle.x - x + ex, circle.y - y + ey), color);
put_pixel(buffer, uPoint32(circle.x + y + ex, circle.y + x + ey), color);
put_pixel(buffer, uPoint32(circle.x - y + ex, circle.y + x + ey), color);
put_pixel(buffer, uPoint32(circle.x + y + ex, circle.y - x + ey), color);
put_pixel(buffer, uPoint32(circle.x - y + ex, circle.y - x + ey), color);
if (d > 0) {
d = d + 4 * (x - y) + 10;
y--;
} else {
d = d + 4 * x + 6;
}
x++;
}
}
void GUI::draw_filled_circle(uBuffer32 buffer, uCircle32 circle, uint32_t color) {
int x = circle.radius;
int y = 0;
int decisionOver2 = 1 - x;
int ex = circle.radius;
int ey = circle.radius;
while (y <= x) {
for (int i = -x; i <= x; i++) {
put_pixel(buffer, uPoint32(circle.x + i + ex, circle.y + y + ey), color);
put_pixel(buffer, uPoint32(circle.x + i + ex, circle.y - y + ey), color);
}
for (int i = -y; i <= y; i++) {
put_pixel(buffer, uPoint32(circle.x + i + ex, circle.y + x + ey), color);
put_pixel(buffer, uPoint32(circle.x + i + ex, circle.y - x + ey), color);
}
y++;
if (decisionOver2 <= 0) {
decisionOver2 += 2 * y + 1;
} else {
x--;
decisionOver2 += 2 * (y - x) + 1;
}
}
}
So now you can:
- Draw a line
- Draw an outline of a rectangle
- Draw a filled rectangle
- Draw an outline of a circle
- Draw a filled circle
Discover more from WTDawson
Subscribe to get the latest posts sent to your email.