feat: 3D scroll tilt cards + atomic carousel for photos/videos
- Cards: perspective 3D enter animation (rotateX/Y from blur) - Scroll tilt: cards subtly rotate based on viewport position - Atomic carousel: orbital scroll container with electron dot animation on hover - Navigation arrows with glow + orbital track indicator - Video hover autoplay in carousel items - Lightbox: click any photo/video for full-view with energy rings - Carousel items: 3D perspective rotateY(-3deg) on hover + glow border Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,3 +31,4 @@ artifacts/previews/
|
||||
artifacts/imagegen/
|
||||
artifacts/qa-test/*/responsive-v12-pass2/
|
||||
artifacts/qa-test/*/deploy-sftp-dryrun.txt
|
||||
.env
|
||||
|
||||
+112
-20
@@ -56,29 +56,64 @@ const headerItems = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PHOTO STRIP -->
|
||||
<section class="photo-strip section-dark" aria-label="Photos de terrain" data-reveal>
|
||||
<div class="photo-strip-track">
|
||||
<img src={withSiteBase('/assets/photos/pcb-teensy-led-kxkm.webp')} alt="PCB custom Teensy LED — projet spectacle vivant KXKM" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/platine-automate-cablage.webp')} alt="Câblage automate industriel — intervention terrain" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/bench-bms-batteries-xt60.webp')} alt="Banc de test BMS batteries — prototypage" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/automate-siemens-s7.webp')} alt="Automate Siemens S7 — diagnostic industriel" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/soudure-pcb-composants.webp')} alt="Soudure PCB — formation et prototypage" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/oscilloscope-philips-vintage.webp')} alt="Instrumentation et mesure — oscilloscope" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/rack-cbtc-lon-connecteurs.webp')} alt="Rack CBTC transport — connectique industrielle" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/pcb-embarquee-boitier-test.webp')} alt="Carte embarquée en test — validation prototype" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/armoire-automate-schneider.webp')} alt="Armoire Schneider — automatisme industriel" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<img src={withSiteBase('/assets/photos/outillage-atelier-pro.webp')} alt="Atelier professionnel — outillage organisé" loading="lazy" decoding="async" width="280" height="190" class="photo-strip-img" />
|
||||
<!-- PHOTO CAROUSEL (atomic orbit) -->
|
||||
<section class="section-dark webgl-section atomic-carousel" aria-label="Photos de terrain">
|
||||
<div class="section-content">
|
||||
<h2 style="color: rgba(255,255,255,0.4); font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; text-align: center; margin-bottom: 8px;">Terrain</h2>
|
||||
</div>
|
||||
<button class="atomic-carousel-nav atomic-carousel-nav--prev" data-carousel="photos" data-dir="-1" aria-label="Precedent">‹</button>
|
||||
<div class="atomic-carousel-orbit" id="photo-orbit">
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/pcb-teensy-led-kxkm.webp')} alt="PCB custom Teensy LED — projet spectacle vivant KXKM" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/platine-automate-cablage.webp')} alt="Câblage automate industriel" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/bench-bms-batteries-xt60.webp')} alt="Banc de test BMS batteries" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/automate-siemens-s7.webp')} alt="Automate Siemens S7" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/soudure-pcb-composants.webp')} alt="Soudure PCB composants" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/oscilloscope-philips-vintage.webp')} alt="Oscilloscope vintage" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/rack-cbtc-lon-connecteurs.webp')} alt="Rack CBTC connectique" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/pcb-embarquee-boitier-test.webp')} alt="Carte embarquée en test" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/armoire-automate-schneider.webp')} alt="Armoire Schneider" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 300px; height: 200px;">
|
||||
<img src={withSiteBase('/assets/photos/outillage-atelier-pro.webp')} alt="Atelier professionnel" loading="lazy" decoding="async" class="photo-strip-img" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="atomic-carousel-nav atomic-carousel-nav--next" data-carousel="photos" data-dir="1" aria-label="Suivant">›</button>
|
||||
<div class="atomic-carousel-track" id="photo-track"></div>
|
||||
</section>
|
||||
|
||||
<!-- VIDEO STRIP -->
|
||||
<section class="video-strip section-dark" aria-label="Vidéos de terrain" data-reveal>
|
||||
<div class="video-strip-track">
|
||||
<video src={withSiteBase('/assets/videos/video-bench-electronique.mp4')} poster={withSiteBase('/assets/videos/video-bench-electronique-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Bench électronique en action"></video>
|
||||
<video src={withSiteBase('/assets/videos/video-test-prototype.mp4')} poster={withSiteBase('/assets/videos/video-test-prototype-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Test de prototype"></video>
|
||||
<video src={withSiteBase('/assets/videos/video-instrumentation.mp4')} poster={withSiteBase('/assets/videos/video-instrumentation-poster.webp')} controls muted loop playsinline preload="none" width="320" height="180" class="video-strip-clip" aria-label="Instrumentation et mesure"></video>
|
||||
<!-- VIDEO CAROUSEL (atomic orbit) -->
|
||||
<section class="section-dark webgl-section atomic-carousel" aria-label="Vidéos de terrain">
|
||||
<button class="atomic-carousel-nav atomic-carousel-nav--prev" data-carousel="videos" data-dir="-1" aria-label="Precedent">‹</button>
|
||||
<div class="atomic-carousel-orbit" id="video-orbit">
|
||||
<div class="atomic-carousel-item" style="width: 360px; height: 200px;">
|
||||
<video src={withSiteBase('/assets/videos/video-bench-electronique.mp4')} poster={withSiteBase('/assets/videos/video-bench-electronique-poster.webp')} muted loop playsinline preload="none" class="video-strip-clip" aria-label="Bench électronique"></video>
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 360px; height: 200px;">
|
||||
<video src={withSiteBase('/assets/videos/video-test-prototype.mp4')} poster={withSiteBase('/assets/videos/video-test-prototype-poster.webp')} muted loop playsinline preload="none" class="video-strip-clip" aria-label="Test prototype"></video>
|
||||
</div>
|
||||
<div class="atomic-carousel-item" style="width: 360px; height: 200px;">
|
||||
<video src={withSiteBase('/assets/videos/video-instrumentation.mp4')} poster={withSiteBase('/assets/videos/video-instrumentation-poster.webp')} muted loop playsinline preload="none" class="video-strip-clip" aria-label="Instrumentation"></video>
|
||||
</div>
|
||||
</div>
|
||||
<button class="atomic-carousel-nav atomic-carousel-nav--next" data-carousel="videos" data-dir="1" aria-label="Suivant">›</button>
|
||||
<div class="atomic-carousel-track" id="video-track"></div>
|
||||
</section>
|
||||
|
||||
<!-- SPRINTS -->
|
||||
@@ -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();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user