/** * REBOURS — Main Script * CAD/CAO-inspired interface · GSAP ScrollTrigger · Technical drawing overlays · Ambient sound */ import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; gsap.registerPlugin(ScrollTrigger); document.addEventListener('DOMContentLoaded', () => { // ---- CONFIG ---- const isMobile = window.innerWidth <= 600; const isTouch = 'ontouchstart' in window; // ---- HEADER HEIGHT → CSS VAR ---- const setHeaderHeight = () => { const h = document.querySelector('.header')?.offsetHeight || 44; document.documentElement.style.setProperty('--header-h', h + 'px'); }; setHeaderHeight(); window.addEventListener('resize', setHeaderHeight); // ========================================================== // 1. CAD CROSSHAIR CURSOR WITH X/Y COORDINATES // ========================================================== let attachCursorHover = () => {}; if (!isMobile && !isTouch) { const cursorH = document.createElement('div'); cursorH.className = 'cad-h'; const cursorV = document.createElement('div'); cursorV.className = 'cad-v'; const cursorCenter = document.createElement('div'); cursorCenter.className = 'cad-center'; const cursorCoords = document.createElement('div'); cursorCoords.className = 'cad-coords'; [cursorH, cursorV, cursorCenter, cursorCoords].forEach(el => document.body.appendChild(el)); let visible = false; window.addEventListener('mousemove', (e) => { const x = e.clientX; const y = e.clientY; cursorH.style.left = (x - 16) + 'px'; cursorH.style.top = y + 'px'; cursorV.style.left = x + 'px'; cursorV.style.top = (y - 16) + 'px'; cursorCenter.style.left = x + 'px'; cursorCenter.style.top = y + 'px'; cursorCoords.style.left = (x + 16) + 'px'; cursorCoords.style.top = (y + 14) + 'px'; cursorCoords.textContent = 'X:' + String(Math.round(x)).padStart(4, '0') + ' Y:' + String(Math.round(y)).padStart(4, '0'); if (!visible) { visible = true; [cursorH, cursorV, cursorCenter, cursorCoords].forEach(el => { el.style.opacity = '1'; }); } }); attachCursorHover = (elements) => { elements.forEach(el => { el.addEventListener('mouseenter', () => { cursorH.classList.add('cad-hover'); cursorV.classList.add('cad-hover'); cursorCenter.classList.add('cad-hover'); }); el.addEventListener('mouseleave', () => { cursorH.classList.remove('cad-hover'); cursorV.classList.remove('cad-hover'); cursorCenter.classList.remove('cad-hover'); }); }); }; attachCursorHover(document.querySelectorAll( 'a, button, input, .product-card, summary, .panel-close' )); // WhatsApp hover — green center dot document.querySelectorAll('.whatsapp-btn').forEach(el => { el.addEventListener('mouseenter', () => cursorCenter.classList.add('cad-whatsapp')); el.addEventListener('mouseleave', () => cursorCenter.classList.remove('cad-whatsapp')); }); } // ========================================================== // 2. INTERACTIVE GRID // ========================================================== const gridContainer = document.getElementById('interactive-grid'); const COLORS = [ 'rgba(232,168,0,0.45)', 'rgba(232,168,0,0.32)', 'rgba(232,168,0,0.18)', ]; function buildGrid() { if (!gridContainer) return; gridContainer.innerHTML = ''; const CELL = isMobile ? 38 : 60; const cols = Math.ceil(window.innerWidth / CELL); const rows = Math.ceil(window.innerHeight / CELL); gridContainer.style.display = 'grid'; gridContainer.style.gridTemplateColumns = `repeat(${cols}, ${CELL}px)`; gridContainer.style.gridTemplateRows = `repeat(${rows}, ${CELL}px)`; for (let i = 0; i < cols * rows; i++) { const cell = document.createElement('div'); cell.className = 'grid-cell'; cell.addEventListener('mouseenter', () => { cell.style.transition = 'none'; cell.style.backgroundColor = COLORS[Math.floor(Math.random() * COLORS.length)]; }); cell.addEventListener('mouseleave', () => { cell.style.transition = 'background-color 1.4s ease-out'; cell.style.backgroundColor = 'transparent'; }); gridContainer.appendChild(cell); } } buildGrid(); let rt; window.addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(buildGrid, 150); }); // ========================================================== // 3. GSAP SCROLL ANIMATIONS — CAD REVEAL // ========================================================== // ---- Header fade in ---- const header = document.querySelector('.header'); if (header) { gsap.fromTo(header, { opacity: 0, y: -10 }, { opacity: 1, y: 0, duration: 0.5, ease: 'power2.out' } ); } // ---- Hero animations (scroll-triggered, replay in/out) ---- const heroLabel = document.querySelector('.hero-left .label'); const heroH1 = document.querySelector('.hero-left h1'); const heroSubs = document.querySelectorAll('.hero-sub'); const heroImg = document.querySelector('.hero-img'); const heroRight = document.querySelector('.hero-right'); const heroTl = gsap.timeline({ defaults: { ease: 'power3.out' }, scrollTrigger: { trigger: '.hero', start: 'top 95%', end: 'bottom 5%', toggleActions: 'play reverse play reverse', }, }); if (heroLabel) { heroTl.fromTo(heroLabel, { opacity: 0, x: -20 }, { opacity: 1, x: 0, duration: 0.6 }, 0.1 ); } if (heroH1) { heroTl.fromTo(heroH1, { opacity: 0, y: 40, clipPath: 'inset(0 0 100% 0)' }, { opacity: 1, y: 0, clipPath: 'inset(0 0 0% 0)', duration: 1 }, 0.2 ); } heroSubs.forEach((sub, i) => { heroTl.fromTo(sub, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.6 }, 0.5 + i * 0.15 ); }); if (heroImg && heroRight) { heroTl.fromTo(heroRight, { clipPath: 'inset(0 0 0 100%)' }, { clipPath: 'inset(0 0 0 0%)', duration: 1.2, ease: 'power4.inOut' }, 0.3 ); heroTl.fromTo(heroImg, { scale: 1.15, opacity: 0 }, { scale: 1, opacity: 0.92, duration: 1.4, ease: 'power2.out' }, 0.4 ); } // Hero parallax on scroll if (heroImg) { gsap.to(heroImg, { yPercent: 15, ease: 'none', scrollTrigger: { trigger: '.hero', start: 'top top', end: 'bottom top', scrub: true, }, }); } // ---- Collection header: construction line draw-in ---- const collectionHeader = document.querySelector('.collection-header'); if (collectionHeader) { const line = document.createElement('div'); line.className = 'cad-construction-line'; collectionHeader.appendChild(line); gsap.fromTo(line, { scaleX: 0 }, { scaleX: 1, transformOrigin: 'left center', duration: 0.8, ease: 'power2.out', scrollTrigger: { trigger: collectionHeader, start: 'top 95%', end: 'bottom 5%', toggleActions: 'play reverse play reverse', }, } ); } // ---- Product cards: scale + fade reveal on scroll (replays) ---- const cards = document.querySelectorAll('.product-card'); cards.forEach((card, i) => { const imgWrap = card.querySelector('.card-img-wrap'); const img = card.querySelector('.card-img-wrap img'); const meta = card.querySelector('.card-meta'); if (!img) return; const tl = gsap.timeline({ scrollTrigger: { trigger: card, start: 'top 95%', end: 'bottom 5%', toggleActions: 'play reverse play reverse', }, }); // Clip-path reveal + scale + fade tl.fromTo(imgWrap, { clipPath: 'inset(8% 8% 8% 8%)' }, { clipPath: 'inset(0% 0% 0% 0%)', duration: 0.8, ease: 'power3.out' }, 0 ); tl.fromTo(img, { opacity: 0, scale: 1.12 }, { opacity: 1, scale: 1, duration: 0.9, ease: 'power2.out' }, 0 ); if (meta) { tl.fromTo(meta, { opacity: 0, y: 15 }, { opacity: 1, y: 0, duration: 0.5, ease: 'power2.out' }, 0.25 ); } }); // ---- Newsletter section: slide in (replays) ---- const nlLeft = document.querySelector('.nl-left'); const nlRight = document.querySelector('.nl-right'); if (nlLeft && nlRight) { gsap.fromTo(nlLeft, { opacity: 0, x: -40 }, { opacity: 1, x: 0, duration: 0.7, ease: 'power2.out', scrollTrigger: { trigger: '.newsletter', start: 'top 95%', end: 'bottom 5%', toggleActions: 'play reverse play reverse' }, } ); gsap.fromTo(nlRight, { opacity: 0, x: 40 }, { opacity: 1, x: 0, duration: 0.7, ease: 'power2.out', delay: 0.15, scrollTrigger: { trigger: '.newsletter', start: 'top 95%', end: 'bottom 5%', toggleActions: 'play reverse play reverse' }, } ); } // ========================================================== // 4. (REMOVED) — no overlay effects on panel image // ========================================================== // ========================================================== // 5. PRODUCT PANEL // ========================================================== const panel = document.getElementById('product-panel'); const panelClose = document.getElementById('panel-close'); const panelCards = document.querySelectorAll('.product-card'); const fields = { img: document.getElementById('panel-img'), index: document.getElementById('panel-index'), name: document.getElementById('panel-name'), type: document.getElementById('panel-type'), mat: document.getElementById('panel-mat'), year: document.getElementById('panel-year'), status: document.getElementById('panel-status'), desc: document.getElementById('panel-desc'), specs: document.getElementById('panel-specs'), notes: document.getElementById('panel-notes'), }; // ---- CHECKOUT LOGIC ---- const checkoutSection = document.getElementById('checkout-section'); const checkoutToggleBtn = document.getElementById('checkout-toggle-btn'); const checkoutFormWrap = document.getElementById('checkout-form-wrap'); const checkoutForm = document.getElementById('checkout-form'); const checkoutSubmitBtn = document.getElementById('checkout-submit-btn'); const checkoutPriceEl = document.querySelector('.checkout-price'); let currentSlug = null; function formatPrice(cents) { return (cents / 100).toLocaleString('fr-FR') + ' €'; } if (checkoutToggleBtn) { checkoutToggleBtn.addEventListener('click', () => { const isOpen = checkoutFormWrap.style.display !== 'none'; checkoutFormWrap.style.display = isOpen ? 'none' : 'block'; checkoutToggleBtn.textContent = isOpen ? '[ COMMANDER CETTE PIÈCE ]' : '[ ANNULER ]'; }); } if (checkoutForm) { checkoutForm.addEventListener('submit', async (e) => { e.preventDefault(); if (!currentSlug) return; const email = document.getElementById('checkout-email').value; checkoutSubmitBtn.disabled = true; checkoutSubmitBtn.textContent = 'CONNEXION STRIPE...'; try { const res = await fetch('/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product: currentSlug, email }), }); const data = await res.json(); if (data.url) { window.location.href = data.url; } else { throw new Error('No URL returned'); } } catch (err) { checkoutSubmitBtn.disabled = false; checkoutSubmitBtn.textContent = 'ERREUR — RÉESSAYER'; console.error(err); } }); } function toSlug(name) { return name .toLowerCase() .normalize('NFD').replace(/[\u0300-\u036f]/g, '') .replace(/_/g, '-') .replace(/[^a-z0-9-]/g, ''); } function openPanel(card, pushState = true) { fields.img.src = card.dataset.img; fields.img.alt = card.dataset.imgAlt || card.dataset.name; fields.index.textContent = card.dataset.index; fields.name.textContent = card.dataset.name; fields.type.textContent = card.dataset.type; fields.mat.textContent = card.dataset.mat; fields.year.textContent = card.dataset.year; fields.status.textContent = card.dataset.status; fields.desc.textContent = card.dataset.desc; fields.specs.textContent = card.dataset.specs; fields.notes.textContent = card.dataset.notes; // Checkout const price = card.dataset.price; const slug = card.dataset.slug; const isOrderable = price && slug; checkoutSection.style.display = 'block'; if (isOrderable) { currentSlug = slug; checkoutPriceEl.textContent = formatPrice(parseInt(price, 10)); checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]'; checkoutToggleBtn.disabled = false; checkoutToggleBtn.classList.remove('checkout-btn--disabled'); } else { currentSlug = null; checkoutPriceEl.textContent = ''; checkoutToggleBtn.textContent = 'PROCHAINEMENT DISPONIBLE'; checkoutToggleBtn.disabled = true; checkoutToggleBtn.classList.add('checkout-btn--disabled'); } checkoutFormWrap.style.display = 'none'; checkoutSubmitBtn.disabled = false; checkoutSubmitBtn.textContent = 'PROCÉDER AU PAIEMENT →'; checkoutForm.reset(); panel.querySelectorAll('details').forEach(d => d.setAttribute('open', '')); panel.classList.add('is-open'); panel.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; attachCursorHover(panel.querySelectorAll( 'summary, .panel-close, .checkout-btn, .checkout-submit' )); if (pushState) { const cardSlug = card.dataset.slug || toSlug(card.dataset.name); history.pushState({ slug: cardSlug }, '', `/collection/${cardSlug}`); } } function closePanel(pushState = true) { panel.classList.remove('is-open'); panel.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; if (pushState) { history.pushState({}, '', '/'); } } panelCards.forEach(card => { card.addEventListener('click', () => openPanel(card)); }); // Auto-open from direct URL (/collection/[slug]) if (window.__OPEN_PANEL__) { const name = window.__OPEN_PANEL__; const card = [...panelCards].find(c => c.dataset.name === name); if (card) openPanel(card, false); } if (panelClose) panelClose.addEventListener('click', () => closePanel()); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePanel(); }); window.addEventListener('popstate', () => { if (panel.classList.contains('is-open')) { closePanel(false); } else { const match = location.pathname.match(/^\/collection\/(.+)$/); if (match) { const slug = match[1]; const card = [...panelCards].find(c => c.dataset.slug === slug || toSlug(c.dataset.name) === slug); if (card) openPanel(card, false); } } }); // ========================================================== // 6. AMBIENT WORKSHOP SOUND (MP3) // ========================================================== let soundOn = true; // Create audio element (preloaded, looped, low volume) const ambientAudio = new Audio('/assets/atelier-ambiance.mp3'); ambientAudio.loop = true; ambientAudio.volume = 0; ambientAudio.preload = 'auto'; const headerNav = document.querySelector('.header-nav'); if (headerNav) { const soundBtn = document.createElement('button'); soundBtn.className = 'sound-toggle sound-active'; soundBtn.setAttribute('aria-label', 'Couper le son ambiant'); soundBtn.innerHTML = '' + '' + '' + '' + '' + 'SON'; soundBtn.addEventListener('click', toggleSound); headerNav.appendChild(soundBtn); attachCursorHover([soundBtn]); } // Autoplay on first user interaction (browsers require it) let autoStarted = false; function autoStartSound() { if (autoStarted || !soundOn) return; autoStarted = true; startAmbientSound(); window.removeEventListener('click', autoStartSound); window.removeEventListener('scroll', autoStartSound); window.removeEventListener('mousemove', autoStartSound); } window.addEventListener('click', autoStartSound, { once: false }); window.addEventListener('scroll', autoStartSound, { once: false }); window.addEventListener('mousemove', autoStartSound, { once: false }); // Smooth volume fade using GSAP function startAmbientSound() { ambientAudio.play().then(() => { gsap.to(ambientAudio, { volume: 0.04, duration: 2, ease: 'power2.out' }); }).catch(() => {}); } function stopAmbientSound() { gsap.to(ambientAudio, { volume: 0, duration: 1.2, ease: 'power2.in', onComplete: () => ambientAudio.pause(), }); } function toggleSound() { soundOn = !soundOn; const btn = document.querySelector('.sound-toggle'); if (!btn) return; if (soundOn) { startAmbientSound(); btn.classList.add('sound-active'); btn.setAttribute('aria-label', 'Couper le son ambiant'); } else { stopAmbientSound(); btn.classList.remove('sound-active'); btn.setAttribute('aria-label', 'Activer le son ambiant'); } } // ========================================================== // 7. CONTACT MODAL // ========================================================== const contactModal = document.getElementById('contact-modal'); const contactForm = document.getElementById('contact-form'); function openContactModal() { if (!contactModal) return; contactModal.classList.add('is-open'); contactModal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; attachCursorHover(contactModal.querySelectorAll('button, input, textarea')); } function closeContactModal() { if (!contactModal) return; contactModal.classList.remove('is-open'); contactModal.setAttribute('aria-hidden', 'true'); if (!panel.classList.contains('is-open')) { document.body.style.overflow = ''; } } // Trigger links document.querySelectorAll('.contact-trigger').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); openContactModal(); }); }); // Close button const contactCloseBtn = contactModal?.querySelector('.contact-modal-close'); if (contactCloseBtn) contactCloseBtn.addEventListener('click', closeContactModal); // Backdrop click const contactBackdrop = contactModal?.querySelector('.contact-modal-backdrop'); if (contactBackdrop) contactBackdrop.addEventListener('click', closeContactModal); // Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && contactModal?.classList.contains('is-open')) { closeContactModal(); } }); // Form → WhatsApp if (contactForm) { contactForm.addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('contact-name').value.trim(); const email = document.getElementById('contact-email').value.trim(); const subject = document.getElementById('contact-subject').value.trim() || 'Contact depuis rebours.studio'; const message = document.getElementById('contact-message').value.trim(); const text = `*${subject}*\n\n${message}\n\n— ${name}\n${email}`; window.open(`https://wa.me/33651755191?text=${encodeURIComponent(text)}`, '_blank'); }); } });