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

Descripción general de los conceptos básicos sobre cómo crear un menú de juego en 3D responsivo, adaptable y accesible

En esta publicación, quiero contarte cómo crear un componente del menú de un juego en 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ú inusual y creativo, animado y en un espacio 3D. En los juegos de RA y RV nuevos, es popular hacer que el menú parezca flotar en el espacio. Hoy recrearemos los elementos esenciales de este efecto, pero con el estilo adicional de un esquema de colores adaptable y adaptaciones para los usuarios que prefieren un movimiento reducido.

HTML

El menú del 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 anunciará bien para las tecnologías de lector de pantalla y funciona sin JavaScript ni CSS.

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

CSS

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

  1. Configurar propiedades personalizadas
  2. Un diseño de flexbox
  3. Un botón personalizado con pseudoelementos 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 de aspecto aleatorio, lo que evita la repetición de código y el uso compartido de valores entre elementos secundarios.

A continuación, se muestran las consultas de medios que se guardan como variables de CSS, también conocidas como contenido multimedia personalizado. Estos 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 colores del sistema y capacidades de rango de color 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 posicionales del mouse para que el menú del juego sea interactivo y se pueda colocar el cursor sobre él. La asignación de nombres a propiedades personalizadas ayuda a la legibilidad del código, 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);
    }
  }
}

Fondos cónicos del fondo del tema claro y oscuro

El tema claro tiene un gradiente cónico de cyan a deeppink vibrante, 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 los 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.

Cómo habilitar la perspectiva 3D

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

body {
  perspective: 40vw;
}

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

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

Este elemento es responsable del diseño de la macro de la lista de botones general, así como de una tarjeta interactiva y flotante en 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 Flex 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 funciones clamp() de CSS para asegurarte de que la tarjeta no rote más allá de rotaciones legibles. Ten en cuenta que el valor medio de la restricción es una propiedad personalizada. Estos valores --x y --y se configurarán desde JavaScript con la interacción del 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 es aceptable para 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 estableciendo un transition en las transformaciones. Esta transición se producirá cuando el mouse interactúe con la tarjeta, lo que permitirá realizar transiciones fluidas a los cambios de rotación. La animación es 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 interactuar con el componente o no puede interactuar con él.

@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 del medio en 50%, ya que el navegador aplicará el estilo predeterminado del elemento 0% y 100% de forma predeterminada. Se trata de una abreviatura de animaciones que se alternan, ya que necesitan comenzar y terminar en la misma posición. Es una excelente manera de articular animaciones alternadas infinitas.

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

Cómo aplicar diseño a los elementos <li>

Cada elemento de la lista (<li>) contiene el botón y sus elementos de borde. Se cambia el diseño display para que el elemento no muestre una ::marker. El estilo position se establece en relative para que los próximos seudoelementos del botón puedan 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 una viñeta.

Cómo aplicar diseño a los elementos <button>

Aplicar estilo a los botones puede ser un trabajo difícil, ya que hay que tener en cuenta muchos estados y tipos de interacción. Estos botones se vuelven complejos rápidamente debido al equilibrio de los pseudoelementos, las animaciones y las interacciones.

<button> diseños iniciales

A continuación, se muestran los estilos básicos que admitirán los otros 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.

Pseudoelementos de botones

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

Captura de pantalla del panel Elementos de las Herramientas para desarrolladores de Chrome con un botón que incluye los elementos ::before y ::after.

Estos elementos son cruciales para mostrar la perspectiva 3D establecida. Uno de estos seudoelementos se alejará del botón y uno se acercará al usuario. El efecto es más evidente 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 separarse en el eje z. transform se establece en la propiedad personalizada --distance, que aumentará cuando se coloque 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 sugiere al navegador que la propiedad de transformación debe estar lista para el cambio y se establece una transición para las propiedades transform y background-color. Observa la diferencia en la duración, sentí que se creó para un agradable efecto escalonado sutil.

.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 sobre un elemento y enfocar

El objetivo de la animación de interacción es extender las capas que componen el botón de aparición plana. Para lograrlo, establece la variable --distance, inicialmente, 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 está desplazando o enfocando el botón, y si no está activado. Si es así, aplica CSS para lo siguiente:

  • Aplica el color de fondo cuando se coloca el cursor sobre un elemento.
  • Aumenta la distancia .
  • Agrega un efecto de aceleración 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 muy buena para la preferencia de movimiento de reduced. Los elementos inferior y superior muestran el efecto de una manera agradable y sutil.

Pequeñas mejoras con JavaScript

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

Cómo brindar compatibilidad con teclas de flecha

La tecla Tab es una buena forma de navegar por el menú, pero supongo que el mando de dirección o los joysticks se desplacen en el enfoque de un control de juegos. La biblioteca roving-ux que se usa a menudo para las interfaces de desafío de la GUI controlará las teclas de flecha por nosotros. El siguiente código le indica a la biblioteca que capture el enfoque dentro de .threeD-button-set y 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 con el paralaje del mouse

Hacer un seguimiento del mouse y hacer que incline el menú está diseñado para imitar las interfaces de videojuegos de RA y RV, en las que, en lugar de un mouse, puedes tener un puntero virtual. Puede ser divertido cuando los elementos conocen muy bien el puntero.

Como se trata de una pequeña función adicional, colocaremos la interacción detrás de una consulta de la preferencia de movimiento del usuario. Además, como parte de la configuración, almacena en la memoria el componente de la lista de botones 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 determinar en qué lado de la caja se encuentra y en qué medida se encuentra. El 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 propiedad personalizados. Divídolo por 20 para rellenar el delta y hacerlo menos trabas, quizás 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 demasiado 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`)
  })
}

Traducciones e instrucciones sobre cómo llegar

Hubo una dificultad al probar el menú del juego en otros idiomas y modos de escritura.

Los elementos <button> tienen un diseño !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 el menú se rote cuando se dibuja el teléfono? ¿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 que aparece más abajo.

Remixes de la comunidad

Aún no hay nada para ver aquí.