Files

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