VOXEL — 3D Artist Portfolio
:root {
–black: #050507;
–white: #f0ede8;
–acid: #c8ff00;
–crimson: #ff2d55;
–electric: #00d4ff;
–muted: #2a2a35;
–glass: rgba(255,255,255,0.04);
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
background: var(–black);
color: var(–white);
font-family: ‘Space Mono’, monospace;
overflow-x: hidden;
cursor: none;
}
/* CUSTOM CURSOR */
#cursor {
position: fixed;
width: 12px; height: 12px;
background: var(–acid);
border-radius: 50%;
pointer-events: none;
z-index: 9999;
transform: translate(-50%, -50%);
transition: width 0.2s, height 0.2s, background 0.2s;
mix-blend-mode: difference;
}
#cursor-ring {
position: fixed;
width: 40px; height: 40px;
border: 1px solid rgba(200,255,0,0.4);
border-radius: 50%;
pointer-events: none;
z-index: 9998;
transform: translate(-50%, -50%);
transition: transform 0.1s linear, width 0.3s, height 0.3s, border-color 0.3s;
}
body:hover #cursor { opacity: 1; }
/* NOISE OVERLAY */
body::before {
content: ”;
position: fixed;
inset: 0;
background-image: url(“data:image/svg+xml,%3Csvg viewBox=’0 0 256 256′ xmlns=’http://www.w3.org/2000/svg’%3E%3Cfilter id=’noise’%3E%3CfeTurbulence type=’fractalNoise’ baseFrequency=’0.9′ numOctaves=’4′ stitchTiles=’stitch’/%3E%3C/filter%3E%3Crect width=’100%25′ height=’100%25′ filter=’url(%23noise)’ opacity=’0.04’/%3E%3C/svg%3E”);
pointer-events: none;
z-index: 9990;
opacity: 0.35;
}
/* NAV */
nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
padding: 28px 48px;
mix-blend-mode: normal;
}
.nav-logo {
font-family: ‘Bebas Neue’, sans-serif;
font-size: 2rem;
letter-spacing: 0.15em;
color: var(–white);
text-decoration: none;
}
.nav-logo span { color: var(–acid); }
.nav-links {
display: flex;
gap: 40px;
list-style: none;
}
.nav-links a {
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(240,237,232,0.55);
text-decoration: none;
transition: color 0.25s;
position: relative;
}
.nav-links a::after {
content: ”;
position: absolute;
bottom: -3px; left: 0;
width: 0; height: 1px;
background: var(–acid);
transition: width 0.3s;
}
.nav-links a:hover { color: var(–white); }
.nav-links a:hover::after { width: 100%; }
.nav-cta {
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(–black);
background: var(–acid);
padding: 10px 22px;
text-decoration: none;
font-family: ‘Space Mono’, monospace;
transition: transform 0.2s, box-shadow 0.2s;
}
.nav-cta:hover {
transform: translate(-2px,-2px);
box-shadow: 4px 4px 0 var(–crimson);
}
/* HERO */
#hero {
position: relative;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
#canvas-container {
position: absolute;
inset: 0;
z-index: 1;
}
canvas { display: block; }
.hero-content {
position: relative;
z-index: 10;
text-align: center;
pointer-events: none;
}
.hero-tag {
font-size: 0.68rem;
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(–acid);
margin-bottom: 20px;
opacity: 0;
animation: fadeUp 1s 0.3s forwards;
}
.hero-title {
font-family: ‘Bebas Neue’, sans-serif;
font-size: clamp(5rem, 14vw, 13rem);
line-height: 0.9;
letter-spacing: 0.02em;
color: transparent;
-webkit-text-stroke: 1px rgba(240,237,232,0.15);
mix-blend-mode: screen;
opacity: 0;
animation: fadeUp 1s 0.5s forwards;
pointer-events: none;
user-select: none;
}
.hero-sub {
font-size: 0.72rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: rgba(240,237,232,0.45);
margin-top: 28px;
opacity: 0;
animation: fadeUp 1s 0.7s forwards;
}
.scroll-hint {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
opacity: 0;
animation: fadeIn 1s 1.5s forwards;
}
.scroll-hint span {
font-size: 0.6rem;
letter-spacing: 0.3em;
text-transform: uppercase;
color: rgba(240,237,232,0.35);
}
.scroll-line {
width: 1px;
height: 50px;
background: linear-gradient(to bottom, var(–acid), transparent);
animation: scrollPulse 2s infinite;
}
/* STATS BAR */
.stats-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
border-top: 1px solid rgba(255,255,255,0.06);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.stat-item {
padding: 36px 48px;
border-right: 1px solid rgba(255,255,255,0.06);
position: relative;
overflow: hidden;
}
.stat-item:last-child { border-right: none; }
.stat-item::before {
content: ”;
position: absolute;
inset: 0;
background: var(–glass);
opacity: 0;
transition: opacity 0.3s;
}
.stat-item:hover::before { opacity: 1; }
.stat-num {
font-family: ‘Bebas Neue’, sans-serif;
font-size: 3.5rem;
color: var(–acid);
line-height: 1;
display: block;
}
.stat-label {
font-size: 0.62rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: rgba(240,237,232,0.4);
margin-top: 6px;
display: block;
}
/* SECTION COMMONS */
section { padding: 120px 48px; }
.section-tag {
font-size: 0.62rem;
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(–acid);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.section-tag::before {
content: ”;
width: 30px; height: 1px;
background: var(–acid);
}
.section-title {
font-family: ‘Syne’, sans-serif;
font-size: clamp(2.5rem, 5vw, 4.5rem);
font-weight: 800;
line-height: 1.05;
max-width: 700px;
}
/* ABOUT */
#about {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 80px;
align-items: center;
max-width: 1400px;
margin: 0 auto;
padding: 120px 48px;
}
.about-text p {
font-size: 0.9rem;
line-height: 1.9;
color: rgba(240,237,232,0.6);
margin-top: 32px;
max-width: 500px;
}
.about-text p + p { margin-top: 16px; }
.about-visual {
position: relative;
height: 500px;
}
.about-canvas {
width: 100%;
height: 100%;
border: 1px solid rgba(255,255,255,0.06);
}
.about-badge {
position: absolute;
bottom: -24px;
right: -24px;
width: 120px; height: 120px;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(–black);
font-size: 0.55rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(240,237,232,0.5);
text-align: center;
animation: spin 20s linear infinite;
}
.about-badge strong {
font-family: ‘Bebas Neue’, sans-serif;
font-size: 1.8rem;
color: var(–acid);
display: block;
line-height: 1;
animation: spin 20s linear infinite reverse;
}
/* WORK GRID */
#work {
max-width: 1400px;
margin: 0 auto;
}
.work-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 60px;
}
.work-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: auto;
gap: 20px;
}
.work-card {
position: relative;
overflow: hidden;
background: var(–muted);
cursor: none;
group: true;
}
.work-card:nth-child(1) { grid-column: span 7; grid-row: span 1; }
.work-card:nth-child(2) { grid-column: span 5; grid-row: span 1; }
.work-card:nth-child(3) { grid-column: span 4; }
.work-card:nth-child(4) { grid-column: span 4; }
.work-card:nth-child(5) { grid-column: span 4; }
.work-card-inner {
width: 100%;
padding-top: 70%;
position: relative;
overflow: hidden;
}
.work-card:nth-child(1) .work-card-inner { padding-top: 55%; }
.work-card:nth-child(2) .work-card-inner { padding-top: 75%; }
.work-card-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.work-card-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(5,5,7,0.9) 0%, transparent 60%);
opacity: 0;
transition: opacity 0.4s;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 24px;
}
.work-card:hover .work-card-overlay { opacity: 1; }
.work-card-num {
font-size: 0.6rem;
letter-spacing: 0.3em;
color: var(–acid);
margin-bottom: 6px;
}
.work-card-title {
font-family: ‘Syne’, sans-serif;
font-size: 1.3rem;
font-weight: 800;
}
.work-card-type {
font-size: 0.62rem;
letter-spacing: 0.2em;
color: rgba(240,237,232,0.5);
margin-top: 4px;
}
/* SERVICES */
#services {
max-width: 1400px;
margin: 0 auto;
}
.services-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: rgba(255,255,255,0.06);
margin-top: 60px;
}
.service-card {
background: var(–black);
padding: 48px 40px;
position: relative;
overflow: hidden;
transition: background 0.3s;
}
.service-card:hover { background: var(–muted); }
.service-num {
font-family: ‘Bebas Neue’, sans-serif;
font-size: 5rem;
color: rgba(255,255,255,0.04);
line-height: 1;
position: absolute;
top: 20px; right: 24px;
}
.service-icon {
width: 44px; height: 44px;
border: 1px solid rgba(200,255,0,0.3);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
color: var(–acid);
font-size: 1.2rem;
transition: background 0.3s, border-color 0.3s;
}
.service-card:hover .service-icon {
background: var(–acid);
color: var(–black);
border-color: var(–acid);
}
.service-name {
font-family: ‘Syne’, sans-serif;
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 12px;
}
.service-desc {
font-size: 0.78rem;
line-height: 1.8;
color: rgba(240,237,232,0.45);
}
/* MARQUEE */
.marquee-section {
padding: 40px 0;
border-top: 1px solid rgba(255,255,255,0.06);
border-bottom: 1px solid rgba(255,255,255,0.06);
overflow: hidden;
}
.marquee-track {
display: flex;
gap: 60px;
animation: marquee 20s linear infinite;
white-space: nowrap;
}
.marquee-item {
font-family: ‘Bebas Neue’, sans-serif;
font-size: 1.8rem;
letter-spacing: 0.1em;
color: rgba(240,237,232,0.12);
display: flex;
align-items: center;
gap: 60px;
}
.marquee-item span {
width: 6px; height: 6px;
background: var(–acid);
border-radius: 50%;
display: inline-block;
}
/* CONTACT */
#contact {
max-width: 1400px;
margin: 0 auto;
text-align: center;
padding: 120px 48px 160px;
}
.contact-title {
font-family: ‘Bebas Neue’, sans-serif;
font-size: clamp(4rem, 10vw, 9rem);
letter-spacing: 0.02em;
line-height: 1;
margin: 40px 0;
background: linear-gradient(135deg, var(–white) 40%, rgba(240,237,232,0.15));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.contact-email {
font-size: 0.8rem;
letter-spacing: 0.3em;
text-transform: uppercase;
color: rgba(240,237,232,0.4);
text-decoration: none;
display: block;
margin-bottom: 48px;
transition: color 0.3s;
}
.contact-email:hover { color: var(–acid); }
.contact-btn {
display: inline-flex;
align-items: center;
gap: 16px;
font-family: ‘Space Mono’, monospace;
font-size: 0.75rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(–black);
background: var(–acid);
padding: 18px 40px;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.contact-btn:hover {
transform: translate(-3px,-3px);
box-shadow: 6px 6px 0 var(–crimson);
}
.contact-btn svg { transition: transform 0.3s; }
.contact-btn:hover svg { transform: translateX(4px); }
/* FOOTER */
footer {
border-top: 1px solid rgba(255,255,255,0.06);
padding: 32px 48px;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-copy {
font-size: 0.62rem;
letter-spacing: 0.15em;
color: rgba(240,237,232,0.25);
}
.footer-links {
display: flex;
gap: 32px;
}
.footer-links a {
font-size: 0.62rem;
letter-spacing: 0.15em;
color: rgba(240,237,232,0.25);
text-decoration: none;
text-transform: uppercase;
transition: color 0.2s;
}
.footer-links a:hover { color: var(–acid); }
/* ANIMATIONS */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeIn {
to { opacity: 1; }
}
@keyframes scrollPulse {
0%, 100% { opacity: 1; transform: scaleY(1); }
50% { opacity: 0.3; transform: scaleY(0.6); }
}
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* REVEAL */
.reveal {
opacity: 0;
transform: translateY(40px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
.reveal-delay-1 { transition-delay: 0.1s; }
.reveal-delay-2 { transition-delay: 0.2s; }
.reveal-delay-3 { transition-delay: 0.3s; }
VOXEL
Hire Me
✦ Creative 3D Artist & Motion Designer
FORM
FLUX
Drag to explore the model
12+Years Experience
340Projects Completed
80+Global Clients
22Awards Won
About Me
Sculpting Digital Realities Since 2012
I’m a multidisciplinary 3D artist pushing the boundaries between concept and execution. My work lives at the intersection of sculpture, motion, and code — crafting experiences that feel both alien and inevitable.
From hyper-realistic product visualization to surreal abstract worlds, every piece I make is an exploration of form, light, and the poetry hidden in geometry.
3D Modeling Motion Design Product Viz Character Art Concept Art VFX Generative Art World Building
3D Modeling Motion Design Product Viz Character Art Concept Art VFX Generative Art World Building
01 / 2024
Obsidian Throne
Character Design · Sculpture
02 / 2024
Liquid Chrome
Abstract · Motion
03 / 2023
Void Matter
Environment · VFX
04 / 2023
Neon Drift
Product Viz · Light
05 / 2023
Crystal Mind
Concept · Surreal
What I Do
Services &
Expertise
01
⬡
3D Modeling
From organic characters to hard-surface machines, I create precise, production-ready 3D assets for any medium or scale.
02
◈
Motion & Animation
Fluid, physics-driven animation that breathes life into static models — for film, games, and interactive experiences.
03
◉
Product Visualization
Photorealistic renders that make your product irresistible, with lighting setups and compositions built for impact.
04
⬠
Concept Art
Translating your ideas into compelling visual concepts — world-building, character design, or abstract exploration.
05
⟁
VFX & Simulations
Fluid dynamics, particle systems, and destruction simulations at film quality — crafted with Houdini and Blender.
06
⬧
Interactive 3D/Web
Real-time 3D experiences using Three.js and WebGL — from interactive product viewers to full immersive websites.
https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
// =====================
// CURSOR
// =====================
const cursor = document.getElementById(‘cursor’);
const ring = document.getElementById(‘cursor-ring’);
let mx = 0, my = 0, rx = 0, ry = 0;
document.addEventListener(‘mousemove’, e => { mx = e.clientX; my = e.clientY; });
function animateCursor() {
cursor.style.left = mx + ‘px’;
cursor.style.top = my + ‘px’;
rx += (mx – rx) * 0.12;
ry += (my – ry) *.12;
ring.style.left = rx + ‘px’;
ring.style.top = ry + ‘px’;
requestAnimationFrame(animateCursor);
}
animateCursor();
document.querySelectorAll(‘a, button, .work-card’).forEach(el => {
el.addEventListener(‘mouseenter’, () => {
cursor.style.width = ‘6px’; cursor.style.height = ‘6px’;
ring.style.width = ’60px’; ring.style.height = ’60px’;
ring.style.borderColor = ‘rgba(200,255,0,0.8)’;
});
el.addEventListener(‘mouseleave’, () => {
cursor.style.width = ’12px’; cursor.style.height = ’12px’;
ring.style.width = ’40px’; ring.style.height = ’40px’;
ring.style.borderColor = ‘rgba(200,255,0,0.4)’;
});
});
// =====================
// HERO SCENE
// =====================
(function() {
const container = document.getElementById(‘canvas-container’);
const w = container.clientWidth, h = container.clientHeight;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(w, h);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 100);
camera.position.set(0, 0, 5.5);
// LIGHTING
const ambientLight = new THREE.AmbientLight(0x111122, 1.5);
scene.add(ambientLight);
const light1 = new THREE.PointLight(0xc8ff00, 80, 30);
light1.position.set(3, 3, 3);
scene.add(light1);
const light2 = new THREE.PointLight(0x00d4ff, 60, 30);
light2.position.set(-3, -2, 2);
scene.add(light2);
const light3 = new THREE.PointLight(0xff2d55, 40, 30);
light3.position.set(0, -4, -2);
scene.add(light3);
// MAIN SCULPTURE — layered torus knots forming a complex shape
const group = new THREE.Group();
scene.add(group);
// Core knot
const knotGeo = new THREE.TorusKnotGeometry(1.2, 0.38, 200, 24, 2, 3);
const knotMat = new THREE.MeshPhysicalMaterial({
color: 0x050507,
metalness: 1.0,
roughness: 0.05,
envMapIntensity: 1.2,
clearcoat: 1.0,
clearcoatRoughness: 0.05,
reflectivity: 1.0,
});
const knot = new THREE.Mesh(knotGeo, knotMat);
group.add(knot);
// Wire overlay
const wireGeo = new THREE.TorusKnotGeometry(1.25, 0.39, 80, 8, 2, 3);
const wireMat = new THREE.MeshBasicMaterial({
color: 0xc8ff00,
wireframe: true,
transparent: true,
opacity: 0.08,
});
const wire = new THREE.Mesh(wireGeo, wireMat);
group.add(wire);
// Outer ring
const ringGeo = new THREE.TorusGeometry(2.2, 0.012, 8, 200);
const ringMat = new THREE.MeshBasicMaterial({ color: 0xc8ff00, transparent: true, opacity: 0.35 });
const ring3d = new THREE.Mesh(ringGeo, ringMat);
ring3d.rotation.x = Math.PI * 0.45;
group.add(ring3d);
const ring3dB = new THREE.Mesh(
new THREE.TorusGeometry(2.6, 0.008, 8, 200),
new THREE.MeshBasicMaterial({ color: 0x00d4ff, transparent: true, opacity: 0.2 })
);
ring3dB.rotation.x = Math.PI * 0.3;
ring3dB.rotation.z = Math.PI * 0.2;
group.add(ring3dB);
// Floating particles
const particleCount = 400;
const pPositions = new Float32Array(particleCount * 3);
const pSpeeds = [];
for (let i = 0; i {
isDragging = true; autoRot = false;
prevMouse = { x: e.clientX, y: e.clientY };
});
renderer.domElement.addEventListener(‘touchstart’, e => {
isDragging = true; autoRot = false;
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
});
window.addEventListener(‘mouseup’, () => { isDragging = false; setTimeout(() => autoRot = true, 2000); });
window.addEventListener(‘touchend’, () => { isDragging = false; setTimeout(() => autoRot = true, 2000); });
window.addEventListener(‘mousemove’, e => {
if (!isDragging) return;
const dx = e.clientX – prevMouse.x;
const dy = e.clientY – prevMouse.y;
targetRot.y += dx * 0.008;
targetRot.x += dy * 0.008;
prevMouse = { x: e.clientX, y: e.clientY };
});
window.addEventListener(‘touchmove’, e => {
if (!isDragging) return;
const dx = e.touches[0].clientX – prevMouse.x;
const dy = e.touches[0].clientY – prevMouse.y;
targetRot.y += dx * 0.008;
targetRot.x += dy * 0.008;
prevMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
});
// Parallax on mousemove
let mouseWorld = { x: 0, y: 0 };
document.addEventListener(‘mousemove’, e => {
mouseWorld.x = (e.clientX / window.innerWidth – 0.5) * 2;
mouseWorld.y = -(e.clientY / window.innerHeight – 0.5) * 2;
});
let t = 0;
function animate() {
requestAnimationFrame(animate);
t += 0.005;
if (autoRot) {
targetRot.y += 0.004;
targetRot.x = Math.sin(t * 0.5) * 0.2;
}
currentRot.x += (targetRot.x – currentRot.x) * 0.05;
currentRot.y += (targetRot.y – currentRot.y) * 0.05;
group.rotation.x = currentRot.x;
group.rotation.y = currentRot.y;
wire.rotation.y = -t * 0.3;
// Light pulse
light1.intensity = 60 + Math.sin(t * 1.5) * 20;
light2.intensity = 50 + Math.cos(t * 1.2) * 15;
// Camera parallax
camera.position.x += (mouseWorld.x * 0.3 – camera.position.x) * 0.04;
camera.position.y += (mouseWorld.y * 0.2 – camera.position.y) * 0.04;
camera.lookAt(0, 0, 0);
// Animate particles
const pos = pGeo.attributes.position.array;
for (let i = 0; i {
const nw = container.clientWidth, nh = container.clientHeight;
camera.aspect = nw / nh;
camera.updateProjectionMatrix();
renderer.setSize(nw, nh);
});
})();
// =====================
// ABOUT MINI SCENE
// =====================
(function() {
const canvas = document.getElementById(‘about-canvas’);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 50);
camera.position.set(0, 0, 4);
const light1 = new THREE.PointLight(0xc8ff00, 50, 20);
light1.position.set(2, 2, 2);
scene.add(light1);
const light2 = new THREE.PointLight(0x00d4ff, 30, 20);
light2.position.set(-2, -2, 1);
scene.add(light2);
scene.add(new THREE.AmbientLight(0x112233, 2));
const geo = new THREE.IcosahedronGeometry(1.4, 2);
const mat = new THREE.MeshPhysicalMaterial({
color: 0x050507, metalness: 0.9, roughness: 0.1, clearcoat: 1,
});
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
const wireGeo = new THREE.IcosahedronGeometry(1.45, 2);
const wireMat = new THREE.MeshBasicMaterial({ color: 0xc8ff00, wireframe: true, transparent: true, opacity: 0.15 });
scene.add(new THREE.Mesh(wireGeo, wireMat));
function resize() {
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
resize();
window.addEventListener(‘resize’, resize);
let t = 0;
function animate() {
requestAnimationFrame(animate);
t += 0.008;
mesh.rotation.y = t;
mesh.rotation.x = Math.sin(t * 0.5) * 0.4;
renderer.render(scene, camera);
}
animate();
})();
// =====================
// WORK CARD MINI SCENES
// =====================
const cardConfigs = [
{ id: ‘c1’, geo: () => new THREE.TorusKnotGeometry(0.9, 0.28, 120, 16, 3, 5), color: 0xff2d55, accent: 0xc8ff00 },
{ id: ‘c2’, geo: () => new THREE.SphereGeometry(1, 64, 64), color: 0x00d4ff, accent: 0xffffff },
{ id: ‘c3’, geo: () => new THREE.OctahedronGeometry(1.2, 2), color: 0xc8ff00, accent: 0xff2d55 },
{ id: ‘c4’, geo: () => new THREE.TorusGeometry(1, 0.35, 32, 100), color: 0x8844ff, accent: 0x00d4ff },
{ id: ‘c5’, geo: () => new THREE.IcosahedronGeometry(1.2, 1), color: 0x00ffa3, accent: 0xc8ff00 },
];
cardConfigs.forEach(cfg => {
const canvas = document.getElementById(cfg.id);
if (!canvas) return;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 50);
camera.position.z = 3.5;
const l1 = new THREE.PointLight(cfg.color, 60, 20); l1.position.set(2, 2, 2); scene.add(l1);
const l2 = new THREE.PointLight(cfg.accent, 40, 20); l2.position.set(-2, -1, 1); scene.add(l2);
scene.add(new THREE.AmbientLight(0x111111, 2));
const mat = new THREE.MeshPhysicalMaterial({
color: 0x080810, metalness: 0.95, roughness: 0.05, clearcoat: 1,
});
const mesh = new THREE.Mesh(cfg.geo(), mat);
scene.add(mesh);
const wireMesh = new THREE.Mesh(cfg.geo(), new THREE.MeshBasicMaterial({ color: cfg.color, wireframe: true, transparent: true, opacity: 0.1 }));
scene.add(wireMesh);
function resize() {
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
resize();
window.addEventListener(‘resize’, resize);
let t = Math.random() * 100;
const speed = 0.006 + Math.random() * 0.006;
function animate() {
requestAnimationFrame(animate);
t += speed;
mesh.rotation.y = t;
mesh.rotation.x = Math.sin(t * 0.7) * 0.4;
wireMesh.rotation.y = -t * 0.5;
l1.intensity = 50 + Math.sin(t * 2) * 15;
renderer.render(scene, camera);
}
animate();
});
// =====================
// SCROLL REVEAL
// =====================
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) { e.target.classList.add(‘visible’); observer.unobserve(e.target); }
});
}, { threshold: 0.15 });
document.querySelectorAll(‘.reveal’).forEach(el => observer.observe(el));