scrollkit 0.8.3__py3-none-any.whl
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/__init__.py +16 -0
- scrollkit/app/__init__.py +6 -0
- scrollkit/app/base.py +918 -0
- scrollkit/app/memory.py +70 -0
- scrollkit/config/__init__.py +1 -0
- scrollkit/config/settings_manager.py +215 -0
- scrollkit/config/transition_names.py +31 -0
- scrollkit/dev/__init__.py +41 -0
- scrollkit/dev/capabilities.py +374 -0
- scrollkit/dev/harness.py +383 -0
- scrollkit/dev/metrics.py +91 -0
- scrollkit/dev/performance.py +174 -0
- scrollkit/dev/validation.py +245 -0
- scrollkit/display/__init__.py +14 -0
- scrollkit/display/_graphics.py +299 -0
- scrollkit/display/_recording.py +165 -0
- scrollkit/display/_sim_backend.py +99 -0
- scrollkit/display/bitmap_text.py +408 -0
- scrollkit/display/boards.py +186 -0
- scrollkit/display/colors.py +176 -0
- scrollkit/display/content.py +604 -0
- scrollkit/display/gradient_text.py +167 -0
- scrollkit/display/interface.py +126 -0
- scrollkit/display/simulator.py +80 -0
- scrollkit/display/text_fill.py +59 -0
- scrollkit/display/text_pixels.py +250 -0
- scrollkit/display/unified.py +595 -0
- scrollkit/effects/__init__.py +28 -0
- scrollkit/effects/drip_splash.py +253 -0
- scrollkit/effects/easing.py +131 -0
- scrollkit/effects/overlay.py +92 -0
- scrollkit/effects/particles.py +355 -0
- scrollkit/effects/reveal_splash.py +132 -0
- scrollkit/effects/scrolling.py +363 -0
- scrollkit/effects/swarm_reveal.py +512 -0
- scrollkit/effects/text_render.py +25 -0
- scrollkit/effects/transitions.py +873 -0
- scrollkit/exceptions.py +55 -0
- scrollkit/network/__init__.py +1 -0
- scrollkit/network/http_client.py +505 -0
- scrollkit/network/mdns.py +44 -0
- scrollkit/network/wifi_manager.py +382 -0
- scrollkit/ota/__init__.py +13 -0
- scrollkit/ota/client.py +528 -0
- scrollkit/ota/display_progress.py +125 -0
- scrollkit/ota/manifest.py +206 -0
- scrollkit/ota/publish.py +379 -0
- scrollkit/simulator/ATTRIBUTION.md +20 -0
- scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +364 -0
- scrollkit/simulator/LICENSE +176 -0
- scrollkit/simulator/README.md +87 -0
- scrollkit/simulator/__init__.py +11 -0
- scrollkit/simulator/adafruit_bitmap_font/__init__.py +5 -0
- scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +273 -0
- scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +70 -0
- scrollkit/simulator/adafruit_display_text/__init__.py +5 -0
- scrollkit/simulator/adafruit_display_text/label.py +336 -0
- scrollkit/simulator/bitmaptools.py +50 -0
- scrollkit/simulator/core/__init__.py +8 -0
- scrollkit/simulator/core/color_utils.py +62 -0
- scrollkit/simulator/core/device_benchmarks.json +254 -0
- scrollkit/simulator/core/feasibility.py +160 -0
- scrollkit/simulator/core/hardware_profile.py +191 -0
- scrollkit/simulator/core/led_matrix.py +307 -0
- scrollkit/simulator/core/matrixportal_s3_baseline.json +12 -0
- scrollkit/simulator/core/performance_manager.py +253 -0
- scrollkit/simulator/core/pixel_buffer.py +188 -0
- scrollkit/simulator/devices/__init__.py +7 -0
- scrollkit/simulator/devices/base_device.py +79 -0
- scrollkit/simulator/devices/matrixportal_s3.py +87 -0
- scrollkit/simulator/displayio/__init__.py +12 -0
- scrollkit/simulator/displayio/bitmap.py +158 -0
- scrollkit/simulator/displayio/display.py +195 -0
- scrollkit/simulator/displayio/fourwire.py +54 -0
- scrollkit/simulator/displayio/group.py +125 -0
- scrollkit/simulator/displayio/ondiskbitmap.py +109 -0
- scrollkit/simulator/displayio/palette.py +115 -0
- scrollkit/simulator/displayio/tilegrid.py +155 -0
- scrollkit/simulator/fonts/3x5.bdf +2474 -0
- scrollkit/simulator/fonts/Arial_16.bdf +7366 -0
- scrollkit/simulator/fonts/Arial_16.bdf.license +3 -0
- scrollkit/simulator/fonts/Arial_Bold_12.bdf +6131 -0
- scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +2 -0
- scrollkit/simulator/fonts/Arial_Bold_18.bdf +32653 -0
- scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +2 -0
- scrollkit/simulator/fonts/Junction_regular_24.bdf +8676 -0
- scrollkit/simulator/fonts/Junction_regular_24.bdf.license +3 -0
- scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +12458 -0
- scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +1921 -0
- scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +12458 -0
- scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +4 -0
- scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +16818 -0
- scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +1921 -0
- scrollkit/simulator/fonts/tom-thumb.bdf +2353 -0
- scrollkit/simulator/fonts/viii-bold.bdf +2673 -0
- scrollkit/simulator/fonts/viii.bdf +2659 -0
- scrollkit/simulator/terminalio/__init__.py +20 -0
- scrollkit/utils/__init__.py +1 -0
- scrollkit/utils/color_utils.py +54 -0
- scrollkit/utils/diagnostics.py +227 -0
- scrollkit/utils/error_handler.py +347 -0
- scrollkit/utils/system_utils.py +245 -0
- scrollkit/utils/url_utils.py +46 -0
- scrollkit/web/__init__.py +6 -0
- scrollkit/web/settings_server.py +328 -0
- scrollkit/web/wifi_setup.py +331 -0
- scrollkit-0.8.3.dist-info/METADATA +248 -0
- scrollkit-0.8.3.dist-info/RECORD +111 -0
- scrollkit-0.8.3.dist-info/WHEEL +5 -0
- scrollkit-0.8.3.dist-info/licenses/LICENSE +31 -0
- scrollkit-0.8.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Board registry: how ScrollKit builds a panel on each supported board.
|
|
3
|
+
|
|
4
|
+
ScrollKit runs the *same* app on more than one HUB75 driver board. The boards
|
|
5
|
+
differ in exactly three places — how the RGB matrix is constructed on
|
|
6
|
+
CircuitPython, the default panel geometry, and the calibrated performance profile
|
|
7
|
+
(that last one lives in ``simulator/core/hardware_profile.py``). This module owns
|
|
8
|
+
the first two and the board-detection logic.
|
|
9
|
+
|
|
10
|
+
This module is imported **on the device** (by ``display/unified.py``), so it must
|
|
11
|
+
stay CircuitPython-safe: no ``dataclasses``, no runtime ``typing``, no
|
|
12
|
+
``os.environ`` (use ``os.getenv``). All hardware imports (``rgbmatrix``,
|
|
13
|
+
``adafruit_matrixportal``, ``board`` …) are **function-local** inside the
|
|
14
|
+
``make_matrix`` builders, which only run on the real board — so the module
|
|
15
|
+
imports cleanly on the desktop and in unit tests with no hardware present.
|
|
16
|
+
|
|
17
|
+
To add a board, see ``docs/guide/hardware.md`` (the "Adding new hardware" guide).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
# Canonical board ids (the keys used everywhere: settings, baselines, profiles).
|
|
24
|
+
MATRIXPORTAL_S3 = "adafruit_matrixportal_s3"
|
|
25
|
+
INTERSTATE75_W = "pimoroni_interstate75_w"
|
|
26
|
+
|
|
27
|
+
DEFAULT_BOARD_ID = MATRIXPORTAL_S3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --- per-board matrix constructors (run ONLY on CircuitPython hardware) --------
|
|
31
|
+
#
|
|
32
|
+
# Each returns the triple UnifiedDisplay stores: (hardware, display, matrix).
|
|
33
|
+
# Imports are function-local on purpose — see the module docstring.
|
|
34
|
+
|
|
35
|
+
__all__ = ['BoardSpec', 'BOARDS', 'resolve_board', 'detect_board_id', 'DEFAULT_BOARD_ID', 'MATRIXPORTAL_S3', 'INTERSTATE75_W']
|
|
36
|
+
|
|
37
|
+
def _make_matrix_s3(spec, width, height, bit_depth):
|
|
38
|
+
"""Adafruit MatrixPortal S3: the adafruit_matrixportal wrapper (unchanged)."""
|
|
39
|
+
from adafruit_matrixportal.matrix import Matrix
|
|
40
|
+
m = Matrix(width=width, height=height, bit_depth=bit_depth)
|
|
41
|
+
return m, m.display, m
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _make_matrix_interstate75(spec, width, height, bit_depth):
|
|
45
|
+
"""Pimoroni Interstate 75 / 75 W: direct rgbmatrix + framebufferio.
|
|
46
|
+
|
|
47
|
+
Prefers the board's own RGBMatrix convenience aliases
|
|
48
|
+
(``board.MTX_COMMON`` / ``board.MTX_ADDRESS``) so no GPIO numbers are
|
|
49
|
+
hard-coded; falls back to the explicit Interstate 75 pin names if a given
|
|
50
|
+
build doesn't expose them.
|
|
51
|
+
"""
|
|
52
|
+
import board
|
|
53
|
+
import rgbmatrix
|
|
54
|
+
import framebufferio
|
|
55
|
+
|
|
56
|
+
common = getattr(board, "MTX_COMMON", None) # rgb_pins, clock/latch/oe
|
|
57
|
+
address = getattr(board, "MTX_ADDRESS", None) # tuple of address pins
|
|
58
|
+
if common is not None and address is not None:
|
|
59
|
+
matrix = rgbmatrix.RGBMatrix(
|
|
60
|
+
width=width, height=height, bit_depth=bit_depth,
|
|
61
|
+
addr_pins=address[:spec.addr_pin_count], **common)
|
|
62
|
+
else:
|
|
63
|
+
matrix = rgbmatrix.RGBMatrix(
|
|
64
|
+
width=width, height=height, bit_depth=bit_depth,
|
|
65
|
+
rgb_pins=[board.R0, board.G0, board.B0,
|
|
66
|
+
board.R1, board.G1, board.B1],
|
|
67
|
+
addr_pins=[board.ROW_A, board.ROW_B, board.ROW_C,
|
|
68
|
+
board.ROW_D, board.ROW_E][:spec.addr_pin_count],
|
|
69
|
+
clock_pin=board.CLK, latch_pin=board.LAT,
|
|
70
|
+
output_enable_pin=board.OE)
|
|
71
|
+
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)
|
|
72
|
+
return matrix, display, matrix
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BoardSpec:
|
|
76
|
+
"""Static description of a supported board.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
board_id: canonical id (the registry key).
|
|
80
|
+
name: human-readable board name.
|
|
81
|
+
default_width / default_height: panel geometry used when the app does
|
|
82
|
+
not pass explicit dimensions.
|
|
83
|
+
pitch: physical LED pitch in mm (cosmetic; used by the simulator render).
|
|
84
|
+
addr_pin_count: HUB75 address lines (4 for 64-row panels, 5 for 64-tall).
|
|
85
|
+
matrix_builder: callable ``(spec, width, height, bit_depth)`` returning
|
|
86
|
+
``(hardware, display, matrix)`` — runs only on CircuitPython.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, board_id, name, default_width, default_height, pitch,
|
|
90
|
+
addr_pin_count, matrix_builder):
|
|
91
|
+
self.board_id = board_id
|
|
92
|
+
self.name = name
|
|
93
|
+
self.default_width = default_width
|
|
94
|
+
self.default_height = default_height
|
|
95
|
+
self.pitch = pitch
|
|
96
|
+
self.addr_pin_count = addr_pin_count
|
|
97
|
+
self._matrix_builder = matrix_builder
|
|
98
|
+
|
|
99
|
+
def make_matrix(self, width, height, bit_depth):
|
|
100
|
+
"""Construct the RGB matrix on hardware (CircuitPython only)."""
|
|
101
|
+
return self._matrix_builder(self, width, height, bit_depth)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
BOARDS = {
|
|
105
|
+
MATRIXPORTAL_S3: BoardSpec(
|
|
106
|
+
MATRIXPORTAL_S3, "Adafruit MatrixPortal S3 (ESP32-S3)",
|
|
107
|
+
default_width=64, default_height=32, pitch=3.0, addr_pin_count=4,
|
|
108
|
+
matrix_builder=_make_matrix_s3),
|
|
109
|
+
INTERSTATE75_W: BoardSpec(
|
|
110
|
+
INTERSTATE75_W, "Pimoroni Interstate 75 W (RP2350)",
|
|
111
|
+
default_width=64, default_height=32, pitch=3.0, addr_pin_count=4,
|
|
112
|
+
matrix_builder=_make_matrix_interstate75),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Map the on-device ``board.board_id`` (or ``os.uname().machine``) strings to a
|
|
117
|
+
# canonical id. The exact RP2350 "W" id should be confirmed on the real board;
|
|
118
|
+
# the substring fallback in _map_ondevice_id() catches naming variants.
|
|
119
|
+
_ONDEVICE_ID_MAP = {
|
|
120
|
+
"adafruit_matrixportal_s3": MATRIXPORTAL_S3,
|
|
121
|
+
"pimoroni_interstate75": INTERSTATE75_W,
|
|
122
|
+
"pimoroni_interstate75_w": INTERSTATE75_W,
|
|
123
|
+
"pimoroni_interstate75_rp2350": INTERSTATE75_W,
|
|
124
|
+
"pimoroni_interstate75_w_rp2350": INTERSTATE75_W,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _read_ondevice_id():
|
|
129
|
+
"""The raw board id string on CircuitPython; ``None`` on desktop."""
|
|
130
|
+
if not (hasattr(sys, "implementation")
|
|
131
|
+
and sys.implementation.name == "circuitpython"):
|
|
132
|
+
return None
|
|
133
|
+
try:
|
|
134
|
+
import board
|
|
135
|
+
bid = getattr(board, "board_id", None)
|
|
136
|
+
if bid:
|
|
137
|
+
return bid
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
try:
|
|
141
|
+
return os.uname().machine
|
|
142
|
+
except Exception:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _map_ondevice_id(raw):
|
|
147
|
+
"""Resolve a raw on-device id (or any id string) to a canonical board id."""
|
|
148
|
+
if not raw:
|
|
149
|
+
return DEFAULT_BOARD_ID
|
|
150
|
+
key = str(raw).strip().lower()
|
|
151
|
+
if key in BOARDS:
|
|
152
|
+
return key
|
|
153
|
+
if key in _ONDEVICE_ID_MAP:
|
|
154
|
+
return _ONDEVICE_ID_MAP[key]
|
|
155
|
+
if "interstate75" in key or "interstate 75" in key:
|
|
156
|
+
return INTERSTATE75_W
|
|
157
|
+
if "matrixportal" in key:
|
|
158
|
+
return MATRIXPORTAL_S3
|
|
159
|
+
return DEFAULT_BOARD_ID
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def detect_board_id():
|
|
163
|
+
"""Best-effort canonical board id for the board we're running on.
|
|
164
|
+
|
|
165
|
+
On CircuitPython this reads ``board.board_id``; on the desktop there is no
|
|
166
|
+
board, so it returns the default (MatrixPortal S3).
|
|
167
|
+
"""
|
|
168
|
+
return _map_ondevice_id(_read_ondevice_id())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def resolve_board(board=None):
|
|
172
|
+
"""Return the :class:`BoardSpec` to use.
|
|
173
|
+
|
|
174
|
+
Precedence: an explicit ``board`` argument, then the ``SCROLLKIT_HW_BOARD``
|
|
175
|
+
environment variable, then auto-detection, then the default board. Accepts a
|
|
176
|
+
canonical id or a raw on-device id; unknown ids fall back to the default.
|
|
177
|
+
"""
|
|
178
|
+
chosen = board
|
|
179
|
+
if not chosen:
|
|
180
|
+
try:
|
|
181
|
+
chosen = os.getenv("SCROLLKIT_HW_BOARD")
|
|
182
|
+
except Exception:
|
|
183
|
+
chosen = None
|
|
184
|
+
if not chosen:
|
|
185
|
+
chosen = detect_board_id()
|
|
186
|
+
return BOARDS.get(_map_ondevice_id(chosen), BOARDS[DEFAULT_BOARD_ID])
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Continuous 24-bit colour generators (device-safe, pure-integer math).
|
|
3
|
+
|
|
4
|
+
These expose the **full** colour space as functions an app or effect samples at any
|
|
5
|
+
resolution — deliberately NOT a catalogue of named palettes. The point is to escape
|
|
6
|
+
fixed mini-palettes (the old 6-colour rainbow ramp, the 16 named colours): generate
|
|
7
|
+
exactly the gradient/spectrum you want, at the resolution that suits the panel.
|
|
8
|
+
|
|
9
|
+
CircuitPython-safe: integer arithmetic, no ``colorsys``, no typing at runtime. Build a
|
|
10
|
+
ramp ONCE (at effect construction or import) and reuse it — none of these belong in a
|
|
11
|
+
per-frame loop. Every function takes/returns packed ``0xRRGGBB`` ints.
|
|
12
|
+
|
|
13
|
+
spectrum(24) # 24 smooth hues around the wheel
|
|
14
|
+
gradient(0x102840, 0x00CCFF, 16) # 16 steps, deep blue -> cyan
|
|
15
|
+
multi_gradient((0x330000, 0xFF4400, 0xFFF0A0), 32) # fire ramp, 32 steps
|
|
16
|
+
hsv(210, 0.8, 1.0) # any hue/sat/value -> one colour
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ['wheel', 'spectrum', 'lerp', 'gradient', 'multi_gradient', 'scale', 'depth_palette', 'hsv']
|
|
21
|
+
|
|
22
|
+
def _clamp8(v):
|
|
23
|
+
if v < 0:
|
|
24
|
+
return 0
|
|
25
|
+
if v > 255:
|
|
26
|
+
return 255
|
|
27
|
+
return v
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def wheel(pos):
|
|
31
|
+
"""Full-saturation, full-value hue for ``pos`` 0..255 -> ``0xRRGGBB``.
|
|
32
|
+
|
|
33
|
+
A continuous colour wheel (red -> green -> blue -> red), cheap integer math so it
|
|
34
|
+
is safe to call on CircuitPython. Wraps, so ``pos`` may be any int.
|
|
35
|
+
"""
|
|
36
|
+
pos = pos & 0xFF
|
|
37
|
+
if pos < 85:
|
|
38
|
+
return ((255 - pos * 3) << 16) | ((pos * 3) << 8)
|
|
39
|
+
if pos < 170:
|
|
40
|
+
pos -= 85
|
|
41
|
+
return ((255 - pos * 3) << 8) | (pos * 3)
|
|
42
|
+
pos -= 170
|
|
43
|
+
return ((pos * 3) << 16) | (255 - pos * 3)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def spectrum(n):
|
|
47
|
+
"""``n`` evenly spaced full-saturation hues across the wheel (cyclic-friendly).
|
|
48
|
+
|
|
49
|
+
Endpoints don't duplicate (positions are ``i*256//n``), so the ramp tiles/rotates
|
|
50
|
+
without a visible seam — ideal for a rotating rainbow.
|
|
51
|
+
"""
|
|
52
|
+
if n <= 0:
|
|
53
|
+
return ()
|
|
54
|
+
return tuple(wheel(i * 256 // n) for i in range(n))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def lerp(a, b, t):
|
|
58
|
+
"""Blend ``0xRRGGBB`` ``a`` -> ``b`` by ``t`` in 0.0..1.0."""
|
|
59
|
+
if t <= 0:
|
|
60
|
+
return a
|
|
61
|
+
if t >= 1:
|
|
62
|
+
return b
|
|
63
|
+
ar = (a >> 16) & 0xFF
|
|
64
|
+
ag = (a >> 8) & 0xFF
|
|
65
|
+
ab = a & 0xFF
|
|
66
|
+
br = (b >> 16) & 0xFF
|
|
67
|
+
bg = (b >> 8) & 0xFF
|
|
68
|
+
bb = b & 0xFF
|
|
69
|
+
r = ar + int((br - ar) * t)
|
|
70
|
+
g = ag + int((bg - ag) * t)
|
|
71
|
+
bl = ab + int((bb - ab) * t)
|
|
72
|
+
return (_clamp8(r) << 16) | (_clamp8(g) << 8) | _clamp8(bl)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def gradient(a, b, n):
|
|
76
|
+
"""``n`` colours interpolated from ``a`` to ``b`` (both inclusive)."""
|
|
77
|
+
if n <= 0:
|
|
78
|
+
return ()
|
|
79
|
+
if n == 1:
|
|
80
|
+
return (a,)
|
|
81
|
+
return tuple(lerp(a, b, i / (n - 1)) for i in range(n))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def multi_gradient(stops, n):
|
|
85
|
+
"""``n`` colours interpolated across a sequence of ``stops`` (>= 1 colour)."""
|
|
86
|
+
stops = tuple(stops)
|
|
87
|
+
if not stops or n <= 0:
|
|
88
|
+
return ()
|
|
89
|
+
if len(stops) == 1:
|
|
90
|
+
return tuple(stops[0] for _ in range(n))
|
|
91
|
+
if n == 1:
|
|
92
|
+
return (stops[0],)
|
|
93
|
+
segs = len(stops) - 1
|
|
94
|
+
out = []
|
|
95
|
+
for i in range(n):
|
|
96
|
+
pos = i * segs / (n - 1) # 0 .. segs
|
|
97
|
+
si = int(pos)
|
|
98
|
+
if si >= segs:
|
|
99
|
+
si = segs - 1
|
|
100
|
+
t = 1.0
|
|
101
|
+
else:
|
|
102
|
+
t = pos - si
|
|
103
|
+
out.append(lerp(stops[si], stops[si + 1], t))
|
|
104
|
+
return tuple(out)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def scale(color, factor):
|
|
108
|
+
"""Scale a ``0xRRGGBB`` colour's brightness by ``factor`` (0.0-1.0). Integer-only.
|
|
109
|
+
|
|
110
|
+
Effects call this once at construction to derive shades from a base colour, so
|
|
111
|
+
there is no per-frame cost.
|
|
112
|
+
"""
|
|
113
|
+
if factor <= 0.0:
|
|
114
|
+
return 0
|
|
115
|
+
f = 256 if factor >= 1.0 else int(factor * 256)
|
|
116
|
+
r = (((color >> 16) & 0xFF) * f) >> 8
|
|
117
|
+
g = (((color >> 8) & 0xFF) * f) >> 8
|
|
118
|
+
b = ((color & 0xFF) * f) >> 8
|
|
119
|
+
return (r << 16) | (g << 8) | b
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def depth_palette(color, strength=0.4, steps=2):
|
|
123
|
+
"""Two (or more) close shades of ``color`` for subtle "lit from above" depth.
|
|
124
|
+
|
|
125
|
+
Returns a ramp from ``color`` (the highlight) down to a darker shade
|
|
126
|
+
``scale(color, 1 - strength)`` (the shadow). Feed it straight to a gradient
|
|
127
|
+
text fill::
|
|
128
|
+
|
|
129
|
+
ScrollingText("NEXT TRAIN", palette=depth_palette(0x66CCFF))
|
|
130
|
+
|
|
131
|
+
This is a colour *transform*, NOT a named palette — it keeps the
|
|
132
|
+
"expose the full space via generators" philosophy: you pass any base colour
|
|
133
|
+
and a subtlety knob, and get a tasteful close ramp instead of hand-picking
|
|
134
|
+
two near-identical hex values.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
color: Base ``0xRRGGBB`` highlight colour.
|
|
138
|
+
strength: 0.0..1.0 depth amount; the shadow is ``color`` darkened by this
|
|
139
|
+
fraction. ~0.4 is a tasteful default; below ~0.15 the two shades
|
|
140
|
+
can collapse to one step on the 4-bit panel.
|
|
141
|
+
steps: Length of the returned ramp. 2 returns just ``(highlight,
|
|
142
|
+
shadow)`` (the renderer expands it to ``palette_steps``); >2
|
|
143
|
+
returns an already-interpolated ramp.
|
|
144
|
+
"""
|
|
145
|
+
shadow = scale(color, max(0.0, 1.0 - strength))
|
|
146
|
+
if steps <= 2:
|
|
147
|
+
return (color, shadow)
|
|
148
|
+
return gradient(color, shadow, steps)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def hsv(h, s=1.0, v=1.0):
|
|
152
|
+
"""HSV -> ``0xRRGGBB``. ``h`` in degrees (0..360, wraps), ``s``/``v`` in 0.0..1.0.
|
|
153
|
+
|
|
154
|
+
The general-purpose generator when you want a specific hue at a chosen saturation
|
|
155
|
+
and value rather than a point on the full-bright :func:`wheel`.
|
|
156
|
+
"""
|
|
157
|
+
h = h % 360
|
|
158
|
+
c = v * s
|
|
159
|
+
x = c * (1 - abs((h / 60.0) % 2 - 1))
|
|
160
|
+
m = v - c
|
|
161
|
+
if h < 60:
|
|
162
|
+
rp, gp, bp = c, x, 0.0
|
|
163
|
+
elif h < 120:
|
|
164
|
+
rp, gp, bp = x, c, 0.0
|
|
165
|
+
elif h < 180:
|
|
166
|
+
rp, gp, bp = 0.0, c, x
|
|
167
|
+
elif h < 240:
|
|
168
|
+
rp, gp, bp = 0.0, x, c
|
|
169
|
+
elif h < 300:
|
|
170
|
+
rp, gp, bp = x, 0.0, c
|
|
171
|
+
else:
|
|
172
|
+
rp, gp, bp = c, 0.0, x
|
|
173
|
+
r = _clamp8(int((rp + m) * 255))
|
|
174
|
+
g = _clamp8(int((gp + m) * 255))
|
|
175
|
+
b = _clamp8(int((bp + m) * 255))
|
|
176
|
+
return (r << 16) | (g << 8) | b
|