Files
RFP_Infinity-Vis/Infinity_Vis_1/infinity_vis_1/patterns.py

258 lines
11 KiB
Python

from __future__ import annotations
import math
import random
from dataclasses import dataclass
from .colors import mix, palette_color, rgb_from_hex, scale
from .models import MappingSpec, PatternParams, RGB
@dataclass(slots=True)
class TilePoint:
row: int
col: int
u: float
v: float
index: int
@property
def tile_key(self) -> str:
return f"{self.row}:{self.col}"
def pattern_ids() -> list[str]:
return sorted(PATTERN_REGISTRY)
def render_pattern(mapping: MappingSpec, pattern_id: str, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
points = _tile_points(mapping)
renderer = PATTERN_REGISTRY.get(pattern_id, _render_solid)
return renderer(points, mapping, params, time_s, tempo_bpm)
def utility_pattern(mapping: MappingSpec, utility_mode: str, selected_tile_id: str | None, time_s: float) -> list[RGB]:
points = _tile_points(mapping)
ordered = mapping.ordered_tiles()
if utility_mode == "blackout":
return [(0, 0, 0) for _ in points]
if utility_mode == "identify":
flash = _wave(time_s * 8.0)
color = (255, 255, 255) if flash > 0.5 else (0, 0, 0)
return [color for _ in points]
if utility_mode == "single_tile":
return [(255, 255, 255) if ordered[index].tile_id == selected_tile_id else (0, 0, 0) for index, _point in enumerate(points)]
return [(0, 0, 0) for _ in points]
def _tile_points(mapping: MappingSpec) -> list[TilePoint]:
rows = max(1, mapping.rows - 1)
cols = max(1, mapping.cols - 1)
points: list[TilePoint] = []
for index, tile in enumerate(mapping.ordered_tiles()):
points.append(
TilePoint(
row=tile.row,
col=tile.col,
u=(tile.col - 1) / cols,
v=(tile.row - 1) / rows,
index=index,
)
)
return points
def _primary_secondary(params: PatternParams, phase: float = 0.5) -> tuple[RGB, RGB]:
if params.color_mode == "palette":
return palette_color(params.palette, phase), palette_color(params.palette, (phase + 0.33) % 1.0)
return rgb_from_hex(params.primary_color), rgb_from_hex(params.secondary_color)
def _wave(value: float) -> float:
return 0.5 + 0.5 * math.sin(value)
def _axis_projection(point: TilePoint, angle_deg: float) -> float:
angle = math.radians(angle_deg % 360.0)
x = point.u - 0.5
y = point.v - 0.5
return (x * math.cos(angle)) + (y * math.sin(angle))
def _direction_progress(point: TilePoint, direction: str) -> float:
if direction == "right_to_left":
return 1.0 - point.u
if direction == "bottom_to_top":
return 1.0 - point.v
if direction == "top_to_bottom":
return point.v
return point.u
def _render_solid(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
primary, _secondary = _primary_secondary(params, 0.55)
return [scale(primary, params.brightness) for _ in points]
def _render_checker(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
return [scale(a if (point.row + point.col) % 2 == 0 else b, params.brightness) for point in points]
def _render_row_gradient(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
return [scale(mix(a, b, point.v), params.brightness) for point in points]
def _render_column_gradient(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
return [scale(mix(a, b, point.u), params.brightness) for point in points]
def _render_breathing(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, _b = _primary_secondary(params, 0.4)
pulse = 0.15 + 0.85 * _wave(time_s * (tempo_bpm / 60.0) * params.tempo_multiplier)
return [scale(a, params.brightness * pulse) for _ in points]
def _render_center_pulse(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
pulse = _wave(time_s * (tempo_bpm / 60.0) * params.tempo_multiplier)
colors: list[RGB] = []
for point in points:
dx = point.u - 0.5
dy = point.v - 0.5
dist = math.sqrt(dx * dx + dy * dy) * 1.5
strength = max(0.0, 1.0 - abs(dist - pulse))
colors.append(scale(mix(b, a, strength), params.brightness))
return colors
def _render_sparkle(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
rng = random.Random(int(time_s * 120.0))
return [scale(a if rng.random() < (0.15 + params.randomness * 0.35) else b, params.brightness) for _ in points]
def _render_wave_line(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
phase = time_s * (tempo_bpm / 60.0) * params.tempo_multiplier
colors: list[RGB] = []
for point in points:
value = _wave((point.u * math.pi * 4.0) + phase * math.pi * 2.0 + point.v * 1.2)
colors.append(scale(mix(b, a, value), params.brightness))
return colors
def _render_scan(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
position = ((time_s * (tempo_bpm / 60.0) * params.tempo_multiplier) % 1.0) * 2.0 - 1.0
band = max(0.08, min(1.2, params.on_width * 0.35))
colors: list[RGB] = []
for point in points:
projection = _axis_projection(point, params.angle)
active = abs(projection - position) <= band
colors.append(scale(a if active else b, params.brightness))
return colors
def _render_arrow(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
head = (time_s * (tempo_bpm / 60.0) * params.tempo_multiplier) % 1.0
colors: list[RGB] = []
for point in points:
progress = _direction_progress(point, params.direction)
width = 0.15 + 0.25 * (1.0 - abs(point.v - 0.5) * 2.0)
colors.append(scale(a if abs(progress - head) < width else b, params.brightness))
return colors
def _render_scan_dual(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
progress = (time_s * (tempo_bpm / 60.0) * params.tempo_multiplier) % 1.0
colors: list[RGB] = []
for point in points:
axis = _direction_progress(point, params.direction)
active = abs(axis - progress) < 0.2 or abs(axis - (1.0 - progress)) < 0.2
colors.append(scale(a if active else b, params.brightness))
return colors
def _render_sweep(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
progress = (time_s * (tempo_bpm / 60.0) * params.tempo_multiplier) % 1.0
return [scale(a if _direction_progress(point, params.direction) <= progress else b, params.brightness) for point in points]
def _render_saw(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
shift = (time_s * (tempo_bpm / 60.0) * params.tempo_multiplier) % 1.0
return [scale(mix(b, a, (_direction_progress(point, params.direction) + shift) % 1.0), params.brightness) for point in points]
def _render_two_dots(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
p1 = (time_s * (tempo_bpm / 60.0) * params.tempo_multiplier) % 1.0
p2 = (p1 + 0.5) % 1.0
colors: list[RGB] = []
for point in points:
axis = _direction_progress(point, params.direction)
active = abs(axis - p1) < 0.12 or abs(axis - p2) < 0.12
colors.append(scale(a if active else b, params.brightness))
return colors
def _render_strobe(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
phase = (time_s * (tempo_bpm / 60.0) * params.tempo_multiplier) % 1.0
on = phase < max(0.02, min(0.98, params.strobe_duty_cycle))
if params.strobe_mode == "global":
return [scale(a if on else b, params.brightness) for _ in points]
rng = random.Random(int(time_s * 80.0))
result: list[RGB] = []
for point in points:
local_on = on
if params.strobe_mode in {"random_pixels", "random_leds"}:
local_on = rng.random() > 0.5
elif params.strobe_mode == "checker":
local_on = on and ((point.row + point.col) % 2 == 0)
result.append(scale(a if local_on else b, params.brightness))
return result
def _render_stopwatch(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
highlight = int(time_s) % max(1, len(points))
return [scale(a if point.index == highlight else b, params.brightness) for point in points]
def _render_snake(points: list[TilePoint], mapping: MappingSpec, params: PatternParams, time_s: float, tempo_bpm: float) -> list[RGB]:
a, b = _primary_secondary(params)
order = sorted(points, key=lambda point: (point.row, point.col if point.row % 2 else -point.col))
head = int(time_s * (tempo_bpm / 60.0) * params.tempo_multiplier * max(1.0, params.step_size)) % len(order)
tail = max(1, int(params.block_size))
active = {order[(head - offset) % len(order)].tile_key for offset in range(tail)}
return [scale(a if point.tile_key in active else b, params.brightness) for point in points]
PATTERN_REGISTRY = {
"arrow": _render_arrow,
"breathing": _render_breathing,
"center_pulse": _render_center_pulse,
"checker": _render_checker,
"column_gradient": _render_column_gradient,
"row_gradient": _render_row_gradient,
"saw": _render_saw,
"scan": _render_scan,
"scan_dual": _render_scan_dual,
"snake": _render_snake,
"solid": _render_solid,
"sparkle": _render_sparkle,
"stopwatch": _render_stopwatch,
"strobe": _render_strobe,
"sweep": _render_sweep,
"two_dots": _render_two_dots,
"wave_line": _render_wave_line,
}