A p5.js-shaped drawing layer for any Adafruit_GFX-based
display — not tied to one board or panel. If you're on the ST77916 QSPI
panel specifically, use the ST77916_QSPI
library's built-in P5/P5Canvas instead; it's faster because it can push
whole rows to the panel over DMA. This library trades that speed for working
unmodified on any Adafruit_GFX subclass: ILI9341, ST7789, ST7735, HX8357,
SSD1351, and so on, on any 32-bit Arduino-supported MCU (ESP32, SAMD, RP2040,
STM32, Teensy...). Plain 8-bit AVR (Uno, Nano, Mega) is a compile-time error
by design — see Caveats for why.
- p5.js vocabulary:
background,stroke/fill,rect/ellipse/circle/triangle/quad, regularpolygon(),arc(),beginShape/vertex/endShape, a full 2D transform stack (translate/rotate/scale/push/pop), Perlinnoise(),random(),map/lerp/dist/smoothstep. - Anti-aliased primitives underneath: Wu lines, round-capped strokes, circles, arcs, scanline polygon fill — shapes stay smooth under rotation and scale, not just axis-aligned.
GFXCanvas565, a private offscreen RGB565 buffer you own, composited onto any real display withdraw()(opaque),drawKeyed()(colour-key transparency), orblendFrom()(canvas-to-canvas alpha blending for HUDs/overlays).- Zero dependency on any specific display driver — only
Adafruit_GFX's base API (drawPixel/drawRGBBitmap). - 4 example sketches: two ports of the ST77916_QSPI demos (adapted to be display-agnostic) plus two written for this library specifically (a Perlin flow field, an analog clock face).
- An off-target test suite (
test/hostcheck/run.sh) that builds and runs the canvas's AA/clipping/compositing logic on your dev machine, no hardware required.
- Draw into an offscreen
GFXCanvas565usingP5(p5.js vocabulary). The canvas is a private RGB565 buffer you own, so drawing can be anti-aliased and blended freely — something most SPI TFT displays don't support directly, since they aren't readable. - Composite the finished canvas onto your real display with
canvas.draw(display, x, y)— this goes through nothing but the baseAdafruit_GFX::drawRGBBitmap(), so it works on any display driver.
Direct-to-display drawing isn't offered: the antialiasing/blending math needs a readable buffer. Always draw into a canvas, then composite once per frame.
Sketch → Include Library → Manage Libraries..., search for GFX_P5, click Install. This also installs the required Adafruit GFX Library dependency automatically.
-
Download this repository as a ZIP (Code → Download ZIP on GitHub).
-
In the Arduino IDE: Sketch → Include Library → Add .ZIP Library..., and select the downloaded file.
Or unzip it directly into your sketchbook's
librariesfolder (~/Documents/Arduino/libraries/on macOS/Linux,Documents\Arduino\libraries\on Windows) — rename the extracted folder toGFX_P5if GitHub added a branch suffix. -
Restart the Arduino IDE.
- Adafruit GFX Library — install via Library Manager, this library builds on top of it.
- Whatever Adafruit_GFX-based driver matches your actual display (e.g.
Adafruit_ILI9341,Adafruit_ST7789) — install via Library Manager and swap it into the examples.
#include <Adafruit_ILI9341.h> // or any other Adafruit_GFX display driver
#include <GFXCanvas565.h>
#include <P5.h>
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);
GFXCanvas565 canvas(240, 320);
P5 p5(canvas);
void setup() {
tft.begin();
canvas.begin();
}
void loop() {
p5.background(P5_BLACK);
p5.noStroke();
p5.fill(P5_ORANGE);
p5.circle(120, 160, 80);
canvas.draw(tft, 0, 0);
}Check canvas.begin()'s return value in real code — it can fail if there
isn't enough free heap for a buffer that size (width * height * 2 bytes).
Open via File → Examples → GFX_P5 in the Arduino IDE. All take a display driver header and three pins at the top — swap those two lines for whatever panel you actually have wired up, everything else stays the same.
| Example | What it shows |
|---|---|
BasicShapes |
Minimal bring-up sketch: shapes, transforms, a rotating polygon. Start here on new hardware. |
HUDOverlay |
Two canvases: a full-screen scene redrawn every frame, and a small HUD canvas composited on top via blendFrom() only when its content changes. |
FlowField |
Perlin-noise flow field — a grid of particles nudged by noise(), leaving trails via beginShape/vertex-style line drawing. |
AnalogClock |
A practical UI sketch, not generative art: an analog clock face using push/translate/rotate for the hands, redrawn only on each second tick. |
NeonSpirograph |
Animated hypotrochoid (Spirograph) curve with a real per-pixel alpha-blended motion trail (blendFrom() at low alpha, not a redraw-dimmer trick) and a two-pass halo/core neon-glow line style. |
RoseWindow |
Static stained-glass mandala drawn once in setup() — every shape primitive the library has, composed via push/translate/rotate radial-symmetry loops. Proof a sketch doesn't need to animate to be a centrepiece. |
canvas.draw(display, x, y)— opaque blit.canvas.drawKeyed(display, x, y, keyColor)— skips pixels equal tokeyColor, for overlaying a HUD/sprite on top of whatever's already on the display.canvas.blendFrom(otherCanvas, x, y, alpha)— alpha-blend one canvas onto another before pushing to the display (true blending needs a readable destination, which the display itself can't offer generically).
All three are synchronous — there's no double-buffer/push-task layer to
configure here (unlike ST77916_QSPI's Config{doubleBuffer,pushTask} +
present()): call draw() and it's on screen when it returns.
drawKeyed() and blendFrom() both take fast paths when they can do so
without changing the result: drawKeyed() batches contiguous non-key runs
per row into a single drawRGBBitmap() call instead of one drawPixel()
per pixel; blendFrom() uses a per-row memcpy() instead of per-pixel
blending when alpha >= 252 and the destination region is fully inside the
current clip rect (at that alpha, blending already collapses to a plain
copy). fillScreen() also takes a memset() path when the fill colour's
high and low bytes match (true for P5_BLACK/P5_WHITE, the common case).
None of these change what ends up in the buffer — only how fast it gets
there.
p5.frameStats() tracks how long each loop() iteration is actually
taking, so you can tell whether a sketch is outgrowing its MCU+display
combo instead of just guessing:
void setup() {
...
p5.frameBudget(30); // optional: flag frames slower than 1000/30 ms
}
void loop() {
p5.background(P5_BLACK);
// ... draw ...
canvas.draw(tft, 0, 0);
const P5FrameStats &fs = p5.frameStats(); // call once per loop()
if (fs.overBudget) {
// e.g. Serial.println("frame over budget"), or drop a visual effect
}
}P5FrameStats fields: fps/avgFrameUs (exponential moving average, settles
over a few frames), lastFrameUs (most recent frame), worstFrameUs (running
max since the last resetFrameStats()), frameCount, and overBudget (only
meaningful after calling frameBudget(targetFps)). Cost is one micros()
call plus an integer average — safe to call every frame, even on 8-bit-free
32-bit MCUs with tight timing.
simulator/index.html previews a sketch's logic in a browser, no board
required: open it directly from disk (double-click, or file:// in any
browser — no build step, no server). Write a setup(p5)/draw(p5, t) pair
using the same vocabulary as the real P5 API (background, stroke/fill,
shapes, push/pop/translate/rotate/scale, random/noise,
p5.frameStats(), ...) and it runs live on an HTML5 canvas, with a few
ported example sketches in the preset dropdown.
It is not a pixel-exact stand-in for GFXCanvas565 — simulator/p5sim.js
maps the same calls onto the Canvas 2D API's own anti-aliasing rather than
reimplementing the scanline AA math, so treat it as a way to preview a
sketch's structure, transforms, and frame-timing before touching hardware,
not as proof of exact on-panel appearance.
Same vocabulary, same AA math. If you're already using ST77916_QSPI, keep
using its P5/P5Canvas/ST77916_Sprite — they're faster on that specific
panel. Reach for GFX_P5 when you need the same sketch code to run on a
different or unknown display driver, or on non-ESP32 boards.
test/hostcheck/run.sh
Builds GFXCanvas565 against a stub Arduino/Adafruit_GFX header and runs it
on your machine: drawing/blending onto the buffer directly, compositing via
draw/drawKeyed/blendFrom onto a second canvas standing in for a real
display, and color()/gray(). No hardware required; run it after any
change to src/GFXCanvas565.cpp.
GFXCanvas565allocateswidth * height * 2bytes from the heap inbegin()— a 240×320 canvas is 153,600 bytes. Size it to fit your board's free RAM (PSRAM-equipped ESP32 boards have plenty of headroom; plain AVR boards do not).- Plain 8-bit AVR (Uno, Nano, Mega) is a hard compile-time error, by
design:
GFXCanvas565.h#errors out underdefined(__AVR__). Even if it compiled, everyellipse()/arc()/polygon()/endShape()call puts twoP5_MAX_VERTICES-sizedfloatarrays on the stack (default 192 vertices → 1.5KB, twice per call chain, ~3KB) — more stack than an Uno's entire 2KB of SRAM, before the canvas buffer or anything else. There's no cheap fix short of a much smallerP5_MAX_VERTICESand dropping freeform/regular-polygon shapes, so the library draws the line at "32-bit MCUs only" instead. CI reflects this: it compile-testsesp32s3andsamd(a genuine second 32-bit target), not AVR. Note__AVR__is only defined for real avr-gcc silicon — 32-bit boards that self-report an"avr"architecture string for legacy-library-compatibility reasons (Teensy 3/4) are unaffected. - There's a 64-crossing-per-scanline limit in
fillPolygonAA(), shared withST77916_QSPI's fill routine — plenty for convex shapes and the regular n-gonspolygon()/beginShapeproduce, but a very self-intersecting freeform polygon could silently drop crossings past the 64th. drawKeyed()/blendFrom()composite onedrawPixel()call at a time (Adafruit_GFX has no colour-key or alpha-blit primitive), so they're portable but not as fast as a display driver's own DMA blit — if your driver exposes one, prefer that for large keyed/blended regions.
MIT. See LICENSE.
Issues and pull requests are welcome at
github.com/Studio-Poetics/GFX_P5.
Please run test/hostcheck/run.sh before opening a PR that touches
src/GFXCanvas565.cpp.
Written by Studio Poetics.