diff --git a/src/App.tsx b/src/App.tsx index 535ef29..88d6325 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import ThreeViewport from './ThreeViewport' +import ContourBackground from './ContourBackground' import { buildRackScene, buildClusterScene, buildNasScene } from './scenes' const EMAIL = '0pietroquintavalle0@gmail.com' @@ -206,34 +207,38 @@ export default function App() {
{/* HERO */}
-
- Monte Carasso, CH • Permit C - / - Bare-Metal & Linux Systems -
+ -

Bridging bare-metal hardware craft with low-level Linux systems.

- -

- Systems technician with 6+ years assembling, repairing, and bench-testing desktop, laptop, and server hardware, backed by 5+ years administering Linux environments. Taking physical infrastructure from component-level triage to production reliability. -

- -
- {EMAIL} -
- -
-
- 25+ - Workstations Assembled +
+
+ Monte Carasso, CH • Permit C + / + Bare-Metal & Linux Systems
-
- 4 Nodes - 12+ Active Tenants + +

Bridging bare-metal hardware craft with low-level Linux systems.

+ +

+ Systems technician with 6+ years assembling, repairing, and bench-testing desktop, laptop, and server hardware, backed by 5+ years administering Linux environments. Taking physical infrastructure from component-level triage to production reliability. +

+ + -
- <30m - Ansible Disaster RTO + +
+
+ 25+ + Workstations Assembled +
+
+ 4 Nodes + 12+ Active Tenants +
+
+ <30m + Ansible Disaster RTO +
diff --git a/src/ContourBackground.tsx b/src/ContourBackground.tsx new file mode 100644 index 0000000..f356327 --- /dev/null +++ b/src/ContourBackground.tsx @@ -0,0 +1,92 @@ +import { useEffect, useRef } from 'react' +import * as THREE from 'three' + +const INK = 0x121212 + +export default function ContourBackground({ className }: { className?: string }) { + const mountRef = useRef(null) + + useEffect(() => { + const mount = mountRef.current + if (!mount) return undefined + + const width = mount.clientWidth || 800 + const height = mount.clientHeight || 600 + + const scene = new THREE.Scene() + const frustum = 9 + let aspect = width / height + const camera = new THREE.OrthographicCamera(-(frustum * aspect) / 2, (frustum * aspect) / 2, frustum / 2, -frustum / 2, 0.1, 100) + camera.position.set(6, 4, 10) + camera.lookAt(0, 0, 0) + + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }) + renderer.setSize(width, height) + renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.75)) + mount.appendChild(renderer.domElement) + + const group = new THREE.Group() + const geo = new THREE.PlaneGeometry(8, 8, 22, 22) + geo.rotateX(-Math.PI / 2) + const mat = new THREE.MeshBasicMaterial({ color: INK, wireframe: true, transparent: true, opacity: 0.16 }) + group.add(new THREE.Mesh(geo, mat)) + group.position.set(-0.9, 1.6, 0) + scene.add(group) + + const posAttr = geo.attributes.position + const base = Float32Array.from(posAttr.array) + + // pointermove on window: mount itself is pointer-events:none so it never sees events directly + const mouse = { x: 0, y: 0 } + const onMove = (e: PointerEvent) => { + const rect = mount.getBoundingClientRect() + mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1 + mouse.y = ((e.clientY - rect.top) / rect.height) * 2 - 1 + } + window.addEventListener('pointermove', onMove) + + const ro = new ResizeObserver(() => { + const w = mount.clientWidth + const h = mount.clientHeight + if (!w || !h) return + aspect = w / h + camera.left = -(frustum * aspect) / 2 + camera.right = (frustum * aspect) / 2 + camera.top = frustum / 2 + camera.bottom = -frustum / 2 + camera.updateProjectionMatrix() + renderer.setSize(w, h) + }) + ro.observe(mount) + + let raf = 0 + const timer = new THREE.Timer() + const tick = () => { + timer.update() + const t = timer.getElapsed() + group.rotation.y = mouse.x * 0.15 + group.rotation.x = mouse.y * 0.06 + for (let i = 0; i < posAttr.count; i++) { + const x = base[i * 3] + const z = base[i * 3 + 2] + posAttr.setY(i, Math.sin(x * 0.55 + t * 0.5) * 0.28 + Math.cos(z * 0.55 + t * 0.35) * 0.28) + } + posAttr.needsUpdate = true + renderer.render(scene, camera) + raf = requestAnimationFrame(tick) + } + tick() + + return () => { + cancelAnimationFrame(raf) + ro.disconnect() + window.removeEventListener('pointermove', onMove) + renderer.dispose() + geo.dispose() + mat.dispose() + if (mount.contains(renderer.domElement)) mount.removeChild(renderer.domElement) + } + }, []) + + return
+} diff --git a/src/index.css b/src/index.css index 97e189a..fa72a26 100644 --- a/src/index.css +++ b/src/index.css @@ -61,7 +61,9 @@ body{ .section-link:hover{color:var(--accent)} /* ---------- HERO ---------- */ -.hero{max-width:820px;display:flex;flex-direction:column;gap:32px} +.hero{position:relative;max-width:820px} +.hero-canvas{z-index:0} +.hero-content{position:relative;z-index:1;display:flex;flex-direction:column;gap:32px} .hero-badges{display:flex;align-items:center;gap:12px;flex-wrap:wrap} .badge-secondary,.badge-accent{font-family:var(--f-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase} .badge-secondary{color:var(--secondary)}