spacr.qt.widgets.ambient

Ambient animated backdrop — soft motion behind every module screen.

The sequencing screen has its own backdrop (spacr.qt.widgets.dna_rain, the ATGC cascade). This is the one for everything else: a slow, diffuse animation that sits behind the settings form and the console, takes no focus and no mouse events, and can be switched off entirely in Preferences.

Five data-art materials and three classic themes remain in the menu. The default is data_art_impulse_lens (spaCR field). The other data-art choices are spaCR advection, spaCR growth, spaCR waves and spaCR spinn. The classic choices provide softer motion:

blobs

Big and small colour blobs drifting over the page, each pulsing in size on its own period. They overlap and blend, so the result reads as soft colour fields rather than as a bag of circles.

aurora

Three overlapping curtains of vertical rays, folding along their own length. The folds are travelling waves — several superposed frequencies running lengthwise along the arc — with brightness surges on a separate schedule, a sharp lower edge, a diffuse top, and the real thing’s vertical colour order: green through the body, red high up, a violet fringe underneath.

drift

A slow starfield in three parallax layers: small, dim, slow ones behind; bigger, brighter, faster ones in front. The one crisp theme. It travels up, down, or every which way — see DRIFT_DIRECTIONS.

The older RippleEngine, CellsEngine, BokehEngine and ResonanceEngine remain importable for direct callers, but are no longer menu choices or factory entries.

There is also a private fractal engine: it is not in AMBIENT_THEMES, no menu lists it, no preference can hold it, and the only way to see it is to start the application with the spaceout command instead of spacr. See SPACEOUT_THEME and FractalEngine.

The random direction of drift produces Brownian-style motion without duplicating the starfield as a separate theme. Themes dominated by many antialiased lines or per-pixel noise are omitted because their raster cost is too high for an always-running backdrop.

Palettes

Every theme declares the palettes it offers (palettes_for()), because a palette that works as a 400 px blob does not necessarily work as a 2 px star. PALETTE_SETS holds the colours themselves; spacr uses the three brand hues from the module-maturity legend, and okabe is the Okabe–Ito set for red–green colour deficiency (see its note).

Both dark and light

spaCR ships several themes, and a blob set tuned only for a near-black page turns to mud on a white one. So the composition mode follows the background:

  • dark page -> CompositionMode_Plus. Overlapping blobs add up and glow, which is what makes two circles read as one colour field. Additive over a light page just clips to white and the whole effect vanishes.

  • light page -> CompositionMode_Multiply, with the palette colour mixed toward white first. Multiply is the exact dual: overlaps get darker and still blend hue-wise, so the same geometry reads the same way. SourceOver would let later blobs cover earlier ones, producing discrete discs instead of a blended field.

AmbientWidget.set_background_color() re-derives all of that, so a live theme switch is one call.

Cost

This paints behind every module screen, on machines that are simultaneously running Cellpose on a GPU and a 40-plate pipeline, so cost is a correctness requirement rather than a nicety. Two things get it there:

  1. The timer stops whenever the widget is not on screen — hidden, on another tab, or in a minimised window. Zero frames, zero CPU. These screens stay open for hours, so this is the whole ball game.

  2. Diffuse themes are painted into a small reusable QImage and scaled up. The buffer’s long edge is whatever the theme declares (_BufferedEngine.base_edge) times the user’s resolution setting, so diffuse fields shade ~37 000 pixels instead of ~2 000 000. The aurora and data-art materials preserve native display detail within the actual screen-pixel budget. Aurora returns an owned frame, avoiding a second full-size copy when a shading worker publishes it.

  3. The shading happens on its own thread (_FrameProducer), so the GUI thread’s whole share of a frame is one drawImage. That is the next section, and it is the one that matters while a pipeline is running.

While a run is going

The animation can lag while a job is running, and the cause is not the obvious one. Measurements on a real X server at 1920x1080 with a real ConsolePanel under the real stylesheet and a real Qt event loop, blobs at the shipped 24 fps cap, best of five interleaved rounds:

condition

delivered

GUI paint

idle

25.0 fps

2.04 ms

a numpy thread (1024² matmul + FFT), flat out

24.5 fps

2.10 ms

200 console lines a second, nothing else

24.9 fps

1.27 ms

one pure-Python thread

24.5 fps

17.21 ms

a worker doing Python work and printing

17.3 fps

17.16 ms

So CPU saturation is not a cause: numpy releases the interpreter lock and a core burning flat out costs this module nothing. A signal flood is not a cause on its own: 200 lines a second are free, and it only bites in the thousands, where the console’s own per-line work saturates the GUI thread and nothing in this module can help. What is left is the interpreter lock: identical drawing work, eleven times slower, because the shading pass is Python and numpy and something else is holding the lock.

Translucent overlays can also trigger repaints outside the animation timer. The console sits over this widget, so each new line can expose it and request a full frame even while the animation timer is stopped. Expensive shading must therefore remain off the GUI thread even when the frame rate is capped.

The frame is split at the seam where the cost occurs. The following historical measurements predate native-resolution Aurora and serve as a comparison, not as a current frame-rate claim. Per theme, milliseconds, idle against one Python thread, min of nine interleaved rounds:

theme

shading (moved)

soften + blit (stays)

blobs

0.240 -> 0.572

0.651 -> 1.097

aurora

1.396 -> 7.176

0.797 -> 1.043

ripple

0.367 -> 0.589

0.644 -> 1.196

bokeh

0.663 -> 3.533

0.679 -> 1.008

cells

0.538 -> 26.179

0.663 -> 0.909

drift

0.528 -> 1.084

(no buffer)

resonance

1.072 -> see below

0.842 -> 1.115

resonance is the one row whose contended figure is a range rather than a number, and the shape of its shading is the reason. The others are one long pass of QPainter calls; the plate is dozens of small NumPy calls, and each one gives the lock back and then queues for it again, so what the cell would measure is how often the shading thread was descheduled rather than how much work the theme does. Four nine-round repeats of the protocol above gave medians of 10, 174, 286 and 407 ms on the same machine. Two things make that liveable and both are already here: the cost is paid on the producer thread, so a late frame is a repeated frame (AmbientWidget.repeated_frames) and never a slow interface; and while a run is going the process holds spacr.qt.gil_priority.BUSY_INTERVAL, where the same measurement is 6 to 24 ms. Its row was taken later than the rest and on a busier machine — blobs read 0.262 and 0.749 -> 1.002 in the same run — so read it against those rather than against the table.

The shading pass is sensitive to interpreter-lock contention, whereas the Qt blit remains inexpensive. _BufferedEngine.shade() therefore runs in _FrameProducer, while _BufferedEngine.blit() stays on the GUI thread. If a shaded frame is not ready, the widget repeats the previous frame instead of blocking the interface.

In that historical blobs benchmark, the rate goes from 17.3 fps to 24.7 on the same worker. What this does not address is a genuinely chatty run: at 200 lines a second both land at about 4 fps, because by then the GUI thread is inside ConsolePanel and not in here at all. Two levers finish the job and neither is in this file — sys.setswitchinterval(0.001) in the Qt bootstrap (measured independently: 32 % of the frame rate to 99 %, for about 6 % of the worker’s throughput) and coalescing PipelineWorker.line_ready.

Three constraints shape the implementation. Animation periods are sampled from continuous ranges rather than replayed from a precomputed loop, avoiding visible jumps and large frame caches. Shading runs outside the GUI thread; if a frame is late, the previous frame is repeated and counted by AmbientWidget.repeated_frames. The animation clock remains on the GUI thread, and rendered frames remain deterministic functions of (seed, clock, size).

drift keeps synchronous drawing at full Detail. Lower Detail renders the same particle population through a bounded image buffer; it does not reduce Density. Its historical row above predates that buffer path.

Performance depends on hardware, display size, theme, and concurrent work. Density controls population independently of Detail. Detail controls sampling resolution, and buffers stay within the actual screen-pixel budget. Increasing Detail does not trim the selected population. Legacy direct engine callers may still set blur, but Preferences exposes no Blur control. Hidden widgets stop rendering entirely. Historical timings above do not establish native 24 FPS at current maximum controls.

The private growth producer uses _lineage to index reproducible wandering tips and recursive front forks. Daughter branches remain connected to their parent filaments; older trails recede as new colonies begin. This is decorative mycelial artwork, not a model fitted to project measurements.

The field’s background-only left drag uses _set_field_grab and _step_field_grab for a bounded local spring with continuous release velocity. AmbientWidget._offer_field_grab publishes the latest handle without waiting for shading, and AmbientWidget._field_grab_background excludes scientific canvases and interactive controls. Release returns the patch without moving or reseeding its underlying material.

Aurora’s buffer_size and buffer_scale retain crisp display sampling; its private _shade returns a freshly owned frame. Drift’s buffer_size bounds lower-Detail drawing, while its private _paint_dots paints the same selected population into that buffer or directly at full Detail.

Classes

AmbientEngine

Base class: a deterministic, time-parameterised painter.

AmbientWidget

The live backdrop: paints an AmbientEngine at a capped rate.

AuroraEngine

Folded curtains of vertical rays, rippling along their own length.

Blob

One drifting, pulsing blob, in normalised units.

BlobsEngine

Diffuse colour blobs, drifting and pulsing.

BokehEngine

Defocused points of light: flat discs with bright rims.

Cell

One drifting cell, in normalised units.

CellsEngine

Cells drifting through the field, turning as they go.

Curtain

One aurora curtain, in normalised units.

Disc

One out-of-focus point source, in normalised units.

DriftEngine

A slow starfield in three parallax layers.

Form

Sampling geometry for the primary fractal or a derived bud.

FractalEngine

Render a deterministic, animated Julia-set field.

Motion

Every user control that shapes the animation, in one value.

PaletteSpec

One named colour set: what to call it and what it is made of.

Particle

One drifting dot, in normalised units.

ResonanceEngine

Chladni figures: sand on a plate that is driven by what is playing.

RippleEngine

Concentric rings expanding from a few sources and fading as they grow.

Source

One ripple origin, in normalised units.

Functions

animation_label(→ str)

Human label for an entry of ANIMATION_CHOICES.

animation_note(→ str)

One-line description of an animation choice, for a tooltip.

coerce_palette(→ str)

palette if theme offers it, else that theme's default.

default_palette_for(→ str)

The palette theme falls back to — DEFAULT_PALETTE when it

dressed(→ Tuple[str, str])

Resolve the ambient theme and palette for the current launch mode.

drift_direction_label(→ str)

Human label for a starfield direction, for a menu.

drift_direction_note(→ str)

One-line description of a starfield direction, for a tooltip.

field_ripple_for_widget(→ None)

Publish settled window or panel feedback to its visible field backdrop.

install_ambient(→ AmbientWidget)

Put a live ambient backdrop behind host.

is_animation_choice(→ bool)

True for anything the Animation preference may hold — including

is_dark_background(→ bool)

True when color is dark enough for additive compositing.

is_valid_drift_direction(→ bool)

True when name is one of DRIFT_DIRECTIONS. Never raises.

is_valid_palette(→ bool)

True when palette is offered by theme. Never raises.

is_valid_theme(→ bool)

True when name is one of AMBIENT_THEMES.

make_engine(→ AmbientEngine)

Build the engine for theme/palette. Raises on unknown names.

palette_colors(→ Tuple[str, ...])

The #rrggbb colours behind palette, for theme.

palette_label(→ str)

Human label for palette as offered by theme.

palette_note(→ str)

One-line description of palette, for a tooltip.

palettes_for(→ Tuple[str, ...])

The palette names theme offers, in menu order.

preferred_motion(→ Motion)

The animation controls, from the user's preferences.

rebuild_the_spaceout_backdrops(→ int)

Rebuild every running spaceout backdrop whose build settings changed.

screen_pixels(→ int)

Return the device-pixel count of the screen containing widget.

theme_label(→ str)

Human label for name, for a menu. Raises on an unknown theme.

theme_note(→ str)

One-line description of name, for a tooltip.

total_frames_painted(→ int)

How many ambient frames this process has painted. For tests.

Module Contents

class spacr.qt.widgets.ambient.AmbientEngine(colors: Sequence[PySide6.QtGui.QColor | str], background: PySide6.QtGui.QColor | str, seed: int | None = None, blur: float = DEFAULT_BLUR, speed: float = DEFAULT_SPEED, size: float = DEFAULT_SIZE, resolution: float = DEFAULT_RESOLUTION, density: float = DEFAULT_DENSITY, direction: str = DEFAULT_DRIFT_DIRECTION)[source]

Base class: a deterministic, time-parameterised painter.

An engine owns no widget and no timer. It holds the constants rolled once from its seed, a clock (time), and whatever reusable buffer it paints through. Everything it draws is a pure function of (seed, time, width, height, colours, background) — which is what lets a test render the same frame twice and compare it byte for byte, and what lets AmbientWidget.set_theme() swap engines without the animation jumping.

Positions are rolled in normalised 0..1 units and multiplied up at paint time, so a resize re-frames the animation instead of re-rolling it.

Every numeric parameter below is CLAMPED to its range rather than rejected: these arrive from saved preferences, and a value that has drifted outside its range should slow the animation down, not refuse to draw it.

Parameters:
  • colors – the palette to paint from, as QColor or as any string QColor accepts.

  • background – the colour behind the palette.

  • seed – the roll that fixes this engine’s constants. The same seed gives the same animation, which is what lets a test compare two renders byte for byte. None rolls a new one.

  • blur – softness of the painted shapes, 0.0 to 3.0.

  • speed – how fast time advances the animation, 0.1 to 4.0.

  • size – scale of the painted shapes, 0.25 to 2.5.

  • resolution – scale of the buffer painted through, 0.25 to 2.0. Below 1.0 paints fewer pixels and scales them up, which is the lever that makes the backdrop affordable on a weak GPU.

  • density – how many shapes are rolled, 0.01 to 3.0.

  • direction – which way the animation drifts. An unrecognised name falls back to the default rather than raising, for the same reason the numbers are clamped.

Roll the constants this engine paints from.

Parameters:
  • colors – the palette to paint with.

  • background – the colour behind the shapes.

  • seed – what makes the animation reproducible.

  • blur – how soft the shapes are drawn.

  • speed – the animation rate multiplier.

  • size – the shape scale.

  • resolution – the render resolution.

  • density – how many shapes there are.

  • direction – which way the field drifts.

advance(dt: float) → None[source]

Step the clock by dt seconds. Negative steps are ignored.

The step is scaled by speed, so the clock counts animation seconds rather than wall-clock ones: every period, rate and travel speed in every theme is expressed against this clock and therefore scales with one multiplier.

frames counts every call, including the ignored ones, so a test can prove a hidden widget stopped asking for frames rather than only proving that its clock stood still.

alpha_scale() → float[source]

What to multiply every element’s peak alpha by, given the density.

Additive compositing means N overlapping shapes are N times the light, so a density control with no compensation is a brightness control wearing a misleading name. Measured, on a page at 0.076: mean frame lightness went from 0.135 at density 1.0 to 0.288 at 3.0. The backdrop would have become the loudest thing behind a settings form — which the alphas in this module were set on a rendered frame specifically to prevent (see AURORA_ALPHA_DARK).

So above 1.0 the field’s light is divided among more elements rather than added to it. Below 1.0 nothing is done: quadrupling the alpha of a quarter as many blobs clips to white rather than compensating, and a sparser field being a quieter one is the right answer anyway.

Density therefore changes the texture of the field — how many shapes it is made of, and how strongly each one states itself — and not how loud the field is. That is the only reading of the control that leaves the backdrop legible at both ends of its range.

effective_density() → float[source]

The requested population, independently of render resolution.

Buffered engines enforce the work budget on pixel sampling instead of removing elements when the user increases Detail.

element_count(base: int, pool: int) → int[source]

How many of a pool of pool elements to draw, when the theme’s own count is base. At least one: a density slider that can empty the screen is an off switch wearing a disguise.

abstract geometry(width: int, height: int) → Tuple[tuple, ...][source]

What this engine would draw right now, in pixels.

Every engine answers this and every engine paints from it, so a test can assert on the geometry and know it is asserting on the frame. The tuple shape is per engine and documented there.

abstract paint(painter: PySide6.QtGui.QPainter, width: int, height: int) → None[source]

Draw this engine’s current frame.

Parameters:
  • painter – the painter to draw with.

  • width – the widget’s width in pixels.

  • height – its height in pixels.

set_background(color: PySide6.QtGui.QColor | str) → None[source]

Tell the engine what it is painting onto — this is what decides additive versus multiply, so it must be called on a theme switch.

Set the percentage of dot centres flashing white, without reseeding.

set_blur(value: float) → None[source]

How much the finished picture is softened. 0.0 is untouched.

Clamped to BLUR_RANGE. Engines that cache anything sized by it drop that cache here, never per frame.

set_colors(colors: Sequence[PySide6.QtGui.QColor | str]) → None[source]

Swap the palette without disturbing the motion.

set_density(value: float) → None[source]

How many elements the theme draws, as a multiplier on its own count. Clamped to DENSITY_RANGE.

Never re-rolls anything: the pool was built for the top of the range at construction and this only changes how much of it is painted, so the elements that were on screen stay exactly where they were.

set_direction(name: str) → None[source]

Which way the elements travel, for the themes that have a way.

Silently ignores an unknown name rather than raising: this reaches every engine, and most of them have nothing to do with it.

set_max_pixels(pixels: int) → None[source]

Set the display-pixel ceiling used to size the render buffer.

set_popup_wave_frequency(value: float) → None[source]

Set popup-origin waves per minute; zero disables automatic waves.

set_resolution(value: float) → None[source]

How many pixels the scene is shaded into, as a multiplier on this theme’s own buffer edge. Clamped to RESOLUTION_RANGE.

set_size(value: float) → None[source]

Scale every element’s size. Clamped to SIZE_RANGE.

set_speed(value: float) → None[source]

Multiply every motion in the theme. Clamped to SPEED_RANGE.

Deliberately a clock multiplier rather than a factor inside geometry(): a user dragging the slider in Preferences changes how fast the animation goes from here, and never makes what is already on screen jump to a different place.

set_time(seconds: float) → None[source]

Jump the clock — used by the tests, and to carry the clock across a theme change.

property background: PySide6.QtGui.QColor[source]

The colour behind the shapes.

A copy, for the same reason as colors().

Returns:

the background colour.

property colors: List[PySide6.QtGui.QColor][source]

The palette this engine paints with.

COPIES, so a caller cannot recolour the engine by mutating what it was handed.

Returns:

one QColor per palette entry.

property work: float[source]

What this engine is asking for, as a multiple of its own default.

Shading cost is buffer pixels times elements. Resolution is a linear scale on the buffer’s edge, so it enters squared; density is linear in the elements. Overridden by the one engine that has no buffer.

class spacr.qt.widgets.ambient.AmbientWidget(parent: PySide6.QtWidgets.QWidget | None = None, *, theme: str = DEFAULT_THEME, palette: str = DEFAULT_PALETTE, background: PySide6.QtGui.QColor | str | None = None, backdrop=None, fps: int = DEFAULT_FPS, seed: int | None = None, blur: float | None = None, speed: float | None = None, size: float | None = None, resolution: float | None = None, density: float | None = None, gravity_radius: float | None = None, ripples_enabled: bool | None = None, blink_percent: float | None = None, popup_wave_frequency: float | None = None, direction: str | None = None, corner_radius: int = 0)[source]

Bases: PySide6.QtWidgets.QWidget

The live backdrop: paints an AmbientEngine at a capped rate.

Screen content sits in front of it, so it never takes focus, is transparent to mouse events, and lowers itself to the bottom of the sibling stacking order. It is fully opaque — it paints the page colour (or the theme wallpaper) itself and the animation on top — so the widget it covers has nothing to repaint underneath.

Parameters:
  • parent – parent widget; follow_parent() sizes it to that.

  • theme – one of AMBIENT_THEMES.

  • palette – one of palettes_for() for that theme. A palette that exists but is not offered by the theme is downgraded to the theme’s default (stale preferences must not break a screen); an unknown name raises.

  • background – the flat colour under the animation; defaults to the current theme’s page colour.

  • backdrop – an image to paint under the animation instead of the flat colour — a path, a QPixmap/QImage, or None. Give it the Space/Cell wallpaper and the animation composites over the picture rather than replacing it.

  • fps – frame-rate cap.

  • seed – RNG seed, for a reproducible animation.

  • blur – retained for older callers; displayed themes ignore blur.

  • speed – motion multiplier; None reads Preferences.

  • size – element-size multiplier; None reads Preferences.

  • resolution – how much detail is shaded, as a multiplier on the theme’s own buffer; None reads Preferences.

  • density – how many elements are drawn, as a multiplier on the theme’s own count; None reads Preferences.

  • gravity_radius – pointer influence radius, as a fraction of the shorter screen edge; zero disables it. None reads Preferences.

  • ripples_enabled – independent field-wave switch; None reads Preferences.

  • direction – which way the starfield travels, one of DRIFT_DIRECTIONS; None reads Preferences. Meaningless to the other themes, and kept anyway so switching away and back does not lose it.

  • corner_radius – round the backdrop’s own corners by this many px; 0 leaves it square. CLIPPED, not masked – a mask region gives stair-stepped corners against the card’s anti-aliased rim – and applied before the base fill so the flat page colour is rounded with the animation rather than showing at the corners behind it.

Build the widget and start its engine.

Parameters:

parent – parent widget.

advance_frame(dt: float) → None[source]

Step the animation by dt seconds and schedule a repaint.

Called directly by the tests and by the tutorial recorder, so no caller ever waits on a real clock. Re-shades before it returns, so the next paint shows the frame that was asked for rather than whatever the shading thread last finished — the timer does not come through here for exactly that reason (_on_tick()).

Parameters:

dt – seconds to step; the engine scales the step by its speed, and a step that is not positive leaves the clock where it is.

backdrop() → PySide6.QtGui.QPixmap | None[source]

The image painted under the animation, or None.

background_color() → PySide6.QtGui.QColor[source]

The colour painted behind the animation.

A copy, so a caller cannot recolour this widget in place.

