Cómo compilar un componente del menú del juego en 3D

Descripción general fundamental de cómo crear un menú de juegos en 3D responsivo, adaptable y accesible.

En esta publicación, quiero compartir las ideas sobre una forma de crear un componente del menú del juego 3D. Prueba la demostración.

Demostración

Si prefieres ver un video, aquí tienes una versión de YouTube de esta publicación:

Descripción general

Los videojuegos suelen presentar a los usuarios un menú creativo e inusual, animados y en espacio 3D. En los nuevos juegos de RA/RV, es popular hacer que el menú parezca flotar en el espacio. Hoy recrearemos los aspectos básicos de este efecto, pero con el estilo adicional de un esquema de colores adaptables y adaptaciones para los usuarios que prefieren un movimiento reducido.

HTML

El menú de un juego es una lista de botones. La mejor manera de representar esto en HTML es la siguiente:

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

Una lista de botones se presentará bien en las tecnologías de lectores de pantalla y funcionará sin JavaScript ni CSS.

una lista de viñetas muy genérica con botones regulares como elementos.

CSS

El diseño de la lista de botones se divide en los siguientes pasos generales:

  1. Configurar propiedades personalizadas
  2. Un diseño de flexbox
  3. Un botón personalizado con seudoelementos decorativos.
  4. Colocación de elementos en un espacio 3D.

Descripción general de las propiedades personalizadas

Las propiedades personalizadas ayudan a desambiguar los valores, ya que asignan nombres significativos a valores que se verían de forma aleatoria, evitan la repetición de código y comparten valores entre elementos secundarios.

A continuación, se muestran las consultas de medios guardadas como variables de CSS, también conocidas como medios personalizados. Son globales y se usarán en varios selectores para que el código sea conciso y legible. El componente del menú del juego usa preferencias de movimiento, el esquema de color del sistema y las capacidades del rango de colores de la pantalla.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);

Las siguientes propiedades personalizadas administran el esquema de colores y mantienen los valores de posición del mouse para que el menú del juego sea interactivo y se pueda colocar el cursor sobre él. Asignar nombres a las propiedades personalizadas ayuda a que el código sea legible, ya que revela el caso de uso del valor o un nombre descriptivo para el resultado del valor.

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

Fondo cónico de fondo con tema claro y oscuro

El tema claro tiene un gradiente cónico de cyan a deeppink intenso, mientras que el tema oscuro tiene un gradiente cónico sutil oscuro. Para obtener más información sobre lo que se puede hacer con gradientes cónicos, consulta conic.style.

html {
  background: conic-gradient(at -10% 50%, deeppink, cyan);

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
Demostración del cambio de fondo entre las preferencias de color claro y oscuro.

Habilitación de la perspectiva 3D

Para que los elementos existan en el espacio 3D de una página web, se debe inicializar un viewport con perspectiva. Decidí poner la perspectiva en el elemento body y usé unidades de viewport para crear el estilo que me gustó.

body {
  perspective: 40vw;
}

Este es el tipo de impacto que puede tener la perspectiva.

Cómo aplicar diseño a la lista de botones <ul>

Este elemento es responsable del diseño general de la lista de botones, además de ser una tarjeta flotante interactiva y 3D. Aquí te mostramos una manera de lograrlo.

Diseño del grupo de botones

Flexbox puede administrar el diseño del contenedor. Cambia la dirección predeterminada de la flexibilidad de filas a columnas con flex-direction y asegúrate de que cada elemento tenga el tamaño de su contenido cambiando de stretch a start para 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;
}

A continuación, establece el contenedor como un contexto de espacio 3D y configura las funciones clamp() de CSS para asegurarte de que la tarjeta no rote más allá de las rotaciones legibles. Ten en cuenta que el valor medio de la abrazadera es una propiedad personalizada, y estos valores --x y --y se configurarán desde JavaScript cuando se interactúe con el mouse más adelante.

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

A continuación, si el movimiento está bien con el usuario visitante, agrega una sugerencia al navegador de que la transformación de este elemento cambiará constantemente con will-change. Además, habilita la interpolación mediante la configuración de un transition en las transformaciones. Esta transición ocurrirá cuando el mouse interactúe con la tarjeta, lo que permitirá transiciones suaves a los cambios de rotación. Se trata de una animación en ejecución constante que demuestra el espacio 3D en el que se encuentra la tarjeta, incluso si un mouse no puede o no interactúa con el componente.

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

La animación rotate-y solo establece el fotograma clave central en 50%, ya que el navegador usará 0% y 100% de forma predeterminada con el estilo predeterminado del elemento. Se trata de una abreviatura de animaciones que se alternan, ya que necesitan comenzar y finalizar en la misma posición. Es una excelente manera de articular animaciones alternas infinitas.

@keyframes rotate-y {
  50% {
    transform: rotateY(15deg) rotateX(-6deg);
  }
}

Aplica diseño a los elementos <li>

Cada elemento de la lista (<li>) contiene el botón y sus elementos de borde. Se cambió el diseño display para que el elemento no muestre un ::marker. El estilo position se establece en relative, por lo que los próximos pseudoelementos del botón pueden posicionarse dentro del área completa que consume el botón.

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

Captura de pantalla de la lista rotada en un espacio 3D para mostrar la perspectiva, y cada elemento de la lista ya no tiene viñetas.

Aplica diseño a los elementos <button>

Diseñar botones puede ser un trabajo difícil; hay muchos estados y tipos de interacción para tener en cuenta. Estos botones se vuelven complejos rápidamente debido al equilibrio de los seudoelementos, las animaciones y las interacciones.

Estilos <button> iniciales

A continuación, se muestran los estilos básicos que respaldarán los demás estados.

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

Captura de pantalla de la lista de botones en perspectiva 3D, esta vez con botones con estilo.

Seudoelementos del botón

Los bordes del botón no son tradicionales, sino pseudoelementos de posición absoluta con bordes.

Captura de pantalla del panel Elements de Chrome Devtools, que muestra un botón con elementos ::before y ::after.

Estos elementos son cruciales para mostrar la perspectiva 3D que se ha establecido. Uno de estos pseudoelementos se alejará del botón y el otro se acercará al usuario. El efecto es más notorio en los botones superior e inferior.

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

Estilos de transformación 3D

Debajo de transform-style, se configura como preserve-3d para que los elementos secundarios puedan espaciarse por su cuenta en el eje z. transform se establece en la propiedad personalizada --distance, que se aumentará cuando desplace el cursor y enfoque.

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

Estilos de animación condicionales

Si el usuario acepta el movimiento, el botón le indica al navegador que la propiedad de transformación debe estar lista para el cambio y que se establece una transición para las propiedades transform y background-color. Fíjate en la diferencia en la duración, sentí que generó un agradable efecto escalonado.

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

Estilos de interacción de colocar el cursor y enfocar

El objetivo de la animación de interacción es extender las capas que conforman el botón que aparece plano. Para lograrlo, configura la variable --distance, primero en 1px. El selector que se muestra en el siguiente ejemplo de código verifica si un dispositivo que debería ver un indicador de enfoque y no está activado cuando coloca el cursor sobre el botón o lo enfoca. Si es así, se aplica CSS para hacer lo siguiente:

  • Aplica el color de fondo al colocar el cursor.
  • Aumentar la distancia .
  • Agrega un efecto de facilidad de rebote.
  • Escalonar las transiciones de los seudoelementos.
.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 perspectiva 3D seguía siendo excelente para la preferencia de movimiento de reduced. Los elementos inferior y superior muestran el efecto de una manera sutil y agradable.

Pequeñas mejoras con JavaScript

La interfaz ya se puede usar desde teclados, lectores de pantalla, controles de juegos, funciones táctiles y un mouse, pero podemos agregar algunos toques ligeros de JavaScript para facilitar algunas situaciones.

Teclas de flecha compatibles

La tecla Tab es una buena forma de navegar por el menú, pero es de esperar que el mando de dirección o los joysticks muevan el foco en un control de juegos. La biblioteca roving-ux que se usa a menudo para las interfaces de desafío de la GUI se encargará de las teclas de flecha por nosotros. El siguiente código le indica a la biblioteca que intercepte el enfoque dentro de .threeD-button-set y que lo reenvíe a los elementos secundarios del botón.

import {rovingIndex} from 'roving-ux'

rovingIndex({
  element: document.querySelector('.threeD-button-set'),
  target: 'button',
})

Interacción de paralaje del mouse

El objetivo de rastrear el mouse y hacer que incline el menú es imitar las interfaces de videojuegos de RA y RV, donde, en lugar de un mouse, puedes tener un puntero virtual. Puede ser divertido cuando los elementos son hiperconscientes del puntero.

Dado que se trata de una función adicional pequeña, pondremos la interacción detrás de una consulta de la preferencia de movimiento del usuario. Además, como parte de la configuración, almacena el componente de la lista de botones en la memoria con querySelector y almacena en caché los límites del elemento en menuRect. Usa estos límites para determinar el desplazamiento de rotación que se aplica a la tarjeta en función de la posición del mouse.

const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

A continuación, necesitamos una función que acepte las posiciones x y y del mouse, y que muestre un valor que podamos usar para rotar la tarjeta. La siguiente función usa la posición del mouse para identificar de qué lado del cuadro se encuentra y en qué medida. El valor delta se muestra desde la función.

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

Por último, observa cómo se mueve el mouse, pasa la posición a nuestra función getAngles() y usa los valores delta como diseños de propiedades personalizadas. Dividido por 20 para rellenar el delta y hacer que sea menos tenso, puede que haya una mejor manera de hacerlo. Si lo recuerdas desde el principio, colocamos los objetos --x y --y en el medio de una función clamp() para evitar que la posición del mouse gire en exceso la tarjeta a una posición ilegible.

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`)
  })
}

Instrucciones y traducciones

Hubo un problema cuando se probaba el menú del juego en otros idiomas y modos de escritura.

Los elementos <button> tienen un estilo !important para writing-mode en la hoja de estilo del usuario-agente. Esto significaba que el HTML del menú del juego debía cambiar para adaptarse al diseño deseado. Cambiar la lista de botones a una lista de vínculos permite que las propiedades lógicas cambien la dirección del menú, ya que los elementos <a> no tienen un estilo !important proporcionado por el navegador.

Conclusión

Ahora que sabes cómo lo hice, ¿cómo lo harías? 🙂 ¿Puedes agregar interacción del acelerómetro al menú para que las tarjetas de tu teléfono hagan rotar el menú? ¿Podemos mejorar la experiencia sin movimiento?

Diversifiquemos nuestros enfoques y aprendamos todas las formas de desarrollar en la Web. Crea una demostración, twittea vínculos y la agregaré a la sección de remixes de la comunidad a continuación.

Remixes de la comunidad

Aún no hay nada que ver aquí.