Building my own pod in the Imagisphere
ポッドの作り方
LittleBigPlanet's motto is play, create, share, which is also a decent description of what I think a personal site should be. So this one is built with love and care, based on the creativity that LittleBigPlanet taught me. It uses actual sounds and materials from the game as well!
How it all comes together#
Everything runs on one Bun process:
- Posts, projects and pages are Markdown with YAML frontmatter in
content/, and the site text is in one JSON file. Bun parses both itself (Bun.YAML,Bun.markdown), so there's no Markdown or YAML dependency. - Pages are rendered on the server with a tiny tagged-template helper that escapes everything by default. Code blocks are highlighted there too, so readers download zero highlighting JavaScript.
- Rendered pages are cached in memory with their brotli and gzip versions, and the cache is dropped whenever content changes. A typical page is served from memory in well under a millisecond.
- Static files get content-hashed URLs (
/_/<hash>/lbp/tex/cardboard.webp), so they can be cached forever and still change when the file does.
I can edit everything directly in the browser. When I sign into "create mode" I get a Markdown editor with a live preview, image upload by drag and drop or paste, and a history of every saved version. Uploaded images are re-encoded to WebP with Bun.Image, which also strips EXIF data like GPS positions.
Other people's cursors#
If someone else is reading the same page, you'll see their pointer with a name tag.
The interesting part is where to draw the other cursors. Screen coordinates are useless, because every visitor has a different window size and the layout reflows.
Instead, every major block on a page carries an id, and a cursor is sent relative to the block it's over:
function measure(clientX: number, clientY: number) {
const anchor = document.elementFromPoint(clientX, clientY)?.closest<HTMLElement>("[data-cur]");
if (anchor) {
const r = anchor.getBoundingClientRect();
return { a: anchor.dataset.cur!, x: (clientX - r.left) / r.width, y: (clientY - r.top) / r.height };
}
return { a: "", x: clientX / innerWidth, y: (clientY + scrollY) / document.documentElement.scrollHeight };
}If you point at the second line of a project card, everyone sees you pointing at the second line of that card, whether they're on a phone or an ultrawide monitor.
On the server, cursor updates aren't forwarded one by one. Each page collects the latest position of every visitor and sends one batch per tick (12.5 times a second), so ten people wiggling their mice cost ten small messages per tick instead of a hundred. The browser smooths between updates, fades idle pointers and stops animating entirely when nothing moves.
The chat#
The chat rides on the same websocket. Messages are kept in an append-only log on disk (compacted on boot), rate limited per connection, and checked against a word filter that reloads without a restart. From create mode I can delete messages and mute people by a random browser id or a salted hash of their IP; raw IPs are never stored.
You can also send stickers, straight from the LittleBigPlanet sticker sheets.
Real materials#
The textures come from the games themselves, via fan archives of extracted assets. A small script downloads the texture sheets, finds the individual maps on each sheet, cuts out the diffuse ones and converts them to WebP. Those give the site its cardboard, felt, burlap, brass and the corrugated cardboard edge you can see under every thicker panel.
My favourite detail is the loading spinner. LittleBigPlanet 2 stores it as a signed distance field (I love this paper from valve BTW! Go read it!) animation , which looks like blurry grey smudges until you threshold it back into crisp shapes. After that it's a CSS mask animated with steps().
The Japanese dotted around the site uses a font subset generated from the characters the site actually contains, about 37 KB per weight. Hover any of it for a pop-up in the style of Lexiglance: 段ボール.
Numbers#
- About 25 KB of JavaScript and CSS over the wire (brotli), with the bubble game and create-mode code split out and loaded only when needed. The home page's HTML is another 13 KB.
- Cached pages leave the server in about 0.15 ms on my machine.
- One process, one websocket per visitor, and everything still works (minus the live bits) without JavaScript.
The stickers, materials and sounds are © Sony Interactive Entertainment and Media Molecule. This is a fan tribute, made with a lot of affection for the games that made my creativity manifest my project in real life.
Like this level? 評価
Give this post a Yay! or a Boo!