Returns:

the background colour.

The selected percentage of visible dots that flash white.

blur() → float[source]

How much the picture is softened; 0.0 is the shipped animation.

changeEvent(event) → None[source]

Follow a live theme switch when nobody else is going to.

A host that passed its own background owns that colour and is expected to re-set it (that is what app_screen does, because it also has to re-resolve the wallpaper). A host that did not gets this for free instead of a stale dark rectangle on a white page.

Parameters:

event – the change event; it goes to the base class first, and only an ApplicationPaletteChange is acted on.

density() → float[source]

How many elements are drawn; 1.0 is each theme’s own count.

direction() → str[source]

Which way the starfield travels. Meaningless to the others, and kept anyway, so switching themes and back does not lose it.

eventFilter(obj, event)[source]

Follow the parent’s size; pause when the window is minimised.

Parameters:
  • obj – the object the event is for — the parent (whose resize this widget follows) or the watched top-level window.

  • event – the event; resize, window-state, hide, show, move and screen-change types are acted on, and every event is still passed on to the base-class filter.

focusInEvent(event) → None[source]

Reject even programmatic focus; this widget is decorative only.

Parameters:

event – the focus event; it is ignored and focus is cleared.

follow_parent() → None[source]

Track the parent’s geometry and sit below its other children.

fps() → int[source]

The cap on repaints per second.

A CAP, NOT A RATE. This is a backdrop and must not take frames from whatever the user is doing in front of it.

Returns:

the frame cap.

frames_shaded() → int[source]

Frames the shading thread has finished for this backdrop.

Below frames_painted under load, by design: the difference is repeated_frames.

gravity_radius() → float[source]

The normalized reach of local pointer gravity; zero disables it.

hideEvent(event)[source]

The whole performance story: a screen the user is not looking at costs nothing. Qt sends this to the children of a hidden parent too, so switching tabs stops the animation on the tab you left.

Parameters:

event – the hide event; passed on to the base class before the animation stops.

is_animating() → bool[source]

The requested state, whether or not the widget is on screen.

is_running() → bool[source]

True while the animation timer is ticking.

paintEvent(event) → None[source]

Paint, timed on the timing timeline when timing is on.

Parameters:

event – the Qt paint event, passed on to the painter.

palette_name() → str[source]

The ambient palette’s name.

Not palette() — QWidget already owns that name and it returns a QPalette.

popup_wave_frequency() → float[source]

Automatic waves per minute from the centre of an open popup.

resolution() → float[source]

How much detail is shaded; 1.0 is each theme’s own buffer.

set_animating(on: bool) → None[source]

Pause or resume without destroying anything.

A paused widget keeps its last frame on screen and its engine in memory; its timer stops ticking. This is the “off” switch for the Preferences toggle when the user wants the colours but not the motion — turning the feature off entirely is the install site’s job, not this widget’s.

Parameters:

on – truthy to animate, falsy to pause.

set_backdrop(source) → None[source]

Paint source under the animation instead of the flat colour.

The animation composites (adds on dark, multiplies on light), so a wallpaper handed in here shows through it rather than being replaced. None goes back to the flat fill.

Parameters:

source – an image path, QPixmap or QImage, or None; anything that does not load as a non-null pixmap also gives the flat fill.

set_background_color(color: PySide6.QtGui.QColor | str) → None[source]

Set the flat fill under the animation.

This is also what tells the engine whether it is painting on a dark or a light page, which picks additive versus multiply compositing — so it must be called on a live theme switch, or a dark-tuned frame ends up on a white page.

Parameters:

color – a QColor or any string QColor accepts; an invalid colour keeps the current one, and alpha is forced opaque. The widget then stops following the application theme.

Update dot flashes through the normal serialized engine mutation.

Parameters:

value – percentage of visible dots flashing white, clamped to zero through ten; zero disables blinking.

set_blur(value: float) → None[source]

Keep the retired softening control compatible with older callers.

Parameters:

value – a legacy value; displayed themes remain unsoftened.

set_density(value: float) → None[source]

Set the element-count multiplier. Clamped to DENSITY_RANGE.

Parameters:

value – the density multiplier, converted with float.

set_direction(name: str) → None[source]

Set the starfield direction. An unknown name is ignored.

Parameters:

name – one of DRIFT_DIRECTIONS.

set_field_effects(effects: dict) → None[source]

Apply saved Spaceout field switches under the renderer’s lock.

Parameters:

effects – saved field-effect switches keyed by effect name.

Returns:

None.

set_fps(fps: int) → None[source]

Cap the frame rate. Caps the shading thread with it, so a lowered cap actually reduces the work rather than just how much of it is shown.

Parameters:

fps – frames per second, converted with int and clamped to MIN_FPS to MAX_FPS.

set_gravity_radius(value: float) → None[source]

Apply local pointer reach while excluding a concurrent shade pass.

Parameters:

value – fraction of the shorter screen edge, clamped to [0, 1]; zero disables hover gravity; the explicit field handle remains available.

set_palette(name: str) → None[source]

Switch colour set, live, keeping the motion exactly where it is.

Raises ValueError if the current theme does not offer name — an explicit request for a palette is not something to silently substitute. The one exception is the spaceout dressing, where the request is replaced rather than refused, for the reason given in dressed().

Parameters:

name – a palette the current theme offers.

set_popup_wave_frequency(value: float) → None[source]

Change popup waves without changing pointer reach or dot population.

Parameters:

value – popup waves per minute, clamped to zero through sixty; zero disables recurring waves. The separate ripple switch controls discrete click, container and window feedback.

set_resolution(value: float) → None[source]

Set the detail multiplier. Clamped to RESOLUTION_RANGE.

Parameters:

value – the resolution multiplier, converted with float.

set_ripples_enabled(enabled: bool) → None[source]

Switch all field ripples independently of mouse gravity and dragging.

Parameters:

enabled – whether recurring and discrete field waves are enabled.

Returns:

None.

set_size_scale(value: float) → None[source]

Set the element-size multiplier. Clamped to SIZE_RANGE.

Parameters:

value – the size multiplier, converted with float.

set_speed(value: float) → None[source]

Set the motion multiplier. Clamped to SPEED_RANGE.

Takes effect on the next step, so nothing already on screen moves.

Parameters:

value – the speed multiplier, converted with float.

set_theme(name: str) → None[source]

Switch animation, live. Raises ValueError on an unknown name.

The clock carries over and the old engine is dropped — there is no second timer and no second engine, so a user flipping through the menu cannot leave anything ticking behind them. If the current palette is not one this theme offers, it downgrades to the theme’s default (see palettes_for() for why the lists differ).

Under the spaceout dressing every request lands on the fractal, whoever asked and for whatever — including spacr.qt.preferences.apply_ambient_preferences(), which calls this with the stored animation on every settings save. See dressed().

Parameters:

name – a paintable theme name.

set_time(seconds: float) → None[source]

Jump the animation clock and repaint.

Parameters:

seconds – the new clock value, in animation seconds (the clock advance_frame() steps, already scaled by speed).

shading_thread_alive() → bool[source]

Whether a shading thread is running for this backdrop.

The CPU guarantee used to be a claim about a timer and is now also a claim about a thread, so it needs something to assert on: a backdrop behind a screen nobody is looking at must not be keeping a core warm.

showEvent(event)[source]

Start animating, and follow the window this widget belongs to.

Parameters:

event – the Qt show event.

size_scale() → float[source]

The element-size multiplier; 1.0 is the shipped animation.

