/** * 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 - 20) + 'px'; cursorH.style.top = y + 'px'; cursorV.style.left = x + 'px'; cursorV.style.top = (y - 20) + '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' )); } // ========================================================== // 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 // ========================================================== // ---- Hero parallax ---- const heroImg = document.querySelector('.hero-img'); 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.from(line, { scaleX: 0, transformOrigin: 'left center', duration: 0.8, ease: 'power2.out', scrollTrigger: { trigger: collectionHeader, start: 'top 85%', }, }); } // ---- Product cards: staggered CAD reveal ---- const cards = document.querySelectorAll('.product-card'); cards.forEach((card, i) => { const img = card.querySelector('.card-img-wrap img'); const meta = card.querySelector('.card-meta'); const imgWrap = card.querySelector('.card-img-wrap'); if (!img) return; // Create a CAD scan line per card const scanLine = document.createElement('div'); scanLine.className = 'card-scanline'; imgWrap.appendChild(scanLine); // Create corner marks per card const cornerOverlay = document.createElement('div'); cornerOverlay.className = 'card-corners'; cornerOverlay.innerHTML = '' + '' + '' + ''; imgWrap.appendChild(cornerOverlay); // Create coordinate annotation per card const coordTag = document.createElement('div'); coordTag.className = 'card-coord-tag'; coordTag.textContent = `[${String(i + 1).padStart(3, '0')}] — ${img.naturalWidth || '1024'} × ${img.naturalHeight || '1024'} px`; imgWrap.appendChild(coordTag); // GSAP timeline triggered on scroll const tl = gsap.timeline({ scrollTrigger: { trigger: card, start: 'top 88%', toggleActions: 'play none none none', }, }); // 1. Image clip-path reveal (bottom → top scan) tl.fromTo(img, { clipPath: 'inset(100% 0 0 0)', filter: 'brightness(1.8) grayscale(100%)' }, { clipPath: 'inset(0% 0 0 0)', filter: 'brightness(1) grayscale(15%)', duration: 0.9, ease: 'power3.out' }, 0 ); // 2. Scan line sweeps up tl.fromTo(scanLine, { top: '100%', opacity: 1 }, { top: '-2%', opacity: 0, duration: 0.9, ease: 'power3.out' }, 0 ); // 3. Corner brackets appear tl.fromTo(cornerOverlay.querySelectorAll('.cc'), { opacity: 0, scale: 0.5 }, { opacity: 1, scale: 1, duration: 0.4, stagger: 0.06, ease: 'back.out(2)' }, 0.3 ); // 4. Coordinate tag types in tl.fromTo(coordTag, { opacity: 0, x: -10 }, { opacity: 1, x: 0, duration: 0.4, ease: 'power2.out' }, 0.5 ); // 5. Meta bar slides in if (meta) { tl.fromTo(meta, { opacity: 0, y: 8 }, { opacity: 1, y: 0, duration: 0.4, ease: 'power2.out' }, 0.4 ); } }); // ---- Newsletter section: slide in ---- const nlLeft = document.querySelector('.nl-left'); const nlRight = document.querySelector('.nl-right'); if (nlLeft && nlRight) { gsap.from(nlLeft, { opacity: 0, x: -30, duration: 0.7, ease: 'power2.out', scrollTrigger: { trigger: '.newsletter', start: 'top 80%' }, }); gsap.from(nlRight, { opacity: 0, x: 30, duration: 0.7, ease: 'power2.out', delay: 0.15, scrollTrigger: { trigger: '.newsletter', start: 'top 80%' }, }); } // ========================================================== // 4. TECHNICAL DRAWING OVERLAY (panel) // ========================================================== const panelImgCol = document.querySelector('.panel-img-col'); let techOverlay = null; let techTimeline = null; function createTechOverlay(card) { if (techOverlay) techOverlay.remove(); techOverlay = document.createElement('div'); techOverlay.className = 'tech-overlay'; // Corner brackets ['tl', 'tr', 'br', 'bl'].forEach(pos => { const corner = document.createElement('div'); corner.className = `tech-corner tech-corner--${pos}`; techOverlay.appendChild(corner); }); // Center crosshair const centerH = document.createElement('div'); centerH.className = 'tech-center-h'; const centerV = document.createElement('div'); centerV.className = 'tech-center-v'; techOverlay.appendChild(centerH); techOverlay.appendChild(centerV); // Horizontal dimension line const dimH = document.createElement('div'); dimH.className = 'tech-dim tech-dim--h'; dimH.innerHTML = '' + '' + '840' + '' + ''; techOverlay.appendChild(dimH); // Vertical dimension line const dimV = document.createElement('div'); dimV.className = 'tech-dim tech-dim--v'; dimV.innerHTML = '' + '' + '420' + '' + ''; techOverlay.appendChild(dimV); // Reference text const ref = document.createElement('div'); ref.className = 'tech-ref'; const idx = card ? card.dataset.index : '001'; ref.textContent = `REF: ${idx} — SCALE 1:5 — UNIT: mm`; techOverlay.appendChild(ref); // Blueprint grid const grid = document.createElement('div'); grid.className = 'tech-grid'; techOverlay.appendChild(grid); // Scan line const scanline = document.createElement('div'); scanline.className = 'tech-scanline'; techOverlay.appendChild(scanline); if (panelImgCol) panelImgCol.appendChild(techOverlay); } function animateTechOverlay() { if (!techOverlay) return; // Kill previous timeline if (techTimeline) techTimeline.kill(); const corners = techOverlay.querySelectorAll('.tech-corner'); const centerH = techOverlay.querySelector('.tech-center-h'); const centerV = techOverlay.querySelector('.tech-center-v'); const dimH = techOverlay.querySelector('.tech-dim--h'); const dimV = techOverlay.querySelector('.tech-dim--v'); const ref = techOverlay.querySelector('.tech-ref'); const grid = techOverlay.querySelector('.tech-grid'); const scanline = techOverlay.querySelector('.tech-scanline'); techTimeline = gsap.timeline(); // 1. Blueprint grid fades in techTimeline.fromTo(grid, { opacity: 0 }, { opacity: 1, duration: 0.6, ease: 'power1.in' }, 0 ); // 2. Scan line sweeps techTimeline.fromTo(scanline, { top: '0%', opacity: 1 }, { top: '100%', opacity: 0, duration: 1.1, ease: 'power2.inOut' }, 0 ); // 3. Corner brackets draw in (scale from corner) corners.forEach((c, i) => { const origins = ['top left', 'top right', 'bottom right', 'bottom left']; techTimeline.fromTo(c, { opacity: 0, scale: 0, borderColor: 'rgba(232,168,0,0)' }, { opacity: 1, scale: 1, borderColor: 'rgba(232,168,0,0.6)', transformOrigin: origins[i], duration: 0.4, ease: 'back.out(1.5)' }, 0.15 + i * 0.08 ); }); // 4. Center crosshair extends techTimeline.fromTo(centerH, { scaleX: 0, opacity: 0 }, { scaleX: 1, opacity: 1, duration: 0.5, ease: 'power2.out' }, 0.3 ); techTimeline.fromTo(centerV, { scaleY: 0, opacity: 0 }, { scaleY: 1, opacity: 1, duration: 0.5, ease: 'power2.out' }, 0.35 ); // 5. Dimension lines extend techTimeline.fromTo(dimH, { opacity: 0, scaleX: 0 }, { opacity: 1, scaleX: 1, transformOrigin: 'center center', duration: 0.6, ease: 'power2.out' }, 0.4 ); techTimeline.fromTo(dimV, { opacity: 0, scaleY: 0 }, { opacity: 1, scaleY: 1, transformOrigin: 'center center', duration: 0.6, ease: 'power2.out' }, 0.45 ); // 6. Reference text types in techTimeline.fromTo(ref, { opacity: 0, x: -15 }, { opacity: 1, x: 0, duration: 0.4, ease: 'power2.out' }, 0.6 ); // 7. Panel image scan reveal const panelImg = document.getElementById('panel-img'); if (panelImg) { techTimeline.fromTo(panelImg, { clipPath: 'inset(0 0 100% 0)', filter: 'brightness(1.8) grayscale(100%)' }, { clipPath: 'inset(0 0 0% 0)', filter: 'brightness(1) contrast(1) grayscale(0%)', duration: 1, ease: 'power3.out' }, 0.05 ); } } function hideTechOverlay() { if (techTimeline) techTimeline.kill(); if (techOverlay) { gsap.to(techOverlay, { opacity: 0, duration: 0.3, onComplete: () => { if (techOverlay) techOverlay.remove(); techOverlay = null; }}); } } // ========================================================== // 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 currentStripeKey = 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 (!currentStripeKey) 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: currentStripeKey, 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 stripeKey = card.dataset.stripeKey; const isOrderable = price && stripeKey; checkoutSection.style.display = 'block'; if (isOrderable) { currentStripeKey = stripeKey; checkoutPriceEl.textContent = formatPrice(parseInt(price, 10)); checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]'; checkoutToggleBtn.disabled = false; checkoutToggleBtn.classList.remove('checkout-btn--disabled'); } else { currentStripeKey = 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.removeAttribute('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' )); // Technical drawing overlay (delayed for panel slide-in) createTechOverlay(card); setTimeout(() => animateTechOverlay(), 350); if (pushState) { const slug = toSlug(card.dataset.name); history.pushState({ slug }, '', `/collection/${slug}`); } } function closePanel(pushState = true) { hideTechOverlay(); 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 => 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 → mailto 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 body = `${message}\n\n— ${name}\n${email}`; const mailto = `mailto:contact@rebours.fr?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; window.location.href = mailto; }); } });