Bedtime. /Graphics API

Graphics API

Drawing primitives for the 400x240 monochrome display

Every call takes the gfx_t* your redraw callback was handed. There is no cursor and no transform stack: each call takes absolute screen coordinates and paints in the current draw colour.

Color model

The panel is 1-bit and the frame starts cleared to black, so drawing means turning pixels on. gfx_set_color() selects what a draw call does to the pixels it touches:

The colour is sticky: it persists across calls and into the next frame. If you change it, set it back to 1 before you return.

Coordinate system

Origin (0,0) is the top-left pixel, x grows right, y grows down, and the screen is SCREEN_WIDTH x SCREEN_HEIGHT = 400x240. Coordinates are gfx_uint_t (uint16_t) and every primitive clips against the display and the clip window, so drawing partly off-screen is safe.

Dithering — grey on a 1-bit panel

The _dithered calls paint an ordered 8x8 Bayer pattern in the current colour instead of a solid fill, which the eye reads as grey. shade is 0-7 and picks how many pixels get painted:

shade01234567
pixels set6%12%25%37%50%62%75%87%

In the usual colour 1 that runs from nearly black to nearly white. The pattern is anchored to screen coordinates rather than to the shape, so neighbouring fills of the same shade line up seamlessly.

A minimal redraw

static void my_redraw(gfx_t* gfx, const os_app_t* app) {
    my_model_t* m = app->data->model;

    gfx_set_font(gfx, gfx_nunito_bold_18);
    gfx_draw_str(gfx, 20, 40, "hello");

    gfx_fill_rect_dithered(gfx, 20, 60, 360, 20, 3);  // grey bar
    gfx_draw_rect_r(gfx, 20, 60, 360, 20, 4);         // outline over it

    gfx_set_color(gfx, 2);                            // XOR the playhead in
    gfx_draw_vline(gfx, 20 + m->cursor, 60, 20);
    gfx_set_color(gfx, 1);                            // leave it as you found it
}
void gfx_draw_pixel(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y)

Paint a single pixel in the current colour

gfx_t*gfxGraphics context
gfx_uint_txX coordinate
gfx_uint_tyY coordinate
void gfx_draw_hline(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w)

Horizontal line running right from (x, y)

gfx_t*gfxGraphics context
gfx_uint_txLeft end
gfx_uint_tyRow
gfx_uint_twLength in pixels

Note — This is the fastest primitive there is — it writes whole framebuffer bytes at a time. Building a fill out of hlines beats building it out of pixels by a wide margin.

void gfx_draw_vline(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t h)

Vertical line running down from (x, y)

gfx_t*gfxGraphics context
gfx_uint_txColumn
gfx_uint_tyTop end
gfx_uint_thLength in pixels
void gfx_draw_hvline(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t len, uint8_t dir)

Axis-aligned line in one of four directions

gfx_t*gfxGraphics context
gfx_uint_txStart X
gfx_uint_tyStart Y
gfx_uint_tlenLength in pixels
uint8_tdir0 = right, 1 = down, 2 = left, 3 = up

Note — `dir` 0 and 1 are gfx_draw_hline() / gfx_draw_vline() exactly; 2 and 3 are the same lines drawn backwards from (x, y). Call the hline/vline pair directly unless the direction is a runtime value.

void gfx_draw_line(gfx_t* gfx, gfx_uint_t x1, gfx_uint_t y1, gfx_uint_t x2, gfx_uint_t y2)

Line between two arbitrary points (Bresenham)

gfx_t*gfxGraphics context
gfx_uint_tx1First point X
gfx_uint_ty1First point Y
gfx_uint_tx2Second point X
gfx_uint_ty2Second point Y
void gfx_draw_line_thick(gfx_t* gfx, gfx_uint_t x1, gfx_uint_t y1, gfx_uint_t x2, gfx_uint_t y2, uint16_t thickness)

Line with a thickness, drawn as a stack of parallel lines

