Skip to content

Commit 6c91cac

Browse files
authored
Merge pull request #2997 from FoamyGuy/circuitpython_matrix
circuitpython matrix implementation
2 parents e4aac8b + c45d70e commit 6c91cac

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
"""
4+
Matrix rain visual effect
5+
6+
Largely ported from Arduino version in Metro_HSTX_Matrix to
7+
CircuitPython by claude with some additional tweaking to the
8+
colors and refresh functionality.
9+
"""
10+
import sys
11+
import random
12+
import time
13+
import displayio
14+
import supervisor
15+
from displayio import Group, TileGrid
16+
from tilepalettemapper import TilePaletteMapper
17+
import adafruit_imageload
18+
19+
# use the built-in HSTX display
20+
display = supervisor.runtime.display
21+
22+
# screen size in tiles, tiles are 16x16
23+
SCREEN_WIDTH = display.width // 16
24+
SCREEN_HEIGHT = display.height // 16
25+
26+
# disable auto_refresh, we'll call refresh() after each frame
27+
display.auto_refresh = False
28+
29+
# group to hold visual elements
30+
main_group = Group()
31+
32+
# show the group on the display
33+
display.root_group = main_group
34+
35+
# Color gradient list from white to dark green
36+
COLORS = [
37+
0xFFFFFF,
38+
0x88FF88,
39+
0x00FF00,
40+
0x00DD00,
41+
0x00BB00,
42+
0x009900,
43+
0x007700,
44+
0x006600,
45+
0x005500,
46+
0x005500,
47+
0x003300,
48+
0x003300,
49+
0x002200,
50+
0x002200,
51+
0x001100,
52+
0x001100,
53+
]
54+
55+
# Palette to use with the mapper. Has 1 extra color
56+
# so it can have black at index 0
57+
shader_palette = displayio.Palette(len(COLORS) + 1)
58+
# set black at index 0
59+
shader_palette[0] = 0x000000
60+
61+
# set the colors from the gradient above in the
62+
# remaining indexes
63+
for i in range(0, len(COLORS)):
64+
shader_palette[i + 1] = COLORS[i]
65+
66+
# mapper to change colors of tiles within the grid
67+
grid_color_shader = TilePaletteMapper(shader_palette, 2, SCREEN_WIDTH, SCREEN_HEIGHT)
68+
69+
# load the spritesheet
70+
katakana_bmp, katakana_pixelshader = adafruit_imageload.load("matrix_characters.bmp")
71+
72+
# how many characters are in the sprite sheet
73+
char_count = katakana_bmp.width // 16
74+
75+
# grid to display characters within
76+
display_text_grid = TileGrid(
77+
bitmap=katakana_bmp,
78+
width=SCREEN_WIDTH,
79+
height=SCREEN_HEIGHT,
80+
tile_height=16,
81+
tile_width=16,
82+
pixel_shader=grid_color_shader,
83+
)
84+
85+
# flip x to get backwards characters
86+
display_text_grid.flip_x = True
87+
88+
# add the text grid to main_group, so it will be visible on the display
89+
main_group.append(display_text_grid)
90+
91+
92+
# Define structures for character streams
93+
class CharStream:
94+
def __init__(self):
95+
self.x = 0 # X position
96+
self.y = 0 # Y position (head of the stream)
97+
self.length = 0 # Length of the stream
98+
self.speed = 0 # How many frames to wait before moving
99+
self.countdown = 0 # Counter for movement
100+
self.active = False # Whether this stream is currently active
101+
self.chars = [" "] * 30 # Characters in the stream
102+
103+
104+
# Array of character streams
105+
streams = [CharStream() for _ in range(250)]
106+
107+
# Stream creation rate (higher = more frequent new streams)
108+
STREAM_CREATION_CHANCE = 65 # % chance per frame to create new stream
109+
110+
# Initial streams to create at startup
111+
INITIAL_STREAMS = 30
112+
113+
114+
def init_streams():
115+
"""Initialize all streams as inactive"""
116+
for _ in range(len(streams)):
117+
streams[_].active = False
118+
119+
# Create initial streams for immediate visual impact
120+
for _ in range(INITIAL_STREAMS):
121+
create_new_stream()
122+
123+
124+
def create_new_stream():
125+
"""Create a new active stream"""
126+
# Find an inactive stream
127+
for _ in range(len(streams)):
128+
if not streams[_].active:
129+
# Initialize the stream
130+
streams[_].x = random.randint(0, SCREEN_WIDTH - 1)
131+
streams[_].y = random.randint(-5, -1) # Start above the screen
132+
streams[_].length = random.randint(5, 20)
133+
streams[_].speed = random.randint(0, 3)
134+
streams[_].countdown = streams[_].speed
135+
streams[_].active = True
136+
137+
# Fill with random characters
138+
for j in range(streams[_].length):
139+
# streams[i].chars[j] = get_random_char()
140+
streams[_].chars[j] = random.randrange(0, char_count)
141+
return
142+
143+
144+
def update_streams():
145+
"""Update and draw all streams"""
146+
# Clear the display (we'll implement this by looping through display grid)
147+
for x in range(SCREEN_WIDTH):
148+
for y in range(SCREEN_HEIGHT):
149+
display_text_grid[x, y] = 0 # Clear character
150+
151+
# Count active streams (for debugging if needed)
152+
active_count = 0
153+
154+
for _ in range(len(streams)):
155+
if streams[_].active:
156+
active_count += 1
157+
streams[_].countdown -= 1
158+
159+
# Time to move the stream down
160+
if streams[_].countdown <= 0:
161+
streams[_].y += 1
162+
streams[_].countdown = streams[_].speed
163+
164+
# Change a random character in the stream
165+
random_index = random.randint(0, streams[_].length - 1)
166+
# streams[i].chars[random_index] = get_random_char()
167+
streams[_].chars[random_index] = random.randrange(0, char_count)
168+
169+
# Draw the stream
170+
draw_stream(streams[_])
171+
172+
# Check if the stream has moved completely off the screen
173+
if streams[_].y - streams[_].length > SCREEN_HEIGHT:
174+
streams[_].active = False
175+
176+
177+
def draw_stream(stream):
178+
"""Draw a single character stream"""
179+
for _ in range(stream.length):
180+
y = stream.y - _
181+
182+
# Only draw if the character is on screen
183+
if 0 <= y < SCREEN_HEIGHT and 0 <= stream.x < SCREEN_WIDTH:
184+
# Set the character
185+
display_text_grid[stream.x, y] = stream.chars[_]
186+
187+
if _ + 1 < len(COLORS):
188+
grid_color_shader[stream.x, y] = [0, _ + 1]
189+
else:
190+
grid_color_shader[stream.x, y] = [0, len(COLORS) - 1]
191+
# Occasionally change a character in the stream
192+
if random.randint(0, 99) < 25: # 25% chance
193+
idx = random.randint(0, stream.length - 1)
194+
stream.chars[idx] = random.randrange(0, 112)
195+
196+
197+
def setup():
198+
"""Initialize the system"""
199+
# Seed the random number generator
200+
random.seed(int(time.monotonic() * 1000))
201+
202+
# Initialize all streams
203+
init_streams()
204+
205+
206+
def loop():
207+
"""Main program loop"""
208+
# Update and draw all streams
209+
update_streams()
210+
211+
# Randomly create new streams at a higher rate
212+
if random.randint(0, 99) < STREAM_CREATION_CHANCE:
213+
create_new_stream()
214+
215+
display.refresh()
216+
available = supervisor.runtime.serial_bytes_available
217+
if available:
218+
c = sys.stdin.read(available)
219+
if c.lower() == "q":
220+
supervisor.reload()
221+
222+
223+
# Main program
224+
setup()
225+
while True:
226+
loop()
Binary file not shown.

0 commit comments

Comments
 (0)