Files
RFP_Infinity-Vis/app/patterns/base.py

277 lines
8.0 KiB
Python

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Iterable
from app.config.models import InfinityMirrorConfig, TileConfig
from app.core.types import PatternParameters, TilePatternSample, clamp
@dataclass(frozen=True)
class ParameterSpec:
key: str
label: str
kind: str
minimum: float = 0.0
maximum: float = 1.0
step: float = 0.01
reset_value: float | None = None
options: tuple[tuple[str, str], ...] = ()
tooltip: str = ""
@dataclass(frozen=True)
class PatternDescriptor:
pattern_id: str
display_name: str
description: str
supported_parameters: tuple[str, ...]
accent_hex: str = "#4D7CFF"
temporal_profile: str = "smooth"
@dataclass
class PatternContext:
config: InfinityMirrorConfig
params: PatternParameters
time_s: float
tempo_bpm: float = 60.0
tempo_phase: float = 0.0
@property
def rows(self) -> int:
return self.config.logical_display.rows
@property
def cols(self) -> int:
return self.config.logical_display.cols
def sorted_tiles(self) -> list[TileConfig]:
return self.config.sorted_tiles()
@property
def tempo_hz(self) -> float:
return max(0.05, float(self.tempo_bpm) / 60.0)
@property
def tempo_multiplier(self) -> float:
return clamp(float(self.params.tempo_multiplier), 0.25, 8.0)
@property
def pattern_tempo_hz(self) -> float:
return self.tempo_hz * self.tempo_multiplier
@property
def pattern_tempo_phase(self) -> float:
return self.tempo_phase * self.tempo_multiplier
class BasePattern(ABC):
descriptor: PatternDescriptor
@abstractmethod
def render(self, context: PatternContext) -> dict[str, TilePatternSample]:
raise NotImplementedError
class PatternRegistry:
def __init__(self, patterns: Iterable[BasePattern]) -> None:
self._patterns = {pattern.descriptor.pattern_id: pattern for pattern in patterns}
def get(self, pattern_id: str) -> BasePattern:
return self._patterns[pattern_id]
def descriptors(self) -> list[PatternDescriptor]:
return [self._patterns[key].descriptor for key in sorted(self._patterns)]
def ids(self) -> list[str]:
return list(sorted(self._patterns))
COMMON_PARAMETER_SPECS: dict[str, ParameterSpec] = {
"brightness": ParameterSpec("brightness", "Brightness", "slider", 0.0, 2.0, 0.01, reset_value=1.0, tooltip="Pattern output level."),
"fade": ParameterSpec("fade", "Smoothing", "slider", 0.0, 1.0, 0.01, reset_value=0.0, tooltip="Higher values create softer transitions."),
"tempo_multiplier": ParameterSpec(
"tempo_multiplier",
"Tempo Multiplier",
"slider",
0.25,
8.0,
0.05,
reset_value=1.0,
tooltip="Scales this pattern relative to the global BPM.",
),
"direction": ParameterSpec(
"direction",
"Direction",
"combo",
options=(
("left_to_right", "Left to Right"),
("right_to_left", "Right to Left"),
("top_to_bottom", "Top to Bottom"),
("bottom_to_top", "Bottom to Top"),
("outward", "Outward"),
("inward", "Inward"),
),
tooltip="Primary motion direction.",
),
"checker_mode": ParameterSpec(
"checker_mode",
"Checker Mode",
"combo",
options=(
("classic", "Classic"),
("diagonal", "Diagonal Split"),
("checkerd", "Checkerd"),
),
tooltip="Classic checker, diagonal half-pixels, or diagonal flip animation.",
),
"scan_style": ParameterSpec(
"scan_style",
"Scan Style",
"combo",
options=(
("line", "Line"),
("bands", "Bands"),
),
tooltip="Single moving scan band or repeating band pattern.",
),
"angle": ParameterSpec(
"angle",
"Angle",
"angle",
minimum=0.0,
maximum=315.0,
step=45.0,
tooltip="Scan direction in 45 degree steps.",
),
"on_width": ParameterSpec(
"on_width",
"On Width",
"slider",
0.1,
2.0,
0.05,
tooltip="Length of the active scan window.",
),
"off_width": ParameterSpec(
"off_width",
"Off Width",
"slider",
0.1,
2.0,
0.05,
tooltip="Gap between active scan windows.",
),
"band_thickness": ParameterSpec(
"band_thickness",
"Band Thickness",
"slider",
0.1,
2.0,
0.05,
tooltip="Visible thickness of the lit band inside the active window.",
),
"flip_horizontal": ParameterSpec(
"flip_horizontal",
"Flip Horizontal",
"checkbox",
tooltip="Mirror scan evaluation left-to-right for installation alignment.",
),
"flip_vertical": ParameterSpec(
"flip_vertical",
"Flip Vertical",
"checkbox",
tooltip="Mirror scan evaluation top-to-bottom for installation alignment.",
),
"strobe_mode": ParameterSpec(
"strobe_mode",
"Strobe Mode",
"combo",
options=(
("global", "Global"),
("random_pixels", "Random Pixels"),
("random_leds", "Random LEDs"),
),
tooltip="Whole-wall strobe, grouped random pixel blocks, or fully shuffled per-LED timing.",
),
"stopwatch_mode": ParameterSpec(
"stopwatch_mode",
"Stopwatch Mode",
"combo",
options=(
("sync", "Sync"),
("random", "Random"),
),
tooltip="Run all tiles together or with deterministic random offsets.",
),
"color_mode": ParameterSpec(
"color_mode",
"Color Mode",
"combo",
options=(
("dual", "Dual"),
("palette", "Palette"),
("mono", "Mono"),
("complementary", "Complementary"),
("random_colors", "Random Colors"),
("custom_random", "Custom Random"),
),
tooltip="How colors are chosen for the pattern.",
),
"primary_color": ParameterSpec("primary_color", "Primary Color", "color", tooltip="Main color."),
"secondary_color": ParameterSpec("secondary_color", "Secondary Color", "color", tooltip="Secondary color."),
"palette": ParameterSpec("palette", "Palette", "combo", tooltip="Palette for palette-driven patterns."),
"symmetry": ParameterSpec(
"symmetry",
"Mirror",
"combo",
options=(("none", "None"), ("horizontal", "Horizontal"), ("vertical", "Vertical"), ("both", "Both")),
tooltip="Mirrors pattern coordinates around the center.",
),
"center_pulse_mode": ParameterSpec(
"center_pulse_mode",
"Pulse Mode",
"combo",
options=(
("expand", "Expand"),
("reverse", "Reverse"),
("outline", "Outline"),
("outline_reverse", "Outline Reverse"),
),
tooltip="Expand from the center, run inward, or use only the rectangular outline rings.",
),
"block_size": ParameterSpec("block_size", "Block Size", "slider", 0.1, 6.0, 0.1, tooltip="Width of active bands."),
"pixel_group_size": ParameterSpec(
"pixel_group_size",
"Pixel Group",
"slider",
1.0,
5.0,
1.0,
reset_value=1.0,
tooltip="Treat several adjacent LEDs as one strobe pixel.",
),
"strobe_duty_cycle": ParameterSpec(
"strobe_duty_cycle",
"Duty / Density",
"slider",
0.005,
0.98,
0.005,
reset_value=0.5,
tooltip="Controls strobe on-time or sparkle fill density depending on the pattern.",
),
"randomness": ParameterSpec(
"randomness",
"Randomness",
"slider",
0.0,
1.5,
0.01,
reset_value=0.35,
tooltip="Controls variation in patterns that intentionally use randomness.",
),
}