Not size() — QWidget already owns that name and it returns a QSize.

speed() → float[source]

The motion multiplier; 1.0 is the shipped animation.

start() → None[source]

Start the animation timer and shading worker if not already running.

The timer and worker share one lifetime. Hiding the widget, switching tabs, minimizing the window, or disabling ambient animation stops both. A widget that is never shown uses the synchronous rendering path.

stop() → None[source]

Stop ticking and retire the shading thread. Costs exactly nothing while stopped — no timer, no thread, and no frame held in memory.

Dropping the published frame matters because these screens stay built: a dozen module screens the user has visited would otherwise each keep a slot warm behind a tab nobody is on, which is 2 MiB apiece for the aurora. The next start() shades a replacement before it starts the thread, so there is nothing to show for it.

theme() → str[source]

Which ambient theme is being painted.

Returns:

the theme’s name.

time() → float[source]

The animation clock, in seconds.

property engine: AmbientEngine[source]

The live engine. Replaced wholesale by set_theme().

class spacr.qt.widgets.ambient.AuroraEngine(*args, **kwargs)[source]

Bases: _BufferedEngine

Folded curtains of vertical rays, rippling along their own length.

Rays rise from an irregular folded lower edge, fan gently toward the sky, and breathe at different rates. A diffuse sheet joins the rays without a flat rectangular top or repeated texture tiles. The frame raster is native at ordinary Detail, within the physical screen-pixel budget, and is returned with independent ownership. AURORA_BUFFER_EDGE remains the legacy comparison edge; it no longer caps the active buffer.

geometry() yields (x, y_bottom, visible_height, brightness) per sampled column of every curtain, in pixels, AURORA_COLUMNS + 1 of them per curtain in curtain order. The painter builds its paths from exactly those numbers, so a test that tracks a fold crest through geometry is tracking the crest that is on screen. brightness is the travelling surge. Seeded irregular ray positions and independent continuous length and brightness cycles modulate the sampled sheet, with tapered tops.

Roll the aurora’s bands and their drift.

anchor(curtain: Curtain, height: int) → Tuple[float, float][source]

(ramp zero, ray length) for one curtain, in pixels.

The ramp’s zero is the altitude the emission stops at, so it sits below everything the fold and the tilt can do — that is what keeps every column of the sheet inside its own colour ramp.

buffer_scale(width: int, height: int) → float[source]

Report the aurora sampling ratio for explicit detail controls.

buffer_size(width: int, height: int) → Tuple[int, int][source]

Sample native display pixels within the actual screen budget.

count() → int[source]

How many curtains are painted right now.

curtain_color(curtain: Curtain, quantised: bool = False) → PySide6.QtGui.QColor[source]

The curtain’s body colour right now.

Always built from the palette’s first colour, wandering up to AURORA_HUE_BLEND of the way towards one of the others and back. Every curtain shares that body colour on purpose: the body of an aurora is a single emission line — 557.7 nm oxygen — and the palette’s remaining entries are the top and the fringe, which the ramp puts above and below it. Giving curtain two a red body and curtain three a violet one, which is what indexing the palette by curtain would do, is the one thing that stops the whole theme reading as an aurora.

Parameters:
  • curtain – simulated curtain whose colour index and shimmer phase choose the palette target and its current blend toward it.

  • quantised – snap the shimmer to AURORA_HUE_STEPS so the ray tile can be cached.

fold(curtain: Curtain, u: float, t: float) → float[source]

Fold displacement at position u along the arc, as a fraction of the canvas height.

u runs 0..1 from one end of the arc to the other. Each component is sin(2*pi*(u - v*t)/lambda): at a fixed time it is a shape in u, and as t advances that shape slides along u at v while the arc itself goes nowhere. That is the whole difference between an aurora and a curtain being dragged sideways, and it is the one property of this engine worth testing directly.

Scaled by the size setting along with everything else: a curtain half the height with folds the same depth is a different phenomenon, not a smaller one.

geometry(width: int, height: int) → Tuple[tuple, ...][source]

Every travelling wave, evaluated along every arc.

The loop body is fold() and pulse() written out with their constant parts hoisted — sin(2*pi*(u - v*t)/lambda + phi) is sin(k*u + (phi - k*v*t)), and k and the bracket do not depend on the column. It is the same arithmetic; it is here rather than behind those two calls because this runs a hundred and twenty times a frame and a Python call is not free. test_aurora_geometry_ is_the_model_it_documents holds the two forms together.

hue_phase(curtain: Curtain) → float[source]

Where this curtain’s slow colour shimmer stands, in 0..1.

pulse(curtain: Curtain, u: float, t: float) → float[source]

The surge’s brightness at u, in 0..1. Another travelling wave, deliberately faster and shorter than every fold.

ramp_colors(curtain: Curtain, quantised: bool = False) → Dict[str, PySide6.QtGui.QColor][source]

The four palette roles for one curtain: the body, the high red, the low fringe, and the overlap between body and high.

Fixed roles rather than a rotation, because the vertical order is physics. With borealis, main is the 557.7 nm green, high the 630.0 nm red, fringe the 427.8 nm violet and blend the pale yellow-green where the first two overlap. A palette with fewer than four colours reuses what it has.

ray_lengths(curtain: Curtain) → Tuple[float, ...][source]

Each ray’s current length, as a fraction of the full one.

One value per entry in AURORA_TILE_RAYS, quantised into AURORA_LENGTH_STEPS so the tile stays cacheable – a length that followed the clock exactly would rebuild every tile every frame, which is the cost the tile cache exists to avoid.

The periods in AURORA_RAY_LIFE are deliberately not multiples of one another, and the curtain’s own phase is added, so two curtains never breathe together either.

class spacr.qt.widgets.ambient.Blob[source]

One drifting, pulsing blob, in normalised units.

class spacr.qt.widgets.ambient.BlobsEngine(*args, **kwargs)[source]

Bases: _BufferedEngine

Diffuse colour blobs, drifting and pulsing.

Motion is two independent sines per blob rather than a random walk, so position is a pure function of the clock: no accumulated error, and set_time can jump anywhere.

geometry() yields (cx, cy, radius) per blob, in pixels.

Start with no buffer – the first shade allocates one.

count() → int[source]

How many blobs are painted right now.

geometry(width: int, height: int) → Tuple[tuple, ...][source]

The shapes to draw at the current time, for a widget this size.

GEOMETRY, NOT PAINTING, so the layout can be computed on a worker thread and tested without a QPainter – an engine owns no widget and no timer, which is what makes the animation deterministic.

Parameters:
  • width – the widget’s width in pixels.

  • height – its height in pixels.

Returns:

one tuple per shape, in draw order.

class spacr.qt.widgets.ambient.BokehEngine(*args, **kwargs)[source]

Bases: _BufferedEngine

Defocused points of light: flat discs with bright rims.

geometry() yields (cx, cy, radius, focus) per disc, in pixels, with focus in 0..1 — the same tuple the painter builds its gradients from, so a test that asserts on it is asserting on the frame.

Start with no buffer – the first shade allocates one.

count() → int[source]

How many discs are painted right now.

geometry(width: int, height: int) → Tuple[tuple, ...][source]

The shapes to draw at the current time, for a widget this size.

GEOMETRY, NOT PAINTING, so the layout can be computed on a worker thread and tested without a QPainter – an engine owns no widget and no timer, which is what makes the animation deterministic.

Parameters:
  • width – the widget’s width in pixels.

  • height – its height in pixels.

Returns:

one tuple per shape, in draw order.

