Découvrez comment créer un menu de jeu 3D responsif, adaptatif et accessible.
Dans ce post, je vais vous expliquer comment créer un composant de menu de jeu en 3D. Essayez la démonstration.
Si vous préférez la vidéo, voici une version YouTube de cet article:
Présentation
Les jeux vidéo présentent souvent un menu créatif et inhabituel, animé et dans un espace 3D. Il est populaire dans les nouveaux jeux de RA/RV pour que le menu semble flotter dans l'espace. Aujourd'hui, nous allons recréer les éléments essentiels de cet effet, mais en y ajoutant un jeu de couleurs adaptatifs et des aménagements pour les utilisateurs qui préfèrent les mouvements réduits.
HTML
Un menu de jeu est une liste de boutons. La meilleure façon de représenter cela en HTML est la suivante:
<ul class="threeD-button-set">
<li><button>New Game</button></li>
<li><button>Continue</button></li>
<li><button>Online</button></li>
<li><button>Settings</button></li>
<li><button>Quit</button></li>
</ul>
Une liste de boutons s'annonce bien avec les technologies de lecteur d'écran et fonctionne sans JavaScript ni CSS.
CSS
Le style de la liste de boutons se décompose en grandes étapes suivantes:
- Configurer des propriétés personnalisées
- Une mise en page Flexbox.
- Bouton personnalisé avec des pseudo-éléments décoratifs.
- Placer des éléments dans un espace 3D
Présentation des propriétés personnalisées
Les propriétés personnalisées aident à lever toute ambiguïté en attribuant des noms significatifs à des valeurs qui semblent aléatoires, évitant ainsi le code répété et le partage de valeurs entre les enfants.
Vous trouverez ci-dessous les requêtes média enregistrées en tant que variables CSS (également appelées médias personnalisés). Ils sont globaux et seront utilisés dans différents sélecteurs pour que le code reste concis et lisible. Le composant de menu du jeu utilise les préférences de mouvement, le schéma de couleurs du système et les fonctionnalités de plage de couleurs de l'écran.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);
Les propriétés personnalisées suivantes gèrent le jeu de couleurs et maintiennent les valeurs de position de la souris pour rendre le menu du jeu interactif. L'attribution d'un nom aux propriétés personnalisées améliore la lisibilité du code, car elle révèle le cas d'utilisation de la valeur ou un nom convivial pour le résultat de la valeur.
.threeD-button-set {
--y:;
--x:;
--distance: 1px;
--theme: hsl(180 100% 50%);
--theme-bg: hsl(180 100% 50% / 25%);
--theme-bg-hover: hsl(180 100% 50% / 40%);
--theme-text: white;
--theme-shadow: hsl(180 100% 10% / 25%);
--_max-rotateY: 10deg;
--_max-rotateX: 15deg;
--_btn-bg: var(--theme-bg);
--_btn-bg-hover: var(--theme-bg-hover);
--_btn-text: var(--theme-text);
--_btn-text-shadow: var(--theme-shadow);
--_bounce-ease: cubic-bezier(.5, 1.75, .75, 1.25);
@media (--dark) {
--theme: hsl(255 53% 50%);
--theme-bg: hsl(255 53% 71% / 25%);
--theme-bg-hover: hsl(255 53% 50% / 40%);
--theme-shadow: hsl(255 53% 10% / 25%);
}
@media (--HDcolor) {
@supports (color: color(display-p3 0 0 0)) {
--theme: color(display-p3 .4 0 .9);
}
}
}
Arrière-plans coniques avec thème clair et sombre
Le thème clair présente un dégradé conique de cyan
à deeppink
, tandis que le thème sombre présente un dégradé conique subtil et sombre. Pour en savoir plus sur les possibilités offertes par les dégradés coniques, consultez la page conic.style.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
Activer la perspective 3D
Pour que les éléments existent dans l'espace 3D d'une page Web, vous devez initialiser une fenêtre d'affichage avec une perspective. J'ai choisi de mettre la perspective sur l'élément body
et utilisé les unités de fenêtre d'affichage pour créer le style qui me plaisait.
body {
perspective: 40vw;
}
C'est le type d'impact que la perspective peut avoir.
Appliquer un style à la liste de boutons <ul>
Cet élément est responsable de la mise en page globale de la macro de la liste de boutons, ainsi que d'une carte flottante interactive et 3D. Voici un moyen d'y parvenir.
Disposition du groupe de boutons
Flexbox peut gérer la mise en page du conteneur. Changez la direction par défaut de l'environnement flexible pour passer des lignes aux colonnes avec flex-direction
et assurez-vous que chaque élément correspond à la taille de son contenu en passant de stretch
à start
pour align-items
.
.threeD-button-set {
/* remove <ul> margins */
margin: 0;
/* vertical rag-right layout */
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2.5vh;
}
Ensuite, établissez le conteneur en tant que contexte d'espace 3D et configurez les fonctions CSS clamp()
pour vous assurer que la rotation de la carte n'excède pas la lisibilité. Notez que la valeur centrale de la limitation est une propriété personnalisée. Ces valeurs --x
et --y
seront définies à partir de JavaScript lors de l'interaction ultérieure avec la souris.
.threeD-button-set {
…
/* create 3D space context */
transform-style: preserve-3d;
/* clamped menu rotation to not be too extreme */
transform:
rotateY(
clamp(
calc(var(--_max-rotateY) * -1),
var(--y),
var(--_max-rotateY)
)
)
rotateX(
clamp(
calc(var(--_max-rotateX) * -1),
var(--x),
var(--_max-rotateX)
)
)
;
}
Ensuite, si le mouvement est autorisé par l'utilisateur visiteur, ajoutez un indice au navigateur pour indiquer au navigateur que la transformation de cet élément va constamment changer avec will-change
.
Vous pouvez également activer l'interpolation en définissant un transition
pour les transformations. Cette transition se produit lorsque la souris interagit avec la fiche, ce qui permet des transitions fluides vers les changements de rotation. Il s'agit d'une animation en continu qui montre l'espace 3D dans lequel se trouve la carte, même si une souris ne peut pas ou ne peut pas interagir avec le composant.
@media (--motionOK) {
.threeD-button-set {
/* browser hint so it can be prepared and optimized */
will-change: transform;
/* transition transform style changes and run an infinite animation */
transition: transform .1s ease;
animation: rotate-y 5s ease-in-out infinite;
}
}
L'animation rotate-y
ne définit l'image clé du milieu que sur 50%
, car le navigateur utilise par défaut 0%
et 100%
sur le style par défaut de l'élément. Il s'agit d'un raccourci pour les animations alternées, qui doivent commencer et se terminer à la même position. C'est un excellent moyen d'articuler des animations alternées infinies.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
Appliquer un style aux éléments <li>
Chaque élément de la liste (<li>
) contient le bouton et ses bordures. Le style display
est modifié afin que l'élément n'affiche pas de ::marker
. Le style position
est défini sur relative
afin que les pseudo-éléments du bouton "à venir" puissent se positionner dans toute la zone utilisée par le bouton.
.threeD-button-set > li {
/* change display type from list-item */
display: inline-flex;
/* create context for button pseudos */
position: relative;
/* create 3D space context */
transform-style: preserve-3d;
}
Appliquer un style aux éléments <button>
Appliquer un style aux boutons peut s'avérer difficile. De nombreux états et types d'interactions doivent être pris en compte. Ces boutons deviennent rapidement complexes en raison de l'équilibrage des pseudo-éléments, des animations et des interactions.
Styles <button>
initiaux
Vous trouverez ci-dessous les styles de base compatibles avec les autres états.
.threeD-button-set button {
/* strip out default button styles */
appearance: none;
outline: none;
border: none;
/* bring in brand styles via props */
background-color: var(--_btn-bg);
color: var(--_btn-text);
text-shadow: 0 1px 1px var(--_btn-text-shadow);
/* large text rounded corner and padded*/
font-size: 5vmin;
font-family: Audiowide;
padding-block: .75ch;
padding-inline: 2ch;
border-radius: 5px 20px;
}
Pseudo-éléments de bouton
Les bordures du bouton ne sont pas des bordures traditionnelles, mais des pseudo-éléments de position absolue avec des bordures.
Ces éléments sont essentiels pour montrer la perspective 3D établie. L'un de ces pseudo-éléments sera retiré du bouton, et l'un d'entre eux sera rapproché de l'utilisateur. L'effet est particulièrement visible au niveau des boutons supérieur et inférieur.
.threeD-button button {
…
&::after,
&::before {
/* create empty element */
content: '';
opacity: .8;
/* cover the parent (button) */
position: absolute;
inset: 0;
/* style the element for border accents */
border: 1px solid var(--theme);
border-radius: 5px 20px;
}
/* exceptions for one of the pseudo elements */
/* this will be pushed back (3x) and have a thicker border */
&::before {
border-width: 3px;
/* in dark mode, it glows! */
@media (--dark) {
box-shadow:
0 0 25px var(--theme),
inset 0 0 25px var(--theme);
}
}
}
Styles de transformation 3D
Sous transform-style
, la valeur est définie sur preserve-3d
afin que les enfants puissent s'espacer sur l'axe z
. transform
est défini sur la propriété personnalisée --distance
, qui augmente lorsque vous passez la souris et sélectionnez.
.threeD-button-set button {
…
transform: translateZ(var(--distance));
transform-style: preserve-3d;
&::after {
/* pull forward in Z space with a 3x multiplier */
transform: translateZ(calc(var(--distance) / 3));
}
&::before {
/* push back in Z space with a 3x multiplier */
transform: translateZ(calc(var(--distance) / 3 * -1));
}
}
Styles d'animation conditionnelle
Si l'utilisateur approuve les mouvements, le bouton indique au navigateur que la propriété de transformation doit être prête à être modifiée et qu'une transition est définie pour les propriétés transform
et background-color
. Remarquez la différence de durée.
J'ai senti qu'il s'agissait d'un joli effet décalé et subtil.
.threeD-button-set button {
…
@media (--motionOK) {
will-change: transform;
transition:
transform .2s ease,
background-color .5s ease
;
&::before,
&::after {
transition: transform .1s ease-out;
}
&::after { transition-duration: .5s }
&::before { transition-duration: .3s }
}
}
Styles d'interaction avec la souris et le curseur
L'objectif de l'animation d'interaction est de répartir les couches qui composent le bouton d'affichage plat. Pour ce faire, définissez la variable --distance
,
initialement sur 1px
. Le sélecteur présenté dans l'exemple de code suivant vérifie si le bouton est survolé ou mis en surbrillance par un appareil qui devrait voir un indicateur de mise au point, et s'il n'est pas activé. Si c'est le cas, elle applique le CSS pour effectuer les opérations suivantes:
- Appliquez la couleur d'arrière-plan de l'élément de pointage.
- Augmentez la distance .
- Ajoutez un effet de lissage de vitesse.
- Échelonnez les transitions de pseudo-éléments.
.threeD-button-set button {
…
&:is(:hover, :focus-visible):not(:active) {
/* subtle distance plus bg color change on hover/focus */
--distance: 15px;
background-color: var(--_btn-bg-hover);
/* if motion is OK, setup transitions and increase distance */
@media (--motionOK) {
--distance: 3vmax;
transition-timing-function: var(--_bounce-ease);
transition-duration: .4s;
&::after { transition-duration: .5s }
&::before { transition-duration: .3s }
}
}
}
La perspective 3D était tout de même intéressante pour la préférence de mouvement reduced
.
Les éléments supérieurs et inférieurs montrent l'effet d'une manière subtile.
Petites améliorations avec JavaScript
L'interface est déjà utilisable à partir d'un clavier, d'un lecteur d'écran, d'une manette de jeu, d'un écran tactile ou d'une souris, mais nous pouvons ajouter quelques touches de JavaScript pour faciliter certains scénarios.
Compatibilité avec les touches fléchées
La touche Tab permet de naviguer facilement dans le menu, mais je m'attendais à ce que le pavé directionnel ou les joysticks déplacent le curseur sur une manette de jeu. La bibliothèque roving-ux, souvent utilisée pour les interfaces GUI Challenge, gère pour nous les touches fléchées. Le code ci-dessous indique à la bibliothèque de piéger le curseur dans .threeD-button-set
et de le transférer aux enfants du bouton.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interaction avec le parallaxe de la souris
Le suivi de la souris et l'inclinaison du menu sont destinés à imiter des interfaces de jeux vidéo de RA et de RV, où un pointeur virtuel peut se trouver au lieu d'une souris. Cela peut être amusant lorsque les éléments sont très conscients du pointeur.
Comme il s'agit d'une petite fonctionnalité supplémentaire, nous allons placer l'interaction derrière une requête sur les préférences de mouvement de l'utilisateur. Lors de la configuration, stockez également le composant de la liste de boutons en mémoire avec querySelector
et mettez en cache les limites de l'élément dans menuRect
. Utilisez ces limites pour déterminer le décalage de rotation appliqué à la carte en fonction de la position de la souris.
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Nous avons ensuite besoin d'une fonction qui accepte les positions x
et y
de la souris et qui renvoie une valeur que nous pouvons utiliser pour faire pivoter la carte. La fonction suivante utilise la position de la souris pour déterminer de quel côté de la boîte il se trouve et dans quelle mesure. Le delta est renvoyé par la fonction.
const getAngles = (clientX, clientY) => {
const { x, y, width, height } = menuRect
const dx = clientX - (x + 0.5 * width)
const dy = clientY - (y + 0.5 * height)
return {dx,dy}
}
Enfin, observez le déplacement de la souris, transmettez la position à notre fonction getAngles()
et utilisez les valeurs delta comme styles de propriétés personnalisés. j'ai divisé par 20 pour remplir le delta
et le rendre moins agité, il existe peut-être une meilleure façon de le faire. Si vous vous souvenez du début, nous plaçons les accessoires --x
et --y
au milieu d'une fonction clamp()
, ce qui évite que la position de la souris ne fasse trop pivoter la carte dans une position illisible.
if (motionOK) {
window.addEventListener('mousemove', ({target, clientX, clientY}) => {
const {dx,dy} = getAngles(clientX, clientY)
menu.attributeStyleMap.set('--x', `${dy / 20}deg`)
menu.attributeStyleMap.set('--y', `${dx / 20}deg`)
})
}
Traductions et instructions
Nous avons rencontré un problème en testant le menu du jeu dans d'autres modes et langages d'écriture.
Les éléments <button>
ont un style !important
pour writing-mode
dans la feuille de style du user-agent. Il fallait donc modifier le code HTML du menu du jeu pour s'adapter à la conception souhaitée. Convertir la liste de boutons en liste de liens permet aux propriétés logiques de changer l'orientation du menu, car les éléments <a>
n'ont pas de style !important
fourni par le navigateur.
Conclusion
Maintenant que vous savez comment j'ai fait, comment feriez-vous ? 😃 Pouvez-vous ajouter une interaction avec l'accéléromètre au menu, de sorte que l'emplacement de votre téléphone fasse pivoter le menu ? Pouvons-nous améliorer l’expérience d’absence de mouvement ?
Diversifiez nos approches et découvrons toutes les manières de créer des applications sur le Web. Créez une démo, envoyez-moi des tweets via des liens et je l'ajouterai à la section "Remix de la communauté" ci-dessous.
Remix de la communauté
Rien à afficher pour le moment !