213 lines
6.6 KiB
Python
213 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from app.core.colors import (
|
|
brighten,
|
|
choose_pair,
|
|
custom_random_color_choices,
|
|
label_contrast,
|
|
sample_random_effect_color,
|
|
)
|
|
from app.core.types import RGBColor, TilePatternSample, clamp
|
|
|
|
from ..base import PatternContext
|
|
|
|
|
|
def _mirror_position(position: float, enabled: bool) -> float:
|
|
return min(position, 1.0 - position) * 2.0 if enabled else position
|
|
|
|
|
|
def _directional_amount(context: PatternContext, row_index: int, col_index: int) -> float:
|
|
rows = max(1, context.rows - 1)
|
|
cols = max(1, context.cols - 1)
|
|
row_position = row_index / rows
|
|
col_position = col_index / cols
|
|
direction = context.params.direction
|
|
|
|
if direction == "right_to_left":
|
|
return 1.0 - col_position
|
|
if direction == "top_to_bottom":
|
|
return row_position
|
|
if direction == "bottom_to_top":
|
|
return 1.0 - row_position
|
|
if direction == "outward":
|
|
return abs(col_position - 0.5) * 2.0
|
|
if direction == "inward":
|
|
return 1.0 - abs(col_position - 0.5) * 2.0
|
|
return col_position
|
|
|
|
|
|
def _random_color_pair(context: PatternContext, seed: float) -> tuple[RGBColor, RGBColor]:
|
|
primary = _random_vivid_color(seed)
|
|
secondary = primary.scaled(0.08)
|
|
return primary, secondary
|
|
|
|
|
|
def _custom_random_color_pair(context: PatternContext, seed: float) -> tuple[RGBColor, RGBColor]:
|
|
choices = custom_random_color_choices(context.params.primary_color, context.params.secondary_color)
|
|
primary = choices[int(_temporal_noise(seed) * len(choices)) % len(choices)]
|
|
secondary = primary.scaled(0.08)
|
|
return primary, secondary
|
|
|
|
|
|
def _spatial_color_seed(amount: float, row_index: int, col_index: int) -> float:
|
|
return (
|
|
amount * 7.31
|
|
+ (row_index + 1) * 0.613
|
|
+ (col_index + 1) * 1.137
|
|
+ (row_index + 1) * (col_index + 1) * 0.071
|
|
)
|
|
|
|
|
|
def _sample_for_tile(
|
|
context: PatternContext,
|
|
amount: float,
|
|
row_index: int | None = None,
|
|
col_index: int | None = None,
|
|
) -> tuple[RGBColor, RGBColor]:
|
|
if context.params.color_mode == "random_colors":
|
|
return _random_color_pair(context, _spatial_color_seed(amount, row_index or 0, col_index or 0))
|
|
if context.params.color_mode == "custom_random":
|
|
return _custom_random_color_pair(context, _spatial_color_seed(amount, row_index or 0, col_index or 0))
|
|
return choose_pair(
|
|
context.params.color_mode,
|
|
context.params.primary_color,
|
|
context.params.secondary_color,
|
|
context.params.palette,
|
|
amount,
|
|
)
|
|
|
|
|
|
def _sample_for_cycle(context: PatternContext, amount: float, seed: float) -> tuple[RGBColor, RGBColor]:
|
|
if context.params.color_mode == "random_colors":
|
|
return _random_color_pair(context, seed + amount * 3.1)
|
|
if context.params.color_mode == "custom_random":
|
|
return _custom_random_color_pair(context, seed + amount * 3.1)
|
|
return _sample_for_tile(context, amount)
|
|
|
|
|
|
def _blend_colors(primary: RGBColor, secondary: RGBColor, amount: float, floor: float = 0.0) -> RGBColor:
|
|
floor = clamp(floor)
|
|
return secondary.mix(primary, floor + (1.0 - floor) * clamp(amount))
|
|
|
|
|
|
def _tile_sample(fill: RGBColor, accent: RGBColor, intensity: float = 1.0, boost: float = 0.1) -> TilePatternSample:
|
|
intensity = clamp(intensity)
|
|
if intensity <= 0.0 and fill.to_8bit_tuple() == (0, 0, 0):
|
|
glow = RGBColor.black()
|
|
rim = RGBColor.black()
|
|
else:
|
|
glow = brighten(fill, boost)
|
|
rim = fill.mix(accent, 0.24)
|
|
return TilePatternSample(
|
|
fill_color=fill,
|
|
glow_color=glow,
|
|
rim_color=rim,
|
|
label_color=label_contrast(fill),
|
|
intensity=intensity,
|
|
)
|
|
|
|
|
|
def _diagonal_split_sample(
|
|
color_a: RGBColor,
|
|
color_b: RGBColor,
|
|
accent: RGBColor,
|
|
orientation: str,
|
|
intensity: float = 1.0,
|
|
boost: float = 0.1,
|
|
) -> TilePatternSample:
|
|
sample = _tile_sample(color_a.mix(color_b, 0.5), accent, intensity=intensity, boost=boost)
|
|
sample.metadata["diagonal_split"] = {
|
|
"orientation": orientation,
|
|
"color_a": color_a,
|
|
"color_b": color_b,
|
|
}
|
|
return sample
|
|
|
|
|
|
def _with_led_pixels(sample: TilePatternSample, led_pixels: dict[str, list[RGBColor]]) -> TilePatternSample:
|
|
sample.metadata["led_pixels"] = led_pixels
|
|
return sample
|
|
|
|
|
|
def _noise(value: float) -> float:
|
|
return value - math.floor(value)
|
|
|
|
|
|
def _temporal_noise(seed: float) -> float:
|
|
return _noise(math.sin(seed * 12.9898) * 43758.5453)
|
|
|
|
|
|
def _random_vivid_color(seed: float) -> RGBColor:
|
|
return sample_random_effect_color(_temporal_noise(seed))
|
|
|
|
|
|
def _axis_data(context: PatternContext, row_index: int, col_index: int) -> tuple[float, int, bool]:
|
|
vertical = context.params.direction in {"top_to_bottom", "bottom_to_top"}
|
|
position = float(row_index if vertical else col_index)
|
|
count = context.rows if vertical else context.cols
|
|
return position, max(1, count), vertical
|
|
|
|
|
|
_SCAN_VECTORS: dict[int, tuple[int, int]] = {
|
|
0: (1, 0),
|
|
45: (1, 1),
|
|
90: (0, 1),
|
|
135: (-1, 1),
|
|
180: (-1, 0),
|
|
225: (-1, -1),
|
|
270: (0, -1),
|
|
315: (1, -1),
|
|
}
|
|
|
|
|
|
def _scan_vector(angle: float) -> tuple[int, int]:
|
|
return _SCAN_VECTORS[int(angle) % 360]
|
|
|
|
|
|
def _scan_point(context: PatternContext, row_index: int, col_index: int, local_x: float, local_y: float) -> tuple[float, float]:
|
|
return col_index + local_x, row_index + local_y
|
|
|
|
|
|
def _scan_projection(
|
|
context: PatternContext,
|
|
row_index: int,
|
|
col_index: int,
|
|
local_x: float,
|
|
local_y: float,
|
|
vector: tuple[int, int],
|
|
) -> float:
|
|
x_pos, y_pos = _scan_point(context, row_index, col_index, local_x, local_y)
|
|
return x_pos * vector[0] + y_pos * vector[1]
|
|
|
|
|
|
def _scan_bounds(context: PatternContext, vector: tuple[int, int]) -> tuple[float, float]:
|
|
corners = (
|
|
(0.0, 0.0),
|
|
(float(context.cols), 0.0),
|
|
(0.0, float(context.rows)),
|
|
(float(context.cols), float(context.rows)),
|
|
)
|
|
projections = [x_pos * vector[0] + y_pos * vector[1] for x_pos, y_pos in corners]
|
|
return min(projections), max(projections)
|
|
|
|
|
|
def _scan_band_amount(
|
|
progress: float,
|
|
phase: float,
|
|
min_progress: float,
|
|
max_progress: float,
|
|
on_width: float,
|
|
off_width: float,
|
|
scan_style: str,
|
|
) -> float:
|
|
if scan_style == "bands":
|
|
period = max(0.1, on_width + off_width)
|
|
local = (progress - min_progress - phase) % period
|
|
return 1.0 if local < on_width else 0.0
|
|
|
|
travel = max(0.1, (max_progress - min_progress) + on_width + max(0.0, off_width))
|
|
band_center = min_progress + (phase % travel)
|
|
return 1.0 if abs(progress - band_center) <= on_width * 0.5 else 0.0
|