617 lines
20 KiB
JavaScript
617 lines
20 KiB
JavaScript
/**
|
|
* 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 = `<span class="txt">${this.txt}</span>`;
|
|
|
|
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();
|
|
}
|
|
|
|
})();
|