From 90e229a763e3d6b2c21c9e03383e9593387c05f4 Mon Sep 17 00:00:00 2001 From: ordinarthur Date: Fri, 20 Mar 2026 22:15:18 +0100 Subject: [PATCH] add style frerot le boss --- .DS_Store | Bin 8196 -> 6148 bytes .claude/launch.json | 11 + public/main.js | 589 ++++++++++++++++++++++++++++++++++++----- public/style.css | 306 +++++++++++++++++++-- src/layouts/Base.astro | 4 + 5 files changed, 823 insertions(+), 87 deletions(-) create mode 100644 .claude/launch.json diff --git a/.DS_Store b/.DS_Store index 64c44bbdf0ab1eb49952f294d61d3a8b33d770f2..0fe76b9ec08f86c6d5f85af502205eebf2fabdbf 100644 GIT binary patch delta 122 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50D98ko2a9DgBxfd<6>Lmc%{lFy8b**Q1_nSsiHK!6)axPr8AEd0(qnP0{eWEca`5RkzjyBIdd^UPre E0N(!-`2YX_ delta 792 zcmb7CJ#Q015S_Kp_8tz39YTnr6u~Yi5{v@|fe?zh2nj<$#6<)VqRV~BUUIp0?#^+* zvP_W%3M6w~M2AE}6?lq;=Cn66yKm<0?96U)w>${|L}^&_ z0K%lWgwCY9TNB;F*{m$i(FX{?uQ=S?UwINT6v za4EP7e;VZhgD!gUR9pk{( zJH`WMH?_?8B6kCwxwaQYFv~{F;}zywys7YpYxz;I+CQK&m9@;7R%^aAH&eP=YR}EI zu3apmed$WOEz{Ha3)k;!8_uT7UqrVCg0B$C$$=iIF?fmAkp~E;rBZHcYFf*jGIVpJ z>IwQq?r=|W8umkGa#v-~?64($E^3)kA8V@M-jZi;V=1duk6CJ6H@Ev}@FCvi%k|VREL~f7;a+_4hBVv*o zVd2-rxz13}wpGt{xLd`nqRtb*zy%BYxdR2v9Ka?xqWeRA{eZaPzNQePM{xys(w~(> zG|3%9C!yXOlVY*+GLB6EPr*U50+?cqjuMgAhoSph>#=MceU!;G{%(m;?PcljcsM#< Z3Ga>nf)$Y%fm diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..5f58119 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "rebours-dev", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["dev"], + "port": 4321 + } + ] +} diff --git a/public/main.js b/public/main.js index 4274238..5c5f9ac 100644 --- a/public/main.js +++ b/public/main.js @@ -1,9 +1,16 @@ /** - * REBOUR — Main Script + * REBOURS — Main Script + * CAD/CAO-inspired interface · GSAP ScrollTrigger · Technical drawing overlays · Ambient sound */ document.addEventListener('DOMContentLoaded', () => { + // ---- CONFIG ---- + const isMobile = window.innerWidth <= 600; + const isTouch = 'ontouchstart' in window; + + gsap.registerPlugin(ScrollTrigger); + // ---- HEADER HEIGHT → CSS VAR ---- const setHeaderHeight = () => { const h = document.querySelector('.header')?.offsetHeight || 44; @@ -12,58 +19,76 @@ document.addEventListener('DOMContentLoaded', () => { setHeaderHeight(); window.addEventListener('resize', setHeaderHeight); - // ---- CUSTOM CURSOR ---- - const cursorDot = document.querySelector('.cursor-dot'); - const cursorOutline = document.querySelector('.cursor-outline'); + // ========================================================== + // 1. CAD CROSSHAIR CURSOR WITH X/Y COORDINATES + // ========================================================== - let mouseX = 0, mouseY = 0; - let outlineX = 0, outlineY = 0; - let rafId = null; + let attachCursorHover = () => {}; - window.addEventListener('mousemove', (e) => { - mouseX = e.clientX; - mouseY = e.clientY; - // Première fois : initialise l'outline à la position courante et rend visible - if (outlineX === 0 && outlineY === 0) { - outlineX = mouseX; - outlineY = mouseY; - cursorDot.style.opacity = '1'; - cursorOutline.style.opacity = '1'; - } - cursorDot.style.transform = `translate(calc(-50% + ${mouseX}px), calc(-50% + ${mouseY}px))`; - if (!rafId) rafId = requestAnimationFrame(animateOutline); - }, { once: false }); + 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'; - function animateOutline() { - rafId = null; - outlineX += (mouseX - outlineX) * 0.18; - outlineY += (mouseY - outlineY) * 0.18; - cursorOutline.style.transform = `translate(calc(-50% + ${outlineX}px), calc(-50% + ${outlineY}px))`; - if (Math.abs(mouseX - outlineX) > 0.1 || Math.abs(mouseY - outlineY) > 0.1) { - rafId = requestAnimationFrame(animateOutline); - } - } + [cursorH, cursorV, cursorCenter, cursorCoords].forEach(el => document.body.appendChild(el)); - let hoverCount = 0; + let visible = false; - function attachCursorHover(elements) { - elements.forEach(el => { - el.addEventListener('mouseenter', () => { - cursorOutline.style.width = '38px'; - cursorOutline.style.height = '38px'; - }); - el.addEventListener('mouseleave', () => { - cursorOutline.style.width = '26px'; - cursorOutline.style.height = '26px'; - }); + 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' + )); } - attachCursorHover(document.querySelectorAll('a, button, input, .product-card, summary, .panel-close')); + // ========================================================== + // 2. INTERACTIVE GRID + // ========================================================== - // ---- INTERACTIVE GRID ---- const gridContainer = document.getElementById('interactive-grid'); - const CELL = window.innerWidth <= 600 ? 38 : 60; const COLORS = [ 'rgba(232,168,0,0.45)', 'rgba(232,168,0,0.32)', @@ -73,7 +98,7 @@ document.addEventListener('DOMContentLoaded', () => { function buildGrid() { if (!gridContainer) return; gridContainer.innerHTML = ''; - const CELL = window.innerWidth <= 600 ? 38 : 60; + const CELL = isMobile ? 38 : 60; const cols = Math.ceil(window.innerWidth / CELL); const rows = Math.ceil(window.innerHeight / CELL); gridContainer.style.display = 'grid'; @@ -99,12 +124,316 @@ document.addEventListener('DOMContentLoaded', () => { let rt; window.addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(buildGrid, 150); }); - // ---- PRODUCT PANEL ---- - const panel = document.getElementById('product-panel'); - const panelClose = document.getElementById('panel-close'); + // ========================================================== + // 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) { + // Add a construction line element + 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'); - // Champs du panel + 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.style.position = 'relative'; + 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'), @@ -125,16 +454,12 @@ document.addEventListener('DOMContentLoaded', () => { const checkoutForm = document.getElementById('checkout-form'); const checkoutSubmitBtn = document.getElementById('checkout-submit-btn'); const checkoutPriceEl = document.querySelector('.checkout-price'); - - // Current product's stripe key (set dynamically when opening panel) let currentStripeKey = null; - // Format price in cents to display string function formatPrice(cents) { return (cents / 100).toLocaleString('fr-FR') + ' €'; } - // Toggle affichage du form if (checkoutToggleBtn) { checkoutToggleBtn.addEventListener('click', () => { const isOpen = checkoutFormWrap.style.display !== 'none'; @@ -145,7 +470,6 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // Submit → appel API → redirect Stripe if (checkoutForm) { checkoutForm.addEventListener('submit', async (e) => { e.preventDefault(); @@ -175,7 +499,6 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // Slug à partir du nom du produit : "Solar_Altar" → "solar-altar" function toSlug(name) { return name .toLowerCase() @@ -197,7 +520,7 @@ document.addEventListener('DOMContentLoaded', () => { fields.specs.textContent = card.dataset.specs; fields.notes.textContent = card.dataset.notes; - // Checkout : afficher uniquement si le produit a un prix et un stripe key + // Checkout const price = card.dataset.price; const stripeKey = card.dataset.stripeKey; const isOrderable = price && stripeKey; @@ -211,24 +534,25 @@ document.addEventListener('DOMContentLoaded', () => { checkoutSection.style.display = 'none'; } - // Reset form state checkoutFormWrap.style.display = 'none'; checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]'; checkoutSubmitBtn.disabled = false; checkoutSubmitBtn.textContent = 'PROCÉDER AU PAIEMENT →'; checkoutForm.reset(); - // Ferme les accordéons panel.querySelectorAll('details').forEach(d => d.removeAttribute('open')); - panel.classList.add('is-open'); panel.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; - // Refresh cursor sur les nouveaux éléments - attachCursorHover(panel.querySelectorAll('summary, .panel-close, .checkout-btn, .checkout-submit')); + attachCursorHover(panel.querySelectorAll( + 'summary, .panel-close, .checkout-btn, .checkout-submit' + )); + + // Technical drawing overlay (delayed for panel slide-in) + createTechOverlay(card); + setTimeout(() => animateTechOverlay(), 350); - // Mise à jour de l'URL if (pushState) { const slug = toSlug(card.dataset.name); history.pushState({ slug }, '', `/collection/${slug}`); @@ -236,6 +560,7 @@ document.addEventListener('DOMContentLoaded', () => { } function closePanel(pushState = true) { + hideTechOverlay(); panel.classList.remove('is-open'); panel.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; @@ -245,37 +570,165 @@ document.addEventListener('DOMContentLoaded', () => { } } - cards.forEach(card => { + panelCards.forEach(card => { card.addEventListener('click', () => openPanel(card)); }); - // Ouverture automatique si on arrive directement sur /collection/[slug] + // Auto-open from direct URL (/collection/[slug]) if (window.__OPEN_PANEL__) { const name = window.__OPEN_PANEL__; - const card = [...cards].find(c => c.dataset.name === name); + const card = [...panelCards].find(c => c.dataset.name === name); if (card) openPanel(card, false); } if (panelClose) panelClose.addEventListener('click', () => closePanel()); - // Echap pour fermer document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePanel(); }); - // Bouton retour navigateur window.addEventListener('popstate', () => { if (panel.classList.contains('is-open')) { closePanel(false); } else { - // Tente de rouvrir si on navigue vers un slug connu const match = location.pathname.match(/^\/collection\/(.+)$/); if (match) { const slug = match[1]; - const card = [...cards].find(c => toSlug(c.dataset.name) === slug); + const card = [...panelCards].find(c => toSlug(c.dataset.name) === slug); if (card) openPanel(card, false); } } }); + // ========================================================== + // 6. AMBIENT WORKSHOP SOUND + // ========================================================== + + let audioCtx = null; + let soundOn = false; + let soundNodes = []; + let clickTimer = null; + + const headerNav = document.querySelector('.header-nav'); + if (headerNav) { + const soundBtn = document.createElement('button'); + soundBtn.className = 'sound-toggle'; + soundBtn.setAttribute('aria-label', 'Activer le son ambiant'); + soundBtn.innerHTML = + '' + + '' + + '' + + '' + + '' + + 'SON'; + soundBtn.addEventListener('click', toggleSound); + headerNav.appendChild(soundBtn); + attachCursorHover([soundBtn]); + } + + function createAudioContext() { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + + function generateBrownNoise(ctx, duration) { + const bufferSize = ctx.sampleRate * duration; + const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + const data = buffer.getChannelData(0); + let lastOut = 0; + for (let i = 0; i < bufferSize; i++) { + const white = Math.random() * 2 - 1; + data[i] = (lastOut + (0.02 * white)) / 1.02; + lastOut = data[i]; + data[i] *= 3.5; + } + return buffer; + } + + function startAmbientSound() { + if (!audioCtx) createAudioContext(); + if (audioCtx.state === 'suspended') audioCtx.resume(); + + const noiseBuffer = generateBrownNoise(audioCtx, 4); + const noiseSource = audioCtx.createBufferSource(); + noiseSource.buffer = noiseBuffer; + noiseSource.loop = true; + + const lowpass = audioCtx.createBiquadFilter(); + lowpass.type = 'lowpass'; + lowpass.frequency.value = 180; + + const gain = audioCtx.createGain(); + gain.gain.value = 0; + gain.gain.linearRampToValueAtTime(0.045, audioCtx.currentTime + 2); + + noiseSource.connect(lowpass); + lowpass.connect(gain); + gain.connect(audioCtx.destination); + noiseSource.start(); + + soundNodes.push({ source: noiseSource, gain: gain }); + scheduleMetalClick(); + } + + function playMetalClick() { + if (!audioCtx || audioCtx.state !== 'running') return; + + const osc = audioCtx.createOscillator(); + osc.type = 'sine'; + osc.frequency.value = 600 + Math.random() * 2400; + + const gain = audioCtx.createGain(); + gain.gain.value = 0.006 + Math.random() * 0.014; + gain.gain.exponentialRampToValueAtTime( + 0.0001, + audioCtx.currentTime + 0.08 + Math.random() * 0.25 + ); + + const filter = audioCtx.createBiquadFilter(); + filter.type = 'bandpass'; + filter.frequency.value = 1000 + Math.random() * 2000; + filter.Q.value = 12 + Math.random() * 25; + + osc.connect(filter); + filter.connect(gain); + gain.connect(audioCtx.destination); + osc.start(); + osc.stop(audioCtx.currentTime + 0.5); + } + + function scheduleMetalClick() { + if (!soundOn) return; + const delay = 2000 + Math.random() * 7000; + clickTimer = setTimeout(() => { + if (!soundOn) return; + playMetalClick(); + scheduleMetalClick(); + }, delay); + } + + function stopAmbientSound() { + if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } + soundNodes.forEach(({ source, gain }) => { + gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 1); + setTimeout(() => { try { source.stop(); } catch (e) {} }, 1200); + }); + soundNodes = []; + } + + 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'); + } + } + }); diff --git a/public/style.css b/public/style.css index 3a44a8b..9bcbfad 100644 --- a/public/style.css +++ b/public/style.css @@ -26,27 +26,55 @@ body { cursor: none; } -/* ---- CURSOR ---- */ -.cursor-dot { - width: 5px; height: 5px; - background: var(--clr-red); - position: fixed; top: 0; left: 0; - transform: translate(-50%, -50%); - pointer-events: none; - z-index: 999999; - opacity: 0; - will-change: transform; -} -.cursor-outline { - width: 26px; height: 26px; - border: 1px solid var(--clr-black); - position: fixed; top: 0; left: 0; - transform: translate(-50%, -50%); +/* ---- LEGACY CURSOR (hidden) ---- */ +.cursor-dot, .cursor-outline { display: none; } + +/* ---- CAD CROSSHAIR CURSOR ---- */ +.cad-h, .cad-v { + position: fixed; pointer-events: none; z-index: 999998; opacity: 0; - transition: width 0.15s, height 0.15s, opacity 0.15s; - will-change: transform; + transition: width 0.15s, height 0.15s; +} +.cad-h { + width: 40px; height: 1px; + background: var(--clr-black); + transform: translateY(-0.5px); +} +.cad-h.cad-hover { width: 56px; margin-left: -8px; } +.cad-v { + width: 1px; height: 40px; + background: var(--clr-black); + transform: translateX(-0.5px); +} +.cad-v.cad-hover { height: 56px; margin-top: -8px; } +.cad-center { + position: fixed; + width: 3px; height: 3px; + background: var(--clr-red); + pointer-events: none; + z-index: 999999; + opacity: 0; + transform: translate(-1.5px, -1.5px); +} +.cad-center.cad-hover { + width: 5px; height: 5px; + transform: translate(-2.5px, -2.5px); +} +.cad-coords { + position: fixed; + pointer-events: none; + z-index: 999999; + font-family: var(--font-mono); + font-size: 0.55rem; + color: rgba(0,0,0,0.55); + opacity: 0; + letter-spacing: 0.05em; + background: rgba(200,200,200,0.7); + padding: 1px 5px; + border: 1px solid rgba(0,0,0,0.08); + white-space: nowrap; } /* ---- INTERACTIVE GRID (derrière tout) ---- */ @@ -592,6 +620,245 @@ hr { border: none; border-top: var(--border); margin: 0; } .footer a { color: var(--clr-black); text-decoration: none; } .footer a:hover { text-decoration: underline; } +/* ---- TECHNICAL DRAWING OVERLAY (panel image) ---- */ +.tech-overlay { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 10; + overflow: hidden; +} +.tech-corner { + position: absolute; + width: 28px; height: 28px; + border-color: rgba(232,168,0,0); + border-style: solid; + border-width: 0; + transition: border-color 0.6s ease, width 0.5s ease, height 0.5s ease; +} +.tech-visible .tech-corner { + border-color: rgba(232,168,0,0.6); +} +.tech-corner--tl { top: 7%; left: 5%; border-top-width: 1px; border-left-width: 1px; } +.tech-corner--tr { top: 7%; right: 5%; border-top-width: 1px; border-right-width: 1px; } +.tech-corner--br { bottom: 7%; right: 5%; border-bottom-width: 1px; border-right-width: 1px; } +.tech-corner--bl { bottom: 7%; left: 5%; border-bottom-width: 1px; border-left-width: 1px; } + +.tech-center-h, .tech-center-v { + position: absolute; + background: rgba(232,168,0,0); + transition: background 0.5s ease 0.15s; +} +.tech-visible .tech-center-h, .tech-visible .tech-center-v { + background: rgba(232,168,0,0.2); +} +.tech-center-h { + top: 50%; left: 38%; right: 38%; + height: 1px; transform: translateY(-0.5px); +} +.tech-center-v { + left: 50%; top: 38%; bottom: 38%; + width: 1px; transform: translateX(-0.5px); +} + +/* Dimension lines */ +.tech-dim { + position: absolute; + display: flex; + align-items: center; + gap: 6px; + opacity: 0; + transition: opacity 0.5s ease 0.3s; +} +.tech-visible .tech-dim { opacity: 1; } +.tech-dim--h { + bottom: 4%; left: 5%; right: 5%; +} +.tech-dim--v { + right: 2.5%; top: 7%; bottom: 7%; + flex-direction: column; + writing-mode: vertical-lr; +} +.tech-dim-line { + flex: 1; +} +.tech-dim--h .tech-dim-line { height: 1px; background: rgba(232,168,0,0.35); } +.tech-dim--v .tech-dim-line { width: 1px; background: rgba(232,168,0,0.35); } +.tech-dim-text { + font-family: var(--font-mono); + font-size: 0.5rem; + color: rgba(232,168,0,0.65); + letter-spacing: 0.06em; +} +/* Dimension arrows */ +.tech-dim-arrow { + font-size: 0.5rem; + color: rgba(232,168,0,0.5); + line-height: 1; +} + +/* Reference text */ +.tech-ref { + position: absolute; + bottom: 1.5%; left: 5%; + font-family: var(--font-mono); + font-size: 0.45rem; + color: rgba(232,168,0,0); + letter-spacing: 0.06em; + transition: color 0.5s ease 0.5s; +} +.tech-visible .tech-ref { color: rgba(232,168,0,0.45); } + +/* Blueprint grid */ +.tech-grid { + position: absolute; + inset: 7% 5%; + background-image: + linear-gradient(rgba(232,168,0,0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(232,168,0,0.04) 1px, transparent 1px); + background-size: 40px 40px; + opacity: 0; + transition: opacity 0.8s ease 0.05s; +} +.tech-visible .tech-grid { opacity: 1; } + +/* Scan line effect */ +.tech-scanline { + position: absolute; + left: 0; right: 0; + height: 2px; + background: linear-gradient(to bottom, + transparent, + rgba(232,168,0,0.6), + rgba(232,168,0,0.9), + rgba(232,168,0,0.6), + transparent); + box-shadow: 0 0 12px rgba(232,168,0,0.4); + z-index: 11; + pointer-events: none; + opacity: 0; + top: 0; +} +.tech-visible .tech-scanline { + animation: techScanLine 1s ease-in-out forwards; + animation-delay: 0.1s; +} +@keyframes techScanLine { + 0% { top: 0%; opacity: 1; } + 90% { opacity: 0.8; } + 100% { top: 100%; opacity: 0; } +} + +/* ---- CARD CAD ELEMENTS (scroll-triggered via GSAP) ---- */ +.card-scanline { + position: absolute; + left: 0; right: 0; + height: 2px; + background: linear-gradient(to bottom, + transparent, + rgba(232,168,0,0.7), + rgba(232,168,0,1), + rgba(232,168,0,0.7), + transparent); + box-shadow: 0 0 16px rgba(232,168,0,0.5), 0 0 4px rgba(232,168,0,0.8); + z-index: 5; + pointer-events: none; + top: 100%; +} +.card-corners { + position: absolute; + inset: 8%; + pointer-events: none; + z-index: 4; +} +.cc { + position: absolute; + width: 16px; height: 16px; + border-color: rgba(232,168,0,0.55); + border-style: solid; + border-width: 0; +} +.cc-tl { top: 0; left: 0; border-top-width: 1px; border-left-width: 1px; } +.cc-tr { top: 0; right: 0; border-top-width: 1px; border-right-width: 1px; } +.cc-br { bottom: 0; right: 0; border-bottom-width: 1px; border-right-width: 1px; } +.cc-bl { bottom: 0; left: 0; border-bottom-width: 1px; border-left-width: 1px; } + +.card-coord-tag { + position: absolute; + bottom: 6px; left: 8px; + font-family: var(--font-mono); + font-size: 0.45rem; + color: rgba(232,168,0,0.6); + letter-spacing: 0.05em; + z-index: 5; + pointer-events: none; + background: rgba(0,0,0,0.35); + padding: 1px 5px; +} + +/* Construction line under collection header */ +.cad-construction-line { + position: absolute; + bottom: 0; left: 0; right: 0; + height: 1px; + background: linear-gradient(to right, + var(--clr-red), + var(--clr-red) 60%, + transparent); + transform-origin: left center; +} +.collection-header { + position: relative; +} + +/* ---- SOUND TOGGLE ---- */ +.sound-toggle { + display: flex; + align-items: center; + gap: 0.35rem; + background: none; + border: 1px solid rgba(0,0,0,0.18); + font-family: var(--font-mono); + font-size: 0.65rem; + color: #999; + padding: 0.2rem 0.55rem; + cursor: none; + transition: color 0.2s, border-color 0.2s; + letter-spacing: 0.04em; +} +.sound-toggle:hover { + color: var(--clr-black); + border-color: var(--clr-black); +} +.sound-toggle.sound-active { + color: var(--clr-red); + border-color: var(--clr-red); +} +.sound-icon { font-size: 0.7rem; line-height: 1; } +.sound-bars { + display: flex; + align-items: flex-end; + gap: 1px; + height: 10px; +} +.sound-bar { + width: 2px; + background: currentColor; + transition: height 0.2s; +} +.sound-bar:nth-child(1) { height: 3px; } +.sound-bar:nth-child(2) { height: 6px; } +.sound-bar:nth-child(3) { height: 4px; } +.sound-toggle.sound-active .sound-bar { + animation: soundPulse 0.8s ease-in-out infinite alternate; +} +.sound-toggle.sound-active .sound-bar:nth-child(2) { animation-delay: 0.15s; } +.sound-toggle.sound-active .sound-bar:nth-child(3) { animation-delay: 0.3s; } +@keyframes soundPulse { + 0% { height: 2px; } + 100% { height: 10px; } +} + /* ---- RESPONSIVE ---- */ @media (max-width: 900px) { .hero, .newsletter { grid-template-columns: 1fr; } @@ -611,6 +878,7 @@ hr { border: none; border-top: var(--border); margin: 0; } .hero-left { min-height: unset; padding: 2rem var(--pad); order: -1; } .hero-right { height: 60vw; } - .cursor-dot, .cursor-outline { display: none; } + .cursor-dot, .cursor-outline, + .cad-h, .cad-v, .cad-center, .cad-coords { display: none; } body { cursor: auto; } } diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index f172a8f..af4c609 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -57,6 +57,10 @@ const { + + + +