class spacr.qt.widgets.ambient.Cell[source]

One drifting cell, in normalised units.

class spacr.qt.widgets.ambient.CellsEngine(*args, **kwargs)[source]

Bases: _BufferedEngine

Cells drifting through the field, turning as they go.

geometry() yields (cx, cy, major, minor, angle) per cell, in pixels and radians.

Start with no buffer – the first shade allocates one.

count() → int[source]

How many cells are painted right now.

geometry(width: int, height: int) → Tuple[tuple, ...][source]

The shapes to draw at the current time, for a widget this size.

GEOMETRY, NOT PAINTING, so the layout can be computed on a worker thread and tested without a QPainter – an engine owns no widget and no timer, which is what makes the animation deterministic.

Parameters:
  • width – the widget’s width in pixels.

  • height – its height in pixels.

Returns:

one tuple per shape, in draw order.

class spacr.qt.widgets.ambient.Curtain[source]

One aurora curtain, in normalised units.

class spacr.qt.widgets.ambient.Disc[source]

One out-of-focus point source, in normalised units.

class spacr.qt.widgets.ambient.DriftEngine(*args, **kwargs)[source]

Bases: AmbientEngine

A slow starfield in three parallax layers.

At full Detail dots are drawn directly and stay crisp. Lower Detail samples the same canvas population through a bounded image buffer. Everything is batched into one drawPoints call per (colour, layer, alpha step) bucket with a cached pen; the per-call overhead of a pen change dominates this theme, not the pixels.

geometry() yields (x, y, diameter) per painted particle, in pixels.

Roll the starfield’s three parallax layers.

alpha_scale() → float[source]

Untouched, unlike every buffered theme.

The compensation on the base class exists because overlapping translucent fields pile up additively. A starfield does not have that problem — measured, its mean frame lightness moves from 0.076 to 0.077 across the whole density range, because a couple of hundred dots light 0.65 % of the page and almost never land on each other. Dividing their alpha by three would not un-brighten anything; it would hide two thirds of the stars in the background.

buffer_size(width: int, height: int) → Tuple[int, int][source]

Sample dots at the chosen Detail within the display pixel ceiling.

count_for(width: int, height: int) → int[source]

How many of the pool this canvas gets.

Area-based, so a small window is not a snowstorm, and then scaled by the density setting — which is the only one of the two the user controls.

dot_size(layer: int) → float[source]

Diameter of a layer dot, in pixels, under the size setting.

geometry(width: int, height: int) → Tuple[tuple, ...][source]

(x, y, diameter) per painted particle, in pixels.

Three directions, one expression each, and all three are pure functions of the clock — no accumulated state, so set_time still jumps anywhere and two engines on the same seed still agree.

up and down are the same shared vector with opposite signs. random gives every speck its own heading and adds a slow wander across it, which is a smooth wandering path rather than a straight line with a wobble; the headings are isotropic, so the field spreads and mixes instead of travelling.

halo_size(layer: int) → float[source]

Diameter of the soft pass around a dot. Equal to the dot itself at blur 0, which is how the default frame stays exactly what it was.

paint(painter: PySide6.QtGui.QPainter, width: int, height: int) → None[source]

Draw the same population through the selected pixel resolution.

property work: float[source]

sampling is capped at native display resolution.

Type:

Density only

class spacr.qt.widgets.ambient.Form[source]

Bases: NamedTuple

Sampling geometry for the primary fractal or a derived bud.

Parameters:
  • cx – Centre x-coordinate in buffer pixels.

  • cy – Centre y-coordinate in buffer pixels.

  • scale – Buffer pixels per unit of the complex plane.

  • angle – Sampling rotation in radians.

  • c_re – Real component of the Julia-set constant.

  • c_im – Imaginary component of the Julia-set constant.

  • iterations – Number of map iterations.

  • origin_re – Real component of the sampled complex-plane origin.

  • origin_im – Imaginary component of the sampled origin.

  • swirl – Angular twist per logarithmic radius interval.

  • tunnel – Palette traversals per logarithmic radius interval.

  • scroll – Palette-ring offset.

  • radius – Form radius in buffer pixels.

  • age – Normalised bud age; zero for the primary form.

  • bud – Whether this geometry describes a bud.

class spacr.qt.widgets.ambient.FractalEngine(*args, **kwargs)[source]

Bases: _BufferedEngine

Render a deterministic, animated Julia-set field.

The engine evaluates z <- z**2 + c for a primary form and optional derived buds. Hashed continuous state controls depth, density, motion, and population as a pure function of seed and animation time. Render cost is measured between frames and used to reduce buffer resolution and bud count when necessary. geometry() returns the primary Form first.

Start with no buffer – the first shade allocates one.

advance(dt: float) → None[source]

Advance the clock and adapt render capacity from recent costs.

afford() → float[source]

Return the guarded render-capacity fraction in [FRACTAL_AFFORD_FLOOR, 1].

beat() → float[source]

Return beat intensity from zero between peaks to one at a peak.

beat_phase() → float[source]

Return the continuously integrated beat phase in cycles.

bud_slots() → int[source]

Return the number of bud slots allowed by state and render cost.

buds(width: int, height: int) → Tuple[Form, ...][source]

Return active bud geometries for a render-buffer size.

constant() → Tuple[float, float][source]

Return the current Julia-set constant as (real, imaginary).

The constant follows an inset path along the main cardioid, with continuous pace modulation.

depth() → float[source]

Return the smoothed vortex-depth state in [0, 1].

fixed_point() → Tuple[float, float][source]

Return the repelling fixed point of the current Julia set.

The selected root is (1 + sqrt(1 - 4c)) / 2 with |2 * beta| > 1.

frame_budget() → float[source]

Return the target duration of one shading pass in milliseconds.

geometry(width: int, height: int) → Tuple[Form, ...][source]

Return frame geometries with the primary form first.

iterations() → int[source]

Return the guarded Julia-map iteration count for this frame.

resolution_edge() → int[source]

Return the guarded maximum render-buffer edge in pixels.

swing(channel: int) → float[source]

Return wander() for channel mapped to [-1, 1].

turn() → float[source]

Return the current vortex rotation in turns.

view(height: int) → Tuple[float, float, float][source]

Return (origin_re, origin_im, scale) for the primary form.

wander(channel: int, at: float | None = None) → float[source]

Return continuous deterministic state noise for one channel.

Parameters:
  • channel – Independent state-channel index.

  • at – Animation time in seconds. None uses the engine clock.

Returns:

Value in [0, 1].

class spacr.qt.widgets.ambient.Motion[source]

Bases: NamedTuple

Every user control that shapes the animation, in one value.

A named tuple rather than five arguments because the set grows: it went from three to five in one change, and every install site that had unpacked a plain tuple would have broken.

Parameters:
  • blur – softness of the painted shapes; the widget clamps it to BLUR_RANGE.

  • speed – animation-clock multiplier, clamped to SPEED_RANGE.

  • size – element-size multiplier, clamped to SIZE_RANGE.

  • resolution – detail (render buffer) multiplier, clamped to RESOLUTION_RANGE.

  • density – element-count multiplier, clamped to DENSITY_RANGE.

  • direction – starfield drift direction, one of DRIFT_DIRECTIONS; an unknown name falls back to the default.

class spacr.qt.widgets.ambient.PaletteSpec[source]

Bases: NamedTuple

One named colour set: what to call it and what it is made of.

class spacr.qt.widgets.ambient.Particle[source]

One drifting dot, in normalised units.

