ページ 難しい
Tech Lead Portfolio
Corporate tech lead portfolio with Three.js wireframe hero, vertical career timeline, expertise grid, and open source repo showcase.
Labで開く
MCP
three.js gsap lenis scrolltrigger splittext
ターゲット: JS HTML
コード
/* ─── Reset & Base ─────────────────────────────────────────────────────── */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #ffffff;
--dark: #0d1b2a;
--text: #0d1b2a;
--muted: #6b7a8d;
--accent: #2563eb;
--light: #f0f4ff;
--border: #e2e8f0;
--radius: 12px;
--radius-sm: 6px;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", "Helvetica Neue", Arial,
sans-serif;
--font-mono: "SFMono-Regular", "Consolas", "Liberation Mono", "Menlo", monospace;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
color: inherit;
text-decoration: none;
}
ul {
list-style: none;
}
img {
display: block;
max-width: 100%;
}
/* ─── Shared Section Layout ────────────────────────────────────────────── */
.section {
padding: clamp(5rem, 10vw, 9rem) clamp(1.5rem, 6vw, 7rem);
max-width: 1240px;
margin: 0 auto;
}
.section__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 2rem;
margin-bottom: clamp(3rem, 6vw, 5rem);
padding-bottom: 2rem;
border-bottom: 1px solid var(--border);
}
.section__label-group {
display: flex;
align-items: baseline;
gap: 1rem;
}
.section-index {
font-size: 0.75rem;
font-weight: 600;
color: var(--accent);
font-family: var(--font-mono);
letter-spacing: 0.05em;
}
.section-label {
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.section__summary {
font-size: 0.9rem;
color: var(--muted);
max-width: 30ch;
line-height: 1.6;
text-align: right;
}
/* ─── Hero ──────────────────────────────────────────────────────────────── */
.hero {
position: relative;
min-height: 100svh;
background: var(--dark);
display: flex;
flex-direction: column;
overflow: hidden;
}
.hero__canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
.hero__inner {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
padding: clamp(2rem, 4vw, 4rem) clamp(1.5rem, 6vw, 7rem);
padding-bottom: 5rem;
}
.hero__eyebrow {
display: flex;
align-items: center;
gap: 0.75rem;
padding-top: 1rem;
}
.eyebrow-tag {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
}
.eyebrow-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
}
.eyebrow-avail {
font-size: 0.75rem;
font-weight: 600;
color: var(--accent);
letter-spacing: 0.04em;
}
.hero__name {
font-size: clamp(4rem, 12vw, 10rem);
font-weight: 900;
line-height: 0.9;
letter-spacing: -0.04em;
color: #ffffff;
margin-top: auto;
padding-top: 2rem;
overflow: hidden;
}
.hero__name .line {
overflow: hidden;
display: block;
}
.hero__foot {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 2rem;
flex-wrap: wrap;
margin-top: 3rem;
}
.hero__tagline {
font-size: clamp(1rem, 2vw, 1.2rem);
font-weight: 400;
line-height: 1.6;
color: rgba(255, 255, 255, 0.55);
max-width: 36ch;
}
.hero__ctas {
display: flex;
gap: 0.875rem;
flex-shrink: 0;
}
.btn {
display: inline-flex;
align-items: center;
height: 48px;
padding: 0 1.75rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
letter-spacing: 0.02em;
transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease;
cursor: pointer;
}
.btn--primary {
background: var(--accent);
color: #fff;
}
.btn--primary:hover {
background: #1d4fd8;
transform: translateY(-1px);
}
.btn--ghost {
background: transparent;
color: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn--ghost:hover {
background: rgba(255, 255, 255, 0.06);
color: #fff;
border-color: rgba(255, 255, 255, 0.35);
}
.hero__scroll-indicator {
position: absolute;
bottom: 2.5rem;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
z-index: 2;
}
.scroll-track {
width: 1px;
height: 48px;
background: rgba(255, 255, 255, 0.12);
border-radius: 1px;
overflow: hidden;
position: relative;
}
.scroll-thumb {
position: absolute;
top: -100%;
left: 0;
width: 100%;
height: 50%;
background: var(--accent);
border-radius: 1px;
animation: scrollThumb 1.8s ease-in-out infinite;
}
@keyframes scrollThumb {
0% {
top: -50%;
}
100% {
top: 100%;
}
}
.scroll-label {
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.15em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.3);
}
/* ─── Expertise Section ─────────────────────────────────────────────────── */
.expertise {
background: var(--bg);
}
.expertise__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
@media (max-width: 900px) {
.expertise__grid {
grid-template-columns: 1fr;
}
}
.expertise-card {
background: var(--light);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2.5rem 2rem;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.expertise-card:hover {
transform: translateY(-4px);
box-shadow: 0 16px 48px rgba(37, 99, 235, 0.08);
}
/* CSS geometric icons */
.expertise-card__icon {
width: 48px;
height: 48px;
margin-bottom: 1.5rem;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.icon-backend {
width: 36px;
height: 36px;
border: 2.5px solid var(--accent);
border-radius: 6px;
position: relative;
}
.icon-backend::before {
content: "";
position: absolute;
top: 6px;
left: 6px;
right: 6px;
height: 2px;
background: var(--accent);
border-radius: 1px;
box-shadow: 0 6px 0 var(--accent), 0 12px 0 var(--accent);
}
.icon-platform {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2.5px solid var(--accent);
position: relative;
}
.icon-platform::before {
content: "";
position: absolute;
inset: 6px;
border-radius: 50%;
border: 2.5px solid var(--accent);
}
.icon-platform::after {
content: "";
position: absolute;
width: 8px;
height: 8px;
background: var(--accent);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.icon-leadership {
width: 36px;
height: 36px;
position: relative;
}
.icon-leadership::before {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 28px;
height: 18px;
border: 2.5px solid var(--accent);
border-radius: 14px 14px 6px 6px;
border-bottom: none;
}
.icon-leadership::after {
content: "";
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 14px;
height: 14px;
border: 2.5px solid var(--accent);
border-radius: 50%;
}
.expertise-card__title {
font-size: 1.1rem;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 1.25rem;
color: var(--text);
}
.expertise-card__list {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.expertise-card__list li {
font-size: 0.875rem;
color: var(--muted);
line-height: 1.5;
padding-left: 1rem;
position: relative;
}
.expertise-card__list li::before {
content: "";
position: absolute;
left: 0;
top: 0.55em;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--accent);
}
/* ─── Timeline Section ──────────────────────────────────────────────────── */
.timeline {
background: var(--bg);
padding-top: 0;
}
.timeline__track {
position: relative;
display: flex;
flex-direction: column;
gap: 0;
}
.timeline__line {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 1px;
background: var(--border);
transform: translateX(-50%);
z-index: 0;
}
.timeline-item {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0 2.5rem;
align-items: start;
padding: 2.5rem 0;
position: relative;
z-index: 1;
}
/* Year dot on center line */
.timeline-item::after {
content: "";
position: absolute;
left: 50%;
top: 2.9rem;
transform: translateX(-50%);
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent);
border: 2px solid var(--bg);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
z-index: 2;
}
.timeline-item__year {
font-size: 0.8rem;
font-weight: 700;
font-family: var(--font-mono);
color: var(--accent);
letter-spacing: 0.06em;
padding-top: 0.35rem;
}
.timeline-item__card {
background: var(--light);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.75rem;
}
.timeline-item__meta {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 0.875rem;
}
.timeline-item__role {
font-size: 0.95rem;
font-weight: 700;
color: var(--text);
letter-spacing: -0.01em;
}
.timeline-item__sep {
color: var(--border);
font-weight: 300;
}
.timeline-item__company {
font-size: 0.875rem;
font-weight: 600;
color: var(--accent);
}
.timeline-item__desc {
font-size: 0.875rem;
color: var(--muted);
line-height: 1.7;
}
/* Left-aligned: year left, empty center col, card right */
.timeline-item--left .timeline-item__year {
text-align: right;
grid-column: 1;
grid-row: 1;
}
.timeline-item--left .timeline-item__card {
grid-column: 3;
grid-row: 1;
}
/* Right-aligned: card left, empty center col, year right */
.timeline-item--right .timeline-item__card {
grid-column: 1;
grid-row: 1;
}
.timeline-item--right .timeline-item__year {
grid-column: 3;
grid-row: 1;
padding-top: 0.35rem;
}
@media (max-width: 768px) {
.timeline__line {
left: 1rem;
}
.timeline-item {
grid-template-columns: 1fr;
padding-left: 3rem;
}
.timeline-item::after {
left: 1rem;
transform: translateX(-50%);
}
.timeline-item--left .timeline-item__year,
.timeline-item--right .timeline-item__year {
grid-column: 1;
grid-row: 1;
text-align: left;
font-size: 0.75rem;
margin-bottom: 0.5rem;
}
.timeline-item--left .timeline-item__card,
.timeline-item--right .timeline-item__card {
grid-column: 1;
grid-row: 2;
}
}
/* ─── Open Source Section ───────────────────────────────────────────────── */
.open-source {
background: var(--light);
border-radius: 0;
max-width: unset;
padding-left: 0;
padding-right: 0;
}
.open-source .section__header {
max-width: 1240px;
margin-left: auto;
margin-right: auto;
padding-left: clamp(1.5rem, 6vw, 7rem);
padding-right: clamp(1.5rem, 6vw, 7rem);
}
.repos__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
max-width: 1240px;
margin: 0 auto;
padding: 0 clamp(1.5rem, 6vw, 7rem);
}
@media (max-width: 900px) {
.repos__grid {
grid-template-columns: 1fr;
}
}
.repo-card {
display: flex;
flex-direction: column;
gap: 0.875rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.75rem;
cursor: pointer;
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
color: var(--text);
}
.repo-card:hover {
transform: translateY(-3px);
box-shadow: 0 12px 36px rgba(37, 99, 235, 0.1);
border-color: rgba(37, 99, 235, 0.3);
}
.repo-card__top {
display: flex;
align-items: center;
gap: 0.6rem;
}
.repo-card__icon {
color: var(--muted);
flex-shrink: 0;
display: flex;
align-items: center;
}
.repo-card__name {
font-size: 0.9rem;
font-weight: 700;
font-family: var(--font-mono);
color: var(--accent);
flex: 1;
}
.repo-card__stars {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.78rem;
font-weight: 600;
color: var(--muted);
margin-left: auto;
flex-shrink: 0;
}
.repo-card__stars svg {
color: #f59e0b;
}
.repo-card__desc {
font-size: 0.85rem;
color: var(--muted);
line-height: 1.65;
flex: 1;
}
.repo-card__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-top: auto;
padding-top: 0.75rem;
border-top: 1px solid var(--border);
}
.lang-pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.25rem 0.7rem;
border-radius: 100px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.03em;
}
.lang-pill::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
}
.lang-pill--go {
background: #e0f0ff;
color: #0078d4;
}
.lang-pill--go::before {
background: #0078d4;
}
.lang-pill--ts {
background: #e0e8ff;
color: #3178c6;
}
.lang-pill--ts::before {
background: #3178c6;
}
.lang-pill--rust {
background: #fbe8e0;
color: #b7410e;
}
.lang-pill--rust::before {
background: #b7410e;
}
.repo-card__updated {
font-size: 0.72rem;
color: var(--muted);
}
/* ─── Contact Section ───────────────────────────────────────────────────── */
.contact {
background: var(--dark);
max-width: unset;
padding-top: clamp(5rem, 10vw, 9rem);
padding-bottom: 0;
padding-left: 0;
padding-right: 0;
}
.contact__inner {
max-width: 1240px;
margin: 0 auto;
padding: 0 clamp(1.5rem, 6vw, 7rem);
padding-bottom: clamp(5rem, 10vw, 9rem);
}
.contact__kicker {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 1.5rem;
}
.contact__cta {
font-size: clamp(3rem, 8vw, 7rem);
font-weight: 900;
letter-spacing: -0.04em;
line-height: 0.92;
color: #ffffff;
margin-bottom: 3.5rem;
overflow: hidden;
}
.contact__cta .line {
overflow: hidden;
display: block;
}
.contact__links {
display: flex;
align-items: center;
gap: 3rem;
flex-wrap: wrap;
}
.contact__email {
font-size: 1.1rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
padding-bottom: 2px;
transition: color 0.2s ease, border-color 0.2s ease;
}
.contact__email:hover {
color: #fff;
border-color: var(--accent);
}
.contact__social {
display: flex;
gap: 1.75rem;
}
.social-link {
font-size: 0.875rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.45);
letter-spacing: 0.03em;
transition: color 0.2s ease;
}
.social-link:hover {
color: rgba(255, 255, 255, 0.9);
}
.contact__foot {
max-width: 1240px;
margin: 0 auto;
padding: 1.75rem clamp(1.5rem, 6vw, 7rem);
border-top: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.contact__foot span {
font-size: 0.78rem;
color: rgba(255, 255, 255, 0.28);
font-weight: 500;
}
/* ─── Reduced Motion ────────────────────────────────────────────────────── */
.reduced-motion *,
.reduced-motion *::before,
.reduced-motion *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
.reduced-motion .scroll-thumb {
animation: none !important;
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import Lenis from "lenis";
import * as THREE from "three";
gsap.registerPlugin(ScrollTrigger, SplitText);
// ─── Shell ────────────────────────────────────────────────────────────────
initDemoShell({
title: "Tech Lead Portfolio",
category: "pages",
tech: ["three.js", "gsap", "lenis", "scrolltrigger", "splittext"],
});
// ─── Reduced Motion ───────────────────────────────────────────────────────
let reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add("reduced-motion");
window.addEventListener("motion-preference", (e) => {
reduced = e.detail.reduced;
document.documentElement.classList.toggle("reduced-motion", reduced);
ScrollTrigger.refresh();
});
const dur = (d) => (reduced ? 0 : d);
// ─── Lenis ────────────────────────────────────────────────────────────────
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
// ─── Three.js Hero Background ─────────────────────────────────────────────
function initHeroCanvas() {
const canvas = document.getElementById("heroCanvas");
if (!canvas) return;
const hero = document.querySelector(".hero");
const w = hero.offsetWidth;
const h = hero.offsetHeight;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, w / h, 0.1, 100);
camera.position.z = 8;
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(w, h);
renderer.setClearColor(0x000000, 0);
const geos = [
new THREE.IcosahedronGeometry(1, 0),
new THREE.OctahedronGeometry(1, 0),
new THREE.TetrahedronGeometry(1, 0),
];
const mat = new THREE.MeshBasicMaterial({
color: 0x2563eb,
wireframe: true,
opacity: 0.15,
transparent: true,
});
const meshes = [];
for (let i = 0; i < 12; i++) {
const m = new THREE.Mesh(geos[i % 3], mat);
m.position.set(
(Math.random() - 0.5) * 16,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 8
);
m.scale.setScalar(0.4 + Math.random() * 1.2);
m.userData.offset = Math.random() * Math.PI * 2;
meshes.push(m);
scene.add(m);
}
let animId = null;
function animate() {
animId = requestAnimationFrame(animate);
const t = Date.now() * 0.0005;
meshes.forEach((m, i) => {
m.rotation.x += 0.002 + i * 0.0003;
m.rotation.y += 0.003 + i * 0.0002;
m.position.y += Math.sin(t + m.userData.offset) * 0.002;
});
renderer.render(scene, camera);
}
if (!reduced) {
animate();
} else {
renderer.render(scene, camera);
}
function onResize() {
const nw = hero.offsetWidth;
const nh = hero.offsetHeight;
camera.aspect = nw / nh;
camera.updateProjectionMatrix();
renderer.setSize(nw, nh);
}
window.addEventListener("resize", onResize);
// Pause rendering when hero is fully scrolled past
ScrollTrigger.create({
trigger: ".hero",
start: "top top",
end: "bottom top",
onLeave: () => {
if (animId) {
cancelAnimationFrame(animId);
animId = null;
}
},
onEnterBack: () => {
if (!animId && !reduced) animate();
},
});
}
// ─── Hero Animations ──────────────────────────────────────────────────────
function animateHero() {
const nameEl = document.getElementById("heroName");
const taglineEl = document.getElementById("heroTagline");
if (nameEl) {
const splitName = new SplitText(nameEl, { type: "words,lines", linesClass: "line" });
gsap.set(splitName.words, { y: 50, opacity: 0 });
gsap.to(splitName.words, {
y: 0,
opacity: 1,
duration: dur(1),
ease: "expo.out",
stagger: { each: dur(0.06) },
delay: dur(0.3),
});
}
if (taglineEl) {
gsap.from(taglineEl, {
y: reduced ? 0 : 24,
opacity: 0,
duration: dur(0.9),
ease: "expo.out",
delay: dur(0.7),
});
}
gsap.from(".hero__eyebrow", {
opacity: 0,
y: reduced ? 0 : -12,
duration: dur(0.7),
ease: "expo.out",
delay: dur(0.15),
});
gsap.from(".hero__ctas", {
opacity: 0,
y: reduced ? 0 : 18,
duration: dur(0.8),
ease: "expo.out",
delay: dur(0.9),
});
gsap.from(".hero__scroll-indicator", {
opacity: 0,
duration: dur(1),
delay: dur(1.4),
});
}
// ─── Expertise Cards ──────────────────────────────────────────────────────
function animateExpertise() {
const cards = document.querySelectorAll(".expertise-card");
if (!cards.length) return;
gsap.set(cards, { scale: 0.92, opacity: 0 });
gsap.to(cards, {
scale: 1,
opacity: 1,
duration: dur(0.75),
ease: "expo.out",
stagger: { each: dur(0.1) },
scrollTrigger: {
trigger: ".expertise__grid",
start: "top 78%",
toggleActions: "play none none reverse",
},
});
}
// ─── Timeline Items ───────────────────────────────────────────────────────
function animateTimeline() {
document.querySelectorAll(".timeline-item").forEach((item) => {
const isLeft = item.classList.contains("timeline-item--left");
const card = item.querySelector(".timeline-item__card");
const year = item.querySelector(".timeline-item__year");
if (card) {
gsap.set(card, { x: reduced ? 0 : isLeft ? 48 : -48, opacity: 0 });
gsap.to(card, {
x: 0,
opacity: 1,
duration: dur(0.8),
ease: "expo.out",
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none reverse",
},
});
}
if (year) {
gsap.set(year, { x: reduced ? 0 : isLeft ? -24 : 24, opacity: 0 });
gsap.to(year, {
x: 0,
opacity: 1,
duration: dur(0.7),
ease: "expo.out",
delay: dur(0.08),
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none reverse",
},
});
}
});
// Grow the center line in from top
gsap.from(".timeline__line", {
scaleY: 0,
transformOrigin: "top center",
duration: dur(1.5),
ease: "expo.out",
scrollTrigger: {
trigger: ".timeline__track",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
}
// ─── Repo Cards ───────────────────────────────────────────────────────────
function animateRepos() {
const cards = document.querySelectorAll(".repo-card");
if (!cards.length) return;
gsap.set(cards, { y: reduced ? 0 : 40, opacity: 0 });
gsap.to(cards, {
y: 0,
opacity: 1,
duration: dur(0.75),
ease: "expo.out",
stagger: { each: dur(0.1) },
scrollTrigger: {
trigger: ".repos__grid",
start: "top 78%",
toggleActions: "play none none reverse",
},
});
}
// ─── Contact CTA ──────────────────────────────────────────────────────────
function animateContact() {
const ctaEl = document.getElementById("contactCta");
if (ctaEl) {
const splitCta = new SplitText(ctaEl, { type: "words,lines", linesClass: "line" });
gsap.set(splitCta.words, { y: 60, opacity: 0 });
gsap.to(splitCta.words, {
y: 0,
opacity: 1,
duration: dur(0.95),
ease: "expo.out",
stagger: { each: dur(0.06) },
scrollTrigger: {
trigger: ".contact__inner",
start: "top 80%",
toggleActions: "play none none reverse",
},
});
}
gsap.from(".contact__kicker", {
opacity: 0,
y: reduced ? 0 : 16,
duration: dur(0.7),
ease: "expo.out",
scrollTrigger: {
trigger: ".contact__inner",
start: "top 80%",
toggleActions: "play none none reverse",
},
});
gsap.from(".contact__links", {
opacity: 0,
y: reduced ? 0 : 20,
duration: dur(0.8),
ease: "expo.out",
delay: dur(0.35),
scrollTrigger: {
trigger: ".contact__inner",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
}
// ─── Section Headers ──────────────────────────────────────────────────────
function animateSectionHeaders() {
document.querySelectorAll(".section__header").forEach((el) => {
gsap.from(el, {
opacity: 0,
y: reduced ? 0 : 16,
duration: dur(0.65),
ease: "expo.out",
scrollTrigger: {
trigger: el,
start: "top 88%",
toggleActions: "play none none reverse",
},
});
});
}
// ─── Init ─────────────────────────────────────────────────────────────────
initHeroCanvas();
animateHero();
animateSectionHeaders();
animateExpertise();
animateTimeline();
animateRepos();
animateContact();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tech Lead Portfolio — Marcus Chen</title>
<link rel="stylesheet" href="style.css" />
<script type="importmap">{"imports":{"gsap":"https://esm.sh/[email protected]","gsap/ScrollTrigger":"https://esm.sh/[email protected]/ScrollTrigger","gsap/SplitText":"https://esm.sh/[email protected]/SplitText","gsap/Flip":"https://esm.sh/[email protected]/Flip","gsap/ScrambleTextPlugin":"https://esm.sh/[email protected]/ScrambleTextPlugin","gsap/TextPlugin":"https://esm.sh/[email protected]/TextPlugin","gsap/all":"https://esm.sh/[email protected]/all","gsap/":"https://esm.sh/[email protected]/","lenis":"https://esm.sh/[email protected]/dist/lenis.mjs","three":"https://esm.sh/[email protected]","three/addons/":"https://esm.sh/[email protected]/examples/jsm/"}}</script>
</head>
<body>
<!-- ─── HERO ─────────────────────────────────────────────────────────── -->
<section class="hero" id="hero">
<canvas class="hero__canvas" id="heroCanvas" aria-hidden="true"></canvas>
<div class="hero__inner">
<div class="hero__eyebrow">
<span class="eyebrow-tag">Engineering Lead</span>
<span class="eyebrow-dot" aria-hidden="true"></span>
<span class="eyebrow-avail">Available for new roles</span>
</div>
<h1 class="hero__name" id="heroName">Marcus<br>Chen</h1>
<div class="hero__foot">
<p class="hero__tagline" id="heroTagline">Building teams and systems<br>that compound in value.</p>
<nav class="hero__ctas" aria-label="Hero navigation">
<a class="btn btn--primary" href="#work">View work</a>
<a class="btn btn--ghost" href="#contact">Get in touch</a>
</nav>
</div>
</div>
<div class="hero__scroll-indicator" aria-hidden="true">
<div class="scroll-track"><div class="scroll-thumb"></div></div>
<span class="scroll-label">scroll</span>
</div>
</section>
<!-- ─── EXPERTISE ─────────────────────────────────────────────────────── -->
<section class="section expertise" id="expertise">
<div class="section__header">
<div class="section__label-group">
<span class="section-index">01</span>
<span class="section-label">Core Expertise</span>
</div>
<p class="section__summary">Eight years leading engineering teams at scale.</p>
</div>
<div class="expertise__grid">
<article class="expertise-card">
<div class="expertise-card__icon" aria-hidden="true">
<div class="icon-backend"></div>
</div>
<h3 class="expertise-card__title">Backend Systems</h3>
<ul class="expertise-card__list">
<li>Distributed systems design</li>
<li>High-throughput service mesh</li>
<li>PostgreSQL, Redis, Kafka</li>
<li>Go, Rust, TypeScript</li>
<li>gRPC + REST API design</li>
</ul>
</article>
<article class="expertise-card">
<div class="expertise-card__icon" aria-hidden="true">
<div class="icon-platform"></div>
</div>
<h3 class="expertise-card__title">Platform Engineering</h3>
<ul class="expertise-card__list">
<li>Kubernetes & cloud-native infra</li>
<li>CI/CD pipeline architecture</li>
<li>Developer experience tooling</li>
<li>AWS, GCP multi-region setups</li>
<li>Observability & SLO design</li>
</ul>
</article>
<article class="expertise-card">
<div class="expertise-card__icon" aria-hidden="true">
<div class="icon-leadership"></div>
</div>
<h3 class="expertise-card__title">Team Leadership</h3>
<ul class="expertise-card__list">
<li>Hiring & growing eng teams</li>
<li>Technical roadmap planning</li>
<li>Cross-functional collaboration</li>
<li>Eng culture & process design</li>
<li>Performance management</li>
</ul>
</article>
</div>
</section>
<!-- ─── TIMELINE ──────────────────────────────────────────────────────── -->
<section class="section timeline" id="work">
<div class="section__header">
<div class="section__label-group">
<span class="section-index">02</span>
<span class="section-label">Career Timeline</span>
</div>
<p class="section__summary">A track record of increasing ownership and impact.</p>
</div>
<div class="timeline__track" aria-label="Career history">
<div class="timeline__line" aria-hidden="true"></div>
<div class="timeline-item timeline-item--left">
<div class="timeline-item__year">2024</div>
<div class="timeline-item__card">
<div class="timeline-item__meta">
<span class="timeline-item__role">VP Engineering</span>
<span class="timeline-item__sep" aria-hidden="true">—</span>
<span class="timeline-item__company">Stratum AI</span>
</div>
<p class="timeline-item__desc">Led a 40-person engineering org through Series B. Rebuilt the core ML inference pipeline, reducing p99 latency from 800ms to 65ms. Grew team from 18 to 40 engineers in 14 months while maintaining culture health scores above 4.6/5.</p>
</div>
</div>
<div class="timeline-item timeline-item--right">
<div class="timeline-item__year">2021</div>
<div class="timeline-item__card">
<div class="timeline-item__meta">
<span class="timeline-item__role">Staff Engineer → Tech Lead</span>
<span class="timeline-item__sep" aria-hidden="true">—</span>
<span class="timeline-item__company">Meridian Finance</span>
</div>
<p class="timeline-item__desc">Designed and delivered a real-time transaction processing platform handling 50k TPS. Mentored six engineers to senior level. Defined the technical strategy that led to the company's $200M acquisition.</p>
</div>
</div>
<div class="timeline-item timeline-item--left">
<div class="timeline-item__year">2019</div>
<div class="timeline-item__card">
<div class="timeline-item__meta">
<span class="timeline-item__role">Senior Engineer</span>
<span class="timeline-item__sep" aria-hidden="true">—</span>
<span class="timeline-item__company">Cloudvault</span>
</div>
<p class="timeline-item__desc">Built the multi-tenant storage abstraction layer now serving 3M+ enterprise customers. Pioneered the company's Rust adoption with an internal migration guide that became an industry reference.</p>
</div>
</div>
<div class="timeline-item timeline-item--right">
<div class="timeline-item__year">2016</div>
<div class="timeline-item__card">
<div class="timeline-item__meta">
<span class="timeline-item__role">Software Engineer</span>
<span class="timeline-item__sep" aria-hidden="true">—</span>
<span class="timeline-item__company">Nexus Labs</span>
</div>
<p class="timeline-item__desc">First engineering hire. Shipped the core API platform from scratch. Established the testing culture and deployment process that scaled the team from 2 to 25 engineers without a single P0 production incident.</p>
</div>
</div>
</div>
</section>
<!-- ─── OPEN SOURCE ────────────────────────────────────────────────────── -->
<section class="section open-source" id="open-source">
<div class="section__header">
<div class="section__label-group">
<span class="section-index">03</span>
<span class="section-label">Open Source</span>
</div>
<p class="section__summary">Giving back to the communities that shaped my craft.</p>
</div>
<div class="repos__grid">
<a class="repo-card" href="#" aria-label="riftdb repository">
<div class="repo-card__top">
<span class="repo-card__icon" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
</span>
<span class="repo-card__name">riftdb</span>
<span class="repo-card__stars">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
2.4k
</span>
</div>
<p class="repo-card__desc">A zero-dependency embedded key-value store for Go with MVCC transactions, range scans, and sub-millisecond write latency.</p>
<div class="repo-card__foot">
<span class="lang-pill lang-pill--go">Go</span>
<span class="repo-card__updated">Updated 2 days ago</span>
</div>
</a>
<a class="repo-card" href="#" aria-label="envi repository">
<div class="repo-card__top">
<span class="repo-card__icon" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
</span>
<span class="repo-card__name">envi</span>
<span class="repo-card__stars">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
1.1k
</span>
</div>
<p class="repo-card__desc">Typed environment variable validation for TypeScript with schema inference, dot-env loading, and strict mode for CI environments.</p>
<div class="repo-card__foot">
<span class="lang-pill lang-pill--ts">TypeScript</span>
<span class="repo-card__updated">Updated 1 week ago</span>
</div>
</a>
<a class="repo-card" href="#" aria-label="sieve repository">
<div class="repo-card__top">
<span class="repo-card__icon" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
</span>
<span class="repo-card__name">sieve</span>
<span class="repo-card__stars">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
890
</span>
</div>
<p class="repo-card__desc">A Rust crate for compile-time rate limiting with leaky-bucket and token-bucket implementations. Zero allocations on the hot path.</p>
<div class="repo-card__foot">
<span class="lang-pill lang-pill--rust">Rust</span>
<span class="repo-card__updated">Updated 3 weeks ago</span>
</div>
</a>
</div>
</section>
<!-- ─── CONTACT ───────────────────────────────────────────────────────── -->
<section class="section contact" id="contact">
<div class="contact__inner">
<p class="contact__kicker">Currently open to senior leadership roles</p>
<h2 class="contact__cta" id="contactCta">Let's Build<br>Something.</h2>
<div class="contact__links">
<a class="contact__email" href="mailto:[email protected]">[email protected]</a>
<div class="contact__social">
<a class="social-link" href="#" aria-label="GitHub profile">GitHub</a>
<a class="social-link" href="#" aria-label="LinkedIn profile">LinkedIn</a>
<a class="social-link" href="#" aria-label="Download Resume">Resume ↓</a>
</div>
</div>
</div>
<div class="contact__foot">
<span>Marcus Chen — Engineering Lead</span>
<span>2025</span>
</div>
</section>
<script type="module" src="script.js"></script>
</body>
</html>Tech Lead Portfolio
Corporate tech lead portfolio with Three.js wireframe hero, vertical career timeline, expertise grid, and open source repo showcase.
Source
- Repository:
libs-genclaude - Original demo id:
32-tech-portfolio
Notes
Corporate tech lead portfolio with Three.js wireframe hero, vertical career timeline, expertise grid, and open source repo showcase.