Hardware components | ||||||
![]() |
| × | 1 | |||
| × | 1 | ||||
| × | 1 | ||||
pydafruit_gfx is a python tool to quickly prototype Adafruit GFX displays in python. (yep, python!)
I find it frustrating when building a display (with Arduino + hardware), you have to: flash the board, a cable can go bad, a wire pops loose, or the board can decides to brick itself. I end up spending 10–20 seconds (if I'm lucky) just to see if moving a font three pixels to the left "feels right".
I've always wanted a faster way to prototype display layouts and graphics, so I built this python tool.
Using the Real GFX CodeInstead of mimicking Adafruit's GFX code, why not just use it.
Pydafruit Gfx uses:
- Adafruit GFX graphics code
- SDL (a display window that works on macOS, Linux, and Windows)
- pybind11
Using Adafruit's actual GFX code is great because there's no uncertainty. Did the font copy correctly? Is that function actually implemented correctly? So instead - just use their code. Their code is the source of truth.
SDL is a backend window that supports basic pixel drawing, which is great for this tool. SDL can draw any color we want, scale the display up (64x64 pixels is tiny on a computer screen), and save the output as images. Adafruit's GFX writes tot he SDL window. You do not need to install SDL, it is built into the pip wheel.
pybind11 turns the whole thing into a python library that you can just pip install.
I've tested the wheels on macOS, Windows, and ubuntu Linux, and they all worked. I'm sure there are some edge cases, unusual setups, or old OSs that could have issues.
SetupOption 1: Install the Wheel (pip install)
If you're on mac, linux, or windows, just download the wheel that matches your python version and platform and install it with pip.You can find the wheels here: link. The repo's README.md also has instructions.
Option 2: Build From Source
If you want to change the code, add features (fonts!), or see how everything works, you can build it yourself.
Clone the repo (including the Adafruit GFX submodule), install SDL2, and run:
pip install -e . --no-build-isolation -Cbuild-dir=_buildAfter installation, run the example:
python hello_world.pyIf a window opens and displays graphics, you're ready to start prototyping.
DemoI needed a demo, so I started writing in python using pydafruit gfx. That code is provided below. In about an hour, I had something I liked.
Next, I copied the.py file into chatGPT and said, "I want this as an Arduino.ino file for an OLED display."
I uploaded the generated code, ran it, and it worked.
I can now iterate quickly in python, get the layout and graphics right, and then convert it into Arduino code once I like it.
Thanks!
/*
made by chatgpt, just pasted in my python code and said convert to arduino io file
it seems a little funky but works
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/TomThumb.h>
#include <Fonts/Org_01.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define FPS 30
#define FRAME_MS (1000UL / FPS)
#define SPF (1.0f / FPS)
#define BLACK SH110X_BLACK
#define WHITE SH110X_WHITE
Adafruit_SH1107 display = Adafruit_SH1107(
SCREEN_HEIGHT, SCREEN_WIDTH, &Wire, OLED_RESET
);
const uint8_t BMP_W = 32;
const uint8_t BMP_H = 32;
const uint8_t bitmap_python[] PROGMEM = {
0x00,0x1F,0xF0,0x00,0x00,0x7F,0xFC,0x00,0x00,0x7F,0xFE,0x00,0x00,0xC7,0xFF,0x00,
0x00,0xC7,0xFF,0x00,0x00,0xFF,0xFF,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0xFF,0x00,
0x0F,0xFF,0xFF,0x78,0x3F,0xFF,0xFF,0x7C,0x7F,0xFF,0xFF,0x7E,0x7F,0xFF,0xFF,0x7E,
0xFF,0xFF,0xFF,0x7F,0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,0xFC,0xFF,0xFF,0xF0,0x01,0xFF,
0xFF,0x80,0x0F,0xFF,0xFF,0x3F,0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,0xFE,0xFF,0xFF,0xFF,
0x7E,0xFF,0xFF,0xFE,0x7E,0xFF,0xFF,0xFE,0x3E,0xFF,0xFF,0xFC,0x1E,0xFF,0xFF,0xF0,
0x00,0xFF,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0xFF,0xFF,0x00,0x00,0xFF,0xE3,0x00,
0x00,0xFF,0xE3,0x00,0x00,0x7F,0xFE,0x00,0x00,0x3F,0xFE,0x00,0x00,0x0F,0xF8,0x00,
};
const uint8_t bitmap_adafruit[] PROGMEM = {
0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0x00,0x00,0x01,0xE0,0x00,0x00,0x01,0xF0,0x00,
0x00,0x03,0xF0,0x00,0x00,0x07,0xF0,0x00,0x00,0x07,0xF8,0x00,0x00,0x0F,0xF8,0x00,
0x7F,0x0F,0xF8,0x00,0xFF,0xEF,0xF8,0x00,0xFF,0xFF,0xF8,0x00,0x7F,0xFE,0x7F,0xC0,
0x3F,0xFE,0x7F,0xF8,0x1F,0xFE,0x7F,0xFF,0x1F,0xC6,0xFF,0xFF,0x0F,0xE3,0xC7,0xFE,
0x07,0xFF,0x87,0xFC,0x01,0xFF,0xFF,0xF0,0x01,0xF3,0x7F,0xE0,0x03,0xE3,0x3F,0x80,
0x07,0xE7,0x3C,0x00,0x07,0xFF,0xBE,0x00,0x07,0xFF,0xFE,0x00,0x0F,0xFF,0xFE,0x00,
0x0F,0xFF,0xFF,0x00,0x0F,0xF9,0xFF,0x00,0x1F,0xF1,0xFF,0x00,0x1F,0x80,0xFF,0x00,
0x1C,0x00,0x7F,0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x0F,0x00,0x00,0x00,0x06,0x00,
};
const uint8_t bitmap_invader[] PROGMEM = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0xE0,0x07,0xC0,
0x03,0xE0,0x07,0xC0,0x03,0xE0,0x07,0xC0,0x03,0xFC,0x3F,0xC0,0x03,0xFC,0x3F,0xC0,
0x00,0x7C,0x3E,0x00,0x00,0x7C,0x3E,0x00,0x03,0xFF,0xFF,0xC0,0x03,0xFF,0xFF,0xC0,
0x03,0xFF,0xFF,0xC0,0x1F,0xFF,0xFF,0xF8,0x1F,0xFF,0xFF,0xF8,0x1F,0x83,0xC1,0xF8,
0xFF,0x83,0xC1,0xFF,0xFF,0x83,0xC1,0xFF,0xFF,0x83,0xC1,0xFF,0xFF,0x83,0xC1,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFB,0xFF,0xFF,0xDF,0xFB,0xFF,0xFF,0xDF,0xFB,0xFF,0xFF,0xDF,
0xFB,0xE0,0x07,0xDF,0xFB,0xFE,0x7F,0xDF,0xFB,0xFE,0x7F,0xDF,0x00,0x7E,0x7E,0x00,
0x00,0x7E,0x7E,0x00,0x00,0x7E,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
struct Star {
float ox;
float oy;
float spd;
};
// These are generated from Python's random.seed(7), then 70 triples of:
// random.uniform(-1,1), random.uniform(-1,1), random.uniform(0.3,1.0)
const Star stars[70] PROGMEM = {
{-0.352334470f, -0.698301652f, 0.755654131f},
{-0.855127427f, 0.071764009f, 0.555982242f},
{-0.884002150f, 0.014871466f, 0.326246961f},
{-0.132708633f, -0.860289153f, 0.363499109f},
{-0.150961622f, 0.653704249f, 0.386661373f},
{-0.553522071f, 0.254866445f, 0.963396260f},
{0.154205897f, -0.206639051f, 0.983378574f},
{-0.906834639f, 0.716936918f, 0.502726500f},
{-0.711489833f, -0.764415524f, 0.515937277f},
{0.632252718f, -0.638547240f, 0.707120115f},
{0.277826938f, -0.255204915f, 0.683421126f},
{-0.874422050f, -0.880797660f, 0.444171099f},
{0.360799946f, -0.144815389f, 0.519903019f},
{0.171123727f, -0.093631247f, 0.509836898f},
{0.588758963f, 0.397988867f, 0.470867558f},
{0.148847421f, 0.050393008f, 0.912596247f},
{0.458890579f, -0.424124470f, 0.986122393f},
{-0.763868443f, -0.163754356f, 0.829998651f},
{-0.696030931f, -0.022073799f, 0.327445080f},
{0.336431713f, 0.529141732f, 0.701118158f},
{0.750955624f, -0.372504974f, 0.786706756f},
{0.188739754f, 0.159790409f, 0.619343732f},
{0.679935561f, 0.889362190f, 0.631868836f},
{0.328304411f, -0.878661145f, 0.791044415f},
{0.294257709f, 0.986191879f, 0.875347351f},
{-0.430808936f, -0.228417115f, 0.768056901f},
{-0.954874144f, -0.076609427f, 0.417633865f},
{-0.765808411f, -0.882091161f, 0.837763092f},
{-0.741319556f, -0.504770333f, 0.573664792f},
{0.742843948f, -0.838837398f, 0.614431181f},
{0.098879818f, 0.766767653f, 0.873495886f},
{0.727968939f, -0.443157871f, 0.590707562f},
{-0.282457669f, 0.768385654f, 0.970411843f},
{-0.698158188f, -0.647564543f, 0.462369807f},
{-0.533327833f, -0.030074539f, 0.712386453f},
{-0.474506761f, -0.991812793f, 0.593262551f},
{-0.261492854f, 0.132682447f, 0.967168548f},
{0.380987314f, 0.030982866f, 0.732314925f},
{0.352400165f, -0.892014214f, 0.929673107f},
{0.559938981f, 0.749026368f, 0.858511185f},
{-0.215242186f, -0.202042335f, 0.372475966f},
{0.268579131f, -0.875504357f, 0.347143331f},
{-0.582473629f, -0.675393624f, 0.538037557f},
{-0.894848792f, -0.999533436f, 0.405885453f},
{-0.797071264f, -0.272780156f, 0.317850621f},
{0.748664755f, 0.228137976f, 0.403985340f},
{-0.495484487f, -0.305220908f, 0.554914408f},
{-0.754315538f, 0.697873853f, 0.995171905f},
{-0.068021082f, -0.032330687f, 0.360119263f},
{-0.795624767f, -0.314728324f, 0.485329824f},
{0.657710756f, -0.677122779f, 0.316167005f},
{0.901971146f, 0.056514790f, 0.402621777f},
{0.086344852f, -0.945915017f, 0.669676609f},
{0.957002485f, 0.726650061f, 0.787337750f},
{-0.477769606f, -0.266600416f, 0.416929424f},
{0.543875817f, 0.065184795f, 0.845338424f},
{-0.340670010f, -0.553916654f, 0.868057873f},
{0.969852101f, 0.705257597f, 0.864255009f},
{0.636665887f, 0.479746041f, 0.458717643f},
{0.035277448f, -0.288874913f, 0.320286106f},
{-0.944125849f, -0.441162922f, 0.481422054f},
{0.385043883f, 0.913030153f, 0.613059374f},
{0.874042403f, 0.976076116f, 0.968500442f},
{-0.270728229f, -0.559075354f, 0.458792079f},
{-0.606587673f, -0.591253273f, 0.736846478f},
{0.800616676f, 0.680871055f, 0.635631398f},
{0.305956086f, 0.599287490f, 0.359344941f},
{0.321171300f, 0.819554275f, 0.847612019f},
{0.500280920f, -0.043934511f, 0.424965203f},
{0.578270862f, -0.334965600f, 0.860576498f},
};
enum Scene {
SCENE_INSERT_COIN,
SCENE_BOOT,
SCENE_STARFIELD,
SCENE_BOUNCE,
SCENE_SPRITES,
SCENE_CREDITS
};
Scene currentScene = SCENE_INSERT_COIN;
unsigned long sceneStartMs = 0;
unsigned long lastFrameMs = 0;
uint16_t frameNo = 0;
// Bounce scene state
float bounceX = SCREEN_WIDTH / 2.0f;
float bounceY = SCREEN_HEIGHT / 2.0f;
float bounceVX = 38.0f;
float bounceVY = 27.0f;
uint16_t bounceScore = 0;
const uint8_t TRAIL = 14;
int8_t trailX[TRAIL];
int8_t trailY[TRAIL];
uint8_t trailCount = 0;
// Sprite scene state
float spriteX[3];
uint8_t spriteLaps[3];
enum FontId {
FONT_DEFAULT,
FONT_FREE_SANS_BOLD_9,
FONT_TOM_THUMB,
FONT_ORG_01
};
struct CreditLine {
const char *text;
FontId font;
};
const CreditLine creditLines[] = {
{"PYDAFRUIT", FONT_FREE_SANS_BOLD_9},
{"", FONT_ORG_01},
{"GFX", FONT_FREE_SANS_BOLD_9},
{"", FONT_ORG_01},
{"SDL2 backend", FONT_ORG_01},
{"pybind11 bindings", FONT_ORG_01},
{"Adafruit_GFX port", FONT_ORG_01},
{"", FONT_ORG_01},
{"draw_pixel", FONT_ORG_01},
{"draw_line", FONT_ORG_01},
{"fill_circle", FONT_ORG_01},
{"draw_bitmap", FONT_ORG_01},
{"set_font", FONT_ORG_01},
{"get_text_bounds", FONT_ORG_01},
{"", FONT_TOM_THUMB},
{"github:pydafruitGFX", FONT_TOM_THUMB},
{"MIT License", FONT_TOM_THUMB},
{"", FONT_TOM_THUMB},
{"* FIN *", FONT_FREE_SANS_BOLD_9},
{"thanks!", FONT_TOM_THUMB},
};
const uint8_t CREDIT_COUNT = sizeof(creditLines) / sizeof(creditLines[0]);
void setFontId(FontId font) {
switch (font) {
case FONT_FREE_SANS_BOLD_9:
display.setFont(&FreeSansBold9pt7b);
break;
case FONT_TOM_THUMB:
display.setFont(&TomThumb);
break;
case FONT_ORG_01:
display.setFont(&Org_01);
break;
case FONT_DEFAULT:
default:
display.setFont();
break;
}
}
int16_t textWidth(const char *text, int16_t x = 0, int16_t y = 0) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(text, x, y, &x1, &y1, &w, &h);
return (int16_t)w;
}
void printCentered(const char *text, int16_t baselineY) {
int16_t tw = textWidth(text, 0, baselineY);
display.setCursor((SCREEN_WIDTH - tw) / 2, baselineY);
display.print(text);
}
float sceneSeconds() {
return (millis() - sceneStartMs) / 1000.0f;
}
void transitionWipe() {
int16_t cx = SCREEN_WIDTH / 2;
for (int16_t i = 0; i < 80; i += 2) {
display.drawCircle(cx, SCREEN_HEIGHT, i, BLACK);
display.drawCircle(cx - 1, SCREEN_HEIGHT, i, BLACK);
display.drawCircle(cx, SCREEN_HEIGHT - 1, i * 2, BLACK);
display.drawCircle(cx, SCREEN_HEIGHT - 2, i * 3, BLACK);
display.display();
delay(10);
}
display.clearDisplay();
display.display();
}
void resetSceneState(Scene scene) {
sceneStartMs = millis();
frameNo = 0;
if (scene == SCENE_BOUNCE) {
bounceX = SCREEN_WIDTH / 2.0f;
bounceY = SCREEN_HEIGHT / 2.0f;
bounceVX = 38.0f;
bounceVY = 27.0f;
bounceScore = 0;
trailCount = 0;
}
if (scene == SCENE_SPRITES) {
spriteX[0] = -BMP_W - 20.0f;
spriteX[1] = -BMP_W - 60.0f;
spriteX[2] = -BMP_W - 40.0f;
spriteLaps[0] = 0;
spriteLaps[1] = 0;
spriteLaps[2] = 0;
}
}
void nextScene() {
currentScene = (Scene)((currentScene + 1) % 6);
resetSceneState(currentScene);
}
void drawInsertCoin() {
display.clearDisplay();
setFontId(FONT_FREE_SANS_BOLD_9);
display.setTextColor(WHITE);
display.setTextSize(1);
printCentered("INSERT", 15);
printCentered("COIN", 32);
setFontId(FONT_TOM_THUMB);
printCentered("press Q to skip", 53);
uint8_t slotW = frameNo % 8;
int16_t slotX = SCREEN_WIDTH / 2 - slotW / 2;
if (slotW > 0) {
display.fillRoundRect(slotX, 36, slotW, 10, 2, WHITE);
}
if (((frameNo / 30) % 2 == 0) && frameNo > 60) {
setFontId(FONT_TOM_THUMB);
printCentered("-- INSERT COIN --", 62);
}
display.display();
if (millis() - sceneStartMs >= 4000UL) {
transitionWipe();
nextScene();
}
}
void drawBoot() {
float t = sceneSeconds();
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
if (t > 0.4f) {
display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, WHITE);
display.drawRect(2, 2, SCREEN_WIDTH - 4, SCREEN_HEIGHT - 4, WHITE);
}
if (t > 0.8f) {
setFontId(FONT_FREE_SANS_BOLD_9);
display.setCursor(6, 19);
display.print("PYDAFRUIT");
display.setCursor(30, 35);
display.print("GFX");
}
if (t > 1.4f) {
setFontId(FONT_TOM_THUMB);
display.setCursor(8, 48);
display.print("SDL2+pybind11");
}
if (t > 2.0f && ((int)(t * 3.0f) % 2 == 0)) {
setFontId(FONT_TOM_THUMB);
display.setCursor(8, 58);
display.print("< DEMO >");
}
display.display();
if (millis() - sceneStartMs >= 3500UL) {
nextScene();
}
}
void drawStarfield() {
float t = sceneSeconds();
int16_t cx = SCREEN_WIDTH / 2;
int16_t cy = SCREEN_HEIGHT / 2;
display.clearDisplay();
float speed = min(t / 1.5f, 1.0f) * 4.0f + 0.4f;
for (uint8_t i = 0; i < 70; i++) {
Star s;
memcpy_P(&s, &stars[i], sizeof(Star));
float z = fmod(t * s.spd * speed, 2.0f);
float sc = z / 2.0f;
int16_t x = (int16_t)(cx + s.ox * sc * SCREEN_WIDTH);
int16_t y = (int16_t)(cy + s.oy * sc * SCREEN_HEIGHT);
if (x < 0 || x >= SCREEN_WIDTH || y < 0 || y >= SCREEN_HEIGHT) {
continue;
}
if (sc > 0.5f) {
int16_t px = (int16_t)(cx + s.ox * max(0.0f, sc - 0.18f) * SCREEN_WIDTH);
int16_t py = (int16_t)(cy + s.oy * max(0.0f, sc - 0.18f) * SCREEN_HEIGHT);
display.drawLine(px, py, x, y, WHITE);
} else {
display.drawPixel(x, y, WHITE);
}
}
setFontId(FONT_TOM_THUMB);
display.setTextColor(WHITE);
display.setCursor(2, SCREEN_HEIGHT - 1);
display.print("STARFIELD");
display.display();
if (millis() - sceneStartMs >= 5000UL) {
nextScene();
}
}
void pushTrail(int16_t x, int16_t y) {
if (trailCount < TRAIL) {
trailX[trailCount] = (int8_t)x;
trailY[trailCount] = (int8_t)y;
trailCount++;
} else {
for (uint8_t i = 1; i < TRAIL; i++) {
trailX[i - 1] = trailX[i];
trailY[i - 1] = trailY[i];
}
trailX[TRAIL - 1] = (int8_t)x;
trailY[TRAIL - 1] = (int8_t)y;
}
}
void drawBounce() {
const int16_t R = 5;
bounceX += bounceVX * SPF;
bounceY += bounceVY * SPF;
bool bounced = false;
if (bounceX - R < 0) {
bounceX = R;
bounceVX = abs(bounceVX);
bounced = true;
}
if (bounceX + R >= SCREEN_WIDTH) {
bounceX = SCREEN_WIDTH - R - 1;
bounceVX = -abs(bounceVX);
bounced = true;
}
if (bounceY - R < 0) {
bounceY = R;
bounceVY = abs(bounceVY);
bounced = true;
}
if (bounceY + R >= SCREEN_HEIGHT) {
bounceY = SCREEN_HEIGHT - R - 1;
bounceVY = -abs(bounceVY);
bounced = true;
}
if (bounced) {
bounceScore++;
}
pushTrail((int16_t)bounceX, (int16_t)bounceY);
display.clearDisplay();
display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, WHITE);
for (uint8_t i = 0; i + 1 < trailCount; i++) {
if (i % 2 == 0) {
display.drawPixel(trailX[i], trailY[i], WHITE);
}
}
display.fillCircle((int16_t)bounceX, (int16_t)bounceY, R, WHITE);
display.drawCircle((int16_t)bounceX, (int16_t)bounceY, R + 2, WHITE);
setFontId(FONT_TOM_THUMB);
display.setTextColor(WHITE);
display.setCursor(3, 7);
display.print("SCORE ");
if (bounceScore < 100) display.print("0");
if (bounceScore < 10) display.print("0");
display.print(bounceScore);
display.display();
if (millis() - sceneStartMs >= 6000UL) {
nextScene();
}
}
void drawSprites() {
const int16_t laneY[3] = {
0,
SCREEN_HEIGHT / 2 - BMP_H / 2,
SCREEN_HEIGHT - BMP_H
};
const float speeds[3] = {20.0f, 34.0f, 19.0f};
display.clearDisplay();
for (int16_t x = 0; x < SCREEN_WIDTH; x += 6) {
display.drawPixel(x, laneY[1] - 1, WHITE);
display.drawPixel(x, laneY[2] - 1, WHITE);
display.drawPixel(x, SCREEN_HEIGHT - BMP_H / 2, WHITE);
}
display.drawFastVLine(SCREEN_WIDTH - 2, 0, SCREEN_HEIGHT, WHITE);
for (uint8_t i = 0; i < 3; i++) {
spriteX[i] += speeds[i] * SPF;
if (spriteX[i] > SCREEN_WIDTH + 4) {
spriteX[i] = -BMP_W;
spriteLaps[i]++;
}
int16_t ix = (int16_t)spriteX[i];
int16_t iy = laneY[i];
if (ix > -BMP_W && ix < SCREEN_WIDTH) {
if (i == 0) {
display.drawBitmap(ix, iy, bitmap_python, BMP_W, BMP_H, WHITE);
} else if (i == 1) {
display.drawBitmap(ix, iy, bitmap_adafruit, BMP_W, BMP_H, WHITE);
} else {
display.drawBitmap(ix, iy, bitmap_invader, BMP_W, BMP_H, WHITE);
}
}
setFontId(FONT_TOM_THUMB);
display.setTextColor(WHITE);
display.setCursor(SCREEN_WIDTH - 18, iy + BMP_H - 2);
display.print("x");
display.print(spriteLaps[i]);
}
setFontId(FONT_TOM_THUMB);
display.setTextColor(WHITE);
display.setCursor(2, 5);
display.print("SPRITE RACE");
display.display();
if (millis() - sceneStartMs >= 7000UL) {
nextScene();
}
}
void drawCredits() {
const float SCROLL_SPEED = 18.0f;
const int16_t LINE_H = 12;
float t = sceneSeconds();
int16_t scroll = (int16_t)(t * SCROLL_SPEED);
display.clearDisplay();
for (uint8_t i = 0; i < CREDIT_COUNT; i++) {
int16_t y = SCREEN_HEIGHT - scroll + i * LINE_H + LINE_H;
if (y < -LINE_H || y > SCREEN_HEIGHT + LINE_H) {
continue;
}
setFontId(creditLines[i].font);
display.setTextColor(WHITE);
printCentered(creditLines[i].text, y);
}
display.fillRect(0, 0, SCREEN_WIDTH, 4, BLACK);
display.fillRect(0, SCREEN_HEIGHT - 4, SCREEN_WIDTH, 4, BLACK);
display.display();
unsigned long durationMs = (unsigned long)(((CREDIT_COUNT * LINE_H + SCREEN_HEIGHT) / SCROLL_SPEED + 1.0f) * 1000.0f);
if (millis() - sceneStartMs >= durationMs) {
nextScene();
}
}
void setup() {
Wire.begin();
if (!display.begin(0x3C, true)) {
while (1) {
delay(10);
}
}
display.clearDisplay();
display.display();
display.setRotation(1);
setFontId(FONT_DEFAULT);
display.setTextColor(WHITE);
display.setTextSize(1);
resetSceneState(currentScene);
lastFrameMs = millis();
}
void loop() {
unsigned long now = millis();
if (now - lastFrameMs < FRAME_MS) {
return;
}
lastFrameMs = now;
frameNo++;
switch (currentScene) {
case SCENE_INSERT_COIN:
drawInsertCoin();
break;
case SCENE_BOOT:
drawBoot();
break;
case SCENE_STARFIELD:
drawStarfield();
break;
case SCENE_BOUNCE:
drawBounce();
break;
case SCENE_SPRITES:
drawSprites();
break;
case SCENE_CREDITS:
drawCredits();
break;
}
}
import pydafruit_gfx as pyfx
import math, time, random
## OLED
## --------------------------------------------------------------
W, H = 128, 64
BLACK = 0x0000
WHITE = 0xFFFF
## SDL2
## --------------------------------------------------------------
SCALE = 6
FPS = 30
SPF = 1.0 / FPS
disp = pyfx.GFXDisplay(W, H, scale=SCALE)
if not disp.begin("pydafruit_gfx Retro OLED"):
raise RuntimeError("Failed to open SDL2 window")
def transition_wipe():
## https://hackaday.io/project/203611-ssid-silly-space-invaders-dashboard
for i in range(0, 80, 2):
if not disp.is_open():
return False
disp.handle_events()
cx = W // 2
disp.draw_circle(cx, H, i, BLACK)
disp.draw_circle(cx - 1, H, i, BLACK)
disp.draw_circle(cx, H - 1, i * 2, BLACK)
disp.draw_circle(cx, H - 2, i * 3, BLACK)
disp.flush()
time.sleep(0.01)
disp.fill_screen(BLACK)
disp.flush()
return True
def run_scene(draw_fn, duration):
t0 = time.monotonic()
frame = 0
while disp.is_open():
t = time.monotonic() - t0
if t >= duration:
return True
ft = time.monotonic()
if not disp.handle_events():
return False
draw_fn(t, frame)
disp.flush()
sleep = SPF - (time.monotonic() - ft)
if sleep > 0:
time.sleep(sleep)
frame += 1
return False
## Coin Spin
## --------------------------------------------------------------
def scene_insert_coin():
TOTAL = 256
frames_done = [0]
def draw(t, frame):
w_val = frame % 256
disp.fill_screen(BLACK)
disp.set_font("FreeSansBold9")
disp.set_text_color(WHITE)
disp.set_text_size(1)
title1 = "INSERT"
_, _, tw, th = disp.get_text_bounds(title1, 0, 0)
disp.set_cursor((W - tw) // 2, 15)
disp.print(title1)
title2 = "COIN"
_, _, tw, th = disp.get_text_bounds(title2, 0, 0)
disp.set_cursor((W - tw) // 2, 32)
disp.print(title2)
disp.set_font("TomThumb")
disp.set_text_color(WHITE)
title3 = "press Q to skip"
_, _, tw, th = disp.get_text_bounds(title3, 0, 0)
disp.set_cursor((W - tw) // 2, 53)
disp.print(title3)
slot_w = w_val % 8
slot_x = W//2 - slot_w // 2
if slot_w > 0:
disp.fill_round_rect(slot_x, 36, slot_w, 10, 2, WHITE)
if (frame // 30) % 2 == 0 and frame > 60:
disp.set_font("TomThumb")
disp.set_text_color(WHITE)
title4 = "-- INSERT COIN --"
_, _, tw, th = disp.get_text_bounds(title4, 0, 0)
disp.set_cursor((W - tw) // 2, 62)
disp.print(title4)
frames_done[0] = frame
ok = run_scene(draw, 4.0)
if ok:
transition_wipe()
return ok
## Intro
## --------------------------------------------------------------
def scene_boot():
def draw(t, frame):
disp.fill_screen(BLACK)
if t > 0.4:
disp.draw_rect(0, 0, W, H, WHITE)
disp.draw_rect(2, 2, W-4, H-4, WHITE)
if t > 0.8:
disp.set_font("FreeSansBold9")
disp.set_text_color(WHITE)
disp.set_cursor(6, 19)
disp.print("PYDAFRUIT")
disp.set_cursor(30, 35)
disp.print("GFX")
if t > 1.4:
disp.set_font("TomThumb")
# disp.set_font("FreeMono9")
disp.set_text_color(WHITE)
disp.set_cursor(8, 48)
disp.print("SDL2+pybind11")
if t > 2.0 and int(t * 3) % 2 == 0:
disp.set_font("TomThumb")
disp.set_text_color(WHITE)
disp.set_cursor(8, 58)
disp.print("< DEMO >")
return run_scene(draw, 3.5)
## star field
## --------------------------------------------------------------
def scene_starfield():
random.seed(7)
N = 70
stars = [(random.uniform(-1,1), random.uniform(-1,1),
random.uniform(0.3, 1.0)) for _ in range(N)]
cx, cy = W//2, H//2
def draw(t, frame):
disp.fill_screen(BLACK)
speed = min(t / 1.5, 1.0) * 4.0 + 0.4
for ox, oy, spd in stars:
z = ((t * spd * speed) % 2.0)
sc = z / 2.0
x = int(cx + ox * sc * W)
y = int(cy + oy * sc * H)
if not (0 <= x < W and 0 <= y < H):
continue
if sc > 0.5:
px = int(cx + ox * max(0, sc - 0.18) * W)
py = int(cy + oy * max(0, sc - 0.18) * H)
disp.draw_line(px, py, x, y, WHITE)
else:
disp.draw_pixel(x, y, WHITE)
disp.set_font("TomThumb")
disp.set_text_color(WHITE)
disp.set_cursor(2, H-1)
disp.print("STARFIELD")
return run_scene(draw, 5.0)
## ball bounce
## --------------------------------------------------------------
def scene_bounce():
R = 5
bx = float(W//2)
by = float(H//2)
vx = 38.0
vy = 27.0
score = 0
trail = []
TRAIL = 14
def draw(t, frame):
nonlocal bx, by, vx, vy, score
bx += vx * SPF
by += vy * SPF
bounced = False
if bx - R < 0: bx = float(R); vx = abs(vx); bounced=True
if bx + R >= W: bx = float(W-R-1); vx = -abs(vx); bounced=True
if by - R < 0: by = float(R); vy = abs(vy); bounced=True
if by + R >= H: by = float(H-R-1); vy = -abs(vy); bounced=True
if bounced:
score += 1
trail.append((int(bx), int(by)))
if len(trail) > TRAIL:
trail.pop(0)
disp.fill_screen(BLACK)
disp.draw_rect(0, 0, W, H, WHITE)
for i, (tx, ty) in enumerate(trail[:-1]):
if i % 2 == 0:
disp.draw_pixel(tx, ty, WHITE)
disp.fill_circle(int(bx), int(by), R, WHITE)
disp.draw_circle(int(bx), int(by), R+2, WHITE)
disp.set_font("TomThumb")
disp.set_text_color(WHITE)
disp.set_cursor(3, 7)
disp.print(f"SCORE {score:03d}")
return run_scene(draw, 6.0)
## sprite race
## --------------------------------------------------------------
BMP_W = BMP_H = 32
bitmap_python = bytes([
0x00,0x1F,0xF0,0x00,0x00,0x7F,0xFC,0x00,0x00,0x7F,0xFE,0x00,0x00,0xC7,0xFF,0x00,
0x00,0xC7,0xFF,0x00,0x00,0xFF,0xFF,0x00,0x00,0xFF,0xFF,0x00,0x00,0x00,0xFF,0x00,
0x0F,0xFF,0xFF,0x78,0x3F,0xFF,0xFF,0x7C,0x7F,0xFF,0xFF,0x7E,0x7F,0xFF,0xFF,0x7E,
0xFF,0xFF,0xFF,0x7F,0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,0xFC,0xFF,0xFF,0xF0,0x01,0xFF,
0xFF,0x80,0x0F,0xFF,0xFF,0x3F,0xFF,0xFF,0xFE,0x7F,0xFF,0xFF,0xFE,0xFF,0xFF,0xFF,
0x7E,0xFF,0xFF,0xFE,0x7E,0xFF,0xFF,0xFE,0x3E,0xFF,0xFF,0xFC,0x1E,0xFF,0xFF,0xF0,
0x00,0xFF,0x00,0x00,0x00,0xFF,0xFF,0x00,0x00,0xFF,0xFF,0x00,0x00,0xFF,0xE3,0x00,
0x00,0xFF,0xE3,0x00,0x00,0x7F,0xFE,0x00,0x00,0x3F,0xFE,0x00,0x00,0x0F,0xF8,0x00,
])
bitmap_adafruit = bytes([
0x00,0x00,0x60,0x00,0x00,0x00,0xE0,0x00,0x00,0x01,0xE0,0x00,0x00,0x01,0xF0,0x00,
0x00,0x03,0xF0,0x00,0x00,0x07,0xF0,0x00,0x00,0x07,0xF8,0x00,0x00,0x0F,0xF8,0x00,
0x7F,0x0F,0xF8,0x00,0xFF,0xEF,0xF8,0x00,0xFF,0xFF,0xF8,0x00,0x7F,0xFE,0x7F,0xC0,
0x3F,0xFE,0x7F,0xF8,0x1F,0xFE,0x7F,0xFF,0x1F,0xC6,0xFF,0xFF,0x0F,0xE3,0xC7,0xFE,
0x07,0xFF,0x87,0xFC,0x01,0xFF,0xFF,0xF0,0x01,0xF3,0x7F,0xE0,0x03,0xE3,0x3F,0x80,
0x07,0xE7,0x3C,0x00,0x07,0xFF,0xBE,0x00,0x07,0xFF,0xFE,0x00,0x0F,0xFF,0xFE,0x00,
0x0F,0xFF,0xFF,0x00,0x0F,0xF9,0xFF,0x00,0x1F,0xF1,0xFF,0x00,0x1F,0x80,0xFF,0x00,
0x1C,0x00,0x7F,0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x0F,0x00,0x00,0x00,0x06,0x00,
])
bitmap_invader = bytes([
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0xE0,0x07,0xC0,
0x03,0xE0,0x07,0xC0,0x03,0xE0,0x07,0xC0,0x03,0xFC,0x3F,0xC0,0x03,0xFC,0x3F,0xC0,
0x00,0x7C,0x3E,0x00,0x00,0x7C,0x3E,0x00,0x03,0xFF,0xFF,0xC0,0x03,0xFF,0xFF,0xC0,
0x03,0xFF,0xFF,0xC0,0x1F,0xFF,0xFF,0xF8,0x1F,0xFF,0xFF,0xF8,0x1F,0x83,0xC1,0xF8,
0xFF,0x83,0xC1,0xFF,0xFF,0x83,0xC1,0xFF,0xFF,0x83,0xC1,0xFF,0xFF,0x83,0xC1,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFB,0xFF,0xFF,0xDF,0xFB,0xFF,0xFF,0xDF,0xFB,0xFF,0xFF,0xDF,
0xFB,0xE0,0x07,0xDF,0xFB,0xFE,0x7F,0xDF,0xFB,0xFE,0x7F,0xDF,0x00,0x7E,0x7E,0x00,
0x00,0x7E,0x7E,0x00,0x00,0x7E,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
])
def scene_sprites():
LANE_Y = [0, H//2 - BMP_H//2, H - BMP_H]
SPEEDS = [20.0, 34.0, 19.0]
BMPS = [bitmap_python, bitmap_adafruit, bitmap_invader]
xs = [float(-BMP_W - 20), float(-BMP_W - 60), float(-BMP_W - 40)]
laps = [0, 0, 0]
def draw(t, frame):
disp.fill_screen(BLACK)
for x in range(0, W, 6):
disp.draw_pixel(x, LANE_Y[1] - 1, WHITE)
disp.draw_pixel(x, LANE_Y[2] - 1, WHITE)
disp.draw_pixel(x, H - BMP_H//2, WHITE)
disp.draw_fast_vline(W-2, 0, H, WHITE)
for i in range(3):
xs[i] += SPEEDS[i] * SPF
if xs[i] > W + 4:
xs[i] = float(-BMP_W)
laps[i] += 1
ix = int(xs[i])
iy = LANE_Y[i]
if -BMP_W < ix < W:
disp.draw_bitmap(ix, iy, BMPS[i], BMP_W, BMP_H, WHITE)
disp.set_font("TomThumb")
disp.set_text_color(WHITE)
disp.set_cursor(W - 18, iy + BMP_H - 2)
disp.print(f"x{laps[i]}")
disp.set_font("TomThumb")
disp.set_text_color(WHITE)
disp.set_cursor(2, 5)
disp.print("SPRITE RACE")
return run_scene(draw, 7.0)
## star wars credits
## --------------------------------------------------------------
def scene_credits():
lines = [
("PYDAFRUIT", "FreeSansBold9"),
("", "Org01"),
("GFX", "FreeSansBold9"),
("", "Org01"),
("SDL2 backend", "Org01"),
("pybind11 bindings", "Org01"),
("Adafruit_GFX port", "Org01"),
("", "Org01"),
("draw_pixel", "Org01"),
("draw_line", "Org01"),
("fill_circle", "Org01"),
("draw_bitmap", "Org01"),
("set_font", "Org01"),
("get_text_bounds", "Org01"),
("", "TomThumb"),
("github:pydafruitGFX", "TomThumb"),
("MIT License", "TomThumb"),
("", "TomThumb"),
("* FIN *", "FreeSansBold9"),
("thanks!", "TomThumb"),
]
SCROLL_SPEED = 18.0
LINE_H = 12
def draw(t, frame):
disp.fill_screen(BLACK)
scroll = t * SCROLL_SPEED
for i, (text, font) in enumerate(lines):
y = H - int(scroll) + i * LINE_H + LINE_H
if y < -LINE_H or y > H + LINE_H:
continue
disp.set_font(font)
_, _, tw, _ = disp.get_text_bounds(text, 0, y)
disp.set_text_color(WHITE)
disp.set_cursor((W - tw) // 2, y)
disp.print(text)
disp.fill_rect(0, 0, W, 4, BLACK)
disp.fill_rect(0, H-4, W, 4, BLACK)
duration = (len(lines) * LINE_H + H) / SCROLL_SPEED + 1.0
return run_scene(draw, duration)
## main loop
## --------------------------------------------------------------
scenes = [
scene_insert_coin,
scene_boot,
scene_starfield,
scene_bounce,
scene_sprites,
scene_credits]
running = True
while running:
for fn in scenes:
if not disp.is_open():
running = False
break
if not fn():
running = False
break





Comments