Canvas
A Canvas is a pixel buffer plus the primitives to draw into it: lines,
rectangles, circles, arcs, text, and images. You pick the size and
pixel format at creation time. When you're done drawing, hand the
buffer to whatever output you want: the on-board display, an SPI
screen, an I2C OLED, a file.
Canvas owns the pixel format so there's only one place to pick it. The output device just takes bytes:
display.Flush(canvas.Buffer); // the on-board panel
spi.Write(canvas.Buffer); // SPI display
i2c.Write(canvas.Buffer); // I2C display
Draw on the on-board panel
The on-board panel is always RGB565, so create the Canvas in that
format and pass its buffer straight to DisplayController.Flush.
- .NET
- Python
- C/C++
using GHIElectronics.Endpoint.Devices.Display;
using GHIElectronics.Endpoint.Drawing;
using var display = DisplayController.GetDefault();
display.Enable();
using var canvas = Canvas.Create(display.Width, display.Height, PixelFormat.Rgb565);
canvas.Clear(Color.Black);
canvas.DrawLine(10, 10, 200, 200, Color.Red, thickness: 2);
canvas.FillCircle(100, 100, 40, Color.Blue);
canvas.DrawText("Hello Endpoint", 20, 20, Color.White, size: 24);
display.Flush(canvas.Buffer);
from GHIElectronics.Endpoint.Devices.Display import DisplayController
from GHIElectronics.Endpoint.Drawing import Canvas, Color, PixelFormat
display = DisplayController.GetDefault()
display.Enable()
with Canvas.Create(display.Width, display.Height, PixelFormat.Rgb565) as canvas:
canvas.Clear(Color.Black)
canvas.DrawLine(10, 10, 200, 200, Color.Red, thickness=2)
canvas.FillCircle(100, 100, 40, Color.Blue)
canvas.DrawText("Hello Endpoint", 20, 20, Color.White, size=24)
display.Flush(canvas.Buffer)
#include <endpoint/Display.h>
#include <endpoint/Drawing.h>
auto display = endpoint::DisplayController::GetDefault();
display->Enable();
auto canvas = endpoint::Canvas::Create(display->Width(), display->Height(),
endpoint::PixelFormat::Rgb565);
canvas.Clear(endpoint::Color::Black);
canvas.DrawLine(10, 10, 200, 200, endpoint::Color::Red, /*thickness*/ 2);
canvas.FillCircle(100, 100, 40, endpoint::Color::Blue);
canvas.DrawText("Hello Endpoint", 20, 20, endpoint::Color::White, /*size*/ 24);
display->Flush(canvas.Buffer());
If the Canvas's PixelFormat doesn't match the panel's (RGB565),
display.Flush throws; no silent format conversion.
Draw to an external SPI display (ILI9341, 240x320)
Same Canvas, different destination. The sample below uses an
Ili9341 driver class that wraps the chip-select / data-command / reset
pins, the init sequence, and the per-frame pixel re-encode behind a
single Flush call. Your Canvas code looks the same as it does for
the on-board panel. Source on GitHub:
- .NET:
GHIElectronics.Endpoint.Drivers.HiLetgo.ILI9341(NuGet package) - Python:
ili9341.py(drop the file into the project) - C/C++:
Ili9341.h(header-only, drop the file into the project)
:::note Wiring an SPI display, gotchas
- Chip select is driven by a GPIO, not by the SPI controller's hardware NSS.
The driver opens the pin you pass and toggles it around each transaction.
On .NET,
SpiConnectionSettingsneedschipSelectLine: 0explicitly; Microsoft's Linux SPI stack does not accept-1. - Data / Command pin: ILI9341 distinguishes commands from pixel data by a separate GPIO (D/C). Low = command byte, high = data byte.
- Reset pin: an active-low pulse brings the chip out of reset reliably at startup. Tie to 3.3 V or leave unwired only if your module has an on-board reset circuit.
- Pixel format on the wire is 16 bpp big-endian, but Canvas
writes RGB565 little-endian (ready for
/dev/fb0). The driver byte-swaps each pixel inFlush; you never see the raw bytes. - Red / Blue swap: some HiLetgo and eBay modules have the physical
color filter wired as BGR regardless of what the chip's MADCTL bit
says, so yellow renders as cyan and red as blue. Set
SwapRedBlue = truefor those modules. - Backlight on most 2.2"–2.8" ILI9341 modules has a current-limit resistor on board; tie LED to +3.3 V for always-on, or drive from a PWM pin if you want to dim it. :::
- .NET
- Python
- C/C++
using System.Device.Gpio;
using System.Device.Spi;
using GHIElectronics.Endpoint.Core;
using GHIElectronics.Endpoint.Drivers.HiLetgo.ILI9341;
using GHIElectronics.Endpoint.Drawing;
SpiController.Initialize(EPDomino24.Spi.Spi1);
using var spi = SpiDevice.Create(ILI9341Controller.GetConnectionSettings(EPDomino24.Spi.Spi1));
using var gpio = new GpioController(new EPDomino24.Gpio.Driver());
using var tft = new ILI9341Controller(
spi: spi,
cs: EPDomino24.Gpio.Pin.PA14,
dc: EPDomino24.Gpio.Pin.PF14,
reset: EPDomino24.Gpio.Pin.PF4,
gpio: gpio);
tft.SwapRedBlue = true; // set for modules where yellow shows as cyan
tft.Enable();
using var canvas = Canvas.Create(tft.Width, tft.Height, PixelFormat.Rgb565);
canvas.FillRectangle(0, 0, tft.Width, tft.Height, Color.Navy);
canvas.DrawText("240 x 320 ILI9341", 10, 10, Color.White, size: 16);
canvas.DrawLine(0, 160, tft.Width, 160, Color.Yellow);
tft.Flush(canvas.Buffer);
from GHIElectronics.Endpoint.Core import EPDomino24, SpiController
from GHIElectronics.Endpoint.Devices.Spi import SpiDevice, SpiConnectionSettings, SpiMode
from GHIElectronics.Endpoint.Devices.Gpio import GpioController
from GHIElectronics.Endpoint.Drawing import Canvas, Color, PixelFormat
from ili9341 import Ili9341 # driver file, link above
SpiController.Initialize(EPDomino24.Spi.Spi1, bufferSize=4096)
settings = SpiConnectionSettings(EPDomino24.Spi.Spi1)
settings.Mode = SpiMode.Mode0
settings.ClockFrequency = 10_000_000
spi = SpiDevice.Create(settings)
gpio = GpioController()
tft = Ili9341(gpio, spi,
cs_pin=EPDomino24.Gpio.Pin.PA14,
dc_pin=EPDomino24.Gpio.Pin.PF14,
rst_pin=EPDomino24.Gpio.Pin.PF4,
swap_red_blue=True) # set for modules where yellow shows as cyan
tft.initialize()
canvas = Canvas.Create(tft.width, tft.height, PixelFormat.Rgb565)
canvas.FillRectangle(0, 0, tft.width, tft.height, Color.Navy)
canvas.DrawText("240 x 320 ILI9341", 10, 10, Color.White, size=16)
canvas.DrawLine(0, 160, tft.width, 160, Color.Yellow)
tft.flush(canvas.Buffer)
#include <endpoint.h>
#include <endpoint/SpiController.h>
#include "Ili9341.h" // driver file, link above
namespace Core = GHIElectronics::Endpoint::Core;
namespace Gpio = GHIElectronics::Endpoint::Devices::Gpio;
namespace Spi = GHIElectronics::Endpoint::Devices::Spi;
namespace Drawing = GHIElectronics::Endpoint::Drawing;
Core::SpiController::Initialize(Core::EPDomino24::Spi::Spi1, /*bufferSize*/ 4096);
Spi::SpiConnectionSettings settings(Core::EPDomino24::Spi::Spi1);
settings.Mode = Spi::SpiMode::Mode0;
settings.ClockFrequency = 10'000'000;
auto spi = Spi::SpiDevice::Create(settings);
Gpio::GpioController gpio;
Ili9341 tft(gpio, spi,
/*cs*/ Core::EPDomino24::Gpio::Pin::PA14,
/*dc*/ Core::EPDomino24::Gpio::Pin::PF14,
/*rst*/ Core::EPDomino24::Gpio::Pin::PF4);
tft.SwapRedBlue(true); // set for modules where yellow shows as cyan
tft.Initialize();
auto canvas = Drawing::Canvas::Create(tft.Width(), tft.Height(), Drawing::PixelFormat::Rgb565);
canvas.FillRectangle(0, 0, tft.Width(), tft.Height(), Drawing::Color::Navy());
canvas.DrawText("240 x 320 ILI9341", 10, 10, Drawing::Color::White(), 16);
canvas.DrawLine(0, 160, tft.Width(), 160, Drawing::Color::Yellow());
tft.Flush(canvas.Buffer());
Draw to an I2C OLED (SSD1306, 128x64 monochrome)
- .NET
- Python
- C/C++
using System.Device.I2c;
using GHIElectronics.Endpoint.Core;
using GHIElectronics.Endpoint.Drawing;
I2cController.Initialize(EPN256.I2c.I2c5);
using var oled = I2cDevice.Create(new I2cConnectionSettings(5, 0x3C));
using var canvas = Canvas.Create(128, 64, PixelFormat.Monochrome);
canvas.DrawText("Hello", 5, 5, Color.White, size: 12);
canvas.FillCircle(100, 32, 10, Color.White);
oled.Write(canvas.Buffer);
from smbus2 import SMBus
from GHIElectronics.Endpoint.Core import EPN256, I2cController
from GHIElectronics.Endpoint.Drawing import Canvas, Color, PixelFormat
I2cController.Initialize(EPN256.I2c.I2c5)
oled = SMBus(5)
with Canvas.Create(128, 64, PixelFormat.Monochrome) as canvas:
canvas.DrawText("Hello", 5, 5, Color.White, size=12)
canvas.FillCircle(100, 32, 10, Color.White)
oled.write_i2c_block_data(0x3C, 0x40, list(canvas.Buffer))
#include <endpoint/Core.h>
#include <endpoint/I2c.h>
#include <endpoint/Drawing.h>
endpoint::I2cController::Initialize(endpoint::EPN256::I2c::I2c5);
endpoint::I2cDevice oled(5, 0x3C);
auto canvas = endpoint::Canvas::Create(128, 64, endpoint::PixelFormat::Monochrome);
canvas.DrawText("Hello", 5, 5, endpoint::Color::White, 12);
canvas.FillCircle(100, 32, 10, endpoint::Color::White);
oled.Write(canvas.Buffer());
API at a glance
| Member | Purpose |
|---|---|
Canvas.Create(w, h, PixelFormat) | New Canvas of any size and pixel format. |
Buffer | Raw pixel bytes. Hand to display.Flush, SpiDevice.Write, I2cDevice.Write, a file, anywhere. |
Width / Height / PixelFormat | Dimensions and format the Canvas was created with. |
Clear(Color) | Fill the whole buffer with a single color. |
SetPixel(x, y, Color) | Single pixel. |
DrawLine(x0, y0, x1, y1, Color, thickness) | Straight line. |
DrawRectangle(x, y, w, h, Color, thickness) | Stroked rectangle. |
FillRectangle(x, y, w, h, Color) | Filled rectangle. |
DrawCircle(cx, cy, r, Color, thickness) | Stroked circle. |
FillCircle(cx, cy, r, Color) | Filled circle. |
DrawArc(cx, cy, r, startDeg, endDeg, Color, thickness) | Arc segment. |
DrawText(text, x, y, Color, size) | Rasterized text (Montserrat 14/20/32). |
DrawImage(bytes, x, y, w, h, PixelFormat) | Blit an RGB565 / Mono / RGB888 image. |
Pixel formats
PixelFormat | Bytes / pixel | Typical use |
|---|---|---|
Monochrome | 1 bit (packed) | SSD1306, SH1106 OLEDs |
Rgb565 | 2 | ILI9341, ST7735, the on-board panel |
Rgb888 | 3 | Parallel RGB panels at 24 bpp |
Argb8888 | 4 | Off-screen compositing |
The same drawing calls work in any format; the only difference is what
Buffer contains and what the destination device expects.