Arunraj_Lab / main.js
Arunraj B
update
89a8570
/* ══════════════════════════════════════════
RECOVER LAB — main.js
Advanced interactions & animations
══════════════════════════════════════════ */
'use strict';
/* ── LOADER ── */
(function initLoader() {
const loader = document.getElementById('loader');
const fill = document.getElementById('loaderFill');
if (!loader || !fill) return;
let progress = 0;
const tick = setInterval(() => {
progress += Math.random() * 18 + 5;
if (progress >= 100) {
progress = 100;
clearInterval(tick);
setTimeout(() => loader.classList.add('hidden'), 300);
}
fill.style.width = progress + '%';
}, 60);
})();
/* ── HERO CANVAS — particle network ── */
(function initCanvas() {
const canvas = document.getElementById('heroCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let W, H, particles = [], mouse = { x: -9999, y: -9999 };
const PARTICLE_COUNT = 80;
const CONNECT_DIST = 120;
const MOUSE_RADIUS = 160;
function getAccent() {
return getComputedStyle(document.documentElement)
.getPropertyValue('--accent').trim() || '#00c2aa';
}
class Particle {
constructor() { this.reset(); }
reset() {
this.x = Math.random() * W;
this.y = Math.random() * H;
this.vx = (Math.random() - 0.5) * 0.4;
this.vy = (Math.random() - 0.5) * 0.4;
this.r = Math.random() * 2 + 1;
this.base = { x: this.x, y: this.y };
}
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > W) this.vx *= -1;
if (this.y < 0 || this.y > H) this.vy *= -1;
// mouse repulsion
const dx = this.x - mouse.x;
const dy = this.y - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < MOUSE_RADIUS) {
const force = (MOUSE_RADIUS - dist) / MOUSE_RADIUS;
this.x += dx / dist * force * 2;
this.y += dy / dist * force * 2;
}
}
draw(accent) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fillStyle = accent;
ctx.globalAlpha = 0.5;
ctx.fill();
ctx.globalAlpha = 1;
}
}
function resize() {
W = canvas.width = canvas.offsetWidth;
H = canvas.height = canvas.offsetHeight;
particles = Array.from({ length: PARTICLE_COUNT }, () => new Particle());
}
function draw() {
ctx.clearRect(0, 0, W, H);
const accent = getAccent();
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].draw(accent);
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < CONNECT_DIST) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = accent;
ctx.globalAlpha = (1 - d / CONNECT_DIST) * 0.2;
ctx.lineWidth = 0.8;
ctx.stroke();
ctx.globalAlpha = 1;
}
}
}
requestAnimationFrame(draw);
}
window.addEventListener('resize', resize);
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
canvas.addEventListener('mouseleave', () => { mouse.x = -9999; mouse.y = -9999; });
resize();
draw();
})();
/* ── NAV SCROLL ── */
(function initNav() {
const nav = document.getElementById('nav');
window.addEventListener('scroll', () => {
nav.classList.toggle('scrolled', window.scrollY > 40);
}, { passive: true });
// Hamburger
const hamburger = document.getElementById('hamburger');
const navLinks = document.getElementById('navLinks');
if (!hamburger || !navLinks) return;
hamburger.addEventListener('click', () => {
navLinks.classList.toggle('open');
const spans = hamburger.querySelectorAll('span');
const isOpen = navLinks.classList.contains('open');
spans[0].style.transform = isOpen ? 'translateY(6.5px) rotate(45deg)' : '';
spans[1].style.opacity = isOpen ? '0' : '';
spans[2].style.transform = isOpen ? 'translateY(-6.5px) rotate(-45deg)' : '';
});
navLinks.querySelectorAll('a').forEach(a =>
a.addEventListener('click', () => {
navLinks.classList.remove('open');
hamburger.querySelectorAll('span').forEach(s => { s.style.transform = ''; s.style.opacity = ''; });
})
);
})();
/* ── THEME PANEL ── */
(function initTheme() {
const html = document.documentElement;
if (!html.dataset.mode) html.dataset.mode = 'dark';
})();
/* ── SCROLL REVEAL ── */
(function initReveal() {
const els = document.querySelectorAll('.reveal-up, .reveal-left, .reveal-right');
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('visible');
obs.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
els.forEach(el => {
obs.observe(el);
// Check if element is already in view
const rect = el.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
el.classList.add('visible');
obs.unobserve(el);
}
});
})();
/* ── COUNTER ANIMATION ── */
(function initCounters() {
const stats = document.querySelectorAll('.hero-stat');
if (!stats.length) return;
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) return;
const el = e.target;
const target = parseInt(el.dataset.count);
const suffix = el.dataset.suffix || '';
const numEl = el.querySelector('.stat-n');
const prefix = numEl.textContent.startsWith('$') ? '$' : '';
let current = 0;
const step = target / 40;
const timer = setInterval(() => {
current += step;
if (current >= target) {
current = target;
clearInterval(timer);
}
numEl.textContent = prefix + Math.floor(current) + suffix;
}, 30);
obs.unobserve(el);
});
}, { threshold: 0.5 });
stats.forEach(s => obs.observe(s));
})();
/* ── ACTIVE NAV LINK ON SCROLL ── */
(function initActiveNav() {
const navLinks = document.querySelectorAll('.nav-link');
if (!navLinks.length) return;
const page = window.location.pathname.split('/').pop() || 'index.html';
const pageMap = {
'index.html': ['#about', '#contact'],
'research.html': ['research.html'],
'team.html': ['team.html'],
'publications.html': ['publications.html'],
'news.html': ['news.html']
};
function setActiveByHref(href) {
navLinks.forEach(l => l.classList.toggle('active', l.getAttribute('href') === href));
}
if (page !== 'index.html') {
const matches = pageMap[page];
if (matches && matches.length) setActiveByHref(matches[0]);
return;
}
const sections = document.querySelectorAll('section[id]');
setActiveByHref('#about');
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
const targetHref = `#${e.target.id}`;
if (targetHref === '#about' || targetHref === '#contact') {
setActiveByHref(targetHref);
}
}
});
}, { threshold: 0.4 });
sections.forEach(s => obs.observe(s));
})();
/* ── CONTACT FORM ── */
(function initForm() {
const form = document.getElementById('contactForm');
if (!form) return;
form.addEventListener('submit', e => {
e.preventDefault();
const formData = new FormData(form);
const firstName = (formData.get('firstName') || '').toString().trim();
const lastName = (formData.get('lastName') || '').toString().trim();
const email = (formData.get('email') || '').toString().trim();
const inquiryType = (formData.get('inquiryType') || 'General Question').toString().trim();
const message = (formData.get('message') || '').toString().trim();
const sender = [firstName, lastName].filter(Boolean).join(' ') || 'Website visitor';
const subject = encodeURIComponent(`RECOVER Lab Inquiry: ${inquiryType}`);
const body = encodeURIComponent(
`Name: ${sender}\nEmail: ${email || 'Not provided'}\nInquiry Type: ${inquiryType}\n\nMessage:\n${message || 'No message provided.'}`
);
const btn = form.querySelector('button[type="submit"]');
const orig = btn.textContent;
btn.textContent = 'Opening Email Draft...';
btn.style.background = 'var(--accent-2)';
btn.disabled = true;
window.location.href = `mailto:arunrajtpr19@gmail.com?subject=${subject}&body=${body}`;
setTimeout(() => {
btn.textContent = orig;
btn.style.background = '';
btn.disabled = false;
form.reset();
}, 3000);
});
})();
/* ── PARALLAX HERO CONTENT ── */
(function initParallax() {
const hero = document.getElementById('hero');
const content = hero ? hero.querySelector('.hero-content') : null;
if (!content) return;
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
if (scrollY < window.innerHeight) {
content.style.transform = `translateY(${scrollY * 0.2}px)`;
content.style.opacity = 1 - scrollY / (window.innerHeight * 0.75);
}
}, { passive: true });
})();
/* ── MAGNETIC BUTTONS ── */
(function initMagnetic() {
document.querySelectorAll('.btn-glow, .btn-ghost').forEach(btn => {
btn.addEventListener('mousemove', e => {
const rect = btn.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = (e.clientX - cx) * 0.2;
const dy = (e.clientY - cy) * 0.2;
btn.style.transform = `translate(${dx}px, ${dy}px) translateY(-2px)`;
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = '';
});
});
})();
/* ── DATA RENDERING FUNCTIONS ── */
// Fetch JSON data from /data/ directory
async function fetchJSON(filename) {
try {
const response = await fetch(`./data/${filename}`);
if (!response.ok) throw new Error(`Failed to fetch ${filename}`);
return await response.json();
} catch (error) {
console.error(`Error fetching ${filename}:`, error);
return null;
}
}
// Render team members from JSON
async function renderTeamData() {
const piGrid = document.querySelector('#teamPiGrid');
const researchersGrid = document.querySelector('#teamResearchersGrid');
const studentsGrid = document.querySelector('#teamStudentsGrid');
const researchersSection = document.querySelector('#teamResearchersSection');
if (!piGrid || !researchersGrid || !studentsGrid) return;
const data = await fetchJSON('team.json');
if (!data) return;
const piMembers = [];
const studentMembers = [];
const researcherMembers = [];
data.forEach(member => {
const role = (member.role || '').toLowerCase();
if (role.includes('principal investigator')) piMembers.push(member);
else if (role.includes('student') || role.includes('candidate')) studentMembers.push(member);
else researcherMembers.push(member);
});
renderTeamGroup(piGrid, piMembers);
renderTeamGroup(researchersGrid, researcherMembers);
renderTeamGroup(studentsGrid, studentMembers);
if (researchersSection) {
researchersSection.style.display = researcherMembers.length ? '' : 'none';
}
// Re-initialize scroll reveal for new elements
initRevealForNewElements();
}
function renderTeamGroup(grid, members) {
grid.innerHTML = '';
if (!members.length) {
grid.innerHTML = '<div class="member"><div class="member-info"><div class="member-focus">No entries available yet.</div></div></div>';
return;
}
members.forEach((member, idx) => {
const delayClass = ['delay-1', 'delay-2', 'delay-3', 'delay-4'][idx % 4] || 'delay-1';
const linksHTML = (member.links || []).map(link => renderMemberLink(link)).join('');
const linksSection = linksHTML
? `<div class="member-links">${linksHTML}</div>`
: `<div class="member-links-placeholder">Coming soon</div>`;
const memberHTML = `
<div class="member reveal-up ${delayClass}">
<div class="member-img">
${member.image ? `<img src="${member.image}" alt="${member.name}" class="member-photo" loading="lazy" />` : ''}
<div class="member-initials-wrap${member.image ? ' is-fallback' : ''}">${member.initials}</div>
<div class="member-glow"></div>
</div>
<div class="member-info">
<div class="member-name">${member.name}</div>
<div class="member-role">${member.role}</div>
<div class="member-focus">${member.focus}</div>
${linksSection}
</div>
</div>
`;
grid.innerHTML += memberHTML;
});
}
function renderMemberLink(link) {
const label = (link.label || '').trim();
const url = (link.url || '').trim();
const icon = getMemberLinkIcon(label);
const safeLabel = label || 'Profile';
if (!url) {
return `<span class="mlink is-disabled" title="Add ${safeLabel} link in data/team.json" aria-disabled="true">${icon}<span class="sr-only">${safeLabel}</span></span>`;
}
const isMail = url.startsWith('mailto:');
const extraAttrs = isMail ? '' : ' target="_blank" rel="noopener noreferrer"';
return `<a href="${url}" class="mlink" aria-label="${safeLabel}" title="${safeLabel}"${extraAttrs}>${icon}<span class="sr-only">${safeLabel}</span></a>`;
}
function getMemberLinkIcon(label) {
const key = label.toLowerCase().replace(/\s+/g, '');
const icons = {
email: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3.8 6.5L12 12.8l8.2-6.3" fill="none" stroke="#EA4335" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.5 7.2v9.6h15V7.2" fill="#ffffff" stroke="#DADCE0" stroke-width="1.2"/><path d="M4.5 16.8l5.4-5" fill="none" stroke="#34A853" stroke-width="1.8" stroke-linecap="round"/><path d="M19.5 16.8l-5.4-5" fill="none" stroke="#4285F4" stroke-width="1.8" stroke-linecap="round"/><path d="M4.5 7.2l3.4 2.6" fill="none" stroke="#FBBC05" stroke-width="1.8" stroke-linecap="round"/></svg>',
linkedin: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="3" fill="#0A66C2"/><circle cx="8" cy="8" r="1.3" fill="#ffffff"/><path d="M7 10.3h2v6.2H7zM11 10.3h1.9v.9c.5-.7 1.4-1.1 2.5-1.1 2 0 3.1 1.3 3.1 3.8v2.6h-2v-2.4c0-1.4-.5-2.1-1.6-2.1-1.2 0-1.9.8-1.9 2.1v2.4h-2z" fill="#ffffff"/></svg>',
huggingface: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" fill="#FFDA5A"/><circle cx="8.8" cy="11" r="1.1" fill="#3B2F2F"/><circle cx="15.2" cy="11" r="1.1" fill="#3B2F2F"/><path d="M8.7 14c.9 1 2 1.5 3.3 1.5s2.4-.5 3.3-1.5" fill="none" stroke="#3B2F2F" stroke-width="1.8" stroke-linecap="round"/><path d="M6.6 10.4c-.2-1.6.3-3 1.4-4.2M17.4 10.4c.2-1.6-.3-3-1.4-4.2" fill="none" stroke="#F59E0B" stroke-width="1.5" stroke-linecap="round"/></svg>',
googlescholar: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4l8 4.2-8 4.2-8-4.2L12 4z" fill="#4285F4"/><path d="M7.2 10.4v4.1c0 1.7 2.1 3.1 4.8 3.1s4.8-1.4 4.8-3.1v-4.1L12 13z" fill="#7BAAF7"/><circle cx="17.8" cy="15.6" r="2.2" fill="#34A853"/><path d="M17.8 14.5v2.2M16.7 15.6H19" stroke="#ffffff" stroke-width="1.2" stroke-linecap="round"/></svg>'
};
return icons[key] || '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>';
}
(function initAssetFallbacks() {
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('error', event => {
if (event.target.classList && event.target.classList.contains('member-photo')) {
const wrap = event.target.closest('.member-img');
if (!wrap) return;
event.target.remove();
const fallback = wrap.querySelector('.member-initials-wrap');
if (fallback) fallback.classList.remove('is-fallback');
}
if (event.target.classList && event.target.classList.contains('carousel-image')) {
event.target.classList.add('is-missing');
}
}, true);
});
})();
(function initCarousel() {
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-carousel').forEach(carousel => {
const track = carousel.querySelector('.carousel-track');
const prev = carousel.querySelector('.carousel-prev');
const next = carousel.querySelector('.carousel-next');
if (!track || !prev || !next) return;
let index = 0;
const interval = Number(carousel.dataset.interval || 5000);
function getSlides() {
return Array.from(track.children);
}
function updateCarousel() {
track.style.transform = `translateX(-${index * 100}%)`;
}
prev.addEventListener('click', () => {
const slides = getSlides();
if (!slides.length) return;
index = (index - 1 + slides.length) % slides.length;
updateCarousel();
});
next.addEventListener('click', () => {
const slides = getSlides();
if (!slides.length) return;
index = (index + 1) % slides.length;
updateCarousel();
});
setInterval(() => {
const slides = getSlides();
if (slides.length <= 1) return;
index = (index + 1) % slides.length;
updateCarousel();
}, interval);
});
});
})();
// Render research projects from JSON
async function renderResearchData() {
const grid = document.querySelector('.research-grid');
if (!grid) return;
const data = await fetchJSON('research.json');
if (!data) return;
grid.innerHTML = '';
data.forEach((project, idx) => {
const delayClass = ['delay-1', 'delay-2', 'delay-3', 'delay-4', 'delay-5', 'delay-6'][idx] || 'delay-1';
const pillsHTML = project.pills.map(pill =>
`<span class="rc-pill">${pill}</span>`
).join('');
const cardHTML = `
<div class="research-card reveal-up ${delayClass}" data-index="${project.index}">
<div class="rc-tag">${project.tag}</div>
<h3 class="rc-title">${project.title}</h3>
<p class="rc-desc">${project.desc}</p>
<div class="rc-footer">
${pillsHTML}
</div>
</div>
`;
grid.innerHTML += cardHTML;
});
initRevealForNewElements();
}
// Render publications from a single JSON source
async function renderPublicationsData() {
const journalsList = document.querySelector('#journalsList');
const conferencesList = document.querySelector('#conferencesList');
const otherOutputsList = document.querySelector('#otherOutputsList');
if (!journalsList || !conferencesList || !otherOutputsList) return;
const data = await fetchJSON('publications.json');
if (!data) return;
const groups = {
journal: [],
conference: [],
other: []
};
data.forEach(pub => {
if (pub.type === 'journal') groups.journal.push(pub);
else if (pub.type === 'conference') groups.conference.push(pub);
else groups.other.push(pub);
});
renderPublicationGroup(journalsList, groups.journal);
renderPublicationGroup(conferencesList, groups.conference);
renderPublicationGroup(otherOutputsList, groups.other);
initRevealForNewElements();
}
function renderPublicationGroup(list, items) {
list.innerHTML = '';
if (!items.length) {
list.innerHTML = '<div class="pub-item"><div class="pub-body"><div class="pub-meta">No entries available yet.</div></div></div>';
return;
}
items.forEach((pub, idx) => {
const delayClass = ['delay-1', 'delay-2', 'delay-3', 'delay-4'][idx % 4] || 'delay-1';
const linkHTML = pub.url
? `<a href="${pub.url}" class="pub-link" aria-label="View publication" target="_blank" rel="noopener noreferrer">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
</a>`
: '';
const itemHTML = `
<div class="pub-item reveal-right ${delayClass}">
<div class="pub-year">${pub.year}</div>
<div class="pub-body">
<div class="pub-title">${pub.title}</div>
<div class="pub-meta">${pub.meta}</div>
</div>
${linkHTML}
</div>
`;
list.innerHTML += itemHTML;
});
}
async function renderPublicationHighlights() {
const track = document.querySelector('#pubHighlightsTrack');
if (!track) return;
const data = await fetchJSON('publication_highlights.json');
if (!data) return;
track.innerHTML = '';
data.forEach((item, idx) => {
const delayClass = ['delay-1', 'delay-2', 'delay-3', 'delay-4'][idx % 4] || 'delay-1';
const slideHTML = `
<article class="carousel-slide pub-highlight-slide reveal-up ${delayClass}">
<div class="pub-highlight-card">
<div class="pub-highlight-media">
<img src="${item.image}" alt="${item.alt}" class="carousel-image pub-highlight-image" />
<div class="carousel-fallback">Add \`${item.image}\`</div>
</div>
<div class="pub-highlight-summary">
<p>${item.summary}</p>
</div>
</div>
</article>
`;
track.innerHTML += slideHTML;
});
}
// Render news items from JSON
async function renderNewsData() {
const grid = document.querySelector('.news-grid');
if (!grid) return;
const data = await fetchJSON('news.json');
if (!data) return;
grid.innerHTML = '';
data.forEach((news, idx) => {
const delayClass = ['delay-1', 'delay-2', 'delay-3', 'delay-4'][idx] || 'delay-1';
const featuredClass = news.featured ? 'featured' : '';
const linkHTML = news.link
? `<a href="${news.link}" class="news-read" target="_blank" rel="noopener noreferrer">Read more →</a>`
: '';
const cardHTML = `
<div class="news-card ${featuredClass} reveal-up ${delayClass}">
<div class="news-type">${news.type}</div>
<div class="news-date">${news.date}</div>
<h3 class="news-title">${news.title}</h3>
<p class="news-body">${news.body}</p>
${linkHTML}
</div>
`;
grid.innerHTML += cardHTML;
});
initRevealForNewElements();
}
// Re-initialize scroll reveal for dynamically added elements
function initRevealForNewElements() {
const els = document.querySelectorAll('.reveal-up, .reveal-left, .reveal-right');
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('visible');
obs.unobserve(e.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
els.forEach(el => {
if (!el.classList.contains('visible')) {
obs.observe(el);
const rect = el.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
el.classList.add('visible');
obs.unobserve(el);
}
}
});
}
// Auto-initialize on specific pages
document.addEventListener('DOMContentLoaded', async () => {
const path = window.location.pathname;
if (path.includes('team.html')) await renderTeamData();
else if (path.includes('research.html')) await renderResearchData();
else if (path.includes('publications.html')) {
await renderPublicationHighlights();
await renderPublicationsData();
}
else if (path.includes('news.html')) await renderNewsData();
});