Skip to content

feat: ambient animated background behind every page - #42

Merged
kris1027 merged 16 commits into
mainfrom
feat/ambient-background
Oct 11, 2026
Merged

kris1027 merged 16 commits into
mainfrom
feat/ambient-background

Conversation

@kris1027

Copy link
Copy Markdown
Owner

What changed

This PR adds the animated background from the Claude Design file BeStats Background.dc.html behind every page. It has a point wave, drifting outlined "screens", dust, a slight tilt that follows the pointer, and a vignette. It is lit in the personal score cyan (--color-score-personal), the colour of the selected tab and your own rating. The plan is in prompts/ambient-background.md.

How it stays cheap

  • No Three.js and no new dependency. The design loaded Three.js (about 160 KB) from unpkg. This PR draws the same scene with plain WebGL2 in four draw calls.
  • The GPU does the animation. The design moved its 26 panels in JavaScript every frame. Here the panels are instanced, and each one's position is worked out from the time in the vertex shader. Each frame the page only sends a few values.
  • Drawn off the main thread. Where the browser supports it, the canvas is handed to a Web Worker (OffscreenCanvas). Otherwise the same renderer runs on the main thread. Without WebGL2, or after a lost GPU context, only the static vignette shows.
  • Loads after the page. Only the vignette is in the prerendered HTML. The scene code is imported when the browser is idle and then fades in, so it is in no route's initial JavaScript. Gzipped sizes: renderer 3.4 KB, worker 5.2 KB, mount module 2.4 KB.
  • Budgets:
    • The pixel ratio is capped at 1.5×.
    • Below 768px, or with a coarse pointer, the scene has half the density.
    • In a hidden tab no frames run, and the animation resumes without a jump.
    • Under reduced motion it draws one still frame.
    • The canvas is sized to the large viewport height, so a phone toolbar collapsing doesn't resize it.

Other details

  • All colours stay in globals.css. This PR adds --color-backdrop-wave, --color-backdrop-dust and the backdrop-vignette utility, and the renderer reads the tokens when it starts.
  • Worker messages are validated with zod/mini.
  • The background is aria-hidden, ignores the pointer and sits at -z-10.
  • The page keeps pure black and Inter, not the design's #07080b and Space Grotesk.
  • components/AGENTS.md records the new client boundary and the second deliberate use of the personal cyan. /showcase has an Ambient background entry.

Verification

  • pnpm typecheck passes, and pnpm lint has no errors (the 3 informational notes are older, in watchlist-show-controls.test.tsx).
  • pnpm test: 2210 tests in 136 files pass. The new tests cover the budgets, the camera maths, colour parsing, the worker protocol, the clock (hidden tab, reduced motion, easing, dispose), the component's idle mount and cleanup, and the layout position.
  • pnpm build succeeds. /shows, /movies and /movies/[id] are still partial prerender (◐).
  • In the production build, none of the 18 initial chunks of /shows contains the scene code.
  • In the running app (Chromium through Playwright):
    • The worker path is used.
    • The scene renders on /shows, the 404, /privacy and /sign-in.
    • Mobile (390px) animates with no horizontal scroll.
    • Under reduced motion, two screenshots 1.5s apart are byte-identical.
  • I rendered the original Three.js design next to this version: it is just as faint.
  • Contrast: the brightest background pixel behind text measures 4.55:1 against text-muted-foreground. The original design measured 3.59:1.
  • Not checked in a browser: the hidden tab pause and the no-WebGL2 fallback (unit tests only), and Safari.

Needs attention

  • The personal cyan now has a second use outside ratings (after the selected tab), at the owner's request. This is recorded in components/AGENTS.md.
  • The contrast measurement was taken at 1× pixel ratio only.

🤖 Generated with Claude Code

kris1027 and others added 4 commits October 11, 2026 11:17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027 kris1027 added docs Documentation additions and updates feature New user-facing functionality test Automated tests and test infrastructure performance Load time, responsiveness, and resource usage improvements ui Layouts, styling, and shared interface components labels Oct 11, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in BeStats Oct 11, 2026
@kris1027 kris1027 self-assigned this Oct 11, 2026
@vercel

vercel Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bestats Ready Ready Preview Oct 11, 2026 9:37am UTC

kris1027 and others added 12 commits October 11, 2026 11:25
A worker that lost its context reported the same failure as one that
could never draw, so the page rebuilt the scene on the main thread.
The worker now reports lost separately and the page gives up, as
decision 7 of the plan asks. Adds the ambient canvas tests the plan
named, covering the worker, the main thread fallback and both losses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget was chosen once at mount, so a window resized across 768px
kept the density it started with. A resize now rechooses the budget and
rebuilds the scene when its geometry changes, and only resizes the
canvas otherwise, picking up a new pixel ratio.

Rebuilding disposes a live scene, whose deliberate context release was
reported as a loss; dispose now drops that listener first, and a
callback from a replaced scene is ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The still frame ignores the pointer, so each move only woke the worker
to drop it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page terminates the worker, which releases its context with it, so
nothing ever sent dispose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan asked for the still frame beside the tokens. AmbientCanvas
takes a still flag that only /showcase passes, which draws the reduced
motion frame whatever the visitor's setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The colour module imported its own value types from the renderer; the
renderer now takes them from colors.ts instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget and colour schemas restated their types by hand, so a new
field could be missed. Each is now typed as the shape it validates and
fails the type check when it falls behind; the pixel ratio check is
written once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lens and clip planes were written twice, the parallax and panel
opacities were bare numbers, and the panel instance layout lived in
three places. The layout is now one list the renderer derives its
offsets from, checked against the shader's declarations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every exported type and constant now says why it is shaped the way it
is, and the pointer comment gives its reason rather than its mechanics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AC-10 asks muted text to keep 4.5:1 over the brightest background
pixel. Measured, additive wave points stacking at the horizon reached
1.8:1 and the panel outlines about 3.8:1. Each shader now caps its
fragment under the luminance --muted-foreground allows, and layers
blend with MAX so overlaps cannot add past it. The worst pixel over 20
frames now measures 5.0:1; everywhere layers do not overlap the scene
looks as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The plan now records the CSS vignette, the client created canvas, the
-z-10 layering, sRGB colours, the worker replies without dispose, the
breakpoint rebuild, the lost context rule, the AC-10 measurement and
cap, and the tests that exist. components/AGENTS.md notes the lost
context and the contrast cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027
kris1027 merged commit 450c084 into main Oct 11, 2026
4 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in BeStats Oct 11, 2026
@kris1027
kris1027 deleted the feat/ambient-background branch October 11, 2026 09:39

This branch was successfully deployed

1 active deployment
Preview — 6999df79 Deployed Oct 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation additions and updates feature New user-facing functionality performance Load time, responsiveness, and resource usage improvements test Automated tests and test infrastructure ui Layouts, styling, and shared interface components

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant