SmartMatrix Digital Sand

Can anyone point me to Teensy 3.5 code using SmartMatrix to make the “Digital Sand” display as shown in nearly every Adafruit panel product? They seem to usually run a Raspberry Pi and there is similar code in the Adafruit PixelDust library (Snow.cpp) but don’t have the chops to convert that to Arduinoish code. I have most of the SmartMatrix library demo programs working so my system is good. This is the panel (and demo) I have.
https://www.adafruit.com/product/5362

This is from the Matrixportal demo Examples → Adafruit Protomatter → pixeldust (which uses Arduino libraries), and I’ve modified it slightly for my uses.

You will need to modify the Adafruit_Protomatter library for the Teensy, or replace its use with the SmartMatrix libraries. Unfortunately, there are some missing board specific dependencies for Teensy 3.2/3.5/3.6. It does build for the Teensy 4.1. Note, the pin assignments are for the Matrix Portal, and you will need to modify them for the Teensy that you use. Note, that you need to use a different Smart Matrix shield (and different pins) between the Teensy 3.x and Teensy 4.x processors.

Obviously, if you use a different accelerometer, you would need to modify that code as well.

/* ----------------------------------------------------------------------
"Pixel dust" Protomatter library example. As written, this is
SPECIFICALLY FOR THE ADAFRUIT MATRIXPORTAL M4 with 64x32 pixel matrix.
Change "HEIGHT" below for 64x64 matrix. Could also be adapted to other
Protomatter-capable boards with an attached LIS3DH accelerometer.

PLEASE SEE THE "simple" EXAMPLE FOR AN INTRODUCTORY SKETCH,
or "doublebuffer" for animation basics.

The original code was an Ardunio example for the Adafruit Matrix Portal.
	Examples -> Adafruit Protomatter -> pixeldust

I (Michael Meissner, arduino@the-meissners.org) have made several changes to
this file to add a non-interactive random mode, and to reformat some of the
code to meet my personal coding style.  If you include my changes, please at
least include the attribution of those changes.

------------------------------------------------------------------------- */

#include <Wire.h>			// For I2C communication
#include <Adafruit_LIS3DH.h>		// For accelerometer
#include <Adafruit_PixelDust.h>		// For sand simulation
#include <Adafruit_Protomatter.h>	// For RGB matrix
#include <Bounce2.h>			// For reading up/down buttons.

#define HEIGHT	32			// Matrix height (pixels) - SET TO 64 FOR 64x64 MATRIX!
#define WIDTH	64			// Matrix width (pixels)
#define MAX_FPS	45			// Maximum redraw rate, frames/second

#if HEIGHT == 64			// 64-pixel tall matrices have 5 address lines:
uint8_t addrPins[] = {17, 18, 19, 20, 21};
#else					// 32-pixel tall matrices have 4 address lines:
uint8_t addrPins[] = {17, 18, 19, 20};
#endif

// Remaining pins are the same for all matrix sizes. These values
// are for MatrixPortal M4. See "simple" example for other boards.
uint8_t		rgbPins[]		= {7, 8, 9, 10, 11, 12};

const uint8_t	clockPin		= 14;
const uint8_t	latchPin		= 15;
const uint8_t	oePin			= 16;

// Pins on the Matrix portal
const uint8_t	upPin			= 2;
const uint8_t	downPin			= 3;

// Pin to switch between using the accelerometer and using random.
const uint8_t	switchPin		= upPin;
const uint32_t	switchInterval		= 25;		// use a debounce interval of 25ms
Bounce		switchBounce;
bool		do_random		= true;

Adafruit_Protomatter matrix (WIDTH,				// Matrix width in pixels,
			     4,					// bit depth
			     1, rgbPins,			// # of matrix chains, array of 6 RGB pins
			     sizeof(addrPins), addrPins,	// # of address pins (height is inferred), array of pins
			     clockPin, latchPin, oePin, true);	// other matrix control pins.

Adafruit_LIS3DH accel = Adafruit_LIS3DH();