gfx_t*gfxGraphics context
gfx_uint_tx1First point X
gfx_uint_ty1First point Y
gfx_uint_tx2Second point X
gfx_uint_ty2Second point Y
uint16_tthicknessWidth in pixels; 1 is gfx_draw_line()
void gfx_draw_bezier(gfx_t* gfx, gfx_uint_t x0, gfx_uint_t y0, gfx_uint_t x1, gfx_uint_t y1, gfx_uint_t x2, gfx_uint_t y2)

Quadratic Bezier curve through 16 straight segments

gfx_t*gfxGraphics context
gfx_uint_tx0Start X
gfx_uint_ty0Start Y
gfx_uint_tx1Control point X — the curve is pulled toward it but does not touch it
gfx_uint_ty1Control point Y
gfx_uint_tx2End X
gfx_uint_ty2End Y
void gfx_draw_rect(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h)

Rectangle outline

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twWidth in pixels
gfx_uint_thHeight in pixels
void gfx_draw_rect_r(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h, gfx_uint_t r)

Rectangle outline with rounded corners

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twWidth in pixels
gfx_uint_thHeight in pixels
gfx_uint_trCorner radius; must not exceed half the shorter side
void gfx_draw_rect_fill(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h)

Solid rectangle

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twWidth in pixels
gfx_uint_thHeight in pixels

Note — Filling with colour 0 is how you erase a region before redrawing it.

void gfx_draw_rect_fill_r(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h, gfx_uint_t r)

Solid rectangle with rounded corners

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twWidth in pixels
gfx_uint_thHeight in pixels
gfx_uint_trCorner radius; must not exceed half the shorter side
void gfx_fill_rect_dithered(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h, uint8_t shade)

Rectangle filled with a dither pattern instead of solid colour

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twWidth in pixels
gfx_uint_thHeight in pixels
uint8_tshade0-7, see gfx_shades — 0 is the sparsest, 7 the densest
void gfx_draw_hline_dithered(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, uint8_t shade)

Dithered horizontal line

gfx_t*gfxGraphics context
gfx_uint_txLeft end
gfx_uint_tyRow
gfx_uint_twLength in pixels
uint8_tshade0-7, see gfx_shades
void gfx_draw_vline_dithered(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t h, uint8_t shade)

Dithered vertical line

gfx_t*gfxGraphics context
gfx_uint_txColumn
gfx_uint_tyTop end
gfx_uint_thLength in pixels
uint8_tshade0-7, see gfx_shades
void gfx_fill_circle_dithered(gfx_t* gfx, gfx_uint_t cx, gfx_uint_t cy, gfx_uint_t r, uint8_t shade)

Dithered filled circle

gfx_t*gfxGraphics context
gfx_uint_tcxCentre X
gfx_uint_tcyCentre Y
gfx_uint_trRadius in pixels
uint8_tshade0-7, see gfx_shades
void gfx_fill_rect_r_dithered(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h, gfx_uint_t r, uint8_t shade)

Dithered rounded rectangle

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twWidth in pixels
gfx_uint_thHeight in pixels
gfx_uint_trCorner radius
uint8_tshade0-7, see gfx_shades
void gfx_fill_rect_gradient_v_dithered_bayer(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h, uint8_t shade_top, uint8_t shade_bottom)

Rectangle with a vertical dither gradient

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twWidth in pixels
gfx_uint_thHeight in pixels
uint8_tshade_topShade at the top row, 0-7
uint8_tshade_bottomShade at the bottom row, 0-7

Note — Equal top and bottom shades give a flat fill, so this doubles as gfx_fill_rect_dithered() when you are animating a gradient in and out.

void gfx_fill_rect_gradient_h_dithered_bayer(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h, uint8_t shade_left, uint8_t shade_right)

Rectangle with a horizontal dither gradient

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twWidth in pixels
gfx_uint_thHeight in pixels
uint8_tshade_leftShade at the left column, 0-7
uint8_tshade_rightShade at the right column, 0-7
void gfx_draw_circle(gfx_t* gfx, gfx_uint_t x0, gfx_uint_t y0, gfx_uint_t r, uint8_t opt)

Circle outline, whole or by quadrant

gfx_t*gfxGraphics context
gfx_uint_tx0Centre X
gfx_uint_ty0Centre Y
gfx_uint_trRadius in pixels
uint8_toptQuadrant mask — GFX_DRAW_ALL, or an OR of the GFX_DRAW_* family

