Como criar um componente de menu de jogo 3D

Uma visão geral básica de como criar um menu de jogos 3D responsivo, adaptável e acessível.

Nesta postagem, quero compartilhar ideias para criar um componente de menu de jogos 3D. Experimente o demonstração.

Demonstração

Se você preferir o vídeo, aqui está uma versão do YouTube desta postagem:

Visão geral

Os videogames geralmente oferecem aos usuários um menu criativo e incomum, animado e no espaço 3D. É popular em novos jogos de RA/RV para fazer o menu parecer flutuando no espaço. Hoje vamos recriar a essência desse efeito, mas com o toque adicional de um esquema de cores adaptável e acomodações para os usuários que preferem movimento reduzido.

HTML

Um menu de jogos é uma lista de botões. A melhor maneira de representar isso em HTML é da seguinte forma:

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

Uma lista de botões será anunciada corretamente para tecnologias de leitores de tela e funciona sem JavaScript ou CSS.

por
lista com marcadores de aparência muito genérica com botões normais como itens.

CSS

O estilo da lista de botões é dividido nas seguintes etapas de alto nível:

  1. Configurar propriedades personalizadas.
  2. Um layout flexbox.
  3. Um botão personalizado com pseudoelementos decorativos.
  4. Colocando elementos no espaço 3D.

Visão geral das propriedades personalizadas

As propriedades personalizadas ajudam a eliminar a ambiguidade de valores fornecendo informações a valores de aparência aleatória, evitando a repetição de códigos e o compartilhamento entre filhos.

Abaixo estão as consultas de mídia salvas como variáveis CSS, também conhecidas como personalizadas mídia. Elas são globais e será usada em vários seletores para manter o código conciso e legível. A o componente do menu de jogos usa movimento preferências, cor do sistema esquema, e a faixa de cores recursos do exibição.

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

As seguintes propriedades personalizadas gerenciam o esquema de cores e seguram o mouse valores posicionais para tornar o menu de jogos interativo ao passar o cursor. Nomeação personalizada ajuda a legibilidade do código, porque revela o caso de uso do valor ou uma nome simples para o resultado do 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);
    }
  }
}

Planos de fundo cônicos com tema claro e escuro

O tema claro tem uma cônica cyan a deeppink vibrante gradiente enquanto o tema escuro tem um gradiente cônico sutil escuro. Para saber mais sobre o que pode ser feito com gradientes cônicos, consulte conic.style.

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

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
Demonstração de mudança do plano de fundo entre as preferências de cor clara e escura.

Ativando a perspectiva 3D

Para que os elementos existam no espaço 3D de uma página da Web, uma janela de visualização com perspectiva que precisa ser inicializada. Optei por colocar a perspectiva no elemento body e usei unidades da janela de visualização para criar o estilo que gostei.

body {
  perspective: 40vw;
}

Esse é o tipo de impacto que a perspectiva pode ter.

Definir o estilo da lista de botões <ul>

Esse elemento é responsável pelo layout geral da macro da lista de botões, bem como por ser um cartão interativo e flutuante em 3D. Confira como fazer isso.

Layout do grupo de botões

O Flexbox pode gerenciar o layout do contêiner. Mudar a direção padrão do Flex de linhas a colunas com flex-direction e verifique se cada item tem o tamanho de seu conteúdo mudando de stretch para start em 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;
}

Em seguida, defina o contêiner como um contexto de espaço 3D e configure o CSS clamp() para garantir que o cartão não gire além das rotações legíveis. Aviso que o valor do meio do fecho seja uma propriedade personalizada, essas --x e --y os valores serão definidos a partir de JavaScript ao passar o mouse interação mais tarde.

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

Em seguida, se o usuário visitante estiver satisfeito com o movimento, adicione uma dica ao navegador a transformação deste item muda constantemente com will-change Além disso, ative a interpolação definindo um transition nas transformações. Isso a transição ocorre quando o mouse interage com o cartão, permitindo um transições para mudanças de rotação. A animação é uma execução constante que demonstra o espaço 3D dentro do cartão, mesmo que um mouse não consiga não está interagindo com o 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;
  }
}

A animação rotate-y só define o frame-chave do meio em 50%, pois o navegador definirá 0% e 100% como o estilo padrão do elemento. Isso é uma abreviação de animações que se alternam, precisando começar e terminar no mesmo posição É uma ótima maneira de articular animações alternadas infinitas.

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

Como definir o estilo dos elementos <li>

Cada item da lista (<li>) contém o botão e os elementos de borda. A O estilo display foi alterado para que o item não mostrasse um ::marker. O estilo position seja definido como relative, para que os pseudoelementos do botão seguinte possam se posicionar em toda a área que o botão consome.

.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 tela da lista girada no espaço 3D para mostrar a perspectiva.
cada item da lista não tem mais um marcador.

Como definir o estilo dos elementos <button>

