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 onlyThe offline renderer had a
lowercase_probthat mixeda/t/g/cin. Here the alphabet is exactlyA,T,G,C— plus thespaCRsplice below.Live, not renderedNothing is written to disk.
DnaRainWidgetpaints 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 headThe 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):
The timer stops whenever the widget is not on screen — hidden, or in a minimised window. Zero frames, zero CPU.
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
drawTextcosts 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.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_RECTSfor 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¶
One falling string. |
|
Qt-free simulation of the falling columns. |
|
Colour / speed / visibility / font-size controls for a rain widget. |
|
The live backdrop: paints |
Functions¶
|
Linear RGB blend, |
|
Return the leading-glyph colour for a trail colour of |
|
Put a live DNA rain behind |
|
Return |
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
spaCRword.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).
row –
floor(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 * cellquantised 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 (seeDnaRainEngine).
- 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
seedand 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.
Noneseeds from the system entropy.spacr_probability – chance per respawn of a
spaCRsplice.
- advance(dt: float) List[Tuple[int, int, int]][source]¶
Step the simulation by
dtseconds.- 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.
- resize(width: int, height: int) bool[source]¶
Resize the canvas and re-lay-out the columns.
- Returns:
True when the size actually changed.
- 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.QWidgetColour / 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=Trueputs 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
rainfrom this bar, and seed the bar from the rain.
- 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.
- 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.QWidgetThe live backdrop: paints
DnaRainEngineat 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, orNone. 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
spaCRsplice.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_coloron a theme switch, precisely so a colour choice survives one.
- backdrop() PySide6.QtGui.QPixmap | None[source]¶
The image painted under the glyphs, or
Nonefor a flat fill.
- column_color(index: int) PySide6.QtGui.QColor[source]¶
The trail colour column
indexis 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.
- head_color() PySide6.QtGui.QColor[source]¶
Current leading-glyph colour, derived from the trail colour.
- set_backdrop(source) None[source]¶
Paint
sourceunder 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
bgthe 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, aQImageorNone.
- 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_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.
- property engine: DnaRainEngine[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
DNAtoggle built from the same class as theAItoggle 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
anchorif one is given; else beside the host’s ownAItoggle, which is the row this control belongs in and the reason no caller has to say so; else appended tolayout; else nowhere, and the caller placesrain.settings_buttonitself.- 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 underhost.kwargs – forwarded to
DnaRainWidget. Passbackdrop=<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_buttonand.settings_popoverattached.
- spacr.qt.widgets.dna_rain.random_hue_color(base: PySide6.QtGui.QColor, hue: float) PySide6.QtGui.QColor[source]¶
Return
basemoved tohue, 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.