portfolio : animation horizontale — toggle panel + composant
All checks were successful
Deploy / Deploy to Production (push) Successful in 19s

Ajout du mode horizontal dans GalleryAnimation (5 rangées, scrollLeft/scrollRight)
avec toggle vertical/horizontal dans le panel projet.

refs #21

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-03-21 14:37:05 +01:00
parent 347ccd33eb
commit 133651c35d
5 changed files with 116 additions and 21 deletions

View file

@ -1,19 +1,32 @@
<script>
/**
* GalleryAnimation — animation CSS de galerie en 3 colonnes défilantes.
* GalleryAnimation — animation CSS de galerie défilante.
* Mode vertical : 3 colonnes (haut/bas)
* Mode horizontal : 5 rangées (gauche/droite)
* @prop {Array<{src: string, srcset: string, webp: string}>} images
* @prop {number} secondsPerImage — durée par image (défaut: 8s)
* @prop {'vertical'|'horizontal'} mode — direction du défilement
*/
let { images = [], secondsPerImage = 8, backgroundColor = null, backgroundImage = null } = $props()
let { images = [], secondsPerImage = 8, backgroundColor = null, backgroundImage = null, mode = 'vertical' } = $props()
const columns = $derived.by(() => {
const count = images.length
const duration = count * secondsPerImage
const defs = [
{ offset: 0, delay: 0 },
{ offset: Math.floor(count / 3), delay: duration / 4 },
{ offset: 0, delay: duration / 2 },
]
const defs = mode === 'horizontal'
? [
{ offset: 0, delay: 0 },
{ offset: Math.floor(count / 5), delay: duration / 5 },
{ offset: Math.floor(2 * count / 5), delay: 2 * duration / 5 },
{ offset: Math.floor(count / 5), delay: 3 * duration / 5 },
{ offset: 0, delay: 4 * duration / 5 },
]
: [
{ offset: 0, delay: 0 },
{ offset: Math.floor(count / 3), delay: duration / 4 },
{ offset: 0, delay: duration / 2 },
]
return defs.map(({ offset, delay }) => ({
images: shiftImages(images, offset),
delay,
@ -28,7 +41,7 @@
</script>
<div
class="gallery-animation gallery-animation--vertical"
class="gallery-animation gallery-animation--{mode}"
style="--gallery-duration: {columns[0]?.duration ?? 24}s{backgroundColor ? `; --background-color: ${backgroundColor}` : ''}{backgroundImage ? `; --gallery-background-image: url(${backgroundImage})` : ''}"
>
<div class="gallery-animation__overlay"></div>
@ -72,7 +85,22 @@
opacity: .8;
}
/* Vertical mode (Portfolio) */
.gallery-animation__overlay {
position: absolute;
inset: 0;
background-color: #000;
opacity: .5;
}
:global(.gallery-animation__image) {
display: block;
object-fit: contain;
}
/* ==========================================================================
MODE VERTICAL — 3 colonnes
========================================================================== */
.gallery-animation--vertical {
display: flex;
flex-direction: row;
@ -105,18 +133,9 @@
animation-duration: var(--gallery-duration);
}
.gallery-animation__overlay {
position: absolute;
inset: 0;
background-color: #000;
opacity: .5;
}
:global(.gallery-animation__image) {
.gallery-animation--vertical :global(.gallery-animation__image) {
width: 100%;
height: auto;
display: block;
object-fit: contain;
}
@keyframes galleryScrollDown {
@ -129,6 +148,65 @@
to { transform: translateY(0); }
}
/* ==========================================================================
MODE HORIZONTAL — 5 rangées
========================================================================== */
.gallery-animation--horizontal {
display: flex;
flex-direction: column;
transform: scale(1.2);
}
.gallery-animation--horizontal :global(.gallery-animation__column) {
--inner-height: 100vh;
height: clamp(calc(var(--inner-height) / 5), 20%, calc(var(--inner-height) / 3));
display: flex;
flex-direction: row;
align-items: stretch;
overflow: hidden;
}
.gallery-animation--horizontal :global(.gallery-animation__track) {
display: flex;
flex-direction: row;
align-items: stretch;
height: 100%;
column-gap: 1rem;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.gallery-animation--horizontal :global(.gallery-animation__column:nth-child(odd) .gallery-animation__track) {
animation-name: galleryScrollRight;
animation-duration: var(--gallery-duration);
}
.gallery-animation--horizontal :global(.gallery-animation__column:nth-child(even) .gallery-animation__track) {
animation-name: galleryScrollLeft;
animation-duration: var(--gallery-duration);
}
.gallery-animation--horizontal :global(.gallery-animation__image) {
height: 100%;
width: auto;
flex-shrink: 0;
}
@keyframes galleryScrollRight {
from { transform: translateX(-50%); }
to { transform: translateX(0); }
}
@keyframes galleryScrollLeft {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
/* ==========================================================================
REDUCED MOTION
========================================================================== */
@media (prefers-reduced-motion: reduce) {
:global(.gallery-animation__track) {
animation: none;

View file

@ -80,7 +80,7 @@
</script>
<div class="portfolio-gallery mobile-only" aria-hidden="true">
<GalleryAnimation images={currentProject.imagesGallery} backgroundColor={currentProject.galleryBackgroundColor} backgroundImage={currentProject.galleryBackgroundImage} />
<GalleryAnimation images={currentProject.imagesGallery} backgroundColor={currentProject.galleryBackgroundColor} backgroundImage={currentProject.galleryBackgroundImage} mode={currentProject.galleryAnimationMode} />
</div>
<section
@ -99,7 +99,7 @@
{#if currentProject}
<!-- Galerie animation (gauche desktop / plein écran mobile) -->
<div class="portfolio-gallery desktop-only" aria-hidden="true">
<GalleryAnimation images={currentProject.imagesGallery} backgroundColor={currentProject.galleryBackgroundColor} backgroundImage={currentProject.galleryBackgroundImage} />
<GalleryAnimation images={currentProject.imagesGallery} backgroundColor={currentProject.galleryBackgroundColor} backgroundImage={currentProject.galleryBackgroundImage} mode={currentProject.galleryAnimationMode} />
</div>
<!-- Mockup device (centre) -->