Estilizar botões pode ser difícil, há muitos estados e tipos de interação considerar. Esses botões ficam complexos rapidamente devido ao balanceamento pseudoelementos, animações e interações.

Estilos iniciais de <button>

Confira abaixo os estilos básicos que vão oferecer suporte aos outros 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 tela da lista de botões em perspectiva 3D, desta vez com estilo
botões.

Pseudoelementos de botão

As bordas do botão não são tradicionais, são posições absolutas pseudoelementos com bordas.

Captura de tela do painel &quot;Elementos do Chrome Devtools&quot; com um botão exibido
::before e ::after.

Esses elementos são cruciais para mostrar a perspectiva 3D que está sendo estabelecidos. Um desses pseudoelementos será empurrado para longe do botão, e uma será puxada para mais perto do usuário. O efeito é mais perceptível no 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 transformação 3D

Abaixo de transform-style é definido como preserve-3d para que os filhos possam espaçar no eixo z. O transform está definido como --distance. propriedade personalizada, que será aumentada ao passar o cursor e em foco.

.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 animação condicionais

Se o usuário concordar com o movimento, o botão indicará ao navegador que a transform deve estar pronta para mudança e uma transição é definida para Propriedades transform e background-color. Observe a diferença em duração, senti que ela causava um bom efeito sutil e 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 interação ao passar o cursor e focar

O objetivo da animação de interação é espalhar as camadas que formavam a botão de aparecimento plano. Para isso, defina a variável --distance, inicialmente para 1px. O seletor mostrado no exemplo de código a seguir verifica veja se o botão está sendo passado ou focado em um dispositivo que deve ver uma indicador de foco e não sendo ativado. Em caso afirmativo, ele aplica CSS para fazer seguinte:

  • Aplique a cor do plano de fundo ao passar o cursor.
  • Aumentar a distância .
  • Adicione um efeito de facilidade de rejeição.
  • Distribua as transições dos 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 }
    }
  }
}

A perspectiva 3D ainda era muito boa para a preferência de movimento reduced. Os elementos de cima e de baixo mostram o efeito de maneira sutil.

Pequenas melhorias com o JavaScript

A interface é utilizável a partir de teclados, leitores de tela, gamepads, toque e uma já é o mouse, mas podemos adicionar alguns toques leves de JavaScript para facilitar alguns de cenários.

Compatibilidade com teclas de seta

A tecla Tab é uma ótima maneira de navegar pelo menu, mas eu esperaria que a tecla direcional ou joysticks para mover o foco em um gamepad. A Biblioteca roving-ux usada com frequência para GUI Interfaces de desafio vão lidar com as teclas de seta para nós. O código abaixo informa biblioteca para capturar o foco em .threeD-button-set e encaminhá-lo para a filhos do botão.

import {rovingIndex} from 'roving-ux'

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

Interação do paralaxe do mouse

Rastrear o mouse e inclinar o menu, tem como objetivo imitar a RA e a RV interfaces de videogame, onde, em vez de um mouse, você pode ter um ponteiro virtual. Pode ser divertido quando os elementos reconhecem muito o ponteiro.

Como se trata de um recurso extra pequeno, colocaremos a interação em uma consulta de preferência de movimento do usuário. Além disso, como parte da configuração, armazene a lista de botões na memória com querySelector e armazenar em cache os limites do elemento menuRect. Use esses limites para determinar o deslocamento de rotação aplicado ao card. com base na posição do mouse.

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

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

Em seguida, precisamos de uma função que aceite as posições x e y do mouse e retorne um valor que podemos usar para girar o cartão. A função a seguir usa o mouse, posição para determinar de que lado da caixa está dentro e o quanto. A delta é retornado da função.

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 fim, observe o movimento do mouse e transmita a posição para a função getAngles(). e usar os valores delta como estilos de propriedade personalizados. Divida por 20 para preencher o delta e torná-lo menos instável, pode haver uma maneira melhor de fazer isso. Se você Lembre-se desde o início, colocamos as propriedades --x e --y no meio de uma clamp(), impede que a posição do mouse gire excessivamente o o cartão em uma posição ilegível.

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

Traduções e instruções

Houve um problema ao testar o menu do jogo em outros modos de escrita e idiomas.

Os elementos <button> têm um estilo !important para writing-mode no usuário na folha de estilo do agente. Isso significava que o HTML do menu de jogos precisava ser alterado para acomodar o design desejado. Alterar a lista de botões para uma lista de links permite que a lógica para mudar a direção do menu, já que os elementos <a> não têm um navegador estilo !important fornecido.

Conclusão

Agora que você sabe como eu fiz isso, o que você faria ‽ 🙂 Pode adicionar o acelerômetro? interação com o menu, então posicionar o smartphone gira o menu? Podemos melhorar? a experiência sem movimento?

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, envie um tweet para mim e adicione os links acesse a seção "Remixes da comunidade" abaixo.

Remixes da comunidade

Não há nada aqui ainda.