First upload, 18 controller version
This commit is contained in:
212
app/patterns/builtin/common.py
Normal file
212
app/patterns/builtin/common.py
Normal file
@@ -0,0 +1,212 @@
|
||||
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
|
||||
Reference in New Issue
Block a user