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