/* ═══════════════════════════════════════════════════════════════════════
AI ENGINEER PORTFOLIO — styles.css
Dr. Yuki Tanaka // Futuristic data-dense monospace aesthetic
═══════════════════════════════════════════════════════════════════════ */
/* ── Reset & Base ───────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #050508;
--text: #e0ffe8;
--dim: #1a2435;
--accent-green: #00ff88;
--accent-purple: #9945ff;
--accent-blue: #00d4ff;
--muted: #3a5a4a;
--border: #0f2020;
--mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
}
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--mono);
font-size: 14px;
line-height: 1.6;
overflow-x: hidden;
}
a { color: var(--accent-green); text-decoration: none; }
a:hover { text-decoration: underline; }
/* ── Section base ───────────────────────────────────────────────────── */
.section {
padding: 100px clamp(24px, 6vw, 120px);
position: relative;
max-width: 1400px;
margin: 0 auto;
}
.section-header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 60px;
}
.section-tag {
font-size: 11px;
color: var(--accent-green);
letter-spacing: 0.2em;
opacity: 0.7;
white-space: nowrap;
}
.section-title {
font-size: clamp(16px, 2vw, 22px);
font-weight: 900;
letter-spacing: 0.18em;
color: var(--text);
white-space: nowrap;
text-transform: uppercase;
}
.section-line {
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--muted) 0%, transparent 100%);
}
/* ── HERO ───────────────────────────────────────────────────────────── */
#hero {
position: relative;
width: 100%;
height: 100vh;
min-height: 600px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
#neural-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
}
#hero-overlay {
position: relative;
z-index: 2;
text-align: center;
padding: 0 clamp(16px, 5vw, 80px);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
#hero-meta {
font-size: 10px;
letter-spacing: 0.35em;
color: var(--accent-green);
opacity: 0.6;
text-transform: uppercase;
}
#hero-name {
font-size: clamp(3rem, 10vw, 9rem);
font-weight: 900;
letter-spacing: 0.06em;
color: var(--text);
text-transform: uppercase;
line-height: 0.9;
text-shadow:
0 0 30px rgba(0,255,136,0.3),
0 0 80px rgba(0,255,136,0.1);
will-change: transform;
}
#hero-title {
font-size: clamp(12px, 1.8vw, 18px);
letter-spacing: 0.3em;
color: var(--accent-blue);
text-transform: uppercase;
opacity: 0.9;
}
#hero-spec-wrapper {
display: flex;
align-items: center;
gap: 0;
font-size: clamp(12px, 1.5vw, 16px);
color: var(--accent-green);
height: 28px;
letter-spacing: 0.05em;
background: rgba(0,255,136,0.05);
border: 1px solid rgba(0,255,136,0.15);
padding: 4px 16px;
border-radius: 2px;
}
#spec-label { opacity: 0.6; }
#spec-cursor {
animation: blink 0.9s step-end infinite;
color: var(--accent-green);
margin-left: 2px;
}
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
#hero-stats {
display: flex;
align-items: center;
gap: 20px;
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.stat-val {
font-size: clamp(20px, 3vw, 32px);
font-weight: 900;
color: var(--accent-green);
line-height: 1;
text-shadow: 0 0 20px rgba(0,255,136,0.5);
}
.stat-label {
font-size: 9px;
letter-spacing: 0.2em;
color: var(--muted);
text-transform: uppercase;
}
.stat-sep {
color: var(--border);
font-size: 24px;
opacity: 0.5;
align-self: center;
margin-bottom: 12px;
}
#hero-scroll-hint {
position: absolute;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
font-size: 9px;
letter-spacing: 0.3em;
color: var(--muted);
z-index: 2;
animation: float-hint 2s ease-in-out infinite;
}
@keyframes float-hint {
0%,100%{ transform: translateX(-50%) translateY(0); opacity:0.5; }
50%{ transform: translateX(-50%) translateY(6px); opacity:1; }
}
/* ── RESEARCH ───────────────────────────────────────────────────────── */
#research { max-width: 1400px; }
.papers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
gap: 24px;
}
.paper-card {
background: var(--dim);
border: 1px solid var(--border);
border-top: 2px solid var(--accent-green);
padding: 28px;
display: flex;
flex-direction: column;
gap: 14px;
position: relative;
overflow: hidden;
opacity: 0;
transform: translateY(40px);
transition: border-color 0.3s, box-shadow 0.3s;
}
.paper-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent-green), var(--accent-purple));
}
.paper-card:hover {
border-color: var(--muted);
box-shadow: 0 0 40px rgba(0,255,136,0.08);
}
.paper-venue-row {
display: flex;
align-items: center;
gap: 10px;
}
.paper-venue {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.15em;
padding: 3px 8px;
border-radius: 2px;
text-transform: uppercase;
}
.venue-neurips { background: rgba(0,255,136,0.15); color: var(--accent-green); border: 1px solid rgba(0,255,136,0.3); }
.venue-icml { background: rgba(153,69,255,0.15); color: var(--accent-purple); border: 1px solid rgba(153,69,255,0.3); }
.venue-iclr { background: rgba(0,212,255,0.15); color: var(--accent-blue); border: 1px solid rgba(0,212,255,0.3); }
.paper-status {
font-size: 9px;
letter-spacing: 0.15em;
color: var(--accent-green);
opacity: 0.7;
margin-left: auto;
}
.paper-title {
font-size: clamp(13px, 1.2vw, 15px);
font-weight: 700;
color: var(--text);
line-height: 1.4;
letter-spacing: 0.02em;
}
.paper-abstract {
font-size: 12px;
color: var(--muted);
line-height: 1.7;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.paper-metrics {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.metric-chip {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 12px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
border-radius: 2px;
}
.metric-label {
font-size: 8px;
letter-spacing: 0.15em;
color: var(--muted);
}
.metric-val {
font-size: 14px;
font-weight: 700;
}
.metric-val.green { color: var(--accent-green); }
.metric-val.blue { color: var(--accent-blue); }
.metric-val.purple { color: var(--accent-purple); }
.paper-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 10px;
border-top: 1px solid var(--border);
flex-wrap: wrap;
}
.paper-authors {
font-size: 11px;
color: var(--muted);
font-style: italic;
}
.paper-link {
font-size: 10px;
letter-spacing: 0.1em;
color: var(--accent-blue);
}
/* ── MODEL VISUALIZATION ────────────────────────────────────────────── */
#model-viz { max-width: 1400px; }
.viz-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 900px) { .viz-grid { grid-template-columns: 1fr; } }
.viz-panel {
background: var(--dim);
border: 1px solid var(--border);
padding: 24px;
position: relative;
overflow: hidden;
}
.viz-panel-header {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 20px;
}
.viz-panel-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.2em;
color: var(--accent-green);
text-transform: uppercase;
}
.viz-panel-sub {
font-size: 10px;
color: var(--muted);
letter-spacing: 0.05em;
}
#loss-canvas {
display: block;
width: 100%;
height: 220px;
background: transparent;
}
.canvas-axes {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.axis-y-label, .axis-x-label {
font-size: 9px;
letter-spacing: 0.2em;
color: var(--muted);
text-transform: uppercase;
}
/* Confusion matrix */
#confusion-matrix {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 2px;
width: 100%;
aspect-ratio: 1;
}
.cm-cell {
aspect-ratio: 1;
border-radius: 1px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-weight: 700;
color: rgba(255,255,255,0.85);
transition: transform 0.15s;
}
.cm-cell:hover {
transform: scale(1.15);
z-index: 2;
}
.confusion-labels {
margin-top: 8px;
}
.confusion-classes {
display: flex;
justify-content: space-around;
font-size: 9px;
color: var(--muted);
letter-spacing: 0.05em;
}
/* ── STACK / TERMINAL ───────────────────────────────────────────────── */
#stack { max-width: 1400px; }
.terminal-wrapper {
background: #020508;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
box-shadow: 0 0 60px rgba(0,255,136,0.05);
max-width: 860px;
margin: 0 auto;
}
.terminal-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: #0a0d14;
border-bottom: 1px solid var(--border);
}
.t-dot {
width: 12px; height: 12px;
border-radius: 50%;
}
.t-red { background: #ff5f57; }
.t-yellow { background: #ffbd2e; }
.t-green { background: #28c840; }
.t-title {
margin-left: auto;
font-size: 11px;
color: var(--muted);
letter-spacing: 0.1em;
}
.terminal-body {
padding: 24px 28px 32px;
display: flex;
flex-direction: column;
gap: 10px;
}
.term-line,
.term-output {
display: flex;
align-items: center;
gap: 10px;
opacity: 0;
transform: translateX(-10px);
}
.term-prompt {
color: var(--accent-green);
font-weight: 700;
font-size: 14px;
min-width: 12px;
flex-shrink: 0;
}
.term-cmd {
color: var(--text);
font-size: 13px;
letter-spacing: 0.03em;
}
.term-key {
color: var(--accent-blue);
font-size: 12px;
min-width: 140px;
flex-shrink: 0;
}
.term-key-plain {
font-size: 12px;
letter-spacing: 0.03em;
}
.term-bar-wrap {
flex: 1;
height: 4px;
background: rgba(255,255,255,0.05);
border-radius: 2px;
overflow: hidden;
max-width: 300px;
}
.term-bar-fill {
display: block;
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--accent-green), var(--accent-blue));
border-radius: 2px;
transition: width 1.2s cubic-bezier(0.4,0,0.2,1);
}
.term-pct {
font-size: 11px;
letter-spacing: 0.08em;
min-width: 120px;
text-align: right;
}
.term-pct.green { color: var(--accent-green); }
.term-pct.blue { color: var(--accent-blue); }
.term-pct.purple { color: var(--accent-purple); }
.blink-cursor {
animation: blink 1s step-end infinite;
}
/* ── CONTACT ────────────────────────────────────────────────────────── */
#contact { max-width: 1400px; }
.contact-terminal {
background: #020508;
border: 1px solid var(--border);
border-radius: 4px;
padding: 40px clamp(20px, 5vw, 60px);
max-width: 700px;
box-shadow: 0 0 80px rgba(0,255,136,0.04);
}
.contact-prompt-line {
font-size: clamp(13px, 2vw, 17px);
margin-bottom: 32px;
letter-spacing: 0.03em;
}
.contact-user { color: var(--accent-green); }
.contact-colon { color: var(--text); }
.contact-path { color: var(--accent-blue); }
.contact-dollar { color: var(--text); margin-left: 6px; }
.contact-cmd { color: var(--text); }
.contact-output {
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 32px;
padding-left: 16px;
}
.contact-row {
display: flex;
gap: 16px;
align-items: baseline;
flex-wrap: wrap;
}
.co-key {
font-size: 10px;
letter-spacing: 0.2em;
color: var(--muted);
min-width: 80px;
}
.co-sep {
color: var(--border);
font-size: 12px;
}
.co-val {
font-size: 13px;
color: var(--accent-green);
letter-spacing: 0.03em;
}
a.co-val:hover { text-shadow: 0 0 12px rgba(0,255,136,0.6); }
.contact-cursor-line {
font-size: clamp(13px, 2vw, 17px);
display: flex;
align-items: center;
gap: 0;
}
.contact-blink {
color: var(--accent-green);
animation: blink 0.9s step-end infinite;
margin-left: 6px;
}
/* ── Reduced motion overrides ────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
.paper-card { opacity: 1; transform: none; }
.term-line, .term-output { opacity: 1; transform: none; }
#spec-cursor, .contact-blink, .blink-cursor { animation: none; opacity: 1; }
#hero-scroll-hint { animation: none; }
}
/* ── Utility ─────────────────────────────────────────────────────────── */
.green { color: var(--accent-green); }
.blue { color: var(--accent-blue); }
.purple { color: var(--accent-purple); }
/* Demo shell overrides to fit dark theme */
.demo-shell-bar {
background: rgba(5,5,8,0.9) !important;
border-bottom-color: var(--border) !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.
}
/**
* AI Engineer Portfolio — main.js
* Dr. Yuki Tanaka // Three.js neural net + GSAP + Canvas 2D
*/
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin';
import Lenis from 'lenis';
import * as THREE from 'three';
// ── Register GSAP plugins ────────────────────────────────────────────
gsap.registerPlugin(ScrollTrigger, ScrambleTextPlugin);
// ── Init shell ───────────────────────────────────────────────────────
initDemoShell({
title: 'AI Engineer Portfolio',
category: 'pages',
tech: ['three.js', 'gsap', 'scrambletext', 'lenis', 'canvas-2d']
});
const reduced = prefersReducedMotion();
// ── Lenis smooth scroll ──────────────────────────────────────────────
const lenis = new Lenis({ lerp: 0.08, smoothWheel: true });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
// ══════════════════════════════════════════════════════════════════════
// THREE.JS NEURAL NETWORK
// ══════════════════════════════════════════════════════════════════════
function initNeuralNet() {
const canvas = document.getElementById('neural-canvas');
const W = canvas.clientWidth || window.innerWidth;
const H = canvas.clientHeight || window.innerHeight;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(W, H);
renderer.setClearColor(0x000000, 0);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, W / H, 0.1, 100);
camera.position.set(0, 0, 8);
// ── 200 nodes distributed on sphere surface ──────────────────────
const NODE_COUNT = 200;
const positions = new Float32Array(NODE_COUNT * 3);
const nodes = [];
for (let i = 0; i < NODE_COUNT; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = 3 + Math.random() * 2;
const x = r * Math.sin(phi) * Math.cos(theta);
const y = r * Math.sin(phi) * Math.sin(theta);
const z = r * Math.cos(phi);
positions[i * 3] = x;
positions[i * 3 + 1] = y;
positions[i * 3 + 2] = z;
nodes.push({ x, y, z, baseSize: 0.04 + Math.random() * 0.04, phase: Math.random() * Math.PI * 2 });
}
const nodeGeo = new THREE.BufferGeometry();
nodeGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const nodeSizes = new Float32Array(NODE_COUNT);
nodeSizes.fill(0.05);
nodeGeo.setAttribute('size', new THREE.BufferAttribute(nodeSizes, 1));
const nodeMat = new THREE.ShaderMaterial({
uniforms: { color: { value: new THREE.Color(0x00ff88) } },
vertexShader: `
attribute float size;
void main() {
gl_PointSize = size * 300.0;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 color;
void main() {
float d = length(gl_PointCoord - vec2(0.5));
if(d > 0.5) discard;
float alpha = 1.0 - smoothstep(0.2, 0.5, d);
gl_FragColor = vec4(color, alpha * 0.85);
}
`,
transparent: true,
depthWrite: false
});
const points = new THREE.Points(nodeGeo, nodeMat);
scene.add(points);
// ── Edges between nodes within distance threshold ────────────────
const DIST_THRESHOLD = 1.3;
const edgePositions = [];
const edges = []; // store for particle paths
for (let i = 0; i < NODE_COUNT; i++) {
for (let j = i + 1; j < NODE_COUNT; j++) {
const dx = nodes[i].x - nodes[j].x;
const dy = nodes[i].y - nodes[j].y;
const dz = nodes[i].z - nodes[j].z;
const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (d < DIST_THRESHOLD) {
edgePositions.push(nodes[i].x, nodes[i].y, nodes[i].z, nodes[j].x, nodes[j].y, nodes[j].z);
edges.push({ from: nodes[i], to: nodes[j] });
}
}
}
const edgeGeo = new THREE.BufferGeometry();
edgeGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(edgePositions), 3));
const edgeMat = new THREE.LineBasicMaterial({
color: 0x9945ff,
transparent: true,
opacity: 0.18
});
scene.add(new THREE.LineSegments(edgeGeo, edgeMat));
// ── 50 particles flowing along random edges ──────────────────────
const PARTICLE_COUNT = 50;
const particlePositions = new Float32Array(PARTICLE_COUNT * 3);
const particleGeo = new THREE.BufferGeometry();
particleGeo.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
const particleMat = new THREE.PointsMaterial({
color: 0x00d4ff,
size: 0.08,
transparent: true,
opacity: 0.9,
depthWrite: false
});
const particleMesh = new THREE.Points(particleGeo, particleMat);
scene.add(particleMesh);
// Each particle picks a random edge and a random progress t
const particleData = [];
for (let i = 0; i < PARTICLE_COUNT; i++) {
const edgeIdx = Math.floor(Math.random() * edges.length);
particleData.push({
edgeIdx,
t: Math.random(),
speed: 0.003 + Math.random() * 0.005,
direction: Math.random() > 0.5 ? 1 : -1
});
}
// ── Resize handler ───────────────────────────────────────────────
function onResize() {
const w = window.innerWidth;
const h = canvas.parentElement.clientHeight || window.innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', onResize);
// ── Animation loop ───────────────────────────────────────────────
let cameraAngle = 0;
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
// Slow camera orbit
cameraAngle += 0.0008;
camera.position.x = Math.sin(cameraAngle) * 8;
camera.position.z = Math.cos(cameraAngle) * 8;
camera.position.y = Math.sin(cameraAngle * 0.4) * 1.5;
camera.lookAt(0, 0, 0);
// Pulse node sizes
const sizeAttr = nodeGeo.attributes.size;
for (let i = 0; i < NODE_COUNT; i++) {
const pulse = nodes[i].baseSize + Math.sin(t * 1.5 + nodes[i].phase) * 0.015;
sizeAttr.array[i] = pulse;
}
sizeAttr.needsUpdate = true;
// Move particles along edges
const posAttr = particleGeo.attributes.position;
for (let i = 0; i < PARTICLE_COUNT; i++) {
const pd = particleData[i];
pd.t += pd.speed * pd.direction;
// Bounce direction at ends, pick new edge when bouncing
if (pd.t >= 1 || pd.t <= 0) {
pd.direction *= -1;
pd.t = Math.max(0, Math.min(1, pd.t));
if (Math.random() > 0.6) {
pd.edgeIdx = Math.floor(Math.random() * edges.length);
}
}
const edge = edges[pd.edgeIdx];
posAttr.array[i * 3] = edge.from.x + (edge.to.x - edge.from.x) * pd.t;
posAttr.array[i * 3 + 1] = edge.from.y + (edge.to.y - edge.from.y) * pd.t;
posAttr.array[i * 3 + 2] = edge.from.z + (edge.to.z - edge.from.z) * pd.t;
}
posAttr.needsUpdate = true;
renderer.render(scene, camera);
}
if (!reduced) {
animate();
} else {
// Single static render for reduced motion
renderer.render(scene, camera);
}
}
// ══════════════════════════════════════════════════════════════════════
// HERO TEXT ANIMATIONS
// ══════════════════════════════════════════════════════════════════════
function initHeroText() {
const nameEl = document.getElementById('hero-name');
if (!reduced) {
// Scramble reveal
gsap.fromTo(nameEl,
{ opacity: 0 },
{
opacity: 1,
duration: 0.1,
onComplete: () => {
gsap.to(nameEl, {
duration: 1.5,
delay: 0.3,
scrambleText: {
text: 'DR. YUKI TANAKA',
chars: '01ABCDEF!@#$%',
speed: 0.4,
delimiter: ''
}
});
}
}
);
// Fade in hero elements
gsap.fromTo('#hero-meta', { opacity: 0, y: -20 }, { opacity: 1, y: 0, duration: 0.8, delay: 0.2 });
gsap.fromTo('#hero-title', { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.8, delay: 1.4 });
gsap.fromTo('#hero-spec-wrapper', { opacity: 0, scale: 0.95 }, { opacity: 1, scale: 1, duration: 0.6, delay: 1.8 });
gsap.fromTo('#hero-stats', { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: 0.8, delay: 2.0 });
gsap.fromTo('#hero-scroll-hint', { opacity: 0 }, { opacity: 1, duration: 0.8, delay: 2.6 });
}
// Typewriter specializations — runs regardless
const specs = ['LLM Fine-tuning', 'Diffusion Models', 'RL Systems', 'Neural Architecture'];
let si = 0;
let typeTimeout = null;
function typeSpec() {
const el = document.getElementById('spec-text');
const text = specs[si % specs.length];
el.textContent = '';
let ci = 0;
const interval = setInterval(() => {
el.textContent += text[ci++];
if (ci >= text.length) {
clearInterval(interval);
typeTimeout = setTimeout(() => {
// Erase
let ei = text.length;
const eraseInterval = setInterval(() => {
el.textContent = text.slice(0, ei--);
if (ei < 0) {
clearInterval(eraseInterval);
si++;
typeSpec();
}
}, 30);
}, 1800);
}
}, 60);
}
const specDelay = reduced ? 0 : 1900;
setTimeout(typeSpec, specDelay);
}
// ══════════════════════════════════════════════════════════════════════
// PAPER CARDS — scroll reveal
// ══════════════════════════════════════════════════════════════════════
function initPaperCards() {
const cards = document.querySelectorAll('.paper-card');
if (reduced) {
cards.forEach(c => { c.style.opacity = '1'; c.style.transform = 'none'; });
return;
}
cards.forEach((card, i) => {
gsap.to(card, {
opacity: 1,
y: 0,
duration: 0.7,
ease: 'power3.out',
delay: i * 0.12,
scrollTrigger: {
trigger: card,
start: 'top 88%',
once: true
}
});
});
}
// ══════════════════════════════════════════════════════════════════════
// CANVAS 2D — Training Loss Curve
// ══════════════════════════════════════════════════════════════════════
function initLossCanvas() {
const canvas = document.getElementById('loss-canvas');
const ctx = canvas.getContext('2d');
// Generate a realistic loss curve: starts ~2.5, drops fast, levels off ~0.15
function generateLossData(numPoints) {
const data = [];
let loss = 2.5;
for (let i = 0; i < numPoints; i++) {
const t = i / numPoints;
const decay = Math.exp(-t * 5) * 2.35;
const plateau = 0.15;
const noise = (Math.random() - 0.5) * 0.08 * Math.exp(-t * 3);
loss = plateau + decay + noise;
data.push(Math.max(0.05, loss));
}
return data;
}
const DATA_POINTS = 120;
const lossData = generateLossData(DATA_POINTS);
function resizeCanvas() {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
return { w: rect.width, h: rect.height };
}
function drawChart(progress) {
const { w, h } = resizeCanvas();
const PAD = { top: 20, right: 20, bottom: 30, left: 48 };
const plotW = w - PAD.left - PAD.right;
const plotH = h - PAD.top - PAD.bottom;
ctx.clearRect(0, 0, w, h);
// Background
ctx.fillStyle = '#020508';
ctx.fillRect(0, 0, w, h);
// Grid lines
const gridCols = 8;
const gridRows = 5;
ctx.strokeStyle = 'rgba(15,32,32,1)';
ctx.lineWidth = 1;
for (let i = 0; i <= gridCols; i++) {
const x = PAD.left + (i / gridCols) * plotW;
ctx.beginPath();
ctx.moveTo(x, PAD.top);
ctx.lineTo(x, PAD.top + plotH);
ctx.stroke();
}
for (let i = 0; i <= gridRows; i++) {
const y = PAD.top + (i / gridRows) * plotH;
ctx.beginPath();
ctx.moveTo(PAD.left, y);
ctx.lineTo(PAD.left + plotW, y);
ctx.stroke();
}
// Y axis labels
ctx.fillStyle = '#3a5a4a';
ctx.font = '9px JetBrains Mono, monospace';
ctx.textAlign = 'right';
const yMax = 2.6;
const yMin = 0.0;
for (let i = 0; i <= gridRows; i++) {
const val = yMax - (i / gridRows) * (yMax - yMin);
const y = PAD.top + (i / gridRows) * plotH;
ctx.fillText(val.toFixed(1), PAD.left - 6, y + 3);
}
// X axis labels
ctx.textAlign = 'center';
for (let i = 0; i <= gridCols; i++) {
const val = Math.round((i / gridCols) * 100);
const x = PAD.left + (i / gridCols) * plotW;
ctx.fillText(val + 'k', x, PAD.top + plotH + 16);
}
// Axes
ctx.strokeStyle = '#1a2435';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + plotH);
ctx.lineTo(PAD.left + plotW, PAD.top + plotH);
ctx.stroke();
// Clip to progress
const drawCount = Math.floor(DATA_POINTS * progress);
if (drawCount < 2) return;
function dataToCanvas(i, val) {
const x = PAD.left + (i / (DATA_POINTS - 1)) * plotW;
const y = PAD.top + (1 - (val - yMin) / (yMax - yMin)) * plotH;
return { x, y };
}
// Glow fill under curve
const firstPt = dataToCanvas(0, lossData[0]);
const lastPt = dataToCanvas(drawCount - 1, lossData[drawCount - 1]);
const gradient = ctx.createLinearGradient(0, PAD.top, 0, PAD.top + plotH);
gradient.addColorStop(0, 'rgba(0,255,136,0.18)');
gradient.addColorStop(1, 'rgba(0,255,136,0.00)');
ctx.beginPath();
ctx.moveTo(firstPt.x, PAD.top + plotH);
ctx.lineTo(firstPt.x, firstPt.y);
for (let i = 1; i < drawCount; i++) {
const pt = dataToCanvas(i, lossData[i]);
ctx.lineTo(pt.x, pt.y);
}
ctx.lineTo(lastPt.x, PAD.top + plotH);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fill();
// Glowing line
ctx.shadowColor = '#00ff88';
ctx.shadowBlur = 12;
ctx.strokeStyle = '#00ff88';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.beginPath();
const startPt = dataToCanvas(0, lossData[0]);
ctx.moveTo(startPt.x, startPt.y);
for (let i = 1; i < drawCount; i++) {
const pt = dataToCanvas(i, lossData[i]);
ctx.lineTo(pt.x, pt.y);
}
ctx.stroke();
ctx.shadowBlur = 0;
// Leading dot
const leadPt = dataToCanvas(drawCount - 1, lossData[drawCount - 1]);
ctx.beginPath();
ctx.arc(leadPt.x, leadPt.y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#00ff88';
ctx.shadowColor = '#00ff88';
ctx.shadowBlur = 16;
ctx.fill();
ctx.shadowBlur = 0;
}
// Initial draw at 0
drawChart(0);
if (reduced) {
drawChart(1);
return;
}
// Animate on scroll entry
let animated = false;
ScrollTrigger.create({
trigger: '#loss-panel',
start: 'top 80%',
onEnter: () => {
if (animated) return;
animated = true;
const obj = { progress: 0 };
gsap.to(obj, {
progress: 1,
duration: 2.2,
ease: 'power2.inOut',
onUpdate: () => drawChart(obj.progress)
});
}
});
window.addEventListener('resize', () => drawChart(animated ? 1 : 0));
}
// ══════════════════════════════════════════════════════════════════════
// CANVAS 2D — Confusion Matrix (8×8 heatmap)
// ══════════════════════════════════════════════════════════════════════
function initConfusionMatrix() {
const container = document.getElementById('confusion-matrix');
// Simulate a realistic 8x8 confusion matrix (high diagonal, some off-diagonal)
const labels = ['NLI', 'QA', 'SUM', 'CLS', 'GEN', 'COD', 'MAT', 'RLX'];
const N = 8;
// Build matrix: diagonal values high, off-diagonal noise
const matrix = [];
for (let i = 0; i < N; i++) {
const row = [];
for (let j = 0; j < N; j++) {
if (i === j) {
row.push(0.82 + Math.random() * 0.17); // diagonal: 0.82–0.99
} else {
const off = Math.random() * 0.12;
row.push(off * (Math.random() > 0.65 ? 1 : 0.3)); // sparse off-diagonal
}
}
matrix.push(row);
}
// Create cells
for (let i = 0; i < N; i++) {
for (let j = 0; j < N; j++) {
const val = matrix[i][j];
const cell = document.createElement('div');
cell.className = 'cm-cell';
// Color: green for diagonal, purple for off-diagonal confusion, dark for zero
let r, g, b, a;
if (i === j) {
// Green diagonal
const intensity = val;
r = Math.round(0 + intensity * 0);
g = Math.round(100 + intensity * 155);
b = Math.round(60 + intensity * 76);
a = 0.3 + intensity * 0.7;
} else if (val > 0.04) {
// Purple off-diagonal errors
const intensity = val / 0.12;
r = Math.round(80 + intensity * 73);
g = Math.round(10 + intensity * 20);
b = Math.round(120 + intensity * 135);
a = 0.2 + intensity * 0.7;
} else {
r = 10; g = 20; b = 30; a = 0.4;
}
cell.style.background = `rgba(${r},${g},${b},${a})`;
if (val > 0.05) {
cell.textContent = val.toFixed(2);
}
// Stagger reveal
cell.style.opacity = '0';
cell.style.transform = 'scale(0.5)';
container.appendChild(cell);
}
}
if (reduced) {
container.querySelectorAll('.cm-cell').forEach(c => {
c.style.opacity = '1';
c.style.transform = 'none';
});
return;
}
ScrollTrigger.create({
trigger: '#confusion-panel',
start: 'top 80%',
onEnter: () => {
const cells = container.querySelectorAll('.cm-cell');
cells.forEach((cell, i) => {
gsap.to(cell, {
opacity: 1,
scale: 1,
duration: 0.35,
delay: i * 0.008,
ease: 'back.out(1.4)'
});
});
}
});
}
// ══════════════════════════════════════════════════════════════════════
// TERMINAL STACK — stagger reveal
// ══════════════════════════════════════════════════════════════════════
function initTerminal() {
const lines = document.querySelectorAll('.term-line, .term-output');
if (reduced) {
lines.forEach(l => { l.style.opacity = '1'; l.style.transform = 'none'; });
document.querySelectorAll('.term-bar-fill').forEach(b => { b.style.width = b.style.getPropertyValue('--pct'); });
return;
}
ScrollTrigger.create({
trigger: '#terminal-body',
start: 'top 82%',
onEnter: () => {
lines.forEach((line, i) => {
const delay = i * 0.1;
gsap.to(line, {
opacity: 1,
x: 0,
duration: 0.4,
delay,
ease: 'power2.out'
});
// Animate skill bars
const bar = line.querySelector('.term-bar-fill');
if (bar) {
const targetPct = bar.style.getPropertyValue('--pct');
gsap.to(bar, {
width: targetPct,
duration: 1.0,
delay: delay + 0.15,
ease: 'power2.out'
});
}
});
}
});
}
// ══════════════════════════════════════════════════════════════════════
// CONTACT — fade in
// ══════════════════════════════════════════════════════════════════════
function initContact() {
if (reduced) return;
gsap.fromTo('.contact-terminal',
{ opacity: 0, y: 30 },
{
opacity: 1, y: 0,
duration: 0.9,
ease: 'power3.out',
scrollTrigger: {
trigger: '#contact',
start: 'top 85%',
once: true
}
}
);
gsap.fromTo('.contact-row',
{ opacity: 0, x: -20 },
{
opacity: 1, x: 0,
duration: 0.5,
stagger: 0.1,
ease: 'power2.out',
scrollTrigger: {
trigger: '.contact-output',
start: 'top 88%',
once: true
}
}
);
}
// ══════════════════════════════════════════════════════════════════════
// SECTION HEADERS — reveal
// ══════════════════════════════════════════════════════════════════════
function initSectionHeaders() {
if (reduced) return;
document.querySelectorAll('.section-header').forEach(header => {
gsap.fromTo(header,
{ opacity: 0, y: 20 },
{
opacity: 1, y: 0,
duration: 0.7,
ease: 'power2.out',
scrollTrigger: {
trigger: header,
start: 'top 90%',
once: true
}
}
);
});
}
// ══════════════════════════════════════════════════════════════════════
// REDUCED MOTION LISTENER
// ══════════════════════════════════════════════════════════════════════
window.addEventListener('motion-preference', (e) => {
// If user toggles motion mid-session, refresh page to reinitialize
// (complex animation state makes incremental change impractical)
location.reload();
});
// ══════════════════════════════════════════════════════════════════════
// BOOT
// ══════════════════════════════════════════════════════════════════════
initNeuralNet();
initHeroText();
initPaperCards();
initLossCanvas();
initConfusionMatrix();
initTerminal();
initContact();
initSectionHeaders();