Spaces:
Running
Running
| // Shockwave animation | |
| const canvas = document.getElementById('shockwaveCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const shockwaves = []; | |
| class Shockwave { | |
| constructor(x, y) { | |
| this.x = x; | |
| this.y = y; | |
| this.radius = 0; | |
| this.maxRadius = 300; | |
| this.speed = 2; | |
| this.opacity = 0.5; | |
| this.color = `rgba(16, 185, 129, ${this.opacity})`; | |
| } | |
| update() { | |
| this.radius += this.speed; | |
| this.opacity = 0.5 * (1 - this.radius / this.maxRadius); | |
| this.color = `rgba(16, 185, 129, ${this.opacity})`; | |
| } | |
| draw() { | |
| ctx.strokeStyle = this.color; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| // Second ring | |
| ctx.strokeStyle = `rgba(59, 130, 246, ${this.opacity * 0.5})`; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius * 0.8, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| } | |
| isDead() { | |
| return this.radius > this.maxRadius; | |
| } | |
| } | |
| function animateShockwaves() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Create new shockwave randomly | |
| if (Math.random() < 0.02) { | |
| shockwaves.push(new Shockwave( | |
| Math.random() * canvas.width, | |
| Math.random() * canvas.height | |
| )); | |
| } | |
| // Update and draw shockwaves | |
| for (let i = shockwaves.length - 1; i >= 0; i--) { | |
| const wave = shockwaves[i]; | |
| wave.update(); | |
| wave.draw(); | |
| if (wave.isDead()) { | |
| shockwaves.splice(i, 1); | |
| } | |
| } | |
| requestAnimationFrame(animateShockwaves); | |
| } | |
| animateShockwaves(); | |
| // Resize canvas on window resize | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }); | |
| // Add interactive shockwave on click | |
| document.addEventListener('click', (e) => { | |
| shockwaves.push(new Shockwave(e.clientX, e.clientY)); | |
| }); | |
| // Service filtering (only if on services page) | |
| if (document.querySelector('.filter-tag')) { | |
| const filterTags = document.querySelectorAll('.filter-tag'); | |
| const serviceCards = document.querySelectorAll('.service-card'); | |
| filterTags.forEach(tag => { | |
| tag.addEventListener('click', () => { | |
| const filter = tag.dataset.filter; | |
| // Update active state | |
| filterTags.forEach(t => t.classList.remove('bg-emerald-600', 'text-white')); | |
| tag.classList.add('bg-emerald-600', 'text-white'); | |
| // Filter cards | |
| serviceCards.forEach(card => { | |
| if (filter === 'all' || card.dataset.category === filter) { | |
| card.style.display = 'block'; | |
| setTimeout(() => card.classList.add('visible'), 100); | |
| } else { | |
| card.style.display = 'none'; | |
| card.classList.remove('visible'); | |
| } | |
| }); | |
| }); | |
| }); | |
| } | |
| // Timeline animation on scroll | |
| const observerOptions = { | |
| threshold: 0.1, | |
| rootMargin: '0px 0px -100px 0px' | |
| }; | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| entry.target.classList.add('visible'); | |
| } | |
| }); | |
| }, observerOptions); | |
| // Observe elements for animation | |
| document.querySelectorAll('.timeline-item, .service-card, .skill-bar').forEach(el => { | |
| observer.observe(el); | |
| }); | |
| // Testimonial slider | |
| let currentSlide = 0; | |
| const testimonialsContainer = document.getElementById('testimonialsContainer'); | |
| const totalSlides = 3; | |
| function goToSlide(slideIndex) { | |
| currentSlide = slideIndex; | |
| testimonialsContainer.style.transform = `translateX(-${slideIndex * 100}%)`; | |
| // Update dots | |
| document.querySelectorAll('.testimonials-slider button').forEach((dot, index) => { | |
| if (index === slideIndex) { | |
| dot.classList.remove('bg-gray-600'); | |
| dot.classList.add('bg-emerald-500'); | |
| } else { | |
| dot.classList.remove('bg-emerald-500'); | |
| dot.classList.add('bg-gray-600'); | |
| } | |
| }); | |
| } | |
| // Auto-advance testimonials | |
| setInterval(() => { | |
| currentSlide = (currentSlide + 1) % totalSlides; | |
| goToSlide(currentSlide); | |
| }, 5000); | |
| // Contact form submission | |
| document.getElementById('contactForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const formData = new FormData(e.target); | |
| const data = Object.fromEntries(formData); | |
| // Show loading state | |
| const submitBtn = e.target.querySelector('button[type="submit"]'); | |
| const originalText = submitBtn.textContent; | |
| submitBtn.innerHTML = '<span class="loading"></span> Sending...'; | |
| submitBtn.disabled = true; | |
| try { | |
| // Simulate API call | |
| await new Promise(resolve => setTimeout(resolve, 2000)); | |
| // Show success message | |
| const successMsg = document.getElementById('successMessage'); | |
| successMsg.style.transform = 'translateX(0)'; | |
| // Reset form | |
| e.target.reset(); | |
| // Hide success message after 3 seconds | |
| setTimeout(() => { | |
| successMsg.style.transform = 'translateX(100%)'; | |
| }, 3000); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| alert('Error sending message. Please try again.'); | |
| } finally { | |
| // Reset button | |
| submitBtn.textContent = originalText; | |
| submitBtn.disabled = false; | |
| } | |
| }); | |
| // FAQ toggle function | |
| window.toggleFAQ = function(button) { | |
| const content = button.nextElementSibling; | |
| const icon = button.querySelector('i'); | |
| content.classList.toggle('hidden'); | |
| icon.style.transform = content.classList.contains('hidden') ? 'rotate(0deg)' : 'rotate(180deg)'; | |
| }; | |
| // Smooth scroll 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' | |
| }); | |
| } | |
| }); | |
| }); | |
| // Parallax scrolling effect | |
| window.addEventListener('scroll', () => { | |
| const scrolled = window.pageYOffset; | |
| const parallaxElements = document.querySelectorAll('.parallax'); | |
| parallaxElements.forEach(el => { | |
| const speed = el.dataset.speed || 0.5; | |
| el.style.transform = `translateY(${scrolled * speed}px)`; | |
| }); | |
| }); | |
| // Add reveal animation to elements on scroll | |
| const revealElements = document.querySelectorAll('.service-card, .timeline-item'); | |
| const revealOnScroll = () => { | |
| revealElements.forEach(element => { | |
| const elementTop = element.getBoundingClientRect().top; | |
| const elementBottom = element.getBoundingClientRect().bottom; | |
| if (elementTop < window.innerHeight && elementBottom > 0) { | |
| element.classList.add('visible'); | |
| } | |
| }); | |
| }; | |
| window.addEventListener('scroll', revealOnScroll); | |
| revealOnScroll(); // Initial check | |
| // Dynamic skill bar animation | |
| const skillBars = document.querySelectorAll('.skill-bar'); | |
| const animateSkillBars = () => { | |
| skillBars.forEach(bar => { | |
| const rect = bar.getBoundingClientRect(); | |
| if (rect.top < window.innerHeight && rect.bottom > 0) { | |
| const progressBar = bar.querySelector('.bg-gradient-to-r'); | |
| const width = progressBar.style.width || '0%'; | |
| if (width === '0%') { | |
| progressBar.style.width = progressBar.parentElement.previousElementSibling.querySelector('span:last-child').textContent; | |
| } | |
| } | |
| }); | |
| }; | |
| window.addEventListener('scroll', animateSkillBars); | |
| animateSkillBars(); | |
| // Keyboard navigation support | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| closeCaseStudy(); | |
| } | |
| }); | |
| // Performance optimization - Debounce scroll events | |
| let scrollTimeout; | |
| window.addEventListener('scroll', () => { | |
| if (scrollTimeout) { | |
| window.cancelAnimationFrame(scrollTimeout); | |
| } | |
| scrollTimeout = window.requestAnimationFrame(() => { | |
| // Scroll-based animations here | |
| revealOnScroll(); | |
| animateSkillBars(); | |
| }); | |
| }); |