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:
@@ -0,0 +1 @@
|
||||
"""Standalone LED patterns. Each module defines one Pattern subclass."""
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user