feat: wave-animate the busy overlay titles

BEAMLINE BUSY and the robot warnings now hop per letter (clipped sine)
so they read as an in-progress signal, not a frozen label. Gated by a
new BusyOverlayStyle.animate flag: 'In viewing mode' stays static
because it is a passive notice. No timer - the phase rides the wall
clock and repaints ride the ~20 fps camera frames, so the wave freezes
with a stalled feed (which has its own error surface).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:34:58 +02:00
committed by duan_j
co-authored by Claude Fable 5
parent fbd40eee90
commit 80063a5df1
2 changed files with 75 additions and 9 deletions
+54 -1
View File
@@ -1,3 +1,5 @@
import math
import time
from dataclasses import dataclass
from aarecommon.models.models import SessionsStateEnum
@@ -42,6 +44,36 @@ class BusyOverlayStyle:
# AxisVideoPanel strips it because only the sample-camera badge is a
# click target and the hint invites a click.
subtext: str = ""
# Wave-animate the title letters. Only the busy/robot warnings: a static
# "In viewing mode" badge must not look like an in-progress operation.
animate: bool = False
def draw_wave_text(
painter: QPainter,
x: int,
baseline: int,
text: str,
fm: QFontMetrics,
fg: QColor,
shadow: QColor | None = None,
amplitude: int = 5,
) -> None:
"""Per-letter hop wave for the busy warnings, so the text reads as an
in-progress signal instead of a frozen label. The phase comes from the
wall clock and repaints ride the ~20 fps camera frames — no timer here;
if the stream stalls the wave freezes, which is acceptable (the text
stays legible and a stalled feed has its own error surface)."""
now = time.monotonic()
for index, char in enumerate(text):
# Clipped sine: letters rest on the baseline and hop up in sequence.
lift = -round(amplitude * max(0.0, math.sin(now * 5.5 - index * 0.55)))
if shadow is not None:
painter.setPen(QPen(shadow))
painter.drawText(QPoint(x + 1, baseline + lift + 1), char)
painter.setPen(QPen(fg))
painter.drawText(QPoint(x, baseline + lift), char)
x += fm.horizontalAdvance(char)
def draw_busy_badge(
@@ -89,7 +121,12 @@ def draw_busy_badge(
painter.setFont(font)
title_x = position_x + (bg_width - title_width) // 2
title_y = position_y + padding_y + font_metrics.ascent()
painter.drawText(QPoint(title_x, title_y), style.text)
if style.animate:
draw_wave_text(
painter, title_x, title_y, style.text, font_metrics, QColor(style.overlay_text)
)
else:
painter.drawText(QPoint(title_x, title_y), style.text)
if style.subtext:
painter.setFont(sub_font)
@@ -115,6 +152,17 @@ def draw_busy_status_text(
x = (viewport_width - font_metrics.horizontalAdvance(style.text)) // 2
baseline = int(viewport_height * 0.68) + font_metrics.ascent() // 2
if style.animate:
draw_wave_text(
painter,
x,
baseline,
style.text,
font_metrics,
qcolor(style.badge_bg),
shadow=qcolor(SHADOW, 200),
)
return
painter.setPen(QPen(qcolor(SHADOW, 200)))
painter.drawText(QPoint(x + 1, baseline + 1), style.text)
painter.setPen(QPen(qcolor(style.badge_bg)))
@@ -156,6 +204,7 @@ def build_busy_overlay_style(
overlay_border=qcolor(BUSY_RED_BORDER, 230),
overlay_text=qcolor(WHITE),
accent_dot=BUSY_RED_DOT,
animate=True,
)
if activity_value == "unmounting":
@@ -167,6 +216,7 @@ def build_busy_overlay_style(
overlay_border=qcolor(BUSY_ORANGE_BORDER, 230),
overlay_text=qcolor(WHITE),
accent_dot=BUSY_ORANGE_DOT,
animate=True,
)
if activity_value == "drying":
@@ -178,6 +228,7 @@ def build_busy_overlay_style(
overlay_border=qcolor(BUSY_YELLOW_BORDER, 235),
overlay_text=qcolor(BUSY_YELLOW_TEXT_DARK),
accent_dot=BUSY_YELLOW_DOT,
animate=True,
)
if activity_value == "cooling":
@@ -189,6 +240,7 @@ def build_busy_overlay_style(
overlay_border=qcolor(BUSY_BLUE_BORDER, 235),
overlay_text=qcolor(WHITE),
accent_dot=BUSY_BLUE_DOT,
animate=True,
)
return BusyOverlayStyle(
@@ -199,4 +251,5 @@ def build_busy_overlay_style(
overlay_border=qcolor(BUSY_PSI_RED_BORDER, 235),
overlay_text=qcolor(WHITE),
accent_dot=BUSY_PSI_RED_DOT,
animate=True,
)
+21 -8
View File
@@ -67,6 +67,7 @@ from aare.gui.widgets.busy_overlay import (
BusyOverlayStyle,
build_busy_overlay_style,
draw_busy_badge,
draw_wave_text,
)
logger = setup_logger(LOGGER_NAME)
@@ -337,14 +338,26 @@ class SampleCameraImageLabel(QGraphicsView):
self._session_badge_rect = None
painter.setFont(font)
baseline = int(self.viewport().height() * 0.68) + font_metrics.ascent() // 2
self._draw_status_text(
painter,
style.text,
style.badge_bg,
self.viewport().width() // 2,
baseline,
font_metrics,
)
if style.animate:
x = self.viewport().width() // 2 - font_metrics.horizontalAdvance(style.text) // 2
draw_wave_text(
painter,
x,
baseline,
style.text,
font_metrics,
qcolor(style.badge_bg),
shadow=qcolor(SHADOW, 200),
)
else:
self._draw_status_text(
painter,
style.text,
style.badge_bg,
self.viewport().width() // 2,
baseline,
font_metrics,
)
painter.restore()
return