/* ══════════════════════════════════════════ 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 = '
No entries available yet.
'; 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 ? `` : ``; const memberHTML = `
${member.image ? `${member.name}` : ''}
${member.initials}
${member.name}
${member.role}
${member.focus}
${linksSection}
`; 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 `${icon}${safeLabel}`; } const isMail = url.startsWith('mailto:'); const extraAttrs = isMail ? '' : ' target="_blank" rel="noopener noreferrer"'; return `${icon}${safeLabel}`; } function getMemberLinkIcon(label) { const key = label.toLowerCase().replace(/\s+/g, ''); const icons = { email: '', linkedin: '', huggingface: '', googlescholar: '' }; return icons[key] || ''; } (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 => `${pill}` ).join(''); const cardHTML = `
${project.tag}

${project.title}

${project.desc}

`; 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 = '
No entries available yet.
'; return; } items.forEach((pub, idx) => { const delayClass = ['delay-1', 'delay-2', 'delay-3', 'delay-4'][idx % 4] || 'delay-1'; const linkHTML = pub.url ? ` ` : ''; const itemHTML = `
${pub.year}
${pub.title}
${pub.meta}
${linkHTML}
`; 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 = ` `; 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 ? `Read more →` : ''; const cardHTML = `
${news.type}
${news.date}

${news.title}

${news.body}

${linkHTML}
`; 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(); });