The Problem
The gauge pages, System View and Network View, share the same visual language. Switching between them with a full black fade felt heavy; a crossfade between the two canvas layers seemed like the right call. The first implementation faded both canvases simultaneously: outgoing opacity from 1→0, incoming from 0→1, running in parallel over 500ms.
On the Pi display, the result was a clear brightness pulse at the
midpoint of the transition. At ~50% opacity, both canvas layers were
semi-transparent simultaneously. The background color, #020202, very nearly black, bled through both at once, and the eye read the
combined output as a brief dimming flash at the exact center of the fade.
Root Cause Analysis
Tkinter on Linux has no native per-widget alpha channel. The stipple
workaround (drawing a gray50 checkerboard rectangle over
the canvas) was the only available approximation, and it was visually unacceptable in motion, rendering as a visible dot pattern rather than
a smooth transition. The CSS equivalent would have been clean; the
Tkinter equivalent was not.
# First attempt — simultaneous crossfade (ABANDONED) # Caused brightness pulse at ~50% overlap def bad_dissolve(self): # Both canvases at 50% simultaneously = # background bleeds through both → visible flash outgoing.alpha(0.5) # fade out incoming.alpha(0.5) # fade in — at same time # Solution — sequential, not simultaneous # No overlap window = no background bleed def sequential_dissolve(self): # Step 1: outgoing → black (88ms, ease-in) # Step 2: swap view at the cut # Step 3: incoming → full (550ms, ease-out) # is_transitioning = True blocks interval redraws
The Fix
Sequential rather than simultaneous. The outgoing canvas fades to black first over 88ms. At the cut point the incoming canvas is raised and begins its fade in over 550ms, a longer and slower reveal. The two phases never overlap, which eliminates the background bleed entirely.
An is_transitioning flag was added to both the gauge
interval timer and the network speed timer. Without it, a 2-second
psutil poll firing during a 638ms transition would issue a canvas
redraw mid-fade, resetting the opacity artificially. The flag makes
both timers skip their draw call if a transition is in progress and
reschedule normally; they don't miss data, they just don't render it at the wrong moment.
This class of bug, concurrent processes contending for a shared resource during a state change, appears in show systems in exactly the same form. Automation cues firing during a manual override. DMX values being written by two operators simultaneously. A Kinesys cue sheet executing while an E-stop clears. The solution is always the same: establish who owns the resource during the critical window and make everyone else wait.