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.
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 has fewer allowed values.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.
Roll the falling columns from the seed.
- Parameters:
width – the field’s width in pixels.
height – its height.
font_size – the cell size the columns are pitched on.
seed – what makes the animation reproducible.
spacr_probability – how often a column splices in the word.
- 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.
- column_text(index: int) str[source]¶
The full string of column
index, tokens concatenated.- Parameters:
index – position of the column in
columns.
- resize(width: int, height: int) bool[source]¶
Resize the canvas and re-lay-out the columns.
- Parameters:
width – new canvas width in pixels; negative values become 0.
height – new canvas height in pixels; negative values become 0.
- Returns:
True when the size actually changed.
- set_font_size(px: int) None[source]¶
Set the glyph size, which is also the column stride.
- Parameters:
px – glyph size in pixels, clamped to
MIN_FONT_PX..MAX_FONT_PX; a change re-lays-out the columns.
- set_speed_multiplier(factor: float) None[source]¶
Scale every column’s speed, preserving their relative rates.
- Parameters:
factor – multiplier applied to every column’s speed; negative values become 0.
- property font_size: int[source]¶
The cell size the columns are laid out on, in pixels.
- Returns:
the font size.
- 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.
Build the controls for the DNA-rain backdrop.
Every parameter is the STARTING value of a control, not a fixed setting: the bar exists to change them, and each has a widget below.
- Parameters:
parent – parent widget.
color – the glyph colour to start on; anything
_as_color()accepts, defaulting toDEFAULT_COLOR.speed – fall speed multiplier.
font_size – glyph size in pixels.
opacity – glyph opacity, 0 to 1.
random_color – start with a colour per column rather than one colour, which makes the swatch inactive until it is turned off.
vertical – lay the bar out as a column instead of a row.
theme – which theme to style against, or
Nonefor the active one. The bar needs an opaque surface of its own because the rain is painted behind it and the global background rule does not reach a widget carrying its own stylesheet.
- bind(rain: DnaRainWidget) None[source]¶
Drive
rainfrom this bar, and seed the bar from the rain.- Parameters:
rain – the rain widget to drive; the bar’s controls are seeded from its current colour, speed, font size, opacity and colour mode, and the bar’s signals are connected to its setters.
- color() PySide6.QtGui.QColor[source]¶
The colour currently chosen in the bar.
A COPY, so the caller cannot edit the bar’s own colour in place.
- Returns:
the chosen colour.
- 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.- Parameters:
color – a
QColoror colour name; an invalid colour keeps the current one.
- set_font_size(px: int) None[source]¶
Move the font slider, clamped to the range the bar offers.
- Parameters:
px – the wanted size in pixels.
- set_opacity(value: float) None[source]¶
Move the opacity slider, clamped to the range the bar offers.
CLAMPED RATHER THAN REFUSED: this is restored from a saved preference, and a value from an older build with a wider range should land at the nearest legal one rather than stop the restore.
- Parameters:
value – the wanted opacity, 0 to 1.
- set_random_color(on: bool) None[source]¶
Turn per-column colour on or off; emits only on a real change.
- Parameters:
on – whether the per-column colour check box is ticked.
- 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.
Build the rain widget over its engine.
- Parameters:
parent – parent widget.
seed – what makes the animation reproducible.
font_size – the cell size.
fps – the repaint cap.
color – the column colour.
background – the colour behind it.
backdrop – whether it paints as a backdrop.
opacity – how strongly it is drawn.
random_colors – whether each column takes its own hue.
spacr_probability – how often the word is spliced in.
theme – the palette’s name.
- 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.- Parameters:
theme – theme name passed to
spacr.qt.theme.palette_for(); itsbgandaccentcolours become the background and trail colours.
- backdrop() PySide6.QtGui.QPixmap | None[source]¶
The image painted under the glyphs, or
Nonefor a flat fill.
- background_color() PySide6.QtGui.QColor[source]¶
The colour painted behind the rain.
A COPY, so a caller cannot recolour this widget by mutating what it was handed.
- Returns:
the background colour.
- 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.
- Parameters:
index – position of the column in the engine’s column list.
- eventFilter(obj, event)[source]¶
Follow the parent’s size; pause when the window is minimised.
- Parameters:
obj – the watched object: the parent widget or the top-level window.
event – the event delivered to it. A parent resize resizes the rain to cover it; a window state change, hide or show of the window pauses or resumes the animation. Every event is then passed to the base class.
- 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 again.
- fps() int[source]¶
The cap on repaints per second.
A CAP, NOT A RATE: this is a backdrop, and it must not take frames from whatever the user is actually doing in front of it.
- Returns:
the frame cap.
- head_color() PySide6.QtGui.QColor[source]¶
Current leading-glyph colour, derived from the trail colour.
- hideEvent(event)[source]¶
Stop animating.
A HIDDEN BACKDROP MUST COST NOTHING. Without this the timer keeps firing and the columns keep advancing for a widget nobody can see.
- Parameters:
event – the Qt hide event.
- opacity() float[source]¶
How strongly the rain is drawn over its background, 0 to 1.
- Returns:
the opacity.
- resizeEvent(event)[source]¶
Re-lay the columns for the new size and repaint.
- Parameters:
event – the Qt resize event.
- 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.
- Parameters:
color – a
QColoror colour name; an invalid colour keeps the current one, and alpha is forced to 255.
- 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.
- Parameters:
color – a
QColoror colour name; an invalid colour keeps the current one.
- set_font_size(px: int) None[source]¶
Set the glyph size, which re-lays-out the columns.
- Parameters:
px – glyph size in pixels, clamped to
MIN_FONT_PX..MAX_FONT_PX.
- set_fps(fps: int) None[source]¶
Cap the frame rate.
- Parameters:
fps – frames per second, clamped to
MIN_FPS..MAX_FPS; the animation timer interval is set from it.
- set_opacity(value: float) None[source]¶
Set glyph alpha in
0..1. Low keeps content in front legible.- Parameters:
value – glyph opacity, clamped to 0..1.
- 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.- Parameters:
on –
Truefor a hue per column,Falsefor the one picked colour; setting the current mode again does nothing.
- set_speed(factor: float) None[source]¶
Scale every column’s speed. Relative rates are preserved, so the columns stay as asynchronous as they were.
- Parameters:
factor – multiplier applied to every column’s speed; negative values become 0.
- showEvent(event)[source]¶
Start animating, and follow the window this widget belongs to.
- Parameters:
event – the Qt show event.
- speed() float[source]¶
How fast the columns fall, as a multiplier of the base rate.
- Returns:
the speed multiplier.
- property engine: DnaRainEngine[source]¶
The simulation behind this widget.
Exposed so a test can step the animation deterministically without a Qt event loop – the engine is Qt-free and seeded.
- Returns:
the engine.
- 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.- Parameters:
a – colour returned at
t=0.b – colour returned at
t=1.t – blend fraction, clamped to 0..1; only the red, green and blue channels are blended.
- 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.
Nested helpers¶
- DnaRainWidget._coalesce.close()¶
Close the run being accumulated and emit it.
spacr/qt/widgets/dna_rain.py:1233