#define N_COLORS   8
#define BOX_HEIGHT 8
#define N_GRAINS (BOX_HEIGHT*N_COLORS*8)
uint16_t colors[N_COLORS];

Adafruit_PixelDust sand(WIDTH, HEIGHT, N_GRAINS, 1, 128, false);

uint32_t prevTime = 0; // Used for frames-per-second throttle

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

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);
  }
}

void setup(void) {
  Serial.begin (115200);
  while (!Serial && millis () < 3000UL) {
    delay (10);
  }

  // Attach bounce pin
  switchBounce.attach (switchPin, INPUT_PULLUP);
  switchBounce.interval (switchInterval);

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

  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
  }
  accel.setRange(LIS3DH_RANGE_4_G);   // 2, 4, 8 or 16 G!

  //sand.randomize(); // Initialize random sand positions

  // Set up initial sand coordinates, in 8x8 blocks
  int n = 0;
  for(int i=0; i<N_COLORS; i++) {
    int xx = i * WIDTH / N_COLORS;
    int yy =  HEIGHT - BOX_HEIGHT;
    for(int y=0; y<BOX_HEIGHT; y++) {
      for(int x=0; x < WIDTH / N_COLORS; x++) {
        //Serial.printf("#%d -> (%d, %d)\n", n,  xx + x, yy + y);
        sand.setPosition(n++, xx + x, yy + y);
      }
    }
  }
  Serial.printf("%d total pixels\n", n);

  colors[0] = matrix.color565 (64,   64,  64); // Dark Gray
  colors[1] = matrix.color565 (120,  79,  23); // Brown
  colors[2] = matrix.color565 (228,   3,   3); // Red
  colors[3] = matrix.color565 (255, 140,   0); // Orange
  colors[4] = matrix.color565 (255, 237,   0); // Yellow
  colors[5] = matrix.color565 (  0, 128,  38); // Green
  colors[6] = matrix.color565 (  0,  77, 255); // Blue
  colors[7] = matrix.color565 (117,   7, 135); // Purple
}

// MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ----------------------------

void loop() {
  const  int	change	= 700;
  static int	counter	= change;
  static int	xx_i	= 0;
  static int	yy_i	= 0;
  static int	zz_i	= 0;
  double	xx;
  double	yy;
  double	zz;
  uint32_t	t;

  // Limit the animation frame rate to MAX_FPS.  Because the subsequent sand
  // calculations are non-deterministic (don't always take the same amount
  // of time, depending on their current states), this helps ensure that
  // things like gravity appear constant in the simulation.
  while(((t = micros()) - prevTime) < (1000000L / MAX_FPS))
    ;

  prevTime = t;

  // See if we pressed the button to switch between modes.
  switchBounce.update ();

  if (switchBounce.fell ()) {
    if (do_random) {
      Serial.println ("Switching to using the accelerometer");
      do_random = false;

    } else {
      Serial.println ("Switching to random mode");
      do_random = true;
      counter   = change;
    }
  }

  // Either do random mode or use the accelerometer.
  if (do_random) {
    if (counter++ > change) {
      counter = 0;
      int xx_j;
      int yy_j;
      int zz_j;
      int num = 0;
      do {
	xx_j = random (-4000, 4000);
	yy_j = random ( -400,  400);
	zz_j = random ( -400,  400);
      } while ((xx_i == xx_j) && (yy_i == yy_j) && (zz_i == zz_j) && (num++ < 5));

      xx_i = xx_j;
      yy_i = yy_j;
      zz_i = zz_j;

      Serial.printf ("xx = %5d, yy = %5d, zz = %5d\n", xx_i, yy_i, zz_i);
    }

    xx = xx_i;
    yy = yy_i;
    zz = zz_i;

  } else {

    // Read accelerometer...
    sensors_event_t event;
    accel.getEvent(&event);

    xx = event.acceleration.x * 1000;
    yy = event.acceleration.y * 1000;
    zz = event.acceleration.z * 1000;

    {
      long xx_l = (long) xx;
      long yy_l = (long) yy;
      long zz_l = (long) zz;
      long xx_f = (long) (((fabs (xx) - (fabs ((double) xx_l))) * 10.0) + 0.5);
      long yy_f = (long) (((fabs (yy) - (fabs ((double) yy_l))) * 10.0) + 0.5);
      long zz_f = (long) (((fabs (zz) - (fabs ((double) zz_l))) * 10.0) + 0.5);
      Serial.printf("xx = %8ld.%ld, yy = %8ld.%ld, zz = %8ld.%ld\n",
		    xx_l, xx_f,
		    yy_l, yy_f,
		    zz_l, zz_f);
    }
  }

  // Run one frame of the simulation
  sand.iterate (xx, yy, zz);

  //sand.iterate(-accel.y, accel.x, accel.z);

  // Update pixel data in LED driver
  dimension_t x, y;
  matrix.fillScreen(0x0);
  for(int i=0; i<N_GRAINS ; i++) {
    sand.getPosition(i, &x, &y);
    int n = i / ((WIDTH / N_COLORS) * BOX_HEIGHT); // Color index
    uint16_t flakeColor = colors[n];
    matrix.drawPixel(x, y, flakeColor);
    //Serial.printf("(%d, %d)\n", x, y);
  }
  matrix.show(); // Copy data to matrix buffers
}


