Skip to main content

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.

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);

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:

:::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, SpiConnectionSettings needs chipSelectLine: 0 explicitly; 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 in Flush; 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 = true for 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. :::
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);

Draw to an I2C OLED (SSD1306, 128x64 monochrome)​

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);

API at a glance​

MemberPurpose
Canvas.Create(w, h, PixelFormat)New Canvas of any size and pixel format.
BufferRaw pixel bytes. Hand to display.Flush, SpiDevice.Write, I2cDevice.Write, a file, anywhere.
Width / Height / PixelFormatDimensions 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​

PixelFormatBytes / pixelTypical use
Monochrome1 bit (packed)SSD1306, SH1106 OLEDs
Rgb5652ILI9341, ST7735, the on-board panel
Rgb8883Parallel RGB panels at 24 bpp
Argb88884Off-screen compositing

The same drawing calls work in any format; the only difference is what Buffer contains and what the destination device expects.