Skip to content

Repository files navigation

Contributors Forks Stargazers Issues MIT License LinkedIn

3D Portfolio

An interactive 3D developer portfolio that still loads on the phone in your pocket.

View Demo »  ·  Report Bug  ·  Request Feature

Table of Contents
  1. About The Project
  2. Features
  3. Getting Started
  4. Usage
  5. Roadmap
  6. Contributing
  7. License
  8. Contact
  9. Acknowledgments

About The Project

3D Portfolio screenshot

A personal portfolio built around a real-time 3D scene: a workstation model in the hero, a rotating globe beside the contact form, a point-cloud starfield, and thirteen technology badges rendered as faceted 3D balls. It started as a Three.js tutorial follow-along on Vite and has since been rebuilt on the Next.js App Router.

The hard part was never the 3D, it was shipping it. A scene that feels effortless on a desktop GPU can be unusable on a mid-range phone, so the front end is built around one decision: measure the device first, then choose what to render. Capable hardware gets the full interactive scene. Everything else gets a pre-rendered still of that same scene, framed identically, at a fraction of the bytes.

That gate cut the mobile first load from 17.59 MB across 131 requests to 0.37 MB across 73, with no visible change to the design.

System architecture diagram

(back to top)

Built With

Next React TypeScript Three.js R3F Tailwind Motion oxlint Node Netlify

Framework Next.js 16.3 (App Router, Turbopack) · React 19.2
3D three.js 0.185 · @react-three/fiber 9.7 · @react-three/drei 10.7
Styling Tailwind CSS 4.3 (CSS-first @theme) · Poppins via next/font
Animation Motion 13.1
Contact EmailJS (browser SDK, no backend)
Tooling oxlint 1.80 · TypeScript 5.9 (strict) · sharp

(back to top)

Features

Capability-tiered 3D. On mount the app reads prefers-reduced-motion, pointer coarseness, viewport size, Save-Data, effective connection type, deviceMemory, hardwareConcurrency and a single memoised WebGL probe, and resolves a render tier of full, lite or static. Each scene declares what it costs; the policy layer decides what actually mounts.

Pre-rendered posters instead of multi-megabyte models. The hero, the globe and all thirteen technology badges have alpha-WebP stills captured from the real scenes, at the same camera, transform and aspect the live canvas would have used. Together they are about 232 KB standing in for roughly 19 MB of glTF, and because they sit in the same slot with object-contain the layout never shifts.

Opt-in escape hatch. Gated scenes offer a Load interactive 3D control, so a phone visitor who wants the real thing can have it.

WebGL contexts kept in budget. Canvases mount only while their section is near the viewport, and unmount on a delay so a fast scroll does not create and destroy a context in a few frames. Peak live contexts went from sixteen, the point where browsers start dropping the oldest, to four or fewer.

Real loading and error states. Every canvas sits behind its own retryable error boundary, so a failed texture degrades to a poster instead of blanking the page. Alongside it: determinate in-canvas progress, layout-matched skeletons for cards, timeline entries and badges, and inline SVG illustrations for the WebGL-unavailable, data-saver, scene-error and 404 cases.

Contact form as a state machine. idle -> sending -> success | error, with per-field validation, focus moved to the first invalid field, a honeypot, a draft preserved on failure and a direct mailto fallback. Delivery sits behind an interface, so the EmailJS call can be swapped for a Route Handler without touching the form.

Accessibility. Skip link, keyboard-operable mobile menu with aria-expanded and Escape/outside-tap dismissal, IntersectionObserver scroll-spy driving aria-current, visible focus rings for keyboard users only, and prefers-reduced-motion honoured across CSS, Motion and the 3D loops.

SEO and PWA surface. Full metadata, OpenGraph and Twitter cards, JSON-LD Person, plus generated sitemap.xml, robots.txt and manifest.webmanifest.

Responsive to the full breakpoint scale. xs 450px, then Tailwind's sm 640, md 768, lg 1024, xl 1280 and 2xl 1536. A single Container primitive owns the measure and widens with the viewport (80rem, then 84rem at xl, 96rem at 2xl), so a 1920px display uses 80% of its width rather than 67%. The contact form and globe split at lg instead of xl, which stops 1024-1280px collapsing into one tall column.

Asset pipeline. scripts/optimize-assets.mjs caps texture resolution, re-encodes with sharp and minifies the glTF JSON. It is idempotent via a manifest, so JPEGs are never re-compressed twice.

(back to top)

Getting Started

Prerequisites

  • Node.js 20.9 or newer (required by Next.js 16)
  • npm (the repo ships package-lock.json)
node --version

Installation

  1. Clone the repository
git clone https://github.com/omunite215/Project_3DPortfolio.git
  1. Navigate to the project directory
cd Project_3DPortfolio
  1. Install dependencies
npm install
  1. (Optional) Point the contact form at your own inbox
cp .env.example .env.local

Every variable is optional - with no .env.local the app builds and runs against the original configuration. See .env.example for the EmailJS keys and the canonical site URL.

  1. Start the development server
npm run dev

The site is served at http://localhost:3000.

(back to top)

Usage

npm run dev              # development server (Turbopack)
npm run build            # production build
npm run start            # serve the production build
npm run lint             # oxlint
npm run lint:fix         # oxlint with autofix
npm run typecheck        # tsc --noEmit
npm run check            # lint + typecheck
npm run optimize:assets  # downsample and re-encode the glTF textures

Changing the content width. The ladder lives in app/styles/globals.css as --container-content, --container-content-xl and --container-content-2xl, consumed by app/components/layout/Container.tsx. Nothing else hardcodes a max-width.

Editing content. Everything on the page - services, technologies, work history, social profiles and projects - is data in app/constants/index.ts. Add a project there and a card appears; add a technology and a badge appears.

Adding a technology badge. Drop the icon in public/tech/, add the entry to technologies, then place a matching still at public/posters/tech/<icon-name>.webp so constrained devices have something to show. Without it the badge falls back to a line illustration rather than breaking.

Forcing a render tier while developing. The tier is derived from the real environment, so the quickest way to exercise the constrained path is Chrome DevTools - enable Emulate CSS prefers-reduced-motion, or throttle to Slow 3G, and reload. Device emulation alone triggers the lite tier.

Re-running the asset pipeline. npm run optimize:assets skips anything already recorded in scripts/assets-optimized.json. Pass --dry to preview:

node scripts/optimize-assets.mjs --dry

(back to top)

Roadmap

  • Draco or Meshopt compression for the two scene.bin files (5.8 MB of geometry, the largest remaining payload)
  • Share one WebGL context across the thirteen technology badges via drei View, removing the per-badge canvas on desktop
  • KTX2 / Basis supercompressed textures with transcoding on the GPU
  • Filter projects by tag
  • Case-study pages per project instead of a single card
  • Move the contact form to a Route Handler so the EmailJS identifiers leave the client bundle
  • Automated Lighthouse CI budget to keep the mobile payload from regressing
  • Playwright visual-regression suite over the baseline screenshots
  • Light theme

See the open issues for a full list of proposed features and known issues.

(back to top)

Contributing

Contributions make the open-source community a great place to learn and build. Any contributions you make are greatly appreciated.

  1. Fork the project
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a pull request

(back to top)

License

Distributed under the MIT License. See LICENSE for more information.

(back to top)

Contact

Om Patel

GitHub LinkedIn Instagram Portfolio Email

Project link: https://github.com/omunite215/Project_3DPortfolio

(back to top)

Acknowledgments

(back to top)


3D Portfolio logo

Built by Om Patel

Releases

Packages

Used by

Contributors

Languages