// HISTORY
// $Log: Meissner_pixeldust_64x32.ino,v $
// Revision 1.13  2020/12/07 03:19:41  michaelmeissner
// Add message about changes.
//
// Revision 1.12  2020/12/07 01:52:23  michaelmeissner
// Iterate on random settings.
//
// Revision 1.11  2020/12/07 01:37:50  michaelmeissner
// Spacing.
//
// Revision 1.10  2020/12/05 23:21:49  michaelmeissner
// Bump up sizes for printing accel. information.
//
// Revision 1.9  2020/12/05 23:19:56  michaelmeissner
// Fix thinko in reporting accel. positions.
//
// Revision 1.8  2020/12/05 22:56:21  michaelmeissner
// Spacing.
//
// Revision 1.7  2020/12/05 22:55:09  michaelmeissner
// Comment parameters to the Adafruit_Protomatter setup.
//
// Revision 1.6  2020/12/05 22:46:46  michaelmeissner
// Spacing; Reset random counter when using button; Lower random change time to 700.
//
// Revision 1.5  2020/12/05 22:38:47  michaelmeissner
// Make things const; Spacing; Switch between random and accel. via the UP button.
//
// Revision 1.4  2020/12/05 22:22:29  michaelmeissner
// Spacing.
//
// Revision 1.3  2020/12/05 22:20:49  michaelmeissner
// Eliminate carriage returns.
//
// Revision 1.2  2020/12/05 22:12:33  michaelmeissner
// Spacing; switch to using random display instead of accelerometer.
//
// Revision 1.1  2020/12/05 20:56:46  michaelmeissner
// Initial version.
//

