Created
September 1, 2025 00:18
-
-
Save pardeike/2ec06a660e49ab937828573c6979c20a to your computer and use it in GitHub Desktop.
LED Animation for 6x8 board
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "SPI.h" | |
#include "Adafruit_WS2801.h" | |
const int numStrands = 1; // number of LED strands to address | |
const int strandLEDs = 50; //Number of LEDs per strand | |
const int numLED = strandLEDs*numStrands; //Total number of LEDs | |
uint8_t analogPin = 14; // A0 - Potentiometer wiper (middle terminal) connected to analog pin 0 | |
uint8_t buttonPin = 15; // A1 | |
uint8_t dataPin = 17; // A3 - Analog pins used to simplify wiring. By using Analog pins we only need to have wires on one side of the Arduino Nano | |
uint8_t clockPin = 19; // A5 | |
Adafruit_WS2801 strip = Adafruit_WS2801(numLED, dataPin, clockPin); | |
int nx = 0, ny = 0; | |
int pCount = strandLEDs - 2; | |
int xMax = 6, yMax = 8; | |
int mapping[] = { 7, 6, 5, 4, 3, 2, | |
8, 9, 10, 11, 12, 13, | |
19, 18, 17, 16, 15, 14, | |
20, 21, 22, 23, 24, 25, | |
31, 30, 29, 28, 27, 26, | |
32, 33, 34, 35, 36, 37, | |
43, 42, 41, 40, 39, 38, | |
44, 45, 46, 47, 48, 49, | |
}; | |
// input color components 0 to 255 to get a color value | |
uint32_t color(byte r, byte g, byte b) { | |
uint32_t c; c = r; c <<= 8; c |= g; c <<= 8; c |= b; | |
return c; | |
} | |
// input a value 0 to 255 to get a color value | |
// the colours are a transition r - g -b - back to r | |
uint32_t wheel(byte WheelPos) | |
{ | |
if (WheelPos < 85) { | |
return color(WheelPos * 3, 255 - WheelPos * 3, 0); | |
} | |
else if (WheelPos < 170) { | |
WheelPos -= 85; | |
return color(255 - WheelPos * 3, 0, WheelPos * 3); | |
} | |
else { | |
WheelPos -= 170; | |
return color(0, WheelPos * 3, 255 - WheelPos * 3); | |
} | |
} | |
// sets a pixel at x/y to a specific rgb color | |
void pixel(int x, int y, byte r, byte g, byte b) { | |
strip.setPixelColor(mapping[x + y * 6], color(r, g, b)); | |
} | |
// sets a pixel at x/y to a color value | |
void pixel(int x, int y, uint32_t c) { | |
strip.setPixelColor(mapping[x + y * 6], c); | |
} | |
// --------------------------------------------------------------------------------------------------- | |
const uint8_t W = 6, H = 8; | |
inline int IDX(int x, int y) { return x + y * W; } | |
// ---- Small framebuffer (for trail-based effects) | |
uint8_t fbR[W * H], fbG[W * H], fbB[W * H]; | |
inline uint8_t u8satAdd(uint8_t a, uint8_t b) { | |
uint16_t s = a + b; return (s > 255) ? 255 : s; | |
} | |
inline uint8_t clampU8(int v) { return v < 0 ? 0 : (v > 255 ? 255 : v); } | |
void fbClear() { for (int i = 0; i < W * H; i++) fbR[i] = fbG[i] = fbB[i] = 0; } | |
void fbFade(uint8_t amt) { | |
for (int i = 0; i < W * H; i++) { | |
fbR[i] = fbR[i] > amt ? fbR[i] - amt : 0; | |
fbG[i] = fbG[i] > amt ? fbG[i] - amt : 0; | |
fbB[i] = fbB[i] > amt ? fbB[i] - amt : 0; | |
} | |
} | |
void fbAddRGB(int x, int y, uint8_t r, uint8_t g, uint8_t b) { | |
if ((unsigned)x >= W || (unsigned)y >= H) return; | |
int i = IDX(x, y); | |
fbR[i] = u8satAdd(fbR[i], r); | |
fbG[i] = u8satAdd(fbG[i], g); | |
fbB[i] = u8satAdd(fbB[i], b); | |
} | |
void fbAddColorScaled(int x, int y, uint32_t c, uint8_t scale) { | |
uint8_t r = (c >> 16) & 0xFF, g = (c >> 8) & 0xFF, b = c & 0xFF; | |
r = (uint16_t)r * scale / 255; | |
g = (uint16_t)g * scale / 255; | |
b = (uint16_t)b * scale / 255; | |
fbAddRGB(x, y, r, g, b); | |
} | |
void fbBlit() { | |
for (int y = 0; y < H; y++) | |
for (int x = 0; x < W; x++) { | |
int i = IDX(x, y); | |
pixel(x, y, fbR[i], fbG[i], fbB[i]); | |
} | |
} | |
uint32_t dimColor(uint32_t c, uint8_t scale) { | |
uint8_t r = (c >> 16) & 0xFF, g = (c >> 8) & 0xFF, b = c & 0xFF; | |
r = (uint16_t)r * scale / 255; | |
g = (uint16_t)g * scale / 255; | |
b = (uint16_t)b * scale / 255; | |
return color(r, g, b); | |
} | |
void clearPixels() { | |
for (int y = 0; y < H; y++) | |
for (int x = 0; x < W; x++) | |
pixel(x, y, 0, 0, 0); | |
} | |
bool myButtonPressed() { // robust rising-edge detector | |
static uint8_t last = LOW; | |
uint8_t cur = digitalRead(buttonPin); | |
bool pressed = (cur == HIGH && last == LOW); | |
last = cur; | |
return pressed; | |
} | |
// ---- Epoch lets modes re-init on switch | |
static uint16_t modeEpoch = 0; | |
// ---- Plasma rainbow | |
void renderPlasma(int pot) { | |
static uint16_t tEpoch = 0; static uint16_t phase = 0; | |
if (tEpoch != modeEpoch) { phase = 0; tEpoch = modeEpoch; } | |
phase += map(pot, 0, 1023, 1, 6); | |
float tt = phase * 0.06f; | |
for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { | |
float xx = x - 2.5f, yy = y - 3.5f; | |
float v = sin(xx * 0.90f + tt) | |
+ sin(yy * 0.62f - tt * 0.8f) | |
+ sin((xx + yy) * 0.48f + tt * 1.3f); | |
uint8_t hue = (uint8_t)(v * 42.5f + 128.0f); | |
float rad = sqrt(xx * xx + yy * yy); | |
uint8_t br = clampU8(255 - (int)(rad * 60.0f)); | |
pixel(x, y, dimColor(wheel(hue), br)); | |
} | |
} | |
// ---- Rotating swirl | |
void renderSwirl(int pot) { | |
static uint16_t tEpoch = 0; static uint16_t rot = 0; | |
if (tEpoch != modeEpoch) { rot = 0; tEpoch = modeEpoch; } | |
rot += map(pot, 0, 1023, 1, 4); | |
float t = rot * 0.055f; | |
const float invTau = 255.0f / (6.2831853f); | |
for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { | |
float dx = x - 2.5f, dy = y - 3.5f; | |
float ang = atan2(dy, dx) + t; | |
int hue = (int)(ang * invTau) & 255; | |
float r = sqrt(dx * dx + dy * dy); | |
float pulse = 0.5f + 0.5f * sin(r * 3.2f - t * 2.0f); | |
uint8_t br = clampU8(40 + (int)(pulse * 215.0f)); | |
pixel(x, y, dimColor(wheel((uint8_t)hue), br)); | |
} | |
} | |
// ---- Lissajous comet (trails) | |
void renderComet(int pot) { | |
static uint16_t tEpoch = 0; static uint16_t t = 0; | |
if (tEpoch != modeEpoch) { fbClear(); t = 0; tEpoch = modeEpoch; } | |
fbFade(map(pot, 0, 1023, 10, 35)); | |
t += map(pot, 0, 1023, 2, 8); | |
float ft = t * 0.05f; | |
float fx = (sin(ft * 1.71f) + 1.0f) * 0.5f * (W - 1); | |
float fy = (sin(ft * 2.37f + 1.1f) + 1.0f) * 0.5f * (H - 1); | |
int hx = (int)(fx + 0.5f), hy = (int)(fy + 0.5f); | |
uint8_t hue = (uint8_t)((int)(ft * 40.0f) & 255); | |
uint32_t headCol = wheel(hue); | |
for (int dy = -1; dy <= 1; dy++) | |
for (int dx = -1; dx <= 1; dx++) { | |
int d2 = dx * dx + dy * dy; | |
uint8_t s = (d2 == 0) ? 255 : (d2 == 1 ? 140 : 60); | |
fbAddColorScaled(hx + dx, hy + dy, headCol, s); | |
} | |
fbAddColorScaled(hx - 2, hy - 1, headCol, 30); | |
fbAddColorScaled(hx + 2, hy + 1, headCol, 30); | |
if (random(6) == 0) fbAddRGB(random(W), random(H), 20, 20, 20); | |
} | |
// ---- Matrix rain (trails) | |
void renderMatrix(int pot) { | |
static uint16_t tEpoch = 0; static int8_t headY[W]; static uint8_t wait[W]; | |
if (tEpoch != modeEpoch) { | |
for (int x = 0; x < W; x++) { headY[x] = -1; wait[x] = 0; } | |
fbClear(); tEpoch = modeEpoch; | |
} | |
fbFade(map(pot, 0, 1023, 14, 40)); | |
uint8_t spawn = map(pot, 0, 1023, 2, 18); | |
for (int x = 0; x < W; x++) { | |
if (headY[x] < 0) { | |
if (random(20) < spawn) { headY[x] = H - 1; wait[x] = random(1, 4); } | |
continue; | |
} | |
fbAddColorScaled(x, headY[x], color(255,255,255), 255); | |
fbAddColorScaled(x, headY[x] + 1, color(0,220,90), 200); | |
fbAddColorScaled(x, headY[x] + 2, color(0,160,60), 120); | |
fbAddColorScaled(x, headY[x] + 3, color(0,100,30), 70); | |
if (wait[x] > 0) wait[x]--; | |
else { | |
headY[x]--; | |
wait[x] = random(1, 4); | |
if (headY[x] < 0) headY[x] = -1; | |
} | |
} | |
if (random(3) == 0) fbAddColorScaled(random(W), random(H), color(0,120,40), 40); | |
} | |
// ---- Fireplace (slow-burn, dancing flames) | |
void renderFireplace(int pot) { | |
static uint16_t tEpoch = 0; | |
static uint8_t heat[W][H]; | |
// New: slow-changing wind per row and an ember bed | |
static int8_t windRow[H]; | |
static int8_t globalWind = 0; | |
static uint8_t frame = 0; | |
static uint8_t emberX[3]; | |
static int8_t emberDX[3]; | |
if (tEpoch != modeEpoch) { | |
memset(heat, 0, sizeof(heat)); | |
memset(windRow, 0, sizeof(windRow)); | |
globalWind = 0; frame = 0; | |
for (int i = 0; i < 3; ++i) { emberX[i] = random(W); emberDX[i] = 0; } | |
tEpoch = modeEpoch; | |
} | |
// Tuned for a "slow burn" | |
uint8_t cool = map(pot, 0, 1023, 90, 55); // more cooling -> calmer, more contrast | |
uint8_t sparkiness = map(pot, 0, 1023, 20, 55); // fewer frequent sparks | |
uint8_t emberPower = map(pot, 0, 1023, 60, 140); // moderate base heat injections | |
// Cool the field | |
for (int y = 0; y < H; y++) | |
for (int x = 0; x < W; x++) | |
heat[x][y] = clampU8((int)heat[x][y] - (int)random(0, cool)); | |
// Update slow wind (global bias + row jitter) | |
if ((frame & 0x03) == 0) { // every 4 frames | |
globalWind += (int8_t)random(-1, 2); // -1..+1 | |
if (globalWind < -3) globalWind = -3; | |
if (globalWind > 3) globalWind = 3; | |
for (int y = 1; y < H - 1; ++y) { | |
if (windRow[y] < globalWind) windRow[y]++; | |
else if (windRow[y] > globalWind) windRow[y]--; | |
if (random(9) == 0) { // occasional local turbulence | |
windRow[y] += (int8_t)random(-1, 2); | |
if (windRow[y] < -2) windRow[y] = -2; | |
if (windRow[y] > 2) windRow[y] = 2; | |
} | |
} | |
} | |
frame++; | |
// Diffuse upward with wind bias + small lateral swirl | |
for (int y = H - 1; y >= 1; --y) { | |
int8_t w = windRow[y]; | |
for (int x = 0; x < W; ++x) { | |
// sample from below and below-neighbors (same as before) | |
uint8_t a = heat[x][y - 1]; | |
uint8_t b = heat[(x + W - 1) % W][y - 1]; | |
uint8_t c = heat[(x + 1) % W][y - 1]; | |
uint8_t d = (y >= 2) ? heat[x][y - 2] : heat[x][y - 1]; | |
// Wind bias: prefer b or c depending on sign | |
uint16_t sum = (uint16_t)a * 3 + (uint16_t)d * 3 + b + c; | |
if (w < 0) sum += b; | |
else if (w > 0) sum += c; | |
heat[x][y] = sum / (8 + (w != 0)); | |
} | |
// Gentle lateral swirl on the row (cheap advection) | |
int s = (windRow[y] > 0) - (windRow[y] < 0); // -1,0,+1 | |
if (s != 0) { | |
uint8_t row[W]; | |
for (int x = 0; x < W; ++x) row[x] = heat[x][y]; | |
for (int x = 0; x < W; ++x) { | |
int src = (x - s + W) % W; | |
heat[x][y] = (uint16_t)row[x] * 3 / 4 + (uint16_t)row[src] / 4; | |
} | |
} | |
} | |
// Ember bed: a few moving hotspots that feed the flame | |
for (int i = 0; i < 3; ++i) { | |
if (random(5) == 0) { // slow drift | |
emberDX[i] += (int8_t)random(-1, 2); | |
if (emberDX[i] < -1) emberDX[i] = -1; | |
if (emberDX[i] > 1) emberDX[i] = 1; | |
} | |
emberX[i] = (uint8_t)((emberX[i] + W + emberDX[i]) % W); | |
int x0 = emberX[i]; | |
uint8_t p = emberPower + random(40); | |
heat[x0][0] = u8satAdd(heat[x0][0], p); | |
heat[(x0 + 1) % W][0] = u8satAdd(heat[(x0 + 1) % W][0], p / 2); | |
heat[(x0 + W - 1) % W][0] = u8satAdd(heat[(x0 + W - 1) % W][0], p / 2); | |
// occasional tiny cinder into row 1 (rare, and not too hot) | |
if (random(100) < sparkiness) | |
heat[x0][1] = u8satAdd(heat[x0][1], random(30, 80)); | |
} | |
// Very rare bonus spark anywhere on bottom (adds surprise but stays warm) | |
for (int x = 0; x < W; ++x) | |
if (random(200) < 2) | |
heat[x][0] = u8satAdd(heat[x][0], random(80, 160)); | |
// Warm, low-white palette + highlight compression for contrast | |
auto heatColorSlow = [](uint8_t h) -> uint32_t { | |
// gamma-ish: compress highlights, emphasize midtones | |
uint8_t hg = (uint8_t)(((uint16_t)h * (uint16_t)h) >> 8); // ~gamma 2.0 | |
uint8_t r, g, b; | |
if (hg < 96) { // deep red -> red | |
r = hg * 2 + (hg >> 2); | |
g = hg >> 3; | |
b = 0; | |
} else if (hg < 170) { // red -> amber | |
r = 220 + (hg - 96) / 2; // cap near 255 | |
g = (hg - 96) * 2; | |
b = 0; | |
} else { // amber -> warm yellow (little blue; avoid "white") | |
r = 255; | |
g = 160 + (hg - 170); // tops ~245 | |
b = (hg - 170) / 3; // tiny blue component | |
} | |
return color(r, g, b); | |
}; | |
// Draw with a soft, smoky ceiling (no longer forced-black top) | |
for (int y = 0; y < H; y++) { | |
for (int x = 0; x < W; x++) { | |
uint32_t c = heatColorSlow(heat[x][y]); | |
// Vignette the ceiling so tongues can occasionally peek through | |
if (y >= H - 2) { | |
if (random(5) == 0) c = color(0, 0, 0); // smoke holes | |
else c = dimColor(c, (y == H - 1) ? 70 : 100); // heavy dim on top row | |
} else if (y >= H - 3) { | |
if (random(8) == 0) c = color(0, 0, 0); | |
else c = dimColor(c, 140); // slightly darker near ceiling | |
} | |
pixel(x, y, c); | |
} | |
} | |
} | |
// ---- Endless Zoom (square tunnel) | |
void renderZoom(int pot) { | |
static uint16_t tEpoch = 0; static uint16_t tick = 0; | |
if (tEpoch != modeEpoch) { tick = 0; tEpoch = modeEpoch; } | |
tick += map(pot, 0, 1023, 1, 6); | |
float t = tick * 0.05f; | |
float bands = 4.0f; | |
for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { | |
float dx = fabs(x - 2.5f), dy = fabs(y - 3.5f); | |
float s = (dx > dy ? dx : dy); // Chebyshev radius -> square rings | |
float z = s * bands - t; | |
float f = z - floor(z); | |
uint8_t br = clampU8((int)(pow(1.0f - f, 3.0f) * 255.0f) + 10); | |
uint8_t hue = ((int)(z * 40.0f) & 255); | |
pixel(x, y, dimColor(wheel(hue), br)); | |
} | |
} | |
// ---- Tetris (auto-play, line clears) | |
namespace TET { | |
struct Rot { int8_t dx[4], dy[4]; uint8_t w, h; }; | |
struct Piece { const Rot* r; uint8_t nrot; uint8_t hue; }; | |
// Define rotations (dx/dy from piece bottom-left) | |
const Rot I[2] = { | |
{{0,1,2,3}, {0,0,0,0}, 4,1}, | |
{{0,0,0,0}, {0,1,2,3}, 1,4} | |
}; | |
const Rot O[1] = { | |
{{0,1,0,1}, {0,0,1,1}, 2,2} | |
}; | |
const Rot T[4] = { | |
{{0,1,2,1}, {0,0,0,1}, 3,2}, | |
{{1,0,1,1}, {0,1,1,2}, 2,3}, | |
{{1,0,1,2}, {0,1,1,1}, 3,2}, | |
{{0,0,0,1}, {0,1,2,1}, 2,3} | |
}; | |
const Rot L[4] = { | |
{{0,0,0,1}, {0,1,2,0}, 2,3}, | |
{{0,1,2,0}, {0,0,0,1}, 3,2}, | |
{{0,1,1,1}, {2,0,1,2}, 2,3}, | |
{{2,0,1,2}, {1,1,1,0}, 3,2} | |
}; | |
const Rot S[2] = { | |
{{1,2,0,1}, {0,0,1,1}, 3,2}, | |
{{0,0,1,1}, {0,1,1,2}, 2,3} | |
}; | |
const Piece P[5] = { | |
{I,2, 10}, {O,1, 32}, {T,4, 64}, {L,4, 110}, {S,2, 170} | |
}; | |
static uint32_t board[W*H]; | |
static int curId, curRot, px, py; // bottom-left anchor | |
static uint32_t curCol; | |
static uint32_t lastDrop; static uint16_t dropMs; | |
static bool inited; | |
inline uint32_t& B(int x, int y) { return board[IDX(x,y)]; } | |
void reset() { | |
memset(board, 0, sizeof(board)); | |
inited = true; lastDrop = 0; | |
} | |
static inline bool topRowReached() { | |
for (int x = 0; x < W; ++x) if (B(x, H - 1) != 0) return true; | |
return false; | |
} | |
bool collideAt(int id, int rot, int ax, int ay) { | |
const Rot& R = P[id].r[rot]; | |
for (int i = 0; i < 4; i++) { | |
int x = ax + R.dx[i], y = ay + R.dy[i]; | |
if (x < 0 || x >= W) return true; // wall | |
if (y < 0) return true; // floor | |
if (y >= H) continue; // above top: ignore | |
if (B(x,y)) return true; // hit stack | |
} | |
return false; | |
} | |
void stamp(int id, int rot, int ax, int ay, uint32_t col) { | |
const Rot& R = P[id].r[rot]; | |
for (int i = 0; i < 4; i++) { | |
int x = ax + R.dx[i], y = ay + R.dy[i]; | |
if ((unsigned)x < W && (unsigned)y < H) B(x,y) = col; | |
} | |
} | |
void spawn() { | |
curId = random(5); | |
curRot = random(P[curId].nrot); | |
const Rot& R = P[curId].r[curRot]; | |
px = (W - R.w) / 2; | |
py = H + 2; // start above top | |
curCol = dimColor(wheel(P[curId].hue), 200); | |
if (collideAt(curId, curRot, px, py)) { // no space: reset | |
reset(); | |
} | |
} | |
void clearLines() { | |
for (int y = 0; y < H; y++) { | |
bool full = true; | |
for (int x = 0; x < W; x++) if (!B(x,y)) { full = false; break; } | |
if (full) { | |
for (int yy = y; yy < H - 1; yy++) | |
for (int x = 0; x < W; x++) B(x,yy) = B(x,yy+1); | |
for (int x = 0; x < W; x++) B(x,H-1) = 0; | |
y--; // recheck same row after collapse | |
} | |
} | |
} | |
void tick(int pot) { | |
if (!inited) { reset(); spawn(); } | |
dropMs = map(pot, 0, 1023, 800, 200); | |
uint32_t now = millis(); | |
// occasional auto-rotate if possible | |
if ((now & 511U) == 0) { | |
int nr = (curRot + 1) % P[curId].nrot; | |
if (!collideAt(curId, nr, px, py)) curRot = nr; | |
} | |
if (now - lastDrop >= dropMs) { | |
lastDrop = now; | |
if (!collideAt(curId, curRot, px, py - 1)) py--; | |
else { // lock | |
stamp(curId, curRot, px, py, curCol); | |
// Reset if the stack touches the top row (y == H-1) | |
if (topRowReached()) { reset(); spawn(); return; } | |
clearLines(); | |
// Safety: if still touching after a line clear, reset anyway | |
if (topRowReached()) { reset(); spawn(); return; } | |
spawn(); | |
} | |
// gentle left/right drift if room | |
int dir = random(3) - 1; | |
if (!collideAt(curId, curRot, px + dir, py)) px += dir; | |
} | |
// render | |
clearPixels(); | |
for (int y = 0; y < H; y++) | |
for (int x = 0; x < W; x++) | |
if (B(x,y)) pixel(x,y,B(x,y)); | |
// draw current piece over board | |
const Rot& R = P[curId].r[curRot]; | |
for (int i = 0; i < 4; i++) { | |
int x = px + R.dx[i], y = py + R.dy[i]; | |
if ((unsigned)x < W && (unsigned)y < H) pixel(x, y, curCol); | |
} | |
} | |
} // namespace TET | |
void renderTetris(int pot) { TET::tick(pot); } | |
// ---- Pong (AI vs AI) | |
void renderPong(int pot) { | |
static uint16_t tEpoch = 0; | |
static float bx, by, vx, vy, lY, rY; | |
if (tEpoch != modeEpoch) { | |
bx = W * 0.5f; by = H * 0.5f; | |
vx = (random(2) ? 0.25f : -0.25f); | |
vy = (random(2) ? 0.18f : -0.18f); | |
lY = rY = by; tEpoch = modeEpoch; | |
} | |
float ai = 0.35f + map(pot, 0, 1023, 0, 30) / 100.0f; // paddle agility | |
float maxStep = 0.7f + map(pot, 0, 1023, 0, 20) / 100.0f; | |
int padH = 3; float half = padH * 0.5f; | |
// paddles follow ball with limited speed | |
float dl = by - lY; if (dl > maxStep) dl = maxStep; if (dl < -maxStep) dl = -maxStep; lY += dl * ai; | |
float dr = by - rY; if (dr > maxStep) dr = maxStep; if (dr < -maxStep) dr = -maxStep; rY += dr * ai; | |
// move ball | |
bx += vx; by += vy; | |
// wall bounce | |
if (by < 0) { by = 0; vy = -vy; } | |
if (by > H - 1) { by = H - 1; vy = -vy; } | |
auto resetBall = [&]() { | |
bx = W * 0.5f; by = H * 0.5f; | |
vx = (random(2) ? 0.25f : -0.25f); | |
vy = (random(2) ? 0.18f : -0.18f); | |
}; | |
// paddle collisions / scoring | |
if (bx <= 0) { | |
if (fabs(by - lY) <= half + 0.6f) { | |
bx = 0; vx = fabs(vx); vy += (by - lY) * 0.06f; // add spin | |
} else { resetBall(); } | |
} | |
if (bx >= W - 1) { | |
if (fabs(by - rY) <= half + 0.6f) { | |
bx = W - 1; vx = -fabs(vx); vy += (by - rY) * 0.06f; | |
} else { resetBall(); } | |
} | |
// render | |
clearPixels(); | |
// center dash | |
for (int y = 0; y < H; y += 2) pixel(W/2, y, 30, 30, 30); | |
// paddles | |
uint32_t pCol = dimColor(wheel(120), 220); | |
for (int dy = -(int)half; dy <= (int)half; dy++) { | |
int ly = (int)(lY + 0.5f) + dy, ry = (int)(rY + 0.5f) + dy; | |
if ((unsigned)ly < H) pixel(0, ly, pCol); | |
if ((unsigned)ry < H) pixel(W - 1, ry, pCol); | |
} | |
// ball | |
uint32_t bCol = dimColor(wheel((millis()>>3) & 255), 255); | |
pixel((int)(bx + 0.5f), (int)(by + 0.5f), bCol); | |
} | |
// ---- Color Pumping (breathing hue + shimmer) | |
void renderColorPump(int pot) { | |
static uint16_t tEpoch = 0; static uint16_t tick = 0; | |
if (tEpoch != modeEpoch) { tick = 0; tEpoch = modeEpoch; } | |
tick += map(pot, 0, 1023, 1, 6); | |
float t = tick * 0.05f; | |
uint8_t baseHue = ((int)(t * 20.0f) & 255); | |
float pump = 0.5f + 0.5f * sin(t * 1.6f); // 0..1 | |
uint8_t baseBr = 40 + (uint8_t)(pump * 215.0f); | |
for (int y = 0; y < H; y++) for (int x = 0; x < W; x++) { | |
float shimmer = 0.5f + 0.5f * sin(t * 2.7f + x * 0.9f + y * 0.6f); | |
uint8_t br = clampU8((int)(baseBr * (0.75f + 0.25f * shimmer))); | |
uint8_t hue = baseHue + (x * 18 + y * 12); | |
pixel(x, y, dimColor(wheel(hue), br)); | |
} | |
} | |
// ---- Text Scroll | |
static const char TEXT_MSG[] = " LEVERANSKOORDINERING "; | |
static const uint8_t FONT_W = 5, FONT_H = 7, FONT_SP = 1; | |
// 5x7 glyph columns for the letters we use (bit0 = bottom .. bit6 = top) | |
static const uint8_t GL_A[5] = {0x3F,0x48,0x48,0x48,0x3F}; | |
static const uint8_t GL_D[5] = {0x7F,0x41,0x22,0x14,0x08}; | |
static const uint8_t GL_E[5] = {0x7F,0x49,0x49,0x49,0x49}; | |
static const uint8_t GL_G[5] = {0x3E,0x41,0x49,0x09,0x0E}; | |
static const uint8_t GL_I[5] = {0x41,0x41,0x7F,0x41,0x41}; | |
static const uint8_t GL_K[5] = {0x7F,0x18,0x24,0x42,0x00}; | |
static const uint8_t GL_L[5] = {0x7F,0x01,0x01,0x01,0x01}; | |
static const uint8_t GL_N[5] = {0x7F,0x60,0x18,0x06,0x7F}; | |
static const uint8_t GL_O[5] = {0x3E,0x41,0x41,0x41,0x3E}; | |
static const uint8_t GL_R[5] = {0x7F,0x48,0x4C,0x4A,0x31}; | |
static const uint8_t GL_S[5] = {0x79,0x49,0x49,0x49,0x4F}; | |
static const uint8_t GL_V[5] = {0x70,0x0C,0x03,0x0C,0x70}; | |
static const uint8_t GL_SP[5]= {0x00,0x00,0x00,0x00,0x00}; | |
static inline uint8_t glyphCol(char ch, uint8_t col) { | |
if (col >= FONT_W) return 0; | |
if (ch >= 'a' && ch <= 'z') ch -= 32; // uppercase | |
switch (ch) { | |
case 'A': return GL_A[col]; case 'D': return GL_D[col]; | |
case 'E': return GL_E[col]; case 'G': return GL_G[col]; | |
case 'I': return GL_I[col]; case 'K': return GL_K[col]; | |
case 'L': return GL_L[col]; case 'N': return GL_N[col]; | |
case 'O': return GL_O[col]; case 'R': return GL_R[col]; | |
case 'S': return GL_S[col]; case 'V': return GL_V[col]; | |
case ' ': default: return GL_SP[col]; | |
} | |
} | |
void renderTextScroll(int pot) { | |
static uint16_t tEpoch = 0; | |
static int16_t scroll; // pixel column offset (leftwards) | |
static uint32_t lastStep; | |
static int16_t textPix; | |
if (tEpoch != modeEpoch) { | |
scroll = W; lastStep = 0; tEpoch = modeEpoch; | |
textPix = strlen(TEXT_MSG) * (FONT_W + FONT_SP); | |
} | |
// speed: lower ms -> faster scroll | |
uint16_t stepMs = map(pot, 0, 1023, 180, 35); | |
uint32_t now = millis(); | |
if (now - lastStep >= stepMs) { | |
lastStep = now; | |
scroll--; | |
if (scroll < -(textPix + W)) scroll = W; // loop | |
} | |
clearPixels(); // black background | |
uint8_t baseHue = (uint8_t)((now >> 4) & 0xFF); | |
for (int y = 0; y < H; y++) { | |
if (y >= FONT_H) continue; // keep row 7 free (top black bar aesthetic) | |
for (int x = 0; x < W; x++) { | |
int mc = x - scroll; | |
if (mc < 0 || mc >= textPix) continue; | |
int chIdx = mc / (FONT_W + FONT_SP); | |
int colIn = mc % (FONT_W + FONT_SP); | |
if (colIn >= FONT_W) continue; // spacing column | |
char ch = TEXT_MSG[chIdx]; | |
uint8_t colBits = glyphCol(ch, colIn); | |
if (colBits & (1 << y)) { | |
uint8_t hue = baseHue + chIdx * 17; // per-letter hue shift | |
pixel(x, y, dimColor(wheel(hue), 235)); | |
} | |
} | |
} | |
} | |
void setup() { | |
pinMode(buttonPin, INPUT); | |
pinMode(analogPin, INPUT); | |
pinMode(clockPin, OUTPUT); | |
pinMode(dataPin, OUTPUT); | |
Serial.begin(57600); | |
strip.begin(); | |
strip.show(); | |
} | |
void loop() { | |
static bool seeded = false; | |
if (!seeded) { randomSeed(analogRead(analogPin)); seeded = true; } | |
int pot = analogRead(analogPin); | |
const uint8_t NUM_MODES = 10; | |
static uint8_t mode = 0, prevMode = 255; | |
static uint32_t lastAuto = 0; | |
static uint32_t autoTimeout = 20000UL; | |
if (myButtonPressed()) { mode = (mode + 1) % NUM_MODES; lastAuto = millis(); } | |
uint32_t now = millis(); | |
if (now - lastAuto > autoTimeout) { mode = (mode + 1) % NUM_MODES; lastAuto = now; } | |
if (mode != prevMode) { | |
fbClear(); // reset any trails | |
prevMode = mode; modeEpoch++; // tell modes to re-init | |
} | |
switch (mode) { | |
case 0: renderPlasma(pot); break; | |
case 1: renderSwirl(pot); break; | |
case 2: renderTetris(pot); break; | |
case 3: renderComet(pot); fbBlit(); break; | |
case 4: renderPong(pot); break; | |
case 5: renderMatrix(pot); fbBlit(); break; | |
case 6: renderTextScroll(pot); break; | |
case 7: renderColorPump(pot); break; | |
case 8: renderFireplace(pot); break; | |
case 9: renderZoom(pot); break; | |
} | |
strip.show(); | |
delay(map(pot, 0, 1023, 12, 60)); // ~16–83 FPS | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment