Rework portfolio as a bento grid

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ayushya Amitabh
2026-10-05 22:34:08 -04:00
co-authored by Claude Sonnet 5.5
parent ac827f1b4f
commit 99b4151fa6
+116 -287
View File
@@ -11,11 +11,13 @@
:root{
--bg:#fafaf9; --fg:#141414; --muted:#6b6b68; --accent:#2563eb;
--border:#141414; --card-bg:#ffffff; --divider:#dcdcd8; --limb:#c7c7c2;
--accent-bg:#2563eb; --accent-fg:#ffffff;
}
@media (prefers-color-scheme: dark){
:root{
--bg:#0b0d0f; --fg:#f2f2f0; --muted:#9a9a95; --accent:#64a1fb;
--border:#f2f2f0; --card-bg:#141719; --divider:#2b2e31; --limb:#33373b;
--accent-bg:#64a1fb; --accent-fg:#0b0d0f;
}
}
*{box-sizing:border-box;}
@@ -23,53 +25,50 @@ html,body{margin:0;padding:0;}
body{background:var(--bg);color:var(--fg);font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;}
a{color:inherit;text-decoration:none;}
h1,h2,h3,p{margin:0;}
button{font-family:inherit;}
.page{width:100%;min-height:100vh;display:flex;flex-direction:column;}
.layout-grid{display:grid;grid-template-columns:1fr 190px 1fr;gap:48px;max-width:1180px;width:100%;margin:0 auto;padding:72px 32px 56px;align-items:start;}
.page{max-width:1120px;margin:0 auto;padding:40px 24px 56px;}
.name-box{border:1px solid var(--border);border-radius:14px;padding:26px 30px;width:100%;}
.name-box h1{font-size:26px;font-weight:700;letter-spacing:0.03em;text-align:center;}
/* Bento grid: 4 columns, 160px rows */
.bento{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:160px;gap:16px;}
.tile{position:relative;overflow:hidden;background:var(--card-bg);border:1px solid var(--divider);border-radius:20px;padding:22px;display:flex;flex-direction:column;justify-content:space-between;transition:border-color .15s ease,transform .15s ease;}
a.tile:hover{border-color:var(--border);transform:translateY(-2px);}
a.tile:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
.label{font-size:11px;letter-spacing:0.14em;color:var(--muted);}
.arrow{font-size:18px;color:var(--accent);align-self:flex-end;transition:transform .15s ease;}
a.tile:hover .arrow{transform:translate(3px,-3px);}
.left-col{display:flex;flex-direction:column;gap:32px;min-width:0;}
.projects-block{display:flex;flex-direction:column;gap:16px;margin-top:48px;}
.stack-row{display:flex;align-items:center;gap:14px;position:relative;}
.hero{grid-column:span 2;grid-row:span 2;border-color:var(--border);}
.hero h1{font-size:clamp(28px,4.4vw,48px);font-weight:700;letter-spacing:0.02em;line-height:1.05;}
.hero h1 span{color:var(--accent);}
.hero p.tagline{font-size:13px;line-height:1.6;color:var(--muted);max-width:34ch;}
.link-btn{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--accent);color:var(--accent);border-radius:8px;padding:11px 20px;font-size:13px;font-weight:600;letter-spacing:0.06em;font-family:inherit;background:transparent;cursor:pointer;transition:background .15s ease,color .15s ease;}
.link-btn:hover{background:var(--accent);color:var(--card-bg);}
.robot-tile{grid-column:span 1;grid-row:span 2;align-items:center;justify-content:center;}
.project{grid-column:span 1;grid-row:span 1;gap:10px;justify-content:flex-start;}
.project.wide{grid-column:span 2;}
.avatar{width:40px;height:40px;border-radius:50%;border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--accent);flex-shrink:0;}
.project h3{font-size:13px;font-weight:700;letter-spacing:0.04em;}
.project p.desc{font-size:12px;line-height:1.5;color:var(--muted);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
.project .arrow{position:absolute;top:18px;right:20px;}
.card-link{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--accent);color:var(--accent);border-radius:8px;padding:9px 18px;font-size:12px;font-weight:700;letter-spacing:0.06em;transition:background .15s ease,color .15s ease;align-self:flex-start;}
.card-link:hover{background:var(--accent);color:var(--card-bg);}
.social{align-items:flex-start;}
.social .name{font-size:15px;font-weight:700;letter-spacing:0.06em;}
.accent-tile{background:var(--accent-bg);color:var(--accent-fg);border-color:var(--accent-bg);}
.accent-tile .label{color:inherit;opacity:.75;}
.accent-tile .arrow{color:inherit;}
.email{grid-column:span 2;}
.email .addr{font-size:clamp(14px,2.2vw,20px);font-weight:700;letter-spacing:0.04em;word-break:break-all;}
.nav-btn{width:40px;height:40px;min-width:40px;min-height:40px;border-radius:50%;border:1px solid var(--border);background:var(--card-bg);color:var(--fg);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background .15s ease,color .15s ease;}
.nav-btn:hover{background:var(--fg);color:var(--bg);}
.nav-btn:hover .tri{border-bottom-color:var(--bg);}
.nav-btn:hover .tri-down{border-top-color:var(--bg);}
.nav-btn[data-disabled="true"]{opacity:0.32;pointer-events:none;}
.tri{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:8px solid var(--accent);transition:border-color .15s ease;}
.tri-down{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:8px solid var(--accent);transition:border-color .15s ease;}
.nav-col{position:absolute;right:-58px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:10px;flex-shrink:0;}
.stack-scroll{scrollbar-width:none;-ms-overflow-style:none;}
.stack-scroll::-webkit-scrollbar{display:none;width:0;height:0;}
.stack-card-top{display:flex;flex-direction:column;gap:14px;}
.avatar{width:52px;height:52px;border-radius:50%;border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:var(--accent);flex-shrink:0;}
.card-text{display:flex;flex-direction:column;gap:6px;}
.card-text h3{font-size:15px;font-weight:700;letter-spacing:0.02em;}
.desc-clamp{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;font-size:13px;line-height:1.5;color:var(--muted);}
.projects-label{text-align:center;font-size:11px;letter-spacing:0.14em;color:var(--muted);}
.mid-col{display:flex;flex-direction:column;align-items:stretch;gap:16px;height:100%;align-self:stretch;}
.divider-v{width:1px;flex:1 1 auto;background:var(--divider);margin:8px auto;}
.email-link{font-size:12px;letter-spacing:0.06em;color:var(--muted);text-align:center;}
.robot-col{display:flex;flex-direction:column;align-items:center;gap:28px;padding-top:6px;}
.marquee-wrap{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);width:100%;max-width:260px;font-size:12px;letter-spacing:0.08em;color:var(--muted);}
.marquee-track{display:inline-block;white-space:nowrap;animation:scrollLeft 16s linear infinite;}
.terminal{grid-column:span 2;font-size:12px;color:var(--muted);}
.marquee-wrap{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);letter-spacing:0.08em;}
.marquee-track{display:inline-block;white-space:nowrap;animation:scrollLeft 20s linear infinite;}
@keyframes scrollLeft{from{transform:translateX(0);}to{transform:translateX(-50%);}}
.prompt{color:var(--fg);}
.prompt b{color:var(--accent);}
.cursor{display:inline-block;width:8px;height:14px;background:var(--accent);vertical-align:-2px;animation:blink 1s steps(1) infinite;}
@keyframes blink{50%{opacity:0;}}
.robot{display:flex;flex-direction:column;align-items:center;}
/* Robot (scaled to fit the tile) */
.robot{display:flex;flex-direction:column;align-items:center;transform:scale(.62);transform-origin:center;}
.antenna-wire{width:2px;height:20px;background:var(--fg);}
.antenna-seg-1{width:2px;height:12px;background:var(--fg);transform:rotate(28deg) translateX(3px);}
.antenna-seg-2{width:2px;height:12px;background:var(--fg);transform:rotate(-28deg) translateX(-3px);margin-top:-2px;}
@@ -86,80 +85,53 @@ button{font-family:inherit;}
.leg{width:20px;height:78px;border-radius:10px;background:var(--limb);}
@media (max-width: 880px){
.layout-grid{grid-template-columns:1fr !important;}
.divider-v{display:none !important;}
.robot-col{padding-top:0 !important;}
.stack-row{flex-direction:column;align-items:stretch;}
.nav-col{flex-direction:row !important;justify-content:center;position:static;transform:none;right:auto;top:auto;}
.tri{transform:rotate(-90deg);}
.tri-down{transform:rotate(-90deg);}
.bento{grid-template-columns:repeat(2,1fr);}
.hero{grid-column:span 2;grid-row:span 1;}
.robot-tile{grid-column:span 1;grid-row:span 2;}
.project.wide{grid-column:span 2;}
}
@media (max-width: 520px){
.page{padding:24px 16px 40px;}
.bento{grid-auto-rows:minmax(150px,auto);gap:12px;}
.robot-tile{grid-column:span 2;grid-row:span 1;min-height:260px;}
.project,.project.wide,.social,.email,.terminal{grid-column:span 2;}
.social{grid-column:span 1;}
}
@media (prefers-reduced-motion: reduce){
.marquee-track,.cursor{animation:none;}
a.tile,.arrow{transition:none;}
}
</style>
</head>
<body>
<div class="page">
<div class="layout-grid">
<main class="page">
<div class="bento">
<div class="left-col">
<div class="name-box"><h1>AYUSHYA.DEV</h1></div>
<div class="projects-block">
<div class="stack-row">
<div class="stack-scroll" id="stackScroll">
<div class="spacer" id="spacer" style="position:relative;">
<div class="layer" id="layer" style="position:absolute;top:0;left:0;">
<!-- cards injected by JS -->
<section class="tile hero">
<p class="label">PORTFOLIO</p>
<div>
<h1>AYUSHYA<span>.DEV</span></h1>
</div>
</div>
</div>
<div class="nav-col">
<button class="nav-btn" id="prevBtn" aria-label="Show previous project" type="button">
<span class="tri"></span>
</button>
<button class="nav-btn" id="nextBtn" aria-label="Show next project" type="button">
<span class="tri-down"></span>
</button>
</div>
</div>
<p class="projects-label">PROJECTS</p>
</div>
</div>
<div class="mid-col">
<a class="link-btn" href="https://github.com/ayushyamitabh/" target="_blank" rel="noopener noreferrer">GITHUB</a>
<a class="link-btn" href="https://linkedin.com/in/ayushyamitabh" target="_blank" rel="noopener noreferrer">LINKEDIN</a>
<div class="divider-v"></div>
<a class="email-link" href="mailto:hello@ayushya.dev">HELLO@AYUSHYA.DEV</a>
</div>
<div class="robot-col">
<div class="marquee-wrap">
<span class="marquee-track" id="marquee"></span>
</div>
<p class="tagline">Building apps, tools, and small corners of the web.</p>
</section>
<div class="tile robot-tile" aria-hidden="true">
<div class="robot">
<div class="antenna-wire"></div>
<div class="antenna-seg-1"></div>
<div class="antenna-seg-2"></div>
<div class="antenna-tip"></div>
<div class="head">
<div class="eye-ball" id="eyeLeft"><div class="pupil" id="pupilLeft"></div></div>
<div class="eye-ball" id="eyeRight"><div class="pupil" id="pupilRight"></div></div>
</div>
<div class="neck-gap"></div>
<div class="body-row">
<div class="arm"></div>
<div class="torso"></div>
<div class="arm"></div>
</div>
<div class="leg-gap"></div>
<div class="legs-row">
<div class="leg"></div>
<div class="leg"></div>
@@ -167,195 +139,56 @@ button{font-family:inherit;}
</div>
</div>
<a class="tile social accent-tile" href="https://github.com/ayushyamitabh/" target="_blank" rel="noopener noreferrer">
<span class="label">CODE</span>
<span class="name">GITHUB</span>
<span class="arrow">&#8599;</span>
</a>
<a class="tile social" href="https://linkedin.com/in/ayushyamitabh" target="_blank" rel="noopener noreferrer">
<span class="label">NETWORK</span>
<span class="name">LINKEDIN</span>
<span class="arrow">&#8599;</span>
</a>
<a class="tile project wide" href="https://noo.ayushya.dev" target="_blank" rel="noopener noreferrer">
<div class="avatar">N</div>
<h3>NOO</h3>
<p class="desc">A fast, native Nextcloud client built with Flutter and Material You &mdash; files, photos, activity, shares, and trash in one themed app.</p>
<span class="arrow">&#8599;</span>
</a>
<a class="tile project" href="https://watchparty-archive.web.app/" target="_blank" rel="noopener noreferrer">
<div class="avatar">WP</div>
<h3>CHROME WATCH PARTY</h3>
<p class="desc">Syncs streamed video across devices, with chat, GIFs, and free group calls.</p>
<span class="arrow">&#8599;</span>
</a>
<a class="tile project" href="https://vaxandme-archive.web.app/" target="_blank" rel="noopener noreferrer">
<div class="avatar">V&amp;M</div>
<h3>VAX &amp; ME</h3>
<p class="desc">Explains expected COVID-19 vaccine side effects, built from FDA data.</p>
<span class="arrow">&#8599;</span>
</a>
<div class="tile terminal" aria-hidden="true">
<p class="label">STATUS</p>
<div class="marquee-wrap"><span class="marquee-track" id="marquee"></span></div>
<p class="prompt"><b>&gt;</b> hello, world <span class="cursor"></span></p>
</div>
<a class="tile email accent-tile" href="mailto:hello@ayushya.dev">
<span class="label">SAY HELLO</span>
<span class="addr">HELLO@AYUSHYA.DEV</span>
<span class="arrow">&#8599;</span>
</a>
</div>
</main>
<script>
(function () {
var PROJECTS = [
{ initials: 'N', name: 'NOO', desc: 'A fast, native Nextcloud client built with Flutter and Material You \u2014 files, photos, activity, shares, and trash in one themed app.', link: 'https://noo.ayushya.dev' },
{ initials: 'WP', name: 'CHROME WATCH PARTY', desc: 'Syncs streamed video across devices so you can watch with your favorite people, with chat, GIFs, and free group calls.', link: 'https://watchparty-archive.web.app/' },
{ initials: 'V&M', name: 'VAX & ME', desc: 'Explains the expected side effects of COVID-19 vaccines, built from FDA data on Pfizer, Moderna, and J&J.', link: 'https://vaxandme-archive.web.app/' },
];
var COUNT = PROJECTS.length;
var PEEK_SCALE = 0.9;
var MOBILE_BREAKPOINT = 880;
var MOBILE_CONTAINER_HEIGHT = 260;
var DESKTOP_STEP = 160;
var DESKTOP_VIEW = 318;
var DESKTOP_CARD = 250;
var stackScroll = document.getElementById('stackScroll');
var spacer = document.getElementById('spacer');
var layer = document.getElementById('layer');
var prevBtn = document.getElementById('prevBtn');
var nextBtn = document.getElementById('nextBtn');
var cardEls = [];
var isMobile = window.matchMedia('(max-width: ' + MOBILE_BREAKPOINT + 'px)').matches;
var containerWidth = 320;
var scrollPos = 0;
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function buildCards() {
PROJECTS.forEach(function (proj) {
var card = document.createElement('div');
card.innerHTML =
'<div class="stack-card-top">' +
'<div class="avatar">' + escapeHtml(proj.initials) + '</div>' +
'<div class="card-text">' +
'<h3>' + escapeHtml(proj.name) + '</h3>' +
'<p class="desc-clamp">' + escapeHtml(proj.desc) + '</p>' +
'</div>' +
'</div>' +
'<a class="card-link" href="' + proj.link + '" target="_blank" rel="noopener noreferrer">LINK</a>';
layer.appendChild(card);
cardEls.push(card);
});
}
function getGeometry() {
if (isMobile) {
var cw = containerWidth;
var peek = Math.max(16, Math.round(cw * 0.1));
var cardSize = Math.round(Math.max(cw - peek * 2, 120));
var step = Math.round(cardSize * 0.64);
return { axisProp: 'left', sizeProp: 'width', crossSizeProp: 'height', peek: peek, cardSize: cardSize, step: step, viewSize: cw };
}
return { axisProp: 'top', sizeProp: 'height', crossSizeProp: 'width', peek: 34, cardSize: DESKTOP_CARD, step: DESKTOP_STEP, viewSize: DESKTOP_VIEW };
}
function lerp(a, b, t) { return a + (b - a) * t; }
var EPS = 1.05;
function render() {
var g = getGeometry();
var maxScroll = (COUNT - 1) * g.step;
var clampedPos = Math.max(0, Math.min(scrollPos, maxScroll));
var progress = clampedPos / g.step;
if (isMobile) {
stackScroll.style.cssText = 'position:relative;height:' + MOBILE_CONTAINER_HEIGHT + 'px;width:100%;overflow-x:auto;overflow-y:hidden;min-width:0;-webkit-overflow-scrolling:touch;';
spacer.style.cssText = 'position:relative;width:' + (g.viewSize + maxScroll) + 'px;height:100%;';
layer.style.cssText = 'position:absolute;top:0;left:0;width:' + g.viewSize + 'px;height:100%;transform:translateX(' + clampedPos + 'px);';
} else {
stackScroll.style.cssText = 'position:relative;height:' + g.viewSize + 'px;flex:1 1 auto;min-width:0;overflow-y:auto;overflow-x:hidden;';
spacer.style.cssText = 'position:relative;height:' + (g.viewSize + maxScroll) + 'px;width:100%;';
layer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:' + g.viewSize + 'px;transform:translateY(' + clampedPos + 'px);';
}
for (var i = 0; i < COUNT; i++) {
var delta = i - progress;
var pos, scale, opacity;
if (delta < -EPS || delta > EPS) {
pos = g.peek; scale = PEEK_SCALE; opacity = 0;
} else if (delta <= 0) {
var t1 = Math.min(-delta, 1);
pos = lerp(g.peek, 0, t1);
scale = lerp(1, PEEK_SCALE, t1);
opacity = lerp(1, 0.55, t1);
} else {
var t2 = Math.min(delta, 1);
pos = lerp(g.peek, g.peek * 2, t2);
scale = lerp(1, PEEK_SCALE, t2);
opacity = lerp(1, 0.55, t2);
}
var zIndex = Math.round((1 - Math.min(Math.abs(delta), 1)) * 100);
var pointerEvents = Math.abs(delta) < 0.05 ? 'auto' : 'none';
var otherAxis = isMobile ? 'top:0;' : 'left:0;';
var sizeStyle = g.sizeProp + ':' + g.cardSize + 'px; ' + g.crossSizeProp + ':100%;';
cardEls[i].style.cssText =
'position:absolute; ' + g.axisProp + ':' + pos + 'px; ' + otherAxis + ' ' + sizeStyle +
' z-index:' + zIndex + '; opacity:' + opacity + '; pointer-events:' + pointerEvents + '; transform: scale(' + scale + ');' +
' padding:24px; background:var(--card-bg); border:1px solid var(--border); border-radius:16px; display:flex; flex-direction:column; justify-content:space-between; gap:14px; box-shadow:0 10px 24px rgba(0,0,0,0.12);';
}
var activeIndex = Math.round(progress);
prevBtn.setAttribute('data-disabled', activeIndex <= 0 ? 'true' : 'false');
nextBtn.setAttribute('data-disabled', activeIndex >= COUNT - 1 ? 'true' : 'false');
}
function getStep() {
return getGeometry().step;
}
function nextCard() {
var step = getStep();
var active = Math.round(scrollPos / step);
var target = Math.min(active + 1, COUNT - 1) * step;
if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' });
else stackScroll.scrollTo({ top: target, behavior: 'smooth' });
}
function prevCard() {
var step = getStep();
var active = Math.round(scrollPos / step);
var target = Math.max(active - 1, 0) * step;
if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' });
else stackScroll.scrollTo({ top: target, behavior: 'smooth' });
}
function measure() {
var rect = stackScroll.getBoundingClientRect();
if (rect.width > 0) containerWidth = rect.width;
render();
}
buildCards();
measure();
prevBtn.addEventListener('click', prevCard);
nextBtn.addEventListener('click', nextCard);
var scrollRafPending = false;
var snapTimer = null;
function snapToNearest() {
var step = getStep();
var maxIndex = COUNT - 1;
var nearest = Math.max(0, Math.min(Math.round(scrollPos / step), maxIndex));
var target = nearest * step;
if (Math.abs(scrollPos - target) > 1) {
if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' });
else stackScroll.scrollTo({ top: target, behavior: 'smooth' });
}
}
stackScroll.addEventListener('scroll', function () {
if (scrollRafPending) return;
scrollRafPending = true;
requestAnimationFrame(function () {
scrollRafPending = false;
scrollPos = isMobile ? stackScroll.scrollLeft : stackScroll.scrollTop;
render();
});
clearTimeout(snapTimer);
snapTimer = setTimeout(snapToNearest, 130);
});
var mq = window.matchMedia('(max-width: ' + MOBILE_BREAKPOINT + 'px)');
function handleMqChange(e) {
stackScroll.scrollTo({ top: 0, left: 0 });
isMobile = e.matches;
scrollPos = 0;
measure();
}
if (mq.addEventListener) mq.addEventListener('change', handleMqChange);
else if (mq.addListener) mq.addListener(handleMqChange);
window.addEventListener('resize', measure);
if (typeof ResizeObserver !== 'undefined') {
new ResizeObserver(measure).observe(stackScroll);
}
var binary = (function (len) {
var out = '';
for (var i = 0; i < len; i++) {
@@ -366,32 +199,28 @@ button{font-family:inherit;}
})(80);
document.getElementById('marquee').textContent = binary + ' ' + binary;
var eyeLeft = document.getElementById('eyeLeft');
var eyeRight = document.getElementById('eyeRight');
var pupilLeft = document.getElementById('pupilLeft');
var pupilRight = document.getElementById('pupilRight');
var pairs = [
[document.getElementById('eyeLeft'), document.getElementById('pupilLeft')],
[document.getElementById('eyeRight'), document.getElementById('pupilRight')]
];
var MAX_OFFSET = 8;
var rafPending = false;
function updatePupils(clientX, clientY) {
[[eyeLeft, pupilLeft], [eyeRight, pupilRight]].forEach(function (pair) {
var eye = pair[0], pupil = pair[1];
var rect = eye.getBoundingClientRect();
pairs.forEach(function (pair) {
var rect = pair[0].getBoundingClientRect();
var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2;
var angle = Math.atan2(clientY - cy, clientX - cx);
pupil.style.transform = 'translate(' + (Math.cos(angle) * MAX_OFFSET) + 'px, ' + (Math.sin(angle) * MAX_OFFSET) + 'px)';
pair[1].style.transform = 'translate(' + (Math.cos(angle) * MAX_OFFSET) + 'px, ' + (Math.sin(angle) * MAX_OFFSET) + 'px)';
});
}
window.addEventListener('mousemove', function (e) {
if (rafPending) return;
rafPending = true;
var clientX = e.clientX, clientY = e.clientY;
requestAnimationFrame(function () {
rafPending = false;
updatePupils(clientX, clientY);
});
var x = e.clientX, y = e.clientY;
requestAnimationFrame(function () { rafPending = false; updatePupils(x, y); });
});
})();
</script>