Thank you Michael. Using bits of your code and some from Overview | Shake Away 2021 with MatrixPortal | Adafruit Learning System and a lot of trial and error I got it working. Comments will be appreciated, I only pretend to be a programmer. This is a digital clock that melts into a sand display when you shake it a few times. Will be a cool Christmas present for some one. See Clock01 - YouTube
I am setting the “sand” to random colors. What I would like to do though, is randomize the pixel colors once, then maintain those colors for each pixel as it moves. I have not found a way to retrieve the pixel color from before the sand iteration. Apparently the way it works is the sand library only moves the pixel positions, It does not repaint the pixel in the buffer, that is left to the SmartMatrix library. I may have to make my own local rgb24 copy of the background buffer or maybe a local copy of the sand buffer or maybe both. I do not see a way to readpixel from the active matrix screen which I think would give me the “before” information.
The hardware for this sketch is Teensy 3.6, SmartMatrix adapter V4 (Adafruit #1902), LIS3DH accelerometer (Adafruit 2809) and 2mm LED panel (Adafruit 5362).

-------------------------------------------------------------------------
// SPDX-FileCopyrightText: 2020 Limor Fried for Adafrudit Industries
// https://learn.adafruit.com/matrixportal-shake-away-2020
// SPDX-License-Identifier: MIT
//
#include <MatrixHardware_Teensy3_ShieldV4.h>        // SmartLED Shield for Teensy 3 (V4)
#include <SmartMatrix.h>
#include <Adafruit_LIS3DH.h>                        // For accelerometer
#include <Adafruit_PixelDust.h>                     // For simulation
#include <TimeLib.h>

#define COLOR_DEPTH 24                  // Choose the color depth used for storing pixels in the layers: 24 or 48 (24 is good for most sketches - If the sketch uses type `rgb24` directly, COLOR_DEPTH must be 24)
const uint16_t kMatrixWidth = 64;       // Set to the width of your display, must be a multiple of 8
const uint16_t kMatrixHeight = 64;      // Set to the height of your display
const uint8_t kRefreshDepth = 36;       // Tradeoff of color quality vs refresh rate, max brightness, and RAM usage.  36 is typically good, drop down to 24 if you need to.  On Teensy, multiples of 3, up to 48: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48.  On ESP32: 24, 36, 48
const uint8_t kDmaBufferRows = 4;       // known working: 2-4, use 2 to save RAM, more to keep from dropping frames and automatically lowering refresh rate.  (This isn't used on ESP32, leave as default)
const uint8_t kPanelType = SM_PANELTYPE_HUB75_64ROW_MOD32SCAN;   // Choose the configuration that matches your panels.  See more details in MatrixCommonHub75.h and the docs: https://github.com/pixelmatix/SmartMatrix/wiki
const uint32_t kMatrixOptions = (SM_HUB75_OPTIONS_NONE);         // see docs for options: https://github.com/pixelmatix/SmartMatrix/wiki
const uint8_t kBackgroundLayerOptions = (SM_BACKGROUND_OPTIONS_NONE);
//const uint8_t kIndexedLayerOptions = (SM_INDEXED_OPTIONS_NONE);
//const uint8_t kScrollingLayerOptions = (SM_SCROLLING_OPTIONS_NONE);
const int displayCenter = (kMatrixHeight / 2);

SMARTMATRIX_ALLOCATE_BUFFERS(matrix, kMatrixWidth, kMatrixHeight, kRefreshDepth, kDmaBufferRows, kPanelType, kMatrixOptions);
SMARTMATRIX_ALLOCATE_BACKGROUND_LAYER(backgroundLayer, kMatrixWidth, kMatrixHeight, COLOR_DEPTH, kBackgroundLayerOptions);

const int defaultBrightness = (80 * 255) / 100;  // 80% brightness

const rgb24 Black = {0x00, 0x00, 0x00};
const rgb24 White = {0xFF, 0xFF, 0xFF};

#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    5                                   // Number of accel readings to trigger sand
#define SHAKE_PERIOD    3000                                // Period (in ms) when SHAKE_EVENTS must happen
#define SAND_TIME       30000                               // Time (in ms) to run simulation before restarting

#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 sand trigger)
Adafruit_PixelDust *sand;               // Sand object (allocated in setup())

Adafruit_LIS3DH accel = Adafruit_LIS3DH(); // Accelerometer
sensors_event_t event;                  // Accelerometer result

/////////////////////////////////////////////////////////////////
void setup(void) {

  // set the Time library to use Teensy 3.0's RTC
  setSyncProvider(getTeensy3Time);

  Serial.begin(115200);
  delay(100);                             // Wait for serial to start
  // Get clock time from the connected PC (if available)
  // Mandatory only if clock battery is not provided
  if (timeStatus() != timeSet) {
    Serial.println("Unable to sync with the RTC");
    Serial.println("Defaulting to stored clock");
  } else {
    Serial.println("RTC has set from PC system time");
  }

  // Setup SmartMatrix
  matrix.addLayer(&backgroundLayer);
  matrix.begin();
  matrix.setBrightness(defaultBrightness);
  Serial.println("Matrix begin()");
  backgroundLayer.setFont(gohufont11b);      // Font is 11 pixels high

  // PixelDust set up is deferred until sandSimulation()

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


}
/////////////////////////////////////////////////////////////////
void loop() {
  char timeBuffer[10];             // Holds formatted time string
  int indent;                      // Left margin for prints to screen
  static int lastsec;              // To trigger a time update
  static int numGrains;            // # of pixels lit
  static bool notStirred = false;  // Shake counter entry flag
  time_t t;                        // Used to record current time in MS
  static time_t last_event_time;   // Calculated end of allowed shake period
  static int num_events;           // Counts shake events

  if (second() != lastsec) {       // Update the clock display once per second
    lastsec = second();
    backgroundLayer.fillScreen(Black);
    Serial.println(second());

    // Draw Clock to SmartMatrix using AM/PM 12 hour time
    //       Font position reference point is top left of first char
    //       Pixel zero is top left of display
    uint8_t Hour = hourFormat12();
    static char AMPM = 'A';
    if (isPM()) AMPM = 'P';

    // Draw time
    indent = 5;                    //  Left margin allow for 2 digit time
    if (Hour < 10) indent = 9;     //  Left margin allow for 3 digits
    sprintf(timeBuffer, "%d:%02d:%02d%c", Hour, minute(), second(), AMPM);
    backgroundLayer.drawString(indent, displayCenter - 18, White, timeBuffer);

    // Draw date
    indent = 4;                    //  Left margin allow for 2 digit month
    if (month() < 10) indent = 6;  //  Left margin allow for 3 digits month
    sprintf(timeBuffer, "%d/%02d/%02d", month(), day(), year());
    backgroundLayer.drawString(indent, displayCenter, White, timeBuffer);
  }

  // Look for shakes
  t = millis();                               // Current time
  if ((t > last_event_time)) {                // Timed out!
    notStirred = false;                       // Reset for another try
    //    num_events = 0;
  }

  accel.getEvent(&event);
  // Square the accelerometer readings (why?)
  // x**2 + y**2 + z**2
  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) {              // A Shake has been detected
    delay(5);                                // Debounce the accelerometer

    // notStirred is true if already counting shakes, false if first one
    if (notStirred) {                        // Count additional shakes
      if (++num_events >= SHAKE_EVENTS) {    // Enough shakes detected?

        // Cue the digital sand
        sandSimulation();

        num_events = 0;       // Reset for next shakes interval
      }
    } else {                // notStirred is false, initial shake detected
      notStirred = true;    // In a shake interval,flag to look for more
      num_events = 1;
      last_event_time = t + SHAKE_PERIOD; // Allowed interval for shakes
    }
  }                         // End of shake detect

  // OK to swap now
  backgroundLayer.swapBuffers();
}                           // End of Loop()


