cm-pixel 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cm_pixel-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Feiko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: cm-pixel
3
+ Version: 0.1.0
4
+ Summary: Open-source driver for the Cooler Master MasterLiquid Atmos II Pixel LED hexagonal screen
5
+ Author-email: Feiko Wielsma <feiko.w@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/FeikoWielsma/cm-pixel
8
+ Project-URL: Source, https://github.com/FeikoWielsma/cm-pixel
9
+ Project-URL: Issues, https://github.com/FeikoWielsma/cm-pixel/issues
10
+ Keywords: cooler-master,atmos,pixel,led,hid,aio
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: System :: Hardware
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: hidapi>=0.14
18
+ Requires-Dist: numpy>=1.21
19
+ Requires-Dist: Pillow>=9
20
+ Dynamic: license-file
21
+
22
+ # cm-pixel
23
+
24
+ Open-source driver + toolkit for the **Cooler Master MasterLiquid Atmos II Pixel LED**
25
+ hexagonal pixel screen — a clean, lightweight alternative to the official MasterCTRL app
26
+ for controlling the LEDs over USB. **Screen only** (pump/fans are PWM and untouched).
27
+
28
+ The USB protocol was reverse-engineered from scratch; see [`PROTOCOL.md`](PROTOCOL.md).
29
+
30
+ ## Hardware
31
+ - Cooler Master "Atmos V2 - Pixel", USB `2516:021C`, plain HID (no special driver).
32
+ - 32×32 addressing grid, **556 real LEDs** arranged in a hexagon, RGB888.
33
+
34
+ ## Install
35
+
36
+ This project uses [uv](https://docs.astral.sh/uv/). From a clone:
37
+ ```bash
38
+ uv sync # creates .venv and installs everything (incl. uv.lock)
39
+ uv run cm-pixel list # run the CLI
40
+ ```
41
+ Or add it to your own uv project:
42
+ ```bash
43
+ uv add cm-pixel
44
+ ```
45
+ (Plain pip also works: `pip install cm-pixel`.)
46
+
47
+ > Close the official MasterCTRL software first — only one program can drive the screen.
48
+ > On Linux you may need a udev rule for HID write access.
49
+
50
+ ## CLI
51
+ ```bash
52
+ cm-pixel color 255 0 0 # solid red
53
+ cm-pixel image photo.png # show an image (auto-fit to the hexagon)
54
+ cm-pixel gif spinner.gif # play a GIF
55
+ cm-pixel text "HI" # static centered text
56
+ cm-pixel scroll "HELLO WORLD" # scrolling marquee
57
+ cm-pixel anim plasma # procedural animation
58
+ cm-pixel anim fire --speed 1.5
59
+ cm-pixel list # list animations
60
+ ```
61
+ Common flags: `--fps`, `--duration`, `--mask` (blank pixels outside the hexagon), `--keep`,
62
+ `--brightness 0-100`, `--rotate 0|90|180|270`. Brightness and rotation are applied in software
63
+ (the device has no command for them); 180° is exact, 90/270 clip slightly at the hexagon vertices.
64
+
65
+ Animations: `plasma rainbow swirl ripple breathe sparkle fire starfield`.
66
+
67
+ ## Library
68
+ ```python
69
+ from cmpixel import PixelDisplay, Canvas
70
+ from cmpixel.font import draw_text
71
+
72
+ with PixelDisplay() as d:
73
+ c = Canvas().fill((0, 0, 20))
74
+ c.set_pixel(16, 16, (255, 255, 255))
75
+ draw_text(c, "GO", color=(0, 255, 0))
76
+ d.send_canvas(c)
77
+ ```
78
+
79
+ ```python
80
+ # play a built-in animation
81
+ from cmpixel import PixelDisplay
82
+ from cmpixel.animations import plasma
83
+ from cmpixel.media import play
84
+
85
+ with PixelDisplay() as d:
86
+ play(d, plasma(speed=1.0), fps=30, duration=10)
87
+ ```
88
+
89
+ ## How it works (short version)
90
+ A frame is 28 HID output reports of 64 bytes: `80 DD <pkt:u16> + 60 data`, concatenated to
91
+ 556×RGB in serpentine LED order. Streaming a frame displays it immediately — no handshake.
92
+ Full details and the LED layout map are in [`PROTOCOL.md`](PROTOCOL.md) and `cmpixel/layout.json`.
93
+
94
+ ## Status
95
+ v0.1 — drawing, text, GIF, procedural animations, software brightness & rotation.
96
+
97
+ ## Disclaimer
98
+ Unofficial, not affiliated with Cooler Master. Use at your own risk; touches only the
99
+ screen framebuffer (no firmware/DFU).
@@ -0,0 +1,78 @@
1
+ # cm-pixel
2
+
3
+ Open-source driver + toolkit for the **Cooler Master MasterLiquid Atmos II Pixel LED**
4
+ hexagonal pixel screen — a clean, lightweight alternative to the official MasterCTRL app
5
+ for controlling the LEDs over USB. **Screen only** (pump/fans are PWM and untouched).
6
+
7
+ The USB protocol was reverse-engineered from scratch; see [`PROTOCOL.md`](PROTOCOL.md).
8
+
9
+ ## Hardware
10
+ - Cooler Master "Atmos V2 - Pixel", USB `2516:021C`, plain HID (no special driver).
11
+ - 32×32 addressing grid, **556 real LEDs** arranged in a hexagon, RGB888.
12
+
13
+ ## Install
14
+
15
+ This project uses [uv](https://docs.astral.sh/uv/). From a clone:
16
+ ```bash
17
+ uv sync # creates .venv and installs everything (incl. uv.lock)
18
+ uv run cm-pixel list # run the CLI
19
+ ```
20
+ Or add it to your own uv project:
21
+ ```bash
22
+ uv add cm-pixel
23
+ ```
24
+ (Plain pip also works: `pip install cm-pixel`.)
25
+
26
+ > Close the official MasterCTRL software first — only one program can drive the screen.
27
+ > On Linux you may need a udev rule for HID write access.
28
+
29
+ ## CLI
30
+ ```bash
31
+ cm-pixel color 255 0 0 # solid red
32
+ cm-pixel image photo.png # show an image (auto-fit to the hexagon)
33
+ cm-pixel gif spinner.gif # play a GIF
34
+ cm-pixel text "HI" # static centered text
35
+ cm-pixel scroll "HELLO WORLD" # scrolling marquee
36
+ cm-pixel anim plasma # procedural animation
37
+ cm-pixel anim fire --speed 1.5
38
+ cm-pixel list # list animations
39
+ ```
40
+ Common flags: `--fps`, `--duration`, `--mask` (blank pixels outside the hexagon), `--keep`,
41
+ `--brightness 0-100`, `--rotate 0|90|180|270`. Brightness and rotation are applied in software
42
+ (the device has no command for them); 180° is exact, 90/270 clip slightly at the hexagon vertices.
43
+
44
+ Animations: `plasma rainbow swirl ripple breathe sparkle fire starfield`.
45
+
46
+ ## Library
47
+ ```python
48
+ from cmpixel import PixelDisplay, Canvas
49
+ from cmpixel.font import draw_text
50
+
51
+ with PixelDisplay() as d:
52
+ c = Canvas().fill((0, 0, 20))
53
+ c.set_pixel(16, 16, (255, 255, 255))
54
+ draw_text(c, "GO", color=(0, 255, 0))
55
+ d.send_canvas(c)
56
+ ```
57
+
58
+ ```python
59
+ # play a built-in animation
60
+ from cmpixel import PixelDisplay
61
+ from cmpixel.animations import plasma
62
+ from cmpixel.media import play
63
+
64
+ with PixelDisplay() as d:
65
+ play(d, plasma(speed=1.0), fps=30, duration=10)
66
+ ```
67
+
68
+ ## How it works (short version)
69
+ A frame is 28 HID output reports of 64 bytes: `80 DD <pkt:u16> + 60 data`, concatenated to
70
+ 556×RGB in serpentine LED order. Streaming a frame displays it immediately — no handshake.
71
+ Full details and the LED layout map are in [`PROTOCOL.md`](PROTOCOL.md) and `cmpixel/layout.json`.
72
+
73
+ ## Status
74
+ v0.1 — drawing, text, GIF, procedural animations, software brightness & rotation.
75
+
76
+ ## Disclaimer
77
+ Unofficial, not affiliated with Cooler Master. Use at your own risk; touches only the
78
+ screen framebuffer (no firmware/DFU).
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: cm-pixel
3
+ Version: 0.1.0
4
+ Summary: Open-source driver for the Cooler Master MasterLiquid Atmos II Pixel LED hexagonal screen
5
+ Author-email: Feiko Wielsma <feiko.w@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/FeikoWielsma/cm-pixel
8
+ Project-URL: Source, https://github.com/FeikoWielsma/cm-pixel
9
+ Project-URL: Issues, https://github.com/FeikoWielsma/cm-pixel/issues
10
+ Keywords: cooler-master,atmos,pixel,led,hid,aio
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: System :: Hardware
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: hidapi>=0.14
18
+ Requires-Dist: numpy>=1.21
19
+ Requires-Dist: Pillow>=9
20
+ Dynamic: license-file
21
+
22
+ # cm-pixel
23
+
24
+ Open-source driver + toolkit for the **Cooler Master MasterLiquid Atmos II Pixel LED**
25
+ hexagonal pixel screen — a clean, lightweight alternative to the official MasterCTRL app
26
+ for controlling the LEDs over USB. **Screen only** (pump/fans are PWM and untouched).
27
+
28
+ The USB protocol was reverse-engineered from scratch; see [`PROTOCOL.md`](PROTOCOL.md).
29
+
30
+ ## Hardware
31
+ - Cooler Master "Atmos V2 - Pixel", USB `2516:021C`, plain HID (no special driver).
32
+ - 32×32 addressing grid, **556 real LEDs** arranged in a hexagon, RGB888.
33
+
34
+ ## Install
35
+
36
+ This project uses [uv](https://docs.astral.sh/uv/). From a clone:
37
+ ```bash
38
+ uv sync # creates .venv and installs everything (incl. uv.lock)
39
+ uv run cm-pixel list # run the CLI
40
+ ```
41
+ Or add it to your own uv project:
42
+ ```bash
43
+ uv add cm-pixel
44
+ ```
45
+ (Plain pip also works: `pip install cm-pixel`.)
46
+
47
+ > Close the official MasterCTRL software first — only one program can drive the screen.
48
+ > On Linux you may need a udev rule for HID write access.
49
+
50
+ ## CLI
51
+ ```bash
52
+ cm-pixel color 255 0 0 # solid red
53
+ cm-pixel image photo.png # show an image (auto-fit to the hexagon)
54
+ cm-pixel gif spinner.gif # play a GIF
55
+ cm-pixel text "HI" # static centered text
56
+ cm-pixel scroll "HELLO WORLD" # scrolling marquee
57
+ cm-pixel anim plasma # procedural animation
58
+ cm-pixel anim fire --speed 1.5
59
+ cm-pixel list # list animations
60
+ ```
61
+ Common flags: `--fps`, `--duration`, `--mask` (blank pixels outside the hexagon), `--keep`,
62
+ `--brightness 0-100`, `--rotate 0|90|180|270`. Brightness and rotation are applied in software
63
+ (the device has no command for them); 180° is exact, 90/270 clip slightly at the hexagon vertices.
64
+
65
+ Animations: `plasma rainbow swirl ripple breathe sparkle fire starfield`.
66
+
67
+ ## Library
68
+ ```python
69
+ from cmpixel import PixelDisplay, Canvas
70
+ from cmpixel.font import draw_text
71
+
72
+ with PixelDisplay() as d:
73
+ c = Canvas().fill((0, 0, 20))
74
+ c.set_pixel(16, 16, (255, 255, 255))
75
+ draw_text(c, "GO", color=(0, 255, 0))
76
+ d.send_canvas(c)
77
+ ```
78
+
79
+ ```python
80
+ # play a built-in animation
81
+ from cmpixel import PixelDisplay
82
+ from cmpixel.animations import plasma
83
+ from cmpixel.media import play
84
+
85
+ with PixelDisplay() as d:
86
+ play(d, plasma(speed=1.0), fps=30, duration=10)
87
+ ```
88
+
89
+ ## How it works (short version)
90
+ A frame is 28 HID output reports of 64 bytes: `80 DD <pkt:u16> + 60 data`, concatenated to
91
+ 556×RGB in serpentine LED order. Streaming a frame displays it immediately — no handshake.
92
+ Full details and the LED layout map are in [`PROTOCOL.md`](PROTOCOL.md) and `cmpixel/layout.json`.
93
+
94
+ ## Status
95
+ v0.1 — drawing, text, GIF, procedural animations, software brightness & rotation.
96
+
97
+ ## Disclaimer
98
+ Unofficial, not affiliated with Cooler Master. Use at your own risk; touches only the
99
+ screen framebuffer (no firmware/DFU).
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ cm_pixel.egg-info/PKG-INFO
5
+ cm_pixel.egg-info/SOURCES.txt
6
+ cm_pixel.egg-info/dependency_links.txt
7
+ cm_pixel.egg-info/entry_points.txt
8
+ cm_pixel.egg-info/requires.txt
9
+ cm_pixel.egg-info/top_level.txt
10
+ cmpixel/__init__.py
11
+ cmpixel/animations.py
12
+ cmpixel/canvas.py
13
+ cmpixel/cli.py
14
+ cmpixel/device.py
15
+ cmpixel/font.py
16
+ cmpixel/layout.json
17
+ cmpixel/media.py
18
+ cmpixel/rubik.py
19
+ tests/test_smoke.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cm-pixel = cmpixel.cli:main
@@ -0,0 +1,3 @@
1
+ hidapi>=0.14
2
+ numpy>=1.21
3
+ Pillow>=9
@@ -0,0 +1 @@
1
+ cmpixel
@@ -0,0 +1,11 @@
1
+ """cmpixel — open-source driver for the Cooler Master Atmos V2 'Pixel' screen."""
2
+ from .device import PixelDisplay, VID, PID, GRID_W, GRID_H, NLED
3
+ from .canvas import Canvas, MASK
4
+ from . import animations, font, media
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "PixelDisplay", "Canvas", "MASK",
9
+ "VID", "PID", "GRID_W", "GRID_H", "NLED",
10
+ "animations", "font", "media",
11
+ ]
@@ -0,0 +1,337 @@
1
+ """Procedural animations. Each is a generator yielding (32,32,3) uint8 frames.
2
+
3
+ All take a `speed` multiplier; most accept extra knobs. They run forever.
4
+ Frames are full-grid; the hexagon mask is applied by the player/device anyway.
5
+ """
6
+ import numpy as np
7
+
8
+ from .canvas import GRID_W, GRID_H, MASK
9
+ from .rubik import rubik
10
+
11
+ _Y, _X = np.mgrid[0:GRID_H, 0:GRID_W].astype(np.float32)
12
+ _CX, _CY = (GRID_W - 1) / 2.0, (GRID_H - 1) / 2.0
13
+ _DX, _DY = _X - _CX, _Y - _CY
14
+ _R = np.sqrt(_DX**2 + _DY**2)
15
+ _ANG = np.arctan2(_DY, _DX)
16
+
17
+
18
+ def hsv_to_rgb(h, s, v):
19
+ """h,s,v in [0,1] arrays -> (H,W,3) uint8."""
20
+ h = (h % 1.0) * 6.0
21
+ i = np.floor(h).astype(int)
22
+ f = h - i
23
+ p = v * (1 - s)
24
+ q = v * (1 - f * s)
25
+ t = v * (1 - (1 - f) * s)
26
+ i = i % 6
27
+ r = np.choose(i, [v, q, p, p, t, v])
28
+ g = np.choose(i, [t, v, v, q, p, p])
29
+ b = np.choose(i, [p, p, t, v, v, q])
30
+ return (np.stack([r, g, b], axis=-1).clip(0, 1) * 255).astype(np.uint8)
31
+
32
+
33
+ def plasma(speed=1.0):
34
+ t = 0.0
35
+ while True:
36
+ v = (np.sin(_X / 4.0 + t)
37
+ + np.sin(_Y / 3.0 - t)
38
+ + np.sin((_X + _Y) / 4.0 + t)
39
+ + np.sin(_R / 3.0 - t))
40
+ h = (v + 4) / 8.0
41
+ yield hsv_to_rgb(h, np.ones_like(h), np.ones_like(h))
42
+ t += 0.15 * speed
43
+
44
+
45
+ def rainbow(speed=1.0, vertical=False):
46
+ t = 0.0
47
+ base = _Y if vertical else _X
48
+ while True:
49
+ h = base / GRID_W + t
50
+ yield hsv_to_rgb(h, np.ones_like(h), np.ones_like(h))
51
+ t += 0.02 * speed
52
+
53
+
54
+ def swirl(speed=1.0):
55
+ t = 0.0
56
+ while True:
57
+ h = (_ANG / (2 * np.pi)) + _R / 16.0 + t
58
+ yield hsv_to_rgb(h, np.ones_like(h), np.ones_like(h))
59
+ t += 0.05 * speed
60
+
61
+
62
+ def ripple(speed=1.0, color=None):
63
+ t = 0.0
64
+ while True:
65
+ wave = (np.sin(_R - t) + 1) / 2.0
66
+ if color is None:
67
+ out = hsv_to_rgb((_R / 16.0 - t / 6.0), np.ones_like(wave), wave)
68
+ else:
69
+ out = (np.array(color, np.float32) * wave[..., None]).astype(np.uint8)
70
+ yield out
71
+ t += 0.3 * speed
72
+
73
+
74
+ def breathe(color=(0, 120, 255), speed=1.0):
75
+ t = 0.0
76
+ col = np.array(color, np.float32)
77
+ while True:
78
+ v = (np.sin(t) + 1) / 2.0
79
+ frame = np.empty((GRID_H, GRID_W, 3), np.uint8)
80
+ frame[:] = (col * v).astype(np.uint8)
81
+ yield frame
82
+ t += 0.08 * speed
83
+
84
+
85
+ def sparkle(color=(255, 255, 255), speed=1.0, fade=0.85, rate=6):
86
+ buf = np.zeros((GRID_H, GRID_W, 3), np.float32)
87
+ leds = np.argwhere(MASK)
88
+ while True:
89
+ buf *= fade
90
+ for _ in range(int(rate * speed)):
91
+ y, x = leds[np.random.randint(len(leds))]
92
+ buf[y, x] = color
93
+ yield buf.clip(0, 255).astype(np.uint8)
94
+
95
+
96
+ def fire(speed=1.0):
97
+ heat = np.zeros((GRID_H + 2, GRID_W), np.float32)
98
+ # fire palette: black -> red -> orange -> yellow -> white
99
+ stops = np.array([[0, 0, 0], [80, 0, 0], [220, 50, 0],
100
+ [255, 160, 0], [255, 255, 120]], np.float32)
101
+ xp = np.linspace(0, 1, len(stops))
102
+ grad = np.stack([np.interp(np.linspace(0, 1, 256), xp, stops[:, c]) for c in range(3)], axis=-1)
103
+ while True:
104
+ heat[-1] = np.random.rand(GRID_W) * 255 # spark row at bottom
105
+ # cool + rise
106
+ nh = (heat[1:] * 0.5 + heat[:-1] * 0.0)
107
+ rolled = (np.roll(heat, -1, axis=0) * 0.42 + heat * 0.30
108
+ + np.roll(heat, (-1, 1), (0, 1)) * 0.14
109
+ + np.roll(heat, (-1, -1), (0, 1)) * 0.14)
110
+ heat[:] = rolled.clip(0, 255)
111
+ idx = heat[:GRID_H].astype(int).clip(0, 255)
112
+ yield grad[idx].astype(np.uint8)
113
+ if speed != 1.0:
114
+ pass
115
+
116
+
117
+ def starfield(speed=1.0, density=0.04):
118
+ stars = [] # (x, y, z)
119
+ while True:
120
+ if np.random.rand() < density * 8:
121
+ stars.append([np.random.uniform(-_CX, _CX), np.random.uniform(-_CY, _CY), 16.0])
122
+ frame = np.zeros((GRID_H, GRID_W, 3), np.uint8)
123
+ alive = []
124
+ for s in stars:
125
+ s[2] -= 0.4 * speed
126
+ if s[2] <= 1:
127
+ continue
128
+ px = int(_CX + s[0] * 8 / s[2])
129
+ py = int(_CY + s[1] * 8 / s[2])
130
+ if 0 <= px < GRID_W and 0 <= py < GRID_H:
131
+ b = int(255 * (1 - s[2] / 16))
132
+ frame[py, px] = (b, b, b)
133
+ alive.append(s)
134
+ stars[:] = alive[-200:]
135
+ yield frame
136
+
137
+
138
+ def _solid(hue, sat, val):
139
+ """hsv_to_rgb for scalar h,s,v -> (3,) float array."""
140
+ return hsv_to_rgb(np.array([[hue]]), np.array([[sat]]), np.array([[val]]))[0, 0].astype(float)
141
+
142
+
143
+ def tunnel(speed=1.0):
144
+ """Hypnotic radial tunnel rushing inward."""
145
+ t = 0.0
146
+ v = 1.0 / (_R + 0.6)
147
+ u = _ANG / (2 * np.pi)
148
+ vig = 1 - np.clip(_R / (GRID_W * 0.6), 0, 1)
149
+ while True:
150
+ stripes = 0.5 + 0.5 * np.sin(v * 18 - t * 4)
151
+ hue = (v * 1.5 + u + t * 0.05) % 1.0
152
+ val = stripes * (0.3 + 0.7 * vig)
153
+ yield hsv_to_rgb(hue, np.ones_like(hue), val)
154
+ t += 0.05 * speed
155
+
156
+
157
+ def metaballs(speed=1.0, n=4):
158
+ """Smooth gooey blobs drifting and merging."""
159
+ t = 0.0
160
+ ph = np.random.rand(n, 2) * 2 * np.pi
161
+ while True:
162
+ field = np.zeros((GRID_H, GRID_W), np.float32)
163
+ for i in range(n):
164
+ cx = GRID_W / 2 + GRID_W * 0.34 * np.sin(t * 0.6 + ph[i, 0])
165
+ cy = GRID_H / 2 + GRID_H * 0.34 * np.cos(t * 0.5 + ph[i, 1])
166
+ field += 30.0 / ((_X - cx) ** 2 + (_Y - cy) ** 2 + 8)
167
+ hue = (field * 0.3 + t * 0.05) % 1.0
168
+ val = np.clip(field * 0.9, 0, 1)
169
+ yield hsv_to_rgb(hue, np.ones_like(hue), val)
170
+ t += 0.05 * speed
171
+
172
+
173
+ def matrix(speed=1.0, fade=0.78):
174
+ """Falling green 'digital rain' columns."""
175
+ buf = np.zeros((GRID_H, GRID_W, 3), np.float32)
176
+ heads = np.zeros(GRID_W)
177
+ spd = 0.3 + np.random.rand(GRID_W) * 0.7
178
+ active = np.random.rand(GRID_W) < 0.4
179
+ while True:
180
+ buf *= fade
181
+ for x in range(GRID_W):
182
+ if not active[x]:
183
+ if np.random.rand() < 0.03:
184
+ active[x] = True
185
+ heads[x] = 0
186
+ continue
187
+ y = int(heads[x])
188
+ if 0 <= y < GRID_H:
189
+ buf[y, x] = (170, 255, 170)
190
+ heads[x] += spd[x] * speed
191
+ if heads[x] > GRID_H + 4:
192
+ active[x] = False
193
+ yield buf.clip(0, 255).astype(np.uint8)
194
+
195
+
196
+ def fireworks(speed=1.0, gravity=0.05):
197
+ """Launch bursts that explode and fall."""
198
+ parts = []
199
+ buf = np.zeros((GRID_H, GRID_W, 3), np.float32)
200
+ while True:
201
+ buf *= 0.82
202
+ if np.random.rand() < 0.06 * speed or not parts:
203
+ cx, cy = np.random.uniform(8, 24), np.random.uniform(6, 18)
204
+ col = _solid(np.random.rand(), 1.0, 1.0)
205
+ n = np.random.randint(18, 30)
206
+ for k in range(n):
207
+ a = 2 * np.pi * k / n
208
+ sp = np.random.uniform(0.4, 1.2)
209
+ parts.append([cx, cy, np.cos(a) * sp, np.sin(a) * sp, 1.0, col])
210
+ alive = []
211
+ for p in parts:
212
+ p[0] += p[2] * speed
213
+ p[1] += p[3] * speed
214
+ p[3] += gravity * speed
215
+ p[4] -= 0.02 * speed
216
+ if p[4] <= 0:
217
+ continue
218
+ xi, yi = int(round(p[0])), int(round(p[1]))
219
+ if 0 <= xi < GRID_W and 0 <= yi < GRID_H:
220
+ buf[yi, xi] = np.array(p[5]) * p[4]
221
+ alive.append(p)
222
+ parts[:] = alive
223
+ yield buf.clip(0, 255).astype(np.uint8)
224
+
225
+
226
+ def aurora(speed=1.0):
227
+ """Northern-lights curtains, green through purple."""
228
+ t = 0.0
229
+ while True:
230
+ center = GRID_H * 0.5 + 6 * np.sin(_X * 0.3 + t) + 3 * np.sin(_X * 0.7 - t * 1.3)
231
+ val = np.clip(np.exp(-((_Y - center) ** 2) / 18.0), 0, 1)
232
+ hue = (0.33 + 0.18 * np.sin(_X * 0.2 + t * 0.5) + _Y * 0.012) % 1.0
233
+ yield hsv_to_rgb(hue, np.full_like(hue, 0.85), val)
234
+ t += 0.06 * speed
235
+
236
+
237
+ def comet(speed=1.0):
238
+ """A color-cycling comet orbiting with a fading tail."""
239
+ buf = np.zeros((GRID_H, GRID_W, 3), np.float32)
240
+ t = 0.0
241
+ while True:
242
+ buf *= 0.80
243
+ r = GRID_W * 0.32
244
+ x = GRID_W / 2 + r * np.cos(t)
245
+ y = GRID_H / 2 + r * np.sin(t)
246
+ xi, yi = int(round(x)), int(round(y))
247
+ if 0 <= xi < GRID_W and 0 <= yi < GRID_H:
248
+ buf[yi, xi] = _solid((t * 0.1) % 1, 1.0, 1.0)
249
+ yield buf.clip(0, 255).astype(np.uint8)
250
+ t += 0.15 * speed
251
+
252
+
253
+ def pinwheel(speed=1.0, arms=5):
254
+ """Rotating rainbow pinwheel."""
255
+ t = 0.0
256
+ while True:
257
+ val = 0.5 + 0.5 * np.sin(_ANG * arms + _R * 0.5 - t * 3)
258
+ hue = (_ANG / (2 * np.pi) + t * 0.1) % 1.0
259
+ yield hsv_to_rgb(hue, np.ones_like(hue), val ** 1.5)
260
+ t += 0.05 * speed
261
+
262
+
263
+ def bounce(speed=1.0):
264
+ """A bouncing ball with a motion trail."""
265
+ buf = np.zeros((GRID_H, GRID_W, 3), np.float32)
266
+ pos = np.array([16.0, 16.0])
267
+ vel = np.array([0.7, 0.5])
268
+ lo, hi = 4, 27
269
+ t = 0.0
270
+ while True:
271
+ buf *= 0.75
272
+ pos += vel * speed
273
+ for k in range(2):
274
+ if pos[k] < lo:
275
+ pos[k], vel[k] = lo, -vel[k]
276
+ if pos[k] > hi:
277
+ pos[k], vel[k] = hi, -vel[k]
278
+ buf[int(round(pos[1])), int(round(pos[0]))] = _solid((t * 0.02) % 1, 1.0, 1.0)
279
+ yield buf.clip(0, 255).astype(np.uint8)
280
+ t += speed
281
+
282
+
283
+ def interference(speed=1.0):
284
+ """Two moving wave sources rippling and interfering."""
285
+ t = 0.0
286
+ while True:
287
+ c1x, c1y = GRID_W / 2 + 10 * np.sin(t * 0.7), GRID_H / 2 + 10 * np.cos(t * 0.5)
288
+ c2x, c2y = GRID_W / 2 + 10 * np.sin(t * 0.4 + 2), GRID_H / 2 + 10 * np.cos(t * 0.6 + 1)
289
+ d1 = np.sqrt((_X - c1x) ** 2 + (_Y - c1y) ** 2)
290
+ d2 = np.sqrt((_X - c2x) ** 2 + (_Y - c2y) ** 2)
291
+ val = (np.sin(d1 - t * 3) + np.sin(d2 - t * 3)) / 2
292
+ hue = (val * 0.5 + 0.5 + t * 0.03) % 1.0
293
+ yield hsv_to_rgb(hue, np.ones_like(hue), val * 0.5 + 0.5)
294
+ t += 0.05 * speed
295
+
296
+
297
+ def twinkle(speed=1.0, n=45):
298
+ """Colorful stars fading in and out."""
299
+ leds = np.argwhere(MASK)
300
+ stars = []
301
+ while True:
302
+ if len(stars) < n and np.random.rand() < 0.6:
303
+ y, x = leds[np.random.randint(len(leds))]
304
+ stars.append([int(x), int(y), np.random.rand(), 0.0, 0.03 + np.random.rand() * 0.06])
305
+ frame = np.zeros((GRID_H, GRID_W, 3), np.uint8)
306
+ alive = []
307
+ for s in stars:
308
+ s[3] += s[4] * speed
309
+ if s[3] >= np.pi:
310
+ continue
311
+ frame[s[1], s[0]] = _solid(s[2], 0.6, np.sin(s[3]))
312
+ alive.append(s)
313
+ stars[:] = alive
314
+ yield frame
315
+
316
+
317
+ ANIMATIONS = {
318
+ "plasma": plasma,
319
+ "rainbow": rainbow,
320
+ "swirl": swirl,
321
+ "ripple": ripple,
322
+ "breathe": breathe,
323
+ "sparkle": sparkle,
324
+ "fire": fire,
325
+ "starfield": starfield,
326
+ "tunnel": tunnel,
327
+ "metaballs": metaballs,
328
+ "matrix": matrix,
329
+ "fireworks": fireworks,
330
+ "aurora": aurora,
331
+ "comet": comet,
332
+ "pinwheel": pinwheel,
333
+ "bounce": bounce,
334
+ "interference": interference,
335
+ "twinkle": twinkle,
336
+ "rubik": rubik,
337
+ }