spacr.qt.widgets.dna_rain

Live DNA rain — a Matrix-style ATGC cascade painted behind a screen.

This is the Qt descendant of spacr.gui_elements.generate_dna_matrix(), which renders the same idea offline to a GIF/MP4. The ideas carried over from it are the ones that make the effect read as “rain” rather than as a scrolling table:

  • every column picks its own string length (10-100 glyphs, capped so it cannot dwarf the canvas — see MAX_STRING_SCREENS),

  • every column starts at its own row above the canvas, so columns do not enter in lockstep,

  • the leading glyph is drawn in a different colour from the trail,

  • a short run of glyphs inside each string is highlighted.

Three deliberate departures:

ATGC only

The offline renderer had a lowercase_prob that mixed a/t/g/c in. Here the alphabet is exactly A, T, G, C — plus the spaCR splice below.

Live, not rendered

Nothing is written to disk. DnaRainWidget paints frames in real time, driven by a capped timer that stops whenever the widget is not on screen — this sits behind the sequencing pipeline, which is doing real work, so it must not cost a core.

The tail fades, not the head

The offline version faded the glyphs nearest the head. That reads backwards; here the trailing (upper) end of each string fades out.

The spaCR splice

Very infrequently a column carries the literal word spaCR, spelled down the column one letter per cell, exactly like the bases around it:

s
p
a
C
R

The casing is load-bearing; the orientation is what makes it part of the rain. It used to be stored as a single token in a single cell and drawn horizontally, overflowing rightwards across its neighbours — which read as a label pasted over the effect rather than as part of it, and cost the renderer a whole second paint pass, measured token widths, widened dirty rectangles and a cleared backing rectangle so neighbouring glyphs did not tangle with the letters. None of that machinery exists now: every cell holds exactly one character, so the word is cached and blitted like any other glyph and cannot overdraw anything. See SPACR_SPLICE_PROBABILITY for the rate and what it works out to in practice.

Legibility

Screen content sits in front of this widget. It therefore never takes focus, is transparent to mouse events, lowers itself to the bottom of the sibling stacking order, and paints its glyphs at DEFAULT_OPACITY over the theme background so anything in front of it stays readable.

Settings

Colour (fixed, or a random hue per column), speed, visibility and font size, all live. They are not on the screen: they live in a popover behind a DNA button beside the AI toggle — see spacr.qt.widgets.dna_rain_settings. DnaRainSettingsBar is the panel itself, which lays out either as that popover’s grid or as the original single row.

Cost

This runs behind the sequencing pipeline, so it has to be close to free. Three things get it there, and all three were measured rather than assumed (1920x1080, 120 columns, 67 rows):

  1. The timer stops whenever the widget is not on screen — hidden, or in a minimised window. Zero frames, zero CPU.

  2. Each string is pre-rendered once into an opaque pixmap with the background already composited in, and blitted at its current offset every frame. Drawing 5400 glyphs with drawText costs 35 ms a frame; the same glyphs as 120 opaque strips cost 0.46 ms — and an opaque strip is 25x cheaper to blit than a translucent one, which is why the alpha is baked in rather than applied by the painter. A strip is re-rendered only when its column respawns or the styling changes; the cache is ~7 MB at 1920x1080. DnaRainWidget.set_backdrop() gives that up deliberately — a picture under the rain is not a constant to bake against — so the translucent path is taken only by the themes that have a wallpaper to show, and dark and light keep the numbers above.

  3. Only the columns that moved are repainted. Positions are quantised to whole cells, so a column is dirty only when its integer row changes — slow columns cost nothing on most ticks. See MAX_DIRTY_RECTS for where partial repaints stop paying.

Together: 0.53 ms a frame, 3.2 % of one core at 1920x1080 and 60 fps, and 0.00 % while off screen. Random colour does not move that number — the hues are baked into the same cached strips, and a pen set is built once per whole degree of hue (_hue_bucket()) rather than per column, per frame.

Attributes

Classes

Column

One falling string.

DnaRainEngine

Qt-free simulation of the falling columns.

DnaRainSettingsBar

Colour / speed / visibility / font-size controls for a rain widget.

DnaRainWidget

The live backdrop: paints DnaRainEngine at a capped rate.

Functions

blend(→ PySide6.QtGui.QColor)

Linear RGB blend, t=0 -> a, t=1 -> b.

derive_head_color(→ PySide6.QtGui.QColor)

