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 le idee su un modo per creare un componente del menu di un gioco in 3D. Prova la demo.

Demo

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

Panoramica

I videogiochi spesso offrono agli utenti un menu creativo e insolito, animato e in uno spazio 3D. È popolare nei nuovi giochi AR/VR per far sembrare il menu fluttuante nello spazio. Oggi ricreiamo gli elementi essenziali di questo effetto, con l'aggiunta di una combinazione di colori adattiva e adattamenti per gli utenti che preferiscono la riduzione del movimento.

HTML

Il menu di un gioco è costituito da 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 è adatto per le tecnologie di screen reader e funziona senza JavaScript o CSS.

un elenco puntato molto generico con pulsanti normali
come elementi.

CSS

L'impostazione di uno stile per l'elenco dei pulsanti è suddivisa nei seguenti passaggi generali:

  1. Impostazione di proprietà personalizzate.
  2. Un layout flexbox.
  3. Un pulsante personalizzato con pseudoelementi decorativi.
  4. Inserire elementi nello spazio 3D.

Panoramica delle proprietà personalizzate

Le proprietà personalizzate aiutano a disambiguare i valori assegnando nomi significativi a valori altrimenti casuali, evitando codice ripetuto e condividendo valori tra elementi secondari.

Di seguito sono riportate le query supporti salvate come variabili CSS, note anche come media personalizzati. Sono globali e verranno utilizzati in vari selettori per mantenere il codice conciso e leggibile. Il componente Menu di gioco utilizza le preferenze di movimento, lo schema di colori di sistema e le funzionalità degli intervalli 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 per passare il mouse. L'assegnazione di nomi alle proprietà personalizzate favorisce 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 gradiente conico da cyan a deeppink chiaro, mentre il tema scuro ha un gradiente conico sottile. Per ulteriori informazioni su cosa si può fare 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 dello sfondo che cambia tra 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 all'elemento body e ho usato le unità area visibile per creare lo stile che mi piaceva.

body {
  perspective: 40vw;
}

Questo è il tipo di impatto che la prospettiva può avere.

Stili dell'elenco di pulsanti <ul>

Questo elemento è responsabile del layout complessivo della macro dell'elenco di pulsanti, nonché di una scheda mobile interattiva e 3D. Ecco un modo per farlo.

Layout del gruppo di pulsanti

Flexbox può gestire il layout del container. Modifica la direzione predefinita di flex dalle righe alle 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, imposta il contenitore come contesto dello spazio 3D e configura le funzioni clamp() CSS per garantire che la scheda non ruoti oltre le rotazioni leggibili. Tieni presente che il valore medio del blocco è una proprietà personalizzata, questi valori --x e --y verranno impostati da JavaScript al momento dell'interazione con il 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)
      )
    )
  ;
}

In seguito, se il movimento è accettabile per l'utente che visita il sito, 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 avverrà quando il mouse interagisce con la scheda, consentendo transizioni fluide alle modifiche 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 soltanto il fotogramma chiave centrale in 50% poiché il browser utilizzerà lo stile predefinito dell'elemento per 0% e 100%. Questa è una forma abbreviata per le animazioni che si alternano, devono iniziare e terminare nella stessa posizione. È un ottimo modo per articolare animazioni infinite alterne.

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

Stili degli elementi <li>

Ogni elemento dell'elenco (<li>) contiene il pulsante e i relativi elementi del bordo. Lo stile display viene modificato in modo che l'elemento non mostri ::marker. Lo stile position è impostato su relative, quindi gli pseudo-elementi dei pulsanti successivi possono posizionarsi all'interno dell'intera area occupata 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 in uno spazio 3D per mostrare la prospettiva; ogni elemento dell&#39;elenco non ha più un punto elenco.

Stili degli elementi <button>

L'applicazione di stili ai pulsanti può essere complessa, occorre tenere conto di molti stati e tipi di interazioni. La complessità di questi pulsanti è rapida perché bilancia pseudo-elementi, animazioni e interazioni.

Stili iniziali di <button>

Di seguito sono riportati gli stili fondamentali 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 stili.

Pseudo-elementi pulsante

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

Screenshot del riquadro Elementi di Chrome Devtools con un pulsante con elementi
::before e ::after.

Questi elementi sono fondamentali per mostrare la prospettiva 3D che è stata consolidata. Uno di questi pseudo-elementi viene allontanato dal pulsante e uno viene tirato più vicino all'utente. L'effetto è soprattutto visibile 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 di trasformazione 3D

Inferiore a transform-style è impostato su preserve-3d, quindi gli elementi secondari possono distribuirsi sull'asse z. L'elemento transform è impostato sulla proprietà personalizzata --distance, che verrà aumentata al passaggio del mouse e con lo 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 suggerisce al browser che la proprietà di trasformazione deve essere pronta per la modifica e che è impostata una transizione per le proprietà transform e background-color. Ho notato la differenza di durata: ho pensato che fosse un buon 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 è diffondere i livelli che compongono il pulsante di visualizzazione piatto. A questo scopo, imposta la variabile --distance, inizialmente su 1px. Il selettore mostrato nell'esempio di codice che segue verifica se il pulsante passa il mouse sopra il pulsante o lo stato attivo viene attivato da un dispositivo che dovrebbe visualizzare un indicatore dello stato attivo e se non è stato attivato. In tal caso, applica il CSS per:

  • Applica il colore di sfondo al passaggio del mouse.
  • Aumenta la distanza .
  • Aggiungi un effetto di rimbalzo in caso di rimbalzo.
  • Sfalsa le transizioni dello pseudo-elemento.
.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 molto utile per la preferenza di movimento reduced. Gli elementi superiore e inferiore mostrano l'effetto in modo elegante e discreto.

Piccoli miglioramenti con JavaScript

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

Tasti freccia di supporto

Il tasto Tab consente di navigare nel menu, ma mi aspetterei che il d-pad o i joystick spostino lo stato attivo su un gamepad. La libreria roving-ux spesso utilizzata per le interfacce GUI Challenge gestisce i tasti freccia al posto nostro. Il codice riportato di seguito indica alla libreria di bloccare lo stato attivo all'interno di .threeD-button-set e inoltrarlo al pulsante secondario.

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 al posto del mouse potresti 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 relativa alla preferenza di movimento dell'utente. Inoltre, durante la configurazione, archivia in memoria il componente dell'elenco di pulsanti con querySelector e memorizza nella cache i limiti dell'elemento in menuRect. Utilizza questi limiti per determinare l'offset della 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 del mouse x e y 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 scatola 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, controlla lo spostamento del mouse, passa la posizione alla funzione getAngles() e utilizza i valori delta come stili di proprietà personalizzati. Ho diviso per 20 per riempire il delta e renderlo meno nervoso. Potrebbe esserci un modo migliore per farlo. Se ricordi dall'inizio, mettiamo gli oggetti --x e --y al centro di una funzione clamp(), per evitare che la posizione del mouse ruoti 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'era una cosa da fare quando si provava il menu del gioco in altre modalità e lingue di scrittura.

Gli elementi <button> hanno uno stile !important per writing-mode nel foglio di stile dello user agent. Ciò significava che l'HTML del menu del gioco doveva cambiare per adeguarlo 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 ho fatto, come faresti‽ 🙂 Puoi aggiungere l'interazione con l'accelerometro al menu, in modo che il riquadro del telefono ruoti il menu? Possiamo migliorare l'esperienza senza movimento?

Diversificaamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, twittami con i link e io la aggiungerò alla sezione dei remix della community qui sotto.

Remix della community

Ancora niente da visualizzare qui.