From f75743b9b8951a092615a278d9202722f4b579f6 Mon Sep 17 00:00:00 2001 From: Aleksandr Albekov Date: Mon, 12 Oct 2026 20:20:02 +0200 Subject: [PATCH] Draw the skull background live with WebGL2 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 --- README.md | 4 +- package-lock.json | 7 + package.json | 1 + scripts/skull-render/package-lock.json | 16 +- scripts/skull-render/package.json | 4 +- scripts/skull-render/serve.mjs | 8 +- scripts/skull-render/skull.html | 276 +-------- src/components/public/PublicSite.module.css | 2 +- src/components/public/PublicSite.tsx | 5 +- .../public/SkullBackground.module.css | 31 + src/components/public/SkullBackground.tsx | 120 +++- src/components/public/background/ascii-bg.js | 208 +++++++ .../public/background/skull-ascii.js | 532 ++++++++++++++++++ src/lib/background.ts | 3 +- src/types/custom-elements.d.ts | 13 + 15 files changed, 951 insertions(+), 279 deletions(-) create mode 100644 src/components/public/background/ascii-bg.js create mode 100644 src/components/public/background/skull-ascii.js create mode 100644 src/types/custom-elements.d.ts diff --git a/README.md b/README.md index 80bca61..0e7daf8 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ Personal website and blog of Aleksandr Albekov — live at **[bulbashenko.com](https://bulbashenko.com)**. -A retro terminal-styled single page with a CRT filter and a weekly pre-rendered ASCII skull background, plus a small admin panel for editing everything on it. +A retro terminal-styled single page with a CRT filter and an animated ASCII skull background, plus a small admin panel for editing everything on it. ## Features - Profile, blog posts (Markdown), projects, gallery and CV in English, Russian and Slovak. - Admin panel at `/admin`: content editing, image uploads, active session management. Sign-in goes through the single sign-on at `auth.bulbashenko.com` ([bulbashenko/auth](https://github.com/bulbashenko/auth)); members of the `site-admins` group get in. -- Weekly ASCII skull video background, rendered in GitHub Actions with Playwright and ffmpeg (`scripts/skull-render`). +- ASCII skull background drawn live with WebGL2 in the active palette (`src/components/public/background`); the skull under the pointer glitches. Browsers without WebGL2, or too slow to keep up, get a weekly video of the same scene, rendered in GitHub Actions with Playwright and ffmpeg (`scripts/skull-render`). - Dynamic Open Graph image, sitemap and robots. ## Stack diff --git a/package-lock.json b/package-lock.json index 2a31cca..341dee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "react-markdown": "^10.1.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", + "three": "0.186.1", "zod": "^4.3.6" }, "devDependencies": { @@ -9554,6 +9555,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/three": { + "version": "0.186.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", + "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", diff --git a/package.json b/package.json index 4cc2118..c61107c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-markdown": "^10.1.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", + "three": "0.186.1", "zod": "^4.3.6" }, "devDependencies": { diff --git a/scripts/skull-render/package-lock.json b/scripts/skull-render/package-lock.json index ca21433..8f83d75 100644 --- a/scripts/skull-render/package-lock.json +++ b/scripts/skull-render/package-lock.json @@ -7,9 +7,9 @@ "name": "skull-render", "dependencies": { "@aws-sdk/client-s3": "^3.1146.0", - "@fontsource/jetbrains-mono": "^5.2.5", + "@fontsource/ibm-plex-mono": "5.3.0", "playwright": "^1.55.0", - "three": "0.184.0" + "three": "0.186.1" } }, "node_modules/@aws-sdk/checksums": { @@ -320,10 +320,10 @@ "node": ">=18.0.0" } }, - "node_modules/@fontsource/jetbrains-mono": { + "node_modules/@fontsource/ibm-plex-mono": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", - "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz", + "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -444,9 +444,9 @@ } }, "node_modules/three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "version": "0.186.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", + "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", "license": "MIT" }, "node_modules/tslib": { diff --git a/scripts/skull-render/package.json b/scripts/skull-render/package.json index 31c4d51..7b9815e 100644 --- a/scripts/skull-render/package.json +++ b/scripts/skull-render/package.json @@ -10,8 +10,8 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1146.0", - "@fontsource/jetbrains-mono": "^5.2.5", + "@fontsource/ibm-plex-mono": "5.3.0", "playwright": "^1.55.0", - "three": "0.184.0" + "three": "0.186.1" } } diff --git a/scripts/skull-render/serve.mjs b/scripts/skull-render/serve.mjs index e15c033..b4f1d50 100644 --- a/scripts/skull-render/serve.mjs +++ b/scripts/skull-render/serve.mjs @@ -1,17 +1,21 @@ -// Minimal static server for skull.html (preview and headless rendering). +// Minimal static server for skull.html (preview and headless rendering). Also serves the site's background elements. import { createServer } from "node:http"; import { readFile } from "node:fs/promises"; import { extname, join, normalize } from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = fileURLToPath(new URL(".", import.meta.url)); +const SITE = fileURLToPath(new URL("../../", import.meta.url)); +// The elements skull.html renders, from the site's source tree. +const SHARED = "/src/components/public/background/"; const TYPES = { ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".woff2": "font/woff2", ".json": "application/json" }; export function serve(port = 0) { const server = createServer(async (req, res) => { const path = normalize(decodeURIComponent(new URL(req.url, "http://x").pathname)).replace(/^(\.\.[/\\])+/, ""); + const file = path.startsWith(SHARED) ? join(SITE, path) : join(ROOT, path === "/" ? "skull.html" : path); try { - const body = await readFile(join(ROOT, path === "/" ? "skull.html" : path)); + const body = await readFile(file); res.writeHead(200, { "Content-Type": TYPES[extname(path)] ?? "application/octet-stream" }); res.end(body); } catch { diff --git a/scripts/skull-render/skull.html b/scripts/skull-render/skull.html index 522a643..1fcf334 100644 --- a/scripts/skull-render/skull.html +++ b/scripts/skull-render/skull.html @@ -3,289 +3,59 @@ -ASCII Punk Skull — Render +ASCII Skull — Render - +