A portfolio built as one continuous scrolling journey β five destinations (Home, Experience, About, Certifications, Contact) connected by a morphing star map and a WebGL aurora, looping seamlessly back to the start.
There is no router and there are no page loads. The whole site is a single scrolling loop whose
position is a signal, read every frame by a single shared requestAnimationFrame pulse running
outside Angular's zone. Everything below follows from that one decision.
- Start here: the onboarding doc
- Getting started
- Features
- Tech stack
- How it works
- Project structure
- Architecture decisions
- Deployment
- Screenshots
The best way to understand how everything works together is the interactive engineering onboarding guide β a self-contained HTML doc (no build step, no dependencies; just open docs/onboarding.html in any browser). It walks through:
- Quick Start β prerequisites, install & run, script reference, deployment
- Architecture β the star-map wayfinding concept, an interactive system diagram, the scroll loop that acts as the site's spine, signals and the 60fps hot path, and the theming model
- Component Map β a directory tour, why there is no router, every component, core service, and data file
- Modification Guide β step-by-step recipes for adding a project, updating certifications, tweaking the constellation, tuning the aurora, and adding a whole new destination
Supporting references:
- CONTEXT.md β the design-language glossary
- docs/adr β Architecture Decision Records for every significant design choice
- Node.js v18.19+
- Angular CLI β
npm install -g @angular/cli
git clone https://github.com/Robotbino/PorfolioWebsite.gitcd PorfolioWebsite && npm install && npm startVisit http://localhost:4200.
The repo also carries
.claude/launch.json, which starts the same server on 4388 for the in-editor preview. Both are the samenpm start.
| Script | What it does |
|---|---|
npm start |
Dev server with live reload on :4200 |
npm run build |
Production build into dist/professional-porfolio/browser, then generates the CSP into _headers |
npm run watch |
Rebuilding development bundle, no server |
npm test |
Unit tests in Karma + Jasmine (headless) |
npm run lint |
ESLint over TypeScript and templates |
npm run format |
Prettier over TypeScript and config |
npm testThe pure logic lives in unit-tested modules with colocated .spec.ts files β scroll math, the
constellation morph and its driver, the frame pulse, theme decisions, certifications math,
destinations, the aurora palette, in-viewport observation, scroll lock, motion math, and the
page-inert isolation behind both overlays. Anything that can be a pure function is one, precisely
so it can be tested without a DOM.
Karma runs headless via karma.conf.js, so this works in CI and in a container.
No Chrome installed? Point Karma at another Chromium build β for example, on Windows:
CHROME_BIN="C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" npm test
- Looping Scroll β The page scrolls through all five sections and wraps invisibly back to the top via a cloned seam, so the journey never ends
- Morphing Star Map β An SVG constellation interpolates between figures as you scroll, acting as wayfinding for where you are in the loop
- WebGL Aurora β A GLSL simplex-noise shader (via OGL) renders a living aurora behind every section; coarse-pointer devices get an animated CSS-gradient fallback instead of a shader
- Single Frame Loop β One shared
requestAnimationFramepulse runs outside Angular's zone and drives every animation, keeping change detection off the hot path - Horizontal Projects Showcase β The Experience section pins and scrolls its project cards sideways on the same rAF pulse, with keyboard framing and a progress indicator
- Certification Spotlight β Each certification opens full-size in a focus-trapped overlay with a verify link; on fine pointers a floating preview tethers to the cursor
- Marquee Email β The Contact finale features a large marquee email with one-click copy-to-clipboard, plus a colophon with a live SAST clock
- Theme Toggle β Dark/light mode with system-preference detection,
localStoragepersistence, and a signals-basedThemeServicethe whole app reacts to - No Theme Flash β An inline pre-paint guard in
index.htmlsets the theme class before Angular boots, mirroringtheme.decision.ts - Accessible β Semantic HTML, ARIA labels, keyboard navigation, and full
prefers-reduced-motionsupport (the morph and aurora settle down when asked) - Responsive β Optimized layouts for desktop, tablet, and mobile
- CV Download β One click, straight to your downloads folder β recruiters, this one's for you ;)
| Category | Technologies |
|---|---|
| Framework | Angular 21 (standalone components + signals) |
| Language | TypeScript 5.6 |
| Graphics | OGL (WebGL2 shader), SVG morphing |
| Styling | CSS3 (Custom Properties, Flexbox, Grid) |
| Typography | Nohemi (display), Atkinson Hyperlegible (body/UI), Instrument Serif (accent) β all self-hosted |
| Icons | Inline SVG (shared/icon) |
| Testing | Jasmine + Karma |
| Hosting | Netlify |
Runtime dependencies are deliberately few: Angular, OGL and RxJS. No animation library, no GSAP, no UI kit, and no icon font β the motion is hand-rolled against the shared frame pulse, and the sixteen icons ship as path data rather than the 299 kB Font Awesome cost to draw them.
The scroll loop is the spine of the site. ScrollLoopService owns the reader's cycle position
(0 = Home, 1 = Experience, β¦ wrapping at the seam), with the arithmetic extracted into pure,
unit-tested functions in scroll-loop.math.ts. The app shell feeds it raw scroll offsets;
everything else β the constellation morph, the loop-aware nav muting, the projects showcase β
reads the position signal from the shared frame pulse without ever triggering change detection
at 60fps.
Three rules keep that hot path honest:
- One pulse.
FramePulseServiceowns the onlyrequestAnimationFrameloop; components subscribe to it rather than starting their own. - One observer seam.
InViewportServiceowns viewport observation, so offscreen work pauses instead of burning frames. - Pure math, testable. Anything that can be a function of numbers is one:
scroll-loop.math.ts,motion.math.ts,certifications.math.ts,constellation-morph.ts,theme.decision.ts.
There is deliberately no Angular Router and no NgModule: the app bootstraps through
bootstrapApplication, and the five destinations are defined once in
src/app/destinations.ts and composed into a single looping page by the app shell. Adding a
destination means adding an entry there β the nav, the constellation, and the loop arithmetic all
derive from that list.
Every component is OnPush, which the lint config enforces: with one shared frame loop driving
the animation, a default-strategy component would be re-checked on every event it never needs.
src/app/
βββ aurora/ # WebGL aurora background (OGL + GLSL shader, coarse-pointer CSS fallback)
βββ constellation/ # Morphing star map (figures, morph driver, interpolation)
βββ core/ # Services: frame pulse (shared rAF), theme, motion settings,
β # in-viewport, scroll lock, nav transitions, aurora palette
βββ landingpage/ # Home / hero section + CV download
βββ layout/site-nav/ # Navigation with loop-aware muting
βββ pages/
β βββ work/ # Experience timeline + horizontal showcase (work-data.ts)
β βββ about/
β βββ certifications/ # Spotlight overlay (certifications-data.ts, certifications.math.ts)
β βββ contact/ # Marquee email, channels, colophon with live SAST clock
βββ shared/
β βββ icon/ # Inline SVG icon set (path data + <app-icon>)
β βββ theme-toggle/
βββ destinations.ts # Single source of truth for the five destinations
βββ scroll-loop.service.ts # Scroll cycle state + seam wrap
βββ scroll-loop.math.ts # Pure scroll math (unit-tested)
βββ motion.math.ts # Frame-rate-independent smoothing (unit-tested)
βββ scroll-reveal.directive.ts # Fade-in-on-scroll behavior
src/
βββ assets/ # CV, portrait, project shots, certificate images, self-hosted fonts
βββ index.html # Font preloads, pre-paint theme guard, JSON-LD
βββ styles.css # Design tokens, @font-face, global type scale
public/ # robots.txt, sitemap, manifest, icons, share card
scripts/gen-headers.mjs # Generates the CSP into the built _headers
.github/workflows/ci.yml # Lint, format, test, build on every push
docs/
βββ onboarding.html # β Interactive engineering onboarding guide β start here
βββ adr/ # Architecture Decision Records
CONTEXT.md # Design-language glossary
Every significant design decision is recorded as an ADR β including the ones that were tried and rejected, because the trail is the point.
| ADR | Decision | Status |
|---|---|---|
| 0001 | Full multipage routing, immersion-first | accepted, superseded by 0003 |
| 0002 | Star-map wayfinding as the unifying concept | accepted, revised by 0003 |
| 0003 | Looping-scroll immersion with real constellations | accepted |
| 0004 | Seamless one-direction loop via a cloned Home buffer | accepted |
| 0005 | Loop-aware (Home-anchored) navigation muting | accepted |
| 0006 | Frosted-glass surface for content cards | rejected |
| 0007 | Deepen the Cycle into ScrollLoopService |
accepted |
| 0008 | Projects showcase β progress, focus, keyboard framing | accepted |
| 0009 | Inline SVG icon set instead of an icon font | accepted |
| 0010 | Standalone components on Angular 21 | accepted |
Live at binohlongwana.netlify.app, deployed to Netlify via netlify.toml:
- Build:
npm run build(ng build, thenscripts/gen-headers.mjs) - Publish directory:
dist/professional-porfolio/browser - SPA fallback:
/* β /index.html 200(also mirrored insrc/_redirects) - Caching and security headers live in netlify.toml; the
Content-Security-Policy is generated into
_headersafter each build, because it carries the hash of the pre-paint theme guard inindex.html
Bino Hlongwana :)
This project is open source and available under the MIT License.