class spacr.qt.widgets.ambient.ResonanceEngine(*args, **kwargs)[source]

Bases: _BufferedEngine

Chladni figures: sand on a plate that is driven by what is playing.

A floor with particles on it, and the particles are sand on a vibrating plate. The physics is in spacr.qt.resonance and is worth one paragraph here because it is what makes this theme different from a particle system with a music-shaped wobble: a plate driven at one of its resonances has lines that do not move, sand is thrown off everywhere else and comes to rest along them, and the figure IS the nodal set. So there is a right answer for where a particle goes, and the drive decides which figure it is and how hard the sand is being shaken rather than deciding the motion directly.

WHAT DRIVES IT. spacr.qt.resonance.playing_moment() – the music bed’s own precomputed envelope and spectrum, read at the position the bed is at. Nothing captures the machine’s audio; nothing here can hear anything spaCR is not playing. With sound off it returns silence and the plate idles on its own clock, which is the case almost everybody sees because sound is off on a fresh install.

WHY THE DRIVE IS READ IN advance() AND NOT WHERE IT IS USED. Every other engine promises that a frame is a pure function of (seed, clock, size), and two tests hold it down: one shades the same clock on two threads and compares the bytes, another steps one engine twelve times and jumps a second straight to the same clock. Reading a real-time signal inside shade() would break both, and it would break them on the SHADING THREAD, where the failure is a picture that differs from the one the GUI thread would have drawn. So the drive is an INPUT, set on the GUI thread between frames under the engine lock exactly as set_palette() is, and the promise becomes “a pure function of (seed, clock, size, drive)” – which is the same promise while nothing is playing, and that is when the tests run.

geometry() yields (x, y, brightness) per painted particle, in pixels.

Start with no floor and no canvas; the first shade builds both.

advance(dt: float) → None[source]

Step the clock and read what is playing.

The one place a real-time signal enters this engine, and it is here because this runs on the GUI thread between frames while the shading thread is locked out – see the class docstring. It costs a lock, an index and two array reads; measured at 6.8 us with the music bed playing, against the 41 ms a frame the cap allows.

What is read is damped by answering() before it is stored, so the Speed preference reaches the music-driven half of the theme as well as the clock.

answering(moment)[source]

moment scaled by how much of it the Speed setting lets in.

THE CLOCK IS NOT THE WHOLE ANIMATION HERE, WHICH IS WHY THIS EXISTS. AmbientEngine.advance() multiplies dt by speed, and that is the whole of the Speed preference for every other theme, because every other theme’s motion is a function of the clock alone. Half of this one is a function of the music instead: the throw on an onset, the per-band brightness and the figure the spectrum’s centre of mass asks for. At the bottom of SPEED_RANGE the breath, the mode walk and the wander all crawl at a tenth, and a kick would still have thrown the sand RESONANCE_THROW of the plate twice a second – leaving the busiest movement in the theme running at full rate for somebody who set the slider to its minimum precisely to stop that.

So the share let in is min(1, speed): the shipped setting and anything above it hear the music in full, and turning the animation down turns the reaction down with it, until at the minimum the plate is the idle plate. Above 1.0 it is not scaled up, because every field of a Moment is already 0 to 1 against the loop’s own loudest: there is nothing above full to give.

Parameters:

moment – what spacr.qt.resonance.playing_moment() returned.

Returns:

the same moment at speed 1.0 or above, a damped copy below it.

energy() → float[source]

How hard the plate is being driven, 0 to 1.

The louder of what is playing and the idle breath, so a quiet passage never takes the picture below what silence would have drawn. That is the rule that makes “it idles beautifully in silence” and “it reacts to the music” the same code path.

geometry(width: int, height: int) → Tuple[tuple, ...][source]

Every grain as (x, y, brightness) in pixels.

Parameters:
  • width – canvas width in pixels.

  • height – canvas height.

Returns:

one tuple per painted grain.

mode_position() → float[source]

Where along spacr.qt.resonance.MODES the figure sits.

plate(width: int, height: int) → Tuple[float, float, float][source]

The square the figure is drawn in: (left, top, side) in px.

sand(count: int | None = None)[source]

Where the sand is now, and how brightly each grain shows.

The whole simulation, and it is four numpy passes: wander the seeded starting points, build the lattice for the mode the drive asks for, relax onto its nodal lines, and throw the result off them by whatever the last onset was worth.

Parameters:

count – how many grains; the density setting’s own count by default.

Returns:

(x, y, brightness) in plate units, x and y in 0..1.

class spacr.qt.widgets.ambient.RippleEngine(*args, **kwargs)[source]

Bases: _BufferedEngine

Concentric rings expanding from a few sources and fading as they grow.

Each ring is a radial gradient annulus rather than a stroked circle: a stroked one is line work, which is both expensive and far too crisp for something meant to sit behind a settings form.

geometry() yields (cx, cy, radius, fade) per ring, in pixels, with fade in 0..1 — 0 as the ring is born and as it dies at the edge of its reach, 1 halfway.

Start with no buffer – the first shade allocates one.

count() → int[source]

How many ripple sources are painted right now.

geometry(width: int, height: int) → Tuple[tuple, ...][source]

The shapes to draw at the current time, for a widget this size.

GEOMETRY, NOT PAINTING, so the layout can be computed on a worker thread and tested without a QPainter – an engine owns no widget and no timer, which is what makes the animation deterministic.

Parameters:
  • width – the widget’s width in pixels.

  • height – its height in pixels.

Returns:

one tuple per shape, in draw order.

class spacr.qt.widgets.ambient.Source[source]

One ripple origin, in normalised units.

spacr.qt.widgets.ambient.animation_label(name: str) → str[source]

Human label for an entry of ANIMATION_CHOICES.

“None” rather than “Off”: the row is called Animation and this is one of the animations it can be set to, the way a font size can be set to zero.

Parameters:

name – NO_ANIMATION or a paintable theme name; any other name raises ValueError.

spacr.qt.widgets.ambient.animation_note(name: str) → str[source]

One-line description of an animation choice, for a tooltip.

The note for “None” states the cost, because that is the only reason a reader picks it — and the claim is asserted rather than advertised: see tests/qt/test_ambient_none.py, which counts painted frames over a real second instead of trusting this sentence.

Parameters:

name – NO_ANIMATION or a paintable theme name; any other name raises ValueError.

spacr.qt.widgets.ambient.coerce_palette(theme: str, palette: str) → str[source]

palette if theme offers it, else that theme’s default.

Only for stored values — a preferences file that still names the palette the user picked under a different theme should not stop a screen from being built. An unknown name is still an error: it is a bug, not a stale setting.

spacr.qt.widgets.ambient.default_palette_for(theme: str) → str[source]

The palette theme falls back to — DEFAULT_PALETTE when it is on offer, otherwise the first one listed.

Parameters:

theme – a paintable theme name; an unknown one raises ValueError.

spacr.qt.widgets.ambient.dressed(theme: str, palette: str) → Tuple[str, str][source]

Resolve the ambient theme and palette for the current launch mode.

Standard launches preserve the requested pair. A Spaceout launch uses its own stored selection, or defaults to the evolving field before one is saved.

Parameters:
  • theme – the requested theme name, not validated here.

  • palette – the requested palette name, treated the same way.

spacr.qt.widgets.ambient.drift_direction_label(name: str) → str[source]

Human label for a starfield direction, for a menu.

Parameters:

name – one of DRIFT_DIRECTIONS; any other value raises ValueError.

spacr.qt.widgets.ambient.drift_direction_note(name: str) → str[source]

