Skip to content

Repository files navigation

portfolio

CI MIT License TypeScript

English | Espanol

keivanmalhani.github.io

The portfolio hero, a WebGL photo transition under the headline

A bilingual portfolio site with a WebGL hero. Photographs cross-dissolve through a procedural displacement field, the whole site is one static bundle under 40 KB gzipped, and the interesting parts are unit-tested without a GPU.

What is worth looking at

The hero could have been a CSS cross-fade. Building it in WebGL was the point, and so was making it testable:

  • The timing model is a pure function. frameAt(elapsedMs, photoCount, config) returns which two frames are on screen, the mix between them, and the displacement strength. No clock, no GL, no DOM. That is why the entire cycle, including the wrap after a full loop and the behavior after a tab has been open for a week, is covered by ordinary unit tests.
  • Cover-fit is computed on the CPU. Getting object-fit: cover wrong in a shader is the most visible bug a WebGL hero can ship, so coverScale is a plain function with its own test matrix rather than arithmetic buried in GLSL.
  • The shaders are checked structurally. A GLSL typo does not fail a TypeScript build; it fails at runtime as a black rectangle. The suite asserts that both stages agree on the GLSL version, that the vertex output is read as a fragment input, that every declared uniform is actually used, that the uniform list the renderer looks up matches the source exactly, and that no texture is sampled with an unclamped UV.
  • Translations cannot silently rot. Every string lives in one module keyed by locale, and a test asserts the two locales have identical key sets, non-empty values everywhere, actually different text (so a copy-paste placeholder fails), and ASCII-only content.

Everything degrades. No WebGL2 falls back to a CSS cross-fade with the same pacing. A missing image is skipped rather than rendering black. No IntersectionObserver means the reveal animations simply start visible.

Structure

src/core/        pure TypeScript: easing, the transition state machine, manifest validation
src/gl/          the WebGL2 renderer and the GLSL sources
src/content.ts   every string and project, in both languages, as data
src/photos.ts    the hero manifest
tests/           68 tests over core, content, and shader structure

Swapping in real photographs

The hero ships with generated placeholder frames, clearly labeled as placeholders in the manifest and in the footer. To use real work:

  1. Drop images into public/photos/. Wide crops around 2000 px on the long edge, progressive JPEG, quality 78 or so.
  2. Edit src/photos.ts: one entry per photo with a real alt, an optional caption, and the placeholder flag removed.
  3. Run npm test. The manifest validator rejects absolute paths, non-image extensions, duplicates, and missing alt text before the site ever renders.

Alt text is required by the validator, not optional. The hero is content.

Development

npm install
npm run dev
npm test
npm run build

Regenerate the README screenshots from the built site:

node scripts/shots.mjs

68 tests, no GPU required. CI runs the suite and the production build on every push, and a second workflow deploys dist to GitHub Pages.

Privacy

No analytics, no tracking, no third-party requests, no cookies, no fonts loaded from a CDN. A Content-Security-Policy meta tag holds it to that. The language preference lives in the URL rather than in storage, which also makes a Spanish link shareable: append ?lang=es.

Honest limitations

  • The hero images are generated placeholders, not photographs. They exist so the site looks finished before real work is dropped in.
  • The WebGL path is verified in headless Chromium via the screenshot script; it has not been tested across the full browser matrix.
  • Reduced motion calms the hero rather than freezing it: the dissolve continues at a slower pace with the distortion disabled. A completely static hero is a displacement: 0, transitionMs: 0 config away if that reading is preferred.

License

MIT, see LICENSE.

About

Photography and software portfolio: WebGL hero, bilingual, zero backend, under 40 KB gzipped

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages