46cf00d92e
* **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
318 lines
10 KiB
C++
318 lines
10 KiB
C++
#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);
|
|
}
|
|
}
|
|
};
|