Feat: transitions Play soignées — exit/enter directionnel + bg crossfade + game-preview
All checks were successful
Deploy / Deploy to Production (push) Successful in 17s

- Phase exiting (300ms) : featured + game-preview glissent/fondent dans slideDir
- Phase entering (350ms) : nouvel contenu entre depuis la direction opposée
- Swap currentIndex à 300ms : carousel grossit + bg crossfade simultanés
- Ajout game-preview (image preview côté droit, grid-area 3/13)
- Ajout preview dans le template JSON PHP
- Masqué sur mobile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-03-09 19:24:41 +01:00
parent bbab752fd6
commit 600ce937a3
2 changed files with 133 additions and 59 deletions

View file

@ -9,6 +9,7 @@ $specificData = [
'description' => $game->description()->value(), 'description' => $game->description()->value(),
'thumbnail' => $game->thumbnail()->toFile()?->url(), 'thumbnail' => $game->thumbnail()->toFile()?->url(),
'backgroundColor' => $game->backgroundColor()->value() ?: null, 'backgroundColor' => $game->backgroundColor()->value() ?: null,
'preview' => $game->preview()->toFile()?->url(),
'playLink' => $game->playLink()->value() ?: null, 'playLink' => $game->playLink()->value() ?: null,
]; ];
})->values() })->values()

View file

