Creazione di un componente del menu di un gioco 3D

Una panoramica di base su come creare un menu di gioco 3D reattivo, adattivo e accessibile.

In questo post voglio condividere una riflessione su un modo per creare un componente del menu di gioco 3D. Prova la demo.

Demo

Se preferisci i video, ecco una versione di YouTube di questo post:

Panoramica

I videogiochi presentano spesso agli utenti un menu creativo e insolito, animato e in 3D. È diffusa nei nuovi giochi AR/VR per far sembrare il menu sospeso nello spazio. Oggi ricreare gli elementi essenziali di questo effetto, ma con l'aggiunta di una combinazione di colori adattiva e adattamenti per gli utenti che preferiscono il movimento ridotto.

HTML

Il menu di un gioco è un elenco di pulsanti. Il modo migliore per rappresentarlo in HTML è il seguente:

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

Un elenco di pulsanti si annuncierà bene con le tecnologie di screen reader e funzionerà senza JavaScript o CSS.

un elenco puntato molto generico con pulsanti normali come elementi.

CSS

Lo stile dell'elenco dei pulsanti è suddiviso nei seguenti passaggi generali:

  1. Impostare le proprietà personalizzate.
  2. Layout flexbox.
  3. Un pulsante personalizzato con pseudo-elementi decorativi.
  4. Posizionare elementi nello spazio 3D.

Panoramica delle proprietà personalizzate

Le proprietà personalizzate aiutano a distinguere i valori assegnando nomi significativi a valori altrimenti casuali, evitando codici ripetuti e condivisione di valori con gli elementi secondari.

Di seguito sono riportate le query supporti salvate come variabili CSS, note anche come contenuti multimediali personalizzati. Sono globali e verranno utilizzati in vari selettori per mantenere il codice conciso e leggibile. Il componente del menu di gioco utilizza le preferenze di movimento, lo schema di colori di sistema e le funzionalità dell'intervallo di colori del display.

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

Le seguenti proprietà personalizzate gestiscono la combinazione di colori e tengono premuti i valori di posizione del mouse per rendere interattivo il menu del gioco al passaggio del mouse. L'assegnazione di nomi alle proprietà personalizzate è utile per la leggibilità del codice, in quanto rivela il caso d'uso del valore o un nome descrittivo per il risultato del valore.

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

Sfondi conici di sfondo con tema chiaro e scuro

Il tema chiaro ha un sfumatura conica dai colori vivaci da cyan a deeppink, mentre il tema scuro ha un sottile gradiente conico scuro. Per ulteriori informazioni su cosa può essere fatto con i gradienti conici, consulta conic.style.

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

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
Dimostrazione della modifica dello sfondo tra le preferenze di colore chiaro e scuro.

Attivazione della prospettiva 3D

Affinché gli elementi esistano nello spazio 3D di una pagina web, è necessario inizializzare un'area visibile con Prospettiva. Ho scelto di applicare la prospettiva sull'elemento body e ho utilizzato le unità dell'area visibile per creare lo stile che mi piaceva.

body {
  perspective: 40vw;
}

Questo è il tipo di impatto che può avere.

Stile dell'elenco di pulsanti <ul>

Questo elemento è responsabile del layout generale delle macro dell'elenco di pulsanti e di una scheda mobile interattiva e 3D. Ecco un modo per raggiungere questo obiettivo.

Layout del gruppo di pulsanti

Flexbox può gestire il layout del container. Cambia la direzione predefinita dell'orientamento flessibile da righe a colonne con flex-direction e assicurati che ogni elemento abbia le dimensioni dei suoi contenuti modificando da stretch a start per 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;
}

Successivamente, stabilisci il contenitore come un contesto dello spazio 3D e configura le funzioni clamp() CSS per assicurarti che la scheda non ruoti oltre rotazioni leggibili. Tieni presente che il valore centrale del blocco è una proprietà personalizzata; questi valori --x e --y verranno impostati da JavaScript all'interazione del mouse in un secondo momento.

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

Successivamente, se il movimento è appropriato per l'utente che visita, aggiungi un suggerimento al browser che la trasformazione di questo elemento cambierà costantemente con will-change. Inoltre, abilita l'interpolazione impostando un transition sulle trasformazioni. Questa transizione avviene quando il mouse interagisce con la scheda, consentendo una transizione fluida alle variazioni di rotazione. L'animazione è un'animazione in esecuzione costante che mostra lo spazio 3D all'interno della scheda, anche se il mouse non può o non interagisce con il 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;
  }
}

L'animazione rotate-y imposta solo il fotogramma chiave centrale su 50% poiché il browser imposterà per impostazione predefinita 0% e 100% allo stile predefinito dell'elemento. È un'abbreviazione per le animazioni che si alternano e devono iniziare e terminare nella stessa posizione. È un ottimo modo per articolare animazioni alternative e infinite.

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

Applicare uno stile agli elementi <li>

Ogni elemento dell'elenco (<li>) contiene il pulsante e i relativi elementi del bordo. Lo stile display è stato modificato per cui l'elemento non mostra una ::marker. Lo stile position è impostato su relative, pertanto gli pseudo-elementi del pulsante successivi possono posizionarsi all'interno dell'intera area utilizzata dal pulsante.

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

Screenshot dell&#39;elenco ruotato nello spazio 3D per mostrare la prospettiva e ogni elemento dell&#39;elenco non contiene più un punto elenco.

Applicare uno stile agli elementi <button>

L'applicazione di stili ai pulsanti può essere difficile, ci sono molti stati e tipi di interazione da tenere in considerazione. Questi pulsanti diventano rapidamente complessi a causa del bilanciamento di pseudo-elementi, animazioni e interazioni.

<button> stili iniziali

Di seguito sono riportati gli stili di base che supporteranno gli altri stati.

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

Screenshot dell&#39;elenco di pulsanti in prospettiva 3D, questa volta con pulsanti
con stile.

Pseudo-elementi di pulsante

I bordi del pulsante non sono bordi tradizionali, ma sono pseudo-elementi con bordi di posizione assoluta.

Screenshot del riquadro Elementi di Chrome DevTools con un pulsante mostrato con gli elementi ::before e ::after.

Questi elementi sono fondamentali per mostrare la prospettiva 3D che è stata stabilita. Uno di questi pseudo-elementi verrà rimosso dal pulsante e uno verrà tirato più vicino all'utente. L'effetto è più evidente nei pulsanti in alto e in basso.

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

Stili trasformazione 3D

Sotto transform-style è impostato su preserve-3d in modo che gli elementi secondari possano distanziarsi sull'asse z. transform è impostato sulla proprietà personalizzata --distance, che verrà aumentata al passaggio del mouse e allo stato attivo.

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

Stili di animazione condizionali

Se l'utente è d'accordo con il movimento, il pulsante comunica al browser che la proprietà di trasformazione deve essere pronta per essere modificata e che è impostata una transizione per le proprietà transform e background-color. Noti la differenza di durata, l'ho trovato come un bel effetto sfalsato.

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

Passa il mouse sopra gli stili di interazione e imposta lo stato attivo

L'obiettivo dell'animazione di interazione è distribuire i livelli che compongono il pulsante visualizzato piatto. Per farlo, imposta la variabile --distance, inizialmente su 1px. Il selettore mostrato nel seguente esempio di codice verifica se il pulsante viene passato sopra il pulsante o se è attivo da un dispositivo che dovrebbe vedere un indicatore di stato attivo e non è in fase di attivazione. In tal caso, applica al CSS i seguenti passaggi:

  • Applica il colore di sfondo del passaggio del mouse.
  • Aumenta la distanza .
  • Aggiungi un effetto di facilità di rimbalzo.
  • Distribuisci le transizioni di pseudo-elementi.
.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 prospettiva 3D era comunque ottima per la preferenza di movimento reduced. Gli elementi superiore e inferiore mostrano l'effetto in modo delicato.

Piccoli miglioramenti con JavaScript

L'interfaccia è già utilizzabile da tastiere, screen reader, gamepad, touch e un mouse, ma possiamo aggiungere alcuni tocchi di JavaScript per agevolare un paio di scenari.

Supporto dei tasti freccia

Il tasto Tab è un ottimo modo per navigare nel menu, ma il pad direzionale o i joystick spostano lo stato attivo su un gamepad. La libreria roving-ux, spesso utilizzata per le interfacce GUI Challenge, gestirà i tasti freccia al posto nostro. Il codice seguente indica alla libreria di bloccare lo stato attivo all'interno di .threeD-button-set e di inoltrare lo stato attivo ai elementi secondari del pulsante.

import {rovingIndex} from 'roving-ux'

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

Interazione con parallasse del mouse

Il tracciamento del mouse e l'inclinazione del menu hanno lo scopo di imitare le interfacce di videogiochi AR e VR, dove invece di un mouse puoi avere un puntatore virtuale. Può essere divertente quando gli elementi sono estremamente consapevoli del puntatore.

Poiché si tratta di una piccola funzionalità aggiuntiva, inseriremo l'interazione dietro una query della preferenza di movimento dell'utente. Inoltre, durante la configurazione, archivia il componente dell'elenco di pulsanti nella memoria con querySelector e memorizza nella cache i limiti dell'elemento in menuRect. Utilizza questi limiti per determinare l'offset di rotazione applicato alla scheda in base alla posizione del mouse.

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

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

Successivamente, abbiamo bisogno di una funzione che accetti le posizioni x e y del mouse e restituisca un valore che possiamo utilizzare per ruotare la scheda. La seguente funzione utilizza la posizione del mouse per determinare in quale lato della casella si trova e di quanto. Il delta viene restituito dalla funzione.

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

Infine, osserva il movimento del mouse, passa la posizione alla nostra funzione getAngles() e utilizza i valori delta come stili di proprietà personalizzate. Ho diviso 20 per comprimere il delta e renderlo meno mosso, c'è un modo migliore per farlo. Se ricordi dall'inizio, mettiamo gli oggetti --x e --y al centro di una funzione clamp() per impedire alla posizione del mouse di ruotare eccessivamente la scheda in una posizione illeggibile.

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

Traduzioni e indicazioni stradali

C'è stato un modo per provare il menu del gioco in altre modalità di scrittura e lingue.

Gli elementi <button> hanno uno stile !important per writing-mode nel foglio di stile dello user agent. Di conseguenza, è stato necessario modificare l'HTML del menu del gioco per adattarlo al design desiderato. La modifica dell'elenco dei pulsanti in un elenco di link consente alle proprietà logiche di modificare la direzione del menu, poiché gli elementi <a> non hanno uno stile !important fornito dal browser.

Conclusione

Ora che sai come ci sono riuscito, come... 🙂 Puoi aggiungere l'interazione con l'accelerometro al menu, così che pianifichi lo smartphone per ruotare il menu? Possiamo migliorare l'esperienza senza movimento?

Diversifica i nostri approcci e scopriamo tutti i modi per creare sul web. Crea una demo, inviami un tweet con i link e lo aggiungerò alla sezione Remix della community di seguito.

Remix della community

Ancora niente da visualizzare