Présentation de base sur la création d'un menu de jeu 3D responsif, adaptatif et accessible.
Dans cet article, je vais vous expliquer comment créer un composant de menu de jeu 3D. Essayez la démo.
Si vous préférez regarder une vidéo, voici une version YouTube de cet article:
Présentation
Les jeux vidéo présentent souvent aux utilisateurs un menu créatif et inhabituel, animé et en espace 3D. Dans les nouveaux jeux de RA/RV, il est courant de faire en sorte que le menu semble flotter dans l'espace. Aujourd'hui, nous allons recréer l'essentiel de cet effet, mais avec l'ajout d'un jeu de couleurs adaptatif et d'options pour les utilisateurs qui préfèrent réduire le mouvement.
HTML
Un menu de jeu est une liste de boutons. Le meilleur moyen de représenter cela en HTML est le suivant:
<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 aux technologies de lecteur d'écran et fonctionne sans JavaScript ni CSS.
CSS
Pour styliser la liste de boutons, procédez comme suit:
- Configurer des propriétés personnalisées
- Mise en page Flexbox.
- Bouton personnalisé avec des pseudo-éléments décoratifs
- Placer des éléments dans l'espace 3D
Présentation des propriétés personnalisées
Les propriétés personnalisées permettent de clarifier les valeurs en attribuant des noms significatifs à des valeurs qui semblent aléatoires, ce qui évite de répéter du code et de partager des valeurs entre les enfants.
Vous trouverez ci-dessous des requêtes multimédias enregistrées en tant que variables CSS, également appelées multimédias personnalisés. Ils sont globaux et seront utilisés dans divers sélecteurs pour que le code reste concis et lisible. Le composant de menu du jeu utilise les préférences de mouvement, le jeu de couleurs du système et les capacité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 conservent les valeurs de position de la souris pour rendre le menu du jeu interactif en cas de survol. Nommer des propriétés personnalisées facilite la lisibilité du code, car cela 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 pour les thèmes clair et sombre
Le thème clair présente un dégradé conique allant de cyan
à deeppink
, tandis que le thème sombre présente un dégradé conique subtil et sombre. Pour en savoir plus sur ce que vous pouvez faire avec les dégradés coniques, consultez 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 des éléments existent dans l'espace 3D d'une page Web, une fenêtre d'affichage avec une perspective doit être initialisée. J'ai choisi de placer la perspective sur l'élément body
et j'ai utilisé des unités de vue 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 liste de boutons, et constitue une fiche flottante interactive et en 3D. Voici comment procéder.
Mise en page du groupe de boutons
Flexbox peut gérer la mise en page du conteneur. Modifiez l'orientation par défaut de flex de lignes en colonnes avec flex-direction
et assurez-vous que chaque élément a la taille de son contenu en remplaçant stretch
par 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, définissez le conteneur comme contexte d'espace 3D et configurez les fonctions CSS clamp()
pour vous assurer que la fiche ne pivote pas au-delà des rotations lisibles. Notez que la valeur médiane de la pince est une propriété personnalisée. Ces valeurs --x
et --y
seront définies à partir de JavaScript lors de l'interaction avec la souris ultérieurement.
.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 l'utilisateur accepte le mouvement, ajoutez un indice au navigateur indiquant que la transformation de cet élément changera constamment avec will-change
.
Activez également l'interpolation en définissant un transition
sur les transformations. Cette transition se produit lorsque la souris interagit avec la fiche, ce qui permet des transitions fluides vers les changements de rotation. L'animation est une animation en cours d'exécution constante qui illustre l'espace 3D dans lequel se trouve la fiche, même si une souris ne peut pas interagir avec le composant ou ne l'interagit pas.
@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 que l'image clé du milieu sur 50%
, car le navigateur définira 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 qui s'alternent et doivent commencer et se terminer à la même position. C'est un excellent moyen d'articuler des animations infinies en alternance.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
Appliquer un style aux éléments <li>
Chaque élément de liste (<li>
) contient le bouton et ses éléments de bordure. Le style display
est modifié pour que l'élément n'affiche pas de ::marker
. Le style position
est défini sur relative
afin que les pseudo-éléments de bouton à venir puissent se positionner dans la zone complète que le bouton occupe.
.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>
Modifier le style des boutons peut être difficile, car de nombreux états et types d'interactions doivent être pris en compte. Ces boutons deviennent rapidement complexes en raison de l'équilibre entre les pseudo-éléments, les animations et les interactions.
Styles <button>
initiaux
Vous trouverez ci-dessous les styles de base qui seront 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 positionnés de manière absolue avec des bordures.
Ces éléments sont essentiels pour mettre en valeur la perspective 3D établie. L'un de ces pseudo-éléments sera éloigné du bouton, et l'autre sera rapproché de l'utilisateur. L'effet est le plus visible sur les boutons du haut et du bas.
.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 sera augmentée lors du pointage et de la sélection.
.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 conditionnels
Si l'utilisateur accepte le mouvement, 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
. Notez la différence de durée. J'ai trouvé que cela créait un effet décalé 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 le survol et la mise au point
L'objectif de l'animation d'interaction est de répartir les calques qui constituaient le bouton à l'apparence plate. Pour ce faire, définissez la variable --distance
sur 1px
au départ. Le sélecteur présenté dans l'exemple de code suivant vérifie si le bouton est pointé ou sélectionné par un appareil qui doit afficher un indicateur de sélection, et non s'il est activé. Le cas échéant, il applique le CSS pour effectuer les opérations suivantes:
- Appliquez la couleur d'arrière-plan du survol.
- Augmentez la distance .
- Ajoutez un effet de rebond.
- Échelonnez les transitions des 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 toujours très intéressante pour la préférence de mouvement reduced
.
Les éléments du haut et du bas montrent l'effet de manière subtile.
Améliorations mineures avec JavaScript
L'interface est déjà utilisable avec des claviers, des lecteurs d'écran, des manettes de jeu, un écran tactile et une souris, mais nous pouvons ajouter quelques touches de JavaScript pour faciliter certains scénarios.
Prise en charge des touches fléchées
La touche de tabulation est un excellent moyen de naviguer dans le menu, mais je m'attends à ce que le pavé directionnel ou les joysticks déplacent la sélection sur une manette de jeu. La bibliothèque roving-ux, souvent utilisée pour les interfaces de défi GUI, gérera les touches fléchées à notre place. Le code ci-dessous indique à la bibliothèque de capturer le focus dans .threeD-button-set
et de le transmettre aux enfants du bouton.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interaction par parallaxe de la souris
Le suivi de la souris et son inclinaison du menu visent à imiter les interfaces de jeux vidéo en RA et VR, où vous pouvez avoir un pointeur virtuel au lieu d'une souris. Il peut être amusant que les éléments soient très sensibles au pointeur.
Étant donné qu'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. De plus, lors de la configuration, stockez le composant de liste de boutons dans la 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 fiche 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)'
)
Ensuite, nous avons besoin d'une fonction qui accepte les positions x
et y
de la souris et renvoie une valeur que nous pouvons utiliser pour faire pivoter la fiche. La fonction suivante utilise la position de la souris pour déterminer de quel côté de la boîte elle se trouve et dans quelle mesure. La delta est renvoyée à partir de 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 mouvement de la souris, transmettez la position à notre fonction getAngles()
et utilisez les valeurs delta comme styles de propriété personnalisés. J'ai divisé par 20 pour rembourrer le delta et le rendre moins nerveux. Il existe peut-être une meilleure façon de procéder. Si vous vous souvenez du début, nous avons placé les arguments --x
et --y
au milieu d'une fonction clamp()
. Cela empêche la position de la souris de trop faire pivoter la fiche 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 itinéraires
Un problème s'est produit lors du test du menu du jeu dans d'autres modes d'écriture et langues.
Les éléments <button>
ont un style !important
pour writing-mode
dans la feuille de style de l'agent utilisateur. Cela signifiait que le code HTML du menu du jeu devait être modifié pour s'adapter à la conception souhaitée. Si vous remplacez la liste de boutons par une liste de liens, les propriétés logiques peuvent modifier le sens du menu, car les éléments <a>
ne disposent pas d'un style !important
fourni par le navigateur.
Conclusion
Maintenant que vous savez comment j'ai fait, comment procéder ? 🙂 Pouvez-vous ajouter une interaction avec l'accéléromètre au menu afin que le menu pivote lorsque vous placez votre téléphone en mode paysage ? Pouvons-nous améliorer l'expérience sans mouvement ?
Diversifions nos approches et découvrons toutes les façons de créer sur le Web. Créez une démo, tweetez-moi des liens et je les ajouterai à la section "Remix de la communauté" ci-dessous.
Remix de la communauté
Aucun élément à afficher pour l'instant.