2026-03-05 17:13:50 +01:00
|
|
|
|
<script>
|
|
|
|
|
|
/**
|
|
|
|
|
|
* GalleryAnimation — animation CSS de galerie en 3 colonnes défilantes.
|
2026-03-06 16:41:50 +01:00
|
|
|
|
* @prop {Array<{src: string, srcset: string, webp: string}>} images
|
|
|
|
|
|
* @prop {number} secondsPerImage — durée par image (défaut: 8s)
|
2026-03-05 17:13:50 +01:00
|
|
|
|
*/
|
|
|
|
|
|
let { images = [], secondsPerImage = 8 } = $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 },
|
|
|
|
|
|
]
|
|
|
|
|
|
return defs.map(({ offset, delay }) => ({
|
|
|
|
|
|
images: shiftImages(images, offset),
|
|
|
|
|
|
delay,
|
|
|
|
|
|
duration,
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function shiftImages(imgs, offset) {
|
|
|
|
|
|
if (!offset) return imgs
|
|
|
|
|
|
return [...imgs.slice(offset), ...imgs.slice(0, offset)]
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="gallery-animation gallery-animation--vertical"
|
|
|
|
|
|
style="--gallery-duration: {columns[0]?.duration ?? 24}s"
|
|
|
|
|
|
>
|
|
|
|
|
|
{#each columns as col}
|
|
|
|
|
|
<div class="gallery-animation__column">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="gallery-animation__track"
|
|
|
|
|
|
style="animation-delay: -{col.delay}s"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- Images × 2 pour le défilement infini -->
|
2026-03-06 16:41:50 +01:00
|
|
|
|
{#each [col.images, col.images] as set}
|
|
|
|
|
|
{#each set as img}
|
|
|
|
|
|
<picture>
|
|
|
|
|
|
<source type="image/webp" srcset={img.webp} sizes="(max-width: 700px) 33vw, 15vw" />
|
|
|
|
|
|
<img
|
|
|
|
|
|
class="gallery-animation__image"
|
|
|
|
|
|
src={img.src}
|
|
|
|
|
|
srcset={img.srcset}
|
|
|
|
|
|
sizes="(max-width: 700px) 33vw, 15vw"
|
|
|
|
|
|
alt=""
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
loading="lazy"
|
|
|
|
|
|
decoding="async"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</picture>
|
|
|
|
|
|
{/each}
|
2026-03-05 17:13:50 +01:00
|
|
|
|
{/each}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
</div>
|
2026-03-06 18:11:31 +01:00
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.gallery-animation {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Vertical mode (Portfolio) */
|
|
|
|
|
|
.gallery-animation--vertical {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gallery-animation--vertical :global(.gallery-animation__column) {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gallery-animation--vertical :global(.gallery-animation__track) {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
animation-timing-function: linear;
|
|
|
|
|
|
animation-iteration-count: infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gallery-animation--vertical :global(.gallery-animation__column:nth-child(odd) .gallery-animation__track) {
|
|
|
|
|
|
animation-name: galleryScrollDown;
|
|
|
|
|
|
animation-duration: var(--gallery-duration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.gallery-animation--vertical :global(.gallery-animation__column:nth-child(even) .gallery-animation__track) {
|
|
|
|
|
|
animation-name: galleryScrollUp;
|
|
|
|
|
|
animation-duration: var(--gallery-duration);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:global(.gallery-animation__image) {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes galleryScrollDown {
|
|
|
|
|
|
from { transform: translateY(0); }
|
|
|
|
|
|
to { transform: translateY(-50%); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes galleryScrollUp {
|
|
|
|
|
|
from { transform: translateY(-50%); }
|
|
|
|
|
|
to { transform: translateY(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
:global(.gallery-animation__track) {
|
|
|
|
|
|
animation: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|