Note — GFX_DRAW_ALL draws full circle and matches GFX_DRAW_UPPER_LEFT | GFX_DRAW_UPPER_RIGHT | GFX_DRAW_LOWER_RIGHT | GFX_DRAW_LOWER_LEFT

void gfx_draw_disc(gfx_t* gfx, gfx_uint_t x0, gfx_uint_t y0, gfx_uint_t r, uint8_t opt)

Filled circle, whole or by quadrant

gfx_t*gfxGraphics context
gfx_uint_tx0Centre X
gfx_uint_ty0Centre Y
gfx_uint_trRadius in pixels
uint8_toptQuadrant mask — GFX_DRAW_ALL, or an OR of the GFX_DRAW_UPPER_RIGHT family

Note — GFX_DRAW_ALL draws full circle and matches GFX_DRAW_UPPER_LEFT | GFX_DRAW_UPPER_RIGHT | GFX_DRAW_LOWER_RIGHT | GFX_DRAW_LOWER_LEFT

void gfx_draw_circle_dotted(gfx_t* gfx, gfx_uint_t x0, gfx_uint_t y0, gfx_uint_t r, uint8_t opt)

Circle outline drawn as a dotted ring

gfx_t*gfxGraphics context
gfx_uint_tx0Centre X
gfx_uint_ty0Centre Y
gfx_uint_trRadius in pixels
uint8_toptQuadrant mask — GFX_DRAW_ALL, or an OR of the GFX_DRAW_UPPER_RIGHT family

Note — GFX_DRAW_ALL draws full circle and matches GFX_DRAW_UPPER_LEFT | GFX_DRAW_UPPER_RIGHT | GFX_DRAW_LOWER_RIGHT | GFX_DRAW_LOWER_LEFT

void gfx_set_color(gfx_t* gfx, uint8_t color)

Select what subsequent draw calls do to the pixels they touch

gfx_t*gfxGraphics context
uint8_tcolor0 = clear (black), 1 = set (white), 2 = XOR

Note — Sticky — it survives into the next frame. See gfx_color.

gfx_uint_t gfx_draw_str(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, const char* str)

Draw a string in the current font and colour

gfx_t*gfxGraphics context
gfx_uint_txLeft edge of the first glyph
gfx_uint_tyText baseline — glyphs sit *above* this row, so y is the bottom of the line
const char*strNUL-terminated ASCII/UTF-8 string

Returns — Width drawn, in pixels — add it to x to continue on the same line

gfx_uint_t gfx_draw_strf(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, const char* fmt, ...)

printf-style gfx_draw_str()

gfx_t*gfxGraphics context
gfx_uint_txLeft edge of the first glyph
gfx_uint_tyText baseline
const char*fmtprintf format string
...

Returns — Width drawn, in pixels

Note — `%f` costs far more than the integer conversions. For a value you redraw every frame, scale to an int yourself: `gfx_draw_strf(gfx, x, y, "%d%%", (int)(v * 100))`.

Warning — The formatted result is built in a shared 128-byte buffer and truncated to fit. Do not call it from an audio or timer callback while the UI thread may also be drawing.

uint16_t gfx_draw_glyph(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, uint16_t encoding)

Draw one glyph by character code

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyBaseline
uint16_tencodingCharacter code (ASCII, or a Unicode code point in a font that has it)

Returns — Advance width of that glyph in pixels

void gfx_set_font(gfx_t* gfx, const uint8_t* font)

Select the font for subsequent text calls

gfx_t*gfxGraphics context
const uint8_t*fontOne of the exported font blobs — gfx_nunito_semibold_14, gfx_nunito_bold_18 or DepartureMono_Regular_10

Note — Sticky, like the colour. Setting a font is cheap; measuring text is not, so hoist gfx_get_str_width() out of loops rather than the gfx_set_font() call.

gfx_uint_t gfx_get_str_width(gfx_t* gfx, const char* str)

Measure a string in the current font without drawing it

gfx_t*gfxGraphics context
const char*strNUL-terminated string

