Mystify Demo, TwoEleven updates & Snake (#21)

* **New Features**
  * Added Mystify screensaver demo with animated polygons and trails
  * Added Snake game with multiple difficulties, high-score persistence, and multi-input (touch/keyboard) support
  * Added CLI tool for building, packaging, and deploying apps (end-to-end build/install/run workflow)
  * Per-grid-size high-score persistence added to 2048 app; expanded keyboard controls (WASD and device-specific mappings)

* **Documentation**
  * Added Snake README with gameplay, controls, and usage instructions
This commit is contained in:
Shadowtrance
2026-02-07 08:35:13 +10:00
committed by GitHub
parent d31b6b48a4
commit 46cf00d92e
41 changed files with 4025 additions and 131 deletions
+9
View File
@@ -0,0 +1,9 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRC_DIRS "Source"
# Library headers must be included directly,
# because all regular dependencies get stripped by elf_loader's cmake script
INCLUDE_DIRS "Include" "../../../Libraries/TactilityCpp/Include"
REQUIRES TactilitySDK
)
@@ -0,0 +1,6 @@
#pragma once
#include "drivers/DisplayDriver.h"
#include "drivers/TouchDriver.h"
void runApplication(DisplayDriver* display, TouchDriver* touch);
+317
View File
@@ -0,0 +1,317 @@
#pragma once
#include "PixelBuffer.h"
#include "drivers/DisplayDriver.h"
#include <cmath>
#include <cstdlib>
#include <esp_random.h>
/**
* Mystify Screensaver Demo
*
* Classic Windows-style mystify screensaver with bouncing polygons and trailing edges.
* Adapted to work with DisplayDriver and PixelBuffer abstractions.
*
* Usage:
* MystifyDemo mystify;
* mystify.init(display);
*
* while (!shouldExit) {
* mystify.update();
* }
*/
class MystifyDemo {
public:
static constexpr int NUM_POLYGONS = 2;
static constexpr int NUM_VERTICES = 4;
static constexpr int TRAIL_LENGTH = 8;
static constexpr int COLOR_CHANGE_INTERVAL = 200; // Frames between color changes
static constexpr int STRIP_HEIGHT = 16; // Draw in strips to avoid SPI buffer overflow
MystifyDemo() = default;
~MystifyDemo() { deinit(); }
// Non-copyable, non-movable (owns PixelBuffer)
MystifyDemo(const MystifyDemo&) = delete;
MystifyDemo& operator=(const MystifyDemo&) = delete;
MystifyDemo(MystifyDemo&&) = delete;
MystifyDemo& operator=(MystifyDemo&&) = delete;
bool init(DisplayDriver* display) {
if (!display) {
return false;
}
display_ = display;
width_ = display->getWidth();
height_ = display->getHeight();
if (width_ <= 0 || height_ <= 0) {
return false;
}
// Seed random generator with hardware entropy
srand(static_cast<unsigned>(esp_random()));
// Allocate full-screen framebuffer
void* mem = malloc(sizeof(PixelBuffer));
if (!mem) {
return false;
}
framebuffer_ = new(mem) PixelBuffer(width_, height_, display->getColorFormat());
initPolygons();
return true;
}
void deinit() {
if (framebuffer_) {
framebuffer_->~PixelBuffer();
free(framebuffer_);
framebuffer_ = nullptr;
}
display_ = nullptr;
}
void update() {
if (!framebuffer_ || !display_) return;
// Clear framebuffer to black
framebuffer_->clear();
// Update and draw each polygon
for (int p = 0; p < NUM_POLYGONS; p++) {
updatePolygon(polygons_[p]);
drawPolygon(polygons_[p]);
}
// Send framebuffer to display in strips (full screen is too large for single SPI transaction)
display_->lock();
for (int y = 0; y < height_; y += STRIP_HEIGHT) {
int stripEnd = (y + STRIP_HEIGHT > height_) ? height_ : y + STRIP_HEIGHT;
display_->drawBitmap(0, y, width_, stripEnd, framebuffer_->getDataAtRow(y));
}
display_->unlock();
}
private:
// Smooth sub-pixel movement with floats
struct Vertex {
float x = 0;
float y = 0;
float dx = 0;
float dy = 0;
};
struct Polygon {
Vertex vertices[NUM_VERTICES];
// History: [trail_index][vertex_index] = {x, y}
int16_t historyX[TRAIL_LENGTH][NUM_VERTICES];
int16_t historyY[TRAIL_LENGTH][NUM_VERTICES];
uint8_t colorIndex;
int historyHead = 0;
bool historyFull = false;
int colorChangeCounter = 0;
};
// Vibrant colors as RGB888 for format-agnostic rendering
struct Color {
uint8_t r, g, b;
};
static constexpr Color COLOR_POOL[] = {
{255, 0, 255}, // Magenta
{0, 255, 255}, // Cyan
{255, 255, 0}, // Yellow
{255, 128, 0}, // Orange
{0, 255, 128}, // Spring green
{128, 0, 255}, // Purple
{255, 64, 128}, // Hot pink
{128, 255, 0}, // Lime
};
static constexpr int COLOR_POOL_SIZE = sizeof(COLOR_POOL) / sizeof(COLOR_POOL[0]);
DisplayDriver* display_ = nullptr;
PixelBuffer* framebuffer_ = nullptr;
uint16_t width_ = 0;
uint16_t height_ = 0;
Polygon polygons_[NUM_POLYGONS];
static float randomFloat(float min, float max) {
return min + (max - min) * (static_cast<float>(rand()) / static_cast<float>(RAND_MAX));
}
void initPolygons() {
for (int p = 0; p < NUM_POLYGONS; p++) {
Polygon& polygon = polygons_[p];
// Pick random color from pool
polygon.colorIndex = rand() % COLOR_POOL_SIZE;
polygon.historyHead = 0;
polygon.historyFull = false;
// Stagger color changes so polygons don't change simultaneously
polygon.colorChangeCounter = rand() % COLOR_CHANGE_INTERVAL;
// Initialize vertices with random positions and velocities
for (int v = 0; v < NUM_VERTICES; v++) {
Vertex& vertex = polygon.vertices[v];
vertex.x = static_cast<float>(rand() % width_);
vertex.y = static_cast<float>(rand() % height_);
// Speed range for smooth movement
vertex.dx = randomFloat(0.8f, 2.0f);
vertex.dy = randomFloat(0.8f, 2.0f);
if (rand() % 2) vertex.dx = -vertex.dx;
if (rand() % 2) vertex.dy = -vertex.dy;
// Ensure dx != dy for more interesting movement patterns
if (std::fabs(vertex.dx - vertex.dy) < 0.3f) {
vertex.dy += (vertex.dy > 0 ? 0.5f : -0.5f);
}
}
// Initialize history with current positions
for (int t = 0; t < TRAIL_LENGTH; t++) {
for (int v = 0; v < NUM_VERTICES; v++) {
polygon.historyX[t][v] = static_cast<int16_t>(polygon.vertices[v].x);
polygon.historyY[t][v] = static_cast<int16_t>(polygon.vertices[v].y);
}
}
}
}
void updatePolygon(Polygon& polygon) {
constexpr float minSpeed = 0.5f;
constexpr float maxSpeed = 2.5f;
// Periodic color change
polygon.colorChangeCounter++;
if (polygon.colorChangeCounter >= COLOR_CHANGE_INTERVAL) {
polygon.colorChangeCounter = 0;
// Pick a different color
uint8_t newColor;
do {
newColor = rand() % COLOR_POOL_SIZE;
} while (newColor == polygon.colorIndex && COLOR_POOL_SIZE > 1);
polygon.colorIndex = newColor;
}
// Move vertices
for (int v = 0; v < NUM_VERTICES; v++) {
Vertex& vertex = polygon.vertices[v];
vertex.x += vertex.dx;
vertex.y += vertex.dy;
// Bounce off edges with slight angle variation for organic movement
if (vertex.x <= 0) {
vertex.x = 0;
vertex.dx = std::fabs(vertex.dx);
vertex.dy *= (1.0f + randomFloat(-0.1f, 0.1f));
} else if (vertex.x >= width_ - 1) {
vertex.x = static_cast<float>(width_ - 1);
vertex.dx = -std::fabs(vertex.dx);
vertex.dy *= (1.0f + randomFloat(-0.1f, 0.1f));
}
if (vertex.y <= 0) {
vertex.y = 0;
vertex.dy = std::fabs(vertex.dy);
vertex.dx *= (1.0f + randomFloat(-0.1f, 0.1f));
} else if (vertex.y >= height_ - 1) {
vertex.y = static_cast<float>(height_ - 1);
vertex.dy = -std::fabs(vertex.dy);
vertex.dx *= (1.0f + randomFloat(-0.1f, 0.1f));
}
// Clamp speeds to prevent runaway acceleration or stalling
auto clampSpeed = [minSpeed, maxSpeed](float& speed) {
float sign = (speed >= 0) ? 1.0f : -1.0f;
float absSpeed = std::fabs(speed);
if (absSpeed < minSpeed) absSpeed = minSpeed;
if (absSpeed > maxSpeed) absSpeed = maxSpeed;
speed = sign * absSpeed;
};
clampSpeed(vertex.dx);
clampSpeed(vertex.dy);
}
// Advance history ring buffer
polygon.historyHead = (polygon.historyHead + 1) % TRAIL_LENGTH;
if (polygon.historyHead == 0) {
polygon.historyFull = true;
}
// Store current positions
for (int v = 0; v < NUM_VERTICES; v++) {
polygon.historyX[polygon.historyHead][v] = static_cast<int16_t>(polygon.vertices[v].x);
polygon.historyY[polygon.historyHead][v] = static_cast<int16_t>(polygon.vertices[v].y);
}
}
void drawPolygon(const Polygon& polygon) {
const Color& baseColor = COLOR_POOL[polygon.colorIndex];
// Draw trail from oldest to newest (so newest is on top)
for (int t = TRAIL_LENGTH - 1; t >= 0; t--) {
int histIndex = polygon.historyHead - t;
if (histIndex < 0) histIndex += TRAIL_LENGTH;
// Skip if we don't have enough history yet
if (!polygon.historyFull && histIndex > polygon.historyHead) {
continue;
}
// Calculate brightness for this trail frame (older = dimmer)
int brightness = 255 - (t * 230 / TRAIL_LENGTH);
if (brightness < 25) brightness = 25;
// Scale color by brightness
uint8_t r = (baseColor.r * brightness) / 255;
uint8_t g = (baseColor.g * brightness) / 255;
uint8_t b = (baseColor.b * brightness) / 255;
// Draw edges connecting vertices
for (int e = 0; e < NUM_VERTICES; e++) {
int nextVertex = (e + 1) % NUM_VERTICES;
int x0 = polygon.historyX[histIndex][e];
int y0 = polygon.historyY[histIndex][e];
int x1 = polygon.historyX[histIndex][nextVertex];
int y1 = polygon.historyY[histIndex][nextVertex];
drawLine(x0, y0, x1, y1, r, g, b);
}
}
}
// Bresenham's line algorithm
void drawLine(int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t b) {
int dx = std::abs(x1 - x0);
int dy = std::abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;
while (true) {
setPixel(x0, y0, r, g, b);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
void setPixel(int x, int y, uint8_t r, uint8_t g, uint8_t b) {
if (x >= 0 && x < width_ && y >= 0 && y < height_) {
framebuffer_->setPixel(x, y, r, g, b);
}
}
};
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <esp_log.h>
#include "drivers/Colors.h"
#include <cstring>
#include <tt_hal_display.h>
class PixelBuffer {
uint16_t pixelWidth;
uint16_t pixelHeight;
ColorFormat colorFormat;
uint8_t* data;
public:
PixelBuffer(uint16_t pixelWidth, uint16_t pixelHeight, ColorFormat colorFormat) :
pixelWidth(pixelWidth),
pixelHeight(pixelHeight),
colorFormat(colorFormat)
{
data = static_cast<uint8_t*>(malloc(pixelWidth * pixelHeight * getPixelSize()));
assert(data != nullptr);
}
~PixelBuffer() {
free(data);
}
uint16_t getPixelWidth() const {
return pixelWidth;
}
uint16_t getPixelHeight() const {
return pixelHeight;
}
ColorFormat getColorFormat() const {
return colorFormat;
}
void* getData() const {
return data;
}
uint32_t getDataSize() const {
return pixelWidth * pixelHeight * getPixelSize();
}
void* getDataAtRow(uint16_t row) const {
auto address = reinterpret_cast<uint32_t>(data) + (row * getRowDataSize());
return reinterpret_cast<void*>(address);
}
uint16_t getRowDataSize() const {
return pixelWidth * getPixelSize();
}
uint8_t getPixelSize() const {
switch (colorFormat) {
case COLOR_FORMAT_MONOCHROME:
return 1;
case COLOR_FORMAT_BGR565:
case COLOR_FORMAT_BGR565_SWAPPED:
case COLOR_FORMAT_RGB565:
case COLOR_FORMAT_RGB565_SWAPPED:
return 2;
case COLOR_FORMAT_RGB888:
return 3;
default:
// TODO: Crash with error
return 0;
}
}
uint8_t* getPixelAddress(uint16_t x, uint16_t y) const {
uint32_t offset = ((y * getPixelWidth()) + x) * getPixelSize();
uint32_t address = reinterpret_cast<uint32_t>(data) + offset;
return reinterpret_cast<uint8_t*>(address);
}
void setPixel(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b) const {
auto address = getPixelAddress(x, y);
switch (colorFormat) {
case COLOR_FORMAT_MONOCHROME:
*address = (uint8_t)((uint16_t)r + (uint16_t)g + (uint16_t)b / 3);
break;
case COLOR_FORMAT_BGR565:
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
break;
case COLOR_FORMAT_BGR565_SWAPPED: {
// TODO: Make proper conversion function
Colors::rgb888ToBgr565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address;
*address = *(address + 1);
*(address + 1) = temp;
break;
}
case COLOR_FORMAT_RGB565: {
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
break;
}
case COLOR_FORMAT_RGB565_SWAPPED: {
// TODO: Make proper conversion function
Colors::rgb888ToRgb565(r, g, b, reinterpret_cast<uint16_t*>(address));
uint8_t temp = *address;
*address = *(address + 1);
*(address + 1) = temp;
break;
}
case COLOR_FORMAT_RGB888: {
uint8_t pixel[3] = { r, g, b };
memcpy(address, pixel, 3);
break;
}
default:
// NO-OP
break;
}
}
void clear(int value = 0) const {
memset(data, value, getDataSize());
}
};
@@ -0,0 +1,35 @@
#pragma once
class Colors {
public:
static void rgb888ToRgb565(uint8_t red, uint8_t green, uint8_t blue, uint16_t* rgb565) {
uint16_t _rgb565 = (red >> 3);
_rgb565 = (_rgb565 << 6) | (green >> 2);
_rgb565 = (_rgb565 << 5) | (blue >> 3);
*rgb565 = _rgb565;
}
static void rgb888ToBgr565(uint8_t red, uint8_t green, uint8_t blue, uint16_t* bgr565) {
uint16_t _bgr565 = (blue >> 3);
_bgr565 = (_bgr565 << 6) | (green >> 2);
_bgr565 = (_bgr565 << 5) | (red >> 3);
*bgr565 = _bgr565;
}
static void rgb565ToRgb888(uint16_t rgb565, uint32_t* rgb888) {
uint32_t _rgb565 = rgb565;
uint8_t b = (_rgb565 >> 8) & 0xF8;
uint8_t g = (_rgb565 >> 3) & 0xFC;
uint8_t r = (_rgb565 << 3) & 0xF8;
uint8_t* r8p = reinterpret_cast<uint8_t*>(rgb888);
uint8_t* g8p = r8p + 1;
uint8_t* b8p = r8p + 2;
*r8p = r | ((r >> 3) & 0x7);
*g8p = g | ((g >> 2) & 0x3);
*b8p = b | ((b >> 3) & 0x7);
}
};
@@ -0,0 +1,49 @@
#pragma once
#include <cassert>
#include <tt_hal_display.h>
#include <Tactility/kernel/Kernel.h>
/**
* Wrapper for tt_hal_display_driver_*
*/
class DisplayDriver {
DisplayDriverHandle handle = nullptr;
public:
explicit DisplayDriver(DeviceId id) {
assert(tt_hal_display_driver_supported(id));
handle = tt_hal_display_driver_alloc(id);
assert(handle != nullptr);
}
~DisplayDriver() {
tt_hal_display_driver_free(handle);
}
bool lock(TickType_t timeout = tt::kernel::MAX_TICKS) const {
return tt_hal_display_driver_lock(handle, timeout);
}
void unlock() const {
tt_hal_display_driver_unlock(handle);
}
uint16_t getWidth() const {
return tt_hal_display_driver_get_pixel_width(handle);
}
uint16_t getHeight() const {
return tt_hal_display_driver_get_pixel_height(handle);
}
ColorFormat getColorFormat() const {
return tt_hal_display_driver_get_colorformat(handle);
}
void drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) const {
tt_hal_display_driver_draw_bitmap(handle, xStart, yStart, xEnd, yEnd, pixelData);
}
};
@@ -0,0 +1,28 @@
#pragma once
#include <cassert>
#include <tt_hal_touch.h>
/**
* Wrapper for tt_hal_touch_driver_*
*/
class TouchDriver {
TouchDriverHandle handle = nullptr;
public:
explicit TouchDriver(DeviceId id) {
assert(tt_hal_touch_driver_supported(id));
handle = tt_hal_touch_driver_alloc(id);
assert(handle != nullptr);
}
~TouchDriver() {
tt_hal_touch_driver_free(handle);
}
bool getTouchedPoints(uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* count, uint8_t maxCount) const {
return tt_hal_touch_driver_get_touched_points(handle, x, y, strength, count, maxCount);
}
};
@@ -0,0 +1,36 @@
#include "Application.h"
#include "MystifyDemo.h"
#include "PixelBuffer.h"
#include "esp_log.h"
#include <Tactility/kernel/Kernel.h>
constexpr auto TAG = "Application";
constexpr int MYSTIFY_FRAME_DELAY_MS = 50; // ~20 FPS for smooth animation
static bool isTouched(TouchDriver* touch) {
uint16_t x, y, strength;
uint8_t pointCount = 0;
return touch->getTouchedPoints(&x, &y, &strength, &pointCount, 1);
}
void runApplication(DisplayDriver* display, TouchDriver* touch) {
// Run the Mystify screensaver demo
MystifyDemo mystify;
if (!mystify.init(display)) {
ESP_LOGE(TAG, "Failed to initialize MystifyDemo");
return;
}
ESP_LOGI(TAG, "Starting Mystify demo - touch to exit");
do {
mystify.update();
// Frame rate limiter - ~20 FPS for smooth animation
tt::kernel::delayTicks(tt::kernel::millisToTicks(MYSTIFY_FRAME_DELAY_MS));
} while (!isTouched(touch));
ESP_LOGI(TAG, "Mystify demo ended");
}
+101
View File
@@ -0,0 +1,101 @@
#include "Application.h"
#include "drivers/DisplayDriver.h"
#include "drivers/TouchDriver.h"
#include <esp_log.h>
#include <tt_app.h>
#include <tt_app_alertdialog.h>
#include <tt_lvgl.h>
constexpr auto TAG = "Main";
/** Find a DisplayDevice that supports the DisplayDriver interface */
static bool findUsableDisplay(DeviceId& deviceId) {
uint16_t display_count = 0;
if (!tt_hal_device_find(DEVICE_TYPE_DISPLAY, &deviceId, &display_count, 1)) {
ESP_LOGE(TAG, "No display device found");
return false;
}
if (!tt_hal_display_driver_supported(deviceId)) {
ESP_LOGE(TAG, "Display doesn't support driver mode");
return false;
}
return true;
}
/** Find a TouchDevice that supports the TouchDriver interface */
static bool findUsableTouch(DeviceId& deviceId) {
uint16_t touch_count = 0;
if (!tt_hal_device_find(DEVICE_TYPE_TOUCH, &deviceId, &touch_count, 1)) {
ESP_LOGE(TAG, "No touch device found");
return false;
}
if (!tt_hal_touch_driver_supported(deviceId)) {
ESP_LOGE(TAG, "Touch doesn't support driver mode");
return false;
}
return true;
}
static void onCreate(AppHandle appHandle, void* data) {
DeviceId display_id;
if (!findUsableDisplay(display_id)) {
tt_app_stop();
tt_app_alertdialog_start("Error", "The display doesn't support the required features.", nullptr, 0);
return;
}
DeviceId touch_id;
if (!findUsableTouch(touch_id)) {
tt_app_stop();
tt_app_alertdialog_start("Error", "The touch driver doesn't support the required features.", nullptr, 0);
return;
}
// Stop LVGL first (because it's currently using the drivers we want to use)
tt_lvgl_stop();
ESP_LOGI(TAG, "Creating display driver");
auto display = new DisplayDriver(display_id);
ESP_LOGI(TAG, "Creating touch driver");
auto touch = new TouchDriver(touch_id);
// Run the main logic
ESP_LOGI(TAG, "Running application");
runApplication(display, touch);
ESP_LOGI(TAG, "Cleanup display driver");
delete display;
ESP_LOGI(TAG, "Cleanup touch driver");
delete touch;
ESP_LOGI(TAG, "Stopping application");
tt_app_stop();
}
static void onDestroy(AppHandle appHandle, void* data) {
// Restart LVGL to resume rendering of regular apps
if (!tt_lvgl_is_started()) {
ESP_LOGI(TAG, "Restarting LVGL");
tt_lvgl_start();
}
}
extern "C" {
int main(int argc, char* argv[]) {
tt_app_register((AppRegistration) {
.onCreate = onCreate,
.onDestroy = onDestroy
});
return 0;
}
}