Edit on GitHub

starplast.ambient

A drifting field of soft colour blobs, behind the panels.

Ported from spaCR's ambient background so the two programs look like one pair of tools, and kept to the one animation that was asked for -- the blobs -- with the same controls under the same names: speed, size, density, blur.

The design decision worth carrying across: position is a pure function of the clock, two independent sines per blob rather than a random walk. A walk accumulates error, cannot be rewound, and gives a different picture every time a frame is dropped; a function of time can be asked for any instant, which is what makes the field paintable at whatever rate the window can manage and identical at the same t on any machine.

Blobs are seeded on a jittered grid rather than uniformly at random. With a dozen of them, uniform sampling reliably leaves one corner empty and puts three in the middle -- which reads as a bug in the drawing rather than as a random arrangement.

SPEED_RANGE = (0.1, 4.0)
SIZE_RANGE = (0.25, 2.5)
DENSITY_RANGE = (0.25, 3.0)
BLUR_RANGE = (0.0, 3.0)
DEFAULT_SPEED = 1.0
DEFAULT_SIZE = 1.0
DEFAULT_DENSITY = 1.0
DEFAULT_BLUR = 0.0
BLOB_COUNT = 12
GRID = (4, 3)
@dataclass
class Blob:

One blob: where it sits, how far and how fast it wanders, how it breathes.

Blob( x: float, y: float, drift_x: float, drift_y: float, rate_x: float, rate_y: float, phase_x: float, phase_y: float, radius: float, pulse: float, pulse_rate: float, pulse_phase: float, color: int)
x: float
y: float
drift_x: float
drift_y: float
rate_x: float
rate_y: float
phase_x: float
phase_y: float
radius: float
pulse: float
pulse_rate: float
pulse_phase: float
color: int
def field(seed: int = 0, density: float = 1.0) -> list:

A reproducible set of blobs, seeded on a jittered grid.

The pool is always rolled at the top of the density range and a prefix of it painted, so raising the density adds blobs rather than redrawing the ones already there: the random numbers are consumed in the same order either way, and blob 3 keeps the numbers blob 3 always had.

def geometry( blobs, t: float, width: int, height: int, speed: float = 1.0, size: float = 1.0) -> list:

(cx, cy, radius, colour index) per blob, in pixels, at time t.

class AmbientWidget(PyQt6.QtWidgets.QWidget):

The blob field, painted behind everything else.

Transparent to the mouse -- it is scenery, and a background that swallowed a click on the panel in front of it would be a bug nobody could describe. The timer runs only while the widget is visible, because sixty repaints a second of a picture nobody is looking at is sixty repaints a second of a picture nobody is looking at.

AmbientWidget(colors=None, background='#0b0d10', parent=None, seed: int = 0)
background
colors
blur
seed
blobs
def keep_behind(self) -> bool:

Put this widget back at the bottom of its parent's children. True if it had to move.

It is an ordinary child of the window, lowered when it is built -- but Qt lowers a dock when its tab is switched away from (QMainWindowLayout::tabChanged), so after one switch that dock sits BELOW the background and comes back painted over: laid out, the right size, isVisible() true, its widget repainting, and invisible. That was the blank panel reported twice against 0.48 and 0.49 (instruction 58). Whoever restacks the window calls this after.

def configure(self, **kw) -> None:

Set any of speed, size, density, blur, colors -- and rebuild if the count changed.

size is accepted under spaCR's name and stored as blob_size, since a widget cannot have an attribute called size.

def start(self) -> None:

Begin animating, if it is not already.

def stop(self) -> None:

Stop animating. The field keeps its time, so starting again continues rather than jumps.

def set_time(self, t: float) -> None:

Jump to an instant. Position is a function of the clock, so this is exact.

def showEvent(self, ev):

showEvent(self, a0: QShowEvent|None)

def hideEvent(self, ev):

hideEvent(self, a0: QHideEvent|None)

def paintEvent(self, _ev):

paintEvent(self, a0: QPaintEvent|None)