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