PicoSystem has a 240x240 resolution screen and can either run in full resolution or a pixel doubled mode.
The decision of which resolution to use is both aesthetically and limitation driven - you may want the retro look of larger pixels, or need the extra RAM saved by having a smaller framebuffer, or need lots of cycles per pixel to calculate visual effects.
|Good for||Vector art||Pixel art|
|Colour||12bits / 4,096 colours||12bits / 4,096 colours|
|Alpha||4bits / 16 levels||4bits / 16 levels|
|Cycles per pixel @ 40Hz||~110||~450|
Our graphics API takes inspiration from many sources and adds a few pinches of our own inspiration into the mix. The end result is both familiar but has its own character.
Each pixel (either on screen or in a buffer like a spritesheet) is represented by a 16-bit value into which the three colour channels + alpha are packed with each occupying 4 bits. A 4 bit value is also known as a "nibble" and can represent numbers between 0 and 15 - the same as a single hex digit.
Because the colour and alpha channels are 4-bit all colour values passed into and returned by API functions are between 0 and 15!
For example the colour value
0xff00 is pure red with full alpha. The API has helper functions like
pen(r, g, b, [a]) which will return the packed colour value but you can manipulate the colour values manually if you prefer.
This pixel format provides a palette of 4,096 different colours on screen - the same as the Original Chip Set (OCS) Amiga!
The screen coordinates start at
0,0 in the top left corner and increase across and down the display. For the framebuffer the dimensions are always either
240x240 to match the current resolution:
The PicoSystem API creates a buffer which contains the pixel data that will be sent to the screen. This buffer has the special name
SCREEN and contains information about the current resolution and a pointer to the start of the pixel data.
The frame buffer consists of a grid of pixels starting in the top left corner of the screen. The first entry in the frame buffer is the top-left corner and subsequent entries travel along the screen from left to right. Once the end of the first row of pixels is reached the next entry will be the first pixel on the next row.
The graphics API includes a few functions for setting and resetting the current drawing "state". This includes things like setting the pen colour, changing the global alpha blending value, and defining a clipping rectangle.
Once you've set a property of the drawing state all future drawing operations will adhere to it. For example if you set the pen to a red colour then all shapes drawn afterwards will be red until you set the pen to a different colour.
At the start of each cycle of the game loop the state (except for the current spritesheet and font) is reset to default values!
The following state properties exist:
All state methods can be called with no parameters to reset the state to default values. For example:
// set pen to a red colour pen(rgb(15, 0, 0)); // circle will be drawn in red circle(10, 10, 5); // reset the pen back to default (white) pen();
The pen sets the colour that will be used for all drawing operations. Colours are constructed from four channels; red, green, blue, and alpha - each channel can have a value from
15 (4 bits per channel).
Pens can be created an stored for later use, or passed directly into the
pen() state setting function.
// opaque red colour_t red = rgb(15, 0, 0); // transparent bright green colour_t green_glass = rgb(15, 0, 0, 4); // set pen colour directly to blue pen(0, 0, 15); // set pen colour directly to transparent purple pen(8, 0, 8, 10); // set previously created pen pen(red);
Sometimes it can be easier to think about colours in relation to hue, saturation, and brightness. We provide a helper function to create pens based on HSV values.
hsv() is different in that it takes values from
1 for each parameter as floating point values instead of the
15 that all other colour functions use.
// orange create via hsv() function colour_t orange = hsv(0.2f, 1.0f, 1.0f);
PicoSystem is a bit special in that it supports 4-bits of alpha blending within its pixel format. Historically microcontrollers aren't really fast enough to perform complicated operations like alpha blending on a per pixel basis but the RP2040 both runs at a high clock speed and has a high speed memory bus allowing for very efficient manipulation of pixel data.
The way the alpha channel information is used depends on the current blend mode but generally allows one drawing operation to overlap another while letting through part of the original information. A good example of this would be looking through stained glass where you can see what's behind it, but the coloured glass tints your view.
The global alpha value is multiplied by the source alpha when performing drawing operations. It allows you to change the opacity of a sprite when blitting.
// set alpha to 50% alpha(8); // sprite at index 4 in the spritesheet will be drawn at 50% opacity sprite(4, 10, 10); // reset global alpha back to default (100%) alpha();
All drawing operations can be constrained to a clipping rectangle. In fact all drawing operations are clipped, it's just that the default clipping rectangle is the same size as the framebuffer.
// set the clipping rectangle to a small square // in the top left corner clip(10, 10, 50, 50); // write text into the box with any excess being clipped text("This is a long message that won't fit", 10, 10);
Clipping can be helpful to ensure that drawing doesn't bleed into areas that don't want to be affected. If, for example, the top 10% of the screen is being used to show the current score and player's health bar then you could clip that area out to ensure that when you draw the background of the play area that it doesn't overlap.
TODO: Write this section up!
Sets the target buffer for drawing operations to happen on.
All drawing operations (for example
pixel(x, y)) will act upon the currently selected target buffer which by default is
SCREEN (i.e. the framebuffer).
This can be useful when you want to draw into an off-screen buffer and then copy the result over to the framebuffer.
Offsets all future drawing operations by the camera coordinates. Useful for following the player character around and not having to manually offset all draw calls.
Set the spritesheet used as the source for calls to
sprite(). Spritesheets must be a multiple of 8 pixels wide and high because individual sprites are 8x8 pixels. Each sprite has an index in the spritesheet. The indices start at
0 and increase across and down the spritesheet.
For example in this
32x32 sprite sheet (4x4 sprites) the indices are:
PicoSystem comes with a default spritesheet which contains 256 (16x16 sprites) different icons, symbols, and glyphs:
This collection of handy icons and graphics was created for us by @s4m_ur4i - check out more of his work at https://s4m-ur4i.itch.io/! They are free for you to use in your own PicoSystem projects - have fun with them!
To draw sprites we work out the index of it in the spritesheet and where we want to put it on the screen.
// draw the third sprite from the default spritesheet (banana) at 10, 10 sprite(3, 10, 10);
The cursor is the location that
text() calls will print to if coordinates are not specified. The cursor position is automatically moved when drawing text so that subsequent
text() follow on from each other.
At the end of a line of text the cursor will move to the start of the next line, the following code will output three lines of text one after another.
text("a crash reduces"); text("your expensive computer"); text("to a simple stone.");
TODO: Write this section up!