pawful.dev
This site, and the API behind its live status.
The portfolio you are reading: a Next.js static export with a moon and cat modeled in Blender and rendered live with three.js, plus Satellite, the Deno service at home that feeds it my presence and coding time.
- Role
- Design and engineering
- With
- Personal
- When
- 2026
- Status
- Active
- Stack
- Next.js
- React
- TypeScript
- three.js
- Blender
- Deno
- Cloudflare Workers
The brief
A portfolio that shows interface work and systems work in the same place, feels like mine rather than a template, and stays fast. The look comes from my Linux desktop theme: deep plum backgrounds, soft pink and lavender accents, and a wallpaper of a sleepy moon and a white cat on a cloud bank.
A moon with a cat on it
The hero is that wallpaper in 3D. A cream moon with soft craters sits in a cloud bank, and a white cat sits on top of it. The cat follows your cursor with its head, blinks, swishes its tail, and reacts when you click it. You can drag the moon to spin it.
Both were modeled in Blender, built procedurally with Python rather than sculpted by hand:
- The moon is a subdivided icosphere with about thirty non-overlapping craters displaced into the surface. Crater floors are tinted through vertex colors, so the model needs no textures.
- The cat is built from ellipsoids, cones, and a curved tube for the tail, fused with a voxel remesh and smoothed into one soft clay shape. The face is separate geometry placed by ray casting onto the head, and the inner ears are painted with vertex colors.
The five project icons on the home page are rendered in Blender with a matching three-light setup, then saved as transparent AVIF files of about 7 to 10 KB each.
Rendering it live
The scene is exported as glTF and compressed with meshopt through gltf-transform, which brings it from about 1 MB to 220 KB, or about 150 KB gzipped. three.js renders it with the lighting rebuilt in code: a warm key light, a lavender sky, and a pink rim from behind.
Compression quantizes vertex positions and moves that scale onto the mesh nodes, so the eyes and tail are animated through parent pivots that compression leaves alone. The tail sways in the vertex shader, bending more toward the tip, rather than through a skeleton.
The 3D only loads where it earns its cost: a desktop with a fine pointer, motion allowed, and no data saver, and only after the page has loaded and the browser is idle. Everywhere else, and before the scene is ready, you see a 17 KB poster captured from the live scene, so the swap from image to 3D is subtle. The render loop stops when the hero scrolls out of view or the tab is hidden.
Motion
Opening a project morphs its icon from the card into the page header with React's ViewTransition, and pages slide in the direction you are traveling. Sections and cards fade up as they enter using CSS scroll timelines, with no JavaScript. The timeline bars in Experience grow as each row enters, and finish by the time the row is fully in view, so the data is never left half-drawn.
With reduced motion turned on, the 3D is replaced by the poster and every animation is removed.
Live data
The status line in the hero and the Right now panel read from Satellite, a Deno service on a server at home that follows my Discord presence. It reports what my editor has open, what is playing, and a coding-time chart built from a per-minute sample of my editor's presence. When Satellite is unreachable, the panel says so plainly and the rest of the page is unaffected.
Built with
Next.js with the App Router, exported to static HTML and served from Cloudflare Workers. Type is Fraunces for headings, with its soft axis turned all the way up, Figtree for text, and Maple Mono, my terminal font, subset to 17 KB for code.