/* Function to create a random color pixel in rgb24 form */
rgb24 randomPixel() {
  uint8_t Red = random (255);
  uint8_t Grn = random (255);
  uint8_t Blu = random (255);
  return {Red, Grn, Blu};
}


/* function to retrieve time from Teensy RTC */
time_t getTeensy3Time()
{
  return Teensy3Clock.get();
}


/*  code to process initial time sync messages from the serial port   */
#define TIME_HEADER  "T"   // Header tag for serial time sync message

unsigned long processSyncMessage() {
  unsigned long pctime = 0L;
  const unsigned long DEFAULT_TIME = 1357041600; // Jan 1 2013

  if (Serial.find(TIME_HEADER)) {
    pctime = Serial.parseInt();
    return pctime;
    if ( pctime < DEFAULT_TIME) { // check the value is a valid time (greater than Jan 1 2013)
      pctime = 0L; // return 0 to indicate that the time is not valid
    }
  }
  return pctime;
}


/* 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);
  }
}


/* Run sand simulation for a few seconds
   At this point the background layer has not been swapped
*/
void sandSimulation() {
  int n_grains = 0;
  time_t t, elapsed;
  time_t prevTime, sandStartTime = millis();
  rgb24 thisPixel;
  int i, j = 0, b;

  // Count number of 'on' pixels (sand grains) in background buffer
  // This must be done *before* swapping buffers
  // as readPixel doesn't work in matrix layer.
  for (i = 0; i < kMatrixHeight; i++) {              // Row index (y coordinate)
    for (b = 0; b < kMatrixWidth; b++) {             // Column index (x coordinate)
      thisPixel = backgroundLayer.readPixel(i, b);

      if (thisPixel.red | thisPixel.green | thisPixel.blue) {
        n_grains++;
      }
    }
  }

  // Set initial sand pixel positions and draw initial matrix state
  // Allocate sand object based on matrix size and bitmap 'on' pixels
  sand = new Adafruit_PixelDust(kMatrixWidth, kMatrixHeight, n_grains, 1);
  if (!sand->begin()) {
    Serial.println("PixelDust init failed");
    return;
  }
  sand->clear();

  // Rescan the display and attach lit pixels to the sand grains
  for (i = 0; i < kMatrixHeight; i++) {              // Row index
    for (b = 0; b < kMatrixWidth; b++) {             // Column index
      thisPixel = backgroundLayer.readPixel(b, i);

      if (thisPixel.red | thisPixel.green | thisPixel.blue) {
        sand->setPosition(j++, b, i);
      }
    }
  }

  while (true) {                // Hijack loop()
    // Only run for specified time
    while ((elapsed = (millis() - sandStartTime)) < SAND_TIME) {

      // Limit the animation frame rate to MAX_FPS.
      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);

      // Update pixel data in LED driver
      backgroundLayer.fillScreen(Black);
      dimension_t x, y;
      for (i = 0; i < n_grains ; i++) {
        sand->getPosition(i, &x, &y);

        // Would be nice to getPixel color here and reuse in the drawPixel
        // so individual grains don't shimmer
        //       backgroundLayer.drawPixel(x, y, Blue);  // works
        backgroundLayer.drawPixel(x, y, randomPixel());  // works
      }

      backgroundLayer.swapBuffers(false);

    }                                      // End of elapsed time test
    return;                                // Exit back to displaying clock
  }                                        // end of while true loop
}

