Skip to content

Commit 1626c2c

Browse files
danribesclaude
andcommitted
feat: Add 3D animated wireframe figures to hero background
Add Three.js-powered mathematical wireframe shapes (torus, icosahedron, dodecahedron, octahedron, torus knot, geodesic sphere, tetrahedron, thin ring) with glowing particle field behind hero content. Includes mouse parallax, IntersectionObserver for off-screen pause, mobile optimization, and reduced-motion support. Also removes orphaned createFloatingShapes() console error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0d068a5 commit 1626c2c

4 files changed

Lines changed: 222 additions & 5 deletions

File tree

css/style.css

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,16 +356,26 @@ ul {
356356
margin-top: 70px;
357357
}
358358

359+
#hero-3d-canvas {
360+
position: absolute;
361+
top: 0;
362+
left: 0;
363+
width: 100%;
364+
height: 100%;
365+
z-index: 0;
366+
pointer-events: none;
367+
}
368+
359369
.hero::before {
360370
content: '';
361371
position: absolute;
362372
top: 0;
363373
left: 0;
364374
right: 0;
365375
bottom: 0;
366-
background-image:
376+
background-image:
367377
/* AI/ML Code snippets & Statistical formulas */
368-
url("data:image/svg+xml,%3Csvg width='800' height='600' xmlns='http://www.w3.org/2000/svg'%3E%3Cg opacity='0.08'%3E%3C!-- Programming code --%3E%3Ctext x='450' y='80' font-family='Courier New, monospace' font-size='12' fill='%23667eea'%3Eclass HealthEconomics:%3C/text%3E%3Ctext x='470' y='100' font-family='Courier New, monospace' font-size='11' fill='%234facfe'%3Edef calculate_icer(self):%3C/text%3E%3Ctext x='490' y='120' font-family='Courier New, monospace' font-size='10' fill='%2343e97b'%3Ereturn (ΔC/ΔE)%3C/text%3E%3Ctext x='450' y='150' font-family='Courier New, monospace' font-size='11' fill='%23667eea'%3Efor state in markov:%3C/text%3E%3Ctext x='470' y='170' font-family='Courier New, monospace' font-size='10' fill='%234facfe'%3Etransition_prob[i][j]%3C/text%3E%3C!-- Cost-effectiveness plane --%3E%3Cline x1='50' y1='500' x2='550' y2='500' stroke='%23667eea' stroke-width='2'/%3E%3Cline x1='50' y1='500' x2='50' y2='200' stroke='%23667eea' stroke-width='2'/%3E%3Ctext x='520' y='520' fill='%23667eea' font-size='14'%3EΔE%3C/text%3E%3Ctext x='20' y='220' fill='%23667eea' font-size='14'%3EΔC%3C/text%3E%3C!-- Threshold line --%3E%3Cline x1='50' y1='500' x2='500' y2='250' stroke='%234facfe' stroke-width='1.5' stroke-dasharray='5,5'/%3E%3Ctext x='300' y='370' fill='%234facfe' font-size='11'%3Eλ = £30k/QALY%3C/text%3E%3C!-- Data points --%3E%3Ccircle cx='200' cy='400' r='4' fill='%2343e97b'/%3E%3Ccircle cx='300' cy='350' r='4' fill='%2343e97b'/%3E%3Ccircle cx='400' cy='330' r='4' fill='%2343e97b'/%3E%3Ccircle cx='350' cy='360' r='4' fill='%2343e97b'/%3E%3C!-- Statistical formulas --%3E%3Ctext x='450' y='400' font-family='monospace' font-size='12' fill='%23667eea'%3EP(x) = e^(β₀+β₁x)%3C/text%3E%3Ctext x='450' y='430' font-family='monospace' font-size='11' fill='%234facfe'%3EQALY = Σ(uᵢ·tᵢ)%3C/text%3E%3Ctext x='450' y='460' font-family='monospace' font-size='11' fill='%2343e97b'%3ES(t) = e^(-λt)%3C/text%3E%3C/g%3E%3C/svg%3E");
378+
url("data:image/svg+xml,%3Csvg width='800' height='600' xmlns='http://www.w3.org/2000/svg'%3E%3Cg opacity='0.04'%3E%3C!-- Programming code --%3E%3Ctext x='450' y='80' font-family='Courier New, monospace' font-size='12' fill='%23667eea'%3Eclass HealthEconomics:%3C/text%3E%3Ctext x='470' y='100' font-family='Courier New, monospace' font-size='11' fill='%234facfe'%3Edef calculate_icer(self):%3C/text%3E%3Ctext x='490' y='120' font-family='Courier New, monospace' font-size='10' fill='%2343e97b'%3Ereturn (ΔC/ΔE)%3C/text%3E%3Ctext x='450' y='150' font-family='Courier New, monospace' font-size='11' fill='%23667eea'%3Efor state in markov:%3C/text%3E%3Ctext x='470' y='170' font-family='Courier New, monospace' font-size='10' fill='%234facfe'%3Etransition_prob[i][j]%3C/text%3E%3C!-- Cost-effectiveness plane --%3E%3Cline x1='50' y1='500' x2='550' y2='500' stroke='%23667eea' stroke-width='2'/%3E%3Cline x1='50' y1='500' x2='50' y2='200' stroke='%23667eea' stroke-width='2'/%3E%3Ctext x='520' y='520' fill='%23667eea' font-size='14'%3EΔE%3C/text%3E%3Ctext x='20' y='220' fill='%23667eea' font-size='14'%3EΔC%3C/text%3E%3C!-- Threshold line --%3E%3Cline x1='50' y1='500' x2='500' y2='250' stroke='%234facfe' stroke-width='1.5' stroke-dasharray='5,5'/%3E%3Ctext x='300' y='370' fill='%234facfe' font-size='11'%3Eλ = £30k/QALY%3C/text%3E%3C!-- Data points --%3E%3Ccircle cx='200' cy='400' r='4' fill='%2343e97b'/%3E%3Ccircle cx='300' cy='350' r='4' fill='%2343e97b'/%3E%3Ccircle cx='400' cy='330' r='4' fill='%2343e97b'/%3E%3Ccircle cx='350' cy='360' r='4' fill='%2343e97b'/%3E%3C!-- Statistical formulas --%3E%3Ctext x='450' y='400' font-family='monospace' font-size='12' fill='%23667eea'%3EP(x) = e^(β₀+β₁x)%3C/text%3E%3Ctext x='450' y='430' font-family='monospace' font-size='11' fill='%234facfe'%3EQALY = Σ(uᵢ·tᵢ)%3C/text%3E%3Ctext x='450' y='460' font-family='monospace' font-size='11' fill='%2343e97b'%3ES(t) = e^(-λt)%3C/text%3E%3C/g%3E%3C/svg%3E");
369379
background-size: 800px 600px;
370380
background-position: right center;
371381
background-repeat: no-repeat;

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
<!-- Hero Section -->
4141
<section id="home" class="hero">
42+
<canvas id="hero-3d-canvas"></canvas>
4243
<div class="container">
4344
<div class="hero-content">
4445
<div class="hero-text">
@@ -727,6 +728,8 @@ <h3>Daniel Ribes</h3>
727728
</div>
728729
</footer>
729730

731+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
732+
<script src="js/hero3d.js"></script>
730733
<script src="js/script.js"></script>
731734
</body>
732735
</html>

js/hero3d.js

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// ===================================
2+
// Hero 3D Background — Three.js
3+
// Animated wireframe mathematical figures
4+
// ===================================
5+
6+
(function () {
7+
'use strict';
8+
9+
// Bail out if reduced motion is preferred
10+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
11+
12+
// Bail out if Three.js didn't load
13+
if (typeof THREE === 'undefined') return;
14+
15+
const canvas = document.getElementById('hero-3d-canvas');
16+
if (!canvas) return;
17+
18+
const heroSection = canvas.closest('.hero');
19+
if (!heroSection) return;
20+
21+
// --- Configuration ---
22+
const isMobile = window.innerWidth < 768;
23+
const COLORS = [0x667eea, 0x4facfe, 0x00f2fe, 0x43e97b, 0x38f9d7, 0x764ba2];
24+
25+
const SHAPE_DEFS = [
26+
{ geo: () => new THREE.TorusGeometry(1.2, 0.4, 16, 50), color: 0x667eea, pos: [-5, 2, -3] },
27+
{ geo: () => new THREE.IcosahedronGeometry(1.0, 0), color: 0x4facfe, pos: [ 5.5, 1.5, -2] },
28+
{ geo: () => new THREE.DodecahedronGeometry(0.9, 0), color: 0x43e97b, pos: [-3, -2, -1] },
29+
{ geo: () => new THREE.OctahedronGeometry(1.0, 0), color: 0x00f2fe, pos: [ 6, -1.5, -4] },
30+
{ geo: () => new THREE.TorusKnotGeometry(0.8, 0.25, 100, 16), color: 0x764ba2, pos: [-6.5, 0, -2] },
31+
{ geo: () => new THREE.IcosahedronGeometry(1.1, 1), color: 0x38f9d7, pos: [ 3, 3, -5] },
32+
{ geo: () => new THREE.TetrahedronGeometry(1.0, 0), color: 0x667eea, pos: [ 0, -3, -2] },
33+
{ geo: () => new THREE.TorusGeometry(1.0, 0.02, 8, 80), color: 0x4facfe, pos: [ 7, 3, -3] }
34+
];
35+
36+
const shapeCount = isMobile ? 5 : SHAPE_DEFS.length;
37+
const particleCount = isMobile ? 100 : 300;
38+
39+
// --- Renderer ---
40+
let renderer;
41+
try {
42+
renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true });
43+
} catch (_) {
44+
return; // WebGL not available — hero looks fine with just CSS
45+
}
46+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
47+
renderer.setSize(heroSection.offsetWidth, heroSection.offsetHeight);
48+
49+
// --- Scene & Camera ---
50+
const scene = new THREE.Scene();
51+
const aspect = heroSection.offsetWidth / heroSection.offsetHeight;
52+
const camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 100);
53+
camera.position.set(0, 0, 8);
54+
55+
// --- Build shapes ---
56+
const shapeGroups = [];
57+
58+
for (let i = 0; i < shapeCount; i++) {
59+
const def = SHAPE_DEFS[i];
60+
const geometry = def.geo();
61+
const color = new THREE.Color(def.color);
62+
const group = new THREE.Group();
63+
64+
// 1. Wireframe mesh
65+
const wireMat = new THREE.MeshBasicMaterial({
66+
color: color,
67+
wireframe: true,
68+
transparent: true,
69+
opacity: 0.55
70+
});
71+
group.add(new THREE.Mesh(geometry, wireMat));
72+
73+
// 2. Inner glow fill
74+
const fillMat = new THREE.MeshBasicMaterial({
75+
color: color,
76+
transparent: true,
77+
opacity: 0.07
78+
});
79+
group.add(new THREE.Mesh(geometry, fillMat));
80+
81+
// 3. Outer glow shell (BackSide, scaled up)
82+
const glowMat = new THREE.MeshBasicMaterial({
83+
color: color,
84+
transparent: true,
85+
opacity: 0.03,
86+
side: THREE.BackSide
87+
});
88+
const glowMesh = new THREE.Mesh(geometry, glowMat);
89+
glowMesh.scale.setScalar(1.2);
90+
group.add(glowMesh);
91+
92+
group.position.set(def.pos[0], def.pos[1], def.pos[2]);
93+
94+
// Store per-shape animation parameters
95+
group.userData = {
96+
rotSpeed: {
97+
x: (Math.random() - 0.5) * 0.01,
98+
y: (Math.random() - 0.5) * 0.01,
99+
z: (Math.random() - 0.5) * 0.005
100+
},
101+
floatOffset: Math.random() * Math.PI * 2,
102+
floatAmp: 0.3 + Math.random() * 0.4,
103+
floatFreq: 0.3 + Math.random() * 0.3,
104+
baseY: def.pos[1]
105+
};
106+
107+
scene.add(group);
108+
shapeGroups.push(group);
109+
}
110+
111+
// --- Particle field ---
112+
const particleGeo = new THREE.BufferGeometry();
113+
const positions = new Float32Array(particleCount * 3);
114+
const colors = new Float32Array(particleCount * 3);
115+
116+
for (let i = 0; i < particleCount; i++) {
117+
positions[i * 3] = (Math.random() - 0.5) * 24;
118+
positions[i * 3 + 1] = (Math.random() - 0.5) * 16;
119+
positions[i * 3 + 2] = (Math.random() - 0.5) * 12 - 4;
120+
121+
const c = new THREE.Color(COLORS[Math.floor(Math.random() * COLORS.length)]);
122+
colors[i * 3] = c.r;
123+
colors[i * 3 + 1] = c.g;
124+
colors[i * 3 + 2] = c.b;
125+
}
126+
127+
particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
128+
particleGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
129+
130+
const particleMat = new THREE.PointsMaterial({
131+
size: 0.04,
132+
vertexColors: true,
133+
transparent: true,
134+
opacity: 0.7,
135+
blending: THREE.AdditiveBlending,
136+
depthWrite: false
137+
});
138+
139+
const particles = new THREE.Points(particleGeo, particleMat);
140+
scene.add(particles);
141+
142+
// --- Mouse tracking ---
143+
const mouse = { x: 0, y: 0 };
144+
const target = { x: 0, y: 0 };
145+
146+
function onMouseMove(e) {
147+
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
148+
mouse.y = (e.clientY / window.innerHeight) * 2 - 1;
149+
}
150+
window.addEventListener('mousemove', onMouseMove, { passive: true });
151+
152+
// --- Visibility (IntersectionObserver) ---
153+
let isVisible = true;
154+
155+
if ('IntersectionObserver' in window) {
156+
const visObs = new IntersectionObserver(
157+
(entries) => { isVisible = entries[0].isIntersecting; },
158+
{ threshold: 0 }
159+
);
160+
visObs.observe(heroSection);
161+
}
162+
163+
// --- Animation loop ---
164+
const clock = new THREE.Clock();
165+
166+
function animate() {
167+
requestAnimationFrame(animate);
168+
if (!isVisible) return;
169+
170+
const t = clock.getElapsedTime();
171+
172+
// Smooth mouse lerp for parallax
173+
target.x += (mouse.x - target.x) * 0.05;
174+
target.y += (mouse.y - target.y) * 0.05;
175+
camera.position.x = target.x * 0.8;
176+
camera.position.y = -target.y * 0.5;
177+
camera.lookAt(0, 0, 0);
178+
179+
// Animate shapes
180+
for (const grp of shapeGroups) {
181+
const u = grp.userData;
182+
grp.rotation.x += u.rotSpeed.x;
183+
grp.rotation.y += u.rotSpeed.y;
184+
grp.rotation.z += u.rotSpeed.z;
185+
grp.position.y = u.baseY + Math.sin(t * u.floatFreq + u.floatOffset) * u.floatAmp;
186+
}
187+
188+
// Slowly rotate particles
189+
particles.rotation.y = t * 0.02;
190+
particles.rotation.x = t * 0.01;
191+
192+
renderer.render(scene, camera);
193+
}
194+
195+
animate();
196+
197+
// --- Resize handler ---
198+
function onResize() {
199+
const w = heroSection.offsetWidth;
200+
const h = heroSection.offsetHeight;
201+
renderer.setSize(w, h);
202+
camera.aspect = w / h;
203+
camera.updateProjectionMatrix();
204+
}
205+
206+
window.addEventListener('resize', onResize, { passive: true });
207+
})();

js/script.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -544,9 +544,6 @@ document.addEventListener('DOMContentLoaded', () => {
544544
// Add loaded class to body for CSS animations
545545
document.body.classList.add('loaded');
546546

547-
// Create floating shapes
548-
createFloatingShapes();
549-
550547
// Update active nav on load
551548
updateActiveNav();
552549

0 commit comments

Comments
 (0)