Ximing Luo's portfolio, presented as a film reel: a list of projects down the left, a player that shows the current one, docked project pages, and a draggable wall of artworks. Live at ximingluo.com.
- Vite, React 19 and TypeScript in strict mode
- Plain CSS, kept in
src/styles(the design lives there; nothing is generated) - WebGL2 for the artwork wall, the ink field over the Home card and the stage photo on the About card; 2D canvas for the grain and the code-glyph hero
- Self-hosted type: Gambetta, Monsieur La Doulaise, B612 Mono, League Gothic and Literata (licences sit beside the files in
public/media/fonts) - No other runtime dependencies
| Command | What it does |
|---|---|
npm run dev |
Development server with hot reload on :5173 |
npm run build |
Type-checks, builds into dist/, then prerenders every route and the sitemap |
npm run preview |
Serves dist/ on :4173 |
npm run typecheck |
tsc --noEmit for the site, then for the build script (tsconfig.node.json) |
npm run lint |
ESLint (TypeScript, React hooks, Prettier compatibility) |
npm run format |
Prettier over the source |
src/
main.tsx, App.tsx entry and the page, in the order the stylesheet layers it
paths.ts the site's paths, shared by the router and the prerender step
data/ the content (see below) and the catalogue built from it
components/ one component per piece of the page; markup only, content from the store
reel/ the typed sequences that drive the site: player, project pane, wall, phone sheet,
keyboard, startup. They write to one store React renders from and animate the
elements the components register.
film/ grain, cursor and timecode loop; the leader; the split cut; tweens; the page tint
wall/ the WebGL wall and the canvas effects
gl/ shared WebGL2 plumbing (context, loop, pointer), the ink field (Home, and the About card's photo)
styles/ fonts, project pages, core, layout, wall
public/ static files served as they are: icons, `404.html`, `echoes.pdf`, `media/`
scripts/prerender.ts runs after the build: one HTML file per route, each with its own head, plus the sitemap
Timing and gestures are deliberate (stepped tweens at film-like frame rates, swipe and wheel thresholds), so
changes to src/reel and src/film should be checked against the live site at both desktop and phone widths.
Everything shown comes from src/data:
work.ts: the projects. Each entry has anid, asection(selected,graphics,software,experiments,research), the listlabelandmeta,title,sub,tagline,year,tags, optionallinks, asummary, aheroandthumb, and the storyblocks:{ p }paragraphs,{ h }headings,{ img, w, h, cap }figures,{ row: [...] }images side by side,{ embed }video,{ live, poster }click-to-load demos and{ link, cap }documents. Paragraphs accept[text](url)links and**bold**.art.ts: the pieces on the wall, in wall order (photo-Nids).experience.ts,about.ts: the About page.recognition.ts: awards, exhibitions, fellowships;workslinks an item to project or piece ids,topputs it in Highlights,earlyfolds it under Earlier.tints.json: the average colour of each thumbnail, keyed by its path, for the page light.types.tsdescribes all of it, so a typo in an entry failsnpm run build.
Media paths are relative to public/media: project images under media/img/<project>/, list thumbnails as
media/cat/<id>.jpg (around 520px wide), documents under media/doc/. To add a project: write its entry in
work.ts, add its images and thumbnail, and add a tint for the thumbnail. To add an artwork: an entry in
art.ts with a hero and a thumb.
echoes.pdf stays at the site root on purpose; the address is printed in places.
The site is one page and routes by path. Every route is also a real file: the build ends with
scripts/prerender.ts, which copies dist/index.html once per route with its own title, description and
preview image, so GitHub Pages answers each address directly and a shared link previews the page it points to.
| URL | Opens |
|---|---|
/<id> |
A project page (/about too; /recognition opens About) |
/art |
The artwork wall |
/art/<photo-id> |
A piece, enlarged over the wall |
/index |
The player with every section unfolded |
/?tint=grey |
A grey leader |
The leader no longer plays; the name at the top left returns to the Home card. Links of the old shape
(/?open=<id>, /?art=<id>, /?about) still work: the address is rewritten to the path once the site is up.
Links to the site before 2026 (/portfolio/<id>, /creative) and paths with a trailing slash are redirected
by public/404.html, which GitHub Pages serves for any unknown path.
Pushing to main runs .github/workflows/deploy.yml: it installs, lints, builds and publishes dist/ to
GitHub Pages, which serves it at ximingluo.com (the custom domain is set in the repository's Pages settings;
public/CNAME is kept as a belt and braces). gh run watch follows a deploy; the workflow can also be started
by hand from the Actions tab.
