Shake away those 2021 blues with a MatrixPortal project that will help you celebrate the end of one year, and the bright-and-shiny promise of a new year!

Here are a couple of pre-compiled .UF2 files. To install, connect MatrixPortal to computer with USB cable. Double-tap the reset button, wait for MATRIXBOOT drive to appear and drag one of the .UF2 files to MATRIXBOOT.

Both start with “2021” on the matrix. With a quick shake, it’s reduced to pixel sand.

The first of these UF2s then switches to the glimmering “2022” after a few seconds of sand (six seconds from the shake, if you’re counting down to midnight). Tap the reset button to start over.

The other UF2 instead returns to 2021 and watches for another shake:

Here’s an earlier 2020–2021 animation…there are settings in the code so it can be updated each year.

If you'd like to check out the code, it's based off of the Protomatter Pixeldust demo, where we use a bitmap of the year’s text for the pixels.

Click Download Project Zip in the embed below to get both the Arduino sketch and the extra files that store the bitmaps.

// SPDX-FileCopyrightText: 2020 Limor Fried for Adafruit Industries
//
// SPDX-License-Identifier: MIT

#include <Adafruit_LIS3DH.h>      // For accelerometer
#include <Adafruit_PixelDust.h>   // For simulation
#include <Adafruit_Protomatter.h> // For LED matrix
#include "2020.h"                 // 2020 bitmap data
#include "2021.h"                 // 2021 bitmap data
#include "2022.h"                 // etc.
#include "2023.h"
#include "2024.h"
#include "2025.h"
#include "2026.h"

#define BITMAP_WIDTH  64 // All the year bitmaps are a fixed size
#define BITMAP_HEIGHT 32
#define THIS_YEAR_BITMAP bitmap_2021 // Name of current/next year bitmap
#define NEXT_YEAR_BITMAP bitmap_2022 // arrays in header files

bool show_new_year = true;

#define SHAKE_ACCEL_G   2.9                                 // Force (in Gs) to trigger shake
#define SHAKE_ACCEL_MS2 (SHAKE_ACCEL_G * 9.8)               // Convert to m/s^2
#define SHAKE_ACCEL_SQ  (SHAKE_ACCEL_MS2 * SHAKE_ACCEL_MS2) // Avoid sqrt() in accel check
#define SHAKE_EVENTS    30                                  // Number of accel readings to trigger sand
#define SHAKE_PERIOD    2000                                // Period (in ms) when SHAKE_EVENTS must happen
#define SAND_TIME       6000                                // Time (in ms) to run simulation before restarting

uint8_t rgbPins[]  = {7, 8, 9, 10, 11, 12};
uint8_t addrPins[] = {17, 18, 19, 20};
uint8_t clockPin   = 14;
uint8_t latchPin   = 15;
uint8_t oePin      = 16;

// 64x32 pixel matrix, 6-bit depth
Adafruit_Protomatter matrix(
  64, 6, 1, rgbPins, 4, addrPins, clockPin, latchPin, oePin, true);

Adafruit_LIS3DH accel = Adafruit_LIS3DH(); // Accelerometer

#define MAX_FPS 60        // Maximum redraw rate, frames/second
uint32_t prevTime = 0;    // For frames-per-second throttle
uint16_t n_grains = 0;    // Number of sand grains (counted on startup)
Adafruit_PixelDust *sand; // Sand object (allocated in setup())

// Error handler used by setup()
void err(int x) {
  uint8_t i;
  pinMode(LED_BUILTIN, OUTPUT);       // Using onboard LED
  for(i=1;;i++) {                     // Loop forever... 
    digitalWrite(LED_BUILTIN, i & 1); // LED on/off blink to alert user
    delay(x);
  }
}

// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------

void setup(void) {
  uint8_t i, j, bytes;
  Serial.begin(115200);
  //while (!Serial);

  ProtomatterStatus status = matrix.begin();
  Serial.printf("Protomatter begin() status: %d\n", status);

  // Count number of 'on' pixels (sand grains) in THIS_YEAR_BITMAP
  for (int i=0; i<sizeof(THIS_YEAR_BITMAP); i++) {
    for (int b=0; b<8; b++) {
      if (THIS_YEAR_BITMAP[i] & (1 << b)) {
        n_grains++;
      }
    }
  }
  Serial.printf("Bitmap has %d grains\n", n_grains);

  // Allocate sand object based on matrix size and bitmap 'on' pixels
  sand = new Adafruit_PixelDust(matrix.width(), matrix.height(), n_grains, 1);
  if  (!sand->begin()) {
    Serial.println("Couldn't start sand");
    err(1000); // Slow blink = malloc error
  }

  if  (!accel.begin(0x19)) {
    Serial.println("Couldn't find accelerometer");
    err(250);  // Fast bink = I2C error
  }
  Serial.println("Accelerometer OK");
  accel.setRange(LIS3DH_RANGE_8_G);
}

