scrollkit 0.8.4__tar.gz → 0.9.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.
- {scrollkit-0.8.4/src/scrollkit.egg-info → scrollkit-0.9.0}/PKG-INFO +15 -5
- {scrollkit-0.8.4 → scrollkit-0.9.0}/README.md +15 -5
- {scrollkit-0.8.4 → scrollkit-0.9.0}/pyproject.toml +1 -1
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/__init__.py +1 -1
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/dev/capabilities.py +32 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/_graphics.py +26 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/__init__.py +8 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/image_animators.py +134 -10
- scrollkit-0.9.0/src/scrollkit/effects/palette_partition.py +294 -0
- scrollkit-0.9.0/src/scrollkit/effects/palette_treatments.py +705 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/swarm_reveal.py +50 -3
- scrollkit-0.9.0/src/scrollkit/effects/swirl_in.py +108 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/network/http_client.py +34 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/network/wifi_manager.py +14 -6
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/ota/client.py +103 -17
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/ota/display_progress.py +6 -0
- scrollkit-0.9.0/src/scrollkit/utils/scheduler.py +71 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/web/wifi_setup.py +2 -1
- {scrollkit-0.8.4 → scrollkit-0.9.0/src/scrollkit.egg-info}/PKG-INFO +15 -5
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit.egg-info/SOURCES.txt +4 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/LICENSE +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/setup.cfg +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/app/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/app/base.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/app/memory.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/config/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/config/settings_manager.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/config/transition_names.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/dev/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/dev/harness.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/dev/metrics.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/dev/performance.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/dev/validation.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/_recording.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/_sim_backend.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/bitmap_text.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/boards.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/colors.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/content.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/gradient_text.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/interface.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/simulator.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/text_fill.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/text_pixels.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/display/unified.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/drip_splash.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/easing.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/overlay.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/particles.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/reveal_splash.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/scrolling.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/text_render.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/effects/transitions.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/exceptions.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/network/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/network/mdns.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/ota/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/ota/manifest.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/ota/publish.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/ATTRIBUTION.md +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/LICENSE +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/README.md +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_display_text/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_display_text/label.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/bitmaptools.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/color_utils.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/device_benchmarks.json +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/feasibility.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/hardware_profile.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/led_matrix.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/matrixportal_s3_baseline.json +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/performance_manager.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/core/pixel_buffer.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/base_device.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/matrixportal_s3.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/bitmap.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/display.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/fourwire.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/group.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/ondiskbitmap.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/palette.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/tilegrid.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/3x5.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_16.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_16.bdf.license +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Junction_regular_24.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Junction_regular_24.bdf.license +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/tom-thumb.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/viii-bold.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/viii.bdf +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/simulator/terminalio/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/utils/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/utils/color_utils.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/utils/diagnostics.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/utils/error_handler.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/utils/system_utils.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/utils/url_utils.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/web/__init__.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit/web/settings_server.py +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit.egg-info/dependency_links.txt +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit.egg-info/requires.txt +0 -0
- {scrollkit-0.8.4 → scrollkit-0.9.0}/src/scrollkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrollkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: LED Matrix Display Framework for CircuitPython and Desktop
|
|
5
5
|
Author-email: Michael Czeiszperger <michael@czei.org>
|
|
6
6
|
License: MIT
|
|
@@ -57,17 +57,28 @@ Most LED-matrix libraries get you a scrolling "Hello, World" and stop. I built S
|
|
|
57
57
|
|
|
58
58
|
## Installation
|
|
59
59
|
|
|
60
|
+
For a stable desktop install, use the version published on PyPI. A Git tag such
|
|
61
|
+
as `v0.8.4` is a released PyPI build; `master` is the development branch and can
|
|
62
|
+
contain unreleased work. Pin an exact version in a project that needs reproducible
|
|
63
|
+
builds.
|
|
64
|
+
|
|
60
65
|
```bash
|
|
61
|
-
#
|
|
66
|
+
# Stable desktop install with simulator support
|
|
62
67
|
pip install "scrollkit[simulator]"
|
|
68
|
+
# Or pin a tested release: pip install "scrollkit[simulator]==X.Y.Z"
|
|
63
69
|
|
|
64
|
-
# To
|
|
70
|
+
# To contribute, test unreleased work, or run the bundled demos: clone master
|
|
71
|
+
# and install it editable.
|
|
65
72
|
git clone https://github.com/czei/scrollkit.git
|
|
66
73
|
cd scrollkit && pip install -e ".[simulator]"
|
|
67
74
|
|
|
68
|
-
# CircuitPython —
|
|
75
|
+
# CircuitPython — deploy a tested copy (or matching .mpy build) of scrollkit/
|
|
76
|
+
# inside your app payload; a device should not track Git master directly.
|
|
69
77
|
```
|
|
70
78
|
|
|
79
|
+
The `release-*` branches and the optional `live` OTA branch are deployment
|
|
80
|
+
channels for application payloads, not alternate `pip` installation channels.
|
|
81
|
+
|
|
71
82
|
## Quick Start
|
|
72
83
|
|
|
73
84
|
```python
|
|
@@ -228,7 +239,6 @@ if is_dev_mode():
|
|
|
228
239
|
| Platform | Backend | Status |
|
|
229
240
|
|---|---|---|
|
|
230
241
|
| Adafruit MatrixPortal S3 | CircuitPython + displayio | ✅ Calibrated from device |
|
|
231
|
-
| Pimoroni Interstate 75 W (RP2350) | CircuitPython + rgbmatrix | ✅ Supported (perf profile uncalibrated) |
|
|
232
242
|
| Desktop (macOS/Linux/Windows) | SLDK Simulator | ✅ |
|
|
233
243
|
| Custom CircuitPython boards | displayio / rgbmatrix | 🔌 Extensible (see [Adding New Hardware](https://scrollkit.dev/guide/hardware/)) |
|
|
234
244
|
|
|
@@ -13,17 +13,28 @@ Most LED-matrix libraries get you a scrolling "Hello, World" and stop. I built S
|
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
+
For a stable desktop install, use the version published on PyPI. A Git tag such
|
|
17
|
+
as `v0.8.4` is a released PyPI build; `master` is the development branch and can
|
|
18
|
+
contain unreleased work. Pin an exact version in a project that needs reproducible
|
|
19
|
+
builds.
|
|
20
|
+
|
|
16
21
|
```bash
|
|
17
|
-
#
|
|
22
|
+
# Stable desktop install with simulator support
|
|
18
23
|
pip install "scrollkit[simulator]"
|
|
24
|
+
# Or pin a tested release: pip install "scrollkit[simulator]==X.Y.Z"
|
|
19
25
|
|
|
20
|
-
# To
|
|
26
|
+
# To contribute, test unreleased work, or run the bundled demos: clone master
|
|
27
|
+
# and install it editable.
|
|
21
28
|
git clone https://github.com/czei/scrollkit.git
|
|
22
29
|
cd scrollkit && pip install -e ".[simulator]"
|
|
23
30
|
|
|
24
|
-
# CircuitPython —
|
|
31
|
+
# CircuitPython — deploy a tested copy (or matching .mpy build) of scrollkit/
|
|
32
|
+
# inside your app payload; a device should not track Git master directly.
|
|
25
33
|
```
|
|
26
34
|
|
|
35
|
+
The `release-*` branches and the optional `live` OTA branch are deployment
|
|
36
|
+
channels for application payloads, not alternate `pip` installation channels.
|
|
37
|
+
|
|
27
38
|
## Quick Start
|
|
28
39
|
|
|
29
40
|
```python
|
|
@@ -184,7 +195,6 @@ if is_dev_mode():
|
|
|
184
195
|
| Platform | Backend | Status |
|
|
185
196
|
|---|---|---|
|
|
186
197
|
| Adafruit MatrixPortal S3 | CircuitPython + displayio | ✅ Calibrated from device |
|
|
187
|
-
| Pimoroni Interstate 75 W (RP2350) | CircuitPython + rgbmatrix | ✅ Supported (perf profile uncalibrated) |
|
|
188
198
|
| Desktop (macOS/Linux/Windows) | SLDK Simulator | ✅ |
|
|
189
199
|
| Custom CircuitPython boards | displayio / rgbmatrix | 🔌 Extensible (see [Adding New Hardware](https://scrollkit.dev/guide/hardware/)) |
|
|
190
200
|
|
|
@@ -208,4 +218,4 @@ of his code — but the idea came from his project, and it deserves the shout-ou
|
|
|
208
218
|
|
|
209
219
|
## License
|
|
210
220
|
|
|
211
|
-
MIT
|
|
221
|
+
MIT
|
|
@@ -146,6 +146,17 @@ def _effects():
|
|
|
146
146
|
out.append({"name": "SwarmReveal", "doc": _first_line(SwarmReveal)})
|
|
147
147
|
except ImportError:
|
|
148
148
|
pass
|
|
149
|
+
try:
|
|
150
|
+
from ..effects.palette_partition import PalettePartition
|
|
151
|
+
out.append({"name": "PalettePartition",
|
|
152
|
+
"doc": _first_line(PalettePartition)})
|
|
153
|
+
except ImportError:
|
|
154
|
+
pass
|
|
155
|
+
try:
|
|
156
|
+
from ..effects.swirl_in import SwirlIn
|
|
157
|
+
out.append({"name": "SwirlIn", "doc": _first_line(SwirlIn)})
|
|
158
|
+
except ImportError:
|
|
159
|
+
pass
|
|
149
160
|
return out
|
|
150
161
|
|
|
151
162
|
|
|
@@ -228,6 +239,26 @@ def _palette_effects():
|
|
|
228
239
|
return out
|
|
229
240
|
|
|
230
241
|
|
|
242
|
+
def _palette_treatments():
|
|
243
|
+
"""``[{name, doc, partition, feasibility}]`` for the partition dwell
|
|
244
|
+
treatments (see :mod:`scrollkit.effects.palette_treatments`).
|
|
245
|
+
|
|
246
|
+
Frame-driven classes animating a :class:`PalettePartition` purely with
|
|
247
|
+
palette writes; ``partition`` names the recommended partition builder.
|
|
248
|
+
Enumerated via the module's explicit ``TREATMENT_CLASSES`` catalog.
|
|
249
|
+
"""
|
|
250
|
+
out = []
|
|
251
|
+
try:
|
|
252
|
+
from ..effects.palette_treatments import TREATMENT_CLASSES
|
|
253
|
+
except ImportError:
|
|
254
|
+
return out
|
|
255
|
+
for cls in TREATMENT_CLASSES:
|
|
256
|
+
out.append({"name": cls.__name__, "doc": _first_line(cls),
|
|
257
|
+
"partition": cls.PARTITION,
|
|
258
|
+
"feasibility": getattr(cls, "FEASIBILITY", None)})
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
|
|
231
262
|
def _named_colors():
|
|
232
263
|
"""name -> 0xRRGGBB int, from the tooling color table (deduped)."""
|
|
233
264
|
from ..utils.color_utils import NAMED_COLORS
|
|
@@ -322,6 +353,7 @@ def capabilities():
|
|
|
322
353
|
for key, fn in (("content_types", _content_types), ("priorities", _priorities),
|
|
323
354
|
("effects", _effects), ("transitions", _transitions),
|
|
324
355
|
("scrolling", _scrolling), ("palette_effects", _palette_effects),
|
|
356
|
+
("palette_treatments", _palette_treatments),
|
|
325
357
|
("image_animators", _image_animators),
|
|
326
358
|
("text_fills", _text_fills), ("color_utilities", _color_utilities),
|
|
327
359
|
("named_colors", _named_colors),
|
|
@@ -120,6 +120,32 @@ class GraphicsMixin:
|
|
|
120
120
|
lg.pop(i)
|
|
121
121
|
return
|
|
122
122
|
|
|
123
|
+
def clear_layers(self):
|
|
124
|
+
"""Strip EVERY persistent layer — the takeover blank.
|
|
125
|
+
|
|
126
|
+
``clear()`` deliberately leaves ``_layer_group`` alone (per-frame label
|
|
127
|
+
reset must not disturb long-lived effect layers), so a full-screen
|
|
128
|
+
takeover message needs this instead: without it, an interrupted ride
|
|
129
|
+
screen's bitmap layers stay composited and the takeover text paints ON
|
|
130
|
+
TOP of them (the "Updating — DO NOT UNPLUG over the ride list" bug,
|
|
131
|
+
2026-07-12). Content objects whose layers vanish are unaffected later:
|
|
132
|
+
``remove_layer`` is idempotent.
|
|
133
|
+
|
|
134
|
+
The bounded painter's canvas lives in ``_layer_group`` too, so its refs
|
|
135
|
+
are reset here — ``_ensure_paint`` rebuilds a composited canvas on the
|
|
136
|
+
next ``set_pixel``/``fill_rect`` (stale refs would silently draw into a
|
|
137
|
+
bitmap no longer in the displayio tree).
|
|
138
|
+
"""
|
|
139
|
+
lg = getattr(self, "_layer_group", None)
|
|
140
|
+
if lg is None:
|
|
141
|
+
return
|
|
142
|
+
while len(lg):
|
|
143
|
+
lg.pop()
|
|
144
|
+
self._paint_bitmap = None
|
|
145
|
+
self._paint_palette = None
|
|
146
|
+
self._paint_tile = None
|
|
147
|
+
self._paint_colors = {}
|
|
148
|
+
|
|
123
149
|
# --- bounded painters -----------------------------------------------------
|
|
124
150
|
def _ensure_paint(self):
|
|
125
151
|
if self._paint_bitmap is not None:
|
|
@@ -22,6 +22,14 @@ into RAM. Import each submodule directly instead:
|
|
|
22
22
|
screen — start/step/detach, not the ``Transition`` contract) —
|
|
23
23
|
``from scrollkit.effects.image_animators import TwinkleAnimator, MotionAnimator,
|
|
24
24
|
ComboAnimator``
|
|
25
|
+
- **Palette partitions** (indexed layers animated purely by palette writes) —
|
|
26
|
+
``from scrollkit.effects.palette_partition import PalettePartition,
|
|
27
|
+
map_diagonal, map_radial, map_route``
|
|
28
|
+
- **Palette treatments** (dwell animations on a partition — the gallery) —
|
|
29
|
+
``from scrollkit.effects.palette_treatments import VelvetSweep, HaloPulse,
|
|
30
|
+
TREATMENT_CLASSES, treatments_for``
|
|
31
|
+
- **Swirl entrance** (sprites spiral in onto their targets) —
|
|
32
|
+
``from scrollkit.effects.swirl_in import SwirlIn``
|
|
25
33
|
- **Text-rendering helpers** —
|
|
26
34
|
``from scrollkit.effects.text_render import pixels_from_font_text, font_text_width``
|
|
27
35
|
|
|
@@ -579,7 +579,9 @@ class RegionRotateAnimator(IntroAnimator):
|
|
|
579
579
|
ci = bitmap[x, y]
|
|
580
580
|
if ci != 0 and not self._in_excl(x, y): # body pixels stay put
|
|
581
581
|
src[(x, y)] = ci
|
|
582
|
-
|
|
582
|
+
dx = x - self._px
|
|
583
|
+
dy = y - self._py
|
|
584
|
+
r = math.sqrt(dx * dx + dy * dy) # math.hypot is absent on CircuitPython
|
|
583
585
|
if r > maxr:
|
|
584
586
|
maxr = r
|
|
585
587
|
if not src or len(src) > 320:
|
|
@@ -598,16 +600,18 @@ class RegionRotateAnimator(IntroAnimator):
|
|
|
598
600
|
self._hidden = False
|
|
599
601
|
|
|
600
602
|
def _stamp(self, ang):
|
|
601
|
-
bmp = self.bitmap
|
|
602
|
-
for x, y in self._stamped: # erase only what we last drew
|
|
603
|
-
bmp[x, y] = 0
|
|
604
603
|
# forward is dest = R(ang)*(p-pivot)+pivot, so the pre-image of a dest pixel is
|
|
605
604
|
# src = R(-ang)*(dest-pivot)+pivot. R(-ang) = [[cos, sin], [-sin, cos]].
|
|
605
|
+
# Compute the whole new pose FIRST (no bitmap writes), then apply it as a
|
|
606
|
+
# DIFF against the previous pose. The device panel refreshes continuously
|
|
607
|
+
# from the bitmap, so the old erase-everything-then-slowly-redraw left a
|
|
608
|
+
# visible "head blinks out" window on every restamp (the desktop simulator
|
|
609
|
+
# only presents between steps, which is why it never showed there).
|
|
606
610
|
ca = math.cos(ang)
|
|
607
611
|
sa = math.sin(ang)
|
|
608
612
|
px, py = self._px, self._py
|
|
609
613
|
src = self._src
|
|
610
|
-
|
|
614
|
+
new = {}
|
|
611
615
|
excl = self._excl
|
|
612
616
|
for Y in range(self._sy0, self._sy1 + 1):
|
|
613
617
|
dY = Y - py
|
|
@@ -619,9 +623,15 @@ class RegionRotateAnimator(IntroAnimator):
|
|
|
619
623
|
sy = int(round(py - dX * sa + dY * ca))
|
|
620
624
|
ci = src.get((sx, sy))
|
|
621
625
|
if ci:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
self._stamped
|
|
626
|
+
new[(X, Y)] = ci
|
|
627
|
+
bmp = self.bitmap
|
|
628
|
+
for xy in self._stamped: # erase only pixels LEAVING the pose
|
|
629
|
+
if xy not in new:
|
|
630
|
+
bmp[xy[0], xy[1]] = 0
|
|
631
|
+
for xy, ci in new.items(): # write only pixels that changed
|
|
632
|
+
if bmp[xy[0], xy[1]] != ci:
|
|
633
|
+
bmp[xy[0], xy[1]] = ci
|
|
634
|
+
self._stamped = list(new)
|
|
625
635
|
self._last_ang = ang
|
|
626
636
|
self._hidden = False
|
|
627
637
|
|
|
@@ -1074,14 +1084,36 @@ class CelWalkAnimator(IntroAnimator):
|
|
|
1074
1084
|
HOLD_FRAMES = 104 # a full off-screen-to-off-screen crossing plus several strides
|
|
1075
1085
|
|
|
1076
1086
|
def __init__(self, sheet_suffix="_walk", period=6, bob=0, path="traverse_lr",
|
|
1077
|
-
tile_w=None, tile_h=None
|
|
1087
|
+
tile_w=None, tile_h=None, head_box=None, head_pivot=None,
|
|
1088
|
+
head_amp_deg=0, head_period=None, head_steps=5):
|
|
1089
|
+
"""Create an authored cel walk, optionally with a procedurally nodding head.
|
|
1090
|
+
|
|
1091
|
+
``head_box`` is an inclusive ``(x0, y0, x1, y1)`` box in each cel and
|
|
1092
|
+
``head_pivot`` is the fixed neck joint. When both are supplied with a
|
|
1093
|
+
non-zero ``head_amp_deg``, the head pixels are split from every authored
|
|
1094
|
+
cel and a small set of rotated head poses is baked at :meth:`start`.
|
|
1095
|
+
The walk still costs only tile-index and position writes per displayed
|
|
1096
|
+
frame; the one-time bake avoids rotating pixels on the MatrixPortal.
|
|
1097
|
+
|
|
1098
|
+
The box must include only the head/neck and the transparent space its arc
|
|
1099
|
+
crosses. The default ``head_period`` is one complete authored gait cycle
|
|
1100
|
+
(``period * number_of_cels``), and ``head_steps`` is the number of baked
|
|
1101
|
+
angles across ``-head_amp_deg`` through ``+head_amp_deg``.
|
|
1102
|
+
"""
|
|
1078
1103
|
self._suffix = sheet_suffix
|
|
1079
1104
|
self._period = max(1, period) # display frames per pose (the gait clock)
|
|
1080
1105
|
self._bob = bob
|
|
1081
1106
|
self._path = path
|
|
1082
1107
|
self._tw = tile_w
|
|
1083
1108
|
self._th = tile_h
|
|
1109
|
+
self._head_box = tuple(head_box) if head_box is not None else None
|
|
1110
|
+
self._head_pivot = tuple(head_pivot) if head_pivot is not None else None
|
|
1111
|
+
self._head_amp = head_amp_deg * 0.017453292519943295
|
|
1112
|
+
self._head_period = head_period
|
|
1113
|
+
steps = max(3, head_steps)
|
|
1114
|
+
self._head_steps = steps if steps & 1 else steps + 1
|
|
1084
1115
|
self._tile = None
|
|
1116
|
+
self._head_tile = None
|
|
1085
1117
|
self._odb = None
|
|
1086
1118
|
|
|
1087
1119
|
def _sheet_path(self):
|
|
@@ -1108,17 +1140,98 @@ class CelWalkAnimator(IntroAnimator):
|
|
|
1108
1140
|
n = sheet.width // tw
|
|
1109
1141
|
if n < 1:
|
|
1110
1142
|
raise ValueError("cel_walk: sheet narrower than one tile")
|
|
1111
|
-
|
|
1143
|
+
head_enabled = self._head_box is not None or self._head_pivot is not None
|
|
1144
|
+
if head_enabled:
|
|
1145
|
+
if self._head_box is None or self._head_pivot is None or not self._head_amp:
|
|
1146
|
+
raise ValueError("cel_walk: head rotation needs box, pivot, and non-zero amplitude")
|
|
1147
|
+
sheet, head_sheet = self._bake_head_poses(display.gfx, self._sheet_path(),
|
|
1148
|
+
n, tw, th, len(pal))
|
|
1149
|
+
gtile = display.gfx.TileGrid(sheet, pixel_shader=pal,
|
|
1150
|
+
tile_width=tw, tile_height=th)
|
|
1151
|
+
head_tile = display.gfx.TileGrid(head_sheet, pixel_shader=pal,
|
|
1152
|
+
tile_width=tw, tile_height=th)
|
|
1153
|
+
else:
|
|
1154
|
+
gtile = display.gfx.TileGrid(sheet, pixel_shader=pal, tile_width=tw, tile_height=th)
|
|
1155
|
+
head_tile = None
|
|
1112
1156
|
# Commit: blank the static base sprite, then composite the walking strip above it.
|
|
1113
1157
|
self._odb = odb
|
|
1114
1158
|
self._tile = gtile
|
|
1159
|
+
self._head_tile = head_tile
|
|
1115
1160
|
self._n = n
|
|
1116
1161
|
self._apply(0)
|
|
1117
1162
|
bitmap.fill(0)
|
|
1118
1163
|
display.add_layer(self._tile)
|
|
1164
|
+
if self._head_tile is not None:
|
|
1165
|
+
display.add_layer(self._head_tile)
|
|
1166
|
+
|
|
1167
|
+
def _bake_head_poses(self, gfx, sheet_path, n, tw, th, ncolors):
|
|
1168
|
+
"""Return a headless cel sheet plus a sheet of pre-rotated head poses.
|
|
1169
|
+
|
|
1170
|
+
A sheet read from ``OnDiskBitmap`` cannot be inspected or changed on
|
|
1171
|
+
CircuitPython. Decode it once only when this opt-in feature is used;
|
|
1172
|
+
the base sheet retains the authored body/leg poses while the sparse head
|
|
1173
|
+
sheet supplies ``n * head_steps`` rotated alternatives.
|
|
1174
|
+
"""
|
|
1175
|
+
x0, y0, x1, y1 = self._head_box
|
|
1176
|
+
px, py = self._head_pivot
|
|
1177
|
+
if x0 < 0 or y0 < 0 or x1 < x0 or y1 < y0 or x1 >= tw or y1 >= th:
|
|
1178
|
+
raise ValueError("cel_walk: head box outside a cel")
|
|
1179
|
+
if not (0 <= px < tw and 0 <= py < th):
|
|
1180
|
+
raise ValueError("cel_walk: head pivot outside a cel")
|
|
1181
|
+
|
|
1182
|
+
base = read_indexed_bmp(gfx, sheet_path)
|
|
1183
|
+
head_sheet = gfx.Bitmap(tw * n * self._head_steps, th, ncolors)
|
|
1184
|
+
max_radius = 1.0
|
|
1185
|
+
head_pixels = []
|
|
1186
|
+
for pose in range(n):
|
|
1187
|
+
pixels = {}
|
|
1188
|
+
offset = pose * tw
|
|
1189
|
+
for y in range(y0, y1 + 1):
|
|
1190
|
+
for x in range(x0, x1 + 1):
|
|
1191
|
+
ci = base[offset + x, y]
|
|
1192
|
+
if ci:
|
|
1193
|
+
pixels[(x, y)] = ci
|
|
1194
|
+
base[offset + x, y] = 0
|
|
1195
|
+
dx, dy = x - px, y - py
|
|
1196
|
+
radius = math.sqrt(dx * dx + dy * dy)
|
|
1197
|
+
if radius > max_radius:
|
|
1198
|
+
max_radius = radius
|
|
1199
|
+
if not pixels:
|
|
1200
|
+
raise ValueError("cel_walk: head box has no pixels in pose %d" % pose)
|
|
1201
|
+
head_pixels.append(pixels)
|
|
1202
|
+
|
|
1203
|
+
margin = int(max_radius * abs(math.sin(self._head_amp))) + 1
|
|
1204
|
+
sx0, sx1 = max(0, x0 - margin), min(tw - 1, x1 + margin)
|
|
1205
|
+
sy0, sy1 = max(0, y0 - margin), min(th - 1, y1 + margin)
|
|
1206
|
+
for pose, pixels in enumerate(head_pixels):
|
|
1207
|
+
for step in range(self._head_steps):
|
|
1208
|
+
# Equally-spaced angles include the upright rest pose because
|
|
1209
|
+
# head_steps is always odd.
|
|
1210
|
+
angle = self._head_amp * ((2.0 * step / (self._head_steps - 1)) - 1.0)
|
|
1211
|
+
ca, sa = math.cos(angle), math.sin(angle)
|
|
1212
|
+
cell_offset = (pose * self._head_steps + step) * tw
|
|
1213
|
+
# Inverse-map the bounded destination rectangle so the rotated
|
|
1214
|
+
# head stays solid instead of developing forward-map holes.
|
|
1215
|
+
for Y in range(sy0, sy1 + 1):
|
|
1216
|
+
dy = Y - py
|
|
1217
|
+
for X in range(sx0, sx1 + 1):
|
|
1218
|
+
dx = X - px
|
|
1219
|
+
sx = int(round(px + dx * ca + dy * sa))
|
|
1220
|
+
sy = int(round(py - dx * sa + dy * ca))
|
|
1221
|
+
ci = pixels.get((sx, sy))
|
|
1222
|
+
if ci:
|
|
1223
|
+
head_sheet[cell_offset + X, Y] = ci
|
|
1224
|
+
return base, head_sheet
|
|
1119
1225
|
|
|
1120
1226
|
def _apply(self, frame):
|
|
1121
1227
|
self._tile[0, 0] = (frame // self._period) % self._n # gait clock -> which pose
|
|
1228
|
+
head_tile = self._head_tile
|
|
1229
|
+
if head_tile is not None:
|
|
1230
|
+
cycle = self._head_period or (self._period * self._n)
|
|
1231
|
+
amount = (math.sin(6.2832 * frame / float(max(1, cycle))) + 1.0) * 0.5
|
|
1232
|
+
head_step = int(round(amount * (self._head_steps - 1)))
|
|
1233
|
+
pose = (frame // self._period) % self._n
|
|
1234
|
+
head_tile[0, 0] = pose * self._head_steps + head_step
|
|
1122
1235
|
span = self.HOLD_FRAMES - 1 if self.HOLD_FRAMES > 1 else 1
|
|
1123
1236
|
t = frame / span
|
|
1124
1237
|
if t > 1.0:
|
|
@@ -1126,13 +1239,23 @@ class CelWalkAnimator(IntroAnimator):
|
|
|
1126
1239
|
x0, x1 = ((self._off, -self._off) if self._path == "traverse_rl"
|
|
1127
1240
|
else (-self._off, self._off))
|
|
1128
1241
|
self._tile.x = int(round(x0 + (x1 - x0) * t))
|
|
1242
|
+
if head_tile is not None:
|
|
1243
|
+
head_tile.x = self._tile.x
|
|
1129
1244
|
if self._bob:
|
|
1130
1245
|
self._tile.y = int(round(self._bob * math.sin(frame * 0.3)))
|
|
1246
|
+
if head_tile is not None:
|
|
1247
|
+
head_tile.y = self._tile.y
|
|
1131
1248
|
|
|
1132
1249
|
def step(self, frame):
|
|
1133
1250
|
self._apply(frame)
|
|
1134
1251
|
|
|
1135
1252
|
def detach(self):
|
|
1253
|
+
head = getattr(self, "_head_tile", None)
|
|
1254
|
+
if head is not None and getattr(self, "display", None) is not None:
|
|
1255
|
+
try:
|
|
1256
|
+
self.display.remove_layer(head)
|
|
1257
|
+
except Exception:
|
|
1258
|
+
pass
|
|
1136
1259
|
t = getattr(self, "_tile", None)
|
|
1137
1260
|
if t is not None and getattr(self, "display", None) is not None:
|
|
1138
1261
|
try:
|
|
@@ -1143,6 +1266,7 @@ class CelWalkAnimator(IntroAnimator):
|
|
|
1143
1266
|
# (CircuitPython has a small open-file limit). No base-restore — the sprite walked off,
|
|
1144
1267
|
# the base is blank, the fade shows empty sky (matches MotionAnimator traverse).
|
|
1145
1268
|
self._tile = None
|
|
1269
|
+
self._head_tile = None
|
|
1146
1270
|
self._odb = None
|
|
1147
1271
|
|
|
1148
1272
|
|