edge lab LIVE! starts Tuesday in Munich. Grab a workshop seat and take home the board you build on. Code FRIEND-HACKSTER saves 20%. () Learn more!
Arnov Sharma
Published © MIT

Project Gestalt

Nier Pod-inspired interactive 64×64 RGB matrix display featuring custom animations and a sound-reactive WaveForm mode.

BeginnerFull instructions provided10 hours107
Project Gestalt

Things used in this project

Hardware components

Raspberry Pi Pico
Raspberry Pi Pico
×1
NextPCB  Custom PCB Board
NextPCB Custom PCB Board
×1

Software apps and online services

Arduino IDE
Arduino IDE
Fusion
Autodesk Fusion

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Main Enclsoure

Lid

Grill

Aesthetic Part

Holder

Mid Holder

Schematics

SCH

Code

CODE

C/C++
#include <Adafruit_Protomatter.h>
#include <math.h>

// ── Pins (Raspberry Pi Pico GPIO) ─────────────────────────────────
#define R1     2
#define G1     3
#define B1     4
#define R2     5
#define G2     6
#define B2     9
#define PIN_A  10
#define PIN_B  16
#define PIN_C  18
#define PIN_D  20
#define CLK    11
#define LAT    12
#define OE     13

// ── Buttons ───────────────────────────────────────────────────────
#define BTN_PREV 0
#define BTN_NEXT 14
#define BTN_WAVE 1

// ── Resolution for TWO Chained 64x32 Panels (64x64 total) ─────────
#define W 64
#define H 64 

uint8_t rgbPins[]  = { R1, G1, B1, R2, G2, B2 };
uint8_t addrPins[] = { PIN_A, PIN_B, PIN_C, PIN_D };

Adafruit_Protomatter matrix(
  W, 3, 1, rgbPins, 4, addrPins, CLK, LAT, OE, true, 2     
);

// ── Global Variables & Timing ─────────────────────────────────────
uint32_t frameCount = 0;
int currentAnim = 0;
const int NUM_ANIMS = 9;

unsigned long previousMillis = 0;
const int frameInterval = 16;

bool lastBtnPrev = HIGH;
bool lastBtnNext = HIGH;
bool lastBtnWave = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;

// ── Audio / Waveform Settings ─────────────────────────────────────
const int MIC_PIN   = 28;    // Using GPIO 28 (ADC2)
const int OVERSAMP  = 128;
const int X_STEP    = 2;
const int NUM_POINTS = W / X_STEP;
const float MAX_WAVE_FRACTION = 0.5f; 

float rmsPeak    = 200.0f;   
float loudSmooth = 0.0f;
bool  noiseInit  = false;
float noiseFloor = 100.0f;   
const float LOUD_GATE = 0.08f;   

float waveY[NUM_POINTS];
bool waveMode = false;