Return the leading-glyph colour for a trail colour of base.

install_dna_rain(→ DnaRainWidget)

Put a live DNA rain behind host, and a DNA button in the chrome.

random_hue_color(→ PySide6.QtGui.QColor)

Return base moved to hue, keeping the family it belongs to.

Module Contents

class spacr.qt.widgets.dna_rain.Column[source]

One falling string.

Variables:
  • tokens – one entry per cell; usually a single base, but a spliced entry is the whole spaCR word.

  • length – number of cells (== len(tokens)).

  • speed – base fall rate in cells/second, before the multiplier.

  • head – row of the leading glyph, fractional and often negative (the string starts above the canvas).

  • rowfloor(head) — the cell the head occupies. Still used for the dirty-span arithmetic, which works in rows.

  • y_px – the strip’s top edge in PIXELS, rounded from the fractional head. THIS is what the column is painted at and what decides whether it is dirty. Painting at row * cell quantised every column to whole glyph heights, so a slow column (4 cells/s) sat still for six frames and then jumped a whole character — the stepping that reads as choppy. Raising the frame rate cannot fix that on its own: the position simply has fewer places it is allowed to be.

  • hi_start – first cell index of the highlighted run.

  • hi_end – one past the last cell index of the highlighted run.

  • word_index – cell index of the multi-character token, or -1.

  • generation – bumped on every respawn; the widget’s pre-rendered strip cache keys off it.

  • hue – this string’s own hue in 0..1, re-rolled on every respawn. Only read when the widget is in random-colour mode; it is rolled unconditionally, and from a stream of its own, so that turning random colour on or off cannot change where anything falls (see DnaRainEngine).

generation: int = 0[source]
property has_word: bool[source]

True when this string carries a multi-character token.

head: float[source]
hi_end: int[source]
hi_start: int[source]
hue: float = 0.0[source]
length: int[source]
row: int[source]
speed: float[source]
tokens: List[str][source]
word_index: int[source]
y_px: int[source]
class spacr.qt.widgets.dna_rain.DnaRainEngine(width: int = 0, height: int = 0, font_size: int = DEFAULT_FONT_PX, seed: int | None = None, spacr_probability: float = SPACR_SPLICE_PROBABILITY)[source]

Qt-free simulation of the falling columns.

Deterministic: the same seed and the same sequence of calls always produce the same animation.

Per-column hues come from a second RNG rather than the main one. Drawing them from the main stream would have shifted every length, speed and start row by one draw, so a seeded rain would have fallen differently depending on a purely cosmetic setting. Two streams keep snapshot() byte-identical whether random colour is on or off.

Parameters:
  • width – canvas width in pixels.

  • height – canvas height in pixels.

  • font_size – glyph size in pixels; also the cell/column stride.

  • seed – RNG seed. None seeds from the system entropy.

  • spacr_probability – chance per respawn of a spaCR splice.

advance(dt: float) List[Tuple[int, int, int]][source]

Step the simulation by dt seconds.

Parameters:

dt – elapsed time in seconds.

Returns:

(column index, first row, last row) spans that changed, already clipped to the canvas. Columns whose integer row did not move contribute nothing — that is the whole point of quantising to cells.

column_text(index: int) str[source]

The full string of column index, tokens concatenated.

relayout() None[source]

Rebuild the column list for the current size and font.

resize(width: int, height: int) bool[source]

Resize the canvas and re-lay-out the columns.

Returns:

True when the size actually changed.

set_font_size(px: int) None[source]

Set the glyph size, which is also the column stride.

set_speed_multiplier(factor: float) None[source]

Scale every column’s speed, preserving their relative rates.

snapshot() Tuple[source]

A hashable state summary — used to compare two runs.

tokens() List[str][source]

Every token currently in the field, flattened.

property cell_size: int[source]

Row height and column stride, in pixels.

columns: List[Column] = [][source]
property font_size: int[source]
frames = 0[source]
property max_length: int[source]

Longest string this canvas allows — see MAX_STRING_SCREENS.

property n_columns: int[source]
property n_rows: int[source]

Number of whole cells that fit vertically. May be 0.

respawns = 0[source]
seed = None[source]
property size: Tuple[int, int][source]

Canvas size in pixels.

