Flash Animation System

Game engine-style O(1) per-window flash animations for UI feedback.

Module: pyqt_reactive.animation.flash_mixin

Overview

The flash animation system provides visual feedback when configuration values change. It uses a game engine architecture to achieve O(1) rendering per window regardless of how many elements are flashing.

Architecture

The system consists of three core components:

  1. _GlobalFlashCoordinator (singleton): ONE 60fps timer for ALL windows

  2. WindowFlashOverlay (per-window): Renders ALL flash rectangles in ONE paintEvent

  3. FlashMixin (per-widget): API for registering elements and triggering flashes

┌─────────────────────────────────────────────────────────────┐
│                  _GlobalFlashCoordinator                    │
│  ┌─────────────────┐  ┌──────────────────────────────────┐  │
│  │ _flash_start_   │  │ _computed_colors: Dict[key, QColor] │
│  │   times: Dict   │  │ (pre-computed each tick)          │  │
│  └─────────────────┘  └──────────────────────────────────┘  │
│                              │                              │
│                              ▼                              │
│                    [60fps timer tick]                       │
│                              │                              │
└──────────────────────────────┼──────────────────────────────┘
                               │
        ┌──────────────────────┼──────────────────────┐
        ▼                      ▼                      ▼
┌───────────────┐      ┌───────────────┐      ┌───────────────┐
│WindowFlashOverlay│   │WindowFlashOverlay│   │WindowFlashOverlay│
│   (Window A)  │      │   (Window B)  │      │   (Window C)  │
│               │      │               │      │               │
│ ONE paintEvent│      │ ONE paintEvent│      │ ONE paintEvent│
│ renders ALL   │      │ renders ALL   │      │ renders ALL   │
│ flash rects   │      │ flash rects   │      │ flash rects   │
└───────────────┘      └───────────────┘      └───────────────┘

Performance Model

Before (O(n) per tick):

Timer tick → compute N colors → store in dict → N widget repaints

After (O(1) per window):

Timer tick → compute colors once → prune expired → ONE overlay.update() per window

Each WindowFlashOverlay.paintEvent() renders all flash rectangles for its window in a single paint call. Geometry is cached and only recomputed on scroll/resize.

Animation Phases

Flash animations have three phases with configurable durations:

  1. fade_in (100ms): Quick fade-in with OutQuad easing

  2. hold (50ms): Hold at maximum intensity

  3. fade_out (350ms): Slow fade-out with InOutCubic easing

Widget-Type-Specific Masking

Flash animations use widget-type-specific masking strategies for precise visual feedback:

Masking Strategies:

  • Checkbox: Tight mask for indicator + label text using Qt style subelement rects

  • Label: Tight mask using sizeHint() to avoid empty layout space

  • Help Button: Fixed square mask when _square_size is set

  • All other widgets: Full rectangle mask

Checkbox Square Cutout:

Textless checkboxes (no label) use square cutouts to avoid rounding:

def _needs_square_checkbox_mask(widget: QWidget) -> bool:
    return isinstance(widget, QCheckBox) and not widget.text()

Function Pane Title Masking:

Function panes mask title row widgets tightly:

def _get_function_pane_title_widgets(groupbox: QWidget) -> List[QWidget]:
    pane = groupbox
    while pane is not None:
        if hasattr(pane, "_flash_title_container") or hasattr(pane, "_module_path_label"):
            break
        pane = pane.parentWidget()

    widgets = []
    module_label = getattr(pane, "_module_path_label", None)
    if module_label and module_label.isVisible():
        widgets.append(module_label)

    title_container = getattr(pane, "_flash_title_container", None)
    if title_container and title_container.isVisible():
        for child in title_container.findChildren(QWidget):
            if child.isVisible() and isinstance(child, LEAF_WIDGET_TYPES):
                widgets.append(child)

    return widgets

FlashElement Types

The system supports multiple element types via FlashElement dataclass:

Element Type

Factory Function

Use Case

Groupbox

create_groupbox_element()

Form section headers (STANDARD mode masks all children, INVERSE mode masks title + leaf_widget)

Groupbox (full rect)

create_groupbox_element(..., use_full_rect=True)

Flash entire groupbox geometry (no margin-top offset)

Tree Item

create_tree_item_element()

Config hierarchy trees

List Item

create_list_item_element()

Step/function lists

INVERSE Mode with Label Widget Masking

INVERSE mode now masks title + leaf_widget + label_widget (not all title row widgets):

self.register_flash_leaf(
    key="my_field",
    groupbox=my_groupbox,
    leaf_widget=my_widget,
    label_widget=my_label  # NEW: mask label too
)

This highlights “all fields that inherited the change” while keeping the changed field and its label visible.

Masking Behavior:

  • STANDARD mode (leaf_widget=None): Mask ALL children, flash only frame/background

  • INVERSE mode (leaf_widget=widget): Mask title + leaf_widget + label_widget, flash frame + all siblings

Usage with FlashMixin

Widgets inherit FlashMixin (alias: VisualUpdateMixin) to participate:

from pyqt_reactive.animation.flash_mixin import FlashMixin

class MyWidget(QWidget, FlashMixin):
    def __init__(self):
        super().__init__()
        self._init_flash_mixin()

    def setup_flash(self, groupbox: QGroupBox):
        # Register element for flashing
        self.register_flash_groupbox("my_key", groupbox)

    def trigger_flash(self):
        # Trigger flash (global - all windows with this key)
        self.queue_flash("my_key")

        # Or local flash (this window only)
        self.queue_flash_local("my_key")

Scope-Based Flash Keys

Flash keys are automatically scoped to prevent cross-window contamination:

# Key "well_filter" becomes "orchestrator::plate_1::well_filter"
scoped_key = self._get_scoped_flash_key("well_filter")

This ensures flashing step_0 in plate1 window doesn’t flash step_0 in plate2.

OpenGL Acceleration

On systems with OpenGL 3.3+, the system uses WindowFlashOverlayGL for GPU-accelerated rendering via instanced draw calls. Falls back to QPainter automatically.

See Also