Files
privacyfirstwebsite/wwwroot/js/main.js

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();
}
})();