Feat: transitions Play soignées — exit/enter directionnel + bg crossfade + game-preview
All checks were successful
Deploy / Deploy to Production (push) Successful in 17s
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:
parent
bbab752fd6
commit
600ce937a3
2 changed files with 133 additions and 59 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue