Colophon
I designed and built this site myself. The code is public on GitHub.
Numbers
- Photos published
- 178
- Image derivatives
- 1768
- Bytes saved by pipeline
- 3.68 GB
- Build time
- 5.1 s
- Pages prerendered
- 18
- Lighthouse performance
- 92
- Lighthouse accessibility
- 100
- Lighthouse best practices
- 100
- Lighthouse SEO
- 100
Last measured 2026-10-15
Stack
- Next.js 16 + TypeScript
- Every page prerendered
- Tailwind CSS 4
- Styling
- Three.js + React Three Fiber
- The globe
- Lenis
- Smooth scrolling
- sharp + exifr
- Image encoding and EXIF, plus a Fuji maker-note parser
- Vercel Blob
- Image hosting
- Vitest + GitHub Actions
- Tests and CI
Image pipeline
01
Originals
content/collections/*/photos (git-ignored)
02
Read EXIF
exifr + Fuji maker-note parser
03
Derivatives
sharp: AVIF + WebP at 5 widths, max 2560px
04
Placeholder
thumbhash + dominant colour
05
Privacy
metadata stripped, GPS rounded to city
06
Upload
Vercel Blob, immutable cache
07
Manifest
content/manifest.json (committed)
08
Static build
Next.js prerenders every page
Code
/** Returns the film simulation recorded in a Fuji maker note, or null if unknown. */
export function parseFilmSim(note: Uint8Array | undefined | null): FilmSim | null {
if (!note) return null;
const tags = readFujiTags(note);
const saturation = tags.get(TAG_SATURATION);
if (saturation !== undefined) {
const mono = monochromeFromSaturation(saturation);
if (mono) return mono;
}
const mode = tags.get(TAG_FILM_MODE);
if (mode !== undefined) return FILM_MODES[mode] ?? null;
// Colour shots with no FilmMode tag were taken on the default, Provia.
return saturation !== undefined ? "Provia" : null;
}/**
* The globe rotation (Euler XYZ) that puts a lat/lng dead centre, north up.
* First spin around Y to bring the point onto the YZ plane, then tilt
* around X by the latitude to bring it level with the camera.
*/
export function rotationToFace(lat: number, lng: number): { x: number; y: number }export const arcFragment = /* glsl */ `
uniform vec3 uColor;
uniform float uDraw;
uniform float uOpacity;
uniform float uBase;
varying float vT;
void main() {
float drawn = step(vT, uDraw);
float head = smoothstep(uDraw - 0.12, uDraw, vT) * drawn * step(uDraw, 0.999);
float alpha = mix(uBase, 0.9 + head * 0.1, drawn);
gl_FragColor = vec4(uColor + head * 0.5, alpha * uOpacity);
}
`;Credits
- Inter Tight
- Rasmus Andersson, SIL Open Font License
- Geist, Geist Mono
- Vercel, SIL Open Font License
- Land data
- Natural Earth (public domain) via world-atlas
- Libraries
- Next.js, React, three.js, React Three Fiber, GSAP, Lenis, sharp, exifr, thumbhash, Shiki, Tailwind CSS