175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
"""What the LEDs do.
|
|
|
|
These write straight to the device rather than going through intents: unlike
|
|
play/pause, nothing else in the system asks for "the figure's start animation".
|
|
|
|
Figure animations drive the shelf strip as well as the ring, which means they compete
|
|
with Home Assistant for it. That is intentional - the device is the single writer and
|
|
the most recent effect wins, whichever side it came from.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from copy import deepcopy
|
|
|
|
from musicmouse.app import App
|
|
from musicmouse.color import ColorRGBW, parse_color
|
|
from musicmouse.config import FigureColors
|
|
from musicmouse.effects import (
|
|
EffectCircularConfig,
|
|
EffectRandomTwoColorInterpolationConfig,
|
|
EffectReverseSwipe,
|
|
EffectStaticConfig,
|
|
EffectSwipeAndChange,
|
|
)
|
|
from musicmouse.events import (
|
|
ActiveFigureChanged,
|
|
PlaybackChanged,
|
|
PlaylistFinished,
|
|
TouchButtonPressed,
|
|
TouchButtonReleased,
|
|
)
|
|
from musicmouse.hardware import MOUSE_LED_RANGES, LedZone, TouchButton
|
|
from musicmouse.reactions.registry import on
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
OFF_COLOR = ColorRGBW(0, 0, 0, 0)
|
|
|
|
#: The mouse strip starts 6 LEDs into its 45, so its swipe is offset to match the ring.
|
|
MOUSE_SWIPE_START_DEGREES = 6 / 45 * 360
|
|
MOUSE_BELL_CURVE_WIDTH = 16
|
|
SWIPE_SPEED = 180
|
|
|
|
#: The web front-end's animation: a three-quarter arc drifting round twice a minute, in
|
|
#: the album's own primary colour. Deliberately unlike the figure swipe - the strips
|
|
#: should say *which* way the mouse was started, not just that it was.
|
|
WEB_CIRCLE_WIDTH = 270.0 # degrees
|
|
WEB_CIRCLE_SPEED = 12.0 # degrees per second
|
|
|
|
|
|
@on(ActiveFigureChanged)
|
|
def figure_placed_or_removed(event: ActiveFigureChanged, app: App) -> None:
|
|
if event.figure is None:
|
|
off_animation(app)
|
|
else:
|
|
start_animation(app, app.colors(event.figure))
|
|
app.mouse.set_button_brightness(
|
|
app.config.general.button_leds_brightness, origin="device"
|
|
)
|
|
|
|
|
|
@on(PlaybackChanged)
|
|
def web_playback(event: PlaybackChanged, app: App) -> None:
|
|
"""Light the strips for playback that no figure started.
|
|
|
|
Reacting to ``PlaybackChanged`` rather than to the play intent covers pause, stop
|
|
and playlist-end in one place. The guard is what keeps this off the figure path:
|
|
while a figure is on the reader its animation owns all three zones.
|
|
"""
|
|
if app.mouse.active_figure is not None:
|
|
return
|
|
if not event.playing:
|
|
off_animation(app)
|
|
return
|
|
|
|
album = app.album_for(event.playlist)
|
|
if album is None:
|
|
return
|
|
web_animation(app, parse_color(album.colors[0]))
|
|
|
|
|
|
@on(PlaylistFinished)
|
|
def playlist_finished(_event: PlaylistFinished, app: App) -> None:
|
|
off_animation(app)
|
|
|
|
|
|
@on(TouchButtonPressed)
|
|
def touch_pressed(event: TouchButtonPressed, app: App) -> None:
|
|
colors = _active_colors(app)
|
|
if colors is None:
|
|
return
|
|
app.mouse.set_effect(
|
|
LedZone.MOUSE, _range_effect(event.button, colors.accent), origin="device"
|
|
)
|
|
|
|
|
|
@on(TouchButtonReleased)
|
|
def touch_released(event: TouchButtonReleased, app: App) -> None:
|
|
colors = _active_colors(app)
|
|
# Clear the touched area first, then restore the whole-body effect over it.
|
|
app.mouse.set_effect(
|
|
LedZone.MOUSE,
|
|
_range_effect(event.button, colors.primary if colors else OFF_COLOR),
|
|
origin="device",
|
|
)
|
|
if colors is None:
|
|
return
|
|
|
|
app.mouse.set_effect(
|
|
LedZone.MOUSE,
|
|
EffectRandomTwoColorInterpolationConfig(
|
|
color1=colors.primary, color2=colors.secondary, start_with_existing=True
|
|
),
|
|
origin="device",
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------- animations
|
|
|
|
|
|
def start_animation(app: App, colors: FigureColors) -> None:
|
|
ring = EffectSwipeAndChange()
|
|
ring.swipe.primary_color = colors.primary
|
|
ring.swipe.secondary_color = colors.secondary
|
|
ring.swipe.swipe_speed = SWIPE_SPEED
|
|
ring.change.color1 = colors.primary
|
|
ring.change.color2 = colors.secondary
|
|
|
|
app.mouse.set_effect(LedZone.RING, ring, origin="device")
|
|
app.mouse.set_effect(LedZone.SHELF, deepcopy(ring), origin="device")
|
|
|
|
mouse = deepcopy(ring)
|
|
mouse.swipe.start_position = MOUSE_SWIPE_START_DEGREES
|
|
mouse.swipe.bell_curve_width_in_leds = MOUSE_BELL_CURVE_WIDTH
|
|
app.mouse.set_effect(LedZone.MOUSE, mouse, origin="device")
|
|
|
|
|
|
def web_animation(app: App, color: ColorRGBW) -> None:
|
|
for zone in LedZone:
|
|
app.mouse.set_effect(
|
|
zone,
|
|
EffectCircularConfig(speed=WEB_CIRCLE_SPEED, width=WEB_CIRCLE_WIDTH, color=color),
|
|
origin="device",
|
|
)
|
|
app.mouse.set_button_brightness(app.config.general.button_leds_brightness, origin="device")
|
|
|
|
|
|
def off_animation(app: App) -> None:
|
|
_log.info("Running off animation")
|
|
app.mouse.set_effect(LedZone.RING, EffectReverseSwipe(), origin="device")
|
|
app.mouse.set_effect(LedZone.SHELF, EffectReverseSwipe(), origin="device")
|
|
app.mouse.set_effect(
|
|
LedZone.MOUSE,
|
|
EffectReverseSwipe(start_position=MOUSE_SWIPE_START_DEGREES),
|
|
origin="device",
|
|
)
|
|
app.mouse.set_button_brightness(0.0, origin="device")
|
|
|
|
|
|
# --------------------------------------------------------------------- helpers
|
|
|
|
|
|
def _active_colors(app: App) -> FigureColors | None:
|
|
"""The current figure's colours, or ``None`` if nothing is playing."""
|
|
figure = app.mouse.active_figure
|
|
if figure is None or not app.player.is_playing:
|
|
return None
|
|
return app.colors(figure)
|
|
|
|
|
|
def _range_effect(button: TouchButton, color: ColorRGBW) -> EffectStaticConfig:
|
|
begin, end = MOUSE_LED_RANGES[button]
|
|
return EffectStaticConfig(color, begin, end)
|