Returns — Width in pixels

// centre a label
const gfx_uint_t w = gfx_get_str_width(gfx, label);
gfx_draw_str(gfx, SCREEN_CENTER_X - (w >> 1), 120, label);
void gfx_draw_xbm(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h, const uint8_t* bitmap)

Blit a 1-bit XBM bitmap

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
gfx_uint_twBitmap width in pixels
gfx_uint_thBitmap height in pixels
const uint8_t*bitmapXBM data: one bit per pixel, LSB first, each row padded to a whole byte

Note — The blit is opaque — 0 bits are painted in the opposite colour, so the bitmap's bounding box is fully overwritten. Prefer ui_draw_img(), which takes a gfx_img_t from the asset pipeline and knows its own dimensions.

void gfx_draw_xbm_rotated(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, gfx_uint_t w, gfx_uint_t h, const uint8_t* bitmap, float angle)

Blit a bitmap rotated about its own centre

gfx_t*gfxGraphics context
gfx_uint_txLeft edge of the unrotated bounding box
gfx_uint_tyTop edge of the unrotated bounding box
gfx_uint_twBitmap width in pixels
gfx_uint_thBitmap height in pixels
const uint8_t*bitmapXBM data
floatangleRotation in radians, clockwise

Note — Per-pixel and transparent, unlike gfx_draw_xbm(): only set bits are painted, and corners can spill outside the original w x h box.

Warning — This variant reads the bitmap MSB first — the opposite of gfx_draw_xbm() and gfx_draw_xbm_scaled(). Asset-pipeline bitmaps come out mirrored within each byte, so check on device before you build a UI on it.

void gfx_draw_xbm_scaled(gfx_t* gfx, gfx_uint_t x, gfx_uint_t y, const uint8_t* bitmap, gfx_uint_t w, gfx_uint_t h, gfx_uint_t scale)

Blit a bitmap shrunk by an integer factor

gfx_t*gfxGraphics context
gfx_uint_txLeft edge
gfx_uint_tyTop edge
const uint8_t*bitmapXBM data — note this comes *before* the dimensions here, unlike gfx_draw_xbm()
gfx_uint_twSource bitmap width in pixels
gfx_uint_thSource bitmap height in pixels
gfx_uint_tscaleDivisor: 2 draws at half size, 3 at a third. 1 is 1:1.

Warning — `scale` shrinks, it does not magnify — there is no scale-up entry point in the TAPP API. Sampling is nearest-neighbour, so thin 1px features can vanish entirely.

void gfx_set_clip(gfx_t* gfx, gfx_uint_t x0, gfx_uint_t y0, gfx_uint_t x1, gfx_uint_t y1)

Restrict drawing to a rectangular window

gfx_t*gfxGraphics context
gfx_uint_tx0Left edge of the window
gfx_uint_ty0Top edge of the window
gfx_uint_tx1Right edge of the window (a coordinate, not a width)
gfx_uint_ty1Bottom edge of the window (a coordinate, not a height)
gfx_set_clip(gfx, 10, 40, 390, 200);   // scrolling list body
draw_rows(gfx, m);
gfx_clip_reset(gfx);

Note — Corner-to-corner, not x/y/w/h. The window is sticky and applies to every primitive, so a forgotten clip makes the rest of your UI silently disappear — always pair it with gfx_clip_reset().

void gfx_clip_reset(gfx_t* gfx)

Drop the clip window and draw to the whole screen again

gfx_t*gfxGraphics context
uint32_t ui_get_frame(gfx_t* gfx)

Get the current frame counter (for animations)

gfx_t*gfxGraphics context

Returns — Frame counter value, incremented on each display refresh

Note — This is the clock to drive animation from — a tapp has no wall time. Divide it down for slower motion: `ui_draw_anim(gfx, x, y, ui_get_frame(gfx) >> 2, &my_anim)`.

extern const uint8_t gfx_nunito_semibold_14[]

Default UI body font

extern const uint8_t gfx_nunito_bold_18[]

Headings / emphasis

extern const uint8_t DepartureMono_Regular_10[]

Monospace — counters, numeric readouts