scrollkit 0.8.5__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.5/src/scrollkit.egg-info → scrollkit-0.9.0}/PKG-INFO +15 -4
- {scrollkit-0.8.5 → scrollkit-0.9.0}/README.md +14 -3
- {scrollkit-0.8.5 → scrollkit-0.9.0}/pyproject.toml +1 -1
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/__init__.py +1 -1
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/capabilities.py +32 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/__init__.py +8 -0
- 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.5 → 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.9.0/src/scrollkit/utils/scheduler.py +71 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0/src/scrollkit.egg-info}/PKG-INFO +15 -4
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit.egg-info/SOURCES.txt +4 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/LICENSE +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/setup.cfg +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/app/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/app/base.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/app/memory.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/config/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/config/settings_manager.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/config/transition_names.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/harness.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/metrics.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/performance.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/validation.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/_graphics.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/_recording.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/_sim_backend.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/bitmap_text.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/boards.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/colors.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/content.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/gradient_text.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/interface.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/simulator.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/text_fill.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/text_pixels.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/unified.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/drip_splash.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/easing.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/image_animators.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/overlay.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/particles.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/reveal_splash.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/scrolling.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/text_render.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/transitions.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/exceptions.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/network/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/network/http_client.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/network/mdns.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/network/wifi_manager.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/client.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/display_progress.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/manifest.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/publish.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/ATTRIBUTION.md +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/LICENSE +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/README.md +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_display_text/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_display_text/label.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/bitmaptools.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/color_utils.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/device_benchmarks.json +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/feasibility.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/hardware_profile.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/led_matrix.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/matrixportal_s3_baseline.json +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/performance_manager.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/pixel_buffer.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/base_device.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/matrixportal_s3.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/bitmap.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/display.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/fourwire.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/group.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/ondiskbitmap.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/palette.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/tilegrid.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/3x5.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_16.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_16.bdf.license +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Junction_regular_24.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Junction_regular_24.bdf.license +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/tom-thumb.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/viii-bold.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/viii.bdf +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/terminalio/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/color_utils.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/diagnostics.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/error_handler.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/system_utils.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/url_utils.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/web/__init__.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/web/settings_server.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/web/wifi_setup.py +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit.egg-info/dependency_links.txt +0 -0
- {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit.egg-info/requires.txt +0 -0
- {scrollkit-0.8.5 → 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
|
|
@@ -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
|
|
@@ -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),
|
|
@@ -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
|
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Palette-partition effect layers: animate any pixel set with palette writes.
|
|
2
|
+
|
|
3
|
+
The cheapest device-safe animation there is. Statically assign every lit
|
|
4
|
+
pixel of an assembled mark (a logo, a word, an icon) to one of N groups,
|
|
5
|
+
bake that partition into an indexed full-panel bitmap ONCE, then animate
|
|
6
|
+
by rewriting N palette entries per frame: zero per-frame pixel work.
|
|
7
|
+
|
|
8
|
+
Each :class:`PalettePartition` owns its bitmap AND its own Palette, so
|
|
9
|
+
animating one layer can never recolor other sprites or another partition.
|
|
10
|
+
``identity_colors`` reserves palette slots that stay constant through any
|
|
11
|
+
treatment (a mascot's eyes, a logo accent) — the identity anchor.
|
|
12
|
+
|
|
13
|
+
Partition builders return ``({(x, y): group_index}, n_groups)`` for the
|
|
14
|
+
BODY pixels only (input slot 1); identity pixels (input slots >= 2) are
|
|
15
|
+
handled by PalettePartition itself. Pair a partition with the treatment
|
|
16
|
+
classes in :mod:`scrollkit.effects.palette_treatments`, or paint it
|
|
17
|
+
directly.
|
|
18
|
+
|
|
19
|
+
Promoted from the DarkOwl LED logo app (2026), where every dwell effect
|
|
20
|
+
in a 24/7 sign runs on these partitions.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import math
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"PalettePartition",
|
|
27
|
+
"bfs_paths",
|
|
28
|
+
"map_anchor_distance",
|
|
29
|
+
"map_angle",
|
|
30
|
+
"map_checker",
|
|
31
|
+
"map_diagonal",
|
|
32
|
+
"map_exposure",
|
|
33
|
+
"map_radial",
|
|
34
|
+
"map_rain",
|
|
35
|
+
"map_regions",
|
|
36
|
+
"map_route",
|
|
37
|
+
"map_topology",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PalettePartition:
|
|
42
|
+
"""One partitioned layer: a bitmap, its palette, and paint().
|
|
43
|
+
|
|
44
|
+
``pixel_slots`` maps ``(x, y) -> input slot``: slot 1 is the body
|
|
45
|
+
(partitioned into groups), slots 2..k+1 are identity pixels that take
|
|
46
|
+
the reserved palette slots 1..k (seeded from ``identity_colors`` and
|
|
47
|
+
written only via :meth:`set_identity`). Group colors start at slot
|
|
48
|
+
``1 + len(identity_colors)``.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
FEASIBILITY = {
|
|
52
|
+
"hardware_safe": True,
|
|
53
|
+
"allocates_per_frame": False,
|
|
54
|
+
"max_pixel_writes_per_frame": 0, # animation is palette writes
|
|
55
|
+
"modeled_frame_ms": 0.5,
|
|
56
|
+
"note": "one-time build cost ~len(pixel_slots); paint() is n_groups"
|
|
57
|
+
" palette writes",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def __init__(self, gfx, pixel_slots, group_map, n_groups,
|
|
61
|
+
identity_colors=(), width=64, height=32):
|
|
62
|
+
self.n_groups = n_groups
|
|
63
|
+
self._group_base = 1 + len(identity_colors)
|
|
64
|
+
bmp = gfx.Bitmap(width, height, self._group_base + n_groups)
|
|
65
|
+
for (x, y), slot in pixel_slots.items():
|
|
66
|
+
if slot == 1:
|
|
67
|
+
bmp[x, y] = self._group_base + group_map[(x, y)]
|
|
68
|
+
else:
|
|
69
|
+
bmp[x, y] = slot - 1 # input slot 2 -> identity slot 1
|
|
70
|
+
self.palette = gfx.Palette(self._group_base + n_groups)
|
|
71
|
+
self.palette.make_transparent(0)
|
|
72
|
+
for i, color in enumerate(identity_colors):
|
|
73
|
+
self.palette[1 + i] = color
|
|
74
|
+
self._identity_colors = tuple(identity_colors)
|
|
75
|
+
self.tile = gfx.TileGrid(bmp, pixel_shader=self.palette)
|
|
76
|
+
self.tile.hidden = True
|
|
77
|
+
|
|
78
|
+
def paint(self, colors):
|
|
79
|
+
"""Write all group colors (len == n_groups). Identity untouched."""
|
|
80
|
+
pal = self.palette
|
|
81
|
+
base = self._group_base
|
|
82
|
+
for i, c in enumerate(colors):
|
|
83
|
+
pal[base + i] = c
|
|
84
|
+
|
|
85
|
+
def fill(self, color):
|
|
86
|
+
"""Every group the same color (the invisible-swap state)."""
|
|
87
|
+
pal = self.palette
|
|
88
|
+
base = self._group_base
|
|
89
|
+
for i in range(self.n_groups):
|
|
90
|
+
pal[base + i] = color
|
|
91
|
+
|
|
92
|
+
def set_identity(self, *colors):
|
|
93
|
+
"""Write the reserved identity slots (e.g. dim the eyes to blink)."""
|
|
94
|
+
for i, c in enumerate(colors):
|
|
95
|
+
self.palette[1 + i] = c
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Partition builders. Each takes ``pixel_slots`` ({(x, y): slot}) plus
|
|
100
|
+
# parameters, and returns ({(x, y): group}, n_groups) over the BODY pixels.
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def _body_pixels(pixel_slots):
|
|
104
|
+
return [p for p, slot in pixel_slots.items() if slot == 1]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def map_diagonal(pixel_slots, n=10):
|
|
108
|
+
"""Bands along x + 2y: a diagonal sweep axis."""
|
|
109
|
+
body = _body_pixels(pixel_slots)
|
|
110
|
+
vals = {p: p[0] + 2 * p[1] for p in body}
|
|
111
|
+
lo = min(vals.values())
|
|
112
|
+
hi = max(vals.values())
|
|
113
|
+
span = (hi - lo) + 1
|
|
114
|
+
return {p: (v - lo) * n // span for p, v in vals.items()}, n
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def map_anchor_distance(pixel_slots, anchor_x, n=10):
|
|
118
|
+
"""Mirrored bands by horizontal distance from an anchor column."""
|
|
119
|
+
body = _body_pixels(pixel_slots)
|
|
120
|
+
vals = {p: abs(p[0] - anchor_x) for p in body}
|
|
121
|
+
hi = max(vals.values())
|
|
122
|
+
return {p: v * n // (hi + 1) for p, v in vals.items()}, n
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def map_radial(pixel_slots, cx, cy, n=10, y_scale=2):
|
|
126
|
+
"""Concentric distance bands around a point. ``y_scale`` compensates
|
|
127
|
+
panel aspect (64x32 pixels are wider than tall; pass 1 for square)."""
|
|
128
|
+
body = _body_pixels(pixel_slots)
|
|
129
|
+
vals = {}
|
|
130
|
+
for (x, y) in body:
|
|
131
|
+
dx = x - cx
|
|
132
|
+
dy = (y - cy) * y_scale
|
|
133
|
+
vals[(x, y)] = (dx * dx + dy * dy) ** 0.5
|
|
134
|
+
hi = max(vals.values())
|
|
135
|
+
return {p: min(int(v * n / (hi + 1e-6)), n - 1)
|
|
136
|
+
for p, v in vals.items()}, n
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def map_angle(pixel_slots, cx, cy, n=14):
|
|
140
|
+
"""Angular wedges around a point (a radar sweep axis)."""
|
|
141
|
+
body = _body_pixels(pixel_slots)
|
|
142
|
+
groups = {}
|
|
143
|
+
for (x, y) in body:
|
|
144
|
+
a = math.atan2(y - cy, x - cx) # -pi..pi
|
|
145
|
+
g = int((a + math.pi) * n / (2 * math.pi + 1e-6))
|
|
146
|
+
groups[(x, y)] = min(g, n - 1)
|
|
147
|
+
return groups, n
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def map_rain(pixel_slots, n=10):
|
|
151
|
+
"""Per-column phase-staggered vertical cycle. Rotating the palette one
|
|
152
|
+
step per beat makes highlights descend each column, out of phase with
|
|
153
|
+
its neighbours."""
|
|
154
|
+
body = _body_pixels(pixel_slots)
|
|
155
|
+
groups = {}
|
|
156
|
+
for (x, y) in body:
|
|
157
|
+
phase = (x * 7 + (x >> 2) * 3) % n # cheap fixed column hash
|
|
158
|
+
groups[(x, y)] = (y + phase) % n
|
|
159
|
+
return groups, n
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def map_checker(pixel_slots, n=4):
|
|
163
|
+
"""Interleaved parity groups: a satin-shimmer texture."""
|
|
164
|
+
body = _body_pixels(pixel_slots)
|
|
165
|
+
if n == 2:
|
|
166
|
+
return {p: (p[0] + p[1]) % 2 for p in body}, 2
|
|
167
|
+
return {p: (p[0] % 2) + 2 * (p[1] % 2) for p in body}, 4
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def map_exposure(pixel_slots):
|
|
171
|
+
"""Group by which side of the stroke faces open sky.
|
|
172
|
+
Groups: 0 top-facing, 1 right, 2 bottom, 3 left, 4 sheltered."""
|
|
173
|
+
body = set(_body_pixels(pixel_slots))
|
|
174
|
+
groups = {}
|
|
175
|
+
dirs = ((0, -1), (1, 0), (0, 1), (-1, 0))
|
|
176
|
+
for (x, y) in body:
|
|
177
|
+
g = 4
|
|
178
|
+
for i, (dx, dy) in enumerate(dirs):
|
|
179
|
+
if (x + dx, y + dy) not in body:
|
|
180
|
+
g = i
|
|
181
|
+
break
|
|
182
|
+
groups[(x, y)] = g
|
|
183
|
+
return groups, 5
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def map_regions(pixel_slots, n=10):
|
|
187
|
+
"""Spatially coherent Voronoi regions over the mark. Seeds are lit
|
|
188
|
+
pixels spaced evenly along x, so regions read as contiguous patches
|
|
189
|
+
rather than confetti."""
|
|
190
|
+
body = sorted(_body_pixels(pixel_slots))
|
|
191
|
+
step = max(1, len(body) // n)
|
|
192
|
+
seeds = [body[i * step + step // 2] for i in range(n)]
|
|
193
|
+
groups = {}
|
|
194
|
+
for (x, y) in body:
|
|
195
|
+
best, best_d = 0, 1 << 30
|
|
196
|
+
for i, (sx, sy) in enumerate(seeds):
|
|
197
|
+
dx = x - sx
|
|
198
|
+
dy = (y - sy) * 2 # panel aspect
|
|
199
|
+
d = dx * dx + dy * dy
|
|
200
|
+
if d < best_d:
|
|
201
|
+
best, best_d = i, d
|
|
202
|
+
groups[(x, y)] = best
|
|
203
|
+
return groups, n
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def bfs_paths(glyph_pixel_sets):
|
|
207
|
+
"""Ordered pixel paths (BFS depth order) per glyph, for packet sprites
|
|
208
|
+
that crawl the actual strokes."""
|
|
209
|
+
paths = []
|
|
210
|
+
for pixel_set in glyph_pixel_sets:
|
|
211
|
+
depth = _bfs_depth(pixel_set)
|
|
212
|
+
paths.append(sorted(depth, key=lambda p: (depth[p], p[1], p[0])))
|
|
213
|
+
return paths
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def map_topology(pixel_slots):
|
|
217
|
+
"""Classify each body pixel by its lit neighborhood.
|
|
218
|
+
|
|
219
|
+
Groups: 0 endpoint, 1 corner/diagonal, 2 junction,
|
|
220
|
+
3 horizontal run, 4 vertical run.
|
|
221
|
+
"""
|
|
222
|
+
body = set(_body_pixels(pixel_slots))
|
|
223
|
+
groups = {}
|
|
224
|
+
for (x, y) in body:
|
|
225
|
+
nbrs = []
|
|
226
|
+
for dx in (-1, 0, 1):
|
|
227
|
+
for dy in (-1, 0, 1):
|
|
228
|
+
if (dx or dy) and (x + dx, y + dy) in body:
|
|
229
|
+
nbrs.append((dx, dy))
|
|
230
|
+
deg = len(nbrs)
|
|
231
|
+
if deg <= 1:
|
|
232
|
+
g = 0
|
|
233
|
+
elif deg >= 3:
|
|
234
|
+
g = 2
|
|
235
|
+
else:
|
|
236
|
+
(ax, ay), (bx, by) = nbrs
|
|
237
|
+
if ax == -bx and ay == -by: # colinear through-pixel
|
|
238
|
+
if ay == 0:
|
|
239
|
+
g = 3 # horizontal run
|
|
240
|
+
elif ax == 0:
|
|
241
|
+
g = 4 # vertical run
|
|
242
|
+
else:
|
|
243
|
+
g = 1 # diagonal run
|
|
244
|
+
else:
|
|
245
|
+
g = 1 # corner
|
|
246
|
+
groups[(x, y)] = g
|
|
247
|
+
return groups, 5
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def map_route(pixel_slots, path_pixel_sets, terminus_pixels, sections=3):
|
|
251
|
+
"""BFS along each glyph's strokes, quantized into ordered sections, so
|
|
252
|
+
a 'packet' visibly crawls the letterforms toward a terminus.
|
|
253
|
+
|
|
254
|
+
path_pixel_sets: list of (set of body (x, y)) in ROUTE order
|
|
255
|
+
terminus_pixels: set of the destination's body pixels (the last stop)
|
|
256
|
+
Returns (group_map, n_groups); groups are ordered along the route,
|
|
257
|
+
with the terminus as the last group.
|
|
258
|
+
"""
|
|
259
|
+
groups = {}
|
|
260
|
+
next_group = 0
|
|
261
|
+
for pixel_set in path_pixel_sets:
|
|
262
|
+
depth = _bfs_depth(pixel_set)
|
|
263
|
+
max_d = max(depth.values()) if depth else 0
|
|
264
|
+
for p, d in depth.items():
|
|
265
|
+
groups[p] = next_group + d * sections // (max_d + 1)
|
|
266
|
+
next_group += sections
|
|
267
|
+
for p in terminus_pixels:
|
|
268
|
+
groups[p] = next_group
|
|
269
|
+
return groups, next_group + 1
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _bfs_depth(pixels):
|
|
273
|
+
"""BFS depth over a set of pixels (8-connected) from the top-left one."""
|
|
274
|
+
if not pixels:
|
|
275
|
+
return {}
|
|
276
|
+
seed = min(pixels, key=lambda p: (p[1], p[0]))
|
|
277
|
+
depth = {seed: 0}
|
|
278
|
+
queue = [seed]
|
|
279
|
+
qi = 0
|
|
280
|
+
while qi < len(queue):
|
|
281
|
+
x, y = queue[qi]
|
|
282
|
+
qi += 1
|
|
283
|
+
d = depth[(x, y)]
|
|
284
|
+
for dx in (-1, 0, 1):
|
|
285
|
+
for dy in (-1, 0, 1):
|
|
286
|
+
q = (x + dx, y + dy)
|
|
287
|
+
if (dx or dy) and q in pixels and q not in depth:
|
|
288
|
+
depth[q] = d + 1
|
|
289
|
+
queue.append(q)
|
|
290
|
+
# unreachable stragglers (shouldn't happen on connected glyphs)
|
|
291
|
+
for p in pixels:
|
|
292
|
+
if p not in depth:
|
|
293
|
+
depth[p] = 0
|
|
294
|
+
return depth
|