spacr_probability[source]
spacr_splices = 0[source]
speed_multiplier = 1.0[source]
class spacr.qt.widgets.dna_rain.DnaRainSettingsBar(parent: PySide6.QtWidgets.QWidget | None = None, *, color: PySide6.QtGui.QColor | str | None = None, speed: float = 1.0, font_size: int = DEFAULT_FONT_PX, opacity: float = DEFAULT_OPACITY, random_color: bool = False, vertical: bool = False, theme: str | None = None)[source]

Bases: PySide6.QtWidgets.QWidget

Colour / speed / visibility / font-size controls for a rain widget.

Everything applies live — bind() wires the signals straight at the rain widget’s setters, no restart involved.

Two layouts, one set of controls. vertical=True puts them in a label/control/readout grid, which is the shape a popover wants; the default row is the original bar. The controls, the state and the signals are identical either way — only the geometry differs.

Parameters:

vertical – lay the controls out as a grid instead of a row.

bind(rain: DnaRainWidget) None[source]

Drive rain from this bar, and seed the bar from the rain.

color() PySide6.QtGui.QColor[source]
font_size() int[source]
opacity() float[source]
pick_color() None[source]

Open the colour picker; keep the current colour on cancel.

random_color() bool[source]

True when the rain is set to colour each column separately.

restyle_for_theme(theme: str | None = None) None[source]

Re-take this panel’s own surface colour from a theme’s palette.

The bar states its own background, so unlike the rest of the screen it is NOT re-styled by re-applying the application stylesheet — it would keep the dark theme’s surface behind freshly light text. The popover calls this every time it opens, which is the only moment the panel is on screen.

Only the chrome. The user’s chosen colour is never touched.

Parameters:

theme – palette to take; defaults to the effective theme.

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

Set the colour and emit color_changed.

set_font_size(px: int) None[source]
set_opacity(value: float) None[source]
set_random_color(on: bool) None[source]

Turn per-column colour on or off; emits only on a real change.

set_speed(factor: float) None[source]
speed() float[source]
color_changed[source]
font_size_changed[source]
opacity_changed[source]
random_color_changed[source]
speed_changed[source]
class spacr.qt.widgets.dna_rain.DnaRainWidget(parent: PySide6.QtWidgets.QWidget | None = None, *, seed: int | None = None, font_size: int = DEFAULT_FONT_PX, fps: int = DEFAULT_FPS, color: PySide6.QtGui.QColor | str | None = None, background: PySide6.QtGui.QColor | str | None = None, backdrop=None, opacity: float = DEFAULT_OPACITY, random_colors: bool = False, spacr_probability: float = SPACR_SPLICE_PROBABILITY, theme: str | None = None)[source]

Bases: PySide6.QtWidgets.QWidget

The live backdrop: paints DnaRainEngine at a capped rate.

Parameters:
  • parent – parent widget; the rain sizes itself to it when follow_parent() is called.

  • seed – RNG seed for a reproducible animation.

  • font_size – glyph size in px (also the column stride).

  • fps – frame-rate cap.

  • color – trail colour; defaults to DEFAULT_COLOR.

  • background – colour painted under the glyphs; defaults to the theme background.

  • random_colors – when true every column takes its own hue instead of all of them sharing color.

  • backdrop – image painted under the glyphs instead of the flat colour — a path, a QPixmap/QImage, or None. Give it the image theme’s wallpaper and the rain stops hiding it.

  • opacity – glyph alpha in 0..1.

  • spacr_probability – per-respawn chance of a spaCR splice.

  • theme – palette to take defaults from; defaults to the user’s effective theme.

advance_frame(dt: float) List[PySide6.QtCore.QRect][source]

Step the simulation and schedule repaints of what changed.

Called by the timer, and directly by the tests so the animation never depends on a real clock.

Parameters:

dt – elapsed seconds.

Returns:

the rectangles that were invalidated. Empty when nothing moved (no repaint is scheduled at all) or when the frame fell back to a single full repaint — see last_full_repaint.

apply_theme(theme: str) None[source]

Re-take the colours from theme’s palette.

Opt-in, and it overrides the shipped teal with the theme accent — which is the Run button’s blue. Nothing in the app calls it; the screen pushes only set_background_color on a theme switch, precisely so a colour choice survives one.

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

The image painted under the glyphs, or None for a flat fill.

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

Current trail colour.

column_color(index: int) PySide6.QtGui.QColor[source]

The trail colour column index is actually painted in.

The picked colour for every column in fixed mode; that column’s own hue in random mode. This is what the settings popover’s swatch cannot show and what a test has to look at.

