473 lines
16 KiB
JavaScript
Executable File
473 lines
16 KiB
JavaScript
Executable File
/* ========================================
|
|
MSPE Ultimate UI Interactions v2.0
|
|
Advanced JavaScript for Premium UX
|
|
======================================== */
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize all modules
|
|
initCustomCursor();
|
|
initParticles();
|
|
initScrollProgress();
|
|
initMagneticButtons();
|
|
initSmoothScroll();
|
|
initParallax();
|
|
initRevealAnimations();
|
|
initTiltEffect();
|
|
initRippleEffect();
|
|
initTypewriter();
|
|
initLazyLoad();
|
|
});
|
|
|
|
/* ==================== Optimized Custom Cursor ==================== */
|
|
function initCustomCursor() {
|
|
// Custom cursor disabled
|
|
return;
|
|
// Only on desktop with no reduced motion preference
|
|
if (window.innerWidth < 768 || 'ontouchstart' in window || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
|
|
// Add class to body to hide default cursor
|
|
document.body.classList.add('custom-cursor-enabled');
|
|
|
|
const cursor = document.createElement('div');
|
|
cursor.classList.add('custom-cursor');
|
|
document.body.appendChild(cursor);
|
|
|
|
const trails = [];
|
|
const trailLength = 5; // Reduced from 12
|
|
|
|
for (let i = 0; i < trailLength; i++) {
|
|
const trail = document.createElement('div');
|
|
trail.classList.add('cursor-trail');
|
|
trail.style.opacity = (1 - i / trailLength) * 0.4;
|
|
const size = 6 - i;
|
|
trail.style.width = size + 'px';
|
|
trail.style.height = size + 'px';
|
|
document.body.appendChild(trail);
|
|
trails.push({ element: trail, x: 0, y: 0 });
|
|
}
|
|
|
|
let mouseX = 0, mouseY = 0;
|
|
let cursorX = 0, cursorY = 0;
|
|
let isMoving = false;
|
|
let rafId = null;
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
mouseX = e.clientX;
|
|
mouseY = e.clientY;
|
|
if (!isMoving) {
|
|
isMoving = true;
|
|
rafId = requestAnimationFrame(animateCursor);
|
|
}
|
|
}, { passive: true });
|
|
|
|
function animateCursor() {
|
|
cursorX += (mouseX - cursorX) * 0.15;
|
|
cursorY += (mouseY - cursorY) * 0.15;
|
|
|
|
cursor.style.transform = `translate3d(${cursorX}px, ${cursorY}px, 0) translate(-50%, -50%)`;
|
|
|
|
trails.forEach((trail, index) => {
|
|
const prev = trails[index - 1] || { x: cursorX, y: cursorY };
|
|
trail.x += (prev.x - trail.x) * 0.25;
|
|
trail.y += (prev.y - trail.y) * 0.25;
|
|
trail.element.style.transform = `translate3d(${trail.x}px, ${trail.y}px, 0)`;
|
|
});
|
|
|
|
// Stop loop when cursor catches up
|
|
if (Math.abs(mouseX - cursorX) > 0.5 || Math.abs(mouseY - cursorY) > 0.5) {
|
|
rafId = requestAnimationFrame(animateCursor);
|
|
} else {
|
|
isMoving = false;
|
|
}
|
|
}
|
|
|
|
// Delegate hover detection instead of per-element listeners
|
|
document.addEventListener('mouseover', (e) => {
|
|
if (e.target.closest('a, button, .btn, input, textarea, select, .service-card, .news-card, .portfolio-item, .nav-link')) {
|
|
cursor.classList.add('active');
|
|
}
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('mouseout', (e) => {
|
|
if (e.target.closest('a, button, .btn, input, textarea, select, .service-card, .news-card, .portfolio-item, .nav-link')) {
|
|
cursor.classList.remove('active');
|
|
}
|
|
}, { passive: true });
|
|
}
|
|
|
|
/* ==================== Optimized Particle Background ==================== */
|
|
function initParticles() {
|
|
// Particles disabled for performance — 20 animated DOM elements cause compositor overhead
|
|
return;
|
|
|
|
const particlesContainer = document.createElement('div');
|
|
particlesContainer.classList.add('particles-bg');
|
|
document.body.appendChild(particlesContainer);
|
|
|
|
// Drastically reduced: 20 on desktop, 10 on mobile (was 60/25)
|
|
const particleCount = window.innerWidth < 768 ? 10 : 20;
|
|
|
|
for (let i = 0; i < particleCount; i++) {
|
|
const particle = document.createElement('div');
|
|
particle.classList.add('particle');
|
|
particle.style.left = Math.random() * 100 + '%';
|
|
particle.style.animationDelay = Math.random() * 20 + 's';
|
|
particle.style.animationDuration = (25 + Math.random() * 15) + 's';
|
|
const size = 2 + Math.random() * 2;
|
|
particle.style.width = size + 'px';
|
|
particle.style.height = size + 'px';
|
|
particlesContainer.appendChild(particle);
|
|
}
|
|
// Geometric shapes removed for performance
|
|
}
|
|
|
|
/* ==================== Enhanced Scroll Progress Indicator ==================== */
|
|
function initScrollProgress() {
|
|
const progressBar = document.createElement('div');
|
|
progressBar.classList.add('scroll-progress');
|
|
document.body.appendChild(progressBar);
|
|
|
|
let ticking = false;
|
|
|
|
window.addEventListener('scroll', () => {
|
|
if (!ticking) {
|
|
requestAnimationFrame(() => {
|
|
const windowHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
|
const scrolled = (window.scrollY / windowHeight) * 100;
|
|
progressBar.style.width = scrolled + '%';
|
|
ticking = false;
|
|
});
|
|
ticking = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ==================== Optimized Magnetic Buttons (disabled for performance) ==================== */
|
|
function initMagneticButtons() {
|
|
// Magnetic effect disabled — per-element mousemove listeners are expensive
|
|
return;
|
|
|
|
// Only apply to .btn elements, not cards/nav-links (too many listeners)
|
|
const magneticElements = document.querySelectorAll('.btn');
|
|
|
|
magneticElements.forEach(el => {
|
|
el.addEventListener('mousemove', (e) => {
|
|
const rect = el.getBoundingClientRect();
|
|
const x = (e.clientX - rect.left - rect.width / 2) * 0.25;
|
|
const y = (e.clientY - rect.top - rect.height / 2) * 0.25;
|
|
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
|
|
}, { passive: true });
|
|
|
|
el.addEventListener('mouseleave', () => {
|
|
el.style.transform = '';
|
|
}, { passive: true });
|
|
});
|
|
}
|
|
|
|
/* ==================== Enhanced Smooth Scroll ==================== */
|
|
function initSmoothScroll() {
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function (e) {
|
|
const href = this.getAttribute('href');
|
|
if (href === '#') return;
|
|
|
|
e.preventDefault();
|
|
const target = document.querySelector(href);
|
|
|
|
if (target) {
|
|
const headerOffset = 100;
|
|
const elementPosition = target.getBoundingClientRect().top;
|
|
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
|
|
|
window.scrollTo({
|
|
top: offsetPosition,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ==================== Lightweight Parallax (hero only) ==================== */
|
|
function initParallax() {
|
|
const heroVisual = document.querySelector('.hero-visual');
|
|
if (!heroVisual || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
|
|
let ticking = false;
|
|
|
|
window.addEventListener('scroll', () => {
|
|
if (!ticking) {
|
|
requestAnimationFrame(() => {
|
|
const scrollY = window.scrollY;
|
|
if (scrollY < window.innerHeight) {
|
|
heroVisual.style.transform = `translate3d(0, ${scrollY * 0.15}px, 0)`;
|
|
}
|
|
ticking = false;
|
|
});
|
|
ticking = true;
|
|
}
|
|
}, { passive: true });
|
|
}
|
|
|
|
/* ==================== Optimized Reveal Animations ==================== */
|
|
function initRevealAnimations() {
|
|
// Only observe sections, not every individual card (let CSS handle card animations)
|
|
const revealElements = document.querySelectorAll('.section');
|
|
|
|
revealElements.forEach(el => el.classList.add('reveal'));
|
|
|
|
const revealObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('active');
|
|
revealObserver.unobserve(entry.target); // Stop observing once revealed
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -30px 0px'
|
|
});
|
|
|
|
revealElements.forEach(el => revealObserver.observe(el));
|
|
}
|
|
|
|
// Counter animation helper
|
|
function animateValue(element) {
|
|
const parent = element.closest('[data-count]');
|
|
if (!parent) return;
|
|
|
|
const target = parseInt(parent.dataset.count) || 0;
|
|
const duration = 2000;
|
|
const start = performance.now();
|
|
|
|
function update(currentTime) {
|
|
const elapsed = currentTime - start;
|
|
const progress = Math.min(elapsed / duration, 1);
|
|
|
|
// Easing function
|
|
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
|
const current = Math.floor(easeOutQuart * target);
|
|
|
|
element.textContent = current;
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(update);
|
|
} else {
|
|
element.textContent = target;
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(update);
|
|
}
|
|
|
|
/* ==================== Lightweight 3D Tilt (disabled — duplicate of initCardPointerLift in main.js) ==================== */
|
|
function initTiltEffect() {
|
|
// Disabled — main.js initCardPointerLift already handles card hover effects
|
|
return;
|
|
|
|
// Only service cards - removed from news, portfolio, feature, philosophy cards
|
|
const cards = document.querySelectorAll('.service-card');
|
|
|
|
cards.forEach(card => {
|
|
card.addEventListener('mousemove', (e) => {
|
|
const rect = card.getBoundingClientRect();
|
|
const rotateX = ((e.clientY - rect.top) - rect.height / 2) / 20;
|
|
const rotateY = (rect.width / 2 - (e.clientX - rect.left)) / 20;
|
|
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateZ(5px)`;
|
|
}, { passive: true });
|
|
|
|
card.addEventListener('mouseleave', () => {
|
|
card.style.transform = '';
|
|
}, { passive: true });
|
|
});
|
|
}
|
|
|
|
/* ==================== Ripple Effect (buttons only, no global click) ==================== */
|
|
function initRippleEffect() {
|
|
// Use event delegation for button ripples
|
|
document.addEventListener('click', function(e) {
|
|
const btn = e.target.closest('.btn');
|
|
if (!btn) return;
|
|
|
|
const rect = btn.getBoundingClientRect();
|
|
const ripple = document.createElement('span');
|
|
ripple.classList.add('btn-ripple');
|
|
ripple.style.left = (e.clientX - rect.left) + 'px';
|
|
ripple.style.top = (e.clientY - rect.top) + 'px';
|
|
btn.appendChild(ripple);
|
|
setTimeout(() => ripple.remove(), 600);
|
|
});
|
|
// Global click ripple removed for performance
|
|
}
|
|
|
|
/* ==================== Typewriter Effect ==================== */
|
|
function initTypewriter() {
|
|
const typingElements = document.querySelectorAll('[data-typing]');
|
|
|
|
typingElements.forEach(el => {
|
|
const text = el.dataset.typing || el.textContent;
|
|
const speed = parseInt(el.dataset.typingSpeed) || 50;
|
|
|
|
el.textContent = '';
|
|
el.style.borderRight = '2px solid var(--neon-cyan)';
|
|
|
|
let charIndex = 0;
|
|
|
|
const typeInterval = setInterval(() => {
|
|
if (charIndex < text.length) {
|
|
el.textContent += text.charAt(charIndex);
|
|
charIndex++;
|
|
} else {
|
|
clearInterval(typeInterval);
|
|
// Blinking cursor effect
|
|
el.style.animation = 'blinkCursor 0.8s infinite';
|
|
}
|
|
}, speed);
|
|
});
|
|
|
|
// Add blink cursor keyframes
|
|
if (!document.querySelector('#blinkCursorKeyframes')) {
|
|
const style = document.createElement('style');
|
|
style.id = 'blinkCursorKeyframes';
|
|
style.textContent = `
|
|
@keyframes blinkCursor {
|
|
0%, 50% { border-color: var(--neon-cyan); }
|
|
51%, 100% { border-color: transparent; }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
}
|
|
|
|
/* Dynamic Background Gradient - REMOVED for performance */
|
|
// The continuous rAF loop updating body background-position was a major perf drain
|
|
|
|
/* ==================== Text Scramble Effect ==================== */
|
|
class TextScramble {
|
|
constructor(el) {
|
|
this.el = el;
|
|
this.chars = '!<>-_\\/[]{}—=+*^?#________';
|
|
this.update = this.update.bind(this);
|
|
}
|
|
|
|
setText(newText) {
|
|
const oldText = this.el.textContent;
|
|
const length = Math.max(oldText.length, newText.length);
|
|
const promise = new Promise(resolve => this.resolve = resolve);
|
|
this.queue = [];
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
const from = oldText[i] || '';
|
|
const to = newText[i] || '';
|
|
const start = Math.floor(Math.random() * 40);
|
|
const end = start + Math.floor(Math.random() * 40);
|
|
this.queue.push({ from, to, start, end });
|
|
}
|
|
|
|
cancelAnimationFrame(this.frameRequest);
|
|
this.frame = 0;
|
|
this.update();
|
|
return promise;
|
|
}
|
|
|
|
update() {
|
|
let output = '';
|
|
let complete = 0;
|
|
|
|
for (let i = 0, n = this.queue.length; i < n; i++) {
|
|
let { from, to, start, end, char } = this.queue[i];
|
|
|
|
if (this.frame >= end) {
|
|
complete++;
|
|
output += to;
|
|
} else if (this.frame >= start) {
|
|
if (!char || Math.random() < 0.28) {
|
|
char = this.randomChar();
|
|
this.queue[i].char = char;
|
|
}
|
|
output += `<span style="color: var(--neon-cyan)">${char}</span>`;
|
|
} else {
|
|
output += from;
|
|
}
|
|
}
|
|
|
|
this.el.innerHTML = output;
|
|
|
|
if (complete === this.queue.length) {
|
|
this.resolve();
|
|
} else {
|
|
this.frameRequest = requestAnimationFrame(this.update);
|
|
this.frame++;
|
|
}
|
|
}
|
|
|
|
randomChar() {
|
|
return this.chars[Math.floor(Math.random() * this.chars.length)];
|
|
}
|
|
}
|
|
|
|
// Initialize text scramble on hover for certain elements
|
|
document.querySelectorAll('.logo-text').forEach(el => {
|
|
const fx = new TextScramble(el);
|
|
const originalText = el.textContent;
|
|
|
|
el.addEventListener('mouseenter', () => {
|
|
fx.setText(originalText);
|
|
});
|
|
});
|
|
|
|
/* ==================== Lazy Load Images ==================== */
|
|
function initLazyLoad() {
|
|
const images = document.querySelectorAll('img[data-src]');
|
|
|
|
const imageObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const img = entry.target;
|
|
img.src = img.dataset.src;
|
|
img.classList.add('loaded');
|
|
imageObserver.unobserve(img);
|
|
}
|
|
});
|
|
}, {
|
|
rootMargin: '50px 0px'
|
|
});
|
|
|
|
images.forEach(img => imageObserver.observe(img));
|
|
}
|
|
|
|
/* ==================== Performance Optimization ==================== */
|
|
// Debounce function for expensive operations
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Throttle function for scroll events
|
|
function throttle(func, limit) {
|
|
let inThrottle;
|
|
return function(...args) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
};
|
|
}
|
|
|
|
/* Loading Screen Enhancement - handled by preloader in main.js */
|
|
|
|
/* ==================== Console Easter Egg ==================== */
|
|
console.log('%c🚀 MSPE - Architects of Digital Resilience', 'color: #00f5ff; font-size: 24px; font-weight: bold; text-shadow: 0 0 10px #00f5ff;');
|
|
console.log('%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'color: #b537f2');
|
|
console.log('%c💼 Looking for opportunities? We\'re hiring!', 'color: #b537f2; font-size: 14px;');
|
|
console.log('%c🔧 Built with passion, precision, and cutting-edge tech', 'color: #ff006e; font-size: 12px;');
|
|
console.log('%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'color: #b537f2');
|