785 lines
24 KiB
JavaScript
785 lines
24 KiB
JavaScript
/**
|
|
* Template Name: FolioOne
|
|
* Template URL: https://bootstrapmade.com/folioone-bootstrap-portfolio-website-template/
|
|
* Updated: Aug 23 2025 with Bootstrap v5.3.7
|
|
* Author: BootstrapMade.com
|
|
* License: https://bootstrapmade.com/license/
|
|
*/
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
/**
|
|
* Apply .scrolled class to the body as the page is scrolled down
|
|
*/
|
|
function toggleScrolled() {
|
|
const selectBody = document.querySelector('body');
|
|
const selectHeader = document.querySelector('#header');
|
|
if (!selectHeader.classList.contains('scroll-up-sticky') && !selectHeader.classList.contains('sticky-top') && !selectHeader.classList.contains('fixed-top')) return;
|
|
window.scrollY > 100 ? selectBody.classList.add('scrolled') : selectBody.classList.remove('scrolled');
|
|
}
|
|
|
|
document.addEventListener('scroll', toggleScrolled);
|
|
window.addEventListener('load', toggleScrolled);
|
|
|
|
/**
|
|
* Mobile nav toggle
|
|
*/
|
|
const mobileNavToggleBtn = document.querySelector('.mobile-nav-toggle');
|
|
|
|
function mobileNavToogle() {
|
|
document.querySelector('body').classList.toggle('mobile-nav-active');
|
|
mobileNavToggleBtn.classList.toggle('bi-list');
|
|
mobileNavToggleBtn.classList.toggle('bi-x');
|
|
}
|
|
if (mobileNavToggleBtn) {
|
|
mobileNavToggleBtn.addEventListener('click', mobileNavToogle);
|
|
}
|
|
|
|
/**
|
|
* Hide mobile nav on same-page/hash links
|
|
*/
|
|
document.querySelectorAll('#navmenu a').forEach(navmenu => {
|
|
navmenu.addEventListener('click', () => {
|
|
if (document.querySelector('.mobile-nav-active')) {
|
|
mobileNavToogle();
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
/**
|
|
* Toggle mobile nav dropdowns
|
|
*/
|
|
document.querySelectorAll('.navmenu .toggle-dropdown').forEach(navmenu => {
|
|
navmenu.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
this.parentNode.classList.toggle('active');
|
|
this.parentNode.nextElementSibling.classList.toggle('dropdown-active');
|
|
e.stopImmediatePropagation();
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Preloader
|
|
*/
|
|
const preloader = document.querySelector('#preloader');
|
|
if (preloader) {
|
|
window.addEventListener('load', () => {
|
|
preloader.remove();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scroll top button
|
|
*/
|
|
let scrollTop = document.querySelector('.scroll-top');
|
|
|
|
function toggleScrollTop() {
|
|
if (scrollTop) {
|
|
window.scrollY > 100 ? scrollTop.classList.add('active') : scrollTop.classList.remove('active');
|
|
}
|
|
}
|
|
scrollTop.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
});
|
|
|
|
window.addEventListener('load', toggleScrollTop);
|
|
document.addEventListener('scroll', toggleScrollTop);
|
|
|
|
/**
|
|
* Init typed.js
|
|
*/
|
|
const selectTyped = document.querySelector('.typed');
|
|
if (selectTyped && typeof Typed !== 'undefined') {
|
|
let typed_strings = selectTyped.getAttribute('data-typed-items');
|
|
typed_strings = typed_strings.split(',');
|
|
new Typed('.typed', {
|
|
strings: typed_strings,
|
|
loop: true,
|
|
typeSpeed: 100,
|
|
backSpeed: 50,
|
|
backDelay: 2000
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Animate the skills items on reveal
|
|
*/
|
|
let skillsAnimation = document.querySelectorAll('.skills-animation');
|
|
if (skillsAnimation.length > 0 && typeof Waypoint !== 'undefined') {
|
|
skillsAnimation.forEach((item) => {
|
|
new Waypoint({
|
|
element: item,
|
|
offset: '80%',
|
|
handler: function (direction) {
|
|
let progress = item.querySelectorAll('.progress .progress-bar');
|
|
progress.forEach(el => {
|
|
el.style.width = el.getAttribute('aria-valuenow') + '%';
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initiate Pure Counter
|
|
*/
|
|
if (typeof PureCounter !== 'undefined') {
|
|
new PureCounter();
|
|
}
|
|
|
|
/**
|
|
* Init swiper sliders
|
|
*/
|
|
function initSwiper() {
|
|
if (typeof Swiper !== 'undefined') {
|
|
document.querySelectorAll(".init-swiper").forEach(function (swiperElement) {
|
|
let config = JSON.parse(
|
|
swiperElement.querySelector(".swiper-config").innerHTML.trim()
|
|
);
|
|
|
|
if (swiperElement.classList.contains("swiper-tab")) {
|
|
initSwiperWithCustomPagination(swiperElement, config);
|
|
} else {
|
|
new Swiper(swiperElement, config);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
window.addEventListener("load", initSwiper);
|
|
|
|
/**
|
|
* Init isotope layout and filters
|
|
*/
|
|
if (typeof Isotope !== 'undefined' && typeof imagesLoaded !== 'undefined') {
|
|
document.querySelectorAll('.isotope-layout').forEach(function (isotopeItem) {
|
|
let layout = isotopeItem.getAttribute('data-layout') ?? 'masonry';
|
|
let filter = isotopeItem.getAttribute('data-default-filter') ?? '*';
|
|
let sort = isotopeItem.getAttribute('data-sort') ?? 'original-order';
|
|
|
|
let initIsotope;
|
|
imagesLoaded(isotopeItem.querySelector('.isotope-container'), function () {
|
|
initIsotope = new Isotope(isotopeItem.querySelector('.isotope-container'), {
|
|
itemSelector: '.isotope-item',
|
|
layoutMode: layout,
|
|
filter: filter,
|
|
sortBy: sort
|
|
});
|
|
});
|
|
|
|
isotopeItem.querySelectorAll('.isotope-filters li').forEach(function (filters) {
|
|
filters.addEventListener('click', function () {
|
|
isotopeItem.querySelector('.isotope-filters .filter-active').classList.remove('filter-active');
|
|
this.classList.add('filter-active');
|
|
initIsotope.arrange({
|
|
filter: this.getAttribute('data-filter')
|
|
});
|
|
if (typeof aosInit === 'function') {
|
|
aosInit();
|
|
}
|
|
}, false);
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initiate glightbox
|
|
*/
|
|
if (typeof GLightbox !== 'undefined') {
|
|
const glightbox = GLightbox({
|
|
selector: '.glightbox'
|
|
});
|
|
}
|
|
|
|
})();
|
|
|
|
/**
|
|
* UX Enhancements - Custom Cursor (Optimized)
|
|
*/
|
|
(function () {
|
|
// Only enable on non-touch devices and desktops (not mobile)
|
|
if (window.matchMedia("(hover: hover) and (pointer: fine)").matches && window.innerWidth >= 1024) {
|
|
window.addEventListener('load', function () {
|
|
const cursor = document.createElement('div');
|
|
cursor.className = 'custom-cursor';
|
|
const cursorFollower = document.createElement('div');
|
|
cursorFollower.className = 'custom-cursor-follower';
|
|
|
|
document.body.appendChild(cursor);
|
|
document.body.appendChild(cursorFollower);
|
|
|
|
let mouseX = 0, mouseY = 0;
|
|
let followerX = 0, followerY = 0;
|
|
|
|
// Use transform for better performance
|
|
document.addEventListener('mousemove', (e) => {
|
|
mouseX = e.clientX;
|
|
mouseY = e.clientY;
|
|
cursor.style.transform = `translate(${mouseX}px, ${mouseY}px)`;
|
|
});
|
|
|
|
// Smooth follower animation with requestAnimationFrame
|
|
function animateFollower() {
|
|
const distX = mouseX - followerX;
|
|
const distY = mouseY - followerY;
|
|
|
|
followerX += distX * 0.15; // Augmenté de 0.1 à 0.15 pour plus de réactivité
|
|
followerY += distY * 0.15;
|
|
|
|
cursorFollower.style.transform = `translate(${followerX}px, ${followerY}px)`;
|
|
|
|
requestAnimationFrame(animateFollower);
|
|
}
|
|
animateFollower();
|
|
|
|
// Add hover effects for interactive elements (avec délégation d'événements)
|
|
document.addEventListener('mouseenter', (e) => {
|
|
if (e.target.matches('a, button, .btn, input, textarea, select')) {
|
|
cursor.classList.add('hover');
|
|
cursorFollower.classList.add('hover');
|
|
}
|
|
}, true);
|
|
|
|
document.addEventListener('mouseleave', (e) => {
|
|
if (e.target.matches('a, button, .btn, input, textarea, select')) {
|
|
cursor.classList.remove('hover');
|
|
cursorFollower.classList.remove('hover');
|
|
}
|
|
}, true);
|
|
});
|
|
}
|
|
})();
|
|
|
|
/**
|
|
* UX Enhancements - Scroll Progress Indicator (Optimized with RAF)
|
|
*/
|
|
(function () {
|
|
const progressBar = document.createElement('div');
|
|
progressBar.className = 'scroll-progress';
|
|
document.body.appendChild(progressBar);
|
|
|
|
let ticking = false;
|
|
|
|
function updateScrollProgress() {
|
|
const windowHeight = window.innerHeight;
|
|
const documentHeight = document.documentElement.scrollHeight;
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
const scrollPercent = Math.min((scrollTop / (documentHeight - windowHeight)) * 100, 100);
|
|
|
|
progressBar.style.width = scrollPercent + '%';
|
|
ticking = false;
|
|
}
|
|
|
|
window.addEventListener('scroll', function () {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(updateScrollProgress);
|
|
ticking = true;
|
|
}
|
|
}, { passive: true });
|
|
|
|
window.addEventListener('load', updateScrollProgress);
|
|
})();
|
|
|
|
/**
|
|
* UX Enhancements - Text Reveal on Scroll (Optimized with RAF)
|
|
*/
|
|
(function () {
|
|
window.addEventListener('load', function () {
|
|
const revealElements = document.querySelectorAll('.reveal-text');
|
|
|
|
if (revealElements.length === 0) return;
|
|
|
|
let ticking = false;
|
|
|
|
function checkReveal() {
|
|
revealElements.forEach(el => {
|
|
if (!el.classList.contains('revealed')) {
|
|
const elementTop = el.getBoundingClientRect().top;
|
|
const elementVisible = 150;
|
|
|
|
if (elementTop < window.innerHeight - elementVisible) {
|
|
el.classList.add('revealed');
|
|
}
|
|
}
|
|
});
|
|
ticking = false;
|
|
}
|
|
|
|
window.addEventListener('scroll', function () {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(checkReveal);
|
|
ticking = true;
|
|
}
|
|
}, { passive: true });
|
|
|
|
checkReveal(); // Initial check
|
|
});
|
|
})();
|
|
|
|
/**
|
|
* UX Enhancements - Smooth Parallax Effect (Optimized with RAF)
|
|
*/
|
|
(function () {
|
|
window.addEventListener('load', function () {
|
|
const parallaxElements = document.querySelectorAll('.parallax-bg');
|
|
|
|
if (parallaxElements.length === 0) return;
|
|
|
|
let ticking = false;
|
|
|
|
function updateParallax() {
|
|
const scrolled = window.pageYOffset;
|
|
parallaxElements.forEach(el => {
|
|
const rate = scrolled * 0.3;
|
|
el.style.transform = `translate3d(0, ${rate}px, 0)`;
|
|
});
|
|
ticking = false;
|
|
}
|
|
|
|
window.addEventListener('scroll', function () {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(updateParallax);
|
|
ticking = true;
|
|
}
|
|
}, { passive: true });
|
|
});
|
|
})();
|
|
|
|
/**
|
|
* UX Enhancements - Enhanced Card Tilt Effect (Subtle 3D) - Optimized
|
|
*/
|
|
(function () {
|
|
window.addEventListener('load', function () {
|
|
const tiltElements = document.querySelectorAll('.service-item, .portfolio-item');
|
|
|
|
if (tiltElements.length === 0) return; // Exit if no elements
|
|
|
|
tiltElements.forEach(el => {
|
|
let ticking = false;
|
|
|
|
el.addEventListener('mousemove', (e) => {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(() => {
|
|
const rect = el.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
const centerX = rect.width / 2;
|
|
const centerY = rect.height / 2;
|
|
|
|
const rotateX = ((y - centerY) / centerY) * 2; // Réduit à 2 pour plus de subtilité
|
|
const rotateY = ((centerX - x) / centerX) * 2;
|
|
|
|
// Préserve le translateY existant et ajoute la rotation 3D
|
|
const baseTranslateY = el.classList.contains('service-item') ? -5 : -10;
|
|
el.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateY(${baseTranslateY}px)`;
|
|
|
|
ticking = false;
|
|
});
|
|
ticking = true;
|
|
}
|
|
});
|
|
|
|
el.addEventListener('mouseleave', () => {
|
|
// Remet la transformation de base au survol (définie dans le CSS)
|
|
el.style.transform = '';
|
|
});
|
|
});
|
|
});
|
|
})();
|
|
|
|
/**
|
|
* UX Enhancements - Improved AOS Configuration (Optimized for Performance)
|
|
*/
|
|
function aosInit() {
|
|
if (typeof AOS !== 'undefined') {
|
|
AOS.init({
|
|
duration: 400, // Encore plus rapide pour éviter le "lag" visuel
|
|
easing: 'ease-out-cubic', // Easing plus dynamique
|
|
once: true,
|
|
mirror: false,
|
|
offset: 50, // Déclenchement très tôt
|
|
delay: 0,
|
|
anchorPlacement: 'top-bottom',
|
|
disable: function () {
|
|
// Désactiver sur mobile pour meilleures performances
|
|
return window.innerWidth < 768;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
window.addEventListener('load', aosInit);
|
|
|
|
/**
|
|
* UX Enhancements - Smooth Scroll Enhancement (Optimized)
|
|
*/
|
|
window.addEventListener('load', function () {
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function (e) {
|
|
const href = this.getAttribute('href');
|
|
if (href !== '#' && href !== '#scroll-top') {
|
|
const target = document.querySelector(href);
|
|
if (target) {
|
|
e.preventDefault();
|
|
target.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* UX Enhancements - Add Loading States to Buttons (Optimized)
|
|
*/
|
|
/**
|
|
* UX Enhancements - Add Loading States to Buttons (Optimized)
|
|
* REMOVED: Interferes with Formspree submission
|
|
*/
|
|
// window.addEventListener('load', function () {
|
|
// const submitButtons = document.querySelectorAll('button[type="submit"], .btn[type="submit"]');
|
|
//
|
|
// submitButtons.forEach(btn => {
|
|
// btn.addEventListener('click', function (e) {
|
|
// if (this.form && this.form.checkValidity()) {
|
|
// const originalText = this.innerHTML;
|
|
// this.innerHTML = '<i class="bi bi-hourglass-split"></i> Envoi en cours...';
|
|
// this.disabled = true;
|
|
//
|
|
// // Reset after 3 seconds (adjust based on your needs)
|
|
// setTimeout(() => {
|
|
// this.innerHTML = originalText;
|
|
// this.disabled = false;
|
|
// }, 3000);
|
|
// }
|
|
// });
|
|
// });
|
|
// });
|
|
|
|
/**
|
|
* UX Enhancements - Ambient Light Effects with Parallax
|
|
*/
|
|
(function () {
|
|
// Only enable on desktop for performance
|
|
if (window.innerWidth >= 768) {
|
|
window.addEventListener('load', function () {
|
|
// Create ambient lights container
|
|
const ambientLights = document.createElement('div');
|
|
ambientLights.className = 'ambient-lights';
|
|
document.body.insertBefore(ambientLights, document.body.firstChild);
|
|
|
|
// Create light orbs
|
|
for (let i = 1; i <= 4; i++) {
|
|
const orb = document.createElement('div');
|
|
orb.className = `light-orb light-orb-${i}`;
|
|
ambientLights.appendChild(orb);
|
|
}
|
|
|
|
// Create light rays
|
|
for (let i = 1; i <= 3; i++) {
|
|
const ray = document.createElement('div');
|
|
ray.className = `light-ray light-ray-${i}`;
|
|
ambientLights.appendChild(ray);
|
|
}
|
|
|
|
// Create light particles (random positions)
|
|
for (let i = 0; i < 20; i++) {
|
|
const particle = document.createElement('div');
|
|
particle.className = 'light-particle';
|
|
particle.style.top = Math.random() * 100 + '%';
|
|
particle.style.left = Math.random() * 100 + '%';
|
|
particle.style.animationDelay = Math.random() * 3 + 's';
|
|
ambientLights.appendChild(particle);
|
|
}
|
|
|
|
// Create gradient spotlights
|
|
for (let i = 1; i <= 2; i++) {
|
|
const spotlight = document.createElement('div');
|
|
spotlight.className = `gradient-spotlight gradient-spotlight-${i}`;
|
|
ambientLights.appendChild(spotlight);
|
|
}
|
|
|
|
// Parallax effect on scroll (optimized with RAF and throttling)
|
|
const orbs = document.querySelectorAll('.light-orb');
|
|
const rays = document.querySelectorAll('.light-ray');
|
|
const spotlights = document.querySelectorAll('.gradient-spotlight');
|
|
let ticking = false;
|
|
|
|
function updateLightParallax() {
|
|
const scrollY = window.pageYOffset;
|
|
|
|
// Orbs: slow parallax (different speeds)
|
|
orbs.forEach((orb, index) => {
|
|
const speed = 0.1 + (index * 0.05); // Different speed for each orb
|
|
const yPos = scrollY * speed;
|
|
orb.style.transform = `translateY(${yPos}px)`;
|
|
});
|
|
|
|
// Rays: medium parallax
|
|
rays.forEach((ray, index) => {
|
|
const speed = 0.15 + (index * 0.03);
|
|
const yPos = scrollY * speed;
|
|
const rotation = 15 + (index * 5);
|
|
ray.style.transform = `translateY(${yPos}px) rotate(${rotation}deg)`;
|
|
});
|
|
|
|
// Spotlights: faster parallax
|
|
spotlights.forEach((spotlight, index) => {
|
|
const speed = 0.25 + (index * 0.1);
|
|
const yPos = scrollY * speed;
|
|
if (index === 0) {
|
|
spotlight.style.transform = `translate(-50%, ${yPos}px)`;
|
|
} else {
|
|
spotlight.style.transform = `translateY(${yPos}px)`;
|
|
}
|
|
});
|
|
|
|
ticking = false;
|
|
}
|
|
|
|
window.addEventListener('scroll', function () {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(updateLightParallax);
|
|
ticking = true;
|
|
}
|
|
}, { passive: true });
|
|
|
|
// Initial position
|
|
updateLightParallax();
|
|
});
|
|
}
|
|
})();
|
|
|
|
/* Matrix-like background animation confined to header canvases (slower) */
|
|
(function () {
|
|
// Only run on larger screens to avoid perf issues on mobile
|
|
function shouldEnableMatrix() {
|
|
// Force-enable on Services page regardless of viewport width
|
|
if (document.body && document.body.classList.contains('services-page')) {
|
|
return true;
|
|
}
|
|
// Otherwise keep default threshold to avoid perf issues on small screens
|
|
return window.innerWidth >= 900;
|
|
}
|
|
|
|
const canvases = Array.from(document.querySelectorAll('.matrix-bg-header'));
|
|
if (!canvases.length) return;
|
|
|
|
const instances = [];
|
|
|
|
const chars = '01<>/{}[]()\u25A0\u25A1'.split('');
|
|
|
|
function createInstance(canvas) {
|
|
const ctx = canvas.getContext('2d');
|
|
let width = 0;
|
|
let height = 0;
|
|
let columns = [];
|
|
let animationId = null;
|
|
|
|
function init() {
|
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
const rect = canvas.parentElement.getBoundingClientRect();
|
|
width = canvas.width = Math.floor(rect.width * dpr);
|
|
// limit canvas height to header height
|
|
height = canvas.height = Math.floor(rect.height * dpr);
|
|
canvas.style.width = rect.width + 'px';
|
|
canvas.style.height = rect.height + 'px';
|
|
|
|
// slightly larger font for clarity, but scaled by DPR
|
|
const fontSize = Math.max(10, Math.floor((rect.width / 160) * dpr));
|
|
ctx.font = fontSize + 'px monospace';
|
|
ctx.textBaseline = 'top';
|
|
|
|
const columnCount = Math.ceil(width / fontSize);
|
|
columns = new Array(columnCount).fill(0).map(() => ({ y: Math.random() * -height }));
|
|
return fontSize;
|
|
}
|
|
|
|
function draw() {
|
|
// A darker translucent fill to keep effect subtle on header
|
|
ctx.fillStyle = 'rgba(16,26,32,0.35)';
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
const computed = getComputedStyle(document.documentElement).getPropertyValue('--matrix-pale-blue').trim() || '#8fcbe9';
|
|
ctx.fillStyle = computed;
|
|
|
|
// Slightly increase text opacity to make the falling characters a bit more visible
|
|
// Use save/restore so only the text rendering is affected (background fill stays same)
|
|
const textAlpha = 0.78; // small increase from default for better visibility
|
|
ctx.save();
|
|
ctx.globalAlpha = textAlpha;
|
|
|
|
const fontSizeMatch = ctx.font.match(/(\d+)px/);
|
|
const fontSize = fontSizeMatch ? parseInt(fontSizeMatch[1], 10) : 12;
|
|
|
|
// slower motion: smaller increments
|
|
for (let i = 0; i < columns.length; i += 1) {
|
|
const col = columns[i];
|
|
// render only rarely to greatly reduce density (slower effect)
|
|
if (Math.random() < 0.88) continue;
|
|
const text = chars[Math.floor(Math.random() * chars.length)];
|
|
const x = i * fontSize;
|
|
ctx.fillText(text, x, col.y);
|
|
// much slower falling speed and subtle randomness
|
|
col.y += (fontSize * 0.18) + Math.random() * (fontSize * 0.08);
|
|
if (col.y > height + Math.random() * 200) {
|
|
col.y = Math.random() * -height * 0.6;
|
|
}
|
|
}
|
|
|
|
// restore alpha after drawing text so other canvas ops are unaffected
|
|
ctx.restore();
|
|
|
|
animationId = requestAnimationFrame(draw);
|
|
}
|
|
|
|
function start() {
|
|
cancelAnimationFrame(animationId);
|
|
init();
|
|
draw();
|
|
}
|
|
|
|
function stop() {
|
|
if (animationId) cancelAnimationFrame(animationId);
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
return { start, stop, init };
|
|
}
|
|
|
|
canvases.forEach(c => instances.push(createInstance(c)));
|
|
|
|
function startAll() {
|
|
if (!shouldEnableMatrix()) return;
|
|
console.debug('[matrix] startAll: starting matrix instances');
|
|
instances.forEach(i => i.start());
|
|
}
|
|
|
|
function stopAll() {
|
|
console.debug('[matrix] stopAll: stopping matrix instances');
|
|
instances.forEach(i => i.stop());
|
|
}
|
|
|
|
// Pause on tab hidden
|
|
document.addEventListener('visibilitychange', function () {
|
|
if (document.hidden) stopAll(); else startAll();
|
|
});
|
|
|
|
let resizeTimer = null;
|
|
window.addEventListener('resize', function () {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(function () {
|
|
if (shouldEnableMatrix()) startAll(); else stopAll();
|
|
}, 300);
|
|
});
|
|
|
|
window.addEventListener('load', function () {
|
|
if (shouldEnableMatrix()) startAll();
|
|
else console.debug('[matrix] load: matrix disabled by shouldEnableMatrix() (width=' + window.innerWidth + ')');
|
|
});
|
|
|
|
})();
|
|
|
|
/**
|
|
* Light/Dark Mode Toggle
|
|
*/
|
|
(function () {
|
|
const body = document.body;
|
|
const toggleBtn = document.createElement('button');
|
|
toggleBtn.className = 'theme-toggle-btn';
|
|
toggleBtn.innerHTML = '<i class="bi bi-moon-stars-fill"></i>';
|
|
toggleBtn.setAttribute('aria-label', 'Toggle theme');
|
|
|
|
// Style the button (fixed position)
|
|
Object.assign(toggleBtn.style, {
|
|
position: 'fixed',
|
|
bottom: '20px',
|
|
right: '20px',
|
|
zIndex: '9999',
|
|
width: '50px',
|
|
height: '50px',
|
|
borderRadius: '50%',
|
|
border: 'none',
|
|
backgroundColor: 'var(--accent-color)',
|
|
color: '#fff',
|
|
fontSize: '1.2rem',
|
|
cursor: 'pointer',
|
|
boxShadow: '0 4px 15px rgba(0,0,0,0.3)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
transition: 'all 0.3s ease'
|
|
});
|
|
|
|
// Hover effect
|
|
toggleBtn.addEventListener('mouseenter', () => {
|
|
toggleBtn.style.transform = 'scale(1.1)';
|
|
});
|
|
toggleBtn.addEventListener('mouseleave', () => {
|
|
toggleBtn.style.transform = 'scale(1)';
|
|
});
|
|
|
|
document.body.appendChild(toggleBtn);
|
|
|
|
function setLightMode(isLight) {
|
|
if (isLight) {
|
|
body.classList.add('light-mode');
|
|
toggleBtn.innerHTML = '<i class="bi bi-sun-fill"></i>';
|
|
} else {
|
|
body.classList.remove('light-mode');
|
|
toggleBtn.innerHTML = '<i class="bi bi-moon-stars-fill"></i>';
|
|
}
|
|
}
|
|
|
|
// Check preference or time
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme) {
|
|
setLightMode(savedTheme === 'light');
|
|
} else {
|
|
const hour = new Date().getHours();
|
|
// Light mode between 00:00 and 17:00
|
|
if (hour < 17) {
|
|
setLightMode(true);
|
|
}
|
|
}
|
|
|
|
// Toggle event
|
|
toggleBtn.addEventListener('click', () => {
|
|
const isLight = body.classList.contains('light-mode');
|
|
setLightMode(!isLight);
|
|
localStorage.setItem('theme', !isLight ? 'light' : 'dark');
|
|
});
|
|
|
|
/**
|
|
* UX Enhancements - Smooth Page Transitions
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const links = document.querySelectorAll('a:not([href^="#"]):not([target="_blank"]):not([href^="mailto:"]):not([href^="tel:"])');
|
|
|
|
links.forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
const href = link.getAttribute('href');
|
|
|
|
// Check if it's a valid internal link
|
|
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {
|
|
e.preventDefault();
|
|
document.body.classList.add('fade-out');
|
|
|
|
setTimeout(() => {
|
|
window.location.href = href;
|
|
}, 300); // Match CSS transition duration
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
})(); |