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 compartir mi forma de pensar para crear un componente de menú de juego en 3D. Prueba la demostración.
Si prefieres ver un video, aquí tienes una versión de esta publicación en YouTube:
Descripción general
Los videojuegos suelen presentar a los usuarios un menú creativo y poco común, 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 aspectos esenciales de este efecto, pero con el estilo agregado de un esquema de colores adaptable y adaptaciones para los usuarios que prefieren reducir el movimiento.
HTML
Un menú de juego es una lista de botones. La mejor manera de representar esto en HTML es de la siguiente manera:
<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 a las tecnologías de lectores de pantalla y funciona sin JavaScript ni CSS.
CSS
El diseño de la lista de botones se puede desglosar en los siguientes pasos de alto nivel:
- Configurar propiedades personalizadas
- Un diseño de flexbox
- Un botón personalizado con pseudoelementos decorativos.
- 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 les asignan nombres significativos a valores que, de otro modo, parecen aleatorios, evitan el código repetido 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. 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 las funciones de 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 posicionales del mouse para que el menú del juego sea interactivo cuando se coloca el cursor sobre él. Asignar nombres a las propiedades personalizadas ayuda a la legibilidad del código, ya que revela el caso de uso del valor o un nombre fácil de entender 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 de temas claros y oscuros
El tema claro tiene un gradiente conico vibrante de cyan
a deeppink
, mientras que el tema oscuro tiene un gradiente conico 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);
}
}
Habilita 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í colocar 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.
Aplica diseño a la lista de botones <ul>
Este elemento es responsable del diseño general de la macro de la lista de botones y también es una tarjeta flotante interactiva y en 3D. A continuación, te mostramos una forma de hacerlo.
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 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 del límite es una propiedad personalizada. Estos valores --x
y --y
se establecerá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 es aceptable para el usuario visitante, agrega una sugerencia al navegador que indique 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 continua que demuestra el espacio 3D en el que se encuentra la tarjeta, incluso si un mouse no puede interactuar con el componente o no lo hace.
@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 establecerá de forma predeterminada 0%
y 100%
en el estilo predeterminado del elemento. Esta es una sigla para las animaciones que se alternan y deben 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);
}
}
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 estilo display
para que el elemento no muestre un ::marker
. El estilo position
se establece en relative
para que los próximos pseudoelementos de 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;
}
Cómo aplicar diseño a los elementos <button>
Diseñar los botones puede ser un trabajo difícil, ya que hay muchos estados y tipos de interacción que se deben tener en cuenta. Estos botones se vuelven complejos rápidamente debido al equilibrio entre 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;
}
Pseudoelementos de botón
Los bordes del botón no son bordes tradicionales, son pseudoelementos de posición absoluta con bordes.
Estos elementos son fundamentales para mostrar la perspectiva 3D que se estableció. Uno de estos pseudoelementos se alejará del botón y el otro se acercará al usuario. El efecto es más notable 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 establece en preserve-3d
para que los elementos secundarios se espacien en el eje z
. El transform
se establece en la propiedad personalizada --distance
, que aumentará cuando se coloque el cursor sobre el elemento y se 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 transform debería estar lista para cambiar y se establece una transición para las propiedades transform
y background-color
. Observa la diferencia en la duración. Creo que se creó un buen 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 ello, 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 coloca el cursor sobre el botón o lo enfoca, y no lo activa. Si es así, aplica CSS para lo siguiente:
- Aplica el color de fondo del cursor encima.
- Aumenta la distancia.
- Agrega un efecto de atenuación de rebote.
- Ajusta las transiciones de pseudoelementos.
.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 en 3D seguía siendo muy atractiva para la preferencia de movimiento reduced
.
Los elementos de la parte superior e inferior muestran el efecto de una manera sutil.
Pequeñas mejoras con JavaScript
La interfaz ya se puede usar desde teclados, lectores de pantalla, gamepads, pantallas táctiles y un mouse, pero podemos agregar algunos toques ligeros de JavaScript para facilitar algunas situaciones.
Compatibilidad con teclas de flecha
La tecla Tab es una buena forma de navegar por el menú, pero esperaría que el pad direccional o los joysticks muevan el enfoque en un gamepad. 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 foco 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 de 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 están muy atentos al puntero.
Como esta es 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 aplicado a la tarjeta según 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 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. Dividí por 20 para rellenar el delta y hacerlo menos inestable. Puede haber una mejor manera de hacerlo. Si recuerdas desde el principio, colocamos los accesorios --x
y --y
en medio de una función clamp()
. Esto evita que la posición del mouse rote demasiado la tarjeta en 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 y direcciones
Hubo un problema cuando probé el menú del juego en otros modos de escritura y idiomas.
Los elementos <button>
tienen un estilo !important
para writing-mode
en la hoja de estilo del agente de usuario. 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 tú? 🙂 ¿Puedes agregar interacción con el acelerómetro al menú para que el teléfono rote el menú? ¿Podemos mejorar la experiencia sin movimiento?
Diversifiquemos nuestros enfoques y aprendamos todas las formas de compilar en la Web. Crea una demo, twittea los vínculos y los agregaré a la sección de remixes de la comunidad a continuación.
Remixes de la comunidad
Todavía no hay nada para ver aquí.