diff --git a/python-backend/config.yml.example b/python-backend/config.yml.example index c11f02a..b854d62 100644 --- a/python-backend/config.yml.example +++ b/python-backend/config.yml.example @@ -111,7 +111,7 @@ general: # lircd itself is set up on the Pi). Omit the whole section to run without a remote. # Play/pause/stop/previous/forward/rewind/volume/mute map to normal music control; # number keys 0-9 play whatever the "remote:" section below assigns them; Guide - # (KEY_EPG) shows the kiosk's clock. Run with --log-level DEBUG to see the name of a + # (KEY_GUIDE) shows the kiosk's clock. Run with --log-level DEBUG to see the name of a # button that does nothing. lirc: host: "musicmouse-pi.local" diff --git a/python-backend/musicmouse/services/lirc/service.py b/python-backend/musicmouse/services/lirc/service.py index 8855abb..fd7c57c 100644 --- a/python-backend/musicmouse/services/lirc/service.py +++ b/python-backend/musicmouse/services/lirc/service.py @@ -47,8 +47,10 @@ _TRANSPORT: dict[str, Callable[[], IntentEvent]] = { "KEY_FORWARD": lambda: NextTrackRequested(source="lirc"), # Does nothing unless general.screen is configured - see ScreenService. "KEY_POWER": lambda: ToggleScreenRequested(source="lirc"), - # "Guide": what time is it. Shows the kiosk's clock overlay. - "KEY_EPG": lambda: ToggleClockRequested(source="lirc"), + # "Guide": what time is it. Shows the kiosk's clock overlay. KEY_GUIDE is what + # irserver's hauppauge.rem calls it (the ansible repo); lircd's own Hauppauge + # config says KEY_EPG. + "KEY_GUIDE": lambda: ToggleClockRequested(source="lirc"), } #: Acted on every repeat, for a continuous ramp while held - same feel as the rotary diff --git a/python-backend/tests/test_lirc.py b/python-backend/tests/test_lirc.py index 36dc6c6..e764ded 100644 --- a/python-backend/tests/test_lirc.py +++ b/python-backend/tests/test_lirc.py @@ -328,8 +328,8 @@ async def test_the_guide_key_asks_for_the_clock( task = await _run_service(sim, _lirc_config(port)) try: await station.wait_connected() - await station.send("b7c0000000000000 00 key_epg hauppauge") - await station.send("b7c0000000000000 00 key_epg hauppauge") # still held + await station.send("b7c0000000000000 00 key_guide hauppauge") + await station.send("b7c0000000000000 00 key_guide hauppauge") # still held await sim.bus.drain() finally: task.cancel() diff --git a/web/src/components/ClockOverlay.tsx b/web/src/components/ClockOverlay.tsx index 7534566..e6ba4ed 100644 --- a/web/src/components/ClockOverlay.tsx +++ b/web/src/components/ClockOverlay.tsx @@ -4,9 +4,11 @@ * minus the mascot swimming through the scene (too distracting for a clock you read * at a glance). * - * Mounted for as long as the app is, and only hidden: Pos1 and the remote's Guide key - * have to show it at once, not after a first render. The second ticker only runs while - * it is on screen. Any click closes it. */ + * Mounted for as long as the app is, and only hidden (by opacity, see clock.css): Pos1 + * and the remote's Guide key have to show it at once, not after a first render and + * paint. What moves - the second ticker, the bubbles, a ringing countdown card - only + * exists while it is on screen, so a hidden clock costs nothing per frame. Any click + * closes it. */ import { memo } from "react"; @@ -53,9 +55,10 @@ export function ClockOverlay({ visible, clock, onClose }: Props) { hidden={!visible} onClick={onClose} role="dialog" + aria-hidden={!visible} aria-label="Uhr" > - {SHOW_DECORATIVE_ANIMATIONS && ( + {visible && SHOW_DECORATIVE_ANIMATIONS && ( `oklch(90% 0.02 var(--hue) / ${alpha})`} /> )} @@ -79,7 +82,7 @@ export function ClockOverlay({ visible, clock, onClose }: Props) { - {countdowns.length > 0 && ( + {visible && countdowns.length > 0 && (
{countdowns.map((countdown) => ( diff --git a/web/src/styles/clock.css b/web/src/styles/clock.css index 9f9e077..fac985b 100644 --- a/web/src/styles/clock.css +++ b/web/src/styles/clock.css @@ -21,11 +21,17 @@ ); cursor: pointer; user-select: none; + will-change: opacity; } -/* `display: flex` above would otherwise beat the `hidden` attribute. */ +/* Hidden by opacity, not `display: none`: the watch stays styled, laid out and + rasterized, so Pos1 or the remote's Guide key shows it in the next frame instead of + after the Pi has painted every gradient of the dial from scratch. `display: flex` + above would otherwise beat the `hidden` attribute anyway. */ .clock-overlay[hidden] { - display: none; + display: flex; + opacity: 0; + pointer-events: none; } /* Upright screens: the countdowns go under the watch instead of beside it. */ @@ -62,7 +68,6 @@ flex: none; width: 78vmin; height: 96vmin; - filter: drop-shadow(0 3vmin 5vmin oklch(15% 0.05 210 / 0.5)); } .clock-strap { @@ -72,6 +77,7 @@ box-sizing: border-box; border: 0.55vmin solid var(--clock-ink); --strap: oklch(70% 0.16 340); + box-shadow: 0 3vmin 5vmin oklch(15% 0.05 210 / 0.35); background: linear-gradient( 90deg, var(--strap) 0%, @@ -144,6 +150,10 @@ box-sizing: border-box; border-radius: 50%; border: 0.6vmin solid var(--clock-ink); + /* The watch's shadow. It used to be a drop-shadow filter on the whole watch, which + made the Pi re-render and re-blur the entire dial on every tick of the second + hand. */ + box-shadow: 0 3vmin 5vmin oklch(15% 0.05 210 / 0.5); background: radial-gradient( circle closest-side, @@ -347,6 +357,9 @@ left: 50%; bottom: 50%; transform-origin: 50% 100%; + /* Each hand on its own layer: turning it moves a finished picture on the GPU, + and the dial underneath is never repainted. */ + will-change: transform; } .clock-hand-hour,