One screen, no scroll, no dependencies, no build step. Name, what I do, five projects, four ways to reach me — over a live view of Solana mainnet.
The background is a real transfer graph. A worker chases the confirmed tip of the chain, pulls each block, and extracts every SOL and SPL-token transfer; addresses become nodes, transfers become edges, and blocks play back at the rate they actually arrive. Node size follows the USD volume an address has moved while it has been on screen — SOL priced live, USDC at par, other tokens unvalued. Nothing is pre-recorded and nothing is faked.
python3 -m http.server 4173Then open http://localhost:4173. Any static file server works; there is
nothing to install and nothing to compile.
Renderer — WebGPU, 262 144 particles in one storage buffer, mutated in
place by a compute shader. Nodes, edges and transfer pulses draw additively
into an rgba16float accumulation texture that a fade pass dims each frame
(float trails, so no 8-bit ghosting), then a present pass tonemaps to screen.
Descendant of fbo-next and, before it,
an old Three.js FBO pen — same idea, state lives on the GPU and the renderer
reads it directly.
Every particle exists from the first frame as unlit dust. When an address appears in a block the CPU lights one: assigns it a cluster anchor and a mass, and the simulation flies it in. Evicted addresses cool back to dust where they stand.
Forces on a lit node, all with a knob in the settings panel:
| force | what it does |
|---|---|
| spring | pulls the node to its cluster anchor |
| repulsion | multigrid density repulsion between lit nodes — 32³ deposit, reduced to a 16³/8³/4³ pyramid, near cells sampled fine and far cells coarse. Off by default: its equilibrium is even spacing, which flattens cluster structure |
| swirl | tangential orbit around the anchor |
| noise | curl-noise drift, the organic breathing |
| travel | how far from its anchor a new node appears before flying in |
Data path — js/worker.js polls getSlot, fetches blocks with
getBlock(jsonParsed, transactionDetails: "accounts") a few in parallel, and
emits them in slot order; js/parse.js derives transfers from pre/post balance
deltas (SOL) and pre/post token balances grouped by mint (SPL). SOL/USD comes
from Coinbase with Binance as fallback. All of it runs off the main thread.
Fallbacks — the page never waits on the network and never looks broken.
It starts from assets/snapshot.json, a real block baked into the repo, and
loops it until live data arrives. Without WebGPU it falls back to a 2D canvas
renderer (js/viz2d.js). With prefers-reduced-motion it paints one settled
frame and stops. Hidden tabs pause both the render loop and the fetcher.
A settings toggle sits in the bottom-right status line. It opens sliders for
the forces above plus node size, edges, pulses, brightness, trails, dust,
idle-fade floor and duration, cloud center, orbit speed and zoom. Values
persist in localStorage.
-
index.html,styles.css— the page; everything visible lives here -
js/viz.js— entry point: picks a renderer, wires the worker, builds the panel -
js/gpu.js,js/gpu_shaders.js— WebGPU renderer and its WGSL -
js/viz2d.js— 2D canvas fallback -
js/worker.js,js/parse.js— block fetching and transfer extraction -
js/gen_snapshot.mjs— dev tool: rebuildassets/snapshot.jsonfrom a savedgetBlockresponse (bun js/gen_snapshot.mjs block.json [sol-price]) -
tools/og-card.html— dev tool: the social card, drawn by the real 2D renderer on the bundled snapshot. Serve the site and shoot it headless:"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ --headless=new --disable-gpu --virtual-time-budget=12000 \ --screenshot=card.png --window-size=1200,630 \ http://127.0.0.1:4173/tools/og-card.html sips -s format jpeg -s formatOptions 92 card.png --out assets/og-card.jpg -
assets/resumes/— role-specific PDF resumes, kept reachable by direct link -
404.html,robots.txt,sitemap.xml,site.webmanifest— the usual
Deployed from main by GitHub Pages.