Override blend mode, FPS sparkline, fix api_input persistence

New features:
- Override composite blend mode: per-pixel alpha from brightness
  (black=transparent, bright=opaque). Ideal for API input over effects.
- API input test preview FPS chart uses shared createFpsSparkline
  (same look as target card charts)

Fixes:
- Fix api_input source not surviving server restart: from_dict was
  still passing removed led_count field to constructor
- Fix composite layer brightness/processing selectors not aligned:
  labels get fixed width, selects fill remaining space
- Fix CSPT input selector showing in non-CSPT CSS test mode
- Fix test modal LED/FPS controls showing for api_input sources
- Server only sends test WS frames when api_input push_generation changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 15:12:57 +03:00
parent be356f30eb
commit afd4a3bc05
8 changed files with 47 additions and 2 deletions
@@ -16,6 +16,7 @@ _BLEND_NORMAL = "normal"
_BLEND_ADD = "add"
_BLEND_MULTIPLY = "multiply"
_BLEND_SCREEN = "screen"
_BLEND_OVERRIDE = "override"
class CompositeColorStripStream(ColorStripStream):
@@ -300,11 +301,34 @@ class CompositeColorStripStream(ColorStripStream):
u16a >>= 8
np.copyto(out, u16a, casting="unsafe")
def _blend_override(self, bottom: np.ndarray, top: np.ndarray, alpha: int,
out: np.ndarray) -> None:
"""Override blend: per-pixel alpha derived from top brightness.
Black pixels are fully transparent (bottom shows through),
bright pixels fully opaque (top replaces bottom). Layer opacity
scales the per-pixel alpha.
"""
u16a, u16b = self._u16_a, self._u16_b
# Per-pixel brightness = max(R, G, B) for each LED
per_px_alpha = np.max(top, axis=1, keepdims=True).astype(np.uint16)
# Scale by layer opacity
per_px_alpha = (per_px_alpha * alpha) >> 8
# Lerp: out = (bottom * (256 - per_px_alpha) + top * per_px_alpha) >> 8
np.copyto(u16a, bottom, casting="unsafe")
np.copyto(u16b, top, casting="unsafe")
u16a *= (256 - per_px_alpha)
u16b *= per_px_alpha
u16a += u16b
u16a >>= 8
np.copyto(out, u16a, casting="unsafe")
_BLEND_DISPATCH = {
_BLEND_NORMAL: "_blend_normal",
_BLEND_ADD: "_blend_add",
_BLEND_MULTIPLY: "_blend_multiply",
_BLEND_SCREEN: "_blend_screen",
_BLEND_OVERRIDE: "_blend_override",
}
# ── Processing loop ─────────────────────────────────────────