diff --git a/.gitignore b/.gitignore index 3daa77e..e8358ba 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ artifacts/previews/ artifacts/imagegen/ artifacts/qa-test/*/responsive-v12-pass2/ artifacts/qa-test/*/deploy-sftp-dryrun.txt +.env diff --git a/src/pages/preview.astro b/src/pages/preview.astro index 993520c..13ac9b0 100644 --- a/src/pages/preview.astro +++ b/src/pages/preview.astro @@ -56,29 +56,64 @@ const headerItems = [ - -
-
- PCB custom Teensy LED — projet spectacle vivant KXKM - Câblage automate industriel — intervention terrain - Banc de test BMS batteries — prototypage - Automate Siemens S7 — diagnostic industriel - Soudure PCB — formation et prototypage - Instrumentation et mesure — oscilloscope - Rack CBTC transport — connectique industrielle - Carte embarquée en test — validation prototype - Armoire Schneider — automatisme industriel - Atelier professionnel — outillage organisé + + - -
-
- - - + + @@ -320,7 +355,64 @@ const headerItems = [ sections.forEach((el) => observer.observe(el)); } + // 3D tilt on scroll — cards tilt based on their position in viewport + function initScrollTilt() { + const cards = document.querySelectorAll('.webgl-card--visible'); + function updateTilt() { + cards.forEach((card) => { + const rect = card.getBoundingClientRect(); + const vh = window.innerHeight; + const center = rect.top + rect.height / 2; + const ratio = (center - vh / 2) / (vh / 2); // -1 (top) to 1 (bottom) + const rotateX = ratio * -3; + const rotateY = ratio * 1.5; + (card as HTMLElement).style.transform = + `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; + }); + requestAnimationFrame(updateTilt); + } + updateTilt(); + } + + // Carousel navigation + function initCarousels() { + document.querySelectorAll('.atomic-carousel-nav').forEach((btn) => { + btn.addEventListener('click', () => { + const dir = parseInt(btn.getAttribute('data-dir') || '1'); + const carouselType = btn.getAttribute('data-carousel'); + const orbit = document.getElementById(carouselType === 'photos' ? 'photo-orbit' : 'video-orbit'); + if (!orbit) return; + const itemWidth = orbit.querySelector('.atomic-carousel-item')?.clientWidth || 320; + orbit.scrollBy({ left: dir * (itemWidth + 24), behavior: 'smooth' }); + }); + }); + + // Track position indicator + ['photo-orbit', 'video-orbit'].forEach((id) => { + const orbit = document.getElementById(id); + const track = document.getElementById(id.replace('orbit', 'track')); + if (!orbit || !track) return; + orbit.addEventListener('scroll', () => { + const pct = orbit.scrollLeft / (orbit.scrollWidth - orbit.clientWidth) * 100; + track.style.setProperty('--track-position', `${pct}%`); + }); + }); + + // Video hover play + document.querySelectorAll('.atomic-carousel-item video').forEach((video) => { + const v = video as HTMLVideoElement; + v.closest('.atomic-carousel-item')?.addEventListener('mouseenter', () => v.play()); + v.closest('.atomic-carousel-item')?.addEventListener('mouseleave', () => { v.pause(); v.currentTime = 0; }); + }); + } + // Run on load and on Astro page transitions initScrollAnimations(); - document.addEventListener('astro:page-load', initScrollAnimations); + setTimeout(initScrollTilt, 2000); + initCarousels(); + document.addEventListener('astro:page-load', () => { + initScrollAnimations(); + setTimeout(initScrollTilt, 2000); + initCarousels(); + }); diff --git a/src/styles/webgl-cards.css b/src/styles/webgl-cards.css index 3ef63ac..0a98605 100644 --- a/src/styles/webgl-cards.css +++ b/src/styles/webgl-cards.css @@ -218,6 +218,187 @@ will-change: transform, opacity; } +/* ── 3D Tilt on scroll ──────────────────────────────── */ +.webgl-card { + perspective: 1000px; + transform-style: preserve-3d; +} + +.webgl-card--visible { + animation: card-3d-enter 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +@keyframes card-3d-enter { + 0% { + opacity: 0; + transform: perspective(1000px) rotateX(8deg) rotateY(-5deg) translateY(40px) scale(0.92); + filter: blur(6px); + } + 60% { + opacity: 1; + transform: perspective(1000px) rotateX(-2deg) rotateY(1deg) translateY(-5px) scale(1.01); + filter: blur(0); + } + 100% { + opacity: 1; + transform: perspective(1000px) rotateX(0deg) rotateY(0deg) translateY(0) scale(1); + filter: blur(0); + } +} + +/* Mouse-driven 3D tilt on hover */ +.webgl-card--visible:hover { + transition: box-shadow 0.3s ease; + box-shadow: + 0 0 24px rgba(91, 209, 216, 0.08), + 0 12px 40px rgba(0, 0, 0, 0.35); +} + +/* ── Atomic carousel for photos/videos ─────────────── */ +.atomic-carousel { + position: relative; + padding: clamp(40px, 6vw, 80px) 0; + overflow: visible; +} + +.atomic-carousel-orbit { + display: flex; + gap: clamp(12px, 2vw, 24px); + overflow-x: auto; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + padding: 20px clamp(20px, 4vw, 48px); + -ms-overflow-style: none; + scrollbar-width: none; +} + +.atomic-carousel-orbit::-webkit-scrollbar { + display: none; +} + +.atomic-carousel-item { + flex: 0 0 auto; + scroll-snap-align: center; + position: relative; + border-radius: 16px; + overflow: hidden; + cursor: pointer; + transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); + transform: perspective(800px) rotateY(0deg); +} + +.atomic-carousel-item::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 16px; + border: 1px solid rgba(91, 209, 216, 0.08); + pointer-events: none; + transition: border-color 0.3s ease, box-shadow 0.3s ease; + z-index: 2; +} + +.atomic-carousel-item:hover { + transform: perspective(800px) rotateY(-3deg) translateY(-8px) scale(1.04); + z-index: 10; +} + +.atomic-carousel-item:hover::before { + border-color: rgba(91, 209, 216, 0.3); + box-shadow: + 0 0 30px rgba(91, 209, 216, 0.12), + inset 0 0 20px rgba(91, 209, 216, 0.03); +} + +/* Electron dots orbiting around hovered item */ +.atomic-carousel-item:hover::after { + content: ''; + position: absolute; + width: 6px; + height: 6px; + border-radius: 50%; + background: #5bd1d8; + box-shadow: 0 0 12px #5bd1d8; + top: -3px; + left: 50%; + z-index: 3; + animation: electron-orbit-item 2s linear infinite; +} + +@keyframes electron-orbit-item { + 0% { top: -3px; left: 50%; } + 25% { top: 50%; left: calc(100% + 3px); } + 50% { top: calc(100% + 3px); left: 50%; } + 75% { top: 50%; left: -3px; } + 100% { top: -3px; left: 50%; } +} + +.atomic-carousel-item img, +.atomic-carousel-item video { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transition: filter 0.3s ease, transform 0.3s ease; +} + +.atomic-carousel-item:hover img, +.atomic-carousel-item:hover video { + filter: brightness(1.15) contrast(1.05); + transform: scale(1.02); +} + +/* Carousel navigation arrows */ +.atomic-carousel-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 40px; + height: 40px; + border-radius: 50%; + background: rgba(91, 209, 216, 0.1); + border: 1px solid rgba(91, 209, 216, 0.2); + color: #5bd1d8; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 20; + transition: all 0.2s ease; + backdrop-filter: blur(8px); +} + +.atomic-carousel-nav:hover { + background: rgba(91, 209, 216, 0.2); + transform: translateY(-50%) scale(1.1); + box-shadow: 0 0 20px rgba(91, 209, 216, 0.2); +} + +.atomic-carousel-nav--prev { left: 8px; } +.atomic-carousel-nav--next { right: 8px; } + +/* Orbital track line under carousel */ +.atomic-carousel-track { + height: 1px; + margin: 16px auto 0; + max-width: 200px; + background: linear-gradient(90deg, transparent, rgba(91, 209, 216, 0.3), transparent); + position: relative; +} + +.atomic-carousel-track::after { + content: ''; + position: absolute; + width: 8px; + height: 8px; + border-radius: 50%; + background: #5bd1d8; + box-shadow: 0 0 10px #5bd1d8; + top: -3px; + left: var(--track-position, 50%); + transition: left 0.3s ease; +} + /* ── Reduced motion ──────────────────────────────────── */ @media (prefers-reduced-motion: reduce) { .webgl-card,