contour grid

This commit is contained in:
2026-09-15 22:49:14 +02:00
parent f400f63321
commit 6b139c9901
3 changed files with 125 additions and 26 deletions
+30 -25
View File
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import ThreeViewport from './ThreeViewport' import ThreeViewport from './ThreeViewport'
import ContourBackground from './ContourBackground'
import { buildRackScene, buildClusterScene, buildNasScene } from './scenes' import { buildRackScene, buildClusterScene, buildNasScene } from './scenes'
const EMAIL = '0pietroquintavalle0@gmail.com' const EMAIL = '0pietroquintavalle0@gmail.com'
@@ -206,34 +207,38 @@ export default function App() {
<main className="site-main"> <main className="site-main">
{/* HERO */} {/* HERO */}
<section className="hero"> <section className="hero">
<div className="hero-badges"> <ContourBackground className="hero-canvas" />
<span className="badge-secondary">Monte Carasso, CH &bull; Permit C</span>
<span className="badge-sep">/</span>
<span className="badge-accent">Bare-Metal &amp; Linux Systems</span>
</div>
<h1 className="hero-title">Bridging bare-metal hardware craft with low-level Linux systems.</h1> <div className="hero-content">
<div className="hero-badges">
<p className="hero-lead"> <span className="badge-secondary">Monte Carasso, CH &bull; Permit C</span>
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. <span className="badge-sep">/</span>
</p> <span className="badge-accent">Bare-Metal &amp; Linux Systems</span>
<div className="hero-ctas">
<a href={`mailto:${EMAIL}`} className="btn btn-outline">{EMAIL}</a>
</div>
<div className="hero-metrics">
<div>
<span className="metric-val">25+</span>
<span className="metric-lbl">Workstations Assembled</span>
</div> </div>
<div>
<span className="metric-val">4 Nodes</span> <h1 className="hero-title">Bridging bare-metal hardware craft with low-level Linux systems.</h1>
<span className="metric-lbl">12+ Active Tenants</span>
<p className="hero-lead">
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.
</p>
<div className="hero-ctas">
<a href={`mailto:${EMAIL}`} className="btn btn-outline">{EMAIL}</a>
</div> </div>
<div>
<span className="metric-val">&lt;30m</span> <div className="hero-metrics">
<span className="metric-lbl">Ansible Disaster RTO</span> <div>
<span className="metric-val">25+</span>
<span className="metric-lbl">Workstations Assembled</span>
</div>
<div>
<span className="metric-val">4 Nodes</span>
<span className="metric-lbl">12+ Active Tenants</span>
</div>
<div>
<span className="metric-val">&lt;30m</span>
<span className="metric-lbl">Ansible Disaster RTO</span>
</div>
</div> </div>
</div> </div>
</section> </section>
+92
View File
@@ -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<HTMLDivElement>(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 <div ref={mountRef} className={className} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }} />
}
+3 -1
View File
@@ -61,7 +61,9 @@ body{
.section-link:hover{color:var(--accent)} .section-link:hover{color:var(--accent)}
/* ---------- HERO ---------- */ /* ---------- 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} .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,.badge-accent{font-family:var(--f-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase}
.badge-secondary{color:var(--secondary)} .badge-secondary{color:var(--secondary)}