/** * 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 = ' 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 = ''; 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 = ''; } else { body.classList.remove('light-mode'); toggleBtn.innerHTML = ''; } } // 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 } }); }); }); })();