/** * Privacy First - Main JavaScript * Handles animations, interactions, and visual effects */ (function() { 'use strict'; // ========================================================================== // Configuration // ========================================================================== const CONFIG = { particles: { count: 80, color: '#00d4aa', colorSecondary: '#7c5cff', maxDistance: 120, speed: 0.3, mouseRadius: 150 }, scroll: { revealOffset: 100, headerOffset: 50 } }; // ========================================================================== // Particle System // ========================================================================== class ParticleSystem { constructor(canvasId) { this.canvas = document.getElementById(canvasId); if (!this.canvas) return; this.ctx = this.canvas.getContext('2d'); this.particles = []; this.mouse = { x: null, y: null }; this.animationId = null; this.init(); this.addEventListeners(); this.animate(); } init() { this.resize(); this.createParticles(); } resize() { this.width = this.canvas.width = this.canvas.offsetWidth; this.height = this.canvas.height = this.canvas.offsetHeight; } createParticles() { this.particles = []; const count = Math.min(CONFIG.particles.count, Math.floor((this.width * this.height) / 15000)); for (let i = 0; i < count; i++) { this.particles.push({ x: Math.random() * this.width, y: Math.random() * this.height, vx: (Math.random() - 0.5) * CONFIG.particles.speed, vy: (Math.random() - 0.5) * CONFIG.particles.speed, radius: Math.random() * 2 + 1, color: Math.random() > 0.5 ? CONFIG.particles.color : CONFIG.particles.colorSecondary, alpha: Math.random() * 0.5 + 0.3 }); } } addEventListeners() { window.addEventListener('resize', () => { this.resize(); this.createParticles(); }); this.canvas.addEventListener('mousemove', (e) => { const rect = this.canvas.getBoundingClientRect(); this.mouse.x = e.clientX - rect.left; this.mouse.y = e.clientY - rect.top; }); this.canvas.addEventListener('mouseleave', () => { this.mouse.x = null; this.mouse.y = null; }); } drawParticle(particle) { this.ctx.beginPath(); this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); this.ctx.fillStyle = particle.color; this.ctx.globalAlpha = particle.alpha; this.ctx.fill(); this.ctx.globalAlpha = 1; } drawConnections() { for (let i = 0; i < this.particles.length; i++) { for (let j = i + 1; j < this.particles.length; j++) { const dx = this.particles[i].x - this.particles[j].x; const dy = this.particles[i].y - this.particles[j].y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < CONFIG.particles.maxDistance) { const alpha = (1 - distance / CONFIG.particles.maxDistance) * 0.3; this.ctx.beginPath(); this.ctx.moveTo(this.particles[i].x, this.particles[i].y); this.ctx.lineTo(this.particles[j].x, this.particles[j].y); this.ctx.strokeStyle = CONFIG.particles.color; this.ctx.globalAlpha = alpha; this.ctx.lineWidth = 0.5; this.ctx.stroke(); this.ctx.globalAlpha = 1; } } } } updateParticle(particle) { // Mouse interaction if (this.mouse.x !== null && this.mouse.y !== null) { const dx = particle.x - this.mouse.x; const dy = particle.y - this.mouse.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < CONFIG.particles.mouseRadius) { const force = (CONFIG.particles.mouseRadius - distance) / CONFIG.particles.mouseRadius; particle.vx += (dx / distance) * force * 0.02; particle.vy += (dy / distance) * force * 0.02; } } // Apply velocity particle.x += particle.vx; particle.y += particle.vy; // Apply friction particle.vx *= 0.99; particle.vy *= 0.99; // Boundary check if (particle.x < 0 || particle.x > this.width) { particle.vx *= -1; particle.x = Math.max(0, Math.min(this.width, particle.x)); } if (particle.y < 0 || particle.y > this.height) { particle.vy *= -1; particle.y = Math.max(0, Math.min(this.height, particle.y)); } } animate() { this.ctx.clearRect(0, 0, this.width, this.height); // Update and draw particles this.particles.forEach(particle => { this.updateParticle(particle); this.drawParticle(particle); }); // Draw connections this.drawConnections(); this.animationId = requestAnimationFrame(() => this.animate()); } destroy() { if (this.animationId) { cancelAnimationFrame(this.animationId); } } } // ========================================================================== // Scroll Reveal // ========================================================================== class ScrollReveal { constructor() { this.elements = document.querySelectorAll('.reveal'); this.init(); } init() { // Initial check this.checkElements(); // Scroll listener with throttle let ticking = false; window.addEventListener('scroll', () => { if (!ticking) { window.requestAnimationFrame(() => { this.checkElements(); ticking = false; }); ticking = true; } }); } checkElements() { this.elements.forEach(element => { if (this.isInViewport(element)) { element.classList.add('active'); } }); } isInViewport(element) { const rect = element.getBoundingClientRect(); return ( rect.top <= (window.innerHeight || document.documentElement.clientHeight) - CONFIG.scroll.revealOffset ); } } // ========================================================================== // Header Controller // ========================================================================== class HeaderController { constructor() { this.header = document.getElementById('header'); this.lastScroll = 0; this.init(); } init() { window.addEventListener('scroll', () => this.handleScroll()); this.handleScroll(); // Initial check } handleScroll() { const currentScroll = window.pageYOffset; if (currentScroll > CONFIG.scroll.headerOffset) { this.header.classList.add('scrolled'); } else { this.header.classList.remove('scrolled'); } this.lastScroll = currentScroll; } } // ========================================================================== // Mobile Navigation // ========================================================================== class MobileNav { constructor() { this.toggle = document.getElementById('nav-toggle'); this.close = document.getElementById('nav-close'); this.menu = document.getElementById('nav-menu'); this.links = document.querySelectorAll('.nav__link'); if (this.toggle && this.menu) { this.init(); } } init() { this.toggle.addEventListener('click', () => this.openMenu()); this.close.addEventListener('click', () => this.closeMenu()); // Close on link click this.links.forEach(link => { link.addEventListener('click', () => this.closeMenu()); }); // Close on outside click document.addEventListener('click', (e) => { if (!this.menu.contains(e.target) && !this.toggle.contains(e.target)) { this.closeMenu(); } }); // Close on escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.closeMenu(); } }); } openMenu() { this.menu.classList.add('active'); document.body.style.overflow = 'hidden'; } closeMenu() { this.menu.classList.remove('active'); document.body.style.overflow = ''; } } // ========================================================================== // Pricing Toggle // ========================================================================== class PricingToggle { constructor() { this.toggle = document.getElementById('pricing-toggle'); this.labels = document.querySelectorAll('.pricing__toggle-label'); this.amounts = document.querySelectorAll('.pricing-card__amount[data-monthly]'); this.isAnnual = false; if (this.toggle) { this.init(); } } init() { this.toggle.addEventListener('click', () => this.handleToggle()); this.updateLabels(); } handleToggle() { this.isAnnual = !this.isAnnual; this.toggle.classList.toggle('annual', this.isAnnual); this.updatePrices(); this.updateLabels(); } updatePrices() { this.amounts.forEach(amount => { const monthly = amount.dataset.monthly; const annual = amount.dataset.annual; amount.textContent = this.isAnnual ? annual : monthly; }); } updateLabels() { this.labels.forEach(label => { const period = label.dataset.period; if (period === 'annual') { label.classList.toggle('active', this.isAnnual); } else { label.classList.toggle('active', !this.isAnnual); } }); } } // ========================================================================== // Smooth Scroll // ========================================================================== class SmoothScroll { constructor() { this.links = document.querySelectorAll('a[href^="#"]'); this.init(); } init() { this.links.forEach(link => { link.addEventListener('click', (e) => this.handleClick(e, link)); }); } handleClick(e, link) { const href = link.getAttribute('href'); if (href === '#') return; const target = document.querySelector(href); if (target) { e.preventDefault(); const headerOffset = 80; const elementPosition = target.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - headerOffset; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); } } } // ========================================================================== // Typing Effect for Hero // ========================================================================== class TypeWriter { constructor(element, words, wait = 2000) { this.element = element; this.words = words; this.txt = ''; this.wordIndex = 0; this.wait = parseInt(wait, 10); this.isDeleting = false; this.type(); } type() { const current = this.wordIndex % this.words.length; const fullTxt = this.words[current]; if (this.isDeleting) { this.txt = fullTxt.substring(0, this.txt.length - 1); } else { this.txt = fullTxt.substring(0, this.txt.length + 1); } this.element.innerHTML = `${this.txt}`; let typeSpeed = 100; if (this.isDeleting) { typeSpeed /= 2; } if (!this.isDeleting && this.txt === fullTxt) { typeSpeed = this.wait; this.isDeleting = true; } else if (this.isDeleting && this.txt === '') { this.isDeleting = false; this.wordIndex++; typeSpeed = 500; } setTimeout(() => this.type(), typeSpeed); } } // ========================================================================== // Form Handlers // ========================================================================== class FormHandler { constructor() { this.notifyForm = document.querySelector('.notify-form'); if (this.notifyForm) { this.init(); } } init() { this.notifyForm.addEventListener('submit', (e) => this.handleSubmit(e)); } handleSubmit(e) { e.preventDefault(); const input = this.notifyForm.querySelector('input[type="email"]'); const button = this.notifyForm.querySelector('button'); if (input.value && this.validateEmail(input.value)) { button.textContent = 'Subscribed!'; button.disabled = true; input.disabled = true; setTimeout(() => { button.textContent = 'Notify Me'; button.disabled = false; input.disabled = false; input.value = ''; }, 3000); } } validateEmail(email) { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(email); } } // ========================================================================== // Mockup Animations // ========================================================================== class MockupAnimations { constructor() { this.mockupServices = document.querySelectorAll('.mockup__service'); this.passportServices = document.querySelectorAll('.passport-card__service'); this.init(); } init() { // Cycle through mockup services if (this.mockupServices.length > 0) { this.cycleServices(this.mockupServices, 2500); } // Cycle through passport services if (this.passportServices.length > 0) { this.cycleServices(this.passportServices, 3000); } } cycleServices(services, interval) { let currentIndex = 0; setInterval(() => { services.forEach(s => s.classList.remove('active')); currentIndex = (currentIndex + 1) % services.length; services[currentIndex].classList.add('active'); }, interval); } } // ========================================================================== // Intersection Observer for Stats Animation // ========================================================================== class StatsAnimation { constructor() { this.stats = document.querySelectorAll('.stat-card__value, .cp-stat__value'); this.animated = new Set(); this.init(); } init() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && !this.animated.has(entry.target)) { this.animateValue(entry.target); this.animated.add(entry.target); } }); }, { threshold: 0.5 }); this.stats.forEach(stat => observer.observe(stat)); } animateValue(element) { const text = element.textContent; const match = text.match(/^([\d,.]+)/); if (!match) return; const finalValue = parseFloat(match[1].replace(/,/g, '')); const suffix = text.replace(match[1], ''); const duration = 1500; const startTime = performance.now(); const animate = (currentTime) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); // Easing function const easeOutQuart = 1 - Math.pow(1 - progress, 4); const currentValue = finalValue * easeOutQuart; // Format number let displayValue; if (finalValue >= 1000) { displayValue = Math.floor(currentValue).toLocaleString(); } else if (finalValue >= 100) { displayValue = Math.floor(currentValue); } else if (finalValue % 1 !== 0) { displayValue = currentValue.toFixed(1); } else { displayValue = Math.floor(currentValue); } element.textContent = displayValue + suffix; if (progress < 1) { requestAnimationFrame(animate); } else { element.textContent = text; // Restore original } }; requestAnimationFrame(animate); } } // ========================================================================== // Initialize Everything // ========================================================================== function init() { // Initialize particle system new ParticleSystem('particle-canvas'); // Initialize scroll reveal new ScrollReveal(); // Initialize header controller new HeaderController(); // Initialize mobile navigation new MobileNav(); // Initialize pricing toggle new PricingToggle(); // Initialize smooth scroll new SmoothScroll(); // Initialize form handlers new FormHandler(); // Initialize mockup animations new MockupAnimations(); // Initialize stats animation new StatsAnimation(); // Log init complete console.log('Privacy First website initialized'); } // Run on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();