One-line description of a starfield direction, for a tooltip.

Parameters:

name – one of DRIFT_DIRECTIONS; any other value raises ValueError.

spacr.qt.widgets.ambient.field_ripple_for_widget(widget, edge=None, rect=None, strength=1.0) → None[source]

Publish settled window or panel feedback to its visible field backdrop.

Parameters:
  • widget – the resized, moved or collapsed widget.

  • edge – an optional snapped window side.

  • rect – optional final rectangle in widget-local coordinates.

  • strength – positive enables release feedback; zero disables it.

Returns:

None.

spacr.qt.widgets.ambient.install_ambient(host: PySide6.QtWidgets.QWidget, layout=None, *, theme: str = DEFAULT_THEME, palette: str = DEFAULT_PALETTE, backdrop=None, corner_radius: int = 0, **kwargs) → AmbientWidget[source]

Put a live ambient backdrop behind host.

The widget becomes a child of host, tracks its geometry, and is lowered to the bottom of the sibling stacking order so every screen widget paints in front of it. It takes no focus and no mouse events, and it does not tick until host is actually on screen.

Note that a backdrop is only as visible as its siblings are transparent: under dark and light every container is an opaque page colour, and an animation behind them reaches the eye through nothing but the few pixels of layout spacing. The caller is responsible for clearing those surfaces first — see AppScreen._clear_page_surfaces.

Parameters:
  • host – the screen the animation sits behind.

  • layout – accepted for signature compatibility with spacr.qt.widgets.dna_rain.install_dna_rain(), which appends its settings bar to it. The ambient backdrop has no on-screen controls — it is configured in Preferences — so nothing is added here, and the two installers stay interchangeable at a call site.

  • theme – one of AMBIENT_THEMES.

  • palette – one of palettes_for() for that theme.

  • backdrop – wallpaper to composite over; see AmbientWidget.set_backdrop().

  • corner_radius – Radius in pixels used to clip the backdrop. The default of zero leaves square corners. For a frameless dialog containing a rounded card, use the card’s radius so the backdrop does not extend beyond its corners.

  • kwargs – forwarded to AmbientWidget (background, fps, seed, blur, speed, size, resolution, density, direction). Everything from blur on defaults to the user’s preferences, so a caller that does not care about them should not pass them.

Returns:

the widget, already shown and lowered.

spacr.qt.widgets.ambient.is_animation_choice(name) → bool[source]

True for anything the Animation preference may hold — including NO_ANIMATION, which is_valid_theme() rejects because it cannot be painted.

Parameters:

name – the value to test, typically a stored preference.

spacr.qt.widgets.ambient.is_dark_background(color: PySide6.QtGui.QColor | str) → bool[source]

True when color is dark enough for additive compositing.

spacr.qt.widgets.ambient.is_valid_drift_direction(name) → bool[source]

True when name is one of DRIFT_DIRECTIONS. Never raises.

Parameters:

name – the value to test.

spacr.qt.widgets.ambient.is_valid_palette(theme, palette) → bool[source]

True when palette is offered by theme. Never raises.

Parameters:
  • theme – the theme name; an unknown theme offers nothing.

  • palette – the palette name to look for among theme’s palettes.

spacr.qt.widgets.ambient.is_valid_theme(name) → bool[source]

True when name is one of AMBIENT_THEMES.

The predicate exists so a caller validating stored preferences does not have to catch ValueError from the strict accessors below.

Parameters:

name – the value to test, typically a stored preference.

spacr.qt.widgets.ambient.make_engine(theme: str, palette: str, background: PySide6.QtGui.QColor | str, seed: int | None = None, blur: float = DEFAULT_BLUR, speed: float = DEFAULT_SPEED, size: float = DEFAULT_SIZE, resolution: float = DEFAULT_RESOLUTION, density: float = DEFAULT_DENSITY, direction: str = DEFAULT_DRIFT_DIRECTION, blink_percent: float = DEFAULT_BLINK_PERCENT, popup_wave_frequency: float = 0.0) → AmbientEngine[source]

Build the engine for theme/palette. Raises on unknown names.

Everything after seed is a user control; the defaults are the shipped animation exactly.

spacr.qt.widgets.ambient.palette_colors(theme: str, palette: str) → Tuple[str, ...][source]

The #rrggbb colours behind palette, for theme.

Parameters:
  • theme – a paintable theme name.

  • palette – a palette theme offers; an unknown theme, or a palette the theme does not offer, raises ValueError.

spacr.qt.widgets.ambient.palette_label(theme: str, palette: str) → str[source]

Human label for palette as offered by theme.

Parameters:
  • theme – a paintable theme name.

  • palette – a palette theme offers; an unknown theme, or a palette the theme does not offer, raises ValueError.

spacr.qt.widgets.ambient.palette_note(theme: str, palette: str) → str[source]

One-line description of palette, for a tooltip.

Parameters:
  • theme – a paintable theme name.

  • palette – a palette theme offers; an unknown theme, or a palette the theme does not offer, raises ValueError.

spacr.qt.widgets.ambient.palettes_for(theme: str) → Tuple[str, ...][source]

The palette names theme offers, in menu order.

Never empty. Raises ValueError on an unknown theme rather than returning (), because an empty tuple reads as “this theme has no palettes” and would quietly leave a settings menu blank.

Parameters:

theme – a paintable theme name.

spacr.qt.widgets.ambient.preferred_motion() → Motion[source]

The animation controls, from the user’s preferences.

Read here rather than passed in by every install site, for the same reason _theme_background() is: the two callers that build ambient widgets are a module screen and Home, and neither of them has any business knowing what the animation’s knobs are called. Falls back to the shipped defaults if preferences cannot be read at all.

spacr.qt.widgets.ambient.rebuild_the_spaceout_backdrops() → int[source]

Rebuild every running spaceout backdrop whose build settings changed.

Returns:

how many were rebuilt.

WHY A SAVED PATTERN USED TO WAIT FOR A RESTART. The window builds ONE backdrop behind the dock and the page and keeps it for the session. Saving Preferences pushed the runtime numbers into it (apply_saved_controls) but the pattern, backend, quality and scale are fixed when it is constructed, and nothing constructed it again – the old note that “changing a screen” would do it stopped being true when the backdrop moved from the screens to the window.

The replacement is built BEFORE the old one is retired, so a GPU that cannot be had right now leaves the running backdrop on screen rather than none. It inherits the old one’s RuntimeControls, its paused state and its visibility, and every reference to the old one on its host or window is moved to it. A heavy import holding the GL lock is waited out on a timer, as at startup.

spacr.qt.widgets.ambient.screen_pixels(widget: PySide6.QtWidgets.QWidget | None = None) → int[source]

Return the device-pixel count of the screen containing widget.

The primary screen is used when no widget screen is available. Headless or invalid screen information falls back to BUFFER_MAX_PIXELS.

spacr.qt.widgets.ambient.theme_label(name: str) → str[source]

Human label for name, for a menu. Raises on an unknown theme.

Parameters:

name – a paintable theme name; any other raises ValueError.

spacr.qt.widgets.ambient.theme_note(name: str) → str[source]

One-line description of name, for a tooltip.

Parameters:

name – a paintable theme name; any other raises ValueError.

spacr.qt.widgets.ambient.total_frames_painted() → int[source]

How many ambient frames this process has painted. For tests.

Nested helpers

DriftEngine._configure.roll(i: int) → Particle

One particle, on the drift layer its index falls in.

spacr/qt/widgets/ambient.py:2744