From c45d70e536a6a8a3c4284d4064606c7c29a413e6 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Sun, 23 Mar 2025 10:51:16 -0500 Subject: [PATCH] circuitpython matrix implementation --- .../Metro_RP2350_CircuitPython_Matrix/code.py | 226 ++++++++++++++++++ .../matrix_characters.bmp | Bin 0 -> 3730 bytes 2 files changed, 226 insertions(+) create mode 100644 Metro/Metro_RP2350_CircuitPython_Matrix/code.py create mode 100644 Metro/Metro_RP2350_CircuitPython_Matrix/matrix_characters.bmp diff --git a/Metro/Metro_RP2350_CircuitPython_Matrix/code.py b/Metro/Metro_RP2350_CircuitPython_Matrix/code.py new file mode 100644 index 000000000..d383c7f14 --- /dev/null +++ b/Metro/Metro_RP2350_CircuitPython_Matrix/code.py @@ -0,0 +1,226 @@ +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT +""" +Matrix rain visual effect + +Largely ported from Arduino version in Metro_HSTX_Matrix to +CircuitPython by claude with some additional tweaking to the +colors and refresh functionality. +""" +import sys +import random +import time +import displayio +import supervisor +from displayio import Group, TileGrid +from tilepalettemapper import TilePaletteMapper +import adafruit_imageload + +# use the built-in HSTX display +display = supervisor.runtime.display + +# screen size in tiles, tiles are 16x16 +SCREEN_WIDTH = display.width // 16 +SCREEN_HEIGHT = display.height // 16 + +# disable auto_refresh, we'll call refresh() after each frame +display.auto_refresh = False + +# group to hold visual elements +main_group = Group() + +# show the group on the display +display.root_group = main_group + +# Color gradient list from white to dark green +COLORS = [ + 0xFFFFFF, + 0x88FF88, + 0x00FF00, + 0x00DD00, + 0x00BB00, + 0x009900, + 0x007700, + 0x006600, + 0x005500, + 0x005500, + 0x003300, + 0x003300, + 0x002200, + 0x002200, + 0x001100, + 0x001100, +] + +# Palette to use with the mapper. Has 1 extra color +# so it can have black at index 0 +shader_palette = displayio.Palette(len(COLORS) + 1) +# set black at index 0 +shader_palette[0] = 0x000000 + +# set the colors from the gradient above in the +# remaining indexes +for i in range(0, len(COLORS)): + shader_palette[i + 1] = COLORS[i] + +# mapper to change colors of tiles within the grid +grid_color_shader = TilePaletteMapper(shader_palette, 2, SCREEN_WIDTH, SCREEN_HEIGHT) + +# load the spritesheet +katakana_bmp, katakana_pixelshader = adafruit_imageload.load("matrix_characters.bmp") + +# how many characters are in the sprite sheet +char_count = katakana_bmp.width // 16 + +# grid to display characters within +display_text_grid = TileGrid( + bitmap=katakana_bmp, + width=SCREEN_WIDTH, + height=SCREEN_HEIGHT, + tile_height=16, + tile_width=16, + pixel_shader=grid_color_shader, +) + +# flip x to get backwards characters +display_text_grid.flip_x = True + +# add the text grid to main_group, so it will be visible on the display +main_group.append(display_text_grid) + + +# Define structures for character streams +class CharStream: + def __init__(self): + self.x = 0 # X position + self.y = 0 # Y position (head of the stream) + self.length = 0 # Length of the stream + self.speed = 0 # How many frames to wait before moving + self.countdown = 0 # Counter for movement + self.active = False # Whether this stream is currently active + self.chars = [" "] * 30 # Characters in the stream + + +# Array of character streams +streams = [CharStream() for _ in range(250)] + +# Stream creation rate (higher = more frequent new streams) +STREAM_CREATION_CHANCE = 65 # % chance per frame to create new stream + +# Initial streams to create at startup +INITIAL_STREAMS = 30 + + +def init_streams(): + """Initialize all streams as inactive""" + for _ in range(len(streams)): + streams[_].active = False + + # Create initial streams for immediate visual impact + for _ in range(INITIAL_STREAMS): + create_new_stream() + + +def create_new_stream(): + """Create a new active stream""" + # Find an inactive stream + for _ in range(len(streams)): + if not streams[_].active: + # Initialize the stream + streams[_].x = random.randint(0, SCREEN_WIDTH - 1) + streams[_].y = random.randint(-5, -1) # Start above the screen + streams[_].length = random.randint(5, 20) + streams[_].speed = random.randint(0, 3) + streams[_].countdown = streams[_].speed + streams[_].active = True + + # Fill with random characters + for j in range(streams[_].length): + # streams[i].chars[j] = get_random_char() + streams[_].chars[j] = random.randrange(0, char_count) + return + + +def update_streams(): + """Update and draw all streams""" + # Clear the display (we'll implement this by looping through display grid) + for x in range(SCREEN_WIDTH): + for y in range(SCREEN_HEIGHT): + display_text_grid[x, y] = 0 # Clear character + + # Count active streams (for debugging if needed) + active_count = 0 + + for _ in range(len(streams)): + if streams[_].active: + active_count += 1 + streams[_].countdown -= 1 + + # Time to move the stream down + if streams[_].countdown <= 0: + streams[_].y += 1 + streams[_].countdown = streams[_].speed + + # Change a random character in the stream + random_index = random.randint(0, streams[_].length - 1) + # streams[i].chars[random_index] = get_random_char() + streams[_].chars[random_index] = random.randrange(0, char_count) + + # Draw the stream + draw_stream(streams[_]) + + # Check if the stream has moved completely off the screen + if streams[_].y - streams[_].length > SCREEN_HEIGHT: + streams[_].active = False + + +def draw_stream(stream): + """Draw a single character stream""" + for _ in range(stream.length): + y = stream.y - _ + + # Only draw if the character is on screen + if 0 <= y < SCREEN_HEIGHT and 0 <= stream.x < SCREEN_WIDTH: + # Set the character + display_text_grid[stream.x, y] = stream.chars[_] + + if _ + 1 < len(COLORS): + grid_color_shader[stream.x, y] = [0, _ + 1] + else: + grid_color_shader[stream.x, y] = [0, len(COLORS) - 1] + # Occasionally change a character in the stream + if random.randint(0, 99) < 25: # 25% chance + idx = random.randint(0, stream.length - 1) + stream.chars[idx] = random.randrange(0, 112) + + +def setup(): + """Initialize the system""" + # Seed the random number generator + random.seed(int(time.monotonic() * 1000)) + + # Initialize all streams + init_streams() + + +def loop(): + """Main program loop""" + # Update and draw all streams + update_streams() + + # Randomly create new streams at a higher rate + if random.randint(0, 99) < STREAM_CREATION_CHANCE: + create_new_stream() + + display.refresh() + available = supervisor.runtime.serial_bytes_available + if available: + c = sys.stdin.read(available) + if c.lower() == "q": + supervisor.reload() + + +# Main program +setup() +while True: + loop() diff --git a/Metro/Metro_RP2350_CircuitPython_Matrix/matrix_characters.bmp b/Metro/Metro_RP2350_CircuitPython_Matrix/matrix_characters.bmp new file mode 100644 index 0000000000000000000000000000000000000000..4264fd97fb4e0fe3cd30d966fc9edfeaf2a90d19 GIT binary patch literal 3730 zcmb7G&2Aev5FTi1OvU~P#}bn6fTg1 zk&};p6dii#V=Rh3fRK|8g52*L?n;VX1L=%c`5!+e&u+ra4RUtk*yhe))9m#>wVOXR@SeDSe%v27 zy|*}}em# zkxbkK6L_{PMn~F8E-amOP(+cQm^V|z2xYyIHbx{7|joEB4&xYS{rNvs-<*R zOYpE7@*AS6$u0mvXLo6ci<0%1KWW<|8x;dYnmu54o^MQ)7(B8kNy-hCrz@;3{McOXcEtAly3vh=wBH!Jk|cWPm;2NSrQZAu+yi3#}L= z0ug3PgER?s)JB0Kel!^`BqKwM46URStZTIx&X6V5R}4~o$d0*+Qm$vwL&%R+wI#p^ zh0l8H^-OXM%K5ssi5A%znQv_5NVL?#PxB;f1~i^ic^>>zu8CquS^*5OzNZdr3_Prw zb!IT^_jX55ybHcz)oO;g%ffHiRPf>SoiWTLBS#rFVX$pRk$cqPDOEW}{-|w&3FXcq z;w2Pw+yzJ2EBu&sb}l(MYR8WeQ6|)IPDQ+6@B;jz5oC*Xt5t&k(S_h~;|d?n^Tzw0 zUla=QkRO<#YGC!k&qb5i_b>HE1DDB@j2-j!68>nF-#pLJ?-XN%syVOaTKV;3$m=h3 zBw=^O3Uc5Xy^-`0Kb@T-)xV)uNzd_1D_;d8 z`QEwB4|6j3T{3LkvOgq02Q$|z6RopOHc(WIA+l!S$E`?`98}S?IP;k&amx><7ar_i zb%hE=I9>F|E4}0|s3T+t<_4$hFB|Fwazs4fh5R5%ei+a6i5su#b#I$IRczvIyr19a zdjH4?r!J3I!Ii#TD?i}IA_fYvasNg~ndiYhOSnc5FoB5_%R10_*)o^#He&Ji+ z3Jq8e~ty z+ZLL=ozD^*bItjw@1m;2sbm@LHL#OBCSoB){-8 zT_fd(m*?Z*bDaZzj2GqS{?<9n&do|ZpD@S|we3DXo)ic&Vco$Gt7TihU#eEplQRc? z_&#LbBE~_8EaEE%T>K9POxD1-l2!jL;P;PvsnOu)Ub$C${N9M1xD#dpUNT0OfRh$L z$+?G!6hA=~Dj7VVuA$f!aszre-Iu7n)X|O~cPY5>8o9g^V%yp5Lmn%?MzUIhHUU8% zHn8{j>0DyZcqq|`sxvFI0%8K-5(a#W{dhV40HBs9lH*gE9x5-Lis)nJ0oJVN?NAGQRI^XJ@u1 z8F4iGc|KOh|)YhT;|{1{`A{U7Cry5kRsMompx_&FJY-?4uL zpOph0VfDB2he(ylk3RiDJ6-A