inline uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
  return matrix.color565(r, g, b);
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 0: The Original Cyber-Ghost
// ══════════════════════════════════════════════════════════════════
void drawPacGhost() {
  matrix.fillScreen(0);
  float t = frameCount * 0.05f;
  
  uint8_t wheelPos = (frameCount >> 1) & 0xFF; 
  uint8_t r, g, b;
  if (wheelPos < 85) { r = 255 - wheelPos * 3; g = 0; b = wheelPos * 3; } 
  else if (wheelPos < 170) { wheelPos -= 85; r = 0; g = wheelPos * 3; b = 255 - wheelPos * 3; } 
  else { wheelPos -= 170; r = wheelPos * 3; g = 255 - wheelPos * 3; b = 0; }
  uint16_t GHOST_COLOR = rgb(r, g, b);
  
  float base_cx = 32.0f + sinf(t * 1.2f) * 16.0f;
  float hover_y = sinf(t * 3.0f) * 2.5f;
  
  for (int y = 8; y < 58; y++) {
    for (int x = base_cx - 18; x < base_cx + 18; x++) {
      if(x < 0 || x >= W || y < 0 || y >= H) continue;
      float relX = x - base_cx; float relY = (y - 12) + hover_y;
      bool drawPixel = false;
      if (relY < 15.0f) {
        if (sqrtf(relX*relX + (relY - 15.0f)*(relY - 15.0f)) < 15.0f) drawPixel = true;
      } else if (relY >= 15.0f && relY < 39.0f) {
        if (relX >= -15.0f && relX <= 15.0f) drawPixel = true;
      } else if (relY >= 39.0f) {
        float footRipple = 2.5f * sinf(relX * 0.7f - t * 4.0f);
        if (relY < (43.0f + footRipple) && relX >= -15.0f && relX <= 15.0f) drawPixel = true;
      }
      if (drawPixel) matrix.drawPixel(x, y, GHOST_COLOR);
    }
  }
  
  float lookX = sinf(t * 1.2f) * 3.0f; float lookY = cosf(t * 1.5f) * 1.5f;
  auto drawEye = [&](float ex, float ey) {
    for (int y = ey - 5; y < ey + 5; y++) {
      for (int x = ex - 3; x < ex + 3; x++) {
        if(x < 0 || x >= W || y < 0 || y >= H) continue;
        float dx = x - ex; float dy = y - (ey + hover_y);
        if((dx*dx / 9.0f) + (dy*dy / 25.0f) < 1.0f) matrix.drawPixel(x, y, rgb(240, 240, 240));
      }
    }
    matrix.fillRect(ex + lookX - 1, (ey + hover_y) + lookY - 1, 2, 2, rgb(0, 50, 255));
  };
  drawEye(base_cx - 6.0f, 22.0f); drawEye(base_cx + 6.0f, 22.0f);
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 1: Cyberpunk 2077 Relic Glitch
// ══════════════════════════════════════════════════════════════════
void drawCyberpunkRelic() {
  matrix.fillScreen(rgb(10, 0, 15)); // Dark corporate background
  uint16_t neonCyan = rgb(0, 255, 255);
  uint16_t neonYellow = rgb(255, 240, 0);
  uint16_t neonRed = rgb(255, 0, 50);

  // Intentional X-axis tearing/glitching for the entire logo
  int glitchX = (random(100) > 85) ? random(-6, 6) : 0;
  
  // Base coordinates for the logo
  int x = 16 + glitchX;
  int y = 8;

  // ── Draw the Custom Relic Logo Geometry ──
  // Outer Frame
  matrix.fillRect(x, y, 32, 6, neonRed);              // Top horizontal
  matrix.fillRect(x + 26, y, 6, 32, neonRed);         // Right vertical
  matrix.fillRect(x + 14, y + 26, 18, 6, neonRed);    // Bottom horizontal (partial)
  matrix.fillRect(x, y, 6, 22, neonRed);              // Left vertical
  
  // Inner 'R' Mechanism
  matrix.fillRect(x, y + 12, 16, 6, neonRed);         // Middle horizontal
  matrix.fillRect(x + 14, y + 12, 6, 9, neonRed);     // Inner right vertical
  
  // The iconic 45-degree angle cut
  matrix.fillRect(x, y + 22, 10, 6, neonRed);         // Bottom-left inward turn
  for(int i = 0; i < 6; i++) {
    matrix.drawLine(x + 8 + i, y + 27, x + 16 + i, y + 19, neonRed); // Thick diagonal line
  }

  // Draw "RELIC" Text below the logo
  matrix.setCursor(17 + glitchX, 44);
  matrix.setTextColor(neonRed);
  matrix.print("RELIC");

  // Glitching core data block inside the logo
  matrix.fillRect(x + 12, y + 16, 4, 4, neonYellow);

  // ── Glitch & Artifact Effects ──
  // Digital barcode / hacking lines cutting across
  for(int i = 0; i < 7; i++) {
      int lineY = random(0, 64);
      matrix.drawFastHLine(random(0, 32), lineY, random(10, 45), neonCyan);
  }
  
  // Occasional full-screen EMP tear (Yellow flash with a black void)
  if (random(100) > 92) {
      int tearY = random(10, 50);
      matrix.fillRect(0, tearY, 64, random(2, 6), neonYellow);
      matrix.fillRect(0, tearY + 2, 64, random(2, 5), 0); // Black tear slicing the screen
  }
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 2: Retro Doom Fire (Integer Cellular Automata)
// ══════════════════════════════════════════════════════════════════
uint8_t firePixels[W * (H + 1)]; // 1D array for speed
void drawRetroFire() {
  // Feed the bottom row with random bright intensity
  for (int x = 0; x < W; x++) {
    firePixels[(H - 1) * W + x] = random(160, 255);
  }
  // Propagate upwards
  for (int y = 0; y < H - 1; y++) {
    for (int x = 0; x < W; x++) {
      int src = (y + 1) * W + x;
      int decay = random(0, 3);
      int dstX = x - decay + 1;
      if (dstX < 0) dstX = 0; if (dstX > W - 1) dstX = W - 1;
      int dst = y * W + dstX;
      int val = firePixels[src] - decay * 2;
      firePixels[dst] = (val > 0) ? val : 0;
      
      // Fast color mapping (Black -> Red -> Orange -> Yellow -> White)
      uint8_t intensity = firePixels[dst];
      uint8_t r = (intensity > 128) ? 255 : intensity * 2;
      uint8_t g = (intensity > 128) ? (intensity - 128) * 2 : 0;
      uint8_t b = (intensity > 192) ? (intensity - 192) * 4 : 0;
      matrix.drawPixel(x, y, rgb(r, g, b));
    }
  }
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 3: XOR Cyber-Fractal (Fast Integer Hypnosis)
// ══════════════════════════════════════════════════════════════════
void drawXORFractal() {
  matrix.fillScreen(0);
  uint16_t t = frameCount * 2;
  
  for (int y = 0; y < H; y++) {
    for (int x = 0; x < W; x++) {
      // Pure mathematical bitwise alien pattern
      uint8_t c = ((x * 4) ^ (y * 4)) - t;
      
      uint8_t r = (c * 2) % 255;
      uint8_t g = (c * 4) % 255;
      uint8_t b = 255 - c;
      
      matrix.drawPixel(x, y, rgb(r, g, b));
    }
  }
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 4: Synthwave Outrun Grid
// ══════════════════════════════════════════════════════════════════
void drawSynthwave() {
  matrix.fillScreen(rgb(5, 0, 15)); // Dark retro-purple sky

  // 1. Synthwave Sun (with scrolling scanline cutouts)
  int sunY = 24;
  for (int y = 10; y < 38; y++) {
    int slice = (y - (frameCount >> 1)) % 6;
    if (y > 24 && slice < 2) continue; // Scanline gaps
    
    int dy = y - sunY;
    if (dy * dy <= 196) { // Circle radius 14 (14^2 = 196)
      int dx = sqrt(196 - dy * dy);
      uint8_t g = (38 - y) * 7; // Gradient: Yellow top, Red bottom
      matrix.drawFastHLine(32 - dx, y, dx * 2, rgb(255, g, 0));
    }
  }

  // 2. Glowing Cyan Horizon
  matrix.drawFastHLine(0, 38, W, rgb(0, 255, 255));

  // 3. Perspective Grid (Floor)
  // Radiating vertical lines
  for (int x = -100; x < 164; x += 18) { 
     matrix.drawLine(32, 38, x, 63, rgb(200, 0, 255));
  }
  
  // Moving horizontal perspective lines (speed controlled by frameCount offset)
  int offset = (frameCount >> 1) % 6;
  for (int y = 39; y < H; y++) {
    int depth = y - 38;
    if ((depth + offset) % (depth / 3 + 2) == 0) {
      matrix.drawFastHLine(0, y, W, rgb(255, 0, 255));
    }
  }
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 5: Neon Qix Trails (Mystify)
// ══════════════════════════════════════════════════════════════════
static float qx[4] = {10, 54, 10, 54};
static float qy[4] = {10, 10, 54, 54};
static float qvx[4] = {1.5, -1.2, 1.3, -1.6};
static float qvy[4] = {1.2, 1.7, -1.1, 1.4};

#define TAIL 8
static int hx[4][TAIL] = {0};
static int hy[4][TAIL] = {0};

void drawNeonQix() {
   matrix.fillScreen(0);
   
   // Shift trail history
   for(int i = 0; i < 4; i++) {
      for(int t = TAIL - 1; t > 0; t--) {
         hx[i][t] = hx[i][t-1];
         hy[i][t] = hy[i][t-1];
      }
      hx[i][0] = (int)qx[i];
      hy[i][0] = (int)qy[i];
   }

   // Move head points and bounce off edges
   for(int i = 0; i < 4; i++) {
      qx[i] += qvx[i]; 
      qy[i] += qvy[i];
      if(qx[i] <= 0 || qx[i] >= W - 1) qvx[i] = -qvx[i];
      if(qy[i] <= 0 || qy[i] >= H - 1) qvy[i] = -qvy[i];
   }

   // Draw fading geometric connecting lines
   for(int t = 0; t < TAIL; t++) {
      if (hx[0][t] == 0 && hy[0][t] == 0) continue; // Skip first frame
      
      uint8_t fade = 255 - (t * (255 / TAIL));
      uint16_t c1 = rgb(fade, 0, fade / 2); // Neon Pink
      uint16_t c2 = rgb(0, fade, fade);     // Neon Cyan
      
      matrix.drawLine(hx[0][t], hy[0][t], hx[1][t], hy[1][t], c1);
      matrix.drawLine(hx[1][t], hy[1][t], hx[2][t], hy[2][t], c2);
      matrix.drawLine(hx[2][t], hy[2][t], hx[3][t], hy[3][t], c1);
      matrix.drawLine(hx[3][t], hy[3][t], hx[0][t], hy[0][t], c2);
   }
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 6: Pikachu "Pika Pika!"
// ══════════════════════════════════════════════════════════════════
void drawPikachu() {
  matrix.fillScreen(rgb(50, 150, 255)); // Bright Pokémon-sky blue background
  
  // "Pika Pika" speech cadence: open mouth twice quickly, then pause
  int talkCycle = frameCount % 60;
  bool mouthOpen = (talkCycle > 0 && talkCycle < 10) || (talkCycle > 15 && talkCycle < 25);
  
  // Head bobs down slightly when talking to give it energy
  int hover = (mouthOpen) ? 2 : 0; 
  
  int cx = 32;
  int cy = 34 + hover;
  
  uint16_t yellow = rgb(255, 235, 20);
  uint16_t black  = rgb(0, 0, 0);
  uint16_t red    = rgb(255, 30, 30);
  uint16_t white  = rgb(255, 255, 255);
  uint16_t tongue = rgb(255, 100, 100);

  // ── 1. Outlines & Black Ear Tips ──
  // We draw slightly larger black shapes in the background to act as thick 8-bit outlines!
  matrix.fillTriangle(cx - 10, cy - 8, cx - 2, cy - 18, cx - 28, cy - 32, black); // Left Ear
  matrix.fillTriangle(cx + 10, cy - 8, cx + 2, cy - 18, cx + 28, cy - 32, black); // Right Ear
  matrix.fillRoundRect(cx - 23, cy - 15, 46, 32, 15, black); // Head Outline

  // ── 2. Yellow Body ──
  // The yellow triangles stop shorter than the black ones to naturally create the black ear tips!
  matrix.fillTriangle(cx - 10, cy - 8, cx - 4, cy - 16, cx - 18, cy - 20, yellow); // Left Ear
  matrix.fillTriangle(cx + 10, cy - 8, cx + 4, cy - 16, cx + 18, cy - 20, yellow); // Right Ear
  matrix.fillRoundRect(cx - 21, cy - 14, 42, 30, 14, yellow); // Head

  // ── 3. Face Details ──
  // Cheeks
  matrix.fillCircle(cx - 15, cy + 5, 5, red);
  matrix.fillCircle(cx + 15, cy + 5, 5, red);
  
  // Eyes (Black base, white glint)
  matrix.fillCircle(cx - 9, cy - 2, 4, black);
  matrix.fillCircle(cx + 9, cy - 2, 4, black);
  matrix.fillCircle(cx - 10, cy - 3, 1, white);
  matrix.fillCircle(cx + 8, cy - 3, 1, white);

  // Nose
  matrix.fillRect(cx - 1, cy + 2, 3, 2, black);

  // ── 4. Animated Mouth ──
  if (mouthOpen) {
      // Open Mouth "Pika!"
      matrix.fillCircle(cx, cy + 7, 4, black);
      matrix.fillCircle(cx, cy + 8, 2, tongue); // Red tongue
      // Hide top half of the circle with yellow to make it a 'D' shape
      matrix.fillRect(cx - 5, cy + 3, 10, 4, yellow);
  } else {
      // Closed Mouth 'w' shape
      // Left curve
      matrix.drawLine(cx - 4, cy + 5, cx - 2, cy + 7, black);
      matrix.drawLine(cx - 2, cy + 7, cx, cy + 5, black);
      // Right curve
      matrix.drawLine(cx, cy + 5, cx + 2, cy + 7, black);
      matrix.drawLine(cx + 2, cy + 7, cx + 4, cy + 5, black);
  }
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 7: Exact 8-Bit Mario Block Jump
// ══════════════════════════════════════════════════════════════════

// Perfect 16x16 Pixel Map of the "?" Block
const char* blockSprite[16] = {
  "KKKKKKKKKKKKKKKK",
  "KYYYYYYYYYYYYYDK",
  "KYYYYYYYYYYYYYDK",
  "KYYYKKKKKKYYYYDK",
  "KYYYKWWWWKYYYYDK",
  "KYYKWWKKWWKYYYDK",
  "KYYKWKYYKWWKKYDK",
  "KYYYKKYYKWWKKYDK",
  "KYYYYYYKWWKKYDKK",
  "KYYYYYYKWWKKYDKK",
  "KYYYYYYYKKYYYYDK",
  "KYYYYYKWWKYYYYDK",
  "KYYYYYKWWKYYYYDK",
  "KYYYYYYKKYYYYYDK",
  "KDDDDDDDDDDDDDDK",
  "KKKKKKKKKKKKKKKK"
};

// Perfect 16x16 Pixel Map of Mario (Modern 8-Bit Style)
const char* marioSprite[16] = {
  ".....KKKKK......",
  "....KRRRRRK.....",
  "...KRRRRRRRRK...",
  "...KBBBSKSK.K...",
  "..KBSBSSSBSSSK..",
  "..KBSBSSSSSSSK..",
  "..KBBSSSSSSBBK..",
  "...KSSSSSSS.K...",
  "..KKKRLRRRKKK...",
  ".KSSKLLRLLRKSSK.",
  ".KSSKLLRLLRKSSK.",
  ".KSSKLLYLLRKSSK.",
  "..KKKLLLLLLKKK..",
  "...KKLLLLLLKK...",
  "..KBBBK..KBBBK..",
  ".KBBBBK..KBBBBK."
};

// Extremely fast sprite renderer reading the character map
void drawSprite(int ox, int oy, const char* sprite[], int scale) {
  for (int y = 0; y < 16; y++) {
    for (int x = 0; x < 16; x++) {
      char c = sprite[y][x];
      if (c == '.') continue; // Skip transparency
      
      uint16_t color = 0;
      if (c == 'K') color = rgb(0,0,0);               // Black Outline
      else if (c == 'Y') color = rgb(255,255,0);      // Yellow
      else if (c == 'D') color = rgb(200,120,0);      // Dark Orange Shading
      else if (c == 'W') color = rgb(255,255,255);    // White
      else if (c == 'R') color = rgb(255,0,0);        // Mario Red
      else if (c == 'B') color = rgb(120,60,0);       // Mario Brown
      else if (c == 'S') color = rgb(255,200,140);    // Mario Skin
      else if (c == 'L') color = rgb(0,80,255);       // Mario Blue
      
      matrix.fillRect(ox + (x * scale), oy + (y * scale), scale, scale, color);
    }
  }
}

void drawMarioBlock() {
  matrix.fillScreen(rgb(100, 180, 255)); // Super Mario Sky Blue

  // Ground Base
  matrix.fillRect(0, 56, 64, 8, rgb(200, 76, 12)); // Brick Red Ground
  matrix.drawFastHLine(0, 56, 64, rgb(0,0,0)); // Ground Outline

  int cycle = frameCount % 80;
  int mY = 24; // Base Mario Y (Resting on the ground)
  int bY = 8;  // Base Block Y

  // Mario Jump Physics (Sine Wave)
  if (cycle >= 10 && cycle <= 40) {
    float t = (cycle - 10) / 30.0f; // Arc from 0.0 to 1.0
    mY = 24 - (int)(sinf(t * 3.14159f) * 16.0f); // 16px high jump
  }

  // Block Bump Collision
  if (cycle >= 23 && cycle <= 27) {
    bY = 4; // Block jumps up when hit by Mario's head!
  }

  // Coin Pop-up!
  if (cycle >= 24 && cycle <= 36) {
    float ct = (cycle - 24) / 12.0f;
    int cY = 8 - (int)(sinf(ct * 3.14159f) * 16.0f);
    matrix.fillRect(28, cY, 8, 12, rgb(255, 255, 0)); // Coin Body
    matrix.drawRect(28, cY, 8, 12, rgb(200, 120, 0)); // Coin Outline
  }

  // Draw the entities (Scale = 2 makes them nice and chunky!)
  drawSprite(16, bY, blockSprite, 2);
  drawSprite(16, mY, marioSprite, 2);
}

// ══════════════════════════════════════════════════════════════════
// ANIMATION 8: The Matrix Digital Rain
// ══════════════════════════════════════════════════════════════════
int rainY[W];
void drawMatrixRain() {
  // Setup arrays on the very first frame of the animation
  if (frameCount == 0) {
    for(int i=0; i<W; i++) rainY[i] = random(-60, 0);
  }
  
  matrix.fillScreen(0);
  for (int x = 0; x < W; x++) {
    rainY[x] += random(1, 3); // Fall speed
    if (rainY[x] > H + 15) rainY[x] = random(-20, 0);
    
    // Draw trail
    for (int t = 0; t < 15; t++) {
      int y = rainY[x] - t;
      if (y >= 0 && y < H) {
        if (t == 0) matrix.drawPixel(x, y, rgb(200, 255, 200)); // Bright head
        else matrix.drawPixel(x, y, rgb(0, 255 - (t * 15), 0));  // Fading tail
      }
    }
  }
}

// ══════════════════════════════════════════════════════════════════
// MODE: Audio Reactive Waveform (Standalone)
// ══════════════════════════════════════════════════════════════════
void drawWaveform() {
  static float samples[OVERSAMP];
  uint32_t sum = 0;

  for (int i = 0; i < OVERSAMP; i++) {
    int v = analogRead(MIC_PIN);
    samples[i] = (float)v;
    sum += v;
    delayMicroseconds(100); // Slight speed up for better sampling
  }

  // DEBUG: Uncomment this line to check values in Serial Monitor (115200)
  // Serial.println(sum / OVERSAMP);

  float dc = (float)sum / (float)OVERSAMP;
  float rmsSum = 0.0f;
  
  for (int i = 0; i < OVERSAMP; i++) {
    float centered = samples[i] - dc;
    rmsSum += centered * centered;
  }
  float rms = sqrtf(rmsSum / (float)OVERSAMP);

  if (!noiseInit) { noiseFloor = rms; noiseInit = true; }
  noiseFloor = (0.95f * noiseFloor) + (0.05f * rms);

  float rmsEffective = rms - (noiseFloor * 1.2f); // Slightly higher gate
  if (rmsEffective < 0.0f) rmsEffective = 0.0f;

  rmsPeak = (0.99f * rmsPeak) + (0.01f * rmsEffective);
  float loud = (rmsPeak > 5.0f) ? (rmsEffective / rmsPeak) : 0.0f;
  
  loudSmooth = (0.7f * loudSmooth) + (0.3f * loud);

  // Build points
  for (int i = 0; i < NUM_POINTS; i++) {
    float s = samples[i * (OVERSAMP / NUM_POINTS)];
    float amp = loudSmooth * (H / 2.0f);
    float y = (H / 2.0f) - ((s - dc) * amp / 500.0f); 
    
    if (y < 0) y = 0; if (y >= H) y = H - 1;
    waveY[i] = y;
  }

  uint16_t waveColor = rgb(0, 255, 0); // Solid green wave
  for (int i = 1; i < NUM_POINTS; i++) {
    matrix.drawLine((i - 1) * X_STEP, (int)waveY[i - 1], i * X_STEP, (int)waveY[i], waveColor);
  }
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  pinMode(BTN_PREV, INPUT_PULLUP);
  pinMode(BTN_NEXT, INPUT_PULLUP);
  pinMode(BTN_WAVE, INPUT_PULLUP);
  
  matrix.begin();
}

void handleButtons() {
  unsigned long now = millis();
  bool rN = digitalRead(BTN_NEXT), rP = digitalRead(BTN_PREV), rW = digitalRead(BTN_WAVE);
  if (now - lastDebounceTime > debounceDelay) {
    if (rN != lastBtnNext && rN == LOW) { currentAnim = (currentAnim + 1) % NUM_ANIMS; frameCount = 0; lastDebounceTime = now; }
    if (rP != lastBtnPrev && rP == LOW) { currentAnim = (currentAnim - 1 + NUM_ANIMS) % NUM_ANIMS; frameCount = 0; lastDebounceTime = now; }
    if (rW != lastBtnWave && rW == LOW) { waveMode = !waveMode; lastDebounceTime = now; }
  }
  lastBtnNext = rN; lastBtnPrev = rP; lastBtnWave = rW;
}

void loop() {
  handleButtons();
  if (millis() - previousMillis >= frameInterval) {
    previousMillis = millis();
    matrix.fillScreen(0);
    if (waveMode) {
      drawWaveform();
    } else {
      switch (currentAnim) {
        case 0: drawPacGhost(); break; case 1: drawCyberpunkRelic(); break; case 2: drawRetroFire(); break;
        case 3: drawXORFractal(); break; case 4: drawSynthwave(); break; case 5: drawNeonQix(); break;
        case 6: drawPikachu(); break; case 7: drawMarioBlock(); break; case 8: drawMatrixRain(); break;
      }
    }
    matrix.show(); frameCount++;
  }
}

Credits

Arnov Sharma
403 projects • 445 followers
I'm Arnov. I build, design, and experiment with tech; 3D printing, PCB design, and retro consoles are my jam.

Comments