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 +21 -0
- cm_pixel-0.1.0/PKG-INFO +99 -0
- cm_pixel-0.1.0/README.md +78 -0
- cm_pixel-0.1.0/cm_pixel.egg-info/PKG-INFO +99 -0
- cm_pixel-0.1.0/cm_pixel.egg-info/SOURCES.txt +19 -0
- cm_pixel-0.1.0/cm_pixel.egg-info/dependency_links.txt +1 -0
- cm_pixel-0.1.0/cm_pixel.egg-info/entry_points.txt +2 -0
- cm_pixel-0.1.0/cm_pixel.egg-info/requires.txt +3 -0
- cm_pixel-0.1.0/cm_pixel.egg-info/top_level.txt +1 -0
- cm_pixel-0.1.0/cmpixel/__init__.py +11 -0
- cm_pixel-0.1.0/cmpixel/animations.py +337 -0
- cm_pixel-0.1.0/cmpixel/canvas.py +86 -0
- cm_pixel-0.1.0/cmpixel/cli.py +106 -0
- cm_pixel-0.1.0/cmpixel/device.py +129 -0
- cm_pixel-0.1.0/cmpixel/font.py +132 -0
- cm_pixel-0.1.0/cmpixel/layout.json +1 -0
- cm_pixel-0.1.0/cmpixel/media.py +61 -0
- cm_pixel-0.1.0/cmpixel/rubik.py +177 -0
- cm_pixel-0.1.0/pyproject.toml +37 -0
- cm_pixel-0.1.0/setup.cfg +4 -0
- cm_pixel-0.1.0/tests/test_smoke.py +63 -0
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.
|
cm_pixel-0.1.0/PKG-INFO
ADDED
|
@@ -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).
|
cm_pixel-0.1.0/README.md
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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
|
+
}
|