spacr.qt.space¶
Procedural deep-space imagery for the Space theme.
Why generate instead of ship a JPEG¶
Two things rule out bundling a photograph:
a multi-megabyte image per aspect ratio bloats the wheel, and
a fixed 1920x1080 asset is soft on a 5K display — the one place a space background actually needs to look good.
So the background is synthesised with numpy at the display’s native
pixel size and cached to the user’s config directory, keyed by
(width, height, variant, seed, CACHE_VERSION). It is computed once
per screen size and reloaded from disk on every later launch.
Everything here is seeded and deterministic: the same
(width, height, variant, seed) always produces byte-identical
pixels, which is what lets the test suite assert on the output.
Three generators, composed¶
starfieldA magnitude distribution drawn from the Euclidean number-count law (
N(>F) ∝ F^-3/2) — many faint stars, very few bright ones — with stellar colour sampled from a blackbody locus spanning hot blue O/B through white A/F/G to cool red K/M, and 4-way diffraction spikes on only the handful of brightest stars.galaxyA two-arm logarithmic spiral (
r = a·e^(bθ)) rendered as an analytic field, with dust lanes trailing the arms, a warm Sérsic-ish core bulge and an exponential disc falloff, projected at an inclination.sunA star disc with classic linear limb darkening (
I(µ) = 1 − u(1 − µ)), value-noise granulation on the photosphere, and a corona that falls off smoothly with radial streamers.
They are composed rather than offered as three separate wallpapers because
one sky containing all three reads as a photograph of space; separate wallpapers read
as three clip-art assets. variant re-weights the composition
(which element is the subject) rather than switching elements on and
off, so no variant ever loses the stars.
Legibility is solved here too, not only for the photographs¶
spacr.qt.imagery has always run every photographic master
through spacr.qt.imagery.solve_dim() against
spacr.qt.imagery.exposure_target() — the brightest a bare window
background may be before white text on it drops under WCAG AA. The
generated sky never went through it, and it showed: measured at
1440x900, the brightest text-line-sized region of the galaxy sky was
0.4879 against a 0.0586 limit (8.3x over), sun 0.8201 (14.0x) and
stars 0.5041 (8.6x). Bare white text over that measured 1.20-1.96:1
where 4.5:1 is required — the “AI” toggle in the title bar sat on the
sun’s halo. render() now solves it, and legibility() reports
the same measured dict spacr.qt.imagery.legibility() returns for a
photograph.
Reconciling the limit with the 40th-percentile anchor¶
TARGET_SKY_PERCENTILE exposes the frame so that empty sky —
not the mean — lands on TARGET_SKY_LUMA, deliberately, so that a
sun stays blown out instead of dragging the whole frame to black. Taken
as a statement about one global exposure that is now flatly incompatible
with the limit, and both of the obvious ways to force it are ruined
pictures. Re-solving the exposure, or dimming the finished frame with
spacr.qt.imagery.solve_dim(), needs a factor of 0.064-0.108; both
take the sky’s own 40th percentile from 0.00091 to 0.00000, i.e. every
faint star and the whole nebula go to pure black, and the peak pixel
falls from 236-252 to 67-90. Measured, rendered and looked at: a dark
grey smudge.
The reconciliation is that the two rules are about different things.
The exposure anchor is about a pixel with nothing in it; the WCAG
limit is about the mean over a region the size of a line of text
(spacr.qt.imagery.TEXT_WINDOW), which is the point
spacr.qt.imagery already makes about photographs — every
photograph has a white pixel somewhere and it is a bright patch that
makes a caption unreadable. A 3 px star inside a 202x48 px text window
moves that window’s mean by a quarter of one per cent; the whole
starfield measured on its own comes to 0.0007-0.0067, i.e. 1-11 % of the
limit. A 207 px sun disc fills the window completely and cannot.
So the exposure anchor is kept exactly as it was — the sky background is
unchanged, to the byte — and the limit is enforced where it is actually
violated, by compressing the highlights of the smooth layers only
(_compress_highlights(), applied to nebula + galaxy + sun + bloom,
never to the starfield). The sun therefore does get noticeably darker,
which is the accepted cost; the galaxy is barely touched because it was
never the offender; the stars keep white cores and diffraction spikes.
_enforce_legibility() then measures the finished frame with the
same spacr.qt.imagery.brightest_window() used on the photographs
and applies whatever residual dim is left, so the guarantee is a
measurement and not an argument.
Real imagery¶
download_nasa_background() optionally fetches a public-domain
NASA/ESA image instead. NASA media is public domain (see
https://www.nasa.gov/nasa-brand-center/images-and-media/) and the
credit line is recorded so the UI can display it. Nothing here touches
the network at import time or from any code path other than that one
function, so an offline machine silently gets the procedural sky.
Functions¶
|
One-line credit for the imagery currently in use. |
|
Return the on-disk path of the background, generating if needed. |
|
Directory holding generated backgrounds. |
|
Return the versioned cache filename for a procedural background. |
|
Delete every cached background. Returns the number removed. |
|
Download a public-domain NASA image and record its credit. |
|
Path of the downloaded NASA image, or |
|
Luminance the brightest text-line-sized region is aimed at. |
|
Render a logarithmic-spiral galaxy, (height, width, 3) float32. |
|
HDR luminance that tone-maps to |
|
Return the cache directory containing downloaded NASA imagery. |
|
Measure how readable the generated sky actually is. |
|
Return the recorded attribution for the downloaded image, if any. |
|
Render the composed sky as an (height, width, 3) uint8 array. |
|
Draw |
|
Draw |
|
Native pixel size of the primary screen, or |
|
Map blackbody temperatures (K) to sRGB floats in [0, 1]. |
|
Render a starfield as an (height, width, 3) float32 HDR buffer. |
|
Render a star with limb darkening, granulation and a corona. |
|
Convert an (h, w, 3) uint8 array to a detached |
|
The exposure |
Module Contents¶
- spacr.qt.space.background_path(width: int, height: int, variant: str = DEFAULT_VARIANT, seed: int = DEFAULT_SEED, regenerate: bool = False) pathlib.Path | None[source]¶
Return the on-disk path of the background, generating if needed.
Returns
None(never raises) when the background cannot be produced — a read-only home directory, no PNG writer, anything. The Space theme falls back to a flat gradient in that case, so a failure here costs some prettiness and nothing else.- Parameters:
width – output width in pixels; clamped to 16-3840.
height – output height in pixels; clamped to 16-2400.
- spacr.qt.space.cache_dir() pathlib.Path[source]¶
Directory holding generated backgrounds.
Honours
$SPACR_SPACE_CACHEso tests (and read-only homes) can redirect it; otherwise~/.spacr/backgrounds, matching where the verbose logger already writes.
- spacr.qt.space.cache_name(width: int, height: int, variant: str, seed: int) str[source]¶
Return the versioned cache filename for a procedural background.
- Parameters:
width – background width in pixels.
height – background height in pixels.
variant – background variant name, such as
'galaxy','sun'or'stars'.seed – random seed the background was rendered with.
- spacr.qt.space.clear_cache() int[source]¶
Delete every cached background. Returns the number removed.
- spacr.qt.space.download_nasa_background(key: str = 'carina', timeout: float = 20.0, opener=None) dict | None[source]¶
Download a public-domain NASA image and record its credit.
Never called at import time, and never from a test. The Space theme works without it; this is strictly an upgrade the user opts into from Preferences.
- Parameters:
key – which entry of
NASA_IMAGESto fetch.timeout – socket timeout in seconds.
opener – injection point for tests — a callable taking
(url, timeout)and returning bytes. Defaults tourllib.request.urlopen.
- Returns:
the credit dict on success,
Noneon any failure (offline, 404, unwritable cache). Callers must treatNoneas “keep using the procedural sky”, not as an error.
- spacr.qt.space.downloaded_background() pathlib.Path | None[source]¶
Path of the downloaded NASA image, or
Noneif there isn’t one.
- spacr.qt.space.exposure_target() float[source]¶
Luminance the brightest text-line-sized region is aimed at.
The Space palette’s own limit from
spacr.qt.theme.max_background_luma(), backed off byspacr.qt.imagery.SAFETY_MARGIN— the identical number the photographic masters are solved to, fetched from the identical function, so the two pipelines cannot drift apart.spacr.qt.imageryimports this module at module scope, so the import has to be deferred to call time. By thenspaceis fully loaded whichever of the two the caller reached first.
- spacr.qt.space.galaxy(width: int, height: int, seed: int = DEFAULT_SEED, center: Tuple[float, float] = (0.3, 0.34), radius_frac: float = 0.38, arms: int = 2, inclination: float = 0.62, position_angle: float = -0.55, pitch: float = 0.3) numpy.ndarray[source]¶
Render a logarithmic-spiral galaxy, (height, width, 3) float32.
- Parameters:
width – width of the rendered image in pixels.
height – height of the rendered image in pixels.
center – galaxy centre as a fraction of (width, height).
radius_frac – disc scale radius as a fraction of the short edge.
arms – number of spiral arms.
inclination – 1.0 = face on, 0.0 = edge on.
position_angle – rotation of the disc, radians.
pitch –
binr = a·e^(bθ); smaller = more tightly wound.
- spacr.qt.space.highlight_ceiling(exposure: float, target: float | None = None) float[source]¶
HDR luminance that tone-maps to
target’s encoded value.exposure_target()is a linear-light relative luminance of the finished image._tone_map’s output is written straight to 8-bit without a gamma encode, so it is an sRGB signal value, and the two are a transfer function apart — the 0.0586 limit is a mid-dark grey around#444444, not a 6 % signal. Encode first, then invert1 - exp(-x·E).- Parameters:
exposure – tone-mapping exposure, as returned by
tone_exposure(); zero or a negative value returnsinf.- Returns:
infwhen there is nothing to solve for — a palette that admits no wallpaper at all (spacr.qt.theme.max_background_luma()is negative for the light theme) or a zero exposure. Callers read that as “no ceiling”, never as “clamp everything to zero”. A palette that admits a white wallpaper lands on a ceiling far above anything the generators emit, which comes to the same thing without a second branch to leave untested.
- spacr.qt.space.imagery_dir() pathlib.Path[source]¶
Return the cache directory containing downloaded NASA imagery.
- spacr.qt.space.legibility(variant: str = DEFAULT_VARIANT, width: int = 0, height: int = 0, seed: int = DEFAULT_SEED) dict[source]¶
Measure how readable the generated sky actually is.
The same dict, measured the same way over the same region, that
spacr.qt.imagery.legibility()returns for a photographic master — so “is the wallpaper legible” is one question with one answer shape whether the pixels were generated or photographed.width/heightdefault toscreen_size().
- spacr.qt.space.read_credits() dict | None[source]¶
Return the recorded attribution for the downloaded image, if any.
- spacr.qt.space.render(width: int, height: int, variant: str = DEFAULT_VARIANT, seed: int = DEFAULT_SEED, legible: bool = True) numpy.ndarray[source]¶
Render the composed sky as an (height, width, 3) uint8 array.
Deterministic: identical arguments always give identical bytes.
- Parameters:
width – width of the rendered image in pixels.
height – height of the rendered image in pixels.
legible – when true (always, outside the tests) the finished frame is bounded by the Space palette’s bare-text limit — see
_compress_highlights()and_enforce_legibility().Falserenders the unbounded sky, which exists so the test suite can measure what the bound is worth rather than assert that it was called.
The starfield is kept in its own buffer to the very end. That is not tidiness: it is the one layer the highlight ceiling must not touch, and keeping it separate is what lets a star core stay at 255 while the sun beside it comes down by two and a half stops.
- spacr.qt.space.sample_star_fluxes(rng, n: int) numpy.ndarray[source]¶
Draw
nrelative fluxes (>= 1.0) from the Euclidean count law.The tail is unbounded in principle; it is clipped at
FLUX_SATURATIONso one absurd draw cannot whiteout the sky.The resulting distribution is emphatically not uniform: by construction the fraction brighter than
k·F_minisk^-1.5, i.e. ~65 % of stars sit in the faintest factor-of-two bin while only ~4 % are 8x brighter than the limit.- Parameters:
rng – NumPy random generator; its
randommethod supplies the draws.n – number of fluxes to draw; zero or fewer returns an empty array.
- spacr.qt.space.sample_star_temperatures(rng, n: int) numpy.ndarray[source]¶
Draw
nstellar temperatures from the naked-eye class mix.- Parameters:
rng – NumPy random generator; its
choiceandrandommethods supply the draws.n – number of temperatures to draw; zero or fewer returns an empty array.
- spacr.qt.space.screen_size(default: Tuple[int, int] = (2560, 1440)) Tuple[int, int][source]¶
Native pixel size of the primary screen, or
defaultheadless.
- spacr.qt.space.star_colors(temps: numpy.ndarray) numpy.ndarray[source]¶
Map blackbody temperatures (K) to sRGB floats in [0, 1].
- Parameters:
temps – 1-D array of temperatures in kelvin; values outside 2000-40000 K are clipped to that range.
- spacr.qt.space.starfield(width: int, height: int, seed: int = DEFAULT_SEED, density: float = STAR_DENSITY, spike_count: int = SPIKE_COUNT) numpy.ndarray[source]¶
Render a starfield as an (height, width, 3) float32 HDR buffer.
- Parameters:
width – output width in pixels.
height – output height in pixels.
- spacr.qt.space.sun(width: int, height: int, seed: int = DEFAULT_SEED, center: Tuple[float, float] = (0.8, 0.74), radius_frac: float = 0.085, temperature: float = 5800.0, corona_scale: float = 1.9) numpy.ndarray[source]¶
Render a star with limb darkening, granulation and a corona.
- Parameters:
width – output width in pixels; the star is rendered at a third of this and upsampled.
height – output height in pixels; the star is rendered at a third of this and upsampled.
- spacr.qt.space.to_qimage(arr: numpy.ndarray)[source]¶
Convert an (h, w, 3) uint8 array to a detached
QImage.- Parameters:
arr – RGB image, an array of shape (h, w, 3); it is converted to contiguous
uint8.
- spacr.qt.space.tone_exposure(luma: numpy.ndarray) float[source]¶
The exposure
_tone_map()would use for this frame.Split out of
_tone_map()becauserender()needs the number itself: the highlight ceiling is a luminance in HDR units, and converting the palette’s limit — which is a luminance in the finished, tone-mapped image — back into HDR units is exactly inverting this curve at this exposure.- Parameters:
luma – 2-D array of HDR luminance for the frame.