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 la 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. 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
- 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 autrement aléatoires, en évitant le code répété et en partageant les 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 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. 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 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, 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, 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 plus tard.
.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 est d'accord avec 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é médiane 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 la liste (<li>
) contient le bouton et ses bordures. 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>
Attribuer un style aux 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'équilibrage des pseudo-éléments, des animations et des 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 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 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 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
. 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
,
initialement sur 1px
. 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 supérieurs et inférieurs montrent l'effet d'une 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.
Compatibilité avec les 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 avec le 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. 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 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 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 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 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. 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.