Restructure: move all files from Public HTML/ to root for Hostinger deployment
This commit is contained in:
Executable
+1110
File diff suppressed because it is too large
Load Diff
Executable
+472
@@ -0,0 +1,472 @@
|
||||
/* ========================================
|
||||
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');
|
||||
Reference in New Issue
Block a user