The PAX graphics stack is being developed for the MCH2022 badge. It's goal is to allow anyone to, in C, use a powerful list of drawing features with good optimisation.
This library is the successor of the revised graphics API for the old badge.team firmware.
Let's get started with a simple example.
In this example, we'll make a green background with a red circle in the middle and some white text in the top left corner.
First, the setup.
In each file you'd like to use PAX graphics in, you'll need to include the pax_gfx
header:
// Include the PAX graphics library.
#include <pax_gfx.h>
The pax_buf_init
function is used to create a new graphics environment.
Here, we use it with the type PAX_BUF_16_565RGB, which is appropriate for the screen used by the MCH2022 badge.
void my_graphics_function() {
// Setup.
pax_buf_t buffer;
pax_buf_init(&buffer, NULL, 320, 240, PAX_BUF_16_565RGB);
}
This tells PAX to create a framebuffer for the screen, which is 320 by 240 pixels in size.
Next, let's make a nice, green background.
You can use many methods of picking a color,
but we'll go with pax_col_rgb
.
The pax_background
method fills the background with the desired color, like so:
void my_graphics_function() {
// Setup.
pax_buf_t buffer;
pax_buf_init(&buffer, NULL, 320, 240, PAX_BUF_16_565RGB);
// Green background.
pax_background(&buffer, pax_col_rgb(0, 255, 0));
}
Here's what it looks like if you write it to the screen:
To draw a circle, you use pax_draw_circle
or pax_simple_circle
with a midpoint co-ordinates and the radius:
void my_graphics_function() {
// Setup.
pax_buf_t buffer;
pax_buf_init(&buffer, NULL, 320, 240, PAX_BUF_16_565RGB);
// Green background.
pax_background(&buffer, pax_col_rgb(0, 255, 0));
// Red circle.
float midpoint_x = buffer.width / 2.0; // Middle of the screen horizontally.
float midpoint_y = buffer.height / 2.0; // Middle of the screen vertically.
float radius = 50; // Nice, big circle.
pax_simple_circle(&buffer, pax_col_rgb(255, 0, 0), midpoint_x, midpoint_y, radius);
}
Here's what it looks like if you write it to the screen:
Text is a bit more complicated, because you get to pick a font.
However, you can pick any font for now. We'll go with pax_font_sky_mono
.
It's point size is 9, which means that a text size of 9 is it's normal look. We'll pick 18 for readability.
The pax_draw_text
method is used to draw text.
It accepts a font, a point size, a top left corner position and the text to draw:
void my_graphics_function() {
// Setup.
pax_buf_t buffer;
pax_buf_init(&buffer, NULL, 320, 240, PAX_BUF_16_565RGB);
// Green background.
pax_background(&buffer, pax_col_rgb(0, 255, 0));
// Red circle.
float midpoint_x = buffer.width / 2.0; // Middle of the screen horizontally.
float midpoint_y = buffer.height / 2.0; // Middle of the screen vertically.
float radius = 50; // Nice, big circle.
pax_simple_circle(&buffer, pax_col_rgb(255, 0, 0), midpoint_x, midpoint_y, radius);
// White text.
float text_x = 0; // Top left corner of the screen.
float text_y = 0; // Top left corner of the screen.
char *my_text = "Hello, World!"; // You can pick any message you'd like.
float text_size = 18; // Twice the normal size for "sky".
pax_draw_text(&buffer, pax_col_rgb(255, 255, 255), pax_font_sky_mono, text_size, text_x, text_y, my_text);
}
Here's what it looks like if you write it to the screen:
Next, you'll want to draw this to the screen.
This differs per screen type, but for the MCH2022 badge's screen you use the
ili9341_write
method.
void my_graphics_function() {
// Setup.
pax_buf_t buffer;
pax_buf_init(&buffer, NULL, 320, 240, PAX_BUF_16_565RGB);
// Green background.
pax_background(&buffer, pax_col_rgb(0, 255, 0));
// Red circle.
float midpoint_x = buffer.width / 2.0; // Middle of the screen horizontally.
float midpoint_y = buffer.height / 2.0; // Middle of the screen vertically.
float radius = 50; // Nice, big circle.
pax_simple_circle(&buffer, pax_col_rgb(255, 0, 0), midpoint_x, midpoint_y, radius);
// White text.
float text_x = 0; // Top left corner of the screen.
float text_y = 0; // Top left corner of the screen.
char *my_text = "Hello, World!"; // You can pick any message you'd like.
float text_size = 18; // Twice the normal size for "7x9".
pax_draw_text(&buffer, pax_col_rgb(255, 255, 255), PAX_FONT_DEFAULT, text_size, text_x, text_y, my_text);
// Put it on the screen.
if (ili9341_write(&display, buffer.buf)) {
ESP_LOGE("my_tag", "Display write failed.");
} else {
ESP_LOGI("my_tag", "Display write success.");
}
}
The code we just added is the actual code which writes your beautiful creation to the screen.
If you use a different screen, you'll need to find it's documentation for which type of buffer it wants and how you write to it.
Finally, there's cleanup.
If you don't want to use the buffer you made during setup anymore, you can clean it up with
pax_buf_destroy
.
void my_graphics_function() {
// Setup.
pax_buf_t buffer;
pax_buf_init(&buffer, NULL, 320, 240, PAX_BUF_16_565RGB);
// Green background.
pax_background(&buffer, pax_col_rgb(0, 255, 0));
// Red circle.
float midpoint_x = buffer.width / 2.0; // Middle of the screen horizontally.
float midpoint_y = buffer.height / 2.0; // Middle of the screen vertically.
float radius = 50; // Nice, big circle.
pax_simple_circle(&buffer, pax_col_rgb(255, 0, 0), midpoint_x, midpoint_y, radius);
// White text.
float text_x = 0; // Top left corner of the screen.
float text_y = 0; // Top left corner of the screen.
char *my_text = "Hello, World!"; // You can pick any message you'd like.
float text_size = 18; // Twice the normal size for "7x9".
pax_draw_text(&buffer, pax_col_rgb(255, 255, 255), PAX_FONT_DEFAULT, text_size, text_x, text_y, my_text);
// Put it on the screen.
if (ili9341_write(&display, buffer.buf)) {
ESP_LOGE("my_tag", "Display write failed.");
} else {
ESP_LOGI("my_tag", "Display write success.");
}
// Cleanup.
pax_buf_destroy(&buffer);
}
An overview of the PAX API methods. The API is split into a few groups:
- Environment setup
- How to set up a framebuffer
- How to apply clipping
- How to clean up after yourself
- Basic drawing
- Drawing with basic shapes
- Drawing text
- Drawing images
- Drawing using shaders
- Colors and color math
- Creating colors
- Performing color math
- Matrix transformations
- Applying transformations
- Using the matrix stack
- Shaders
- Using shaders
- List of shaders
- Creating shaders
- Image codecs
- Decoding PNG images
- Requires a seperate component
A reference for how to use certain features.
For setting up a buffer, use pax_buf_init
:
For the MCH2022 badge, the size is 320x240 and the format is PAX_BUF_16_565RGB
.
pax_buf_t buffer;
pax_buf_init(&buffer, memory, width, height, format);
If you want, you can use a different type for intermediary buffers (e.g. to store image textures):
PAX converts colors automatically for you.
When you're done and you won't use a given buffer anymore, you can use pax_buf_destroy
:
PAX will automatically free any memory it used for the buffer.
pax_buf_destroy(&buffer);
PAX has a small collection of color functions for creating and modifying colors.
For simple colors, pax_col_rgb
or pax_col_argb
are usually the appropriate functions:
pax_col_t color_0 = pax_col_argb(alpha, red, green, blue);
pax_col_t color_1 = pax_col_rgb (red, green, blue);
You can also use pax_col_hsv
or pax_col_ahsv
to convert HSV to RGB:
pax_col_t color_2 = pax_col_ahsv(alpha, hue, saturation, brightness);
pax_col_t color_3 = pax_col_hsv (hue, saturation, brightness);
Otherwise, you can specify it in hexadecimal with the 0xAARRGGBB
format:
pax_col_t color_4 = 0xff7f3f1f;
Finally, there's functions for merging colors.
In PAX, you can opt to draw shapes in different ways:
- Simple (ignoring transforms and with a color)
- Normal (with a color or with an image)
- Outline (the outline of a shape)
- Shaded (with a shader for some specific look, for advanced users)
What variant is best for you?
- You want to draw a line of some form around something?
- Outline.
- You want to draw a simple menu with just some text and basic shapes?
- Simple (you probably won't need to use transformations, but you could).
- You want to add something more complex than a single color?
- Shaded (you can even make your own shader).
- You want to draw an image?
- Normal.
- Anything else with just one color per shape?
- Normal.
There are five basic shapes you can draw:
shape | simple | normal | outline | with shader |
---|---|---|---|---|
rectangle | pax_simple_rect |
pax_draw_rect |
pax_outline_rect |
pax_shade_rect |
line | pax_simple_line |
pax_draw_line |
Not available. | Not available. |
triangle | pax_simple_tri |
pax_draw_tri |
pax_outline_tri |
pax_shade_tri |
circle | pax_simple_circle |
pax_draw_circle |
pax_outline_circle |
pax_shade_circle |
arc | pax_simple_arc |
pax_draw_arc |
pax_outline_arc |
pax_shade_arc |
Each method here consists of a set of arguments based on the shape:
shape | arguments | description |
---|---|---|
rectangle | x, y, width, height |
top left corner of rectangle |
line | x0, y0, x1, y1 |
both points that define the line |
triangle | x0, y0, x1, y1, x2, y2 |
all three points that define the triangle |
circle | x, y, radius |
midpoint of circle and radius |
arc | x, y, radius, angle0, angle1 |
arc from angle0 to angle1 in radians, with midpoint and radius (angles start to the right and go counterclockwise) |
You can also draw images:
name | arguments | description |
---|---|---|
pax_draw_image | image, x, y | Draws an image at the image's normal size. |
pax_draw_image_sized | image, x, y, width, height | Draw an image with a prespecified size. |
If the method is one of the pax_shade_
methods, two additional arguments are added after color
:
pax_shader_t *shader, pax_quad_t *uvs
(Except triangle)- Circles and arcs behave as though they are a cutout of a rectangle (in terms of UVs).
- Rectangle UVs 0 -> 3 are the corners: top left, top right, bottom right, bottom left.
pax_shader_t *shader, pax_tri_t *uvs
(Triangle only)- Triangles define their UVs per point.
UVs are "texture co-ordinates" in the computer graphics world. They are floating-point and range from 0 to 1. It is up to the shader to turn these into pixel co-ordinates for e.g. adding a texture to a shape.
Note: It is acceptable for a rectangle to have a negative width and/or height.
In PAX, you can use different fonts for text (even though there's only one font called "7x9" for now).
You draw text by using pax_draw_text
:
// Draw some text.
pax_draw_text(&buffer, color, font, font_size, x, y, text);
Font is usually PAX_FONT_DEFAULT
or another PAX_FONT_
.
The font_size
is the line height: 9 by default for the font "7x9".
If this number is 0, the font's default size is used.
You can also calculate the size of a given string with pax_text_size
:
// Get the size of some text.
pax_vec1_t size = pax_text_size(font, font_size, text);
float text_width = size.x;
float text_height = size.y;
The font_size is the same as the line height for drawing text.
The text features also respect newlines (in the forms of \n
, \r\n
or \r
).
The flagship features of PAX are also the more difficult to use, so here's a basic overview:
- There is a clipping system, used to isolate drawing to just a small part of the screen,
- There are matrix transformations, used to distort and move around everything you draw,
- Finally, there are shaders, used to add an image to a shape or for more complex coloring of shapes.
When you apply clipping, PAX acts as if the buffer is smaller than it might in reality be.
Clipping can be useful if you want to e.g. draw a big shape but prevent it from overlapping with another area.
Clipping redefines the rectangle in which you can draw.
To apply clipping, use pax_clip
:
// Apply clipping. Automatically fixes negative width and/or height.
pax_clip(&buffer, x, y, width, height);
To remove clipping (AKA be able to draw on the entire buffer again), use pax_noclip
:
// Remove clipping.
pax_noclip(&buffer);
Imagine this: You just painstakingly created some vector graphics, but they're too big to fit.
You could re-do all that work, or you could take advantage of transformations.
In PAX, transformations apply to all method starting with pax_draw_
or pax_shade_
.
They can be used to re-size, rotate and move shapes around.
First, you need to know what a stack is.
In simple terms, a stack is like a bunch of paper in a box:
You can grab only the top piece of paper, and you can only add paper to the top.
This is like method calls in C: The method you call won't modify your variables unless you want it to.
Let's start with a visual example.
Here, you see an initial situation: a scale matrix has been applied.
Let's say we want to do something else, but we need this matrix later.
To do this, you'll need to push the matrix stack.
Now, to push the stack, use pax_push_2d
:
// Push the matrix stack. Duplicates the top matrix.
pax_push_2d(&buffer);
At this point, it's safe to perform some operation. Let's say a rotation happened.
But what if you want no transformation but still to remember the old ones?
You use pax_reset_2d
with the argument PAX_RESET_TOP
:
// Reset the top matrix.
// A matrix that represents no transformation is also called the 'identity' matrix.
pax_push_2d(&buffer, PAX_RESET_TOP);
Finally, you want to use the initial matrix again.
For this, you need to pop the stack.
Use pax_pop_2d
:
// Pop the matrix stack.
pax_pop_2d(&buffer);
Now that you know what the stack is,
you can use pax_apply_2d
to perform a transformation:
// You can do as many in a row as you'd like.
pax_apply_2d(&buffer, my_perfect_transformation);
Let's say you'd like to change the scale of the vector graphics you made.
For this, you use matrix_2d_scale
:
pax_apply_2d(&buffer, matrix_2d_scale(x_scale, y_scale));
// Draw your things that need to be scaled here.
But now, you decide you want it placed elsewhere.
To move around things, use matrix_2d_translate
:
// By scaling, you are multiplying the size by the given factors.
pax_apply_2d(&buffer, matrix_2d_translate(x_offset, y_offset));
There's also the fancy rotation matrix matrix_2d_rotate
:
// Angle is in radians, positive angles rotate everything counterclockwise.
pax_apply_2d(&buffer, matrix_2d_rotate(angle));
The shader is PAX' most advanced feature.
Internally, they are used for boring things like drawing text, but you can truly turn it into anything you'd like.
The way most users will see shaders is with PAX_SHADER_TEXTURE
:
pax_shader_t my_texture_shader = PAX_SHADER_TEXTURE(&my_texture);
You can also make your own shaders, for example one that shows some nice rainbows:
// The shader callback.
pax_col_t my_shader_callback(pax_col_t tint, int x, int y, float u, float v, void *args) {
return pax_col_hsv(x / 50.0 * 255.0 + y / 150.0 * 255.0, 255, 255);
}
// The shader object.
pax_shader_t my_shader = {
.callback = my_shader,
.callback_args = NULL,
.alpha_promise_0 = false,
.alpha_promise_255 = true
};
The callback_args
property is passed directly to the selected callback as the args
parameter.
For more information on how to make and use your own shaders, and how the alpha_promise_
attributes work, see shaders.md.