File manager - Edit - /home/webapp69.cm.in.th/u69319090028/portfolio/script.js
Back
/** * Portfolio — Chinnachot Na Wilaithong * script.js — All interactive behaviors */ document.addEventListener('DOMContentLoaded', () => { /* ============================================================ 0. SHARED REFERENCES (declared first to avoid TDZ errors) ============================================================ */ const header = document.getElementById('main-header'); const sections = document.querySelectorAll('section[id]'); /* ============================================================ 1. SMOOTH SCROLL — All anchor links ============================================================ */ document.querySelectorAll('a[href^="#"]').forEach(link => { link.addEventListener('click', e => { const target = document.querySelector(link.getAttribute('href')); if (!target) return; e.preventDefault(); const offset = header ? header.offsetHeight : 0; const top = target.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top, behavior: 'smooth' }); closeMobileNav(); }); }); /* ============================================================ 2. ACTIVE NAV HIGHLIGHT ============================================================ */ function highlightNav() { let current = ''; const scrollPos = window.scrollY + (header ? header.offsetHeight + 10 : 80); sections.forEach(sec => { if (sec.offsetTop <= scrollPos) current = sec.id; }); document.querySelectorAll('.nav-link').forEach(link => { link.classList.remove('active'); if (link.getAttribute('href') === '#' + current) { link.classList.add('active'); } }); } /* ============================================================ 3. STICKY HEADER — scrolled class + back-to-top visibility ============================================================ */ const onScroll = () => { if (header) { window.scrollY > 40 ? header.classList.add('scrolled') : header.classList.remove('scrolled'); } highlightNav(); const btn = document.getElementById('back-to-top'); if (btn) { window.scrollY > 400 ? btn.classList.add('show') : btn.classList.remove('show'); } }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); /* ============================================================ 4. HAMBURGER MOBILE MENU ============================================================ */ const hamburger = document.getElementById('hamburger'); const mainNav = document.getElementById('main-nav'); function closeMobileNav() { hamburger && hamburger.classList.remove('open'); mainNav && mainNav.classList.remove('nav-open'); } if (hamburger && mainNav) { hamburger.addEventListener('click', () => { hamburger.classList.toggle('open'); mainNav.classList.toggle('nav-open'); }); // Close on outside tap document.addEventListener('click', e => { if (!mainNav.contains(e.target) && !hamburger.contains(e.target)) { closeMobileNav(); } }); } /* ============================================================ 5. RADIAL PROGRESS BARS — Animate on scroll into view ============================================================ */ const CIRCUMFERENCE = 2 * Math.PI * 42; // r=42 → ~264 function animateSkill(card) { const pct = parseInt(card.dataset.percent, 10) || 0; const circle = card.querySelector('.radial-progress'); const label = card.querySelector('.radial-pct'); if (!circle || !label) return; const targetOffset = CIRCUMFERENCE - (CIRCUMFERENCE * pct / 100); // Animate stroke circle.style.strokeDasharray = CIRCUMFERENCE; circle.style.strokeDashoffset = CIRCUMFERENCE; // Trigger reflow circle.getBoundingClientRect(); requestAnimationFrame(() => { circle.style.transition = 'stroke-dashoffset 1.6s cubic-bezier(0.4,0,0.2,1)'; circle.style.strokeDashoffset = targetOffset; }); // Animate number counter let start = 0; const duration = 1600; const step = (ts) => { if (!start) start = ts; const elapsed = ts - start; const progress = Math.min(elapsed / duration, 1); // ease-out const eased = 1 - Math.pow(1 - progress, 3); label.textContent = Math.round(eased * pct) + '%'; if (progress < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); } const skillObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { animateSkill(entry.target); skillObserver.unobserve(entry.target); } }); }, { threshold: 0.4 }); document.querySelectorAll('.skill-card').forEach(card => { const circle = card.querySelector('.radial-progress'); if (circle) { circle.style.strokeDasharray = CIRCUMFERENCE; circle.style.strokeDashoffset = CIRCUMFERENCE; } skillObserver.observe(card); }); /* ============================================================ 6. TIMELINE FADE-IN ON SCROLL ============================================================ */ const tlObserver = new IntersectionObserver(entries => { entries.forEach((entry, i) => { if (entry.isIntersecting) { entry.target.classList.add('visible'); tlObserver.unobserve(entry.target); } }); }, { threshold: 0.15 }); document.querySelectorAll('.tl-item').forEach((item, i) => { item.style.transitionDelay = (i * 0.12) + 's'; tlObserver.observe(item); }); /* ============================================================ 7. EXPERIENCE TABS (Desktop) ============================================================ */ const expTabs = document.querySelectorAll('.exp-tab'); const expPanels = document.querySelectorAll('.exp-panel'); expTabs.forEach(tab => { tab.addEventListener('click', () => { const target = tab.dataset.tab; expTabs.forEach(t => t.classList.remove('active')); expPanels.forEach(p => p.classList.remove('active')); tab.classList.add('active'); const panel = document.getElementById('panel-' + target); if (panel) panel.classList.add('active'); }); }); /* ============================================================ 8. LIGHTBOX ============================================================ */ const lightbox = document.getElementById('lightbox'); const lbImg = document.getElementById('lightbox-img'); const lbTitle = document.getElementById('lightbox-title'); const lbDesc = document.getElementById('lightbox-desc'); const lbClose = document.getElementById('lightbox-close'); function openLightbox(src, title, desc) { if (!lightbox || !lbImg) return; lbImg.src = src; lbImg.alt = title || ''; if (lbTitle) lbTitle.textContent = title || ''; if (lbDesc) lbDesc.textContent = desc || ''; lightbox.classList.add('open'); document.body.style.overflow = 'hidden'; } function closeLightbox() { if (!lightbox) return; lightbox.classList.remove('open'); document.body.style.overflow = ''; setTimeout(() => { if (lbImg) lbImg.src = ''; }, 300); } // Click on experience items document.querySelectorAll('.exp-item').forEach(item => { item.addEventListener('click', () => { openLightbox( item.dataset.lbSrc, item.dataset.lbTitle, item.dataset.lbDesc ); }); }); // Certificate zoom button const certZoomBtn = document.getElementById('cert-zoom-btn'); if (certZoomBtn) { certZoomBtn.addEventListener('click', () => { const certImg = document.querySelector('.cert-img-wrap img'); if (certImg) { openLightbox( certImg.src, 'รางวัลนักศึกษาดีเด่น ปีการศึกษา 2568', 'รางวัลแห่งความภาคภูมิใจที่มอบให้แก่นักศึกษาผู้อุทิศตนเพื่อส่วนรวม ทั้งด้านวิชาการและกิจกรรม' ); } }); } if (lbClose) lbClose.addEventListener('click', closeLightbox); lightbox && lightbox.addEventListener('click', e => { if (e.target === lightbox) closeLightbox(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox(); }); /* ============================================================ 9. MOBILE CAROUSEL — Swipe + Dots + Buttons ============================================================ */ const track = document.getElementById('carousel-track'); const dotsWrap = document.getElementById('carousel-dots'); const prevBtn = document.getElementById('carousel-prev'); const nextBtn = document.getElementById('carousel-next'); if (track) { const slides = track.querySelectorAll('.carousel-slide'); const total = slides.length; let current = 0; let startX = 0; let isDragging = false; // Build dots if (dotsWrap) { slides.forEach((_, i) => { const dot = document.createElement('button'); dot.className = 'dot' + (i === 0 ? ' active' : ''); dot.setAttribute('aria-label', 'ไปที่สไลด์ ' + (i + 1)); dot.addEventListener('click', () => goTo(i)); dotsWrap.appendChild(dot); }); } function updateDots() { if (!dotsWrap) return; dotsWrap.querySelectorAll('.dot').forEach((d, i) => { d.classList.toggle('active', i === current); }); } function goTo(index) { current = (index + total) % total; track.style.transform = `translateX(-${current * 100}%)`; track.style.transition = 'transform 0.4s ease'; updateDots(); } prevBtn && prevBtn.addEventListener('click', () => goTo(current - 1)); nextBtn && nextBtn.addEventListener('click', () => goTo(current + 1)); // Touch swipe track.addEventListener('touchstart', e => { startX = e.touches[0].clientX; isDragging = true; }, { passive: true }); track.addEventListener('touchend', e => { if (!isDragging) return; const diff = startX - e.changedTouches[0].clientX; if (Math.abs(diff) > 50) { goTo(diff > 0 ? current + 1 : current - 1); } isDragging = false; }, { passive: true }); // Auto-advance every 4s let autoInterval = setInterval(() => goTo(current + 1), 4000); const resetAuto = () => { clearInterval(autoInterval); autoInterval = setInterval(() => goTo(current + 1), 4000); }; prevBtn && prevBtn.addEventListener('click', resetAuto); nextBtn && nextBtn.addEventListener('click', resetAuto); } /* ============================================================ 10. BACK TO TOP ============================================================ */ const backToTop = document.getElementById('back-to-top'); if (backToTop) { backToTop.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); } /* ============================================================ 11. HERO PARTICLES — Lightweight floating dots ============================================================ */ const particlesContainer = document.getElementById('hero-particles'); if (particlesContainer) { const PARTICLE_COUNT = 35; for (let i = 0; i < PARTICLE_COUNT; i++) { const p = document.createElement('div'); const size = Math.random() * 3 + 1; const x = Math.random() * 100; const y = Math.random() * 100; const dur = Math.random() * 15 + 10; const delay = Math.random() * 10; const opacity = Math.random() * 0.4 + 0.1; p.style.cssText = ` position:absolute; width:${size}px; height:${size}px; left:${x}%; top:${y}%; background:rgba(0,200,255,${opacity}); border-radius:50%; animation: particle-float ${dur}s ${delay}s ease-in-out infinite alternate; pointer-events:none; `; particlesContainer.appendChild(p); } // Inject keyframe once if (!document.getElementById('particle-kf')) { const style = document.createElement('style'); style.id = 'particle-kf'; style.textContent = ` @keyframes particle-float { from { transform: translateY(0) translateX(0); opacity: 0.1; } to { transform: translateY(-40px) translateX(20px); opacity: 0.5; } } `; document.head.appendChild(style); } } /* ============================================================ 12. GENERAL SCROLL REVEAL — Section headers & cards (skill-card uses its own observer above; excluded here) ============================================================ */ const revealEls = document.querySelectorAll('.section-header, .cert-card, .contact-card'); const revealObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('revealed'); revealObserver.unobserve(entry.target); } }); }, { threshold: 0.08 }); revealEls.forEach(el => { el.classList.add('reveal-init'); revealObserver.observe(el); }); /* Inject reveal CSS once */ if (!document.getElementById('reveal-css')) { const s = document.createElement('style'); s.id = 'reveal-css'; s.textContent = ` .reveal-init { opacity: 0; transform: translateY(28px); transition: opacity 0.65s ease, transform 0.65s ease; } .reveal-init.revealed { opacity: 1 !important; transform: translateY(0) !important; } `; document.head.appendChild(s); } }); // END DOMContentLoaded
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.33 |
proxy
|
phpinfo
|
Settings