Raw graphics
picogame’s C module is also a plain drawing library. A Canvas draws shapes, bitmaps and text
into any RGB565 buffer, and a Bitmap wraps any buffer of pixels without copying it. Sprites and
Scene are built on top of that, and you do not have to use them.
Two kinds of screen
Section titled “Two kinds of screen”How you draw depends on where the screen’s pixels live.
- Framebuffer boards (Fruit Jam and other DVI boards): the whole screen is a buffer in RAM.
Put a
Canvason it and draw, the way you would in SDL or Pygame. Everything below the next heading is for these boards. - SPI panels (PicoPad and most boards with a small display): the pixels live in the panel,
and there is no room for a copy - a 320×240 RGB565 frame is 150 KB, and an RP2040 game has
25-40 KB of free RAM. picogame never keeps a whole frame there; it renders a few rows at a time
and sends them to the panel. You still draw in immediate mode, but inside a
StripDrawcallback that runs once per strip.
Open the screen
Section titled “Open the screen”import boardimport displayioimport picodviimport picogame as pg
displayio.release_displays()framebuffer = picodvi.Framebuffer(320, 240, color_depth=16, clk_dp=board.CKP, clk_dn=board.CKN, red_dp=board.D0P, red_dn=board.D0N, green_dp=board.D1P, green_dn=board.D1N, blue_dp=board.D2P, blue_dn=board.D2N)screen = pg.Canvas(320, 240, buffer=framebuffer)
def color(r, g, b): """RGB565 in the byte order picodvi reads.""" return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)screen now draws into the memory the DVI output is showing. There is no refresh call: a pixel
you write is on the screen.
Use color() for every colour on this screen, palettes included. pg.rgb565() returns the byte
order SPI panels want, so on a DVI screen its red comes out blue.
Shapes
Section titled “Shapes”screen.clear(color(0, 0, 40))screen.fill_rect(200, 10, 50, 30, color(255, 0, 0))screen.fill_circle(290, 25, 15, color(0, 255, 0))screen.line(200, 50, 310, 50, color(255, 255, 255))A Canvas also has pixel, rect, circle, ring, triangle, fill_triangle, ellipse,
fill_ellipse, fill_round_rect and frame3d, plus batch calls for many triangles or spans at
once. The full list is in the reference.
For single pixels you can also write the buffer directly: memoryview(framebuffer) has one
item per pixel, so pixels[y * 320 + x] = color(255, 255, 255).
Images from a file
Section titled “Images from a file”adafruit_imageload reads the file. pg.Bitmap then uses the same pixels, so only the palette
is converted:
import arrayimport adafruit_imageload
def load_image(path): """Load an 8-bit (indexed) BMP as a picogame Bitmap.""" bitmap, palette = adafruit_imageload.load( path, bitmap=lambda width, height, colors: displayio.Bitmap(width, height, 256), palette=displayio.Palette) colors = array.array("H", [0] * 256) for index in range(len(palette)): rgb = palette[index] colors[index] = color(rgb >> 16, (rgb >> 8) & 0xFF, rgb & 0xFF) pixels = memoryview(bitmap) return pg.Bitmap(pixels, bitmap.width, bitmap.height, format=pg.PAL8, palette=colors, stride=len(pixels) // bitmap.height)
picture = load_image("/picture.bmp")screen.blit(picture, 10, 10)Save your image as an 8-bit (256-colour) BMP. Most image editors can do that; with Pillow it is
Image.open("picture.png").convert("RGB").quantize(256).save("picture.bmp").
blit(bitmap, x, y, frame=0, flip_x=False, flip_y=False) draws a bitmap at any position,
clipped to the canvas. To leave one palette colour see-through, pass its index as
transparent= when you make the pg.Bitmap.
The built-in font
Section titled “The built-in font”import terminalio
screen.text(200, 60, "Hello, DVI", color(255, 255, 255), terminalio.FONT)screen.text(200, 76, "on a box", color(0, 0, 0), terminalio.FONT, color(255, 200, 0))The arguments are x, y, text, color, font, and an optional background colour as the sixth
argument. x, y is the top-left corner. terminalio.FONT is 6×12 pixels per character. This
C path only takes the built-in font.
Your own font (BDF or PCF)
Section titled “Your own font (BDF or PCF)”For any other font, adafruit_bitmap_font loads the glyphs and each one becomes a small
Bitmap you blit:
from adafruit_bitmap_font import bitmap_font
font = bitmap_font.load_font("/fonts/myfont.pcf")glyph_cache = {}
def glyph_bitmap(glyph, text_color): """Turn one font glyph into a picogame Bitmap with a transparent background.""" width, height = glyph.width, glyph.height pixels = bytearray(width * height) for y in range(height): for x in range(width): pixels[y * width + x] = glyph.bitmap[x, y] palette = array.array("H", (0, text_color)) return pg.Bitmap(pixels, width, height, format=pg.PAL8, palette=palette, transparent=0)
def draw_text(canvas, x, y, text, text_color): """Draw text with its baseline at y.""" font.load_glyphs(text) for char in text: glyph = font.get_glyph(ord(char)) if glyph is None: continue if glyph.width and glyph.height: # a space has an empty bitmap key = (char, text_color) if key not in glyph_cache: glyph_cache[key] = glyph_bitmap(glyph, text_color) canvas.blit(glyph_cache[key], x + glyph.dx, y - glyph.height - glyph.dy) x += glyph.shift_x
draw_text(screen, 10, 170, "Proportional text", color(255, 220, 0))This handles proportional fonts: every glyph is placed by its own offset and advance. Each
character is converted once per colour and then comes from glyph_cache.
The same code reads BDF and PCF. Prefer PCF for large fonts: it is a binary format, usually
about half the size of the same BDF, and adafruit_bitmap_font reads only the glyphs you ask
for. bdftopcf (part of the X.Org tools) converts a BDF to PCF.
Off-screen buffers
Section titled “Off-screen buffers”A Canvas can draw into any bytearray, and pg.Bitmap turns that bytearray into something you
can blit:
panel_pixels = bytearray(120 * 40 * 2) # width * height * 2 bytes (RGB565)panel = pg.Canvas(120, 40, buffer=panel_pixels)panel.clear(color(70, 70, 90))panel.rect(0, 0, 120, 40, color(255, 255, 255))panel.text(8, 14, "off-screen", color(255, 255, 255), terminalio.FONT)
panel_bitmap = pg.Bitmap(panel_pixels, 120, 40)screen.blit(panel_bitmap, 190, 190)Use this to build a panel, a sprite or a whole frame once and stamp it many times. It is a plain
bytearray, so your own code - viper, a native module, ulab - can work on the same pixels.
Animation
Section titled “Animation”There is no frame loop to hook into: erase what moved, draw it at its new place, repeat.
import time
x = 0while True: screen.fill_rect(x, 200, 20, 20, color(0, 0, 40)) # erase the old box x = (x + 2) % 160 screen.fill_rect(x, 200, 20, 20, color(0, 200, 255)) # draw the new one time.sleep(1 / 60)You are drawing into the picture that is on the screen, so a large redraw can show up half done. Compose big changes in an off-screen buffer and blit the result in one call.
On an SPI panel: StripDraw
Section titled “On an SPI panel: StripDraw”On an SPI panel, a Scene renders the screen a strip of rows at a time, and a StripDraw layer
calls your function for each strip. Inside it you get a Canvas for that strip and use the same
calls as above:
import boardimport terminalioimport picogame as pg
display = board.DISPLAYWIDTH, HEIGHT = display.width, display.heightSTRIP_HEIGHT = 8scene = pg.Scene(display, bytearray(WIDTH * STRIP_HEIGHT * 2), bytearray(WIDTH * STRIP_HEIGHT * 2))ball_x = 0
def draw(view, strip_x, strip_y, strip_width, strip_height): # view covers screen rows strip_y .. strip_y + strip_height - 1, # so subtract strip_y from every screen y view.clear(pg.rgb565(0, 0, 40)) view.fill_circle(ball_x, HEIGHT // 2 - strip_y, 20, pg.rgb565(0, 255, 0)) view.text(10, 10 - strip_y, "Hello", pg.rgb565(255, 255, 255), terminalio.FONT)
scene.add(pg.StripDraw(draw, 0, 0, WIDTH, HEIGHT))while True: ball_x = (ball_x + 2) % WIDTH scene.refresh()Here colours come from pg.rgb565(), since this is the byte order SPI panels use. Anything that
does not fit in one strip - an image, a long line of text - is simply drawn again, clipped, in
the next strip.
Your function runs for every strip, so keep it to drawing. Work out positions and game state
once per frame, before scene.refresh().
A StripDraw repaints on every refresh, which is right for content that moves. For content that
changes only now and then, create it with always_dirty=False and call its invalidate() after a
change; the rest of the time the scene skips it.
When to use the rest
Section titled “When to use the rest”Everything above is the whole C module seen from below. Scene, Sprite, Tilemap and the
picogame_* helper libraries add redrawing only what changed, collisions, animation, input and
text widgets. How picogame works and
Drawing paths show how they fit together.