Clock: show it at once and render it cheaply; Guide is KEY_GUIDE

The remote's hauppauge.rem names the Guide button KEY_GUIDE, not lircd's
KEY_EPG, so the remote never toggled the clock.

The overlay is now hidden by opacity rather than display: none, so it is
already laid out and painted when Pos1 or Guide shows it. The drop-shadow
filter on the whole watch is gone (it re-rendered and re-blurred the dial on
every second-hand tick), the hands sit on their own layers, and the bubbles
and countdown cards only exist while the clock is on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 16:59:06 +02:00
parent b56b41996b
commit 17db811bba
5 changed files with 31 additions and 13 deletions

View File

@@ -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 && (
<Drift field={BUBBLES} bubbleColor={(alpha) => `oklch(90% 0.02 var(--hue) / ${alpha})`} />
)}
@@ -79,7 +82,7 @@ export function ClockOverlay({ visible, clock, onClose }: Props) {
</div>
</div>
{countdowns.length > 0 && (
{visible && countdowns.length > 0 && (
<div className="clock-countdowns">
{countdowns.map((countdown) => (
<CountdownCard key={countdown.kind} countdown={countdown} />

View File

@@ -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,