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:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user