Skip to content
Studio-PoeticsPublic

About

A p5.js-shaped drawing API that composites onto any Adafruit_GFX display

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

GFX_P5 Compile Sketches

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.

Features

  • p5.js vocabulary: background, stroke/fill, rect/ellipse/circle/triangle/quad, regular polygon(), arc(), beginShape/vertex/endShape, a full 2D transform stack (translate/rotate/scale/push/pop), Perlin noise(), 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 with draw() (opaque), drawKeyed() (colour-key transparency), or blendFrom() (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.

How it works

  1. Draw into an offscreen GFXCanvas565 using P5 (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.
  2. Composite the finished canvas onto your real display with canvas.draw(display, x, y) — this goes through nothing but the base Adafruit_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.

Installing

Arduino Library Manager (recommended, once published)

Sketch → Include Library → Manage Libraries..., search for GFX_P5, click Install. This also installs the required Adafruit GFX Library dependency automatically.

Manual install

  1. Download this repository as a ZIP (Code → Download ZIP on GitHub).

  2. In the Arduino IDE: Sketch → Include Library → Add .ZIP Library..., and select the downloaded file.

    Or unzip it directly into your sketchbook's libraries folder (~/Documents/Arduino/libraries/ on macOS/Linux, Documents\Arduino\libraries\ on Windows) — rename the extracted folder to GFX_P5 if GitHub added a branch suffix.

  3. Restart the Arduino IDE.

Dependencies

  • 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.

Quick start

#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).

Examples

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.

Compositing options

  • canvas.draw(display, x, y) — opaque blit.
  • canvas.drawKeyed(display, x, y, keyColor) — skips pixels equal to keyColor, 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.

Knowing if your sketch fits its frame budget

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.

Browser simulator

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.

Relationship to ST77916_QSPI's P5

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.

Testing

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.

Caveats

  • GFXCanvas565 allocates width * height * 2 bytes from the heap in begin() — 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 under defined(__AVR__). Even if it compiled, every ellipse()/arc()/polygon()/endShape() call puts two P5_MAX_VERTICES-sized float arrays 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 smaller P5_MAX_VERTICES and dropping freeform/regular-polygon shapes, so the library draws the line at "32-bit MCUs only" instead. CI reflects this: it compile-tests esp32s3 and samd (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 with ST77916_QSPI's fill routine — plenty for convex shapes and the regular n-gons polygon()/beginShape produce, but a very self-intersecting freeform polygon could silently drop crossings past the 64th.
  • drawKeyed()/blendFrom() composite one drawPixel() 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.

License

MIT. See LICENSE.

Contributing

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.

About

A p5.js-shaped drawing API that composites onto any Adafruit_GFX display

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages