Skip to content

Draw the skull background live with WebGL2 - #27

Merged
bulbashenko merged 1 commit into
mainfrom
live-skull-background
Oct 12, 2026
Merged

bulbashenko merged 1 commit into
mainfrom
live-skull-background

Conversation

@bulbashenko

Copy link
Copy Markdown
Collaborator

The skull background is now drawn live in the browser. Before, the site played a weekly pre-rendered video. The new design (skull A, block ramp, field, diagonal skulls) follows the active palette and reacts to the pointer. The video stays as the fallback for devices that can't run it.

What changes for visitors

Device Before Now
Desktop with WebGL2 Video (~6 MB) Live background, no video download
No WebGL2, lost context, or under ~28 fps Video Video of the same new scene
Reduced motion / Save-Data Poster Poster (unchanged)
Phones and touch devices Nothing Nothing (unchanged)
  • Palette. Colours come from the active palette (bg, g1–g3) instead of the design's acid green #b6ff3a. Switching the palette in Tweaks recolours the background immediately.
  • Pointer. The skull under the pointer glitches: rows tear and glyphs swap.

How it works

The approach is the one auth uses for its eye (bulbashenko-com/auth#7).

  • <skull-ascii>. three.js renders the scene at one pixel per cell, as the design did. Shaders then pick each cell's glyph and colour, blur a glow layer, and draw glyphs from an atlas. Nothing is read back to the CPU and fillText is never called.
  • One draw call per material. All 11 skulls are instances of one merged mesh per material: 4 draw calls per frame instead of ~700.
  • <ascii-bg>. The field and rain run in one plain WebGL2 shader, ported from auth.
  • Loading. three.js is bundled from npm and loaded only in live mode.
  • Fallback triggers. A missing, software-only or lost WebGL2 context, or a frame watchdog (mean frame interval above 35 ms over 3 s), switches the tab to the video for the rest of the session.

Fallback render

scripts/skull-render now renders the same elements in manual + loop="20" mode, in grayscale. As before, the site tints the video with the palette.

  • Seamless loop. The choreography, glitches and flicker are seeded functions of time, so frame 600 is pixel-identical to frame 0.
  • Font. The render package switches from JetBrains Mono to IBM Plex Mono, the font the site already loads.

Verification

Main-thread time, headless Chromium, SwiftShader, 1600×900:

Version Main thread
Original design page ~1000 ms/s (thread saturated)
This port ~15–30 ms/s

Real Intel HD 630 iGPU on the production build: 60 fps, and the frame watchdog never switched to the video, including at 1920×1080 with DPR 2.

Visual parity: compared against the design page by screenshots. Switching to instancing left render frames pixel-identical.

Fallback paths:

  • With WebGL disabled, the background switches to the fallback and the page shows no errors.
  • A full render.mjs run produced a 5.9 MB MP4, a 6.0 MB WebM and a 64 KB poster in about 7 minutes.

Checks: tsc and next build pass. ESLint is clean for the changed files. The one remaining ESLint error, PublicSite.tsx:39 (set-state-in-effect), predates this change.

Lockfiles were regenerated with npm 10 (node:22-slim), as CI requires.

After merging

The next weekly Skull background run (or a manual dispatch) replaces the old video with a render of the new scene. Until then, the fallback shows the old skull video.

🤖 Generated with Claude Code

The new design (skull A, block ramp, field, diagonal skulls) is drawn in
the browser instead of played from a pre-rendered video, the same way
auth draws its eye: three.js renders the scene at one pixel per cell, and
shaders turn it into glyphs from an atlas. Nothing is read back and
fillText is never called. All 11 skulls are instances of one merged mesh
per material, so a frame takes 4 draw calls instead of about 700.

Colours come from the active palette (bg, g1-g3) instead of the
design's acid green, and they follow the Tweaks palette switch. The skull
under the pointer glitches.

The weekly video stays as the fallback for browsers without WebGL2, with
a lost context, or too slow to keep 28 fps. It is now rendered from the
same elements in loop mode, so it shows the new background and loops
seamlessly. Reduced motion and Save-Data still get the poster, and
phones still get nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bulbashenko
bulbashenko merged commit 60deafa into main Oct 12, 2026
@bulbashenko
bulbashenko deleted the live-skull-background branch October 12, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant