Créer un composant de menu de jeu en 3D

Présentation des principes de base de la création d'un menu de jeu 3D réactif, adaptatif et accessible.

Dans cet article, je vais vous expliquer comment créer un composant de menu de jeu en 3D. Essayez la démonstration.

Démonstration

Si vous préférez la vidéo, voici une version YouTube de ce post:

Présentation

Les jeux vidéo présentent souvent aux utilisateurs un menu original et inhabituel, animé et en 3D. Il est courant de donner l'impression que le menu flotte dans l'espace dans les nouveaux jeux en RA/RV. Aujourd'hui, nous allons recréer les bases de cet effet, mais avec l'élégance supplémentaire d'un jeu de couleurs adaptatif et d'adaptations 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 auprès des technologies de lecteur d'écran et fonctionne sans JavaScript ni CSS.

une liste à puces très générique
avec des boutons standards comme éléments.

CSS

Le style de la liste des boutons se décompose comme suit:

  1. Configurer des propriétés personnalisées
  2. Une mise en page Flexbox.
  3. Bouton personnalisé avec des pseudo-éléments décoratifs
  4. Placer des éléments dans un espace 3D

Présentation des propriétés personnalisées

Les propriétés personnalisées aident à distinguer les valeurs en attribuant des noms significatifs aux valeurs qui semblent aléatoires, ce qui évite de devoir répéter du code et de partager des valeurs entre des 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 divers sélecteurs pour que le code reste concis et lisible. Le composant de menu de jeu utilise les préférences de mouvement, le schéma de couleurs système et les fonctionnalités de gamme 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 contiennent les valeurs de position de la souris pour rendre le menu du jeu interactif lorsque vous pointez sur l'écran. L'attribution de noms aux propriétés personnalisées aide 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 conique avec thème clair et sombre

Le thème clair présente un dégradé conique coloré de cyan à deeppink, tandis que le thème sombre présente un dégradé conique sombre et subtil. Pour en savoir plus sur ce qu'il est possible de 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);
  }
}
Démonstration d'un arrière-plan passant des préférences de couleurs claires aux couleurs sombres.

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 placer la perspective sur l'élément body et d'utiliser les unités de la fenêtre d'affichage pour créer le style qui m'a plu.

body {
  perspective: 40vw;
}

C'est le type d'impact que les perspectives peuvent avoir.

Appliquer un style à la liste du bouton <ul>

Cet élément est responsable de la mise en page globale des macros de la liste des 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 disposition des conteneurs. Changez l'orientation par défaut de l'environnement flexible en remplaçant les lignes par des colonnes avec flex-direction, et assurez-vous que chaque élément a 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 en tant que contexte d'espace 3D et configurez les fonctions clamp() CSS pour vous assurer que la carte ne pivote pas au-delà des rotations lisibles. 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 convient à l'utilisateur visiteur, indiquez au navigateur que la transformation de cet élément changera constamment avec will-change. De plus, activez l'interpolation en définissant un transition sur les transformations. Cette transition se produit lorsque la souris interagit avec la carte, ce qui permet des transitions fluides vers les changements de rotation. Il s'agit d'une animation constante qui montre l'espace 3D dans lequel se trouve la carte, même si une souris ne peut pas ou n'interagit pas 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 définit uniquement l'image clé du milieu 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 qui alternent, devant commencer et se terminer à la même position. C'est un excellent moyen de présenter 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 éléments de bordure. Le style display est modifié de sorte 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 la zone complète 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;
}

Capture d&#39;écran de la liste pivotée dans l&#39;espace 3D pour afficher la perspective. Chaque élément de la liste n&#39;a plus de puce.

Appliquer un style aux éléments <button>

Attribuer un style aux boutons peut être difficile. De nombreux états et types d'interaction 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 prendront en charge 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;
}

Capture d&#39;écran de la liste de boutons en perspective 3D, cette fois avec des boutons stylisés.

Pseudo-éléments de bouton

Les bordures du bouton ne sont pas des bordures traditionnelles, ce sont des pseudo-éléments de position absolue avec bordures.

Capture d&#39;écran du panneau &quot;Éléments des outils pour les développeurs Chrome&quot; avec un bouton contenant les éléments ::before et ::after.

Ces éléments sont essentiels pour montrer la perspective 3D qui a été établie. L'un de ces pseudo-éléments est repoussé du bouton, tandis que l'autre est rapproché de l'utilisateur. L'effet est particulièrement visible dans 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

En dessous, transform-style est défini 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 passage de la souris 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 conditionnelles

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. Remarquez la différence au niveau de la durée. J'ai trouvé que cela donnait un joli 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 la souris et le curseur

L'objectif de l'animation d'interaction est de répartir les couches constituant le bouton qui apparaît plat. Pour ce faire, définissez la variable --distance, initialement sur 1px. Le sélecteur affiché dans l'exemple de code suivant vérifie si le bouton est pointé ou sélectionné par un appareil qui devrait voir un indicateur de mise au point, et s'il n'est pas activé. Dans ce cas, elle applique le CSS pour effectuer les opérations suivantes:

  • Appliquez la couleur d'arrière-plan au passage de la souris.
  • Augmentez la distance .
  • Ajoutez un effet de lissage de rebond.
  • É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 toujours très claire pour la préférence de mouvement de reduced. Les éléments du haut et du bas illustrent l'effet de façon subtile.

Petites améliorations avec JavaScript

L'interface est déjà utilisable avec un clavier, un lecteur d'écran, une manette de jeu, un écran tactile et une souris, mais nous pouvons ajouter quelques petites touches de JavaScript pour faciliter quelques scénarios.

Touches fléchées secondaires

La touche de tabulation permet de naviguer facilement dans le menu, mais je m'attendrais à 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 de défi IUG gère les touches fléchées à notre place. Le code ci-dessous indique à la bibliothèque de piéger 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 la parallaxe de la souris

Le suivi de la souris et l'inclinaison du menu ont pour but d'imiter les interfaces de jeux vidéo en RA et RV, où vous pouvez avoir un pointeur virtuel au lieu d'une souris. Cela peut être amusant quand les éléments sont hyper-conscients au pointeur.

Comme il s'agit d'une petite fonctionnalité supplémentaire, nous allons placer l'interaction derrière une requête de préférence de mouvement de l'utilisateur. Lors de la configuration, stockez également 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 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 de 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 mouvement de la souris, transmettez la position à la fonction getAngles() et utilisez les valeurs delta en tant que styles de propriété personnalisés. j'ai divisé par 20 pour remplir le delta et réduire les tensions. Il y a peut-être une meilleure façon de le faire. Souvenez-vous du début : nous plaçons les propriétés --x et --y au milieu d'une fonction clamp(). Cela empêche la position de la souris de faire pivoter la carte de manière excessive 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> sont associés au 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 des boutons par une liste de liens, les propriétés logiques peuvent changer l'orientation du menu, car les éléments <a> n'ont pas le style !important fourni par le navigateur.

Conclusion

Maintenant que vous savez comment j'ai fait, comment feriez-vous‽ 😃 Pouvez-vous ajouter l'interaction de l'accéléromètre au menu, afin que le fait de superposer votre téléphone fasse pivoter le menu ? Pouvons-nous améliorer l'expérience sans mouvement ?

Diversissons nos approches et apprenons toutes les façons de créer sur le Web. Créez une démo, cliquez sur les liens tweet me, et je l'ajouterai à la section "Remix" de la communauté ci-dessous.

Remix de la communauté

Aucun élément à afficher pour l'instant