277 lines
8.0 KiB
Python
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.",
|
|
),
|
|
}
|