Drawing Primitives & Sprites#

Every embedded graphics library provides a core set of drawing primitives — the basic shapes and operations combined to build interfaces. Understanding what’s available and how the underlying algorithms work helps produce efficient display code and debug visual issues when things don’t look right.

Basic Primitives#

The standard set found in virtually every library:

  • PixeldrawPixel(x, y, color) — the atomic operation everything else builds on
  • LinedrawLine(x0, y0, x1, y1, color) — usually Bresenham’s algorithm; watch out for endpoint handling at screen boundaries
  • RectangledrawRect() for outline, fillRect() for solid — the fastest fill operation since it maps directly to row/column writes on most display controllers
  • CircledrawCircle() and fillCircle() — midpoint circle algorithm; filled circles are noticeably slower than filled rectangles
  • TriangledrawTriangle() and fillTriangle() — filled triangles use scanline algorithms and are useful for arrow indicators and custom shapes
  • Rounded rectangledrawRoundRect() — quarter-circle corners connected by straight lines

On most display controllers, fillRect() is special because a column/row address window can be set and stream pixel data directly, which is much faster than setting individual pixels. Prefer rectangles for clearing regions and drawing backgrounds.

Sprites and Bitmaps#

A sprite is a small bitmap image drawn onto the screen at a given position. In embedded graphics, sprites are typically stored as C arrays of pixel data (generated by an image conversion tool) and drawn with a drawBitmap() or pushSprite() function.

Common formats:

  • XBM — X BitMap, a 1-bit (monochrome) format stored as a C array. Simple, widely supported, good for icons on monochrome displays
  • BMP — can be decoded on the fly, but the decoding overhead is non-trivial on small MCUs
  • Raw RGB565 — pre-converted pixel arrays ready to push directly to a color display; fast but large (2 bytes per pixel)

TFT_eSPI and LovyanGFX offer a TFT_eSprite / LGFX_Sprite class that creates an off-screen buffer that provides the same drawing API as the main screen and can be pushed to the display in one operation. This is useful for flicker-free rendering of complex elements — draw everything into the sprite, then blit it to the screen.

Transparency#

True alpha-channel transparency is expensive on MCUs — it requires blending math (multiplies and shifts) for every pixel. Most embedded libraries take shortcuts:

  • Color key transparency: designate one specific color (often magenta 0xF81F in RGB565) as “transparent” — pixels of that color are simply not drawn
  • 1-bit mask: a separate bitmask where 1 = draw, 0 = skip — used for non-rectangular sprite shapes
  • No transparency: just overwrite whatever was there, which is by far the fastest

Smooth edges on sprites over varying backgrounds require LVGL’s rendering engine or a custom blending routine.

Z-Ordering on Small Displays#

On a desktop, the window manager handles which elements appear on top of which. On an MCU display, this is managed by controlling draw order — things drawn last appear on top. For simple UIs, a fixed draw order works: clear background, draw static elements, draw dynamic elements on top. For more complex UIs with overlapping elements, redrawing from back to front whenever anything changes may be necessary, which is where a framebuffer approach (draw everything into a buffer, then flush once) avoids flicker.

Tips#

  • Use fillRect() for clearing regions rather than redrawing individual pixels — it’s significantly faster on most display controllers because it maps to a windowed write
  • For icons and small images, XBM format is the simplest path to a working sprite on monochrome displays — most image editors can export it directly
  • Draw into a sprite buffer (TFT_eSprite, LGFX_Sprite) rather than directly to the screen when rendering complex elements — it eliminates flicker from sequential draw operations

Caveats#

  • Filled circles and triangles are much slower than filled rectangles — Rectangles map to the display controller’s column/row address window; circles and triangles require per-pixel or per-scanline calculations
  • Color-key transparency is fragile — If the actual image content happens to contain the transparency key color (often magenta 0xF81F), those pixels will disappear. Choose a key color that doesn’t appear in the assets
  • Raw RGB565 sprite arrays are large — A 64x64 sprite at 2 bytes per pixel is 8KB. On memory-constrained MCUs, a few sprites can consume a significant fraction of available RAM

In Practice#

  • Flickering during screen updates means draw operations are being sent directly to the display without buffering — use a framebuffer or sprite to compose the frame off-screen and flush once
  • A sprite that appears with a colored border or background where transparency was expected indicates the transparency key color doesn’t match the actual pixel values — verify the exact color value in both the asset and the code
  • Drawing that appears correct but is visibly slow usually means the code is calling drawPixel() in a loop rather than using batch operations like fillRect() or pushImage() — profiling individual draw calls reveals the bottleneck
Page last modified: February 28, 2026