Claudio cooked

This commit is contained in:
2026-09-15 23:36:44 +02:00
parent 6b139c9901
commit 6cbe8ae739
15 changed files with 1680 additions and 915 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"hash": "c1692715",
"configHash": "46535a3e",
"lockfileHash": "e3b0c442",
"browserHash": "7d791f90",
"optimized": {},
"chunks": {}
}
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+8 -9
View File
@@ -1,21 +1,20 @@
<!doctype html> <!doctype html>
<html lang="en" style="color-scheme: light"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#FAF9F6" /> <title>Pietro Quintavalle — Bare-metal systems and Linux infrastructure</title>
<title>Pietro Quintavalle — Bare-Metal Systems & Linux Infrastructure</title> <meta name="description" content="Bare-metal deployments, server rack integration and low-level Linux systems diagnostics by Pietro Quintavalle, Ticino, Switzerland." />
<meta name="description" content="Bare-metal deployments, server rack integration, and low-level Linux systems diagnostics by Pietro Quintavalle." />
<meta name="keywords" content="Pietro Quintavalle, Bare-Metal Systems, Linux Infrastructure, Server Hardware, Systems Technician, Switzerland" />
<meta name="author" content="Pietro Quintavalle" /> <meta name="author" content="Pietro Quintavalle" />
<meta property="og:title" content="Pietro Quintavalle — Bare-Metal Systems & Linux Infrastructure" /> <meta name="theme-color" content="#eef0f3" />
<meta property="og:description" content="Bare-metal deployments, server rack integration, and low-level Linux systems diagnostics by Pietro Quintavalle." /> <meta property="og:title" content="Pietro Quintavalle — Bare-metal systems and Linux infrastructure" />
<meta property="og:description" content="Bare-metal deployments, server rack integration and low-level Linux systems diagnostics." />
<meta property="og:site_name" content="Pietro Quintavalle" /> <meta property="og:site_name" content="Pietro Quintavalle" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23141821'/%3E%3Ctext x='32' y='43' text-anchor='middle' font-family='Arial Black,Arial' font-weight='900' font-size='30' fill='%23eef0f3'%3EPQ%3C/text%3E%3C/svg%3E" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Syne:wght@500;700;800&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+136 -350
View File
@@ -1,385 +1,171 @@
import type { ReactNode } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import ThreeViewport from './ThreeViewport' import ParticleScene, { SHAPE_NAMES } from './scene/ParticleScene'
import ContourBackground from './ContourBackground' import { usePageMotion } from './usePageMotion'
import { buildRackScene, buildClusterScene, buildNasScene } from './scenes' import * as C from './content'
const EMAIL = '0pietroquintavalle0@gmail.com' const KEYFRAMES = ['.hero', '#work', '#skills', '#contact']
const GITHUB = 'https://github.com/Quinta0'
const LINKEDIN = 'https://www.linkedin.com/in/pietro-quintavalle-996b96267/'
type CaseStudy = { function Name({ lines }: { lines: string[] }) {
label: string const minLen = Math.min(...lines.map(l => l.length))
title: string return (
desc: string <h1 className="name" id="name" aria-label={lines.join(' ')}>
tags: string[] {lines.map((ln, li) => (
tint: string <span className="ln" key={ln} aria-hidden="true" style={{ '--s': Math.max(.55, minLen / ln.length) } as React.CSSProperties}>
headerL: string {[...ln].map((ch, i) => <span className="ch" key={i} style={{ transitionDelay: `${0.25 + li * 0.35 + i * 0.04}s` }}>{ch}</span>)}
headerR: string </span>
footerL: string ))}
footerR: string </h1>
diagram: ReactNode )
interactive?: boolean
} }
const cases: CaseStudy[] = [ function Marquee({ items, reverse }: { items: string[]; reverse?: boolean }) {
{ const row = [...items, ...items]
label: '01 / Turnkey Rack Assembly', return <div className={`mq rv${reverse ? ' rev' : ''}`} aria-hidden="true"><div>{row.map((s, i) => <span key={i}>{s}</span>)}</div></div>
title: 'Turnkey Server Cabinet & ZFS Storage Pool', }
desc: 'Procured and physically assembled a turnkey 19" server cabinet, structured patch panels, and storage chassis for a private client. Dressed and labeled all cable runs, deployed TrueNAS with ZFS RAID 10, automated granular user ACLs, and configured offsite snapshot synchronization for a <30 min recovery target.',
tags: ['19" EIA-310 Cabinet', 'ZFS RAID 10', 'Structured Cabling'],
tint: '#EFECE6',
headerL: 'RACK_CHASSIS // EIA-310',
headerR: '• DRAG TO ROTATE',
footerL: 'STRUCTURED HARNESS',
footerR: 'SUB-30M RTO SYNC',
diagram: <ThreeViewport build={buildRackScene} className="case-canvas" />,
interactive: true,
},
{
label: '02 / Startup Infrastructure • DEC Energy',
title: 'Dual-Node Bare-Metal Kubernetes Cluster',
desc: 'Serving as freelance hardware partner for USI startup DEC Energy. Sourced parts, built, and deployed an on-premise 2-node cluster to host critical LLM inference, accounting, and Gitea source control. Tuned fan curves and conducted thermal bench stress testing to ensure quiet open-office operation under full tensor load.',
tags: ['Bare-Metal K8s', 'LLM Inference', 'Thermal Profiling'],
tint: '#EAE8E1',
headerL: 'DUAL_NODE // TOPOLOGY',
headerR: '• DRAG TO ROTATE',
footerL: 'ON-PREMISES INGESTION',
footerR: 'USI STARTUP',
diagram: <ThreeViewport build={buildClusterScene} className="case-canvas" />,
interactive: true,
},
{
label: '03 / Protocol Reverse Engineering',
title: 'libratbag USB HID Kernel Driver',
desc: "Sniffed and reverse-engineered an undocumented vendor's raw USB HID packets using Wireshark. Decoded byte streams for DPI profiles, polling rates, and on-board memory layouts, writing a complete C device driver merged directly into upstream libratbag.",
tags: ['Upstream PR #1873', 'Wireshark Sniffing', 'C Device Driver'],
tint: '#F2EFEB',
headerL: 'USB HID DECODER',
headerR: 'KERNEL C INTERFACE',
footerL: 'RAW BYTE INSPECTION',
footerR: 'PR #1873 MERGED',
diagram: (
<svg viewBox="0 0 460 210" className="case-svg" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="30" y1="40" x2="430" y2="40" stroke="var(--ink)" strokeWidth="1" />
<path d="M 60 40 L 90 85" stroke="var(--accent)" strokeWidth="1.2" strokeDasharray="2 2" />
<path d="M 180 40 L 150 85" stroke="var(--ink)" strokeWidth="1" strokeDasharray="2 2" />
<rect x="50" y="85" width="360" height="50" rx="3" stroke="var(--ink)" strokeWidth="1.5" fill="var(--canvas)" />
<rect x="50" y="85" width="55" height="50" fill="var(--border)" stroke="var(--ink)" strokeWidth="1" />
<text x="77" y="110" fill="var(--ink)" fontFamily="Space Mono" fontSize="9" textAnchor="middle" fontWeight="bold">0x08</text>
<text x="77" y="125" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7" textAnchor="middle">REPORT</text>
<rect x="105" y="85" width="55" height="50" fill="#FFFFFF" stroke="var(--ink)" strokeWidth="1" />
<text x="132" y="110" fill="var(--ink)" fontFamily="Space Mono" fontSize="9" textAnchor="middle" fontWeight="bold">0x14</text>
<text x="132" y="125" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7" textAnchor="middle">CMD</text>
<rect x="160" y="85" width="65" height="50" fill="#FFFFFF" stroke="var(--ink)" strokeWidth="1" />
<text x="192" y="110" fill="var(--ink)" fontFamily="Space Mono" fontSize="9" textAnchor="middle">0x01 02</text>
<text x="192" y="125" fill="var(--secondary)" fontFamily="Space Mono" fontSize="7" textAnchor="middle">PROF_2</text>
<rect x="225" y="85" width="85" height="50" fill="#FFFFFF" stroke="var(--accent)" strokeWidth="1.5" />
<text x="267" y="110" fill="var(--accent)" fontFamily="Space Mono" fontSize="10" textAnchor="middle" fontWeight="bold">0x0640</text>
<text x="267" y="125" fill="var(--accent)" fontFamily="Space Mono" fontSize="7" textAnchor="middle">1600 DPI</text>
<rect x="310" y="85" width="100" height="50" fill="var(--canvas)" stroke="var(--ink)" strokeWidth="1" />
<text x="360" y="113" fill="var(--secondary)" fontFamily="Space Mono" fontSize="8" textAnchor="middle">00 00 ...</text>
<line x1="267" y1="135" x2="267" y2="165" stroke="var(--accent)" strokeWidth="1" />
<rect x="207" y="165" width="120" height="22" rx="2" fill="var(--ink)" />
<text x="267" y="179" fill="var(--canvas)" fontFamily="Space Mono" fontSize="8" textAnchor="middle">UPSTREAM KERNEL C</text>
</svg>
),
},
{
label: '04 / Self-Directed Production',
title: 'Homelab as Code: 4-Node Multi-User Fabric',
desc: 'Operating 4 bare-metal production hosts (UNRAID, Proxmox VE 8, TrueNAS, Docker, LXC) actively serving 12+ daily active users without managed third-party hosting. Zero exposed WAN ports enforced via Cloudflare Tunnels, WireGuard, SSO/2FA, and Fail2Ban. Provisioned idempotently via Ansible with <30m bare-metal disaster recovery.',
tags: ['Zero-Trust WAN', 'Ansible IaC', 'Proxmox / UNRAID'],
tint: '#EDEAE3',
headerL: 'TOPOLOGY MAP',
headerR: '• HOVER BAYS',
footerL: 'ANSIBLE PROVISIONED',
footerR: 'DAILY PRODUCTION',
diagram: <ThreeViewport build={buildNasScene} className="case-canvas" />,
interactive: true,
},
]
const ossCards = [
{
kicker: 'RUNTIME PIPELINES',
title: 'Vysp3r/ProtonPlus • steam-for-linux',
meta: 'Issue #1232',
desc: 'Diagnosed a deployment pipeline fault in the Steam Linux Runtime installation sequence where unwritten manifest files caused execution crashes. Formulated a console depot recovery workaround and contributed code upstream to automate runtime integrity validation.',
tags: ['#SteamRuntime', '#Proton', '#LinuxGaming'],
},
{
kicker: 'COMPATIBILITY TOOLING',
title: 'Recol/DLSS-Updater',
meta: 'PR #283, #252, #256',
desc: 'Engineered dynamic multi-vendor library detection (NVIDIA DLSS, AMD FSR, Intel XeSS). Replaced brittle static mapping tables with bounded manifest parsing to maintain driver compatibility across DLL upgrades; resolved UI state desynchronization bugs.',
tags: ['#Python', '#Qt', '#DriverInterfacing'],
},
{
kicker: 'AUTOMATION UTILITY',
title: 'Quinta0/ObsidianSetup',
meta: '26 Stars on GitHub',
desc: 'Authored an idempotent cross-platform provisioning utility for automated workspace configuration, plugin synchronization, and environment deployment.',
tags: ['#PowerShell', '#EnvironmentProvisioning'],
},
{
kicker: 'TEAM SYSTEMS ARCHITECTURE',
title: 'City Simulator • USI Systems Project',
meta: 'Agile / Scrum',
desc: 'Coordinated a 12-person development team using Agile/Scrum to build a full-stack city simulation. Owned backend architecture in Spring Boot while integrating JavaScript/HTML/CSS frontend components.',
tags: ['#Java', '#SpringBoot', '#ScrumTeam'],
},
]
const experience = [
{
title: 'Infrastructure & Hardware Consultant (Freelance)',
period: '2026',
org: 'DEC Energy (USI Startup) • Ticino, Switzerland',
desc: 'Trusted technical partner advising on workstation hardware (custom PCs, multi-monitor ergonomics, laptop fleet selection). Designed, procured, and deployed an on-premise 2-node bare-metal Kubernetes cluster for local LLM inference, accounting, and Git source control.',
},
{
title: 'IT Support & Hardware Technician (Freelance)',
period: 'Jan 2020 – Present',
org: 'Private Clients • Ticino, Switzerland',
desc: 'Assembled, configured, and repaired 25+ custom workstations and laptops: component sourcing, OS imaging (Windows/Linux), driver debugging, and remote/onsite diagnostics. Maintained comprehensive hardware logs to eliminate recurring component failures.',
},
{
title: 'Bare-Metal Infrastructure Operator',
period: 'Jan 2020 – Present',
org: 'Self-Directed Production Homelab',
desc: 'Engineered and maintained 4 bare-metal production nodes serving 12+ daily active users without managed external hosting. Deployed turnkey server rack cabinets, configured ZFS RAID 10 arrays, and implemented zero exposed WAN ports via Cloudflare Tunnels and WireGuard meshes.',
},
]
const skillColumns = [
{
heading: 'HARDWARE & LAB',
items: ['Desktop / Server Assembly', 'Component Fault Triage', 'Thermal Bench Stressing', 'Firmware / BIOS Flashing', 'Soldering & Harness Wiring', 'Rack Mounting & Cabling'],
},
{
heading: 'SYSTEMS & STORAGE',
items: ['Linux Daily (5+ yrs)', 'Debian, Arch, Ubuntu, NixOS', 'Proxmox VE 8, KVM, LXC', 'TrueNAS • ZFS RAID 10', 'UNRAID • Docker / K8s', 'Windows Server (AD, GPO, DNS)'],
},
{
heading: 'NETWORK & TELEMETRY',
items: ['VLANs • Ubiquiti UniFi', 'Zero-Trust Tunnels', 'WireGuard / Tailscale', 'UFW, iptables, Fail2Ban', 'Ansible IaC • Bash', 'Grafana, Prometheus, Kuma'],
},
]
const education = [
{
date: '2022 – 2024 • USI Lugano',
title: 'Foundational Higher Education in Computer Science',
desc: 'Completed 2 full years of core CS curricula: Algorithms & data structures, OOP (Java, C++), discrete logic, and software architecture. Concluded formal coursework to transition full-time into hands-on bare-metal systems and datacenter infrastructure.',
},
{
date: '2018 – 2022 • SCC Bellinzona',
title: 'Maturità Cantonale & Federal VET Diploma (AFC)',
desc: 'Completed Swiss dual-track qualification (Maturità: 4.5/6, AFC: 5/6).',
},
]
const certifications = ['Google IT Support Professional', 'Google Data Analytics Professional', 'Microsoft Foundations of IT Systems']
const languages = [
{ name: 'Italian', level: 'Native (C2)' },
{ name: 'English', level: 'Full Professional (C2)' },
{ name: 'German', level: 'Working Proficiency (B1–B2)' },
{ name: 'Spanish / Norwegian', level: 'Basic (A1)' },
]
export default function App() { export default function App() {
usePageMotion()
const [label, setLabel] = useState(SHAPE_NAMES[0])
const [swap, setSwap] = useState(false)
const barRef = useRef<HTMLDivElement>(null)
// label swaps with a short fade
const pending = useRef<number | null>(null)
const onLabel = useCallback((text: string) => {
setSwap(true)
if (pending.current) clearTimeout(pending.current)
pending.current = window.setTimeout(() => { setLabel(text); setSwap(false) }, 400)
}, [])
useEffect(() => () => { if (pending.current) clearTimeout(pending.current) }, [])
const onProgress = useCallback((f: number) => { if (barRef.current) barRef.current.style.transform = `scaleX(${f.toFixed(4)})` }, [])
return ( return (
<> <>
<header className="site-header"> <ParticleScene keyframes={KEYFRAMES} onLabel={onLabel} onProgress={onProgress} />
<div className="header-bar"> <div className="grain" aria-hidden="true" />
<nav className="main-nav"> <div className="vignette" aria-hidden="true" />
<a href="#works">WORKS</a> <div className="cur" aria-hidden="true" /><div className="cur-ring" aria-hidden="true" />
<a href="#oss">DIAGNOSTICS &amp; OSS</a> <div className="progress" ref={barRef} aria-hidden="true" />
<a href="#background">EXPERIENCE</a>
<a href={`mailto:${EMAIL}`} className="nav-contact">CONTACT</a>
</nav>
</div>
</header>
<main className="site-main"> <nav>
{/* HERO */} <a className="brand" href="#top">PQ</a>
<ul>
<li><a href="#work">Work</a></li>
<li><a href="#oss">Open source</a></li>
<li><a href="#projects">Projects</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
<div className="shape-label" aria-hidden="true"><i /><span className={swap ? 'swap' : ''}>{label}</span></div>
<main id="top">
<section className="hero"> <section className="hero">
<ContourBackground className="hero-canvas" /> <div className="wrap">
<p className="intro rv">Monte Carasso, Ticino. Six years at the bench, five years on Linux, four servers in production. Looking for datacenter operations, hardware integration or Linux systems support, on-site or hybrid.</p>
<div className="hero-content"> <Name lines={['Pietro', 'Quintavalle']} />
<div className="hero-badges"> <div className="tag rv">
<span className="badge-secondary">Monte Carasso, CH &bull; Permit C</span> <p>I build infrastructure from the <em>solder joint</em> up. Component-level diagnostics, ZFS storage, zero-trust networks, and the occasional driver written from a packet dump.</p>
<span className="badge-sep">/</span>
<span className="badge-accent">Bare-Metal &amp; Linux Systems</span>
</div> </div>
<div className="facts rv">
<h1 className="hero-title">Bridging bare-metal hardware craft with low-level Linux systems.</h1> {C.facts.map(f => <div key={f.label}><b data-count={f.n} data-prefix={f.prefix}>0</b><small>{f.label}</small></div>)}
<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 className="hint rv"><b />Scroll. The particles follow.</div>
</div>
</section>
<div className="hero-metrics"> <section id="qualities">
<div> <div className="wrap">
<span className="metric-val">25+</span> <h2 className="rv">What you get when you hire me</h2>
<span className="metric-lbl">Workstations Assembled</span> <div className="qual rv">
</div> {C.qualities.map(q => <article key={q.title}><h3>{q.title}</h3><p>{q.text}</p></article>)}
<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>
{/* WORKS */} <section id="work">
<section id="works" className="works"> <div className="wrap">
<div className="section-head"> <h2 className="rv">Experience</h2>
<span className="section-kicker">Physical Deployments &amp; Infrastructure</span> <div className="tl rv" id="tl"><div className="beam" />
<span className="section-kicker">[ 01 &mdash; 04 ]</span> {C.jobs.map(j => (
</div> <div className="job" key={j.title} data-shape={j.shape} data-label={j.label}>
<time>{j.time}</time>
<div className="case-list"> <h3>{j.title}</h3>
{cases.map(c => ( <span className="org">{j.org}</span>
<article className="case" key={c.title}> <ul>{j.bullets.map(b => <li key={b}>{b}</li>)}</ul>
<div className="case-copy">
<span className="case-label">{c.label}</span>
<h2 className="case-title">{c.title}</h2>
<p className="case-desc">{c.desc}</p>
<div className="tag-row">
{c.tags.map(tag => <span className="tag-pill" key={tag}>{tag}</span>)}
</div>
</div>
<div className="case-diagram" style={{ background: c.tint }}>
<div className="diagram-card">
<div className="diagram-row diagram-header">
<span>{c.headerL}</span>
<span className={c.interactive ? 'diagram-interactive' : undefined}>{c.headerR}</span>
</div>
<div className="diagram-svg-wrap">{c.diagram}</div>
<div className="diagram-row diagram-footer">
<span>{c.footerL}</span>
<span>{c.footerR}</span>
</div>
</div>
</div>
</article>
))}
</div>
</section>
{/* OSS */}
<section id="oss" className="oss">
<div className="section-head">
<span className="section-kicker">Systems Diagnostics &amp; Open-Source</span>
<a href={GITHUB} target="_blank" rel="noopener" className="section-link">github.com/Quinta0 ↗</a>
</div>
<div className="oss-grid">
{ossCards.map(card => (
<div className="oss-card" key={card.title}>
<div className="oss-card-top">
<div>
<span className="oss-kicker">{card.kicker}</span>
<h3>{card.title}</h3>
</div>
<span className="oss-meta">{card.meta}</span>
</div>
<p>{card.desc}</p>
<div className="tag-row tag-row-mini">
{card.tags.map(tag => <span key={tag}>{tag}</span>)}
</div>
</div>
))}
</div>
</section>
{/* BACKGROUND */}
<section id="background" className="background">
<div className="bg-row bg-row-bordered">
<div className="bg-label">Professional Experience</div>
<div className="bg-content exp-list">
{experience.map(job => (
<div className="exp-item" key={job.title}>
<div className="exp-head">
<h3>{job.title}</h3>
<span className="exp-period">{job.period}</span>
</div>
<div className="exp-org">{job.org}</div>
<p>{job.desc}</p>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</section>
<div className="bg-row bg-row-bordered"> <section id="oss">
<div className="bg-label">Technical Capabilities</div> <div className="wrap">
<div className="bg-content skills-grid"> <h2 className="rv">Open source, close to the metal</h2>
{skillColumns.map(col => ( <p className="dim rv" style={{ marginBottom: 40 }}>Upstream contributions to projects that sit between hardware and the kernel. Hover a row and the sculpture becomes the work.</p>
<div className="skills-col" key={col.heading}> <div className="oss rv">
<span className="skills-heading">{col.heading}</span> {C.oss.map(o => (
<ul> <a href={o.href} target="_blank" rel="noopener" key={o.href} data-shape={o.shape} data-label={o.label}>
{col.items.map(item => <li key={item}>{item}</li>)} <div><span className="repo">{o.repo}</span><h3>{o.title}</h3><p className="what">{o.text}</p></div>
</ul> <span className="ref">{o.ref}</span>
</div> </a>
))} ))}
</div> </div>
</div> </div>
</section>
<div className="bg-row bg-row-bordered"> <section id="projects">
<div className="bg-label">Education &amp; Credentials</div> <div className="wrap">
<div className="bg-content edu-grid"> <h2 className="rv">Selected projects</h2>
<div className="edu-list"> <div className="proj rv">
{education.map(edu => ( {C.projects.map(p => (
<div key={edu.title}> <article key={p.title} data-tilt data-shape={p.shape} data-label={p.label}>
<span className="edu-date">{edu.date}</span> <h3>{p.title}{p.star && <> <span className="star">{p.star}</span></>}</h3>
<h4>{edu.title}</h4> <p>{p.text}</p>
<p>{edu.desc}</p> <div className="stack">{p.stack}</div>
</div> </article>
))} ))}
</div> </div>
</div>
</section>
<div className="creds-list"> <section id="skills">
<div> <div className="wrap"><h2 className="rv">Tools I reach for</h2></div>
<span className="creds-heading">CERTIFICATIONS</span> <Marquee items={C.toolsRowA} />
<ul> <Marquee items={C.toolsRowB} reverse />
{certifications.map(cert => <li key={cert}>&bull; {cert}</li>)} <div className="wrap">
</ul> <p className="skills-note rv">Also on the bench: Rust, C, C++, Python and SQL for diagnostics, and a self-directed track on local LLM deployment and quantisation on bare-metal hardware with llama.cpp and Claude Code.</p>
</div> </div>
<div> </section>
<span className="creds-heading">LANGUAGES</span>
<ul> <section id="education">
{languages.map(l => <li key={l.name}>&bull; <strong>{l.name}</strong>: {l.level}</li>)} <div className="wrap">
</ul> <h2 className="rv">Education and languages</h2>
</div> <div className="edu rv">
<dl>{C.education.map(e => <div key={e.title}><dt>{e.title}</dt><dd>{e.text}</dd></div>)}</dl>
<div>
<dl>
<div><dt>Certifications</dt><dd>{C.certifications}</dd></div>
<div><dt>Languages</dt><dd className="langs">{C.languages.map(([l, lv]) => <span key={l}><b>{l}</b> {lv}</span>)}</dd></div>
<div><dt>Status</dt><dd>Italian national, Swiss Permit C, born 2001.</dd></div>
</dl>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section id="contact" className="contact">
<div className="wrap">
<h2 className="rv">Need someone who can rack it, image it, and keep it up?</h2>
<a className="mail rv" href={`mailto:${C.EMAIL}`}>{C.EMAIL}</a>
<div className="links rv">
<a data-mag href={C.GITHUB} target="_blank" rel="noopener">GitHub</a>
<a data-mag href={C.LINKEDIN} target="_blank" rel="noopener">LinkedIn</a>
<a data-mag href={C.WHOAMI} target="_blank" rel="noopener">whoami</a>
</div>
</div>
</section>
</main> </main>
<footer><span>Pietro Quintavalle, Ticino, Switzerland</span><span>Built with Three.js, one canvas, fifteen thousand particles.</span></footer>
<footer className="site-footer">
<div>
<span className="footer-name">Pietro Quintavalle</span> &bull; Open for Datacenter &amp; Systems roles across Switzerland
</div>
<div className="footer-links">
<a href={GITHUB} target="_blank" rel="noopener">GitHub ↗</a>
<a href={LINKEDIN} target="_blank" rel="noopener">LinkedIn ↗</a>
</div>
</footer>
</> </>
) )
} }
-92
View File
@@ -1,92 +0,0 @@
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' }} />
}
-68
View File
@@ -1,68 +0,0 @@
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
export type ViewportHandle = {
update?: () => void
dispose?: () => void
}
export type BuildFn = (
scene: THREE.Scene,
camera: THREE.PerspectiveCamera,
controls: OrbitControls,
renderer: THREE.WebGLRenderer,
container: HTMLDivElement,
) => ViewportHandle | void
export default function ThreeViewport({ build, className }: { build: BuildFn; className?: string }) {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = containerRef.current
if (!container) return
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(38, container.clientWidth / container.clientHeight, 0.1, 100)
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setSize(container.clientWidth, container.clientHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
container.appendChild(renderer.domElement)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.enableZoom = false
controls.autoRotate = true
controls.autoRotateSpeed = 1
const handle = build(scene, camera, controls, renderer, container) ?? undefined
let frameId = 0
const animate = () => {
frameId = requestAnimationFrame(animate)
controls.update()
handle?.update?.()
renderer.render(scene, camera)
}
animate()
const onResize = () => {
camera.aspect = container.clientWidth / container.clientHeight
camera.updateProjectionMatrix()
renderer.setSize(container.clientWidth, container.clientHeight)
}
window.addEventListener('resize', onResize)
return () => {
cancelAnimationFrame(frameId)
window.removeEventListener('resize', onResize)
handle?.dispose?.()
controls.dispose()
renderer.dispose()
container.removeChild(renderer.domElement)
}
}, [build])
return <div ref={containerRef} className={className} />
}
+97
View File
@@ -0,0 +1,97 @@
// All page copy lives here so the layout stays clean
export const EMAIL = '0pietroquintavalle0@gmail.com'
export const PHONE = '+41 79 906 82 53'
export const GITHUB = 'https://github.com/Quinta0'
export const LINKEDIN = 'https://linkedin.com/in/pietro-quintavalle'
export const WHOAMI = 'https://quinta0.github.io/whoami'
export const facts = [
{ n: 6, label: 'years assembling and repairing hardware' },
{ n: 25, label: 'systems built for private clients' },
{ n: 12, label: 'active users on self-hosted infrastructure' },
{ n: 30, prefix: '<', label: 'minutes to rebuild a cluster from bare metal' },
]
export const qualities = [
{ title: 'Diagnoses at the component, not the ticket', text: 'Thermal bench-testing, firmware and BIOS flashing, soldering and harness wiring. When a node fails I find the part, not the symptom.' },
{ title: 'Treats every host as disposable', text: 'Ansible playbooks and Docker Compose rebuild the whole environment from a blank disk in under thirty minutes. Nothing important lives only in one place.' },
{ title: 'Zero exposed WAN ports, by default', text: 'Cloudflare Tunnels, WireGuard and Tailscale meshes, SSO with 2FA, host firewalls and intrusion detection on every node. Security is part of the build, not a later phase.' },
{ title: 'Reads the protocol when there is no documentation', text: 'Reverse-engineered a vendor USB HID protocol from packet captures and shipped a working Linux driver upstream. Undocumented hardware is a puzzle, not a blocker.' },
]
export type Shaped = { shape?: string; label?: string }
export const jobs: (Shaped & { time: string; title: string; org: string; bullets: string[] })[] = [
{
time: '2026 to now', title: 'Infrastructure and hardware consultant', org: 'DEC Energy, USI startup, freelance',
shape: 'cluster', label: 'DEC Energy: two nodes, one GPU, one HBA',
bullets: [
'Designed, procured and deployed an on-premise two-node Kubernetes cluster from parts to production: local LLM inference, web hosting, accounting and Gitea/GitLab source control.',
'Tuned fan curves and ran thermal bench stress tests so the nodes stay quiet in an open office under full inference load.',
"Specified and assembled the engineering team's workstations and remain on-call for support.",
],
},
{
time: 'January 2020 to now', title: 'IT support technician', org: 'Private clients across Ticino, freelance',
bullets: [
'Built, configured and repaired more than 25 desktop and laptop systems: sourcing, assembly, Windows and Linux deployment, driver debugging, ongoing maintenance.',
'Remote and on-site support for hardware failures, software conflicts and local networking faults, with repair logs kept to stop issues recurring.',
],
},
{
time: 'January 2020 to now', title: 'Infrastructure engineer, self-directed', org: 'Four bare-metal servers serving 12+ users',
shape: 'homelab', label: 'Homelab as code: nine bays, twelve users, no open ports',
bullets: [
'UNRAID, Proxmox VE 8, TrueNAS, Docker and LXC running production workloads without any managed hosting.',
'Delivered a turnkey rack for a client: cabinet, chassis, structured cabling, TrueNAS with ZFS RAID 10, per-user ACLs and automated offsite snapshots with under thirty minutes of recovery time.',
'Hardened every edge: no open WAN ports, SSO and 2FA, intrusion detection, host-level firewalls.',
],
},
]
export const oss: (Shaped & { href: string; repo: string; title: string; text: string; ref: string })[] = [
{
href: 'https://github.com/libratbag/libratbag/pull/1873', repo: 'libratbag/libratbag',
title: 'A complete USB HID device driver, written from a packet capture',
text: "Sniffed the vendor's raw HID reports in Wireshark, decoded the byte layout for DPI profiles, polling rates and on-board memory, then wrote a complete C driver that was merged upstream.",
ref: 'PR #1873', shape: 'hid', label: 'HID report: the bytes that became a driver',
},
{
href: 'https://github.com/ValveSoftware/steam-for-linux/issues/13248', repo: 'ValveSoftware/steam-for-linux, Vysp3r/ProtonPlus',
title: 'Runtime installation pipeline fault, diagnosed and fixed upstream',
text: 'Traced execution crashes in the Steam Linux Runtime to unwritten manifest files, documented a depot recovery workaround and contributed code to automate runtime integrity checks.',
ref: 'Issues #13248, #1232',
},
{
href: 'https://github.com/Recol/DLSS-Updater/pull/283', repo: 'Recol/DLSS-Updater',
title: 'Multi-vendor upscaler library detection',
text: 'Replaced brittle static tables with bounded manifest parsing so NVIDIA DLSS, AMD FSR and Intel XeSS libraries survive DLL upgrades; also fixed UI state desynchronisation.',
ref: 'PRs #283, #252, #256',
},
]
export const projects: (Shaped & { title: string; star?: string; text: string; stack: string })[] = [
{ title: 'Homelab as code', shape: 'homelab', label: 'Homelab as code: nine bays, twelve users, no open ports',
text: 'Four production hosts rebuilt from bare metal by Ansible in under thirty minutes. Twelve-plus daily users, no exposed WAN ports: Cloudflare Tunnels, WireGuard, SSO with 2FA and Fail2Ban on every node.',
stack: 'Ansible, Docker Compose, UNRAID, Grafana, Prometheus' },
{ title: 'Turnkey TrueNAS rack', shape: 'nas', label: 'Client rack: the cabinet that opens this page',
text: 'A 19-inch cabinet for a private client: patch panels, storage chassis and nine drive bays, every cable run dressed and labelled. TrueNAS on top with ZFS RAID 10, per-user ACLs and offsite snapshots for a sub-30-minute recovery target.',
stack: 'TrueNAS, ZFS, rack infrastructure, structured cabling' },
{ title: 'ObsidianSetup', star: '26 stars on GitHub',
text: 'Cross-platform provisioning utility for environment configuration and plugin management, adopted by other users.',
stack: 'PowerShell' },
{ title: 'City simulator, USI group project', shape: 'city', label: 'City simulator: twelve people, one Spring Boot backend',
text: 'Coordinated a twelve-person Scrum team building a full-stack city simulation. Owned the Spring Boot backend and its integration with the web frontend.',
stack: 'Java, Spring Boot, JavaScript, HTML, CSS' },
]
export const toolsRowA = ['Proxmox VE', 'TrueNAS', 'ZFS', 'UNRAID', 'Kubernetes', 'Docker', 'LXC', 'KVM', 'Ansible', 'GitHub Actions', 'Bash', 'PowerShell', 'Debian', 'Arch', 'NixOS', 'Windows Server', 'Active Directory']
export const toolsRowB = ['WireGuard', 'Tailscale', 'Cloudflare Tunnels', 'UniFi', 'VLANs', 'iptables', 'Fail2Ban', 'AdGuard Home', 'Grafana', 'Prometheus', 'Uptime Kuma', 'Spring Boot', 'TypeScript', 'llama.cpp', 'Rust', 'C']
export const education = [
{ title: 'BSc Economics, English stream', text: 'Università della Svizzera italiana, Lugano, since September 2024. Microeconomics, game theory, quantitative finance, statistics, financial modelling.' },
{ title: 'Computer science coursework, two full years', text: 'USI, 2022 to 2024. Algorithms and data structures, object-oriented systems in Java and C++, software architecture, discrete logic.' },
{ title: 'Maturità Cantonale and Federal VET Diploma, AFC Economics', text: 'Scuola Cantonale di Commercio, Bellinzona, 2018 to 2022. Maturità 4.5/6, AFC 5/6.' },
]
export const certifications = 'Google IT Support Professional Certificate. Google Data Analytics Professional Certificate. Microsoft Foundations of IT Systems, Networking and Data Protection.'
export const languages = [['Italian', 'native'], ['English', 'C2'], ['German', 'B1–B2'], ['Spanish', 'A1'], ['Norwegian', 'A1']]
+148 -201
View File
@@ -1,218 +1,165 @@
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
a:focus-visible,button:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
:root{ :root{
--canvas:#FAF9F6; --bg:#eef0f3; --bg-2:#e3e6eb; --fg:#141821; --muted:#5b6472;
--surface:#FFFFFF; --amber:#c86a00; --ice:#2f5fd6; --line:rgba(20,24,33,.14);
--ink:#121212; --display:'Syne',"Helvetica Neue",Arial,sans-serif;
--secondary:#6E6B65; --body:'IBM Plex Sans',"Segoe UI",system-ui,sans-serif;
--border:#E6E4DD;
--accent:#C1121F;
--container:1152px;
--pad:24px;
--f-sans:'Plus Jakarta Sans',sans-serif;
--f-mono:'Space Mono',monospace;
} }
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){ --bg:#eef0f3; --bg-2:#e3e6eb; --fg:#141821; --muted:#5b6472; } }
:root[data-theme="dark"]{ --bg:#eef0f3; --bg-2:#e3e6eb; --fg:#141821; --muted:#5b6472; }
:root[data-theme="light"]{ --bg:#eef0f3; --fg:#141821; }
html{scroll-behavior:smooth} *{box-sizing:border-box;margin:0;padding:0}
body{ html{scroll-behavior:auto}
background:var(--canvas); body{background:var(--bg);color:var(--fg);font-family:var(--body);font-size:17px;line-height:1.55;overflow-x:hidden;-webkit-font-smoothing:antialiased;cursor:none}
color:var(--ink); @media (pointer:coarse){ body{cursor:auto} }
font-family:var(--f-sans); a{color:inherit;text-decoration:none}
font-size:15px; ::selection{background:var(--amber);color:#fff}
line-height:1.5;
-webkit-font-smoothing:antialiased;
}
::selection{background:var(--ink);color:#fff}
/* ---------- HEADER ---------- */ #scene{position:fixed;inset:0;z-index:0;display:block}
.site-header{ .grain{position:fixed;inset:-100%;z-index:1;pointer-events:none;opacity:.05;
position:fixed;top:0;left:0;right:0;z-index:50; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background:rgba(250,249,246,.9); animation:grain 1.2s steps(6) infinite}
backdrop-filter:blur(10px); @keyframes grain{0%{transform:translate(0,0)}20%{transform:translate(-3%,2%)}40%{transform:translate(2%,-4%)}60%{transform:translate(-4%,-1%)}80%{transform:translate(3%,3%)}100%{transform:translate(0,0)}}
-webkit-backdrop-filter:blur(10px); .vignette{position:fixed;inset:0;z-index:1;pointer-events:none;background:radial-gradient(ellipse 80% 70% at 50% 45%,transparent 45%,rgba(238,240,243,.7) 100%)}
border-bottom:1px solid rgba(230,228,221,.6);
}
.header-bar{
max-width:var(--container);margin:0 auto;padding:0 var(--pad);
height:80px;display:flex;justify-content:space-between;align-items:center;
}
.logo{
display:flex;align-items:center;gap:8px;flex-shrink:0;white-space:nowrap;
font-size:13px;font-weight:500;letter-spacing:-.01em;
color:var(--ink);text-decoration:none;
}
.logo-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);flex-shrink:0}
.main-nav{
display:flex;align-items:center;gap:32px;font-family:var(--f-mono);font-size:12px;color:var(--secondary);
min-width:0;overflow-x:auto;scrollbar-width:none;
}
.main-nav::-webkit-scrollbar{display:none}
.main-nav a{color:inherit;text-decoration:none;transition:color .15s;white-space:nowrap}
.main-nav a:hover{color:var(--ink)}
.nav-contact{color:var(--ink);text-decoration:underline;text-decoration-color:var(--border)}
.nav-contact:hover{text-decoration-color:var(--ink)}
/* ---------- LAYOUT ---------- */ /* cursor */
.site-main{max-width:var(--container);margin:0 auto;padding:176px var(--pad) 128px;display:flex;flex-direction:column;gap:144px} .cur,.cur-ring{position:fixed;top:0;left:0;z-index:50;pointer-events:none;border-radius:50%;transform:translate(-50%,-50%);mix-blend-mode:difference}
.section-head{display:flex;justify-content:space-between;align-items:baseline;border-bottom:1px solid var(--border);padding-bottom:12px;margin-bottom:64px} .cur{width:6px;height:6px;background:#fff}
.section-kicker{font-family:var(--f-mono);font-size:12px;color:var(--secondary);letter-spacing:.05em;text-transform:uppercase} .cur-ring{width:34px;height:34px;border:1px solid #fff;transition:width .25s,height .25s,opacity .25s}
.section-link{font-family:var(--f-mono);font-size:12px;color:var(--ink);text-decoration:none} .cur-ring.big{width:64px;height:64px;opacity:.6}
.section-link:hover{color:var(--accent)} @media (pointer:coarse){ .cur,.cur-ring{display:none} }
/* ---------- HERO ---------- */ /* nav */
.hero{position:relative;max-width:820px} nav{position:fixed;top:0;left:0;right:0;z-index:20;display:flex;justify-content:space-between;align-items:center;padding:22px clamp(20px,4vw,56px);font-size:14px;font-weight:500;mix-blend-mode:difference}
.hero-canvas{z-index:0} nav .brand{font-family:var(--display);font-weight:800;letter-spacing:-.02em;font-size:17px}
.hero-content{position:relative;z-index:1;display:flex;flex-direction:column;gap:32px} nav ul{display:flex;gap:26px;list-style:none}
.hero-badges{display:flex;align-items:center;gap:12px;flex-wrap:wrap} nav a{position:relative;padding:4px 0}
.badge-secondary,.badge-accent{font-family:var(--f-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase} nav a::after{content:"";position:absolute;left:0;right:100%;bottom:0;height:1px;background:#fff;transition:right .3s cubic-bezier(.6,0,.2,1)}
.badge-secondary{color:var(--secondary)} nav a:hover::after,nav a:focus-visible::after{right:0}
.badge-accent{color:var(--accent)} @media (max-width:720px){ nav ul{display:none} }
.badge-sep{color:var(--border)} .progress{position:fixed;top:0;left:0;height:2px;width:100%;transform-origin:0 0;transform:scaleX(0);background:var(--amber);z-index:21}
.hero-title{font-size:60px;font-weight:300;letter-spacing:-.02em;line-height:1.08;color:var(--ink)}
.hero-lead{color:var(--secondary);font-size:20px;font-weight:300;line-height:1.65;max-width:640px}
.hero-ctas{display:flex;gap:16px;flex-wrap:wrap}
.btn{
display:inline-flex;align-items:center;gap:8px;
padding:8px 16px;border-radius:6px;font-family:var(--f-mono);font-size:12px;
text-decoration:none;transition:background .15s,color .15s;
}
.btn-dark{background:var(--ink);color:var(--canvas);border:1px solid var(--ink)}
.btn-dark:hover{background:var(--surface);color:var(--ink)}
.btn-outline{background:var(--surface);color:var(--ink);border:1px solid var(--border)}
.btn-outline:hover{border-color:var(--ink)}
.hero-metrics{
padding-top:16px;display:grid;grid-template-columns:repeat(3,1fr);gap:24px;
font-family:var(--f-mono);font-size:12px;color:var(--secondary);border-top:1px solid var(--border);max-width:576px;
}
.hero-metrics>div{display:flex;flex-direction:column}
.metric-val{color:var(--ink);font-size:16px;font-weight:500;font-family:var(--f-sans)}
.metric-lbl{color:var(--secondary);line-height:1.3}
/* ---------- CASE STUDIES ---------- */ /* shape label */
.case-list{display:flex;flex-direction:column;gap:112px} .shape-label{position:fixed;left:clamp(20px,4vw,56px);bottom:26px;z-index:20;font-size:13px;color:var(--muted);display:flex;align-items:center;gap:12px;pointer-events:none}
.case{display:grid;grid-template-columns:5fr 7fr;gap:32px;align-items:center} .shape-label i{width:6px;height:6px;border-radius:50%;background:var(--amber);box-shadow:0 0 10px rgba(200,106,0,.6);animation:blink 2.4s infinite}
.case-copy{display:flex;flex-direction:column;gap:16px} @keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
.case-label{font-family:var(--f-mono);font-size:12px;color:var(--secondary);text-transform:uppercase;letter-spacing:.03em} .shape-label span{display:inline-block;transition:opacity .4s,transform .4s}
.case-title{font-size:30px;font-weight:400;letter-spacing:-.01em;color:var(--ink)} .shape-label span.swap{opacity:0;transform:translateY(6px)}
.case-desc{color:var(--secondary);font-size:14px;line-height:1.65}
.tag-row{display:flex;flex-wrap:wrap;gap:6px}
.tag-pill{
padding:4px 10px;background:var(--surface);border:1px solid var(--border);border-radius:999px;
font-family:var(--f-mono);font-size:11px;color:var(--secondary);
}
.case-diagram{border-radius:16px;padding:32px;overflow:hidden}
.diagram-card{
background:var(--surface);border-radius:12px;border:1px solid rgba(230,228,221,.6);
box-shadow:0 1px 2px rgba(0,0,0,.04);padding:24px;display:flex;flex-direction:column;justify-content:space-between;
}
.diagram-row{display:flex;justify-content:space-between;font-family:var(--f-mono);font-size:11px;color:var(--secondary)}
.diagram-header{padding-bottom:12px}
.diagram-footer{padding-top:12px;border-top:1px solid rgba(230,228,221,.4);font-size:10px}
.diagram-svg-wrap{width:100%;overflow-x:auto;padding:8px 0}
.case-svg{width:100%;min-width:320px;height:auto}
.diagram-interactive{color:var(--accent);font-weight:600}
.case-canvas{ /* layout */
position:relative;width:100%;height:288px;border-radius:6px;overflow:hidden; main{position:relative;z-index:2}
cursor:grab;touch-action:pan-y; section{min-height:100vh;padding:20vh clamp(20px,6vw,120px) 12vh;display:flex;flex-direction:column;justify-content:center}
} .wrap{max-width:1180px;width:100%;margin:0 auto;position:relative}
.case-canvas:active{cursor:grabbing} section:not(.hero):not(.contact) .wrap::before{content:"";position:absolute;inset:-60px -80px;z-index:-1;pointer-events:none;
.case-canvas canvas{display:block;touch-action:pan-y} background:radial-gradient(ellipse 70% 60% at 38% 50%,rgba(238,240,243,.94) 35%,rgba(238,240,243,.75) 60%,rgba(238,240,243,0) 85%)}
.nas-hud{ h1,h2,h3{font-family:var(--display);letter-spacing:-.03em;line-height:.95}
position:absolute;z-index:5;pointer-events:none; h2{font-size:clamp(38px,6vw,84px);font-weight:800;margin-bottom:.5em;max-width:14ch}
background:var(--ink);color:var(--canvas); h3{font-size:clamp(20px,2.2vw,28px);font-weight:700;letter-spacing:-.02em;line-height:1.15}
font-family:var(--f-mono);font-size:10px; p.lead{font-size:clamp(18px,1.5vw,22px);max-width:56ch;color:var(--fg)}
padding:4px 8px;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.2); p.dim{color:var(--muted);max-width:60ch}
}
.led-pulse{animation:ledBlink 2.4s ease-in-out infinite} /* reveal: opacity only, one quiet fade */
@keyframes ledBlink{0%,100%{opacity:.3}50%{opacity:1}} .rv{opacity:0;transition:opacity 1s ease}
.bus-flow{stroke-dasharray:4,4;animation:busTravel 12s linear infinite} .rv.in{opacity:1}
@keyframes busTravel{to{stroke-dashoffset:-100}}
/* ---------- OSS ---------- */ /* hero */
.oss-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px} .hero{min-height:100vh;padding-top:24vh;justify-content:flex-start;pointer-events:none}
.oss-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px;display:flex;flex-direction:column;gap:12px} .hero .wrap>*{pointer-events:auto}
.oss-card-top{display:flex;justify-content:space-between;align-items:flex-start;gap:12px} .intro{font-size:15px;color:var(--muted);margin-bottom:2.2em;max-width:44ch}
.oss-kicker{display:block;font-family:var(--f-mono);font-size:10px;color:var(--accent);font-weight:600;letter-spacing:.03em} .name{font-size:clamp(56px,12.5vw,190px);font-weight:800;line-height:.86;letter-spacing:-.05em;overflow:hidden}
.oss-card-top h3{font-size:16px;font-weight:700;color:var(--ink)} .name .ln{display:block;overflow:hidden;padding-bottom:.06em;white-space:nowrap;font-size:calc(1em * var(--s,1))}
.oss-meta{font-family:var(--f-mono);font-size:11px;color:var(--secondary);white-space:nowrap} .name .ch{display:inline-block;transform:translateY(115%) rotate(6deg);opacity:0;transition:transform 1.1s cubic-bezier(.16,1,.3,1),opacity .6s;transform-origin:0 100%}
.oss-card p{font-size:12.5px;color:var(--secondary);line-height:1.65} .name.on .ch{transform:none;opacity:1}
.tag-row-mini{padding-top:8px;border-top:1px solid rgba(230,228,221,.6);font-family:var(--f-mono);font-size:10px;color:var(--secondary);gap:8px} .tag{display:flex;flex-wrap:wrap;gap:16px 40px;align-items:flex-end;margin-top:6vh}
.tag p{font-size:clamp(18px,1.7vw,24px);max-width:36ch;font-weight:500}
.tag p em{font-style:normal;color:var(--amber)}
.hint{margin-top:9vh;font-size:13px;color:var(--muted);display:flex;align-items:center;gap:14px}
.hint b{display:block;width:1px;height:46px;background:linear-gradient(var(--fg),transparent);animation:drop 1.8s cubic-bezier(.6,0,.2,1) infinite;transform-origin:top}
@keyframes drop{0%{transform:scaleY(0);opacity:0}40%{opacity:1}100%{transform:scaleY(1);opacity:0}}
/* ---------- BACKGROUND ---------- */ /* facts row */
.background{display:flex;flex-direction:column;gap:64px;padding-top:48px;border-top:1px solid var(--border)} .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:24px 40px;margin-top:9vh;border-top:1px solid var(--line);padding-top:26px;max-width:900px}
.bg-row{display:grid;grid-template-columns:4fr 8fr;gap:48px} .facts div{display:flex;flex-direction:column;gap:4px}
.bg-row-bordered{padding-top:32px;border-top:1px solid rgba(230,228,221,.6)} .facts b{font-family:var(--display);font-size:clamp(30px,3vw,44px);font-weight:700;letter-spacing:-.03em;line-height:1}
.bg-label{font-family:var(--f-mono);font-size:12px;color:var(--secondary);text-transform:uppercase;letter-spacing:.1em} .facts small{font-size:13px;color:var(--muted)}
.bg-content{max-width:640px}
.quote{font-size:24px;font-weight:300;line-height:1.4;color:var(--ink);margin-bottom:16px}
.bg-text{color:var(--secondary);font-size:14px;line-height:1.65}
.exp-list{display:flex;flex-direction:column;gap:32px} /* qualities: ruled list, not cards */
.exp-item{display:flex;flex-direction:column;gap:4px} .qual{display:grid;grid-template-columns:1fr 1fr;gap:0 60px;border-top:1px solid var(--line)}
.exp-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px} .qual article{padding:34px 0 36px;border-bottom:1px solid var(--line)}
.exp-head h3{font-size:14px;font-weight:700;color:var(--ink)} .qual article:nth-child(-n+2){border-top:0}
.exp-period{font-family:var(--f-mono);font-size:12px;color:var(--secondary);white-space:nowrap} .qual h3{margin-bottom:12px;max-width:16ch}
.exp-org{font-family:var(--f-mono);font-size:12px;color:var(--accent)} .qual p{color:var(--muted);max-width:40ch}
.exp-item p{color:var(--secondary);font-size:12.5px;line-height:1.65;padding-top:4px} @media (max-width:760px){ .qual{grid-template-columns:1fr} }
.skills-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;font-family:var(--f-mono);font-size:12px} /* timeline */
.skills-col{display:flex;flex-direction:column;gap:8px} .tl{position:relative;padding-left:44px;max-width:860px}
.skills-heading{color:var(--ink);font-weight:700;border-bottom:1px solid var(--border);padding-bottom:4px} .tl::before{content:"";position:absolute;left:8px;top:0;bottom:0;width:1px;background:var(--line)}
.skills-col ul{list-style:none;display:flex;flex-direction:column;gap:4px} .tl .beam{position:absolute;left:8px;top:0;width:1px;height:100%;background:linear-gradient(var(--amber),var(--ice));transform:scaleY(0);transform-origin:top;transition:transform 1.6s cubic-bezier(.6,0,.2,1)}
.skills-col li{color:var(--secondary);font-size:11px} .tl.in .beam{transform:scaleY(1)}
.job{position:relative;padding:0 0 64px}
.job::before{content:"";position:absolute;left:-40px;top:10px;width:9px;height:9px;border-radius:50%;background:var(--bg);border:1px solid var(--fg)}
.job time{display:block;font-size:14px;color:var(--muted);margin-bottom:8px}
.job h3{margin-bottom:6px}
.job .org{font-size:15px;color:var(--amber);margin-bottom:16px;display:block}
.job ul{list-style:none;display:grid;gap:10px;max-width:60ch;color:var(--fg)}
.job li{padding-left:18px;position:relative;color:#2b3140}
.job li::before{content:"";position:absolute;left:0;top:.65em;width:8px;height:1px;background:var(--muted)}
.edu-grid{display:grid;grid-template-columns:1fr 1fr;gap:32px} /* oss */
.edu-list{display:flex;flex-direction:column;gap:16px} .oss{display:grid;gap:0;border-top:1px solid var(--line);max-width:960px}
.edu-date{font-family:var(--f-mono);font-size:12px;color:var(--secondary)} .oss a{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:20px 40px;align-items:start;padding:32px 0;border-bottom:1px solid var(--line);transition:padding-left .4s cubic-bezier(.16,1,.3,1)}
.edu-list h4{font-size:14px;font-weight:700;color:var(--ink);margin-top:2px} .oss a:hover,.oss a:focus-visible{padding-left:14px}
.edu-list p{font-size:12px;color:var(--secondary);margin-top:4px;line-height:1.5} .oss .repo{font-size:14px;color:var(--ice);margin-bottom:8px;display:block}
.creds-list{display:flex;flex-direction:column;gap:16px;font-family:var(--f-mono);font-size:12px} .oss .what{color:var(--muted);max-width:56ch}
.creds-heading{display:block;color:var(--ink);font-weight:700;border-bottom:1px solid var(--border);padding-bottom:4px} .oss .ref{font-family:var(--display);font-weight:700;font-size:15px;color:var(--fg);white-space:nowrap;padding-top:2px;opacity:.7}
.creds-list ul{list-style:none;display:flex;flex-direction:column;gap:4px;padding-top:6px} @media (max-width:640px){ .oss a{grid-template-columns:1fr} }
.creds-list li{color:var(--secondary);font-size:11px}
.creds-list li strong{color:var(--ink)}
/* ---------- FOOTER ---------- */ /* projects: staggered, not a card grid */
.site-footer{ .proj{display:grid;grid-template-columns:repeat(12,1fr);gap:40px 24px}
max-width:var(--container);margin:0 auto;padding:80px var(--pad); .proj article{grid-column:span 5;padding:28px 0 0;border-top:1px solid var(--line);position:relative;transform-style:preserve-3d;transition:transform .5s cubic-bezier(.16,1,.3,1)}
border-top:1px solid var(--border); .proj article:nth-child(2){grid-column:7/span 6;margin-top:80px}
display:flex;justify-content:space-between;align-items:baseline;gap:16px;flex-wrap:wrap; .proj article:nth-child(3){grid-column:2/span 5;margin-top:-30px}
font-family:var(--f-mono);font-size:12px;color:var(--secondary); .proj article:nth-child(4){grid-column:8/span 5;margin-top:20px}
} .proj article::after{content:"";position:absolute;left:0;top:-1px;width:0;height:1px;background:var(--amber);transition:width .6s cubic-bezier(.16,1,.3,1)}
.footer-name{color:var(--ink);font-weight:500} .proj article:hover::after{width:100%}
.footer-links{display:flex;gap:24px;color:var(--ink)} .proj .stack{font-size:13px;color:var(--muted);margin:14px 0 0;line-height:1.8}
.footer-links a{color:inherit;text-decoration:none} .proj p{color:#2b3140;max-width:44ch;margin-top:10px}
.footer-links a:hover{color:var(--accent)} .proj .star{color:var(--amber);font-size:14px}
@media (max-width:860px){ .proj article,.proj article:nth-child(n){grid-column:1/-1;margin-top:0} }
/* ---------- RESPONSIVE ---------- */ /* skills: two counter-rotating marquees */
@media(max-width:1024px){ .mq{overflow:hidden;white-space:nowrap;padding:18px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin-top:-1px;font-family:var(--display);font-weight:700;font-size:clamp(22px,3.2vw,44px);letter-spacing:-.02em}
.case{grid-template-columns:1fr} .mq div{display:inline-block;animation:mq 60s linear infinite}
.bg-row{grid-template-columns:1fr;gap:16px} .mq.rev div{animation-direction:reverse;color:var(--muted)}
.bg-content{max-width:100%} .mq span{padding:0 .55em}
} .mq span::after{content:"/";color:var(--amber);padding-left:1.1em;font-weight:500}
@media(max-width:768px){ @keyframes mq{to{transform:translateX(-50%)}}
.oss-grid{grid-template-columns:1fr} .skills-note{margin-top:40px;color:var(--muted);max-width:60ch}
.skills-grid,.edu-grid{grid-template-columns:1fr}
} /* education */
@media(max-width:640px){ .edu{display:grid;grid-template-columns:1fr 1fr;gap:40px 60px;max-width:960px}
:root{--pad:16px} .edu dl{display:grid;gap:22px}
.header-bar{height:64px;gap:16px} .edu dt{font-weight:600}
.main-nav{gap:16px;font-size:10px} .edu dd{color:var(--muted);font-size:15px}
.site-main{padding:96px var(--pad) 80px;gap:80px} .edu .langs{display:flex;flex-wrap:wrap;gap:10px 22px;font-size:15px;color:var(--muted)}
.section-head{margin-bottom:48px} .edu .langs b{color:var(--fg);font-weight:500}
.hero{gap:24px} @media (max-width:760px){ .edu{grid-template-columns:1fr} }
.hero-title{font-size:30px;line-height:1.12}
.hero-lead{font-size:16px} /* contact */
.hero-metrics{grid-template-columns:repeat(3,1fr);gap:12px} .contact{min-height:100vh;justify-content:center;text-align:left}
.case-list{gap:64px} .contact h2{font-size:clamp(44px,8.5vw,140px);max-width:none;line-height:.9}
.case{gap:24px} .mail{display:inline-block;font-family:var(--display);font-weight:700;font-size:clamp(20px,2.6vw,34px);letter-spacing:-.02em;margin-top:20px;position:relative;padding:8px 0}
.case-title{font-size:20px} .mail::after{content:"";position:absolute;left:0;bottom:0;height:2px;width:100%;background:var(--amber);transform:scaleX(.18);transform-origin:left;transition:transform .5s cubic-bezier(.16,1,.3,1)}
.case-desc{font-size:12px} .mail:hover::after,.mail:focus-visible::after{transform:scaleX(1)}
.case-diagram{padding:16px;border-radius:12px} .links{display:flex;flex-wrap:wrap;gap:14px;margin-top:48px}
.diagram-card{padding:16px} .links a{padding:14px 22px;border:1px solid var(--line);border-radius:999px;font-size:15px;font-weight:500;transition:border-color .3s,background .3s;will-change:transform}
.case-canvas{height:256px} .links a:hover,.links a:focus-visible{border-color:var(--fg);background:rgba(20,24,33,.05)}
.site-footer{padding:48px var(--pad);flex-direction:column;align-items:flex-start} footer{padding:40px clamp(20px,6vw,120px);font-size:13px;color:var(--muted);display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px;position:relative;z-index:2}
:focus-visible{outline:2px solid var(--amber);outline-offset:4px}
@media (prefers-reduced-motion:reduce){
.grain,.hint b,.shape-label i,.mq div{animation:none}
.name .ch{transition:none;transform:none;opacity:1}
.rv{transition:none;opacity:1}
} }
#root{display:contents}
+177
View File
@@ -0,0 +1,177 @@
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { vert, frag } from './shaders'
import { shapeRack, shapeKnot, shapeGlobe, shapeText, getHoverShape, hoverFlat } from './shapes'
import { hover } from './hoverStore'
export const SHAPE_NAMES = [
'Rack: four production servers, bare metal',
'Mesh: zero exposed ports, every node peered',
'Perimeter: SSO, 2FA, host firewalls, intrusion detection',
'Contact: the whole thing reassembles on request',
]
type Props = {
/** CSS selectors of the sections each scroll shape is keyed to, in order */
keyframes: string[]
onLabel?: (text: string) => void
onProgress?: (scrollFraction: number) => void
}
export default function ParticleScene({ keyframes, onLabel, onProgress }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches
const coarse = matchMedia('(pointer: coarse)').matches
const isMobile = coarse || innerWidth < 760
const COUNT = isMobile ? 6000 : 15000
let renderer: THREE.WebGLRenderer
try { renderer = new THREE.WebGLRenderer({ canvas, antialias: false, alpha: true, powerPreference: 'high-performance' }) }
catch { canvas.style.display = 'none'; return }
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(42, innerWidth / innerHeight, 0.1, 100)
camera.position.set(0, 0, 18)
const group = new THREE.Group()
scene.add(group)
const g = new THREE.BufferGeometry()
const rand = new Float32Array(COUNT); for (let i = 0; i < COUNT; i++) rand[i] = Math.random()
g.setAttribute('position', new THREE.BufferAttribute(shapeRack(COUNT), 3))
g.setAttribute('aP0', new THREE.BufferAttribute(shapeRack(COUNT), 3))
g.setAttribute('aP1', new THREE.BufferAttribute(shapeKnot(COUNT), 3))
g.setAttribute('aP2', new THREE.BufferAttribute(shapeGlobe(COUNT), 3))
g.setAttribute('aP3', new THREE.BufferAttribute(shapeText(COUNT), 3))
g.setAttribute('aPH', new THREE.BufferAttribute(new Float32Array(COUNT * 3), 3))
g.setAttribute('aPH2', new THREE.BufferAttribute(new Float32Array(COUNT * 3), 3))
g.setAttribute('aRand', new THREE.BufferAttribute(rand, 1))
const uniforms = {
uProgress: { value: 0 }, uTime: { value: 0 }, uPR: { value: Math.min(devicePixelRatio, 2) },
uMouse: { value: new THREE.Vector3(999, 999, 0) }, uMouseOn: { value: 0 }, uIntro: { value: reduce ? 1 : 0 },
uDim: { value: 1 }, uHover: { value: 0 }, uSwap: { value: 1 },
uC1: { value: new THREE.Color('#1f3d9e') }, uC2: { value: new THREE.Color('#d9741a') },
}
const material = new THREE.ShaderMaterial({ uniforms, vertexShader: vert, fragmentShader: frag, transparent: true, depthWrite: false, blending: THREE.NormalBlending })
const points = new THREE.Points(g, material)
group.add(points)
const resize = () => {
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
renderer.setSize(innerWidth, innerHeight, false)
camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix()
camera.position.z = innerWidth < 760 ? 26 : 18
}
// scroll: real fraction for the bar, section-keyed progress for the morph
let scrollT = 0, scrollCur = 0, progT = 0, progCur = 0
let mx = 0, my = 0, mxCur = 0, myCur = 0, mouseOn = 0, mouseOnCur = 0
let hoverT = 0, hoverCur = 0, swapCur = 1, hoverKey: string | null = null
const updateScroll = () => {
const max = document.documentElement.scrollHeight - innerHeight
scrollT = max > 0 ? scrollY / max : 0
const els = keyframes.map(q => document.querySelector<HTMLElement>(q)).filter(Boolean) as HTMLElement[]
if (els.length < 2) return
const c = els.map(el => el.offsetTop + el.offsetHeight / 2 - innerHeight / 2), y = scrollY, last = els.length - 1
let v = last
if (y <= c[0]) v = 0
else for (let i = 0; i < last; i++) { if (y < c[i + 1]) { v = i + THREE.MathUtils.smoothstep((y - c[i]) / Math.max(1, c[i + 1] - c[i]), 0.45, 1); break } }
progT = v / 3
}
const onMove = (e: PointerEvent) => { mx = (e.clientX / innerWidth) * 2 - 1; my = -(e.clientY / innerHeight) * 2 + 1; mouseOn = 1 }
const onLeave = () => { mouseOn = 0 }
const onResize = () => { resize(); updateScroll() }
addEventListener('resize', onResize)
addEventListener('scroll', updateScroll, { passive: true })
addEventListener('pointermove', onMove, { passive: true })
document.addEventListener('mouseleave', onLeave)
// hover shapes come from the shared store; swapping cross-fades through a scatter
const applyHover = () => {
if (!hover.on) { hoverT = 0; return }
const k = hover.key!
if (hoverKey !== k) {
const ph = g.getAttribute('aPH') as THREE.BufferAttribute, ph2 = g.getAttribute('aPH2') as THREE.BufferAttribute
if (hoverKey) { (ph2.array as Float32Array).set(ph.array as Float32Array); ph2.needsUpdate = true; swapCur = 0 }
else { (ph2.array as Float32Array).set(getHoverShape(k, COUNT)); ph2.needsUpdate = true; swapCur = 1 }
;(ph.array as Float32Array).set(getHoverShape(k, COUNT)); ph.needsUpdate = true; hoverKey = k
}
hoverT = 1
}
const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0), ray = new THREE.Raycaster(), hit = new THREE.Vector3(), ndc = new THREE.Vector2()
let last = performance.now(), elapsed = 0, introT = 0, labelIdx = -1, lastLabel = ''
let raf = 0
const frame = (now: number) => {
raf = requestAnimationFrame(frame)
const dt = Math.min((now - last) / 1000, 0.05); last = now; elapsed += dt
const t = elapsed
applyHover()
scrollCur += (scrollT - scrollCur) * 0.06; progCur += (progT - progCur) * 0.07
mxCur += (mx - mxCur) * 0.08; myCur += (my - myCur) * 0.08
mouseOnCur += (mouseOn - mouseOnCur) * 0.1
if (!reduce) { introT = Math.min(1, introT + dt * 0.55); uniforms.uIntro.value = 1 - Math.pow(1 - introT, 3) }
const prog = progCur * 3
uniforms.uProgress.value = prog
uniforms.uTime.value = t
group.rotation.y = scrollCur * Math.PI * 1.35 + t * 0.03
group.rotation.x = Math.sin(scrollCur * Math.PI) * 0.35
const face = THREE.MathUtils.smoothstep(prog, 2.55, 3.0)
group.rotation.y = THREE.MathUtils.lerp(group.rotation.y, Math.round(group.rotation.y / (Math.PI * 2)) * Math.PI * 2, face)
group.rotation.x = THREE.MathUtils.lerp(group.rotation.x, 0, face)
hoverCur += (hoverT - hoverCur) * 0.06; swapCur += (1 - swapCur) * 0.08
const hEase = hoverCur * hoverCur * (3 - 2 * hoverCur)
uniforms.uHover.value = hEase; uniforms.uSwap.value = swapCur
if (hoverCur > 0.001) {
const flat = hoverKey ? hoverFlat[hoverKey] : false
const ty = (flat ? 0 : 0.62) + t * 0.08, tx = flat ? 0 : 0.3
const near = ty + Math.round((group.rotation.y - ty) / (Math.PI * 2)) * Math.PI * 2
group.rotation.y = THREE.MathUtils.lerp(group.rotation.y, near, hEase)
group.rotation.x = THREE.MathUtils.lerp(group.rotation.x, tx, hEase)
}
const mid = THREE.MathUtils.smoothstep(prog, 0.15, 0.6) * (1 - THREE.MathUtils.smoothstep(prog, 2.5, 2.95))
const offX = innerWidth < 760 ? 0 : THREE.MathUtils.lerp(5.5, 4.5, THREE.MathUtils.smoothstep(prog, 0, 0.8)) * (1 - THREE.MathUtils.smoothstep(prog, 2.5, 2.95))
const hx = innerWidth < 760 ? 0 : 3.6, hz = innerWidth < 760 ? 2 : 5.5
group.position.x += (THREE.MathUtils.lerp(offX, hx, hEase) - group.position.x) * 0.05
group.position.z += (hz * hEase - group.position.z) * 0.05
uniforms.uDim.value = THREE.MathUtils.lerp(1 - mid * 0.55, 1, hEase)
camera.position.x += (mxCur * 0.9 - camera.position.x) * 0.05
camera.position.y += (myCur * 0.6 - camera.position.y) * 0.05
camera.lookAt(0, 0, 0)
ndc.set(mxCur, myCur); ray.setFromCamera(ndc, camera)
if (ray.ray.intersectPlane(plane, hit)) { group.worldToLocal(hit); uniforms.uMouse.value.copy(hit) }
uniforms.uMouseOn.value = coarse ? 0 : mouseOnCur
// label + progress callbacks
const label = hover.on && hover.label ? hover.label : SHAPE_NAMES[Math.min(3, Math.round(progCur * 3))]
if (label !== lastLabel) { lastLabel = label; labelIdx = -1; onLabel?.(label) }
void labelIdx
onProgress?.(scrollCur)
renderer.render(scene, camera)
}
const boot = () => { resize(); updateScroll(); raf = requestAnimationFrame(frame) }
// wait for the display font so the "PQ" sampling uses it
const ready = document.fonts?.load ? Promise.race([document.fonts.load('800 300px Syne'), new Promise(r => setTimeout(r, 1500))]) : Promise.resolve()
ready.then(boot, boot)
return () => {
cancelAnimationFrame(raf)
removeEventListener('resize', onResize); removeEventListener('scroll', updateScroll)
removeEventListener('pointermove', onMove); document.removeEventListener('mouseleave', onLeave)
g.dispose(); material.dispose(); renderer.dispose()
}
}, [keyframes, onLabel, onProgress])
return <canvas id="scene" ref={canvasRef} aria-hidden="true" />
}
+5
View File
@@ -0,0 +1,5 @@
// Tiny shared state between the page and the particle scene
export type HoverState = { key: string | null; label: string | null; on: boolean }
export const hover: HoverState = { key: null, label: null, on: false }
export function hoverIn(key: string, label: string) { hover.key = key; hover.label = label; hover.on = true }
export function hoverOut() { hover.on = false }
+64
View File
@@ -0,0 +1,64 @@
// GLSL for the particle sculpture
export const noiseGLSL = `
vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}
vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}
float snoise(vec3 v){
const vec2 C=vec2(1.0/6.0,1.0/3.0); const vec4 D=vec4(0.0,0.5,1.0,2.0);
vec3 i=floor(v+dot(v,C.yyy)); vec3 x0=v-i+dot(i,C.xxx);
vec3 g=step(x0.yzx,x0.xyz); vec3 l=1.0-g; vec3 i1=min(g.xyz,l.zxy); vec3 i2=max(g.xyz,l.zxy);
vec3 x1=x0-i1+C.xxx; vec3 x2=x0-i2+C.yyy; vec3 x3=x0-D.yyy;
i=mod289(i);
vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));
float n_=0.142857142857; vec3 ns=n_*D.wyz-D.xzx;
vec4 j=p-49.0*floor(p*ns.z*ns.z); vec4 x_=floor(j*ns.z); vec4 y_=floor(j-7.0*x_);
vec4 x=x_*ns.x+ns.yyyy; vec4 y=y_*ns.x+ns.yyyy; vec4 h=1.0-abs(x)-abs(y);
vec4 b0=vec4(x.xy,y.xy); vec4 b1=vec4(x.zw,y.zw);
vec4 s0=floor(b0)*2.0+1.0; vec4 s1=floor(b1)*2.0+1.0; vec4 sh=-step(h,vec4(0.0));
vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy; vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
vec3 p0=vec3(a0.xy,h.x); vec3 p1=vec3(a0.zw,h.y); vec3 p2=vec3(a1.xy,h.z); vec3 p3=vec3(a1.zw,h.w);
vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));
p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;
vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0); m=m*m;
return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
}`;
export const vert = noiseGLSL + `
attribute vec3 aP0; attribute vec3 aP1; attribute vec3 aP2; attribute vec3 aP3; attribute vec3 aPH; attribute vec3 aPH2; attribute float aRand;
uniform float uProgress; uniform float uTime; uniform float uPR; uniform vec3 uMouse; uniform float uMouseOn; uniform float uIntro; uniform float uDim; uniform float uHover; uniform float uSwap;
varying float vA; varying float vHeat;
void main(){
float p=clamp(uProgress,0.0,3.0);
float s01=smoothstep(0.0,1.0,p), s12=smoothstep(1.0,2.0,p), s23=smoothstep(2.0,3.0,p);
vec3 pos = aP0*(1.0-s01) + aP1*(s01-s12) + aP2*(s12-s23) + aP3*s23;
float f=fract(p); float tr = (p>=3.0)?0.0:f*(1.0-f)*4.0; // 0 at rest, 1 mid-morph
// hover: the sculpture becomes the project under the cursor
vec3 hp = mix(aPH2, aPH, uSwap);
pos = mix(pos, hp, uHover);
tr = tr*(1.0-uHover) + max(uHover*(1.0-uHover)*4.0, uSwap*(1.0-uSwap)*4.0)*0.7;
vec3 nv = vec3(snoise(pos*0.45+uTime*0.12), snoise(pos*0.45+vec3(13.7)+uTime*0.12), snoise(pos*0.45+vec3(71.2)+uTime*0.12));
pos += nv*(0.07*(1.0-uHover*0.85) + tr*(2.2+aRand*2.0));
// intro: particles fall in from a scattered cloud
vec3 spread = (aRand-0.5)*vec3(30.0,20.0,30.0) + nv*6.0;
pos = mix(spread, pos, uIntro);
// mouse repulsion
vec3 d = pos - uMouse; float dist=length(d);
float rep = smoothstep(3.2,0.0,dist)*uMouseOn;
pos += (d/max(dist,0.001))*rep*1.6;
vec4 mv = modelViewMatrix*vec4(pos,1.0);
gl_Position = projectionMatrix*mv;
float size = (0.8+aRand*1.4)*uPR*(1.0+tr*1.0+rep*1.4);
gl_PointSize = min(size*(200.0/-mv.z), 9.0*uPR);
vA = (0.35+0.55*aRand)*uIntro*uDim*(1.0-tr*0.45);
vHeat = clamp(tr*0.9+rep+aRand*0.25,0.0,1.0);
}`;
export const frag = `
uniform vec3 uC1; uniform vec3 uC2; varying float vA; varying float vHeat;
void main(){
vec2 uv=gl_PointCoord-0.5; float d=length(uv); if(d>0.5) discard;
float a=smoothstep(0.5,0.12,d);
vec3 c=mix(uC1,uC2,vHeat);
gl_FragColor=vec4(c, a*vA);
}`;
+140
View File
@@ -0,0 +1,140 @@
// Point-cloud generators: the four scroll shapes and the hover dioramas
const rnd = (a = 1, b = 0): number =>Math.random()*(a-b)+b;
/* Shape 0 — a server rack: stacked chassis outlines, drive bays, cable loom */
export function shapeRack(n: number): Float32Array {
const a = new Float32Array(n*3);
const W=4.6, D=3.2, units=9, H=0.72, y0=-(units*H)/2;
for(let i=0;i<n;i++){
let x,y,z; const r=Math.random();
if(r<0.42){ // horizontal chassis edges
const u=Math.floor(rnd(units+1)), edge=Math.floor(rnd(4)), t=rnd(1);
y=y0+u*H;
if(edge===0){x=-W/2+t*W;z=D/2}else if(edge===1){x=-W/2+t*W;z=-D/2}else if(edge===2){x=W/2;z=-D/2+t*D}else{x=-W/2;z=-D/2+t*D}
} else if(r<0.55){ // vertical rails
const c=Math.floor(rnd(4)); x=(c%2?1:-1)*W/2; z=(c<2?1:-1)*D/2; y=y0+rnd(units*H);
} else if(r<0.88){ // front face drive bays, dense
x=rnd(W/2-0.15,-W/2+0.15); const u=Math.floor(rnd(units)); const bay=Math.floor(rnd(3));
y=y0+u*H+0.12+bay*0.2+rnd(0.06); z=D/2+0.02;
} else { // rear cable loom drooping
const t=rnd(1); x=rnd(0.6,-0.6)+Math.sin(t*9+i)*0.25; y=y0+t*units*H; z=-D/2-0.35-Math.sin(t*3.1)*0.3;
}
a[i*3]=x;a[i*3+1]=y;a[i*3+2]=z;
}
return a;
}
/* Shape 1 — torus knot: the mesh network */
export function shapeKnot(n: number): Float32Array {
const a=new Float32Array(n*3), p=2,q=3, R=3.0, r=1.35, tube=0.55;
const P=(t: number)=>{const c=R+r*Math.cos(q*t);return [c*Math.cos(p*t),c*Math.sin(p*t),r*Math.sin(q*t)];};
for(let i=0;i<n;i++){
const t=rnd(Math.PI*2), c=P(t), c2=P(t+0.001);
let tx=c2[0]-c[0],ty=c2[1]-c[1],tz=c2[2]-c[2]; const tl=Math.hypot(tx,ty,tz); tx/=tl;ty/=tl;tz/=tl;
// normal ~ toward the ring centre, then binormal = T x N
let nx=-c[0],ny=-c[1],nz=0; const dn=tx*nx+ty*ny; nx-=tx*dn;ny-=ty*dn;nz-=tz*dn; const nl=Math.hypot(nx,ny,nz)||1; nx/=nl;ny/=nl;nz/=nl;
const bx=ty*nz-tz*ny, by=tz*nx-tx*nz, bz=tx*ny-ty*nx;
const ang=rnd(Math.PI*2), rad=Math.random()<0.7?tube:tube*Math.sqrt(Math.random());
const ca=Math.cos(ang)*rad, sa=Math.sin(ang)*rad;
a[i*3]=c[0]+nx*ca+bx*sa; a[i*3+1]=c[1]+ny*ca+by*sa; a[i*3+2]=c[2]+nz*ca+bz*sa;
}
return a;
}
/* Shape 2 — globe with latitude bands + a defensive shell: the zero-trust perimeter */
export function shapeGlobe(n: number): Float32Array {
const a=new Float32Array(n*3);
for(let i=0;i<n;i++){
const r=Math.random();
let R=4.2, theta, phi;
if(r<0.6){ // lat/long grid
const useLat=Math.random()<0.5;
if(useLat){ phi=Math.acos(1-2*(Math.floor(rnd(13))/12)); theta=rnd(Math.PI*2); }
else { theta=Math.floor(rnd(24))*(Math.PI/12); phi=Math.acos(1-2*Math.random()); }
} else if(r<0.85){ phi=Math.acos(1-2*Math.random()); theta=rnd(Math.PI*2); R=4.2+rnd(0.05); }
else { phi=Math.acos(1-2*Math.random()); theta=rnd(Math.PI*2); R=5.6+rnd(0.15); } // outer shell
a[i*3]=R*Math.sin(phi)*Math.cos(theta); a[i*3+1]=R*Math.cos(phi); a[i*3+2]=R*Math.sin(phi)*Math.sin(theta);
}
return a;
}
/* Canvas sampler: anything drawn white on black becomes particles */
function sampleCanvas(n: number, w: number, h: number, draw: (ctx: CanvasRenderingContext2D) => void, S: number, zSpread: number): Float32Array {
const c=document.createElement('canvas'); c.width=w; c.height=h;
const ctx=c.getContext('2d')!; ctx.fillStyle='#000'; ctx.fillRect(0,0,w,h); ctx.fillStyle='#fff'; ctx.strokeStyle='#fff';
draw(ctx);
const img=ctx.getImageData(0,0,w,h).data, pts: number[]=[];
for(let y=0;y<h;y+=2)for(let x=0;x<w;x+=2){ if(img[(y*w+x)*4]>128) pts.push(x,y); }
const a=new Float32Array(n*3);
for(let i=0;i<n;i++){
const k=Math.floor(rnd(pts.length/2))*2;
a[i*3]=(pts[k]-w/2)*S+rnd(0.04,-0.04); a[i*3+1]=-(pts[k+1]-h/2)*S+rnd(0.04,-0.04);
a[i*3+2]=rnd(zSpread,-zSpread)*(Math.random()<0.15?1:0.15);
}
return a;
}
/* Shape 3 — the initials */
export function shapeText(n: number): Float32Array {
return sampleCanvas(n,720,360,ctx=>{ ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.font='800 300px Syne, "Helvetica Neue", Arial, sans-serif'; ctx.fillText('PQ',360,194); },0.0215,0.9);
}
/* Primitive builder for the project dioramas: boxes as edges or solid fill, plus lines */
type Prim = { t: 'edges' | 'fill'; c: number[]; s: number[]; w: number } | { t: 'line'; a: number[]; b: number[]; w: number }
function fromPrims(n: number, prims: Prim[], scale: number): Float32Array {
const tot=prims.reduce((t,p)=>t+p.w,0), a=new Float32Array(n*3);
for(let i=0;i<n;i++){
let r=Math.random()*tot, pr=prims[0]; for(const q of prims){ pr=q; if(r<q.w)break; r-=q.w; }
let x=0,y=0,z=0;
if(pr.t==='line'){ const u=rnd(1); x=pr.a[0]+(pr.b[0]-pr.a[0])*u; y=pr.a[1]+(pr.b[1]-pr.a[1])*u; z=pr.a[2]+(pr.b[2]-pr.a[2])*u; }
else {
const [cx,cy,cz]=pr.c,[w,h,d]=pr.s;
if(pr.t==='edges'){ const e=Math.floor(rnd(12)), u=rnd(1)-0.5, s1=(e&1)?0.5:-0.5, s2=(e&2)?0.5:-0.5;
if(e<4){x=u*w;y=s1*h;z=s2*d} else if(e<8){x=s1*w;y=u*h;z=s2*d} else {x=s1*w;y=s2*h;z=u*d} }
else { x=(rnd(1)-0.5)*w; y=(rnd(1)-0.5)*h; z=(rnd(1)-0.5)*d; }
x+=cx;y+=cy;z+=cz;
}
a[i*3]=x*scale;a[i*3+1]=y*scale;a[i*3+2]=z*scale;
}
return a;
}
export const hoverShapes: Record<string, (n: number) => Float32Array> = {
homelab(n: number){ // the homelab chassis: nine bays, copper strip, grille top
const pr: Prim[]=[{t:'edges',c:[0,0,0],s:[2.3,2.4,2.6],w:3},{t:'fill',c:[0,1.16,0],s:[2.15,0.01,2.45],w:0.9},{t:'fill',c:[0,0.25,1.32],s:[2.3,0.22,0.06],w:0.9}];
const bw=0.17,g=0.02,x0=-(9*bw+8*g)/2+bw/2;
for(let i=0;i<9;i++){ const x=x0+i*(bw+g); pr.push({t:'edges',c:[x,-0.43,1.3],s:[bw,1.05,0.18],w:0.35}); pr.push({t:'fill',c:[x,-0.43,1.12],s:[bw-0.04,0.9,0.3],w:0.32}); }
return fromPrims(n,pr,2.1);
},
cluster(n: number){ // DEC Energy: two boards, GPU on one, HBA on the other, bridged
const pr: Prim[]=[];
for(const bx of [-0.95,0.95]){ pr.push({t:'fill',c:[bx,0,0],s:[1.5,0.08,1.8],w:0.7},{t:'edges',c:[bx,0,0],s:[1.5,0.08,1.8],w:1.6},{t:'fill',c:[bx,0.1,-0.25],s:[0.45,0.12,0.45],w:0.5}); }
pr.push({t:'fill',c:[-0.45,0.25,0.1],s:[0.12,0.42,1.2],w:1.0},{t:'fill',c:[0.45,0.22,0.1],s:[0.12,0.35,1.0],w:0.8});
pr.push({t:'line',a:[-0.5,0.12,-0.25],b:[0.5,0.12,-0.25],w:0.5});
for(const bx of [-0.95,0.95]) for(let k=0;k<3;k++) pr.push({t:'line',a:[bx-0.55,0.05,0.5+k*0.2],b:[bx+0.55,0.05,0.5+k*0.2],w:0.12});
return fromPrims(n,pr,2.4);
},
hosts(n: number){ // four hosts on the floor, one hub, one tunnel out
const pr: Prim[]=[]; const hosts=[[-1.3,-1.2,-1.0],[1.3,-1.2,-1.0],[-1.3,-1.2,1.0],[1.3,-1.2,1.0]];
for(const h of hosts){ pr.push({t:'edges',c:h,s:[1.7,0.5,1.4],w:1},{t:'fill',c:[h[0],h[1],h[2]+0.68],s:[1.5,0.32,0.05],w:0.45},{t:'line',a:[h[0],h[1]+0.25,h[2]],b:[0,0.6,0],w:0.35}); }
pr.push({t:'fill',c:[0,0.6,0],s:[0.45,0.45,0.45],w:0.6},{t:'line',a:[0,0.6,0],b:[0,2.6,0],w:0.5},{t:'edges',c:[0,2.9,0],s:[1.2,0.5,0.9],w:0.5});
return fromPrims(n,pr,1.55);
},
city(n: number){ // the simulated city: a grid of blocks
const pr: Prim[]=[{t:'edges',c:[0,-0.05,0],s:[9.2,0.02,9.2],w:1.2}]; let seed=7; const rr=()=>{seed=(seed*16807)%2147483647;return seed/2147483647;};
for(let i=0;i<8;i++)for(let j=0;j<8;j++){ const hgt=0.3+rr()*rr()*3.2, x=-3.85+i*1.1, z=-3.85+j*1.1; pr.push({t:'edges',c:[x,hgt/2,z],s:[0.8,hgt,0.8],w:0.35+hgt*0.15}); if(rr()<0.35)pr.push({t:'fill',c:[x,hgt/2,z],s:[0.78,hgt,0.78],w:0.08}); }
return fromPrims(n,pr,0.72);
},
hid(n: number){ // the libratbag report: header, command, profile, DPI, then upstream
return sampleCanvas(n,1000,420,ctx=>{
ctx.lineWidth=5; ctx.beginPath(); ctx.moveTo(40,40); ctx.lineTo(960,40); ctx.stroke();
ctx.setLineDash([8,10]); ctx.beginPath(); ctx.moveTo(110,40); ctx.lineTo(160,130); ctx.moveTo(420,40); ctx.lineTo(380,130); ctx.stroke(); ctx.setLineDash([]);
const boxes=[[60,120,'0x08'],[180,120,'0x14'],[300,140,'0x01 02'],[440,190,'0x0640'],[630,300,'00 00 …']];
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.font='700 42px Syne, Arial, sans-serif';
for(const [x,w,t] of boxes as [number,number,string][]){ ctx.lineWidth=(t==='0x0640')?10:5; ctx.strokeRect(x,130,w,110); ctx.fillText(t,x+w/2,185); }
ctx.lineWidth=5; ctx.beginPath(); ctx.moveTo(535,240); ctx.lineTo(535,300); ctx.stroke();
ctx.fillRect(360,300,350,70); ctx.fillStyle='#000'; ctx.font='700 30px Syne, Arial, sans-serif'; ctx.fillText('MERGED UPSTREAM',535,336);
ctx.fillStyle='#fff'; ctx.font='500 26px Syne, Arial, sans-serif'; ctx.textAlign='left';
ctx.fillText('REPORT',60,275); ctx.fillText('CMD',180,275); ctx.fillText('PROFILE 2',300,275); ctx.fillText('1600 DPI',440,275);
},0.0142,0.5);
}
};
hoverShapes.nas=shapeRack; // the client rack is the same one that opens the page
export const hoverFlat: Record<string, boolean> = { hid: true };
const shapeCache: Record<string, Float32Array> = {};
export function getHoverShape(k: string, count: number): Float32Array { return shapeCache[k] || (shapeCache[k] = hoverShapes[k](count)); }
-195
View File
@@ -1,195 +0,0 @@
import * as THREE from 'three'
import type { BuildFn } from './ThreeViewport'
const wireMat = new THREE.LineBasicMaterial({ color: 0x121212 })
const chassisMat = new THREE.MeshBasicMaterial({ color: 0xf7f6f2, transparent: true, opacity: 0.75 })
const darkPartMat = new THREE.MeshBasicMaterial({ color: 0x1c1b19 })
const swissRedMat = new THREE.MeshBasicMaterial({ color: 0xc1121f })
const copperMat = new THREE.MeshBasicMaterial({ color: 0xb9814f })
const tealDriveMat = new THREE.MeshBasicMaterial({ color: 0x1f8f86, transparent: true, opacity: 0.75 })
export const buildRackScene: BuildFn = (scene, camera) => {
camera.position.set(4, 2.5, 4.5)
const rack = new THREE.Group()
rack.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.6, 2.8, 1.4)), wireMat))
for (let i = 0; i < 4; i++) {
const server = new THREE.Mesh(new THREE.BoxGeometry(1.45, 0.38, 1.25), chassisMat)
server.position.set(0, -0.9 + i * 0.55, 0)
server.add(new THREE.LineSegments(new THREE.EdgesGeometry(server.geometry), wireMat))
for (let d = 0; d < 4; d++) {
const drive = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.22, 0.05), darkPartMat)
drive.position.set(-0.45 + d * 0.3, 0, 0.63)
server.add(drive)
}
const led = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.04, 0.04), swissRedMat)
led.position.set(0.6, 0.1, 0.64)
server.add(led)
rack.add(server)
}
scene.add(rack)
}
export const buildClusterScene: BuildFn = (scene, camera) => {
camera.position.set(3.8, 3.5, 3.8)
const cluster = new THREE.Group()
const makeBoard = (x: number) => {
const board = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.08, 1.8), chassisMat)
board.position.set(x, 0, 0)
board.add(new THREE.LineSegments(new THREE.EdgesGeometry(board.geometry), wireMat))
const cpu = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.12, 0.45), darkPartMat)
cpu.position.set(0, 0.06, -0.2)
board.add(cpu)
return board
}
const b1 = makeBoard(-0.9)
const gpu = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.4, 1.2), darkPartMat)
gpu.position.set(0.5, 0.2, 0.1)
b1.add(gpu)
cluster.add(b1)
const b2 = makeBoard(0.9)
const hba = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.35, 1), darkPartMat)
hba.position.set(-0.5, 0.18, 0.1)
b2.add(hba)
cluster.add(b2)
const bridge = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.04, 0.06), swissRedMat)
bridge.position.set(0, 0.08, -0.2)
cluster.add(bridge)
scene.add(cluster)
}
function makeGrilleTexture() {
const c = document.createElement('canvas')
c.width = c.height = 128
const ctx = c.getContext('2d')!
ctx.fillStyle = '#1a1a1a'
ctx.fillRect(0, 0, 128, 128)
ctx.fillStyle = '#3a3a3a'
for (let x = 5; x < 128; x += 10) {
for (let y = 5; y < 128; y += 10) ctx.fillRect(x - 1.5, y - 1.5, 3, 3)
}
const tex = new THREE.CanvasTexture(c)
tex.wrapS = tex.wrapT = THREE.RepeatWrapping
tex.repeat.set(2, 2)
return tex
}
export const buildNasScene: BuildFn = (scene, camera, controls, renderer, container) => {
camera.position.set(3.6, 2.6, 3.8)
const nas = new THREE.Group()
const W = 2.3, H = 2.4, D = 2.6
const halfH = H / 2
const stripY = 0.25
const bayTopY = stripY - 0.15
const bayHeight = bayTopY - (-halfH + 0.25)
nas.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(W, H, D)), wireMat))
const grilleTex = makeGrilleTexture()
const topMat = new THREE.MeshBasicMaterial({ map: grilleTex, transparent: true, opacity: 0.85 })
const top = new THREE.Mesh(new THREE.PlaneGeometry(W - 0.12, D - 0.12), topMat)
top.rotation.x = -Math.PI / 2
top.position.y = halfH - 0.05
nas.add(top)
const strip = new THREE.Group()
strip.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(W, 0.26, 0.08)), new THREE.LineBasicMaterial({ color: 0xb9814f })))
const stripFace = new THREE.Mesh(new THREE.PlaneGeometry(W - 0.04, 0.22), copperMat)
stripFace.position.z = 0.045
strip.add(stripFace)
strip.position.set(0, stripY, D / 2 + 0.02)
nas.add(strip)
const bayWidth = 0.17, gap = 0.02
const startX = -(9 * bayWidth + 8 * gap) / 2 + bayWidth / 2
const hitMeshes: THREE.Mesh[] = []
const leds: { mat: THREE.MeshBasicMaterial; phase: number }[] = []
for (let i = 0; i < 9; i++) {
const x = startX + i * (bayWidth + gap)
const bay = new THREE.Group()
bay.position.set(x, bayTopY - bayHeight / 2, D / 2)
bay.add(new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(bayWidth, bayHeight, 0.18)), wireMat))
const drive = new THREE.Mesh(new THREE.BoxGeometry(bayWidth - 0.03, bayHeight - 0.12, 0.32), tealDriveMat)
drive.position.z = -0.16
bay.add(drive)
const hitMesh = new THREE.Mesh(new THREE.BoxGeometry(bayWidth, bayHeight, 0.5), new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 }))
hitMesh.position.z = -0.1
hitMesh.userData.label = `BAY ${String(i + 1).padStart(2, '0')} // WD RED 10TB (ONLINE)`
bay.add(hitMesh)
hitMeshes.push(hitMesh)
const ledMat = new THREE.MeshBasicMaterial({ color: 0xc1121f, transparent: true, opacity: 0.5 })
const led = new THREE.Mesh(new THREE.CircleGeometry(0.018, 8), ledMat)
led.position.set(0, bayHeight / 2 - 0.08, 0.1)
bay.add(led)
leds.push({ mat: ledMat, phase: i * 0.5 })
nas.add(bay)
}
scene.add(nas)
const hud = document.createElement('div')
hud.className = 'nas-hud'
hud.hidden = true
container.appendChild(hud)
const raycaster = new THREE.Raycaster()
const pointer = new THREE.Vector2()
const onPointerMove = (e: PointerEvent) => {
const rect = renderer.domElement.getBoundingClientRect()
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(pointer, camera)
const hit = raycaster.intersectObjects(hitMeshes)[0]
if (hit) {
hud.textContent = hit.object.userData.label
hud.style.left = `${Math.min(e.clientX - rect.left + 12, rect.width - 180)}px`
hud.style.top = `${Math.max(e.clientY - rect.top - 28, 10)}px`
hud.hidden = false
controls.autoRotate = false
} else {
hud.hidden = true
controls.autoRotate = true
}
}
const onPointerLeave = () => {
hud.hidden = true
controls.autoRotate = true
}
renderer.domElement.addEventListener('pointermove', onPointerMove)
renderer.domElement.addEventListener('pointerleave', onPointerLeave)
const clock = new THREE.Clock()
return {
update: () => {
const t = clock.getElapsedTime()
for (const l of leds) l.mat.opacity = 0.3 + Math.abs(Math.sin(t * 1.6 + l.phase)) * 0.6
},
dispose: () => {
renderer.domElement.removeEventListener('pointermove', onPointerMove)
renderer.domElement.removeEventListener('pointerleave', onPointerLeave)
container.removeChild(hud)
grilleTex.dispose()
topMat.dispose()
},
}
}
+95
View File
@@ -0,0 +1,95 @@
import { useEffect } from 'react'
import { hoverIn, hoverOut } from './scene/hoverStore'
// Everything in the page that moves: name reveal, counters, fades, tilt, magnetic links, cursor, hover wiring.
export function usePageMotion() {
useEffect(() => {
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches
const coarse = matchMedia('(pointer: coarse)').matches
const cleanups: (() => void)[] = []
// hero name
const nameT = setTimeout(() => document.getElementById('name')?.classList.add('on'), 150)
cleanups.push(() => clearTimeout(nameT))
// counters
const countUp = (el: HTMLElement) => {
const target = +el.dataset.count!, prefix = el.dataset.prefix || '', t0 = performance.now(), dur = 1600
const step = (now: number) => {
const k = Math.min(1, (now - t0) / dur), e = 1 - Math.pow(1 - k, 3)
el.textContent = prefix + Math.round(target * e) + (k === 1 && !prefix ? '+' : '')
if (k < 1) requestAnimationFrame(step)
}
requestAnimationFrame(step)
}
// one quiet fade per block
const io = new IntersectionObserver(es => es.forEach(e => {
if (!e.isIntersecting) return
e.target.classList.add('in'); io.unobserve(e.target)
if (e.target.classList.contains('facts')) e.target.querySelectorAll<HTMLElement>('[data-count]').forEach(countUp)
}), { threshold: 0.15 })
document.querySelectorAll('.rv').forEach(el => io.observe(el))
cleanups.push(() => io.disconnect())
// timeline beam
const tl = document.getElementById('tl')
if (tl) { const tio = new IntersectionObserver(es => es.forEach(e => { if (e.isIntersecting) tl.classList.add('in') }), { threshold: 0.2 }); tio.observe(tl); cleanups.push(() => tio.disconnect()) }
// hover-morph rows
const rows = [...document.querySelectorAll<HTMLElement>('[data-shape]')]
rows.forEach(el => {
const on = () => hoverIn(el.dataset.shape!, el.dataset.label || '')
el.addEventListener('pointerenter', on); el.addEventListener('pointerleave', hoverOut)
el.addEventListener('focusin', on); el.addEventListener('focusout', hoverOut)
cleanups.push(() => { el.removeEventListener('pointerenter', on); el.removeEventListener('pointerleave', hoverOut); el.removeEventListener('focusin', on); el.removeEventListener('focusout', hoverOut) })
})
if (coarse) { // touch: the row nearest the middle of the screen drives the sculpture
const iv = setInterval(() => {
const mid = innerHeight / 2; let best: HTMLElement | null = null, bd = 1e9
for (const r of rows) { const b = r.getBoundingClientRect(); if (b.bottom < 0 || b.top > innerHeight) continue; const d = Math.abs((b.top + b.bottom) / 2 - mid); if (d < bd) { bd = d; best = r } }
if (best && bd < innerHeight * 0.3) hoverIn(best.dataset.shape!, best.dataset.label || ''); else hoverOut()
}, 250)
cleanups.push(() => clearInterval(iv))
}
if (!coarse && !reduce) {
document.querySelectorAll<HTMLElement>('[data-tilt]').forEach(el => {
const mv = (e: PointerEvent) => { const r = el.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width - 0.5, y = (e.clientY - r.top) / r.height - 0.5; el.style.transform = `perspective(900px) rotateX(${-y * 6}deg) rotateY(${x * 8}deg) translateZ(6px)` }
const lv = () => { el.style.transform = '' }
el.addEventListener('pointermove', mv); el.addEventListener('pointerleave', lv)
cleanups.push(() => { el.removeEventListener('pointermove', mv); el.removeEventListener('pointerleave', lv) })
})
document.querySelectorAll<HTMLElement>('[data-mag]').forEach(el => {
const mv = (e: PointerEvent) => { const r = el.getBoundingClientRect(); const x = e.clientX - (r.left + r.width / 2), y = e.clientY - (r.top + r.height / 2); el.style.transform = `translate(${x * 0.25}px,${y * 0.35}px)`; el.style.transition = 'transform .15s' }
const lv = () => { el.style.transition = 'transform .5s cubic-bezier(.16,1,.3,1)'; el.style.transform = '' }
el.addEventListener('pointermove', mv); el.addEventListener('pointerleave', lv)
cleanups.push(() => { el.removeEventListener('pointermove', mv); el.removeEventListener('pointerleave', lv) })
})
}
// custom cursor
if (!coarse) {
const dot = document.querySelector<HTMLElement>('.cur'), ring = document.querySelector<HTMLElement>('.cur-ring')
if (dot && ring) {
let cx = innerWidth / 2, cy = innerHeight / 2, rx = cx, ry = cy, raf = 0
const mv = (e: PointerEvent) => { cx = e.clientX; cy = e.clientY }
addEventListener('pointermove', mv, { passive: true })
const loop = () => { rx += (cx - rx) * 0.18; ry += (cy - ry) * 0.18; dot.style.transform = `translate(${cx}px,${cy}px) translate(-50%,-50%)`; ring.style.transform = `translate(${rx}px,${ry}px) translate(-50%,-50%)`; raf = requestAnimationFrame(loop) }
loop()
const big = () => ring.classList.add('big'), small = () => ring.classList.remove('big')
const links = [...document.querySelectorAll('a,button')]
links.forEach(a => { a.addEventListener('pointerenter', big); a.addEventListener('pointerleave', small) })
cleanups.push(() => { cancelAnimationFrame(raf); removeEventListener('pointermove', mv); links.forEach(a => { a.removeEventListener('pointerenter', big); a.removeEventListener('pointerleave', small) }) })
}
}
// smooth anchors
const anchors = [...document.querySelectorAll<HTMLAnchorElement>('nav a[href^="#"]')]
const click = (e: Event) => { const a = e.currentTarget as HTMLAnchorElement; const t = document.querySelector(a.getAttribute('href')!); if (t) { e.preventDefault(); t.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth' }) } }
anchors.forEach(a => a.addEventListener('click', click))
cleanups.push(() => anchors.forEach(a => a.removeEventListener('click', click)))
return () => cleanups.forEach(f => f())
}, [])
}
+799
View File
@@ -0,0 +1,799 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pietro Quintavalle — Bare-metal systems and Linux infrastructure</title>
<meta name="description" content="Bare-metal deployments, server rack integration and low-level Linux systems diagnostics by Pietro Quintavalle, Ticino, Switzerland.">
<meta name="author" content="Pietro Quintavalle">
<meta name="theme-color" content="#eef0f3">
<meta property="og:title" content="Pietro Quintavalle — Bare-metal systems and Linux infrastructure">
<meta property="og:description" content="Bare-metal deployments, server rack integration and low-level Linux systems diagnostics.">
<meta property="og:site_name" content="Pietro Quintavalle">
<meta property="og:type" content="website">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23141821'/%3E%3Ctext x='32' y='43' text-anchor='middle' font-family='Arial Black,Arial' font-weight='900' font-size='30' fill='%23eef0f3'%3EPQ%3C/text%3E%3C/svg%3E">
<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=Syne:wght@500;700;800&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#eef0f3; --bg-2:#e3e6eb; --fg:#141821; --muted:#5b6472;
--amber:#c86a00; --ice:#2f5fd6; --line:rgba(20,24,33,.14);
--display:'Syne',"Helvetica Neue",Arial,sans-serif;
--body:'IBM Plex Sans',"Segoe UI",system-ui,sans-serif;
}
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){ --bg:#eef0f3; --bg-2:#e3e6eb; --fg:#141821; --muted:#5b6472; } }
:root[data-theme="dark"]{ --bg:#eef0f3; --bg-2:#e3e6eb; --fg:#141821; --muted:#5b6472; }
:root[data-theme="light"]{ --bg:#eef0f3; --fg:#141821; }
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:auto}
body{background:var(--bg);color:var(--fg);font-family:var(--body);font-size:17px;line-height:1.55;overflow-x:hidden;-webkit-font-smoothing:antialiased;cursor:none}
@media (pointer:coarse){ body{cursor:auto} }
a{color:inherit;text-decoration:none}
::selection{background:var(--amber);color:#fff}
#scene{position:fixed;inset:0;z-index:0;display:block}
.grain{position:fixed;inset:-100%;z-index:1;pointer-events:none;opacity:.05;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
animation:grain 1.2s steps(6) infinite}
@keyframes grain{0%{transform:translate(0,0)}20%{transform:translate(-3%,2%)}40%{transform:translate(2%,-4%)}60%{transform:translate(-4%,-1%)}80%{transform:translate(3%,3%)}100%{transform:translate(0,0)}}
.vignette{position:fixed;inset:0;z-index:1;pointer-events:none;background:radial-gradient(ellipse 80% 70% at 50% 45%,transparent 45%,rgba(238,240,243,.7) 100%)}
/* cursor */
.cur,.cur-ring{position:fixed;top:0;left:0;z-index:50;pointer-events:none;border-radius:50%;transform:translate(-50%,-50%);mix-blend-mode:difference}
.cur{width:6px;height:6px;background:#fff}
.cur-ring{width:34px;height:34px;border:1px solid #fff;transition:width .25s,height .25s,opacity .25s}
.cur-ring.big{width:64px;height:64px;opacity:.6}
@media (pointer:coarse){ .cur,.cur-ring{display:none} }
/* nav */
nav{position:fixed;top:0;left:0;right:0;z-index:20;display:flex;justify-content:space-between;align-items:center;padding:22px clamp(20px,4vw,56px);font-size:14px;font-weight:500;mix-blend-mode:difference}
nav .brand{font-family:var(--display);font-weight:800;letter-spacing:-.02em;font-size:17px}
nav ul{display:flex;gap:26px;list-style:none}
nav a{position:relative;padding:4px 0}
nav a::after{content:"";position:absolute;left:0;right:100%;bottom:0;height:1px;background:#fff;transition:right .3s cubic-bezier(.6,0,.2,1)}
nav a:hover::after,nav a:focus-visible::after{right:0}
@media (max-width:720px){ nav ul{display:none} }
.progress{position:fixed;top:0;left:0;height:2px;width:100%;transform-origin:0 0;transform:scaleX(0);background:var(--amber);z-index:21}
/* shape label */
.shape-label{position:fixed;left:clamp(20px,4vw,56px);bottom:26px;z-index:20;font-size:13px;color:var(--muted);display:flex;align-items:center;gap:12px;pointer-events:none}
.shape-label i{width:6px;height:6px;border-radius:50%;background:var(--amber);box-shadow:0 0 10px rgba(200,106,0,.6);animation:blink 2.4s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
.shape-label span{display:inline-block;transition:opacity .4s,transform .4s}
.shape-label span.swap{opacity:0;transform:translateY(6px)}
/* layout */
main{position:relative;z-index:2}
section{min-height:100vh;padding:20vh clamp(20px,6vw,120px) 12vh;display:flex;flex-direction:column;justify-content:center}
.wrap{max-width:1180px;width:100%;margin:0 auto;position:relative}
section:not(.hero):not(.contact) .wrap::before{content:"";position:absolute;inset:-60px -80px;z-index:-1;pointer-events:none;
background:radial-gradient(ellipse 70% 60% at 38% 50%,rgba(238,240,243,.94) 35%,rgba(238,240,243,.75) 60%,rgba(238,240,243,0) 85%)}
h1,h2,h3{font-family:var(--display);letter-spacing:-.03em;line-height:.95}
h2{font-size:clamp(38px,6vw,84px);font-weight:800;margin-bottom:.5em;max-width:14ch}
h3{font-size:clamp(20px,2.2vw,28px);font-weight:700;letter-spacing:-.02em;line-height:1.15}
p.lead{font-size:clamp(18px,1.5vw,22px);max-width:56ch;color:var(--fg)}
p.dim{color:var(--muted);max-width:60ch}
/* reveal: opacity only, one quiet fade */
.rv{opacity:0;transition:opacity 1s ease}
.rv.in{opacity:1}
/* hero */
.hero{min-height:100vh;padding-top:24vh;justify-content:flex-start;pointer-events:none}
.hero .wrap>*{pointer-events:auto}
.intro{font-size:15px;color:var(--muted);margin-bottom:2.2em;max-width:44ch}
.name{font-size:clamp(56px,12.5vw,190px);font-weight:800;line-height:.86;letter-spacing:-.05em;overflow:hidden}
.name .ln{display:block;overflow:hidden;padding-bottom:.06em}
.name .ch{display:inline-block;transform:translateY(115%) rotate(6deg);opacity:0;transition:transform 1.1s cubic-bezier(.16,1,.3,1),opacity .6s;transform-origin:0 100%}
.name.on .ch{transform:none;opacity:1}
.tag{display:flex;flex-wrap:wrap;gap:16px 40px;align-items:flex-end;margin-top:6vh}
.tag p{font-size:clamp(18px,1.7vw,24px);max-width:36ch;font-weight:500}
.tag p em{font-style:normal;color:var(--amber)}
.hint{margin-top:9vh;font-size:13px;color:var(--muted);display:flex;align-items:center;gap:14px}
.hint b{display:block;width:1px;height:46px;background:linear-gradient(var(--fg),transparent);animation:drop 1.8s cubic-bezier(.6,0,.2,1) infinite;transform-origin:top}
@keyframes drop{0%{transform:scaleY(0);opacity:0}40%{opacity:1}100%{transform:scaleY(1);opacity:0}}
/* facts row */
.facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:24px 40px;margin-top:9vh;border-top:1px solid var(--line);padding-top:26px;max-width:900px}
.facts div{display:flex;flex-direction:column;gap:4px}
.facts b{font-family:var(--display);font-size:clamp(30px,3vw,44px);font-weight:700;letter-spacing:-.03em;line-height:1}
.facts small{font-size:13px;color:var(--muted)}
/* qualities: ruled list, not cards */
.qual{display:grid;grid-template-columns:1fr 1fr;gap:0 60px;border-top:1px solid var(--line)}
.qual article{padding:34px 0 36px;border-bottom:1px solid var(--line)}
.qual article:nth-child(-n+2){border-top:0}
.qual h3{margin-bottom:12px;max-width:16ch}
.qual p{color:var(--muted);max-width:40ch}
@media (max-width:760px){ .qual{grid-template-columns:1fr} }
/* timeline */
.tl{position:relative;padding-left:44px;max-width:860px}
.tl::before{content:"";position:absolute;left:8px;top:0;bottom:0;width:1px;background:var(--line)}
.tl .beam{position:absolute;left:8px;top:0;width:1px;height:100%;background:linear-gradient(var(--amber),var(--ice));transform:scaleY(0);transform-origin:top;transition:transform 1.6s cubic-bezier(.6,0,.2,1)}
.tl.in .beam{transform:scaleY(1)}
.job{position:relative;padding:0 0 64px}
.job::before{content:"";position:absolute;left:-40px;top:10px;width:9px;height:9px;border-radius:50%;background:var(--bg);border:1px solid var(--fg)}
.job time{display:block;font-size:14px;color:var(--muted);margin-bottom:8px}
.job h3{margin-bottom:6px}
.job .org{font-size:15px;color:var(--amber);margin-bottom:16px;display:block}
.job ul{list-style:none;display:grid;gap:10px;max-width:60ch;color:var(--fg)}
.job li{padding-left:18px;position:relative;color:#2b3140}
.job li::before{content:"";position:absolute;left:0;top:.65em;width:8px;height:1px;background:var(--muted)}
/* oss */
.oss{display:grid;gap:0;border-top:1px solid var(--line);max-width:960px}
.oss a{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:20px 40px;align-items:start;padding:32px 0;border-bottom:1px solid var(--line);transition:padding-left .4s cubic-bezier(.16,1,.3,1)}
.oss a:hover,.oss a:focus-visible{padding-left:14px}
.oss .repo{font-size:14px;color:var(--ice);margin-bottom:8px;display:block}
.oss .what{color:var(--muted);max-width:56ch}
.oss .ref{font-family:var(--display);font-weight:700;font-size:15px;color:var(--fg);white-space:nowrap;padding-top:2px;opacity:.7}
@media (max-width:640px){ .oss a{grid-template-columns:1fr} }
/* projects: staggered, not a card grid */
.proj{display:grid;grid-template-columns:repeat(12,1fr);gap:40px 24px}
.proj article{grid-column:span 5;padding:28px 0 0;border-top:1px solid var(--line);position:relative;transform-style:preserve-3d;transition:transform .5s cubic-bezier(.16,1,.3,1)}
.proj article:nth-child(2){grid-column:7/span 6;margin-top:80px}
.proj article:nth-child(3){grid-column:2/span 5;margin-top:-30px}
.proj article:nth-child(4){grid-column:8/span 5;margin-top:20px}
.proj article::after{content:"";position:absolute;left:0;top:-1px;width:0;height:1px;background:var(--amber);transition:width .6s cubic-bezier(.16,1,.3,1)}
.proj article:hover::after{width:100%}
.proj .stack{font-size:13px;color:var(--muted);margin:14px 0 0;line-height:1.8}
.proj p{color:#2b3140;max-width:44ch;margin-top:10px}
.proj .star{color:var(--amber);font-size:14px}
@media (max-width:860px){ .proj article,.proj article:nth-child(n){grid-column:1/-1;margin-top:0} }
/* skills: two counter-rotating marquees */
.mq{overflow:hidden;white-space:nowrap;padding:18px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin-top:-1px;font-family:var(--display);font-weight:700;font-size:clamp(22px,3.2vw,44px);letter-spacing:-.02em}
.mq div{display:inline-block;animation:mq 60s linear infinite}
.mq.rev div{animation-direction:reverse;color:var(--muted)}
.mq span{padding:0 .55em}
.mq span::after{content:"/";color:var(--amber);padding-left:1.1em;font-weight:500}
@keyframes mq{to{transform:translateX(-50%)}}
.skills-note{margin-top:40px;color:var(--muted);max-width:60ch}
/* education */
.edu{display:grid;grid-template-columns:1fr 1fr;gap:40px 60px;max-width:960px}
.edu dl{display:grid;gap:22px}
.edu dt{font-weight:600}
.edu dd{color:var(--muted);font-size:15px}
.edu .langs{display:flex;flex-wrap:wrap;gap:10px 22px;font-size:15px;color:var(--muted)}
.edu .langs b{color:var(--fg);font-weight:500}
@media (max-width:760px){ .edu{grid-template-columns:1fr} }
/* contact */
.contact{min-height:100vh;justify-content:center;text-align:left}
.contact h2{font-size:clamp(44px,8.5vw,140px);max-width:none;line-height:.9}
.mail{display:inline-block;font-family:var(--display);font-weight:700;font-size:clamp(20px,2.6vw,34px);letter-spacing:-.02em;margin-top:20px;position:relative;padding:8px 0}
.mail::after{content:"";position:absolute;left:0;bottom:0;height:2px;width:100%;background:var(--amber);transform:scaleX(.18);transform-origin:left;transition:transform .5s cubic-bezier(.16,1,.3,1)}
.mail:hover::after,.mail:focus-visible::after{transform:scaleX(1)}
.links{display:flex;flex-wrap:wrap;gap:14px;margin-top:48px}
.links a{padding:14px 22px;border:1px solid var(--line);border-radius:999px;font-size:15px;font-weight:500;transition:border-color .3s,background .3s;will-change:transform}
.links a:hover,.links a:focus-visible{border-color:var(--fg);background:rgba(20,24,33,.05)}
footer{padding:40px clamp(20px,6vw,120px);font-size:13px;color:var(--muted);display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px;position:relative;z-index:2}
:focus-visible{outline:2px solid var(--amber);outline-offset:4px}
@media (prefers-reduced-motion:reduce){
.grain,.hint b,.shape-label i,.mq div{animation:none}
.name .ch{transition:none;transform:none;opacity:1}
.rv{transition:none;opacity:1}
}
</style>
</head>
<body>
<canvas id="scene" aria-hidden="true"></canvas>
<div class="grain" aria-hidden="true"></div>
<div class="vignette" aria-hidden="true"></div>
<div class="cur" aria-hidden="true"></div><div class="cur-ring" aria-hidden="true"></div>
<div class="progress" aria-hidden="true"></div>
<nav>
<a class="brand" href="#top">PQ</a>
<ul>
<li><a href="#work">Work</a></li>
<li><a href="#oss">Open source</a></li>
<li><a href="#projects">Projects</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
<div class="shape-label" aria-hidden="true"><i></i><span id="shapeName">Rack: four production servers, bare metal</span></div>
<main id="top">
<section class="hero">
<div class="wrap">
<p class="intro rv">Monte Carasso, Ticino. Six years at the bench, five years on Linux, four servers in production. Looking for datacenter operations, hardware integration or Linux systems support, on-site or hybrid.</p>
<h1 class="name" id="name"><span class="ln">Pietro</span><span class="ln">Quintavalle</span></h1>
<div class="tag rv">
<p>I build infrastructure from the <em>solder joint</em> up. Component-level diagnostics, ZFS storage, zero-trust networks, and the occasional driver written from a packet dump.</p>
</div>
<div class="facts rv">
<div><b data-count="6">0</b><small>years assembling and repairing hardware</small></div>
<div><b data-count="25">0</b><small>systems built for private clients</small></div>
<div><b data-count="12">0</b><small>active users on self-hosted infrastructure</small></div>
<div><b data-count="30" data-prefix="<">0</b><small>minutes to rebuild a cluster from bare metal</small></div>
</div>
<div class="hint rv"><b></b>Scroll. The particles follow.</div>
</div>
</section>
<section id="qualities">
<div class="wrap">
<h2 class="rv">What you get when you hire me</h2>
<div class="qual rv">
<article>
<h3>Diagnoses at the component, not the ticket</h3>
<p>Thermal bench-testing, firmware and BIOS flashing, soldering and harness wiring. When a node fails I find the part, not the symptom.</p>
</article>
<article>
<h3>Treats every host as disposable</h3>
<p>Ansible playbooks and Docker Compose rebuild the whole environment from a blank disk in under thirty minutes. Nothing important lives only in one place.</p>
</article>
<article>
<h3>Zero exposed WAN ports, by default</h3>
<p>Cloudflare Tunnels, WireGuard and Tailscale meshes, SSO with 2FA, host firewalls and intrusion detection on every node. Security is part of the build, not a later phase.</p>
</article>
<article>
<h3>Reads the protocol when there is no documentation</h3>
<p>Reverse-engineered a vendor USB HID protocol from packet captures and shipped a working Linux driver upstream. Undocumented hardware is a puzzle, not a blocker.</p>
</article>
</div>
</div>
</section>
<section id="work">
<div class="wrap">
<h2 class="rv">Experience</h2>
<div class="tl rv" id="tl"><div class="beam"></div>
<div class="job" data-shape="cluster" data-label="DEC Energy: two nodes, one GPU, one HBA">
<time>2026 to now</time>
<h3>Infrastructure and hardware consultant</h3>
<span class="org">DEC Energy, USI startup, freelance</span>
<ul>
<li>Designed, procured and deployed an on-premise two-node Kubernetes cluster from parts to production: local LLM inference, web hosting, accounting and Gitea/GitLab source control.</li>
<li>Tuned fan curves and ran thermal bench stress tests so the nodes stay quiet in an open office under full inference load.</li>
<li>Specified and assembled the engineering team's workstations and remain on-call for support.</li>
</ul>
</div>
<div class="job">
<time>January 2020 to now</time>
<h3>IT support technician</h3>
<span class="org">Private clients across Ticino, freelance</span>
<ul>
<li>Built, configured and repaired more than 25 desktop and laptop systems: sourcing, assembly, Windows and Linux deployment, driver debugging, ongoing maintenance.</li>
<li>Remote and on-site support for hardware failures, software conflicts and local networking faults, with repair logs kept to stop issues recurring.</li>
</ul>
</div>
<div class="job" data-shape="homelab" data-label="Homelab as code: nine bays, twelve users, no open ports">
<time>January 2020 to now</time>
<h3>Infrastructure engineer, self-directed</h3>
<span class="org">Four bare-metal servers serving 12+ users</span>
<ul>
<li>UNRAID, Proxmox VE 8, TrueNAS, Docker and LXC running production workloads without any managed hosting.</li>
<li>Delivered a turnkey rack for a client: cabinet, chassis, structured cabling, TrueNAS with ZFS RAID 10, per-user ACLs and automated offsite snapshots with under thirty minutes of recovery time.</li>
<li>Hardened every edge: no open WAN ports, SSO and 2FA, intrusion detection, host-level firewalls.</li>
</ul>
</div>
</div>
</div>
</section>
<section id="oss">
<div class="wrap">
<h2 class="rv">Open source, close to the metal</h2>
<p class="dim rv" style="margin-bottom:40px">Upstream contributions to projects that sit between hardware and the kernel. Hover a row and the sculpture becomes the work.</p>
<div class="oss rv">
<a href="https://github.com/libratbag/libratbag/pull/1873" target="_blank" rel="noopener" data-shape="hid" data-label="HID report: the bytes that became a driver">
<div><span class="repo">libratbag/libratbag</span><h3>A complete USB HID device driver, written from a packet capture</h3><p class="what">Sniffed the vendor's raw HID reports in Wireshark, decoded the byte layout for DPI profiles, polling rates and on-board memory, then wrote a complete C driver that was merged upstream.</p></div>
<span class="ref">PR #1873</span>
</a>
<a href="https://github.com/ValveSoftware/steam-for-linux/issues/13248" target="_blank" rel="noopener">
<div><span class="repo">ValveSoftware/steam-for-linux, Vysp3r/ProtonPlus</span><h3>Runtime installation pipeline fault, diagnosed and fixed upstream</h3><p class="what">Traced execution crashes in the Steam Linux Runtime to unwritten manifest files, documented a depot recovery workaround and contributed code to automate runtime integrity checks.</p></div>
<span class="ref">Issues #13248, #1232</span>
</a>
<a href="https://github.com/Recol/DLSS-Updater/pull/283" target="_blank" rel="noopener">
<div><span class="repo">Recol/DLSS-Updater</span><h3>Multi-vendor upscaler library detection</h3><p class="what">Replaced brittle static tables with bounded manifest parsing so NVIDIA DLSS, AMD FSR and Intel XeSS libraries survive DLL upgrades; also fixed UI state desynchronisation.</p></div>
<span class="ref">PRs #283, #252, #256</span>
</a>
</div>
</div>
</section>
<section id="projects">
<div class="wrap">
<h2 class="rv">Selected projects</h2>
<div class="proj rv">
<article data-tilt data-shape="homelab" data-label="Homelab as code: nine bays, twelve users, no open ports">
<h3>Homelab as code</h3>
<p>Four production hosts rebuilt from bare metal by Ansible in under thirty minutes. Twelve-plus daily users, no exposed WAN ports: Cloudflare Tunnels, WireGuard, SSO with 2FA and Fail2Ban on every node.</p>
<div class="stack">Ansible, Docker Compose, UNRAID, Grafana, Prometheus</div>
</article>
<article data-tilt data-shape="nas" data-label="Client rack: the cabinet that opens this page">
<h3>Turnkey TrueNAS rack</h3>
<p>A 19-inch cabinet for a private client: patch panels, storage chassis and nine drive bays, every cable run dressed and labelled. TrueNAS on top with ZFS RAID 10, per-user ACLs and offsite snapshots for a sub-30-minute recovery target.</p>
<div class="stack">TrueNAS, ZFS, rack infrastructure, structured cabling</div>
</article>
<article data-tilt>
<h3>ObsidianSetup <span class="star">26 stars on GitHub</span></h3>
<p>Cross-platform provisioning utility for environment configuration and plugin management, adopted by other users.</p>
<div class="stack">PowerShell</div>
</article>
<article data-tilt data-shape="city" data-label="City simulator: twelve people, one Spring Boot backend">
<h3>City simulator, USI group project</h3>
<p>Coordinated a twelve-person Scrum team building a full-stack city simulation. Owned the Spring Boot backend and its integration with the web frontend.</p>
<div class="stack">Java, Spring Boot, JavaScript, HTML, CSS</div>
</article>
</div>
</div>
</section>
<section id="skills">
<div class="wrap">
<h2 class="rv">Tools I reach for</h2>
</div>
<div class="mq rv"><div id="mq1"></div></div>
<div class="mq rev rv"><div id="mq2"></div></div>
<div class="wrap">
<p class="skills-note rv">Also on the bench: Rust, C, C++, Python and SQL for diagnostics, and a self-directed track on local LLM deployment and quantisation on bare-metal hardware with llama.cpp and Claude Code.</p>
</div>
</section>
<section id="education">
<div class="wrap">
<h2 class="rv">Education and languages</h2>
<div class="edu rv">
<dl>
<div><dt>BSc Economics, English stream</dt><dd>Università della Svizzera italiana, Lugano, since September 2024. Microeconomics, game theory, quantitative finance, statistics, financial modelling.</dd></div>
<div><dt>Computer science coursework, two full years</dt><dd>USI, 2022 to 2024. Algorithms and data structures, object-oriented systems in Java and C++, software architecture, discrete logic.</dd></div>
<div><dt>Maturità Cantonale and Federal VET Diploma, AFC Economics</dt><dd>Scuola Cantonale di Commercio, Bellinzona, 2018 to 2022. Maturità 4.5/6, AFC 5/6.</dd></div>
</dl>
<div>
<dl>
<div><dt>Certifications</dt><dd>Google IT Support Professional Certificate. Google Data Analytics Professional Certificate. Microsoft Foundations of IT Systems, Networking and Data Protection.</dd></div>
<div><dt>Languages</dt><dd class="langs"><span><b>Italian</b> native</span><span><b>English</b> C2</span><span><b>German</b> B1–B2</span><span><b>Spanish</b> A1</span><span><b>Norwegian</b> A1</span></dd></div>
<div><dt>Status</dt><dd>Italian national, Swiss Permit C, born 2001.</dd></div>
</dl>
</div>
</div>
</div>
</section>
<section id="contact" class="contact">
<div class="wrap">
<h2 class="rv">Need someone who can rack it, image it, and keep it up?</h2>
<a class="mail rv" href="mailto:0pietroquintavalle0@gmail.com">0pietroquintavalle0@gmail.com</a>
<div class="links rv">
<a data-mag href="tel:+41799068253">+41 79 906 82 53</a>
<a data-mag href="https://github.com/Quinta0" target="_blank" rel="noopener">GitHub</a>
<a data-mag href="https://linkedin.com/in/pietro-quintavalle" target="_blank" rel="noopener">LinkedIn</a>
<a data-mag href="https://quinta0.github.io/whoami" target="_blank" rel="noopener">whoami</a>
</div>
</div>
</section>
</main>
<footer><span>Pietro Quintavalle, Ticino, Switzerland</span><span>Built with Three.js, one canvas, fifteen thousand particles.</span></footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
'use strict';
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const coarse = matchMedia('(pointer: coarse)').matches;
const isMobile = coarse || innerWidth < 760;
/* ---------- Three.js particle sculpture ---------- */
const canvas = document.getElementById('scene');
let renderer;
try { renderer = new THREE.WebGLRenderer({canvas, antialias:false, alpha:true, powerPreference:'high-performance'}); }
catch(e){ canvas.style.display='none'; }
const COUNT = isMobile ? 6000 : 15000;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(42, innerWidth/innerHeight, 0.1, 100);
camera.position.set(0, 0, 18);
const group = new THREE.Group();
scene.add(group);
const rnd = (a=1,b=0)=>Math.random()*(a-b)+b;
/* Shape 0 — a server rack: stacked chassis outlines, drive bays, cable loom */
function shapeRack(n){
const a = new Float32Array(n*3);
const W=4.6, D=3.2, units=9, H=0.72, y0=-(units*H)/2;
for(let i=0;i<n;i++){
let x,y,z; const r=Math.random();
if(r<0.42){ // horizontal chassis edges
const u=Math.floor(rnd(units+1)), edge=Math.floor(rnd(4)), t=rnd(1);
y=y0+u*H;
if(edge===0){x=-W/2+t*W;z=D/2}else if(edge===1){x=-W/2+t*W;z=-D/2}else if(edge===2){x=W/2;z=-D/2+t*D}else{x=-W/2;z=-D/2+t*D}
} else if(r<0.55){ // vertical rails
const c=Math.floor(rnd(4)); x=(c%2?1:-1)*W/2; z=(c<2?1:-1)*D/2; y=y0+rnd(units*H);
} else if(r<0.88){ // front face drive bays, dense
x=rnd(W/2-0.15,-W/2+0.15); const u=Math.floor(rnd(units)); const bay=Math.floor(rnd(3));
y=y0+u*H+0.12+bay*0.2+rnd(0.06); z=D/2+0.02;
} else { // rear cable loom drooping
const t=rnd(1); x=rnd(0.6,-0.6)+Math.sin(t*9+i)*0.25; y=y0+t*units*H; z=-D/2-0.35-Math.sin(t*3.1)*0.3;
}
a[i*3]=x;a[i*3+1]=y;a[i*3+2]=z;
}
return a;
}
/* Shape 1 — torus knot: the mesh network */
function shapeKnot(n){
const a=new Float32Array(n*3), p=2,q=3, R=3.0, r=1.35, tube=0.55;
const P=t=>{const c=R+r*Math.cos(q*t);return [c*Math.cos(p*t),c*Math.sin(p*t),r*Math.sin(q*t)];};
for(let i=0;i<n;i++){
const t=rnd(Math.PI*2), c=P(t), c2=P(t+0.001);
let tx=c2[0]-c[0],ty=c2[1]-c[1],tz=c2[2]-c[2]; const tl=Math.hypot(tx,ty,tz); tx/=tl;ty/=tl;tz/=tl;
// normal ~ toward the ring centre, then binormal = T x N
let nx=-c[0],ny=-c[1],nz=0; const dn=tx*nx+ty*ny; nx-=tx*dn;ny-=ty*dn;nz-=tz*dn; const nl=Math.hypot(nx,ny,nz)||1; nx/=nl;ny/=nl;nz/=nl;
const bx=ty*nz-tz*ny, by=tz*nx-tx*nz, bz=tx*ny-ty*nx;
const ang=rnd(Math.PI*2), rad=Math.random()<0.7?tube:tube*Math.sqrt(Math.random());
const ca=Math.cos(ang)*rad, sa=Math.sin(ang)*rad;
a[i*3]=c[0]+nx*ca+bx*sa; a[i*3+1]=c[1]+ny*ca+by*sa; a[i*3+2]=c[2]+nz*ca+bz*sa;
}
return a;
}
/* Shape 2 — globe with latitude bands + a defensive shell: the zero-trust perimeter */
function shapeGlobe(n){
const a=new Float32Array(n*3);
for(let i=0;i<n;i++){
const r=Math.random();
let R=4.2, theta, phi;
if(r<0.6){ // lat/long grid
const useLat=Math.random()<0.5;
if(useLat){ phi=Math.acos(1-2*(Math.floor(rnd(13))/12)); theta=rnd(Math.PI*2); }
else { theta=Math.floor(rnd(24))*(Math.PI/12); phi=Math.acos(1-2*Math.random()); }
} else if(r<0.85){ phi=Math.acos(1-2*Math.random()); theta=rnd(Math.PI*2); R=4.2+rnd(0.05); }
else { phi=Math.acos(1-2*Math.random()); theta=rnd(Math.PI*2); R=5.6+rnd(0.15); } // outer shell
a[i*3]=R*Math.sin(phi)*Math.cos(theta); a[i*3+1]=R*Math.cos(phi); a[i*3+2]=R*Math.sin(phi)*Math.sin(theta);
}
return a;
}
/* Canvas sampler: anything drawn white on black becomes particles */
function sampleCanvas(n,w,h,draw,S,zSpread){
const c=document.createElement('canvas'); c.width=w; c.height=h;
const ctx=c.getContext('2d'); ctx.fillStyle='#000'; ctx.fillRect(0,0,w,h); ctx.fillStyle='#fff'; ctx.strokeStyle='#fff';
draw(ctx);
const img=ctx.getImageData(0,0,w,h).data, pts=[];
for(let y=0;y<h;y+=2)for(let x=0;x<w;x+=2){ if(img[(y*w+x)*4]>128) pts.push(x,y); }
const a=new Float32Array(n*3);
for(let i=0;i<n;i++){
const k=Math.floor(rnd(pts.length/2))*2;
a[i*3]=(pts[k]-w/2)*S+rnd(0.04,-0.04); a[i*3+1]=-(pts[k+1]-h/2)*S+rnd(0.04,-0.04);
a[i*3+2]=rnd(zSpread,-zSpread)*(Math.random()<0.15?1:0.15);
}
return a;
}
/* Shape 3 — the initials */
function shapeText(n){
return sampleCanvas(n,720,360,ctx=>{ ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.font='800 300px Syne, "Helvetica Neue", Arial, sans-serif'; ctx.fillText('PQ',360,194); },0.0215,0.9);
}
/* Primitive builder for the project dioramas: boxes as edges or solid fill, plus lines */
function fromPrims(n,prims,scale){
const tot=prims.reduce((t,p)=>t+p.w,0), a=new Float32Array(n*3);
for(let i=0;i<n;i++){
let r=Math.random()*tot, pr=prims[0]; for(const q of prims){ pr=q; if(r<q.w)break; r-=q.w; }
let x,y,z;
if(pr.t==='line'){ const u=rnd(1); x=pr.a[0]+(pr.b[0]-pr.a[0])*u; y=pr.a[1]+(pr.b[1]-pr.a[1])*u; z=pr.a[2]+(pr.b[2]-pr.a[2])*u; }
else {
const [cx,cy,cz]=pr.c,[w,h,d]=pr.s;
if(pr.t==='edges'){ const e=Math.floor(rnd(12)), u=rnd(1)-0.5, s1=(e&1)?0.5:-0.5, s2=(e&2)?0.5:-0.5;
if(e<4){x=u*w;y=s1*h;z=s2*d} else if(e<8){x=s1*w;y=u*h;z=s2*d} else {x=s1*w;y=s2*h;z=u*d} }
else { x=(rnd(1)-0.5)*w; y=(rnd(1)-0.5)*h; z=(rnd(1)-0.5)*d; }
x+=cx;y+=cy;z+=cz;
}
a[i*3]=x*scale;a[i*3+1]=y*scale;a[i*3+2]=z*scale;
}
return a;
}
const hoverShapes={
homelab(n){ // the homelab chassis: nine bays, copper strip, grille top
const pr=[{t:'edges',c:[0,0,0],s:[2.3,2.4,2.6],w:3},{t:'fill',c:[0,1.16,0],s:[2.15,0.01,2.45],w:0.9},{t:'fill',c:[0,0.25,1.32],s:[2.3,0.22,0.06],w:0.9}];
const bw=0.17,g=0.02,x0=-(9*bw+8*g)/2+bw/2;
for(let i=0;i<9;i++){ const x=x0+i*(bw+g); pr.push({t:'edges',c:[x,-0.43,1.3],s:[bw,1.05,0.18],w:0.35}); pr.push({t:'fill',c:[x,-0.43,1.12],s:[bw-0.04,0.9,0.3],w:0.32}); }
return fromPrims(n,pr,2.1);
},
cluster(n){ // DEC Energy: two boards, GPU on one, HBA on the other, bridged
const pr=[];
for(const bx of [-0.95,0.95]){ pr.push({t:'fill',c:[bx,0,0],s:[1.5,0.08,1.8],w:0.7},{t:'edges',c:[bx,0,0],s:[1.5,0.08,1.8],w:1.6},{t:'fill',c:[bx,0.1,-0.25],s:[0.45,0.12,0.45],w:0.5}); }
pr.push({t:'fill',c:[-0.45,0.25,0.1],s:[0.12,0.42,1.2],w:1.0},{t:'fill',c:[0.45,0.22,0.1],s:[0.12,0.35,1.0],w:0.8});
pr.push({t:'line',a:[-0.5,0.12,-0.25],b:[0.5,0.12,-0.25],w:0.5});
for(const bx of [-0.95,0.95]) for(let k=0;k<3;k++) pr.push({t:'line',a:[bx-0.55,0.05,0.5+k*0.2],b:[bx+0.55,0.05,0.5+k*0.2],w:0.12});
return fromPrims(n,pr,2.4);
},
hosts(n){ // four hosts on the floor, one hub, one tunnel out
const pr=[]; const hosts=[[-1.3,-1.2,-1.0],[1.3,-1.2,-1.0],[-1.3,-1.2,1.0],[1.3,-1.2,1.0]];
for(const h of hosts){ pr.push({t:'edges',c:h,s:[1.7,0.5,1.4],w:1},{t:'fill',c:[h[0],h[1],h[2]+0.68],s:[1.5,0.32,0.05],w:0.45},{t:'line',a:[h[0],h[1]+0.25,h[2]],b:[0,0.6,0],w:0.35}); }
pr.push({t:'fill',c:[0,0.6,0],s:[0.45,0.45,0.45],w:0.6},{t:'line',a:[0,0.6,0],b:[0,2.6,0],w:0.5},{t:'edges',c:[0,2.9,0],s:[1.2,0.5,0.9],w:0.5});
return fromPrims(n,pr,1.55);
},
city(n){ // the simulated city: a grid of blocks
const pr=[{t:'edges',c:[0,-0.05,0],s:[9.2,0.02,9.2],w:1.2}]; let seed=7; const rr=()=>{seed=(seed*16807)%2147483647;return seed/2147483647;};
for(let i=0;i<8;i++)for(let j=0;j<8;j++){ const hgt=0.3+rr()*rr()*3.2, x=-3.85+i*1.1, z=-3.85+j*1.1; pr.push({t:'edges',c:[x,hgt/2,z],s:[0.8,hgt,0.8],w:0.35+hgt*0.15}); if(rr()<0.35)pr.push({t:'fill',c:[x,hgt/2,z],s:[0.78,hgt,0.78],w:0.08}); }
return fromPrims(n,pr,0.72);
},
hid(n){ // the libratbag report: header, command, profile, DPI, then upstream
return sampleCanvas(n,1000,420,ctx=>{
ctx.lineWidth=5; ctx.beginPath(); ctx.moveTo(40,40); ctx.lineTo(960,40); ctx.stroke();
ctx.setLineDash([8,10]); ctx.beginPath(); ctx.moveTo(110,40); ctx.lineTo(160,130); ctx.moveTo(420,40); ctx.lineTo(380,130); ctx.stroke(); ctx.setLineDash([]);
const boxes=[[60,120,'0x08'],[180,120,'0x14'],[300,140,'0x01 02'],[440,190,'0x0640'],[630,300,'00 00 …']];
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.font='700 42px Syne, Arial, sans-serif';
for(const [x,w,t] of boxes){ ctx.lineWidth=(t==='0x0640')?10:5; ctx.strokeRect(x,130,w,110); ctx.fillText(t,x+w/2,185); }
ctx.lineWidth=5; ctx.beginPath(); ctx.moveTo(535,240); ctx.lineTo(535,300); ctx.stroke();
ctx.fillRect(360,300,350,70); ctx.fillStyle='#000'; ctx.font='700 30px Syne, Arial, sans-serif'; ctx.fillText('MERGED UPSTREAM',535,336);
ctx.fillStyle='#fff'; ctx.font='500 26px Syne, Arial, sans-serif'; ctx.textAlign='left';
ctx.fillText('REPORT',60,275); ctx.fillText('CMD',180,275); ctx.fillText('PROFILE 2',300,275); ctx.fillText('1600 DPI',440,275);
},0.0142,0.5);
}
};
hoverShapes.nas=shapeRack; // the client rack is the same one that opens the page
const hoverFlat={hid:true};
const shapeCache={};
function getHoverShape(k){ return shapeCache[k]||(shapeCache[k]=hoverShapes[k](COUNT)); }
const noiseGLSL = `
vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}
vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}
float snoise(vec3 v){
const vec2 C=vec2(1.0/6.0,1.0/3.0); const vec4 D=vec4(0.0,0.5,1.0,2.0);
vec3 i=floor(v+dot(v,C.yyy)); vec3 x0=v-i+dot(i,C.xxx);
vec3 g=step(x0.yzx,x0.xyz); vec3 l=1.0-g; vec3 i1=min(g.xyz,l.zxy); vec3 i2=max(g.xyz,l.zxy);
vec3 x1=x0-i1+C.xxx; vec3 x2=x0-i2+C.yyy; vec3 x3=x0-D.yyy;
i=mod289(i);
vec4 p=permute(permute(permute(i.z+vec4(0.0,i1.z,i2.z,1.0))+i.y+vec4(0.0,i1.y,i2.y,1.0))+i.x+vec4(0.0,i1.x,i2.x,1.0));
float n_=0.142857142857; vec3 ns=n_*D.wyz-D.xzx;
vec4 j=p-49.0*floor(p*ns.z*ns.z); vec4 x_=floor(j*ns.z); vec4 y_=floor(j-7.0*x_);
vec4 x=x_*ns.x+ns.yyyy; vec4 y=y_*ns.x+ns.yyyy; vec4 h=1.0-abs(x)-abs(y);
vec4 b0=vec4(x.xy,y.xy); vec4 b1=vec4(x.zw,y.zw);
vec4 s0=floor(b0)*2.0+1.0; vec4 s1=floor(b1)*2.0+1.0; vec4 sh=-step(h,vec4(0.0));
vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy; vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
vec3 p0=vec3(a0.xy,h.x); vec3 p1=vec3(a0.zw,h.y); vec3 p2=vec3(a1.xy,h.z); vec3 p3=vec3(a1.zw,h.w);
vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));
p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;
vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0); m=m*m;
return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
}`;
const vert = noiseGLSL + `
attribute vec3 aP0; attribute vec3 aP1; attribute vec3 aP2; attribute vec3 aP3; attribute vec3 aPH; attribute vec3 aPH2; attribute float aRand;
uniform float uProgress; uniform float uTime; uniform float uPR; uniform vec3 uMouse; uniform float uMouseOn; uniform float uIntro; uniform float uDim; uniform float uHover; uniform float uSwap;
varying float vA; varying float vHeat;
void main(){
float p=clamp(uProgress,0.0,3.0);
float s01=smoothstep(0.0,1.0,p), s12=smoothstep(1.0,2.0,p), s23=smoothstep(2.0,3.0,p);
vec3 pos = aP0*(1.0-s01) + aP1*(s01-s12) + aP2*(s12-s23) + aP3*s23;
float f=fract(p); float tr = (p>=3.0)?0.0:f*(1.0-f)*4.0; // 0 at rest, 1 mid-morph
// hover: the sculpture becomes the project under the cursor
vec3 hp = mix(aPH2, aPH, uSwap);
pos = mix(pos, hp, uHover);
tr = tr*(1.0-uHover) + max(uHover*(1.0-uHover)*4.0, uSwap*(1.0-uSwap)*4.0)*0.7;
vec3 nv = vec3(snoise(pos*0.45+uTime*0.12), snoise(pos*0.45+vec3(13.7)+uTime*0.12), snoise(pos*0.45+vec3(71.2)+uTime*0.12));
pos += nv*(0.07*(1.0-uHover*0.85) + tr*(2.2+aRand*2.0));
// intro: particles fall in from a scattered cloud
vec3 spread = (aRand-0.5)*vec3(30.0,20.0,30.0) + nv*6.0;
pos = mix(spread, pos, uIntro);
// mouse repulsion
vec3 d = pos - uMouse; float dist=length(d);
float rep = smoothstep(3.2,0.0,dist)*uMouseOn;
pos += (d/max(dist,0.001))*rep*1.6;
vec4 mv = modelViewMatrix*vec4(pos,1.0);
gl_Position = projectionMatrix*mv;
float size = (0.8+aRand*1.4)*uPR*(1.0+tr*1.0+rep*1.4);
gl_PointSize = min(size*(200.0/-mv.z), 9.0*uPR);
vA = (0.35+0.55*aRand)*uIntro*uDim*(1.0-tr*0.45);
vHeat = clamp(tr*0.9+rep+aRand*0.25,0.0,1.0);
}`;
const frag = `
uniform vec3 uC1; uniform vec3 uC2; varying float vA; varying float vHeat;
void main(){
vec2 uv=gl_PointCoord-0.5; float d=length(uv); if(d>0.5) discard;
float a=smoothstep(0.5,0.12,d);
vec3 c=mix(uC1,uC2,vHeat);
gl_FragColor=vec4(c, a*vA);
}`;
let uniforms, points;
function buildParticles(){
const g=new THREE.BufferGeometry();
const rand=new Float32Array(COUNT); for(let i=0;i<COUNT;i++)rand[i]=Math.random();
g.setAttribute('position', new THREE.BufferAttribute(shapeRack(COUNT),3));
g.setAttribute('aP0', new THREE.BufferAttribute(shapeRack(COUNT),3));
g.setAttribute('aP1', new THREE.BufferAttribute(shapeKnot(COUNT),3));
g.setAttribute('aP2', new THREE.BufferAttribute(shapeGlobe(COUNT),3));
g.setAttribute('aP3', new THREE.BufferAttribute(shapeText(COUNT),3));
g.setAttribute('aPH', new THREE.BufferAttribute(new Float32Array(COUNT*3),3));
g.setAttribute('aPH2', new THREE.BufferAttribute(new Float32Array(COUNT*3),3));
g.setAttribute('aRand', new THREE.BufferAttribute(rand,1));
uniforms={
uProgress:{value:0}, uTime:{value:0}, uPR:{value:Math.min(devicePixelRatio,2)},
uMouse:{value:new THREE.Vector3(999,999,0)}, uMouseOn:{value:0}, uIntro:{value:reduce?1:0}, uDim:{value:1}, uHover:{value:0}, uSwap:{value:1},
uC1:{value:new THREE.Color('#1f3d9e')}, uC2:{value:new THREE.Color('#d9741a')}
};
const m=new THREE.ShaderMaterial({uniforms,vertexShader:vert,fragmentShader:frag,transparent:true,depthWrite:false,blending:THREE.NormalBlending});
points=new THREE.Points(g,m);
group.add(points);
}
function resize(){
if(!renderer)return;
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight,false);
camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
camera.position.z = innerWidth<760 ? 26 : 18;
}
addEventListener('resize',()=>{resize();updateScroll();});
/* scroll + mouse state */
let hoverT=0, hoverCur=0, swapCur=1, hoverKey=null, hoverLabel=null;
let progT=0, progCur=0, keyEls=null;
let scrollT=0, scrollCur=0, mx=0,my=0, mxCur=0,myCur=0, mouseOn=0, mouseOnCur=0;
function updateScroll(){
const max=document.documentElement.scrollHeight-innerHeight; scrollT=max>0?scrollY/max:0;
if(!keyEls) keyEls=['.hero','#work','#skills','#contact'].map(q=>document.querySelector(q));
const c=keyEls.map(el=>el.offsetTop+el.offsetHeight/2-innerHeight/2), y=scrollY;
let v=3;
if(y<=c[0]) v=0; else for(let i=0;i<3;i++){ if(y<c[i+1]){ const f=(y-c[i])/Math.max(1,c[i+1]-c[i]); v=i+THREE.MathUtils.smoothstep(f,0.45,1); break; } }
progT=v/3;
}
addEventListener('scroll',updateScroll,{passive:true});
addEventListener('pointermove',e=>{ mx=(e.clientX/innerWidth)*2-1; my=-(e.clientY/innerHeight)*2+1; mouseOn=1; },{passive:true});
addEventListener('pointerleave',()=>{mouseOn=0});
document.addEventListener('mouseleave',()=>{mouseOn=0});
const plane=new THREE.Plane(new THREE.Vector3(0,0,1),0), ray=new THREE.Raycaster(), hit=new THREE.Vector3(), ndc=new THREE.Vector2();
const clock=new THREE.Clock();
let introT=0;
function frame(){
requestAnimationFrame(frame);
if(!renderer||!points)return;
const dt=Math.min(clock.getDelta(),0.05);
const t=clock.elapsedTime;
scrollCur+= (scrollT-scrollCur)*0.06; progCur+=(progT-progCur)*0.07;
mxCur+=(mx-mxCur)*0.08; myCur+=(my-myCur)*0.08;
mouseOnCur+=(mouseOn-mouseOnCur)*0.1;
if(!reduce){ introT=Math.min(1,introT+dt*0.55); uniforms.uIntro.value = 1-Math.pow(1-introT,3); }
const prog=progCur*3.0;
uniforms.uProgress.value=prog;
uniforms.uTime.value=t;
// sculpture drifts as you scroll; camera parallax from cursor
group.rotation.y = scrollCur*Math.PI*1.35 + t*0.03;
group.rotation.x = Math.sin(scrollCur*Math.PI)*0.35;
// last shape (initials) faces the camera flat
const face = THREE.MathUtils.smoothstep(prog,2.55,3.0);
group.rotation.y = THREE.MathUtils.lerp(group.rotation.y, Math.round(group.rotation.y/(Math.PI*2))*Math.PI*2, face);
group.rotation.x = THREE.MathUtils.lerp(group.rotation.x, 0, face);
// hover: settle into a three-quarter view (or flat for the packet diagram)
hoverCur += (hoverT-hoverCur)*0.06; swapCur += (1-swapCur)*0.08;
const hEase = hoverCur*hoverCur*(3-2*hoverCur);
uniforms.uHover.value = hEase; uniforms.uSwap.value = swapCur;
if(hoverCur>0.001){
const flat = hoverKey && hoverFlat[hoverKey];
const ty = (flat?0:0.62) + t*0.08, tx = flat?0:0.3;
const near = ty + Math.round((group.rotation.y-ty)/(Math.PI*2))*Math.PI*2;
group.rotation.y = THREE.MathUtils.lerp(group.rotation.y, near, hEase);
group.rotation.x = THREE.MathUtils.lerp(group.rotation.x, tx, hEase);
}
// rack sits slightly right of the name on wide screens
const mid = THREE.MathUtils.smoothstep(prog,0.15,0.6)*(1-THREE.MathUtils.smoothstep(prog,2.5,2.95));
const offX = innerWidth<760 ? 0 : THREE.MathUtils.lerp(5.5, 4.5, THREE.MathUtils.smoothstep(prog,0,0.8))*(1-THREE.MathUtils.smoothstep(prog,2.5,2.95));
const hx = innerWidth<760 ? 0 : 3.6, hz = innerWidth<760 ? 2 : 5.5;
group.position.x += (THREE.MathUtils.lerp(offX,hx,hEase)-group.position.x)*0.05;
group.position.z += (hz*hEase-group.position.z)*0.05;
uniforms.uDim.value = THREE.MathUtils.lerp(1-mid*0.55, 1, hEase);
camera.position.x += (mxCur*0.9-camera.position.x)*0.05;
camera.position.y += (myCur*0.6-camera.position.y)*0.05;
camera.lookAt(0,0,0);
// mouse -> world on z=0 plane -> group local
ndc.set(mxCur,myCur); ray.setFromCamera(ndc,camera);
if(ray.ray.intersectPlane(plane,hit)){ group.worldToLocal(hit); uniforms.uMouse.value.copy(hit); }
uniforms.uMouseOn.value=coarse?0:mouseOnCur;
renderer.render(scene,camera);
}
/* shape label */
const names=['Rack: four production servers, bare metal','Mesh: zero exposed ports, every node peered','Perimeter: SSO, 2FA, host firewalls, intrusion detection','Contact: the whole thing reassembles on request'];
const labelEl=document.getElementById('shapeName'); let labelIdx=0;
function setLabel(txt){ if(labelEl.textContent===txt)return; labelEl.classList.add('swap'); setTimeout(()=>{labelEl.textContent=txt;labelEl.classList.remove('swap')},400); }
setInterval(()=>{ if(hoverT>0){ setLabel(hoverLabel); return; } const i=Math.min(3,Math.round(progCur*3)); if(i!==labelIdx||labelEl.textContent!==names[i]){ labelIdx=i; setLabel(names[i]);} },200);
/* progress bar */
const bar=document.querySelector('.progress');
(function pb(){ bar.style.transform='scaleX('+scrollCur.toFixed(4)+')'; requestAnimationFrame(pb); })();
/* ---------- DOM motion ---------- */
// split hero name into characters
const name=document.getElementById('name');
name.querySelectorAll('.ln').forEach((ln,li)=>{
const txt=ln.textContent; ln.textContent='';
[...txt].forEach((ch,i)=>{ const s=document.createElement('span'); s.className='ch'; s.textContent=ch; s.style.transitionDelay=(0.25+li*0.35+i*0.04)+'s'; ln.appendChild(s); });
});
name.setAttribute('aria-label','Pietro Quintavalle');
// counters
function countUp(el){
const target=+el.dataset.count, prefix=el.dataset.prefix||''; const t0=performance.now(), dur=1600;
(function step(now){ const k=Math.min(1,(now-t0)/dur); const e=1-Math.pow(1-k,3); el.textContent=prefix+Math.round(target*e)+(k===1&&!prefix?'+':''); if(k<1)requestAnimationFrame(step); })(t0);
}
// reveal (opacity only)
const io=new IntersectionObserver(es=>es.forEach(e=>{ if(e.isIntersecting){ e.target.classList.add('in'); io.unobserve(e.target);
if(e.target.classList.contains('facts')) e.target.querySelectorAll('[data-count]').forEach(countUp); } }),{threshold:0.15});
document.querySelectorAll('.rv').forEach(el=>io.observe(el));
// marquees
const rowA=['Proxmox VE','TrueNAS','ZFS','UNRAID','Kubernetes','Docker','LXC','KVM','Ansible','GitHub Actions','Bash','PowerShell','Debian','Arch','NixOS','Windows Server','Active Directory'];
const rowB=['WireGuard','Tailscale','Cloudflare Tunnels','UniFi','VLANs','iptables','Fail2Ban','AdGuard Home','Grafana','Prometheus','Uptime Kuma','Spring Boot','TypeScript','llama.cpp','Rust','C'];
function fill(id,arr){ const el=document.getElementById(id); const h=arr.map(s=>'<span>'+s+'</span>').join(''); el.innerHTML=h+h; }
fill('mq1',rowA); fill('mq2',rowB);
// hover-morph: rows that carry a shape
function hoverIn(el){
const k=el.dataset.shape; if(!points||!hoverShapes[k])return;
if(hoverKey!==k){
const ph=points.geometry.getAttribute('aPH'), ph2=points.geometry.getAttribute('aPH2');
if(hoverKey){ ph2.array.set(ph.array); ph2.needsUpdate=true; swapCur=0; } else { ph2.array.set(getHoverShape(k)); ph2.needsUpdate=true; swapCur=1; }
ph.array.set(getHoverShape(k)); ph.needsUpdate=true; hoverKey=k;
}
hoverLabel=el.dataset.label; hoverT=1;
}
function hoverOut(){ hoverT=0; }
document.querySelectorAll('[data-shape]').forEach(el=>{
el.addEventListener('pointerenter',()=>hoverIn(el));
el.addEventListener('pointerleave',hoverOut);
el.addEventListener('focusin',()=>hoverIn(el)); el.addEventListener('focusout',hoverOut);
});
if(coarse){ // touch: the row nearest the middle of the screen drives the sculpture
const rows=[...document.querySelectorAll('[data-shape]')];
setInterval(()=>{ const mid=innerHeight/2; let best=null,bd=1e9; for(const r of rows){ const b=r.getBoundingClientRect(); if(b.bottom<0||b.top>innerHeight)continue; const d=Math.abs((b.top+b.bottom)/2-mid); if(d<bd){bd=d;best=r;} } if(best&&bd<innerHeight*0.3)hoverIn(best); else hoverOut(); },250);
}
// timeline beam
const tl=document.getElementById('tl'); new IntersectionObserver(es=>es.forEach(e=>{ if(e.isIntersecting){tl.classList.add('in')} }),{threshold:0.2}).observe(tl);
// tilt on projects (pointer-driven only)
if(!coarse&&!reduce){
document.querySelectorAll('[data-tilt]').forEach(el=>{
el.addEventListener('pointermove',e=>{ const r=el.getBoundingClientRect(); const x=(e.clientX-r.left)/r.width-0.5, y=(e.clientY-r.top)/r.height-0.5; el.style.transform='perspective(900px) rotateX('+(-y*6)+'deg) rotateY('+(x*8)+'deg) translateZ(6px)'; });
el.addEventListener('pointerleave',()=>{ el.style.transform=''; });
});
// magnetic links
document.querySelectorAll('[data-mag]').forEach(el=>{
el.addEventListener('pointermove',e=>{ const r=el.getBoundingClientRect(); const x=e.clientX-(r.left+r.width/2), y=e.clientY-(r.top+r.height/2); el.style.transform='translate('+x*0.25+'px,'+y*0.35+'px)'; el.style.transition='transform .15s'; });
el.addEventListener('pointerleave',()=>{ el.style.transition='transform .5s cubic-bezier(.16,1,.3,1)'; el.style.transform=''; });
});
}
// custom cursor
if(!coarse){
const dot=document.querySelector('.cur'), ring=document.querySelector('.cur-ring'); let cx=innerWidth/2,cy=innerHeight/2,rx=cx,ry=cy;
addEventListener('pointermove',e=>{cx=e.clientX;cy=e.clientY;},{passive:true});
(function cl(){ rx+=(cx-rx)*0.18; ry+=(cy-ry)*0.18; dot.style.transform='translate('+cx+'px,'+cy+'px) translate(-50%,-50%)'; ring.style.transform='translate('+rx+'px,'+ry+'px) translate(-50%,-50%)'; requestAnimationFrame(cl); })();
document.querySelectorAll('a,button').forEach(a=>{ a.addEventListener('pointerenter',()=>ring.classList.add('big')); a.addEventListener('pointerleave',()=>ring.classList.remove('big')); });
}
// smooth anchor scroll
document.querySelectorAll('nav a[href^="#"]').forEach(a=>a.addEventListener('click',e=>{ const t=document.querySelector(a.getAttribute('href')); if(t){e.preventDefault(); t.scrollIntoView({behavior:reduce?'auto':'smooth'});} }));
/* boot: wait for the display font so the PQ sampling is right, then go */
function boot(){
if(!renderer)return;
buildParticles(); resize(); updateScroll(); frame();
setTimeout(()=>name.classList.add('on'),150);
}
if(document.fonts&&document.fonts.load){ Promise.race([document.fonts.load('800 300px Syne'),new Promise(r=>setTimeout(r,1500))]).then(boot,boot); } else boot();
})();
</script>
</body>
</html>