Claudio cooked
This commit is contained in:
+799
@@ -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>
|
||||
Reference in New Issue
Block a user