Skip to content

Themes and shader packs

A Theme is the whole look of your world: the field behind everything, the palette drawn from it, and the glass parameters every widget renders with.

{
id, beingId, name: string,
background: { kind: 'shader' | 'image' | 'video', src: string | null, pack: ShaderPack | null, params: Record<string, number> },
palette: { primary, accent, text, muted, glow }, // hex/rgb strings
glass: { blur: number, opacity: number, radius: number },
motion: 'calm' | 'alive' | 'still',
fontDisplay: string, // 'Fraunces'
fontBody: string, // 'Inter'
isActive: boolean,
}

You can hold several Theme rows at once; exactly one is isActive. Everything is per-user — there’s no shared “world skin”.

The seven shader packs

ShaderPack is 'aurora' | 'ocean' | 'nebula' | 'forest' | 'dawn' | 'ember' | 'void'. Each is a full-viewport WebGL canvas (ShaderCanvas in packages/ui) with its own custom GLSL, tuned default params, and a starter palette (packages/ui/src/background/packs.ts):

PackTaglineBase hue
AuroraCurtains of light over a cold, clear night.200°
OceanCaustics and light shafts, twenty metres down.200°
NebulaA slow cloud of colour, with a heart that breathes.300°
ForestDappled light through a canopy, dust in the rays.100°
DawnThe minute before the sun, every day.25°
EmberWarm dark, rising sparks, the end of a good day.20°
VoidAlmost nothing. Room to think.230°

Each pack exposes three tunable params, all live-previewed as you drag them in the theme panel:

  • Intensity (0.2–1.5) — how bright and saturated the field runs.
  • Speed (0.1–1.6) — how fast it drifts.
  • Hue (0–360°) — shifted relative to the pack’s own base hue, so “Aurora at hue 260” is still recognisably Aurora, just leaning violet.

themeFromPack(pack) (packages/ui/src/background/packs.ts) turns a bare pack name into a complete, ready-to-POST theme (palette + glass + params all filled in from the preset) — this is what both Arrival and the theme panel call so a new theme is never a half-empty object.

Bring your own image or video

Instead of a shader pack, background.kind can be 'image' or 'video' with src pointing at a file you uploaded through the Files skill (api.files.uploadapi.files.downloadUrl(entry.id)). The engine adds parallax and depth blur on top so a static photo still feels alive; motion video plays back muted and looping. Uploading through the theme panel is a one-click affordance over the same files.write-backed upload endpoint the Twin’s file index uses — see Connecting files.

A real sky, over every pack

The renderer (ShaderEngine in packages/ui/src/background/engine.ts) doesn’t just run one mood shader — over it, for every one of the seven packs, it draws the actual sky: real stars, constellation lines, planets and the moon, all positioned by real astronomy rather than an animation loop pretending to be one.

  • 1,018 real stars, to visual magnitude 4.6, with their true right ascension, declination, magnitude and colour index (packages/ui/src/background/stars.ts, derived from the d3-celestial catalogue, itself from the Hipparcos and Yale Bright Star catalogues), plus 743 real constellation line segments. Both are generated data, not hand-placed points.
  • The sun’s true altitude — not the clock hour — drives day, twilight, golden hour and night. packages/ui/src/background/astro.ts implements the same low-precision solar/lunar algorithms SunCalc uses (the Meeus family), accurate to a fraction of a degree: far finer than the eye can tell on a desktop background, and exactly right for “the sun is just below the horizon right now.”
  • The moon’s true phase and position — illuminated fraction, phase angle and the bright limb’s actual angle, not a canned set of eight sprite frames — computed from the same sun/moon geometry.
  • Five planets (Mercury, Venus, Mars, Jupiter, Saturn), positioned with JPL mean Keplerian orbital elements valid 1800–2050, each with a realistic visual magnitude and colour tint.
  • Constellation lines are culled live: only segments whose both ends are currently above the horizon and in front of the viewer are drawn, recomputed once a second as the sky turns.

It adjusts to your timezone, never your exact location

The sky needs a latitude and longitude to know what’s actually overhead, and Novaterra never asks for location permission to get one. resolveLocation() defaults to locationFromTimeZone(browserTimeZone()) — your browser’s IANA zone (Intl.DateTimeFormat), mapped to a representative city’s coordinates for zones the engine knows (Europe/London → London, America/New_York → New York, and so on) or a coarse hemisphere-and-longitude estimate from the zone’s UTC offset for anywhere else. The theme panel’s Sky location control shows exactly this — “Lisbon, from your timezone” — with a manual latitude/longitude override underneath for anyone who wants precision, and a one-click Use my timezone to clear it. No geolocation prompt ever appears; the sky is always at least approximately right for where your clock says you are, and exactly right if you tell it your coordinates.

Dynamic palette extraction

Whatever is actually on screen — shader canvas, image, or video frame — is sampled every frame at a tiny 32×18 resolution (extractPalette in packages/ui/src/background/palette.ts): it averages the whole frame for a base tone, isolates the darkest pixels for a “deep” tone, and picks the single most saturated, mid-bright pixel as an accent, which is then lifted in HSL space so it reads as a glow rather than a dot of noise. The result writes straight into CSS custom properties on <html>--nv-bg, --nv-bg-2, --nv-glow, --nv-glass-tint — so every glass widget’s tint follows the scene without any component needing to know what background is running underneath it. A small distance check skips re-applying the palette when the new sample is nearly identical to the last one, so the glass doesn’t flicker on ordinary shader drift.

Changing your world

Open the theme panel from the small orb in the corner of /world (ThemePanel in apps/web/src/desktop/ThemePanel.tsx). It lets you:

  • Pick one of the seven packs, or upload an image/video.
  • Tune intensity, speed and hue (shader packs only).
  • Set the sky’s location — your timezone by default, or exact coordinates (shader packs only; see above).
  • Tune the glass: blur, opacity, corner radius.
  • Set motion to still, calm, or alive — this also drives how strongly the field ripples when a Signal arrives.
  • Save, which creates a new Theme row and activates it in one step, or pick any previously saved theme from the Saved list and reactivate it.

Every change previews live before you save — nothing commits until you press Keep this world.

GET /api/desktop/themes → Theme[] (creates + returns Aurora if you have none)
POST /api/desktop/themes → Theme (create; { isActive: true } deactivates the rest)
POST /api/desktop/themes/:id/activate → Theme
DELETE /api/desktop/themes/:id (refused for the active theme)

Activating a theme also publishes an ambient WebSocket event ({ mood, hint: "theme:<name>" }) so any open tab reacts immediately, even one that didn’t trigger the change.

Arrival: choosing your first world

The first shader pack you ever see is chosen, not assigned. Arrival’s first of four steps (“Choose your world”) shows all seven packs live at 30fps, capped and scaled down for the preview, and whichever you pick becomes your first Theme row via exactly the same POST /api/desktop/themes → activate flow as the panel above. See First run and arrival for the full ninety seconds.