eventFilter(obj, event)[source]

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

focusInEvent(event) None[source]

Reject even programmatic focus; this widget is decorative only.

follow_parent() None[source]

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

font_size() int[source]
fps() int[source]
head_color() PySide6.QtGui.QColor[source]

Current leading-glyph colour, derived from the trail colour.

hideEvent(event)[source]
is_running() bool[source]

True while the animation timer is ticking.

opacity() float[source]
paintEvent(event)[source]
random_colors() bool[source]

True while every column paints in its own hue.

resizeEvent(event)[source]
set_backdrop(source) None[source]

Paint source under the glyphs instead of a flat colour.

The rain is an opaque backdrop by construction — it repaints only the cells that changed, so it has to be able to clear them, and clearing to a translucent colour smears the previous frame. That is the right trade on the dark and light themes, where the thing behind it is a flat bg the rain can reproduce exactly. On an image theme it is not: the flat colour is nothing like the wallpaper, and an opaque rain hid the photograph completely on the one screen that has a rain.

Handing the wallpaper in fixes that without giving up the dirty-rectangle repaint: the clear becomes a blit of the corresponding piece of the image, aligned to where the window’s own stylesheet paints it, and the strips switch to per-pixel alpha so the glyphs composite over the picture instead of over a colour baked into them.

The cost is the one the module docstring quantifies: a translucent strip is roughly 25x more expensive to blit than an opaque one, so this path is used only when there is a picture to show. set_backdrop(None) puts the fast path back.

Parameters:

source – a path, a QPixmap, a QImage or None.

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

Set the colour painted under the glyphs.

The rain paints its own background because it repaints only the cells that changed; a translucent backdrop would smear, so the colour is forced opaque. Under the Space theme this is the palette’s flat-sky fallback rather than the star field.

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

Set the trail colour; the head colour re-derives from it.

In random-colour mode this is still live: the picked colour lends its saturation and lightness to every column’s hue, so it chooses how vivid and how bright the random field is.

set_font_size(px: int) None[source]

Set the glyph size, which re-lays-out the columns.

set_fps(fps: int) None[source]

Cap the frame rate.

set_opacity(value: float) None[source]

Set glyph alpha in 0..1. Low keeps content in front legible.

set_random_colors(on: bool) None[source]

Give every column its own hue (or put them all back on one).

Per column, not per session, and re-rolled on every respawn — see column_color(). The colours are baked into the pre-rendered strips, so flipping this drops the strip cache; it costs one full re-render, the same as moving the colour picker.

set_speed(factor: float) None[source]

Scale every column’s speed. Relative rates are preserved, so the columns stay as asynchronous as they were.

showEvent(event)[source]
speed() float[source]
start() None[source]

Start ticking (no-op if already running).

stop() None[source]

Stop ticking. Costs exactly nothing while stopped.

property engine: DnaRainEngine[source]
last_full_repaint = False[source]
spacr.qt.widgets.dna_rain.blend(a: PySide6.QtGui.QColor, b: PySide6.QtGui.QColor, t: float) PySide6.QtGui.QColor[source]

Linear RGB blend, t=0 -> a, t=1 -> b.

spacr.qt.widgets.dna_rain.derive_head_color(base: PySide6.QtGui.QColor, background: PySide6.QtGui.QColor) PySide6.QtGui.QColor[source]

Return the leading-glyph colour for a trail colour of base.

The user picks one colour; the head is derived from it rather than being a second colour they cannot control. It is pushed away in HSL lightness, in whichever direction gains the most separation from both the trail and the background — so it survives the extremes, including a trail colour identical to the background.

Parameters:
  • base – the user’s trail colour.

  • background – what the rain is painted onto.

Returns:

a colour distinguishable from both, hue/saturation kept.

spacr.qt.widgets.dna_rain.install_dna_rain(host: PySide6.QtWidgets.QWidget, layout=None, *, anchor: PySide6.QtWidgets.QWidget | None = None, **kwargs) DnaRainWidget[source]

Put a live DNA rain behind host, and a DNA button in the chrome.

The rain 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.

The controls are not placed on the screen. They live in a popover behind a DNA toggle built from the same class as the AI toggle beside it; a decorative backdrop does not get to keep a permanent strip of a screen whose job is a settings form.

