// original code: https://wokwi.com/projects/318864638990090834
#define JUMPING_TEXT false // change it to true for jumping text at the bottom
#define CLK 13
#define DIN 11
#define CS 10
#define X_SEGMENTS 4 // No of chains
#define Y_SEGMENTS 6 // No of panels
#define NUM_SEGMENTS (X_SEGMENTS * Y_SEGMENTS)
#define STAR_HEIGHT 8
#define TREE_TOP STAR_HEIGHT
#define TREE_HEIGHT 28
#define TRUNK_HEIGHT 4
#define displayText "Merry Christmas" // text to display at the bottom
#define textBase (Y_SEGMENTS - 1) * 8
int textOffset = 0;
bool invertText = false;
byte fb[8 * NUM_SEGMENTS];
struct Burst {
int cx;
int cy;
int r;
bool active;
};
Burst leftBurst = {2, 2, 0, true};
Burst rightBurst = {29, 2, 0, true};
const byte font5x7[][5] = {
{0x00,0x00,0x00,0x00,0x00}, // space
{0x7E,0x18,0x10,0x18,0x7E}, // M
{0x38,0x7C,0x54,0x54,0x5C}, // e
{0x7C,0x08,0x04,0x04,0x08}, // r
{0x00,0x5C,0x50,0x50,0x7C}, // y
{0x3C,0x42,0x42,0x42,0x24}, // C
{0x7E,0x7E,0x08,0x08,0x78}, // h
{0x00,0x44,0x7D,0x40,0x00}, // i
{0x48,0x54,0x54,0x54,0x24}, // s
{0x04,0x3F,0x44,0x40,0x20}, // t
{0x20,0x54,0x54,0x54,0x78}, // a
{0x7C,0x18,0x78,0x18,0x78} // m
};
const byte font5x7_inverted[][5] = {
{0x7F,0x7F,0x7F,0x7F,0x7F}, // space
{0x01,0x67,0x6F,0x67,0x01}, // M
{0x47,0x03,0x2B,0x2B,0x23}, // e
{0x03,0x77,0x7B,0x7B,0x77}, // r
{0x7F,0x23,0x2F,0x2F,0x03}, // y
{0x43,0x3D,0x3D,0x3D,0x5B}, // C
{0x01,0x01,0x77,0x77,0x07}, // h
{0x7F,0x3B,0x02,0x3F,0x7F}, // i
{0x37,0x2B,0x2B,0x2B,0x5B}, // s
{0x7B,0x40,0x3B,0x3F,0x5F}, // t
{0x5F,0x2B,0x2B,0x2B,0x07}, // a
{0x03,0x67,0x07,0x67,0x07} // m
};
int fontIndex(char c) {
switch (c) {
case ' ': return 0;
case 'M': return 1;
case 'e': return 2;
case 'r': return 3;
case 'y': return 4;
case 'C': return 5;
case 'h': return 6;
case 'i': return 7;
case 's': return 8;
case 't': return 9;
case 'a': return 10;
case 'm': return 11;
}
return 0;
}
void shiftAll(byte send_to_address, byte send_this_data)
{
digitalWrite(CS, LOW); // Starts listening
for (int i = 0; i < NUM_SEGMENTS; i++) {
shiftOut(DIN, CLK, MSBFIRST, send_to_address);
shiftOut(DIN, CLK, MSBFIRST, send_this_data);
}
digitalWrite(CS, HIGH); // Shows the output
}
void setup() {
Serial.begin(115200);
pinMode(CLK, OUTPUT);
pinMode(DIN, OUTPUT);
pinMode(CS, OUTPUT);
// Setup each MAX7219
shiftAll(0x0f, 0x00); //display test register
shiftAll(0x0b, 0x07); //scan limit register
shiftAll(0x0c, 0x01); //shutdown register
shiftAll(0x0a, 0x0f); //intensity register
shiftAll(0x09, 0x00); //decode mode register
}
void loop() {
clear();
drawStar(3); // distance from top
updateBursts();
drawTree(16, TREE_TOP, TREE_HEIGHT);
drawTrunk(16, TREE_TOP + TREE_HEIGHT, TRUNK_HEIGHT);
for (int i = 0; i < 4; i++) {
twinkle(16, TREE_TOP, TREE_HEIGHT);
}
scrollTextRed(displayText);
show();
delay(40);
}
void drawTree(int cx, int top, int height) {
for (int y = 0; y < height; y++) {
int halfWidth = y / 2 + 1;
for (int x = cx - halfWidth; x <= cx + halfWidth; x++) {
safe_pixel(x, top + y, 1);
}
}
}
void drawTrunk(int cx, int top, int height) {
for (int y = 0; y < height; y++) {
for (int x = cx - 1; x <= cx + 1; x++) {
safe_pixel(x, top + y, 1);
}
}
}
void set_pixel(uint8_t x, uint8_t y, uint8_t mode) {
byte *addr = &fb[x / 8 + y * X_SEGMENTS];
byte mask = 128 >> (x % 8);
switch (mode) {
case 0: // clear pixel
*addr &= ~mask;
break;
case 1: // plot pixel
*addr |= mask;
break;
case 2: // XOR pixel
*addr ^= mask;
break;
}
}
void safe_pixel(uint8_t x, uint8_t y, uint8_t mode) {
if ((x >= X_SEGMENTS * 8) || (y >= Y_SEGMENTS * 8))
return;
set_pixel(x, y, mode);
}
// turn off every LED in the framebuffer
void clear() {
byte *addr = fb;
for (byte i = 0; i < 8 * NUM_SEGMENTS; i++)
*addr++ = 0;
}
// send the raster order framebuffer in the correct order
void show() {
for (byte row = 0; row < 8; row++) {
digitalWrite(CS, LOW);
byte segment = NUM_SEGMENTS;
while (segment--) {
byte x = segment % X_SEGMENTS;
byte y = segment / X_SEGMENTS * 8;
byte addr = (row + y) * X_SEGMENTS;
if (segment & X_SEGMENTS) { // odd rows of segments
shiftOut(DIN, CLK, MSBFIRST, 8 - row);
shiftOut(DIN, CLK, LSBFIRST, fb[addr + x]);
}
else { // even rows of segments
shiftOut(DIN, CLK, MSBFIRST, 1 + row);
shiftOut(DIN, CLK, MSBFIRST, fb[addr - x + X_SEGMENTS - 1]);
}
}
digitalWrite(CS, HIGH);
}
}
void twinkle(int cx, int top, int height) {
int y = random(top, top + height);
int halfWidth = (y - top) / 2 + 1;
int x = random(cx - halfWidth, cx + halfWidth + 1);
safe_pixel(x, y, 2);
}
void drawStar(int topY) {
int cx = 16;
int size = 7; // odd number only
topY = topY - (size - 5);
int half = size / 2;
for (int i = 0; i < size; i++) {
int rowWidth = (i <= half) ? i : size - i - 1;
for (int w = -rowWidth; w <= rowWidth; w++) {
safe_pixel(cx + w, topY + i, 1);
}
}
}
void burstTwinkles(Burst &b) {
int count = b.r * 2;
for (int i = 0; i < count; i++) {
int angle = random(0, 360);
float rad = radians(angle);
int x = b.cx + cos(rad) * (b.r + random(-1, 2));
int y = b.cy + sin(rad) * (b.r + random(-1, 2));
if (y < 0 || y >= STAR_HEIGHT) continue;
bool nearStar =
(x >= 14 && x <= 18) &&
(y >= 0 && y <= 6);
if (nearStar) continue;
safe_pixel(x, y, 2);
}
}
void drawBurst(Burst &b) {
if (!b.active) return;
for (int y = 0; y < STAR_HEIGHT; y++) {
for (int x = 0; x < X_SEGMENTS * 8; x++) {
bool nearStar =
(x >= 14 && x <= 18) &&
(y >= 0 && y <= 6);
if (nearStar) continue;
int dx = x - b.cx;
int dy = y - b.cy;
int d2 = dx * dx + dy * dy;
if (d2 >= b.r * b.r && d2 < (b.r + 1) * (b.r + 1)) {
safe_pixel(x, y, 1);
}
}
}
burstTwinkles(b);
b.r++;
if (b.r > 6) {
b.active = false;
}
}
void updateBursts() {
drawBurst(leftBurst);
drawBurst(rightBurst);
if (!leftBurst.active && !rightBurst.active) {
leftBurst = {2, 2, 0, true};
rightBurst = {29, 2, 0, true};
}
}
void scrollTextRed(const char *text) {
int msgWidth = textWidth(text);
int gap = 8;
int cycleWidth = msgWidth + gap;
textOffset--;
if (textOffset < -cycleWidth) {
textOffset += cycleWidth;
}
for (int k = 0; k < 2; k++) {
int startX = textOffset + (k * cycleWidth);
int xCursor = startX;
if (startX > (X_SEGMENTS * 8)) continue;
if (startX + msgWidth < 0) continue;
bool currentInvert = invertText;
for (int i = 0; text[i]; i++) {
int idx = fontIndex(text[i]);
currentInvert = !currentInvert;
for (int col = 0; col < 5; col++) {
byte column = font5x7[idx][col];
if (JUMPING_TEXT && !currentInvert) {
column = font5x7_inverted[idx][col];
}
for (int row = 0; row < 7; row++) {
if (column & (1 << row)) {
safe_pixel(xCursor + col, textBase + row + 1, 1);
}
}
}
xCursor += 6;
}
}
invertText = !invertText;
}
int textWidth(const char *text) {
int width = 0;
for (int i = 0; text[i]; i++) {
width += 6;
}
return width;
}