Web Pages Hard
Game Portfolio
A retro GameBoy-inspired portfolio website featuring a live 3D GameBoy model rendered in Three.js with animated pixel-accurate screen, game-themed UI elements, and nostalgic design.
Open in Lab
MCP
html css javascript threejs gsap lenis
Targets: JS HTML
Code
:root {
color-scheme: dark;
--bg: #0b0d14;
--panel: rgba(15, 20, 32, 0.82);
--line: rgba(255, 255, 255, 0.12);
--text: #f6f5ef;
--muted: rgba(246, 245, 239, 0.6);
--accent: #f8d34a;
--accent-2: #ff6b6b;
--accent-3: #73f6ff;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Rajdhani", "Segoe UI", sans-serif;
background: radial-gradient(circle at top left, rgba(248, 211, 74, 0.2), transparent 55%),
radial-gradient(circle at 80% 10%, rgba(115, 246, 255, 0.18), transparent 55%),
radial-gradient(circle at 20% 80%, rgba(255, 107, 107, 0.18), transparent 60%), #090b12;
color: var(--text);
overflow-x: hidden;
}
a {
color: inherit;
text-decoration: none;
}
.bg-grid {
position: fixed;
inset: 0;
background-image: linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
background-size: 48px 48px;
opacity: 0.2;
pointer-events: none;
}
.bg-glow {
position: fixed;
inset: 0;
background: radial-gradient(circle at 60% 40%, rgba(255, 255, 255, 0.08), transparent 60%);
pointer-events: none;
}
.shell {
position: relative;
z-index: 1;
padding: 32px clamp(20px, 6vw, 90px) 90px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
padding: 16px 24px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 20px;
backdrop-filter: blur(16px);
}
.brand {
display: flex;
align-items: center;
gap: 16px;
}
.brand-mark {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(140deg, rgba(248, 211, 74, 0.9), rgba(255, 107, 107, 0.9));
color: #0b0d14;
display: grid;
place-items: center;
font-family: "Press Start 2P", system-ui;
font-size: 0.7rem;
}
.brand-title {
font-family: "Press Start 2P", system-ui;
font-size: 0.85rem;
margin: 0;
}
.brand-subtitle {
margin: 6px 0 0;
color: var(--muted);
font-size: 0.9rem;
}
.nav {
display: flex;
align-items: center;
gap: 18px;
font-size: 0.95rem;
}
.nav a {
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.65rem;
}
.nav-cta {
padding: 0.5rem 1.1rem;
border-radius: 999px;
border: 1px solid rgba(248, 211, 74, 0.6);
color: var(--text);
background: rgba(248, 211, 74, 0.1);
}
.hero {
margin-top: 42px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 32px;
align-items: center;
}
.hero-copy h1 {
font-family: "Press Start 2P", system-ui;
font-size: clamp(1.8rem, 3vw, 2.8rem);
line-height: 1.3;
margin-bottom: 20px;
}
.kicker {
text-transform: uppercase;
letter-spacing: 0.3em;
font-size: 0.7rem;
color: var(--accent);
margin-bottom: 14px;
}
.lead {
color: var(--muted);
max-width: 52ch;
line-height: 1.6;
}
.hero-actions {
display: flex;
gap: 12px;
margin-top: 24px;
flex-wrap: wrap;
}
.btn {
padding: 0.85rem 1.6rem;
border-radius: 12px;
border: 1px solid var(--accent);
background: transparent;
color: var(--text);
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.65rem;
cursor: pointer;
}
.btn.primary {
background: rgba(248, 211, 74, 0.2);
}
.btn.ghost {
border-color: rgba(115, 246, 255, 0.5);
}
.hero-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-top: 28px;
}
.hero-stats div {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 16px;
}
.hero-stats span {
display: block;
color: var(--muted);
font-size: 0.8rem;
}
.hero-stats strong {
font-size: 1.1rem;
}
.hero-stage {
display: grid;
place-items: center;
gap: 14px;
}
#gameboy-stage {
width: min(360px, 80vw);
height: min(520px, 60vh);
border-radius: 28px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(5, 8, 12, 0.8);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
overflow: hidden;
}
.stage-caption {
text-align: center;
font-size: 0.85rem;
color: var(--muted);
}
.stage-caption p {
margin: 0 0 6px;
font-family: "Press Start 2P", system-ui;
font-size: 0.7rem;
color: var(--accent-3);
}
.section {
margin-top: 56px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: baseline;
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.7rem;
color: var(--muted);
margin-bottom: 20px;
}
.section-header h2 {
font-family: "Press Start 2P", system-ui;
font-size: 1rem;
margin: 0;
color: var(--text);
}
.card-grid {
display: grid;
gap: 18px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 16px;
padding: 18px;
}
.card h3 {
margin: 12px 0 8px;
font-size: 1.1rem;
}
.card p {
color: var(--muted);
line-height: 1.5;
}
.card .badge {
display: inline-flex;
padding: 4px 8px;
border-radius: 8px;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
background: rgba(255, 107, 107, 0.2);
border: 1px solid rgba(255, 107, 107, 0.5);
color: var(--accent-2);
}
.card-meta {
display: flex;
justify-content: space-between;
margin-top: 12px;
color: var(--muted);
font-size: 0.85rem;
}
.loadout-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.loadout-item {
border: 1px dashed rgba(248, 211, 74, 0.5);
border-radius: 14px;
padding: 16px;
text-align: center;
background: rgba(9, 12, 20, 0.7);
}
.stats-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.stats-grid div {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 16px;
padding: 16px;
text-align: center;
}
.stats-grid h3 {
font-family: "Press Start 2P", system-ui;
font-size: 1.2rem;
margin: 0 0 8px;
color: var(--accent);
}
.tool-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.tool-card {
background: rgba(15, 20, 30, 0.75);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 14px;
padding: 16px;
text-align: center;
font-family: "Press Start 2P", system-ui;
font-size: 0.65rem;
}
.contact-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.contact-card {
display: grid;
grid-template-columns: auto 1fr;
gap: 16px;
align-items: center;
padding: 16px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(10, 14, 22, 0.8);
}
.contact-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: grid;
place-items: center;
background: rgba(255, 107, 107, 0.2);
color: var(--accent-2);
text-transform: uppercase;
font-family: "Press Start 2P", system-ui;
font-size: 0.6rem;
}
@media (max-width: 900px) {
.topbar {
flex-direction: column;
align-items: flex-start;
}
.nav {
flex-wrap: wrap;
}
}
@media (max-width: 640px) {
.shell {
padding: 24px 18px 72px;
}
}const data = {
missions: [
{
title: "Neon Raid",
description: "Design a HUD overlay for a cyber bike chase sequence.",
status: "Urgent",
reward: "1200 XP",
},
{
title: "Skyline Tower",
description: "Prototype a vertical navigation system for a megacity map.",
status: "Active",
reward: "860 XP",
},
{
title: "Quantum Bazaar",
description: "Create animated product cards for a futuristic marketplace.",
status: "Active",
reward: "940 XP",
},
{
title: "Specter Console",
description: "Build a command deck UI for a stealth operations team.",
status: "Queued",
reward: "760 XP",
},
],
loadout: [
{ name: "HUD Design", level: "Epic" },
{ name: "Motion FX", level: "Legendary" },
{ name: "3D Layout", level: "Rare" },
{ name: "Audio Sync", level: "Rare" },
{ name: "UX Systems", level: "Epic" },
{ name: "Prototype", level: "Legendary" },
],
stats: [
{ value: "48", label: "Missions completed" },
{ value: "12", label: "Live ops launches" },
{ value: "6", label: "Teams led" },
],
};
const missionGrid = document.getElementById("mission-cards");
const loadoutGrid = document.getElementById("loadout-items");
const statsGrid = document.getElementById("stats-grid");
if (missionGrid) {
missionGrid.innerHTML = data.missions
.map(
(mission) => `
<article class="card">
<span class="badge">${mission.status}</span>
<h3>${mission.title}</h3>
<p>${mission.description}</p>
<div class="card-meta">
<span>Reward</span>
<strong>${mission.reward}</strong>
</div>
</article>
`
)
.join("");
}
if (loadoutGrid) {
loadoutGrid.innerHTML = data.loadout
.map(
(item) => `
<article class="loadout-item">
<h4>${item.name}</h4>
<span>${item.level}</span>
</article>
`
)
.join("");
}
if (statsGrid) {
statsGrid.innerHTML = data.stats
.map(
(stat) => `
<div>
<h3>${stat.value}</h3>
<p>${stat.label}</p>
</div>
`
)
.join("");
}
const lenis = window.Lenis
? new Lenis({
smoothWheel: true,
smoothTouch: false,
})
: null;
function raf(time) {
if (lenis) lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
if (window.gsap) {
gsap.from(".topbar", { opacity: 0, y: -20, duration: 1, ease: "power3.out" });
gsap.utils
.toArray(".card, .loadout-item, .stats-grid div, .tool-card, .contact-card")
.forEach((item, index) => {
gsap.from(item, {
opacity: 0,
y: 20,
duration: 0.7,
delay: 0.05 * index,
ease: "power2.out",
});
});
}
class GameBoyScene {
constructor(container) {
this.container = container;
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(35, 1, 0.1, 100);
this.camera.position.set(0, 1.6, 6);
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setClearColor(0x000000, 0);
container.appendChild(this.renderer.domElement);
this.clock = new THREE.Clock();
this.buildLights();
this.buildGameBoy();
this.createScreenTexture();
this.bindEvents();
this.animate();
}
buildLights() {
this.scene.add(new THREE.AmbientLight(0xffffff, 0.7));
const key = new THREE.DirectionalLight(0xffffff, 0.9);
key.position.set(4, 6, 4);
this.scene.add(key);
const rim = new THREE.DirectionalLight(0x73f6ff, 0.5);
rim.position.set(-3, 2, -4);
this.scene.add(rim);
}
buildGameBoy() {
this.gameboy = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({
color: 0xd8d0c0,
roughness: 0.7,
metalness: 0.1,
});
const darkMat = new THREE.MeshStandardMaterial({
color: 0x3a3a3a,
roughness: 0.6,
metalness: 0.2,
});
const accentMat = new THREE.MeshStandardMaterial({
color: 0xff6b6b,
roughness: 0.5,
metalness: 0.2,
});
const body = new THREE.Mesh(new THREE.BoxGeometry(2.6, 4.2, 0.7), bodyMat);
body.castShadow = true;
body.receiveShadow = true;
const screenBezel = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.4, 0.05), darkMat);
screenBezel.position.set(0, 0.8, 0.38);
const screen = new THREE.Mesh(
new THREE.PlaneGeometry(1.5, 1.1),
new THREE.MeshStandardMaterial({ color: 0x88aa55, emissive: 0x335511 })
);
screen.position.set(0, 0.8, 0.405);
this.screenMesh = screen;
const dpad = new THREE.Group();
const dpadBase = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.14, 0.08), darkMat);
const dpadCross = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.5, 0.08), darkMat);
dpad.add(dpadBase, dpadCross);
dpad.position.set(-0.7, -0.4, 0.4);
const buttonA = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.16, 0.08, 32), accentMat);
buttonA.rotation.x = Math.PI / 2;
buttonA.position.set(0.6, -0.35, 0.42);
const buttonB = buttonA.clone();
buttonB.position.set(1.0, -0.55, 0.42);
const speaker = new THREE.Group();
for (let i = 0; i < 4; i++) {
const slot = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.04, 0.03), darkMat);
slot.position.set(0.65, -1.2 - i * 0.12, 0.39);
speaker.add(slot);
}
this.gameboy.add(body, screenBezel, screen, dpad, buttonA, buttonB, speaker);
this.scene.add(this.gameboy);
}
createScreenTexture() {
this.screenCanvas = document.createElement("canvas");
this.screenCanvas.width = 128;
this.screenCanvas.height = 128;
this.screenCtx = this.screenCanvas.getContext("2d");
this.screenTexture = new THREE.CanvasTexture(this.screenCanvas);
this.screenTexture.magFilter = THREE.NearestFilter;
this.screenTexture.minFilter = THREE.NearestFilter;
this.screenMesh.material.map = this.screenTexture;
this.screenMesh.material.needsUpdate = true;
}
drawScreen(time) {
const ctx = this.screenCtx;
const w = this.screenCanvas.width;
const h = this.screenCanvas.height;
ctx.fillStyle = "#b9d87a";
ctx.fillRect(0, 0, w, h);
const gridX = 12;
const gridY = 8;
const cols = 10;
const rows = 16;
const cell = 6;
const wellW = cols * cell;
const wellH = rows * cell;
const wellLeft = gridX;
const wellTop = gridY;
ctx.fillStyle = "#a6c56b";
ctx.fillRect(wellLeft - 2, wellTop - 2, wellW + 4, wellH + 4);
ctx.fillStyle = "#8cb05f";
ctx.fillRect(wellLeft, wellTop, wellW, wellH);
ctx.strokeStyle = "#6f8f4a";
ctx.lineWidth = 1;
for (let x = 0; x <= cols; x++) {
ctx.beginPath();
ctx.moveTo(wellLeft + x * cell, wellTop);
ctx.lineTo(wellLeft + x * cell, wellTop + wellH);
ctx.stroke();
}
for (let y = 0; y <= rows; y++) {
ctx.beginPath();
ctx.moveTo(wellLeft, wellTop + y * cell);
ctx.lineTo(wellLeft + wellW, wellTop + y * cell);
ctx.stroke();
}
const stackRows = 6;
ctx.fillStyle = "#48653b";
for (let y = rows - 1; y >= rows - stackRows; y--) {
for (let x = 0; x < cols; x++) {
if ((x + y) % 3 !== 0) {
ctx.fillRect(wellLeft + x * cell + 1, wellTop + y * cell + 1, cell - 2, cell - 2);
}
}
}
const pieceFrames = [
[
[0, 0],
[1, 0],
[0, 1],
[1, 1],
],
[
[0, 0],
[1, 0],
[2, 0],
[1, 1],
],
[
[0, 0],
[1, 0],
[2, 0],
[3, 0],
],
[
[0, 0],
[1, 0],
[1, 1],
[2, 1],
],
];
const frame = Math.floor(time * 1.2) % pieceFrames.length;
const fall = (time * 3) % (rows - stackRows - 2);
const piece = pieceFrames[frame];
const pieceX = 4;
const pieceY = Math.floor(fall);
ctx.fillStyle = "#2f4e2d";
piece.forEach(([dx, dy]) => {
ctx.fillRect(
wellLeft + (pieceX + dx) * cell + 1,
wellTop + (pieceY + dy) * cell + 1,
cell - 2,
cell - 2
);
});
ctx.fillStyle = "#2f4e2d";
ctx.fillRect(80, 12, 36, 6);
ctx.fillStyle = "#1f2f1e";
ctx.fillRect(80, 12, 10 + frame * 6, 6);
this.screenTexture.needsUpdate = true;
}
bindEvents() {
window.addEventListener("resize", () => this.onResize());
}
onResize() {
const width = this.container.clientWidth;
const height = this.container.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}
animate() {
const elapsed = this.clock.getElapsedTime();
this.drawScreen(elapsed);
this.gameboy.rotation.y = Math.sin(elapsed * 0.5) * 0.35;
this.gameboy.rotation.x = Math.sin(elapsed * 0.35) * 0.12;
this.gameboy.position.y = Math.sin(elapsed * 0.8) * 0.12;
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(() => this.animate());
}
}
const stage = document.getElementById("gameboy-stage");
if (stage && window.THREE) {
new GameBoyScene(stage);
}<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GameBoy Motion Portfolio</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Rajdhani:wght@400;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="bg-grid" aria-hidden="true"></div>
<div class="bg-glow" aria-hidden="true"></div>
<main class="shell">
<header class="topbar">
<div class="brand">
<span class="brand-mark">GB</span>
<div>
<p class="brand-title">Nova Byte</p>
<p class="brand-subtitle">Game UI + Motion Portfolio</p>
</div>
</div>
<nav class="nav">
<a href="#missions">Missions</a>
<a href="#loadout">Loadout</a>
<a href="#stats">Stats</a>
<a href="#contacts" class="nav-cta">Contact</a>
</nav>
</header>
<section class="hero">
<div class="hero-copy">
<p class="kicker">Player One</p>
<h1>Retro hardware. Modern 3D motion systems.</h1>
<p class="lead">
I design game-inspired product interfaces and cinematic HUDs. The GameBoy-inspired rig on the right
is rendered live in Three.js with a pixel-accurate screen animation.
</p>
<div class="hero-actions">
<button class="btn primary">Start run</button>
<button class="btn ghost">Download kit</button>
</div>
<div class="hero-stats">
<div>
<span>XP</span>
<strong>84%</strong>
</div>
<div>
<span>Rank</span>
<strong>Platinum</strong>
</div>
<div>
<span>Runs</span>
<strong>48</strong>
</div>
</div>
</div>
<div class="hero-stage">
<div id="gameboy-stage" aria-hidden="true"></div>
<div class="stage-caption">
<p>GameBoy Motion Rig</p>
<span>3D model + animated screen texture</span>
</div>
</div>
</section>
<section class="section" id="missions">
<div class="section-header">
<h2>Missions</h2>
<span>Active quest log</span>
</div>
<div class="card-grid" id="mission-cards"></div>
</section>
<section class="section" id="loadout">
<div class="section-header">
<h2>Loadout</h2>
<span>Skill modules</span>
</div>
<div class="loadout-grid" id="loadout-items"></div>
</section>
<section class="section" id="stats">
<div class="section-header">
<h2>Stats</h2>
<span>Career highlights</span>
</div>
<div class="stats-grid" id="stats-grid"></div>
</section>
<section class="section" id="toolkit">
<div class="section-header">
<h2>Tool Kit</h2>
<span>Production stack</span>
</div>
<div class="tool-grid">
<article class="tool-card">Figma</article>
<article class="tool-card">Three.js</article>
<article class="tool-card">GSAP</article>
<article class="tool-card">Spline</article>
<article class="tool-card">After Effects</article>
<article class="tool-card">Webflow</article>
</div>
</section>
<section class="section" id="contacts">
<div class="section-header">
<h2>Contact</h2>
<span>Let’s team up</span>
</div>
<div class="contact-grid">
<a class="contact-card" href="#">
<div class="contact-icon">in</div>
<div>
<h3>LinkedIn</h3>
<p>/nova-byte</p>
</div>
</a>
<a class="contact-card" href="#">
<div class="contact-icon">gh</div>
<div>
<h3>GitHub</h3>
<p>@nova-byte</p>
</div>
</a>
<a class="contact-card" href="#">
<div class="contact-icon">em</div>
<div>
<h3>Email</h3>
<p>nova.byte@gmail.com</p>
</div>
</a>
</div>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/@studio-freight/lenis@1.0.42/bundled/lenis.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script src="script.js"></script>
</body>
</html>Game Portfolio
A retro GameBoy-inspired portfolio website featuring a live 3D GameBoy model rendered in Three.js. The GameBoy’s screen displays an animated pixel-art game (similar to Tetris) with smooth animations, while the entire portfolio uses game-themed UI elements and nostalgic design patterns.
How it works
The portfolio combines retro aesthetics with modern web technologies:
- Three.js 3D Model — Live-rendered GameBoy model with realistic materials and lighting
- Animated Screen — Canvas-based pixel-art animation on the GameBoy screen
- Game-Themed UI — Missions, loadout, stats, and tool kit sections styled like game interfaces
- Smooth Scroll — Lenis integration for smooth scrolling experience
- GSAP Animations — Fade-in animations for cards and content elements
Key features
- Live 3D GameBoy model with Three.js
- Animated pixel-art screen (Tetris-like game)
- Game-themed UI elements (missions, loadout, stats)
- Retro color palette with yellow, red, and cyan accents
- Press Start 2P font for authentic retro feel
- Smooth scroll with Lenis
- GSAP-powered animations
- Responsive design
Technical details
The GameBoy scene includes:
- Realistic 3D model with proper materials
- Dynamic lighting (ambient, directional, rim lights)
- Animated screen texture with canvas rendering
- Smooth rotation and position animations
- Pixel-perfect screen rendering with nearest-neighbor filtering
When to use it
- Game developer portfolios
- Retro-themed websites
- Nostalgic brand experiences
- Creative developer showcases
- Interactive portfolio galleries
- Game UI/UX designer portfolios