Where the button lands, in order: beside anchor if one is given; else beside the host’s own AI toggle, which is the row this control belongs in and the reason no caller has to say so; else appended to layout; else nowhere, and the caller places rain.settings_button itself.

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

  • layout – optional layout to append the DNA button to. Used only when there is no anchor to sit beside.

  • anchor – widget to sit beside — the button is inserted into anchor’s layout immediately before it. Defaults to the AI toggle found under host.

  • kwargs – forwarded to DnaRainWidget. Pass backdrop=<wallpaper path> on an image theme so the rain shows the picture through itself rather than replacing it.

Returns:

the rain widget, with .settings_bar, .settings_button and .settings_popover attached.

spacr.qt.widgets.dna_rain.random_hue_color(base: PySide6.QtGui.QColor, hue: float) PySide6.QtGui.QColor[source]

Return base moved to hue, keeping the family it belongs to.

Random colour is per column, and a column is one falling string among a hundred. Rolling all three HSL components would have given the field a scatter of near-blacks, near-whites and greys — most of which do not read as glyphs at 20 % opacity behind a settings form. Only the hue is random; saturation and lightness are taken from the colour in the picker, floored and clamped so the result is always a colour and always visible.

Parameters:
  • base – the picked colour, which lends its saturation/lightness.

  • hue – hue in 0..1.

Returns:

a fully saturated-enough, mid-lightness colour at hue.

spacr.qt.widgets.dna_rain.BASES = ('A', 'T', 'G', 'C')[source]
spacr.qt.widgets.dna_rain.DEFAULT_COLOR = '#009B9B'[source]
spacr.qt.widgets.dna_rain.DEFAULT_FONT_PX = 16[source]
spacr.qt.widgets.dna_rain.DEFAULT_FPS = 60[source]
spacr.qt.widgets.dna_rain.DEFAULT_OPACITY = 0.2[source]
spacr.qt.widgets.dna_rain.HEAD_ALPHA_BOOST = 3.0[source]
spacr.qt.widgets.dna_rain.HEAD_LIGHT_STEP = 0.45[source]
spacr.qt.widgets.dna_rain.HEAD_MIN_SEPARATION = 0.2[source]
spacr.qt.widgets.dna_rain.HIGHLIGHT_RUN_CELLS = 8[source]
spacr.qt.widgets.dna_rain.MAX_DIRTY_RECTS = 2[source]
spacr.qt.widgets.dna_rain.MAX_DT = 0.25[source]
spacr.qt.widgets.dna_rain.MAX_FONT_PX = 96[source]
spacr.qt.widgets.dna_rain.MAX_FPS = 60[source]
spacr.qt.widgets.dna_rain.MAX_OPACITY_PCT = 90[source]
spacr.qt.widgets.dna_rain.MAX_SPEED_CELLS_PER_S = 22.0[source]
spacr.qt.widgets.dna_rain.MAX_SPEED_MULTIPLIER = 4.0[source]
spacr.qt.widgets.dna_rain.MAX_STRING_CELLS = 100[source]
spacr.qt.widgets.dna_rain.MAX_STRING_SCREENS = 1.5[source]
spacr.qt.widgets.dna_rain.MIN_FONT_PX = 4[source]
spacr.qt.widgets.dna_rain.MIN_FPS = 1[source]
spacr.qt.widgets.dna_rain.MIN_OPACITY_PCT = 5[source]
spacr.qt.widgets.dna_rain.MIN_SPEED_CELLS_PER_S = 4.0[source]
spacr.qt.widgets.dna_rain.MIN_SPEED_MULTIPLIER = 0.2[source]
spacr.qt.widgets.dna_rain.MIN_STRING_CELLS = 10[source]
spacr.qt.widgets.dna_rain.MIN_TAIL_ALPHA = 0.12[source]
spacr.qt.widgets.dna_rain.RANDOM_MAX_LIGHTNESS = 0.72[source]
spacr.qt.widgets.dna_rain.RANDOM_MIN_LIGHTNESS = 0.3[source]
spacr.qt.widgets.dna_rain.RANDOM_MIN_SATURATION = 0.55[source]
spacr.qt.widgets.dna_rain.SPACR_SPLICE_PROBABILITY = 0.0016[source]
spacr.qt.widgets.dna_rain.SPACR_TOKEN = 'spaCR'[source]
spacr.qt.widgets.dna_rain.TAIL_FADE_FRACTION = 0.35[source]
spacr.qt.widgets.dna_rain.TRAIL_STEPS = 12[source]