Ship the portal FastAPI controller with presets, sequences, and live audio.

Move the app under src/, add ring-spanning patterns with shared fonts and
beat-driven motion, and wire global audio settings into the web UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-09 00:59:03 +12:00
co-authored by Cursor
parent 5094c7bcee
commit 3d444d1cb5
127 changed files with 10318 additions and 62632 deletions
+1
View File
@@ -0,0 +1 @@
"""Standalone LED patterns. Each module defines one Pattern subclass."""
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Drifting aurora curtains across the portal ring."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_pulse
from leds.colors import blend, dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
class Aurora(Pattern):
id = "aurora"
label = "Aurora"
fps = 28
colors = {
"green": Color("#2cff9a"),
"violet": Color("#6a4cff"),
"night": Color("#040814"),
}
params = {
"spread": Param(0.7, min=0.2, max=1.0, step=0.05, label="Spread"),
"bg_brightness": Param(0.45, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
spread = float(extras["spread"])
bg_b = float(extras["bg_brightness"])
green = params["colors"]["green"]
violet = params["colors"]["violet"]
night = params["colors"]["night"]
pulse = resolve_pulse(params, t, fallback_bpm=96, attack=0.2)
energy = band(params, "energy")
hat = band(params, "hat")
_, ring, _ = ring_metrics(params, w)
drift = t * (ring * 0.08)
for y in range(h):
row = y / max(h - 1, 1)
for x in range(w):
gx = global_x(x, params, w)
a = math.sin((gx + drift) * 0.09 + y * 0.7)
b = math.sin((gx - drift * 0.55) * 0.16 + t * 1.1)
c = math.sin((gx + drift * 0.25) * 0.04 + y * 1.4)
field = 0.5 + 0.5 * (0.5 * a + 0.35 * b + 0.15 * c)
field = min(1.0, field * (0.55 + 0.45 * spread) + 0.18 * pulse + 0.15 * energy)
curtain = field * (0.55 + 0.45 * (1.0 - abs(row - 0.4)))
sky = dim(night, bg_b * (0.4 + 0.25 * field))
ribbon = blend(green, violet, 0.35 + 0.45 * row + 0.1 * hat)
glow = dim(ribbon, curtain)
set_pixel(
surface,
x,
y,
(
min(255, sky[0] + glow[0]),
min(255, sky[1] + glow[1]),
min(255, sky[2] + glow[2]),
),
)
if __name__ == "__main__":
run_cli(Aurora)
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Bouncing blob."""
from __future__ import annotations
import math
from leds.colors import wheel
from leds.pattern import Pattern, run_cli, set_pixel, size
class Bounce(Pattern):
id = "bounce"
label = "Bounce"
fps = 35
def draw(self, surface, t, params):
w, h = size(surface)
frame = int(t * self.fps)
surface.fill((0, 0, 0))
phase = frame * 0.2
x = int((math.sin(phase) + 1) / 2 * (w - 1))
y = int((math.cos(phase * 1.3) + 1) / 2 * (h - 1))
color = wheel(frame * 5)
for dy in range(-1, 2):
for dx in range(-1, 2):
px, py = x + dx, y + dy
if 0 <= px < w and 0 <= py < h:
set_pixel(surface, px, py, color)
if __name__ == "__main__":
run_cli(Bounce)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Brazier fires at the hexagonal panel seams, driven by kick/bass FFT."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_beat_wave, resolve_pulse
from leds.colors import dim, heat_color
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, portal_ring_offsets, ring_metrics
class Braziers(Pattern):
id = "braziers"
label = "Braziers"
fps = 30
colors = {
"ember": Color("#ff6a18"),
"ash": Color("#120806"),
}
params = {
"heat": Param(0.8, min=0.2, max=1.0, step=0.05, label="Heat"),
"bg_brightness": Param(0.35, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
heat = float(extras["heat"])
bg_b = float(extras["bg_brightness"])
ember = params["colors"]["ember"]
ash = params["colors"]["ash"]
pulse = resolve_pulse(params, t, fallback_bpm=118, attack=0.22)
wave = resolve_beat_wave(params, t, fallback_bpm=118)
kick = band(params, "kick")
bass = band(params, "bass")
energy = band(params, "energy")
x_offset, ring, _ = ring_metrics(params, w)
seams = list(portal_ring_offsets().values())
drive = max(pulse, kick, bass * 0.85)
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
dist = min(min((gx - s) % ring, (s - gx) % ring) for s in seams)
column = math.exp(-(dist**2) / (8.0 + 10.0 * (1.0 - heat)))
rise = (h - 1 - y) / max(h - 1, 1)
flicker = 0.75 + 0.25 * ((gx * 3 + y * 5) % 7) / 6.0
level = column * (0.35 + 0.65 * rise) * flicker
level = min(
1.0,
level * heat * (0.55 + 0.55 * drive + 0.2 * wave + 0.15 * energy),
)
bg = dim(ash, bg_b * (0.4 + 0.3 * rise))
flame = heat_color(level)
tint = dim(ember, level * 0.35)
set_pixel(
surface,
x,
y,
(
min(255, bg[0] + flame[0] + tint[0]),
min(255, bg[1] + flame[1] + tint[1]),
min(255, bg[2] + flame[2] + tint[2]),
),
)
if drive > 0.4:
for seam in seams:
local = seam - x_offset
if not (0 <= local < w):
continue
sy = max(0, h - 1 - int(drive * (h - 1)))
set_pixel(surface, local, sy, dim((255, 230, 160), drive))
if __name__ == "__main__":
run_cli(Braziers)
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Rainbow comet."""
from __future__ import annotations
from leds.colors import dim, wheel
from leds.pattern import Pattern, run_cli, set_pixel, size
class Comet(Pattern):
id = "comet"
label = "Comet"
fps = 30
def draw(self, surface, t, params):
w, h = size(surface)
frame = int(t * self.fps)
head = frame % (w + 12)
surface.fill((0, 0, 0))
for y in range(h):
for i in range(12):
x = head - i
if 0 <= x < w:
set_pixel(surface, x, y, dim(wheel(frame * 3 + i * 10), (12 - i) / 12))
if __name__ == "__main__":
run_cli(Comet)
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Stars stepping around the portal ring on each live beat."""
from __future__ import annotations
import math
from leds.bpm_limits import (
band,
resolve_beat_index,
resolve_beat_phase,
resolve_pulse,
)
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
STARS = (
0.02,
0.09,
0.17,
0.23,
0.31,
0.38,
0.47,
0.54,
0.62,
0.71,
0.79,
0.88,
0.95,
)
class Constellation(Pattern):
id = "constellation"
label = "Constellation"
fps = 30
colors = {
"star": Color("#ffe9a8"),
"link": Color("#6a90c8"),
"night": Color("#04060e"),
}
params = {
"trail": Param(0.55, min=0.1, max=1.0, step=0.05, label="Trail"),
"bg_brightness": Param(0.4, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
trail = float(extras["trail"])
bg_b = float(extras["bg_brightness"])
star = params["colors"]["star"]
link = params["colors"]["link"]
night = params["colors"]["night"]
pulse = resolve_pulse(params, t, fallback_bpm=108, attack=0.16)
phase = resolve_beat_phase(params, t, fallback_bpm=108)
step = resolve_beat_index(params, t, fallback_bpm=108)
hat = band(params, "hat")
energy = band(params, "energy")
x_offset, ring, _ = ring_metrics(params, w)
active = step % len(STARS)
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
mist = 0.25 + 0.2 * math.sin(gx * 0.05 + t * 0.4) + 0.08 * energy
set_pixel(surface, x, y, dim(night, mist * bg_b))
mid = h // 2
for i, frac in enumerate(STARS):
gx = int(frac * ring) % max(ring, 1)
nxt = STARS[(i + 1) % len(STARS)]
gx2 = int(nxt * ring) % max(ring, 1)
span = (gx2 - gx) % max(ring, 1)
if span > ring // 2:
continue
age = (active - i) % len(STARS)
link_level = max(0.0, 1.0 - age / max(len(STARS) * trail, 1)) * 0.35
if link_level <= 0:
continue
for k in range(0, span, 2):
px = (gx + k) % max(ring, 1)
local = px - x_offset
if 0 <= local < w:
set_pixel(surface, local, mid, dim(link, link_level))
for i, frac in enumerate(STARS):
gx = int(frac * ring) % max(ring, 1)
local = gx - x_offset
if not (0 <= local < w):
continue
age = (active - i) % len(STARS)
if i == active:
level = 0.5 + 0.5 * max(pulse, hat)
else:
level = max(0.0, 1.0 - age / max(len(STARS) * trail, 1)) * 0.45
if level <= 0:
continue
for dx, dy in ((0, 0), (1, 0), (-1, 0), (0, 1), (0, -1)):
xx, yy = local + dx, mid + dy
if 0 <= xx < w and 0 <= yy < h:
fall = 1.0 if dx == 0 and dy == 0 else 0.45
set_pixel(surface, xx, yy, dim(star, level * fall))
if i == active and pulse > 0:
yy = max(0, mid - int(phase * mid))
set_pixel(surface, local, yy, dim(star, pulse))
if __name__ == "__main__":
run_cli(Constellation)
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Dark disc with a breathing gold corona."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_pulse
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
class Eclipse(Pattern):
id = "eclipse"
label = "Eclipse"
fps = 28
colors = {
"corona": Color("#ffb428"),
"flare": Color("#ff6a18"),
"void": Color("#020206"),
}
params = {
"size": Param(0.55, min=0.25, max=0.9, step=0.05, label="Disc"),
"bg_brightness": Param(0.3, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
disc = float(extras["size"])
bg_b = float(extras["bg_brightness"])
corona = params["colors"]["corona"]
flare = params["colors"]["flare"]
void = params["colors"]["void"]
pulse = resolve_pulse(params, t, fallback_bpm=88, attack=0.22)
energy = band(params, "energy")
cx = (w - 1) / 2.0
cy = (h - 1) / 2.0
reach = math.hypot(cx, cy) + 0.5
radius = (0.35 + 0.45 * disc + 0.08 * pulse) * reach
for y in range(h):
for x in range(w):
dist = math.hypot(x - cx, y - cy)
angle = math.atan2(y - cy, x - cx)
spike = 0.5 + 0.5 * math.sin(angle * 7.0 + t * 1.6)
rim = math.exp(-((dist - radius) ** 2) / (2.4 + 2.0 * pulse))
inner = 1.0 if dist < radius else math.exp(-((dist - radius) ** 2) / 1.4)
sky = dim(void, bg_b * (0.25 + 0.2 * energy))
glow = dim(corona, rim * (0.45 + 0.55 * pulse) * (0.7 + 0.3 * spike))
fire = dim(flare, rim * rim * (0.2 + 0.5 * pulse))
shade = dim(void, inner * 0.85)
set_pixel(
surface,
x,
y,
(
min(255, sky[0] + glow[0] + fire[0] + shade[0]),
min(255, sky[1] + glow[1] + fire[1] + shade[1]),
min(255, sky[2] + glow[2] + fire[2] + shade[2]),
),
)
if __name__ == "__main__":
run_cli(Eclipse)
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Rising ember sparks from the portal floor."""
from __future__ import annotations
import random
from leds.bpm_limits import resolve_pulse
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
class Embers(Pattern):
id = "embers"
label = "Embers"
fps = 30
colors = {
"spark": Color("#ff7a20"),
"ash": Color("#120806"),
}
params = {
"count": Param(14, min=4, max=28, step=1, label="Sparks"),
"bg_brightness": Param(0.35, min=0.0, max=1.0, step=0.05, label="Background"),
}
def __init__(self) -> None:
self._sparks: dict[int, list[dict[str, float]]] = {}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
wanted = max(4, int(extras["count"]))
bg_b = float(extras["bg_brightness"])
spark = params["colors"]["spark"]
ash = params["colors"]["ash"]
pulse = resolve_pulse(params, t, fallback_bpm=108, attack=0.22)
key = id(surface)
sparks = self._sparks.get(key)
if sparks is None or len(sparks) != wanted:
sparks = [
{
"x": float(random.randint(0, max(w - 1, 0))),
"y": float(random.uniform(0, h)),
"speed": random.uniform(0.12, 0.38),
"life": random.uniform(0.4, 1.0),
}
for _ in range(wanted)
]
self._sparks[key] = sparks
for y in range(h):
rise = (h - 1 - y) / max(h - 1, 1)
for x in range(w):
set_pixel(surface, x, y, dim(ash, bg_b * (0.35 + 0.4 * rise)))
for ember in sparks:
ember["y"] -= ember["speed"] * (0.7 + 0.6 * pulse)
ember["life"] -= 0.012
if ember["y"] < -1 or ember["life"] <= 0:
ember["x"] = float(random.randint(0, max(w - 1, 0)))
ember["y"] = float(h + random.uniform(0, 2))
ember["speed"] = random.uniform(0.12, 0.38)
ember["life"] = random.uniform(0.45, 1.0)
px, py = int(ember["x"]), int(ember["y"])
if 0 <= px < w and 0 <= py < h:
set_pixel(surface, px, py, dim(spark, ember["life"]))
if py + 1 < h:
set_pixel(surface, px, py + 1, dim(spark, ember["life"] * 0.35))
if __name__ == "__main__":
run_cli(Embers)
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Fire / heat rise."""
from __future__ import annotations
import random
from leds.colors import heat_color
from leds.pattern import Pattern, run_cli, set_pixel, size
class Fire(Pattern):
id = "fire"
label = "Fire"
fps = 35
def __init__(self) -> None:
self._heat: dict[int, list[float]] = {}
def draw(self, surface, t, params):
w, h = size(surface)
n = w * h
key = id(surface)
heat = self._heat.get(key)
if heat is None or len(heat) != n:
heat = [0.0] * n
self._heat[key] = heat
for x in range(w):
heat[(h - 1) * w + x] = random.random() * 0.5 + 0.5
for y in range(h - 2, -1, -1):
for x in range(w):
below = (y + 1) * w + x
left = below - 1 if x > 0 else below
right = below + 1 if x < w - 1 else below
decay = random.uniform(0.0, 0.18)
heat[y * w + x] = max(0.0, (heat[left] + heat[below] + heat[right]) / 3 - decay)
for y in range(h):
for x in range(w):
set_pixel(surface, x, y, heat_color(heat[y * w + x]))
if __name__ == "__main__":
run_cli(Fire)
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Ancient signs scrolling continuously around the portal ring."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_pulse, resolve_scroll
from leds.colors import dim
from leds.font import blit_sequence, resolve_glyphs, sequence_width, vcenter
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
BACKGROUNDS = ("night", "wood", "stone", "solid", "stars")
def _fill_background(
surface,
*,
w: int,
h: int,
t: float,
params: dict,
style: str,
base,
bg_b: float,
pulse: float,
energy: float,
) -> None:
_ = t
name = style if style in BACKGROUNDS else "night"
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
if name == "wood":
grain = 0.4 + 0.6 * (0.5 + 0.5 * math.sin(gx * 0.9 + y * 2.2))
frost = 0.04 + 0.12 * pulse + 0.08 * energy
bg = dim(base, grain * bg_b)
set_pixel(
surface,
x,
y,
(
min(255, bg[0] + int(frost * bg_b * 40)),
min(255, bg[1] + int(frost * bg_b * 70)),
min(255, bg[2] + int(frost * bg_b * 90)),
),
)
elif name == "stone":
grain = 0.55 + 0.45 * math.sin(gx * 0.37 + y * 1.1)
level = grain * (0.9 + 0.1 * pulse + 0.08 * energy)
set_pixel(surface, x, y, dim(base, level * bg_b))
elif name == "solid":
set_pixel(surface, x, y, dim(base, (0.9 + 0.1 * pulse) * bg_b))
elif name == "stars":
level = 0.12 + 0.08 * energy
twinkle = (gx * 17 + y * 31) % 29
if twinkle == 0:
level = 0.45 + 0.25 * pulse
elif twinkle == 7:
level = 0.28 + 0.12 * pulse
set_pixel(surface, x, y, dim(base, level * bg_b))
else: # night
veil = 0.55 + 0.45 * math.sin(gx * 0.12 + y * 0.35)
set_pixel(
surface,
x,
y,
dim(base, (0.45 + 0.55 * veil + 0.08 * pulse + 0.08 * energy) * bg_b),
)
class Glyphs(Pattern):
id = "glyphs"
label = "Glyphs"
fps = 28
colors = {
"gold": Color("#f0c050"),
"night": Color("#08060c"),
}
params = {
"background": Param(
"night",
kind="choice",
choices=BACKGROUNDS,
label="Background",
),
"text_brightness": Param(1.0, min=0.0, max=1.0, step=0.05, label="Text brightness"),
"bg_brightness": Param(0.55, min=0.0, max=1.0, step=0.05, label="Background brightness"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
pulse = resolve_pulse(params, t, fallback_bpm=92, attack=0.15)
snare = band(params, "snare")
energy = band(params, "energy")
text_b = float(extras["text_brightness"])
bg_b = float(extras["bg_brightness"])
bg_name = str(extras.get("background") or "night")
glyphs = resolve_glyphs(params, default="hieroglyphs")
gold = dim(params["colors"]["gold"], text_b * (0.85 + 0.15 * max(pulse, snare)))
night = params["colors"]["night"]
x_offset, ring, _ = ring_metrics(params, w)
gap = 2
span = sequence_width(glyphs, gap=gap)
period = max(span + gap, 1)
_fill_background(
surface,
w=w,
h=h,
t=t,
params=params,
style=bg_name,
base=night,
bg_b=bg_b,
pulse=pulse,
energy=energy,
)
scroll = resolve_scroll(
params,
t,
fallback_bpm=92,
pixels_per_beat=max(2.0, ring / 16.0),
loop=period,
)
y0 = vcenter(h, 7)
first = math.floor((x_offset + scroll) / period) - 1
last = math.ceil((x_offset + w + scroll) / period) + 1
for k in range(first, last + 1):
blit_sequence(
surface,
-scroll - x_offset + k * period,
y0,
glyphs,
gold,
gap=gap,
)
if h >= 9 and bg_name in ("night", "stone"):
line = dim(gold, 0.3 + 0.15 * pulse)
for x in range(w):
set_pixel(surface, x, h - 1, line)
if __name__ == "__main__":
run_cli(Glyphs)
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Gold Latin mottoes scrolling continuously around the portal ring."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_pulse, resolve_scroll
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
from leds.text import CHAR_HEIGHT, draw_text, text_width
MOTTOES = (
"AVE",
"SPQR",
"VENI VIDI VICI",
"CARPE DIEM",
"TEMPUS FUGIT",
"MEMENTO MORI",
"ALEA IACTA EST",
)
SEPARATOR = " * "
DEFAULT_TEXT = SEPARATOR.join(MOTTOES)
class Inscription(Pattern):
id = "inscription"
label = "Inscription"
fps = 28
colors = {
"ink": Color("#e0b44a"),
"stone": Color("#2a1c10"),
}
params = {
"text": Param(
DEFAULT_TEXT,
kind="text",
max_length=160,
label="Text",
placeholder="AVE * SPQR",
),
"text_brightness": Param(1.0, min=0.0, max=1.0, step=0.05, label="Text brightness"),
"bg_brightness": Param(0.55, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
pulse = resolve_pulse(params, t, fallback_bpm=96, attack=0.18)
bass = band(params, "bass")
energy = band(params, "energy")
ink = dim(
params["colors"]["ink"],
float(extras["text_brightness"]) * (0.85 + 0.15 * max(pulse, bass)),
)
stone = params["colors"]["stone"]
bg_b = float(extras["bg_brightness"])
banner = str(extras.get("text") or DEFAULT_TEXT).strip() or DEFAULT_TEXT
x_offset, ring, _ = ring_metrics(params, w)
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
grain = 0.55 + 0.45 * math.sin(gx * 0.37 + y * 1.1)
level = grain * (0.92 + 0.08 * energy)
set_pixel(surface, x, y, dim(stone, level * bg_b))
span = text_width(banner)
if span <= 0:
return
loop = max(span + max(ring // 8, 8), ring)
scroll = resolve_scroll(
params,
t,
fallback_bpm=96,
pixels_per_beat=max(2.0, ring / 8.0),
loop=loop,
)
origin = -scroll - x_offset
y0 = max(0, (h - CHAR_HEIGHT) // 2)
draw_text(surface, banner, origin, y0, ink)
draw_text(surface, banner, origin + loop, y0, ink)
if __name__ == "__main__":
run_cli(Inscription)
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Beat-reactive lattice wards across each panel."""
from __future__ import annotations
import math
from leds.bpm_limits import resolve_beat_index, resolve_drive, resolve_pulse
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x
class Lattice(Pattern):
id = "lattice"
label = "Lattice"
fps = 30
colors = {
"line": Color("#7ad0ff"),
"node": Color("#ffe08a"),
"void": Color("#060810"),
}
params = {
"spacing": Param(4, min=3, max=8, step=1, label="Spacing"),
"bg_brightness": Param(0.4, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
spacing = max(3, int(extras["spacing"]))
bg_b = float(extras["bg_brightness"])
line = params["colors"]["line"]
node = params["colors"]["node"]
void = params["colors"]["void"]
pulse = resolve_pulse(params, t, fallback_bpm=120, attack=0.12)
drive = resolve_drive(params, t, fallback_bpm=120)
step = resolve_beat_index(params, t, fallback_bpm=120)
shift = step % spacing
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
haze = 0.28 + 0.12 * math.sin(gx * 0.08 + t * 0.5)
set_pixel(surface, x, y, dim(void, haze * bg_b))
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
on_v = (gx + shift) % spacing == 0
on_h = y % max(2, spacing // 2) == 0
if on_v or on_h:
set_pixel(surface, x, y, dim(line, 0.28 + 0.55 * drive))
if on_v and on_h:
set_pixel(surface, x, y, dim(node, 0.5 + 0.5 * pulse))
if __name__ == "__main__":
run_cli(Lattice)
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Diagonal meteors with fading trails."""
from __future__ import annotations
import random
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
class Meteor(Pattern):
id = "meteor"
label = "Meteor"
fps = 32
colors = {
"head": Color("#ffe8c8"),
"tail": Color("#ff6a28"),
"night": Color("#05040a"),
}
params = {
"count": Param(6, min=2, max=16, step=1, label="Meteors"),
"bg_brightness": Param(0.25, min=0.0, max=1.0, step=0.05, label="Background"),
}
def __init__(self) -> None:
self._showers: dict[int, list[dict[str, float]]] = {}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
wanted = max(2, int(extras["count"]))
bg_b = float(extras["bg_brightness"])
head = params["colors"]["head"]
tail = params["colors"]["tail"]
night = params["colors"]["night"]
key = id(surface)
showers = self._showers.get(key)
if showers is None or len(showers) != wanted:
showers = [
{
"x": float(random.randint(-4, w + 2)),
"y": float(random.randint(-h, h - 1)),
"speed": random.uniform(0.6, 1.6),
"len": random.randint(4, 8),
}
for _ in range(wanted)
]
showers[0]["x"] = w / 2
showers[0]["y"] = max(h / 2, 1.0)
self._showers[key] = showers
for y in range(h):
for x in range(w):
set_pixel(surface, x, y, dim(night, bg_b))
for meteor in showers:
meteor["x"] += meteor["speed"]
meteor["y"] += meteor["speed"] * 0.55
if meteor["x"] > w + 6 or meteor["y"] > h + 4:
meteor["x"] = float(random.randint(-8, w // 2))
meteor["y"] = float(random.randint(-h, -1))
meteor["speed"] = random.uniform(0.6, 1.6)
meteor["len"] = random.randint(4, 8)
length = int(meteor["len"])
for i in range(length):
px = int(meteor["x"] - i)
py = int(meteor["y"] - i * 0.55)
if not (0 <= px < w and 0 <= py < h):
continue
fade = (length - i) / length
color = head if i == 0 else dim(tail, fade)
set_pixel(surface, px, py, color)
if __name__ == "__main__":
run_cli(Meteor)
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Words of omen fading on torchlit stone, locked to live beats."""
from __future__ import annotations
import math
import re
from leds.bpm_limits import (
band,
resolve_beat_index,
resolve_beat_phase,
resolve_beat_wave,
resolve_pulse,
)
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x
from leds.text import CHAR_HEIGHT, draw_text, text_width
WORDS = (
"AVE",
"FATE",
"OMEN",
"LUX",
"NOX",
"REX",
"ASTRA",
"FIAT",
)
DEFAULT_TEXT = " ".join(WORDS)
_SPLIT = re.compile(r"[\s,/|;]+")
def _words(text: str) -> tuple[str, ...]:
parts = tuple(part for part in _SPLIT.split(text.strip().upper()) if part)
return parts or WORDS
class Oracle(Pattern):
id = "oracle"
label = "Oracle"
fps = 30
colors = {
"ink": Color("#ffce6a"),
"ember": Color("#4a1808"),
}
params = {
"beats_per_word": Param(4, min=2, max=16, step=1, label="Beats / word"),
"text": Param(
DEFAULT_TEXT,
kind="text",
max_length=160,
label="Words",
placeholder="AVE FATE OMEN",
),
"text_brightness": Param(1.0, min=0.0, max=1.0, step=0.05, label="Text brightness"),
"bg_brightness": Param(0.55, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
ink = params["colors"]["ink"]
ember = params["colors"]["ember"]
beats_per_word = max(2, int(extras.get("beats_per_word", 4)))
text_b = float(extras["text_brightness"])
bg_b = float(extras["bg_brightness"])
words = _words(str(extras.get("text") or DEFAULT_TEXT))
pulse = resolve_pulse(params, t, fallback_bpm=80, attack=0.2)
wave = resolve_beat_wave(params, t, fallback_bpm=80)
kick = band(params, "kick")
energy = band(params, "energy")
idx = (resolve_beat_index(params, t, fallback_bpm=80) // beats_per_word) % len(words)
local_beat = (
resolve_beat_index(params, t, fallback_bpm=80) % beats_per_word
) + resolve_beat_phase(params, t, fallback_bpm=80)
if local_beat < 0.35:
alpha = local_beat / 0.35
elif local_beat > beats_per_word - 0.8:
alpha = max(0.0, (beats_per_word - local_beat) / 0.8)
else:
alpha = 1.0
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
heat = 0.35 + 0.65 * (
0.5
+ 0.5
* math.sin(gx * 0.45 + t * 8.0)
* math.cos(y * 0.9 - t * 3.1)
)
heat = min(1.0, heat + 0.3 * max(pulse, kick) * wave + 0.15 * energy)
edge = 1.0 - abs((y / max(h - 1, 1)) - 0.5) * 0.5
set_pixel(surface, x, y, dim(ember, heat * edge * bg_b))
word = words[idx]
tw = text_width(word)
x0 = (w - tw) // 2
y0 = max(0, (h - CHAR_HEIGHT) // 2)
if alpha > 0:
draw_text(
surface,
word,
x0,
y0,
dim(ink, alpha * text_b * (0.8 + 0.2 * max(pulse, kick))),
)
if __name__ == "__main__":
run_cli(Oracle)
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Plasma field."""
from __future__ import annotations
import math
from leds.colors import hsv_to_rgb
from leds.pattern import Pattern, run_cli, set_pixel, size
class Plasma(Pattern):
id = "plasma"
label = "Plasma"
fps = 25
def draw(self, surface, t, params):
w, h = size(surface)
phase = t * self.fps * 0.08
for y in range(h):
for x in range(w):
v = (
math.sin(x * 0.15 + phase)
+ math.sin(y * 0.2 - phase * 1.3)
+ math.sin((x + y) * 0.1 + phase * 0.7)
) / 3.0
set_pixel(surface, x, y, hsv_to_rgb((v + 1) / 2, 1.0, 1.0))
if __name__ == "__main__":
run_cli(Plasma)
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Ritual procession of runes circling the full hexagonal portal."""
from __future__ import annotations
import math
from leds.bpm_limits import (
band,
resolve_beat_index,
resolve_beat_phase,
resolve_pulse,
)
from leds.colors import dim
from leds.font import blit_glyph, font_param, glyph_width, resolve_glyphs, vcenter
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
class Procession(Pattern):
id = "procession"
label = "Procession"
fps = 30
colors = {
"mark": Color("#e8c56a"),
"path": Color("#1a1008"),
"accent": Color("#6ec8ff"),
}
params = {
"font": font_param("mixed"),
"density": Param(0.55, min=0.2, max=1.0, step=0.05, label="Density"),
"bg_brightness": Param(0.45, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
density = float(extras["density"])
bg_b = float(extras["bg_brightness"])
mark = params["colors"]["mark"]
path = params["colors"]["path"]
accent = params["colors"]["accent"]
pulse = resolve_pulse(params, t, fallback_bpm=110, attack=0.14)
phase = resolve_beat_phase(params, t, fallback_bpm=110)
step = resolve_beat_index(params, t, fallback_bpm=110)
kick = band(params, "kick")
energy = band(params, "energy")
x_offset, ring, _ = ring_metrics(params, w)
glyphs = resolve_glyphs(params, default="mixed")
spacing = max(6, int(14 * (1.35 - density)))
scroll = int((step + phase) * spacing) % max(ring, 1)
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
grain = 0.35 + 0.65 * (0.5 + 0.5 * math.sin(gx * 0.21 + y * 1.4))
torch = (0.08 * pulse + 0.1 * kick) * (1.0 - abs(y - h / 2) / max(h / 2, 1))
set_pixel(surface, x, y, dim(path, (grain + torch + 0.05 * energy) * bg_b))
y0 = vcenter(h, 7)
for i, glyph in enumerate(glyphs):
gw = glyph_width(glyph)
gx0 = (i * spacing - scroll) % max(ring, 1)
for base in (gx0, gx0 - ring, gx0 + ring):
local = base - x_offset
if local + gw < 0 or local >= w:
continue
color = accent if (i + step) % 5 == 0 else mark
blit_glyph(surface, local, y0, glyph, dim(color, 0.65 + 0.35 * max(pulse, kick)))
head = int((step + phase) * (ring / 8.0)) % max(ring, 1)
local_head = head - x_offset
if 0 <= local_head < w:
for dy in range(h):
falloff = 1.0 - abs(dy - h / 2) / (h / 2 + 0.1)
set_pixel(
surface,
local_head,
dy,
dim(accent, (0.35 + 0.65 * max(pulse, energy)) * falloff),
)
if __name__ == "__main__":
run_cli(Procession)
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""Breathing solid pulse."""
from __future__ import annotations
import math
from leds.colors import dim
from leds.pattern import Color, Pattern, run_cli
class Pulse(Pattern):
id = "pulse"
label = "Pulse"
fps = 30
colors = {"color": Color("#7800b4")}
def draw(self, surface, t, params):
frame = t * self.fps
level = (math.sin(frame * 0.12) + 1) / 2
surface.fill(dim(params["colors"]["color"], 0.15 + level * 0.85))
if __name__ == "__main__":
run_cli(Pulse)
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Falling rain drops."""
from __future__ import annotations
import random
from leds.colors import dim
from leds.pattern import Color, Pattern, run_cli, set_pixel, size
class Rain(Pattern):
id = "rain"
label = "Rain"
fps = 30
colors = {"drop": Color("#00dc50")}
def __init__(self) -> None:
self._drops: dict[int, list[dict]] = {}
def draw(self, surface, t, params):
w, h = size(surface)
key = id(surface)
drops = self._drops.get(key)
if drops is None:
drops = [
{
"x": random.randint(0, w - 1),
"y": random.randint(-h, 0),
"speed": random.randint(1, 3),
}
for _ in range(max(w // 2, 1))
]
self._drops[key] = drops
surface.fill((0, 0, 0))
color = params["colors"]["drop"]
for drop in drops:
drop["y"] += drop["speed"]
if drop["y"] >= h:
drop["y"] = random.randint(-3, -1)
drop["x"] = random.randint(0, w - 1)
drop["speed"] = random.randint(1, 3)
x, y = drop["x"], int(drop["y"])
if 0 <= y < h:
set_pixel(surface, x, y, color)
if y > 0:
set_pixel(surface, x, y - 1, dim(color, 0.4))
if __name__ == "__main__":
run_cli(Rain)
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Rainbow scroll across the portal."""
from __future__ import annotations
from leds.colors import wheel
from leds.pattern import Pattern, is_matrix, run_cli
class Rainbow(Pattern):
id = "rainbow"
label = "Rainbow"
fps = 30
def draw(self, surface, t, params):
frame = int(t * self.fps)
if not is_matrix(surface):
n = len(surface)
for i in range(n):
surface[i] = wheel(frame * 2 + i * 256 // max(n, 1))
return
w, h = surface.width, surface.height
strip = getattr(surface, "strip", None)
if strip is not None and hasattr(surface, "index_at"):
for y in range(h):
for x in range(w):
strip[surface.index_at(x, y)] = wheel(x * 3 + y * 8 + frame * 2)
return
for y in range(h):
for x in range(w):
surface[x, y] = wheel(x * 3 + y * 8 + frame * 2)
if __name__ == "__main__":
run_cli(Rainbow)
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Temple rite: beat-locked rings with energy racing the portal perimeter."""
from __future__ import annotations
import math
from leds.bpm_limits import (
band,
resolve_beat_index,
resolve_beat_phase,
resolve_pulse,
)
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
class Rite(Pattern):
id = "rite"
label = "Rite"
fps = 32
colors = {
"gold": Color("#f0c060"),
"blood": Color("#6a1208"),
"void": Color("#060408"),
}
params = {
"intensity": Param(0.85, min=0.2, max=1.0, step=0.05, label="Intensity"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
intensity = float(extras["intensity"])
gold = params["colors"]["gold"]
blood = params["colors"]["blood"]
void = params["colors"]["void"]
pulse = resolve_pulse(params, t, fallback_bpm=120, attack=0.18)
phase = resolve_beat_phase(params, t, fallback_bpm=120)
step = resolve_beat_index(params, t, fallback_bpm=120)
kick = band(params, "kick")
bass = band(params, "bass")
energy = band(params, "energy")
x_offset, ring, panel = ring_metrics(params, w)
cx = (w - 1) / 2.0
cy = (h - 1) / 2.0
radius = phase * (max(w, h) * 0.85)
hit = max(pulse, kick, bass * 0.7)
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
dist = math.hypot(x - cx, y - cy)
ring_glow = math.exp(-((dist - radius) ** 2) / 2.8)
floor = 0.12 + 0.1 * math.sin(gx * 0.08 + t * 0.7) + 0.08 * energy
base = dim(void, floor)
warm = dim(
blood,
(0.25 + 0.55 * hit) * intensity * (1.0 - dist / (max(w, h))),
)
rim = dim(gold, ring_glow * intensity * (0.4 + 0.6 * hit))
set_pixel(
surface,
x,
y,
(
min(255, base[0] + warm[0] + rim[0]),
min(255, base[1] + warm[1] + rim[1]),
min(255, base[2] + warm[2] + rim[2]),
),
)
courier = int(((step + phase) / 8.0) * ring) % max(ring, 1)
for dx in range(-2, 3):
gx = (courier + dx) % max(ring, 1)
local = gx - x_offset
if not (0 <= local < w):
continue
level = (1.0 - abs(dx) / 3.0) * (0.45 + 0.55 * hit) * intensity
for y in range(h):
edge = 1.0 - abs(y - cy) / (cy + 0.5)
set_pixel(surface, local, y, dim(gold, level * edge))
face = step % 5
if panel == face:
for x in range(w):
set_pixel(surface, x, 0, dim(gold, 0.2 + 0.4 * hit))
set_pixel(surface, x, h - 1, dim(gold, 0.15 + 0.35 * hit))
if __name__ == "__main__":
run_cli(Rite)
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Full-height band rolling left to right."""
from __future__ import annotations
from leds.colors import dim, wheel
from leds.pattern import Pattern, run_cli, set_pixel, size
class Rolling(Pattern):
id = "rolling"
label = "Rolling"
fps = 30
def draw(self, surface, t, params):
w, h = size(surface)
frame = int(t * self.fps)
tail = 10
head = frame % (w + tail)
surface.fill((0, 0, 0))
for y in range(h):
hue_shift = y * 18 + frame * 3
for i in range(tail):
x = head - i
if 0 <= x < w:
set_pixel(surface, x, y, dim(wheel(hue_shift + i * 8), (tail - i) / tail))
if __name__ == "__main__":
run_cli(Rolling)
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Elder Futhark stave drifting continuously around the portal ring."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_pulse, resolve_scroll
from leds.colors import dim
from leds.font import blit_sequence, font_param, resolve_glyphs, sequence_width, vcenter
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
class Runes(Pattern):
id = "runes"
label = "Runes"
fps = 28
colors = {
"carve": Color("#7ec8e8"),
"wood": Color("#120c08"),
}
params = {
"font": font_param("runes"),
"text_brightness": Param(1.0, min=0.0, max=1.0, step=0.05, label="Text brightness"),
"bg_brightness": Param(0.55, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
pulse = resolve_pulse(params, t, fallback_bpm=100, attack=0.16)
hat = band(params, "hat")
energy = band(params, "energy")
text_b = float(extras["text_brightness"])
bg_b = float(extras["bg_brightness"])
carve = dim(params["colors"]["carve"], text_b * (0.85 + 0.15 * max(pulse, hat)))
wood = params["colors"]["wood"]
x_offset, ring, _ = ring_metrics(params, w)
glyphs = resolve_glyphs(params, default="runes")
gap = 2
span = sequence_width(glyphs, gap=gap)
loop = max(span + gap, ring)
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
grain = 0.4 + 0.6 * (0.5 + 0.5 * math.sin(gx * 0.9 + y * 2.2))
frost = (0.04 + 0.08 * energy) * bg_b
bg = dim(wood, grain * bg_b)
set_pixel(
surface,
x,
y,
(
min(255, bg[0] + int(frost * 40)),
min(255, bg[1] + int(frost * 70)),
min(255, bg[2] + int(frost * 90)),
),
)
scroll = resolve_scroll(
params,
t,
fallback_bpm=100,
pixels_per_beat=max(2.0, ring / 10.0),
loop=loop,
)
origin = -scroll - x_offset
y0 = vcenter(h, 7)
blit_sequence(surface, origin, y0, glyphs, dim(carve, 0.9 + 0.1 * pulse), gap=gap, wrap=loop)
for i in range(3):
gx = (scroll * 3 + i * 17 + x_offset) % max(ring, 1)
local = gx - x_offset
if not (0 <= local < w):
continue
gy = (i * 3 + scroll // 5) % max(h, 1)
if (gx * 13 + gy * 29 + scroll) % 11 == 0:
set_pixel(surface, local, gy, dim((220, 240, 255), text_b))
if __name__ == "__main__":
run_cli(Runes)
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Cylon / scanner beam."""
from __future__ import annotations
from leds.colors import dim
from leds.pattern import Color, Pattern, run_cli, set_pixel, size
class Scanner(Pattern):
id = "scanner"
label = "Scanner"
fps = 35
colors = {"beam": Color("#ff0000")}
def draw(self, surface, t, params):
w, h = size(surface)
frame = int(t * self.fps)
pos = frame % max(w * 2 - 2, 1)
if pos >= w:
pos = w * 2 - 2 - pos
surface.fill((0, 0, 0))
color = params["colors"]["beam"]
for y in range(h):
for dx in range(-3, 4):
x = pos + dx
if 0 <= x < w:
set_pixel(surface, x, y, dim(color, 1.0 - abs(dx) / 4.0))
if __name__ == "__main__":
run_cli(Scanner)
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""A serpent coiling around the full portal ring."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_pulse
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
class Serpent(Pattern):
id = "serpent"
label = "Serpent"
fps = 30
colors = {
"scale": Color("#2ecf6a"),
"belly": Color("#d4c45a"),
"eye": Color("#ff4020"),
"void": Color("#06080a"),
}
params = {
"length": Param(0.35, min=0.15, max=0.7, step=0.05, label="Length"),
"bg_brightness": Param(0.35, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
length = float(extras["length"])
bg_b = float(extras["bg_brightness"])
scale = params["colors"]["scale"]
belly = params["colors"]["belly"]
eye = params["colors"]["eye"]
void = params["colors"]["void"]
pulse = resolve_pulse(params, t, fallback_bpm=104, attack=0.14)
energy = band(params, "energy")
x_offset, ring, _ = ring_metrics(params, w)
body = max(8, int(ring * length))
head = int(t * (ring * 0.22)) % max(ring, 1)
mid = (h - 1) / 2.0
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
grain = 0.3 + 0.2 * math.sin(gx * 0.11 + y)
set_pixel(surface, x, y, dim(void, (grain + 0.1 * energy) * bg_b))
for i in range(body):
gx = (head - i) % max(ring, 1)
local = gx - x_offset
if not (0 <= local < w):
continue
wave = math.sin((head - i) * 0.28 + t * 3.2) * (1.6 + 0.8 * pulse)
py = int(mid + wave)
fade = 1.0 - i / body
color = eye if i < 2 else (belly if i % 5 == 0 else scale)
level = fade * (0.55 + 0.45 * (pulse if i < 4 else 1.0))
for dy in range(-1, 2):
yy = py + dy
if 0 <= yy < h:
thick = 1.0 if dy == 0 else 0.45
set_pixel(surface, local, yy, dim(color, level * thick))
if __name__ == "__main__":
run_cli(Serpent)
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Solid fill cycling through declared colours."""
from __future__ import annotations
from leds.pattern import Color, Pattern, run_cli
class Solid(Pattern):
id = "solid"
label = "Solid"
fps = 1
colors = {
"a": Color("#ff0000"),
"b": Color("#00ff00"),
"c": Color("#0000ff"),
}
def draw(self, surface, t, params):
palette = list(params["colors"].values()) or [(255, 0, 0)]
frame = int(t * self.fps)
surface.fill(palette[frame % len(palette)])
if __name__ == "__main__":
run_cli(Solid)
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Twinkling sparkles."""
from __future__ import annotations
import random
from leds.pattern import Color, Pattern, each_pixel, is_matrix, run_cli, set_pixel, size
class Sparkle(Pattern):
id = "sparkle"
label = "Sparkle"
fps = 40
colors = {"spark": Color("#dcdcff")}
def draw(self, surface, t, params):
for x, y in each_pixel(surface):
if is_matrix(surface):
r, g, b = surface[x, y]
else:
r, g, b = surface[x]
set_pixel(surface, x, y, (r >> 2, g >> 2, b >> 2))
w, h = size(surface)
color = params["colors"]["spark"]
for _ in range(6):
set_pixel(surface, random.randint(0, w - 1), random.randint(0, h - 1), color)
if __name__ == "__main__":
run_cli(Sparkle)
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Radar-style spinning line from panel centre."""
from __future__ import annotations
import math
from leds.colors import dim, wheel
from leds.pattern import Pattern, is_matrix, iter_line, run_cli, set_pixel, size
class SpinLine(Pattern):
id = "spin_line"
label = "Spin line"
fps = 28
def draw(self, surface, t, params):
w, h = size(surface)
frame = int(t * self.fps)
surface.fill((0, 0, 0))
if not is_matrix(surface):
n = len(surface)
pos = int((frame * 3) % max(n, 1))
for i in range(8):
idx = (pos - i) % n
surface[idx] = dim(wheel(frame * 5 + i * 20), (8 - i) / 8)
return
cx = (w - 1) / 2.0
cy = (h - 1) / 2.0
angle = frame * 0.14
reach = max(w, h) * 1.2
x1 = int(cx + math.cos(angle) * reach)
y1 = int(cy + math.sin(angle) * reach)
color = wheel(frame * 4)
for x, y in iter_line(int(cx), int(cy), x1, y1):
if 0 <= x < w and 0 <= y < h:
set_pixel(surface, x, y, color)
hub_x, hub_y = int(cx), int(cy)
if 0 <= hub_x < w and 0 <= hub_y < h:
set_pixel(surface, hub_x, hub_y, (255, 255, 255))
if __name__ == "__main__":
run_cli(SpinLine)
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Scrolling colour bands."""
from __future__ import annotations
from leds.pattern import Color, Pattern, run_cli, set_pixel, size
class Stripes(Pattern):
id = "stripes"
label = "Stripes"
fps = 25
colors = {
"a": Color("#c80000"),
"b": Color("#00c800"),
"c": Color("#0000c8"),
}
def draw(self, surface, t, params):
w, h = size(surface)
frame = int(t * self.fps)
offset = frame % max(w, 1)
palette = [
params["colors"]["a"],
params["colors"]["b"],
params["colors"]["c"],
]
for y in range(h):
for x in range(w):
band = (x + offset) // 3 % 3
set_pixel(surface, x, y, palette[band])
if __name__ == "__main__":
run_cli(Stripes)
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Rising and falling tide with a foaming crest."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_beat_wave, resolve_pulse
from leds.colors import blend, dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
class Tide(Pattern):
id = "tide"
label = "Tide"
fps = 28
colors = {
"water": Color("#1a6ad0"),
"foam": Color("#d8f0ff"),
"sand": Color("#1a140c"),
}
params = {
"depth": Param(0.65, min=0.25, max=1.0, step=0.05, label="Depth"),
"bg_brightness": Param(0.4, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
depth = float(extras["depth"])
bg_b = float(extras["bg_brightness"])
water = params["colors"]["water"]
foam = params["colors"]["foam"]
sand = params["colors"]["sand"]
pulse = resolve_pulse(params, t, fallback_bpm=84, attack=0.2)
wave = resolve_beat_wave(params, t, fallback_bpm=84)
energy = band(params, "energy")
_, ring, _ = ring_metrics(params, w)
mean = (h - 1) * (0.25 + 0.55 * (1.0 - depth) + 0.12 * wave)
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
crest = mean + 1.4 * math.sin(gx * (math.tau / max(ring, 1)) * 3.0 + t * 1.8)
crest += 0.6 * math.sin(gx * 0.21 - t * 2.4)
below = y - crest
if below >= 0:
mix = min(1.0, 0.35 + 0.65 * (below / max(h, 1)) + 0.15 * pulse)
shade = blend(water, foam, 0.08 + 0.2 * energy)
if below < 1.2:
set_pixel(surface, x, y, dim(foam, 0.55 + 0.45 * pulse))
else:
set_pixel(surface, x, y, dim(shade, mix))
else:
grain = 0.35 + 0.2 * math.sin(gx * 0.17 + y)
set_pixel(surface, x, y, dim(sand, grain * bg_b))
if __name__ == "__main__":
run_cli(Tide)
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Mist veil flowing around the portal, driven by live beat and FFT energy."""
from __future__ import annotations
import math
from leds.bpm_limits import (
band,
resolve_bpm,
resolve_beat_wave,
resolve_pulse,
)
from leds.colors import dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
from leds.portal_span import global_x, ring_metrics
class Veil(Pattern):
id = "veil"
label = "Veil"
fps = 28
colors = {
"mist": Color("#9bb7d8"),
"gold": Color("#c9a24a"),
"shadow": Color("#0a0810"),
}
params = {
"flow": Param(0.65, min=0.15, max=1.0, step=0.05, label="Flow"),
"bg_brightness": Param(0.5, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
bpm = resolve_bpm(params, fallback=90)
flow = float(extras["flow"])
bg_b = float(extras["bg_brightness"])
mist = params["colors"]["mist"]
gold = params["colors"]["gold"]
shadow = params["colors"]["shadow"]
pulse = resolve_pulse(params, t, fallback_bpm=90, attack=0.2)
wave = resolve_beat_wave(params, t, fallback_bpm=90)
energy = band(params, "energy")
flux = band(params, "flux")
hat = band(params, "hat")
_, ring, _ = ring_metrics(params, w)
drift = t * (ring * (bpm / 60.0) / 8.0) * flow
for y in range(h):
for x in range(w):
gx = global_x(x, params, w)
a = math.sin((gx + drift) * 0.07 + y * 0.55)
b = math.sin((gx - drift * 0.6) * 0.13 - y * 0.9 + t * 0.8)
c = math.sin((gx + drift * 0.3) * 0.03 + t * 1.1)
field = 0.5 + 0.5 * (0.5 * a + 0.35 * b + 0.15 * c)
field = min(
1.0,
field + 0.22 * pulse * wave + 0.2 * energy + 0.12 * flux,
)
edge = 1.0 - abs((y / max(h - 1, 1)) - 0.5) * 0.55
base = dim(shadow, bg_b * (0.35 + 0.25 * field))
fog = dim(mist, field * edge * (0.35 + 0.45 * flow + 0.15 * hat))
gild = dim(gold, field * field * (0.1 + 0.3 * pulse) * edge)
set_pixel(
surface,
x,
y,
(
min(255, base[0] + fog[0] + gild[0]),
min(255, base[1] + fog[1] + gild[1]),
min(255, base[2] + fog[2] + gild[2]),
),
)
if __name__ == "__main__":
run_cli(Veil)
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Spinning portal vortex with a gold rim."""
from __future__ import annotations
import math
from leds.bpm_limits import band, resolve_pulse
from leds.colors import blend, dim
from leds.pattern import Color, Param, Pattern, run_cli, set_pixel, size
class Vortex(Pattern):
id = "vortex"
label = "Vortex"
fps = 30
colors = {
"core": Color("#6a20ff"),
"rim": Color("#f0c060"),
"void": Color("#05030a"),
}
params = {
"twist": Param(0.75, min=0.2, max=1.0, step=0.05, label="Twist"),
"bg_brightness": Param(0.4, min=0.0, max=1.0, step=0.05, label="Background"),
}
def draw(self, surface, t, params):
w, h = size(surface)
extras = params["extras"]
twist = float(extras["twist"])
bg_b = float(extras["bg_brightness"])
core = params["colors"]["core"]
rim = params["colors"]["rim"]
void = params["colors"]["void"]
pulse = resolve_pulse(params, t, fallback_bpm=112, attack=0.16)
energy = band(params, "energy")
kick = band(params, "kick")
cx = (w - 1) / 2.0
cy = (h - 1) / 2.0
reach = math.hypot(cx, cy) + 0.5
spin = t * (1.4 + 2.2 * twist)
for y in range(h):
for x in range(w):
dx = x - cx
dy = y - cy
dist = math.hypot(dx, dy) / max(reach, 1.0)
angle = math.atan2(dy, dx) + spin + dist * (3.0 + 4.0 * twist)
arm = 0.5 + 0.5 * math.sin(angle * 3.0)
hole = max(0.0, 1.0 - dist * (1.15 - 0.25 * pulse))
band_glow = math.exp(-((dist - (0.55 + 0.2 * pulse)) ** 2) / 0.08)
mix = blend(void, core, min(1.0, arm * hole + 0.2 * energy))
gold = dim(rim, band_glow * (0.45 + 0.55 * max(pulse, kick)))
base = dim(mix, (0.35 + 0.65 * hole) * (0.55 + 0.45 * bg_b))
set_pixel(
surface,
x,
y,
(
min(255, base[0] + gold[0]),
min(255, base[1] + gold[1]),
min(255, base[2] + gold[2]),
),
)
if __name__ == "__main__":
run_cli(Vortex)
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Sine-wave field."""
from __future__ import annotations
import math
from leds.colors import dim
from leds.pattern import Color, Pattern, run_cli, set_pixel, size
class Wave(Pattern):
id = "wave"
label = "Wave"
fps = 25
colors = {"color": Color("#0050c8")}
def draw(self, surface, t, params):
w, h = size(surface)
frame = t * self.fps
color = params["colors"]["color"]
for y in range(h):
for x in range(w):
wave = math.sin(x * 0.25 + frame * 0.15) * math.cos(y * 0.35 - frame * 0.1)
set_pixel(surface, x, y, dim(color, (wave + 1) / 2))
if __name__ == "__main__":
run_cli(Wave)