void loop() {
  Serial.print("Tick");
  uint16_t sandColor = show_new_year ? 0xF800 : 0xFFFF; // Red or white

  // Set initial sand pixel positions and draw initial matrix state
  sand->clear();
  matrix.fillScreen(0);
  int grain = 0, pixel = 0; // Sand grain and pixel indices
  for (int i=0; i<sizeof(THIS_YEAR_BITMAP); i++) {
    for (int b=0; b<8; b++, pixel++) {
      if (THIS_YEAR_BITMAP[i] & (1 << (7-b))) {
        int x = pixel % BITMAP_WIDTH;
        int y = pixel / BITMAP_WIDTH;
        //Serial.printf("Set pixel %d @ (%d, %d)\n", grain, x, y);
        sand->setPosition(grain++, x, y);
        matrix.drawPixel(x, y, sandColor);
      }
    }
  }
  matrix.show();

  // Wait for shake
  uint32_t first_event_time = millis() - SHAKE_PERIOD * 2, last_event_time = first_event_time;
  uint8_t  num_events = 0;
  sensors_event_t event;
  for (;;) {
    uint32_t t = millis(); // Current time
    accel.getEvent(&event);
    float mag2 = event.acceleration.x * event.acceleration.x +
                 event.acceleration.y * event.acceleration.y +
                 event.acceleration.z * event.acceleration.z;
    if (mag2 >= SHAKE_ACCEL_SQ) {                 // Accel exceeds shake threshold
      if ((t - last_event_time) > SHAKE_PERIOD) { // Long time since last event?
        first_event_time = t;                     // Start of new count
        num_events = 1;
      } else if ((t - first_event_time) < SHAKE_PERIOD) { // Still in shake interval?
        if (++num_events >= SHAKE_EVENTS) {               // Enough events?
          break;
        }
      }
      last_event_time = t;
    }
  }

  // Run sand simulation for a few seconds
  uint32_t elapsed, sandStartTime = millis();

  while((elapsed = (millis() - sandStartTime)) < SAND_TIME) {

    // Limit the animation frame rate to MAX_FPS.
    uint32_t t;
    while(((t = micros()) - prevTime) < (1000000L / MAX_FPS));
    prevTime = t;

    // Read accelerometer...
    sensors_event_t event;
    accel.getEvent(&event);
  
    // Run one frame of the simulation
    sand->iterate(event.acceleration.x * 1024, event.acceleration.y * 1024, event.acceleration.z * 1024);

    if (elapsed > SAND_TIME * 3 / 4) {
      float scale = 1.0 - (float)(elapsed - (SAND_TIME * 3 / 4)) / (float)(SAND_TIME / 4);
      if (scale < 0.0) scale = 0.0;
      else if (scale > 1.0) scale = 1.0;
      scale = pow(scale, 2.6);
      uint16_t rb = (int)(31.0 * scale + 0.5);
      uint16_t g = (int)(63.0 * scale + 0.5);
      if (show_new_year)
        sandColor = (rb * 0b100000000000); // Just show red
      else
        sandColor = (rb * 0b100000000001) + (g << 5);
    }

    // Update pixel data in LED driver
    matrix.fillScreen(0);
    dimension_t x, y;
    for(int i=0; i<n_grains ; i++) {
      sand->getPosition(i, &x, &y);
      matrix.drawPixel(x, y, sandColor);
    }
    matrix.show();
  }

  // If the show_new_year flag is set, don't return to shake detect,
  // instead switch to sparkly display forever (reset to start over)
  if (show_new_year) {
    uint16_t frame = 0;
    matrix.fillScreen(0);
    for(;;) {
      int pixel = 0;
      for (int i=0; i<sizeof(NEXT_YEAR_BITMAP); i++) {
        for (int b=0; b<8; b++, pixel++) {
          if (NEXT_YEAR_BITMAP[i] & (1 << (7-b))) {
            int x = pixel % BITMAP_WIDTH;
            int y = pixel / BITMAP_WIDTH;
            matrix.drawPixel(x, y, (random() & 1) ? ((((x - y + frame) / 8) & 1) ? 0xFFFF : 0x001F) : 0);
          }
        }
      }
      matrix.show();
      delay(18);
      frame++;
    }
  }
}

The year bitmaps started as 64x32 pixel PNG images that were converted to C header files through the media2array.py Python script.

This guide was first published on Dec 30, 2020. It was last updated on Dec 30, 2020.