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(),
'thumbnail' => $game->thumbnail()->toFile()?->url(),
'backgroundColor' => $game->backgroundColor()->value() ?: null,
'preview' => $game->preview()->toFile()?->url(),
'playLink' => $game->playLink()->value() ?: null,
];
})->values()

View file

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