I really like the effect, great job!

I find the transition from sand to the clock again to be abrupt. Some suggestions:

  • Simple: fade out the sand and fade in the clock
  • Advanced: make the clock pixels “sticky” so that sand that hits one of the clock pixels gets stuck there.
    • If the user isn’t actively moving sand when you want the clock to be shown, dump a load of sand from the top whatever doesn’t stick drops off the bottom

I do not see a way to readpixel from the active matrix screen

I may have to make my own local rgb24 copy of the background buffer or maybe a local copy of the sand buffer or maybe both

I’m not exactly sure what you need to do with an additional layer, but you can use multiple layers in SmartMatrix Library. The background layer is opaque, so if you’re adding multiple background layers, the last one you add will be the only one visible. You can create layers that aren’t refreshed to the display (by not calling addLayer(), but there are some things to look out for there. swapBuffers won’t work if the layer isn’t being refreshed. You can draw to a layer, get the pixel value, and clear the layer, so if you want to use a layer as a “scratchpad” for seeing where text is for example, you could do that. For text, using indexedLayer (see MatrixClock example) will only use one bit per pixels instead of 24 bits.

Happy to chat about this project more but I’m out of time now

Thank you Louis. I have the clock working as I hoped. The digital sand feature assigns random colors and maintains that color for each pixel as the sand plays out. I like this a lot better than continuously random generated colors. I did this by declaring an rgb24[4096] array which records the initial randomly selected color for each grain of sand. Then for each sand iteration I look up the color and paint the pixels. I made a nice Walnut box for it.

Short video at:

Boy am I glad I posted this code here. Had a hard drive die Thursday and lost the working version. Was able to recreate from this post in a few hours.

Louis: I was able to get fade in/out working as you suggested. It looks nice.