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

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.

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.

une liste à puces d&#39;apparence très générique
avec des boutons normaux comme éléments.

CSS

Le style de la liste de boutons se décompose en grandes étapes suivantes:

  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 à 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);
  }
}
Démonstration du changement de couleur d'arrière-plan entre les préférences de couleur claire et sombre.

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;
}

Capture d&#39;écran de la liste pivotée dans l&#39;espace 3D pour afficher la perspective, et chaque élément de la liste ne comporte plus de puce.

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;
}

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, mais des pseudo-éléments de position absolue avec des bordures.

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

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 !