Spaces:
Sleeping
Sleeping
| // Smooth scrolling for navigation links | |
| document.querySelectorAll('a[href^="#"]').forEach(anchor => { | |
| anchor.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| const target = document.querySelector(this.getAttribute('href')); | |
| if (target) { | |
| target.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| } | |
| }); | |
| }); | |
| // Navbar scroll effect | |
| window.addEventListener('scroll', function() { | |
| const navbar = document.querySelector('.navbar'); | |
| if (window.scrollY > 50) { | |
| navbar.style.background = 'rgba(255, 255, 255, 0.98)'; | |
| navbar.style.boxShadow = '0 5px 20px rgba(15, 23, 42, 0.1)'; | |
| } else { | |
| navbar.style.background = 'rgba(255, 255, 255, 0.95)'; | |
| navbar.style.boxShadow = 'none'; | |
| } | |
| }); | |
| // Mobile menu toggle | |
| const hamburger = document.querySelector('.hamburger'); | |
| const navMenu = document.querySelector('.nav-menu'); | |
| hamburger?.addEventListener('click', function() { | |
| navMenu.classList.toggle('active'); | |
| this.classList.toggle('active'); | |
| }); | |
| // Animate circular progress rings on scroll | |
| const observerOptions = { | |
| threshold: 0.3, | |
| rootMargin: '0px' | |
| }; | |
| const observer = new IntersectionObserver(function(entries) { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| const techIcons = entry.target.querySelectorAll('.tech-icon'); | |
| techIcons.forEach((icon, index) => { | |
| const skill = icon.getAttribute('data-skill'); | |
| const circle = icon.querySelector('.progress-ring-circle'); | |
| const circumference = 2 * Math.PI * 40; // radius = 40 | |
| const offset = circumference - (skill / 100) * circumference; | |
| setTimeout(() => { | |
| circle.style.strokeDashoffset = offset; | |
| }, index * 100); | |
| }); | |
| } | |
| }); | |
| }, observerOptions); | |
| const skillsSection = document.querySelector('.skills'); | |
| if (skillsSection) { | |
| observer.observe(skillsSection); | |
| } | |
| // Interactive hover effects for tech icons | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const techIcons = document.querySelectorAll('.tech-icon'); | |
| techIcons.forEach(icon => { | |
| const skill = icon.getAttribute('data-skill'); | |
| const circle = icon.querySelector('.progress-ring-circle'); | |
| const percentage = icon.querySelector('.percentage'); | |
| const circumference = 2 * Math.PI * 40; // radius = 40 | |
| // Set initial state | |
| circle.style.strokeDasharray = circumference; | |
| circle.style.strokeDashoffset = circumference; | |
| icon.addEventListener('mouseenter', function() { | |
| const offset = circumference - (skill / 100) * circumference; | |
| circle.style.strokeDashoffset = offset; | |
| percentage.textContent = skill + '%'; | |
| }); | |
| icon.addEventListener('mouseleave', function() { | |
| circle.style.strokeDashoffset = circumference; | |
| }); | |
| }); | |
| }); | |
| // Animate elements on scroll | |
| const animateOnScroll = () => { | |
| const elements = document.querySelectorAll('.timeline-item, .project-card, .stat-card'); | |
| elements.forEach(element => { | |
| const elementTop = element.getBoundingClientRect().top; | |
| const elementBottom = element.getBoundingClientRect().bottom; | |
| if (elementTop < window.innerHeight && elementBottom > 0) { | |
| element.style.opacity = '1'; | |
| element.style.transform = 'translateY(0)'; | |
| } | |
| }); | |
| }; | |
| window.addEventListener('scroll', animateOnScroll); | |
| window.addEventListener('load', animateOnScroll); | |
| // Character-by-character streaming effect for specific lines | |
| const streamingElement = document.getElementById('streaming-text'); | |
| if (streamingElement) { | |
| const textLines = [ | |
| 'Hi, nice to meet you!', | |
| "I'm Tran Bao Ngoc", | |
| 'A passionate AI developer', | |
| 'I love Python ๐', | |
| 'I love researching AI ๐ค', | |
| 'I love playing games ๐ฎ' | |
| ]; | |
| let currentLineIndex = 0; | |
| let currentCharIndex = 0; | |
| let isDeleting = false; | |
| let typingSpeed = 45; | |
| let deleteSpeed = 25; | |
| function typewriterEffect() { | |
| const currentLine = textLines[currentLineIndex]; | |
| if (!isDeleting) { | |
| // Typing characters | |
| if (currentCharIndex <= currentLine.length) { | |
| streamingElement.innerHTML = currentLine.substring(0, currentCharIndex); | |
| currentCharIndex++; | |
| if (currentCharIndex > currentLine.length) { | |
| // Finished typing line, pause then start deleting | |
| setTimeout(() => { | |
| isDeleting = true; | |
| typewriterEffect(); | |
| }, 1800); // Pause for 1.8 seconds | |
| } else { | |
| setTimeout(typewriterEffect, typingSpeed + Math.random() * 20); | |
| } | |
| } | |
| } else { | |
| // Deleting characters | |
| if (currentCharIndex > 0) { | |
| currentCharIndex--; | |
| streamingElement.innerHTML = currentLine.substring(0, currentCharIndex); | |
| setTimeout(typewriterEffect, deleteSpeed); | |
| } else { | |
| // Finished deleting, move to next line | |
| isDeleting = false; | |
| currentLineIndex = (currentLineIndex + 1) % textLines.length; | |
| // Pause before starting next line | |
| setTimeout(typewriterEffect, 500); | |
| } | |
| } | |
| } | |
| // Start the typewriter effect after a delay | |
| setTimeout(typewriterEffect, 2000); | |
| } | |
| // Parallax effect for shapes | |
| window.addEventListener('scroll', () => { | |
| const shapes = document.querySelectorAll('.shape'); | |
| const scrollY = window.pageYOffset; | |
| shapes.forEach((shape, index) => { | |
| const speed = 0.5 + (index * 0.1); | |
| shape.style.transform = `translateY(${scrollY * speed}px)`; | |
| }); | |
| }); | |
| // Add active class to current navigation item | |
| window.addEventListener('scroll', () => { | |
| const sections = document.querySelectorAll('section'); | |
| const navLinks = document.querySelectorAll('.nav-link'); | |
| let current = ''; | |
| sections.forEach(section => { | |
| const sectionTop = section.offsetTop - 100; | |
| if (window.pageYOffset >= sectionTop) { | |
| current = section.getAttribute('id'); | |
| } | |
| }); | |
| navLinks.forEach(link => { | |
| link.classList.remove('active'); | |
| if (link.getAttribute('href').substring(1) === current) { | |
| link.classList.add('active'); | |
| } | |
| }); | |
| }); | |
| console.log('Portfolio loaded successfully! ๐'); |