@ -9,38 +9,46 @@
const games = $derived(data?.games ?? []) const games = $derived(data?.games ?? [])
// --- State --- // --- State ---
let currentIndex = $state(0) // index de la sélection (mis à jour immédiatement) let currentIndex = $state(0) // index actif (carousel + bg)
let displayedIndex = $state(0) // index affiché (mis à jour au milieu de la transition) let displayedIndex = $state(0) // index du contenu affiché
let isOut = $state(false) let phase = $state('idle') // 'idle' | 'exiting' | 'entering'
let isTransitioning = $state(false) let slideDir = $state(1) // 1 = click droite (exit gauche), -1 = click gauche (exit droite)
let slideDir = $state(1) // -1 : vers la droite, 1 : vers la gauche
// Bg crossfade
let prevBgColor = $state(null)
let bgFading = $state(false)
const displayedGame = $derived(games[displayedIndex] ?? null) const displayedGame = $derived(games[displayedIndex] ?? null)
let t1 = null let t1 = null
let t2 = null let t2 = null
let t3 = null
function selectGame(i) { function selectGame(i) {
if (i === currentIndex || isTransitioning || !games.length) return if (i === currentIndex || phase !== 'idle' || !games.length) return
slideDir = i > currentIndex ? -1 : 1 // slideDir = 1 → clic à droite → content sort par la gauche, entre par la droite
currentIndex = i slideDir = i > currentIndex ? 1 : -1
isOut = true phase = 'exiting'
isTransitioning = true
clearTimeout(t1) clearTimeout(t1)
clearTimeout(t2) clearTimeout(t2)
clearTimeout(t3)
// Milieu : swap du contenu affiché et du fond (éléments invisibles à ce moment) // Phase 2 (300ms) : swap contenu + carousel + bg simultanés
t1 = setTimeout(() => { t1 = setTimeout(() => {
prevBgColor = displayedGame?.backgroundColor ?? null
displayedIndex = i displayedIndex = i
isOut = false currentIndex = i
}, 300) phase = 'entering'
bgFading = true
// Fin de transition // Fin du bg fade
t2 = setTimeout(() => { t3 = setTimeout(() => { bgFading = false }, 500)
isTransitioning = false
}, 600) // Fin de l'entrée
t2 = setTimeout(() => { phase = 'idle' }, 350)
}, 300)
} }
// Réinitialisation quand on quitte la slide // Réinitialisation quand on quitte la slide
@ -48,16 +56,18 @@
if (!isActive) { if (!isActive) {
clearTimeout(t1) clearTimeout(t1)
clearTimeout(t2) clearTimeout(t2)
currentIndex = 0 clearTimeout(t3)
displayedIndex = 0 currentIndex = 0
isOut = false displayedIndex = 0
isTransitioning = false phase = 'idle'
bgFading = false
} }
}) })
onMount(() => () => { onMount(() => () => {
clearTimeout(t1) clearTimeout(t1)
clearTimeout(t2) clearTimeout(t2)
clearTimeout(t3)
}) })
</script> </script>
@ -67,9 +77,13 @@
style={displayedGame?.backgroundColor ? `--background-color: ${displayedGame.backgroundColor}` : ''} style={displayedGame?.backgroundColor ? `--background-color: ${displayedGame.backgroundColor}` : ''}
> >
<!-- Fond : image + overlay, crossfade au changement de jeu --> <!-- Ancien fond en fondu sortant (crossfade bg) -->
<div class="play-bg" class:is-out={isOut} aria-hidden="true"> {#if bgFading && prevBgColor}
</div> <div class="play-bg-fade" style="background: {prevBgColor}" aria-hidden="true"></div>
{/if}
<!-- Fond SVG + overlay -->
<div class="play-bg" aria-hidden="true"></div>
<!-- Lignes courbes décoratives (SVG) --> <!-- Lignes courbes décoratives (SVG) -->
<div class="play-curves" aria-hidden="true"> <div class="play-curves" aria-hidden="true">
@ -122,11 +136,24 @@
</svg> </svg>
</div> </div>
<!-- Preview image (côté droit, décoratif) -->
{#if displayedGame?.preview}
<div
class="game-preview"
class:exiting={phase === 'exiting'}
class:entering={phase === 'entering'}
style="--slide-dir: {slideDir}"
aria-hidden="true"
>
<img src={displayedGame.preview} alt="" />
</div>
{/if}
<!-- Section principale : lettering + description + boutons de jeu --> <!-- Section principale : lettering + description + bouton -->
<div <div
class="play-featured" class="play-featured"
class:is-out={isOut} class:exiting={phase === 'exiting'}
class:entering={phase === 'entering'}
style="--slide-dir: {slideDir}" style="--slide-dir: {slideDir}"
aria-live="polite" aria-live="polite"
> >
@ -185,7 +212,20 @@
background: var(--background-color); background: var(--background-color);
} }
/* --- Background --- */ /* --- Bg crossfade --- */
.play-bg-fade {
grid-area: 1/1 / span 20 / span 20;
pointer-events: none;
z-index: 0;
animation: bg-fade-out 0.5s ease forwards;
}
@keyframes bg-fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
/* --- Background SVG --- */
.play-bg { .play-bg {
background-image: url(/static/media/background-play.f8db5aa42e2983197126.svg); background-image: url(/static/media/background-play.f8db5aa42e2983197126.svg);
background-position: 50%; background-position: 50%;
@ -198,18 +238,6 @@
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
}
.play-bg.is-out {
opacity: 0;
}
.play-bg-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
} }
/* --- Lignes courbes SVG --- */ /* --- Lignes courbes SVG --- */
@ -225,33 +253,57 @@
height: 100%; height: 100%;
} }
/* --- Animations partagées featured + preview --- */
/* slideDir = 1 (clic droite) → exit vers la gauche, entrée depuis la droite */
/* slideDir = -1 (clic gauche) → exit vers la droite, entrée depuis la gauche */
@keyframes content-exit {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(calc(var(--slide-dir) * -40px)); }
}
@keyframes content-enter {
from { opacity: 0; transform: translateX(calc(var(--slide-dir) * 40px)); }
to { opacity: 1; transform: translateX(0); }
}
.play-featured.exiting,
.game-preview.exiting {
animation: content-exit 0.3s ease forwards;
}
.play-featured.entering,
.game-preview.entering {
animation: content-enter 0.35s ease forwards;
}
/* --- Section principale --- */ /* --- Section principale --- */
.play-featured { .play-featured {
grid-area: 4/4 / span 12 / span 10; grid-area: 2/4 / span 12 / span 10;
z-index: var(--z-content); z-index: var(--z-content);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
gap: 1.5rem; gap: 1.5rem;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.play-featured.is-out {
opacity: 0;
transform: translateX(calc(var(--slide-dir) * 40px));
} }
.play-lettering { .play-lettering {
width: clamp(180px,18.77vw,362px); width: clamp(180px, 18.77vw, 362px);
object-fit: contain; object-fit: contain;
object-position: left center; object-position: left center;
} }
.play-description { .play-description {
font-size: var(--font-size-paragraph); font-family: Danzza, sans-serif;
color: var(--color-text); font-size: var(--font-size-subtitle);
line-height: 1.5; font-weight: 400;
max-width: 42ch; line-height: 1.6;
margin: 0 0 20px;
max-width: 600px;
opacity: .9;
text-shadow: 0 1px 15px rgba(0,0,0,.6);
text-align: left;
} }
.play-actions { .play-actions {
@ -260,6 +312,23 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
/* --- Preview image (droite) --- */
.game-preview {
grid-area: 3/13 / span 14 / span 7;
z-index: var(--z-content);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.game-preview img {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
}
/* --- Carrousel --- */ /* --- Carrousel --- */
.play-carousel { .play-carousel {
grid-area: 16/4 / span 3 / span 14; grid-area: 16/4 / span 3 / span 14;
@ -278,19 +347,17 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: clamp(140px,15.09vw,291px); width: clamp(140px, 15.09vw, 291px);
border-radius: 12px;
border: 2px solid transparent;
gap: 0.5rem; gap: 0.5rem;
background: none; background: none;
border: none;
cursor: pointer; cursor: pointer;
opacity: 0.5; opacity: 0.5;
transition: opacity 0.4s var(--ease-standard), transform 0.4s var(--ease-standard); transition: all 0.4s var(--ease-standard);
} }
.play-carousel-item.active button { .play-carousel-item.active button {
opacity: 1; opacity: 1;
width: clamp(170px,18.41vw,355px); width: clamp(170px, 18.41vw, 355px);
} }
.play-carousel-item button img { .play-carousel-item button img {
@ -306,11 +373,15 @@
.play-carousel-item.active button img { .play-carousel-item.active button img {
border: 4px solid var(--color-primary); border: 4px solid var(--color-primary);
border-radius: 4.9375rem; border-radius: 25%;
} }
/* --- Mobile (≤ 700px) --- */ /* --- Mobile (≤ 700px) --- */
@media screen and (max-width: 700px) { @media screen and (max-width: 700px) {
.game-preview {
display: none;
}
.play-featured { .play-featured {
grid-area: 3/2 / span 12 / span 18; grid-area: 3/2 / span 12 / span 18;
} }
@ -323,8 +394,10 @@
/* Reduced motion */ /* Reduced motion */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.play-featured, .play-featured,
.play-bg, .game-preview,
.play-bg-fade,
.play-carousel-item button { .play-carousel-item button {
animation: none;
transition: none; transition: none;
} }
} }