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.
Files changed (121) hide show
  1. {scrollkit-0.8.5/src/scrollkit.egg-info → scrollkit-0.9.0}/PKG-INFO +15 -4
  2. {scrollkit-0.8.5 → scrollkit-0.9.0}/README.md +14 -3
  3. {scrollkit-0.8.5 → scrollkit-0.9.0}/pyproject.toml +1 -1
  4. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/__init__.py +1 -1
  5. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/capabilities.py +32 -0
  6. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/__init__.py +8 -0
  7. scrollkit-0.9.0/src/scrollkit/effects/palette_partition.py +294 -0
  8. scrollkit-0.9.0/src/scrollkit/effects/palette_treatments.py +705 -0
  9. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/swarm_reveal.py +50 -3
  10. scrollkit-0.9.0/src/scrollkit/effects/swirl_in.py +108 -0
  11. scrollkit-0.9.0/src/scrollkit/utils/scheduler.py +71 -0
  12. {scrollkit-0.8.5 → scrollkit-0.9.0/src/scrollkit.egg-info}/PKG-INFO +15 -4
  13. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit.egg-info/SOURCES.txt +4 -0
  14. {scrollkit-0.8.5 → scrollkit-0.9.0}/LICENSE +0 -0
  15. {scrollkit-0.8.5 → scrollkit-0.9.0}/setup.cfg +0 -0
  16. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/app/__init__.py +0 -0
  17. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/app/base.py +0 -0
  18. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/app/memory.py +0 -0
  19. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/config/__init__.py +0 -0
  20. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/config/settings_manager.py +0 -0
  21. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/config/transition_names.py +0 -0
  22. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/__init__.py +0 -0
  23. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/harness.py +0 -0
  24. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/metrics.py +0 -0
  25. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/performance.py +0 -0
  26. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/dev/validation.py +0 -0
  27. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/__init__.py +0 -0
  28. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/_graphics.py +0 -0
  29. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/_recording.py +0 -0
  30. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/_sim_backend.py +0 -0
  31. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/bitmap_text.py +0 -0
  32. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/boards.py +0 -0
  33. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/colors.py +0 -0
  34. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/content.py +0 -0
  35. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/gradient_text.py +0 -0
  36. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/interface.py +0 -0
  37. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/simulator.py +0 -0
  38. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/text_fill.py +0 -0
  39. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/text_pixels.py +0 -0
  40. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/display/unified.py +0 -0
  41. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/drip_splash.py +0 -0
  42. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/easing.py +0 -0
  43. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/image_animators.py +0 -0
  44. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/overlay.py +0 -0
  45. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/particles.py +0 -0
  46. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/reveal_splash.py +0 -0
  47. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/scrolling.py +0 -0
  48. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/text_render.py +0 -0
  49. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/effects/transitions.py +0 -0
  50. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/exceptions.py +0 -0
  51. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/network/__init__.py +0 -0
  52. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/network/http_client.py +0 -0
  53. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/network/mdns.py +0 -0
  54. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/network/wifi_manager.py +0 -0
  55. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/__init__.py +0 -0
  56. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/client.py +0 -0
  57. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/display_progress.py +0 -0
  58. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/manifest.py +0 -0
  59. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/ota/publish.py +0 -0
  60. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/ATTRIBUTION.md +0 -0
  61. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +0 -0
  62. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/LICENSE +0 -0
  63. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/README.md +0 -0
  64. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/__init__.py +0 -0
  65. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/__init__.py +0 -0
  66. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +0 -0
  67. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +0 -0
  68. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_display_text/__init__.py +0 -0
  69. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/adafruit_display_text/label.py +0 -0
  70. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/bitmaptools.py +0 -0
  71. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/__init__.py +0 -0
  72. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/color_utils.py +0 -0
  73. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/device_benchmarks.json +0 -0
  74. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/feasibility.py +0 -0
  75. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/hardware_profile.py +0 -0
  76. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/led_matrix.py +0 -0
  77. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/matrixportal_s3_baseline.json +0 -0
  78. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/performance_manager.py +0 -0
  79. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/core/pixel_buffer.py +0 -0
  80. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/__init__.py +0 -0
  81. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/base_device.py +0 -0
  82. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/devices/matrixportal_s3.py +0 -0
  83. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/__init__.py +0 -0
  84. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/bitmap.py +0 -0
  85. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/display.py +0 -0
  86. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/fourwire.py +0 -0
  87. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/group.py +0 -0
  88. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/ondiskbitmap.py +0 -0
  89. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/palette.py +0 -0
  90. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/displayio/tilegrid.py +0 -0
  91. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/3x5.bdf +0 -0
  92. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_16.bdf +0 -0
  93. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_16.bdf.license +0 -0
  94. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf +0 -0
  95. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +0 -0
  96. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf +0 -0
  97. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +0 -0
  98. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Junction_regular_24.bdf +0 -0
  99. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/Junction_regular_24.bdf.license +0 -0
  100. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +0 -0
  101. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +0 -0
  102. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +0 -0
  103. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +0 -0
  104. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +0 -0
  105. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +0 -0
  106. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/tom-thumb.bdf +0 -0
  107. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/viii-bold.bdf +0 -0
  108. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/fonts/viii.bdf +0 -0
  109. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/simulator/terminalio/__init__.py +0 -0
  110. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/__init__.py +0 -0
  111. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/color_utils.py +0 -0
  112. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/diagnostics.py +0 -0
  113. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/error_handler.py +0 -0
  114. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/system_utils.py +0 -0
  115. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/utils/url_utils.py +0 -0
  116. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/web/__init__.py +0 -0
  117. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/web/settings_server.py +0 -0
  118. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit/web/wifi_setup.py +0 -0
  119. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit.egg-info/dependency_links.txt +0 -0
  120. {scrollkit-0.8.5 → scrollkit-0.9.0}/src/scrollkit.egg-info/requires.txt +0 -0
  121. {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.8.5
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
- # Desktop development with simulator
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 modify ScrollKit itself (or run the demos): clone and install editable
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 — copy scrollkit/ to your device's lib/ alongside your source
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
- # Desktop development with simulator
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 modify ScrollKit itself (or run the demos): clone and install editable
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 — copy scrollkit/ to your device's lib/ alongside your source
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scrollkit"
7
- version = "0.8.5"
7
+ version = "0.9.0"
8
8
  description = "LED Matrix Display Framework for CircuitPython and Desktop"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -13,4 +13,4 @@ need from submodules, e.g.::
13
13
  from scrollkit.display.unified import UnifiedDisplay
14
14
  """
15
15
 
16
- __version__ = "0.8.5"
16
+ __version__ = "0.9.0"
@@ -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