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