Una panoramica di base su come creare un menu di gioco 3D reattivo, adattabile e accessibile.
In questo post voglio condividere il mio pensiero su un modo per creare un componente di menu di gioco 3D. Prova la demo.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
I videogiochi spesso presentano agli utenti un menu creativo e insolito, animato e in uno spazio 3D. È una tecnica molto diffusa nei nuovi giochi AR/VR per far sembrare il menu fluttuante nello spazio. Oggi ricreeremo gli elementi essenziali di questo effetto, ma con l'aggiunta di una combinazione di colori adattiva e accorgimenti per gli utenti che preferiscono un movimento ridotto.
HTML
Un menu di gioco è un elenco di pulsanti. Il modo migliore per rappresentare questo 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 annuncia bene alle tecnologie di screen reader e funziona senza JavaScript o CSS.

CSS
Lo stile dell'elenco di pulsanti si suddivide nei seguenti passaggi di alto livello:
- Configurazione delle proprietà personalizzate.
- Un layout flexbox.
- Un pulsante personalizzato con pseudo-elementi decorativi.
- Posizionamento degli elementi nello spazio 3D.
Panoramica delle proprietà personalizzate
Le proprietà personalizzate aiutano a disambiguare i valori assegnando nomi significativi a valori altrimenti dall'aspetto casuale, evitando codice ripetuto e condividendo i valori tra i figli.
Di seguito sono riportate le query supporti salvate come variabili CSS, note anche come media personalizzati. Questi 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, la combinazione di colori del sistema e le funzionalità di gamma 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 contengono i valori posizionali del mouse per rendere interattivo il menu di gioco al passaggio del mouse. Assegnare un nome alle proprietà personalizzate migliora 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 per i temi chiaro e scuro
Il tema chiaro ha una vivace cyan a deeppink sfumatura
conica
mentre il tema scuro ha una sfumatura conica scura e delicata. Per scoprire di più 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);
}
}
Attivare la prospettiva 3D
Affinché gli elementi esistano nello spazio 3D di una pagina web, è necessario inizializzare un viewport con
prospettiva. Ho scelto di inserire la prospettiva nell'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 Perspective.
Stilizzazione dell'elenco dei pulsanti <ul>
Questo elemento è responsabile del layout generale della macro dell'elenco dei pulsanti, oltre a essere una scheda interattiva e mobile 3D. Ecco un modo per farlo.
Layout del gruppo di pulsanti
Flexbox può gestire il layout del contenitore. Modifica la direzione predefinita di flex
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, imposta il contenitore come contesto di spazio 3D e configura le funzioni CSS clamp()
per assicurarti che la scheda non ruoti oltre le rotazioni leggibili. Tieni presente
che il valore centrale del clamp è una proprietà personalizzata. I valori --x e --y
verranno impostati da JavaScript in seguito all'interazione
del mouse.
.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 è accettabile per l'utente in visita, aggiungi un suggerimento al browser che
la trasformazione di questo elemento cambierà costantemente con
will-change.
Inoltre, attiva l'interpolazione impostando un transition sulle trasformazioni. Questa
transizione si verifica quando il mouse interagisce con la scheda, consentendo transizioni fluide
ai cambiamenti di rotazione. L'animazione è un'animazione in esecuzione costante
che mostra lo spazio 3D in cui si trova la scheda, anche se un mouse non può
interagire con il componente o non lo fa.
@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 intermedio su 50%, poiché il browser imposta 0% e 100% sullo stile predefinito dell'elemento. Questo
è un modo abbreviato per indicare le animazioni che si alternano, che devono iniziare e terminare nella stessa
posizione. È un ottimo modo per articolare animazioni alternate infinite.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
Stilizzazione degli elementi <li>
Ogni elemento dell'elenco (<li>) contiene il pulsante e i relativi elementi di bordo. Lo stile
display viene modificato in modo che l'elemento non mostri un
::marker. Lo stile position
è impostato su relative, in modo che gli pseudo-elementi del pulsante imminente possano 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;
}

Stilizzazione degli elementi <button>
Lo stile dei pulsanti può essere un lavoro 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.
Stili iniziali <button>
Di seguito sono riportati gli stili di base che supportano 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;
}

Pseudo-elementi del pulsante
I bordi del pulsante non sono bordi tradizionali, ma pseudo-elementi di posizionamento assoluto con bordi.

Questi elementi sono fondamentali per mostrare la prospettiva 3D che è stata stabilita. Uno di questi pseudo-elementi verrà allontanato dal pulsante, mentre l'altro verrà avvicinato 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 di trasformazione 3D
Sotto transform-style è impostato su preserve-3d, in modo che i bambini possano distanziarsi sull'asse z. transform è impostato sulla proprietà personalizzata --distance, che aumenterà al passaggio del mouse e
alla messa a fuoco.
.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 non ha problemi con il movimento, il pulsante suggerisce al browser che la proprietà
transform deve essere pronta per la modifica e viene impostata una transizione per le proprietà
transform e background-color. Nota la differenza di
durata, mi sembra che crei un bell'effetto sfalsato sottile.
.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 }
}
}
Stili di interazione al passaggio del mouse e con lo stato attivo
Lo scopo dell'animazione dell'interazione è quello di distribuire i livelli che compongono il
pulsante con aspetto piatto. Per farlo, imposta la variabile --distance inizialmente su 1px. Il selettore mostrato nel seguente esempio di codice controlla
se il pulsante è selezionato o se il cursore è passato sopra con un dispositivo che dovrebbe visualizzare un
indicatore di selezione e non viene attivato. In questo caso, applica CSS per eseguire le
seguenti operazioni:
- Applica il colore di sfondo al passaggio del mouse.
- Aumenta la distanza .
- Aggiungi un effetto di rimbalzo.
- Alterna le transizioni degli 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 molto utile per la preferenza di movimento reduced.
Gli elementi superiore e inferiore mostrano l'effetto in modo sottile.
Piccoli miglioramenti con JavaScript
L'interfaccia è già utilizzabile da tastiere, screen reader, gamepad, touch e mouse, ma possiamo aggiungere alcuni tocchi leggeri di JavaScript per semplificare un paio di scenari.
Supporto dei tasti freccia
Il tasto Tab è un ottimo modo per 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, gestirà i tasti freccia per noi. Il codice riportato di seguito indica alla
libreria di bloccare lo stato attivo all'interno di .threeD-button-set e di spostarlo sui
pulsanti secondari.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interazione di parallasse del mouse
Il tracciamento del mouse e l'inclinazione del menu hanno lo scopo di imitare le interfacce dei videogiochi AR e VR, in cui al posto di un mouse potresti avere un puntatore virtuale. Può essere divertente quando gli elementi sono iperconsapevoli del puntatore.
Poiché si tratta di una piccola funzionalità aggiuntiva, l'interazione verrà inserita dietro una query
della preferenza di movimento dell'utente. Inoltre, durante la configurazione, memorizza il componente
dell'elenco dei pulsanti in 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 del mouse x e y e restituisca
un valore che possiamo utilizzare per ruotare la carta. La seguente funzione utilizza la posizione del mouse
per determinare in quale lato della casella si trova e di quanto. La funzione restituisce la differenza.
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à personalizzati. Ho diviso per 20 per aumentare il delta e renderlo meno sensibile, ma potrebbe esserci un modo migliore per farlo. Se ricordi
dall'inizio, abbiamo inserito le proprietà --x e --y al centro di una
funzione clamp(), in questo modo la posizione del mouse non ruota 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
Durante i test del menu di gioco in altre modalità di scrittura e lingue, è emerso un problema.
Gli elementi <button> hanno uno stile !important per writing-mode nel foglio di stile dell'agente utente. Ciò significava che l'HTML del menu di gioco doveva essere modificato per adattarsi
al design desiderato. Se modifichi l'elenco dei pulsanti in un elenco di link, le proprietà logiche
possono cambiare 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 tu‽ 🙂 Puoi aggiungere l'interazione con l'accelerometro al menu, in modo che l'inclinazione dello smartphone ruoti il menu? Possiamo migliorare l'esperienza di assenza di movimento?
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, inviami un tweet con i link e la aggiungerò alla sezione dei remix della community qui sotto.
Remix della community
Ancora nessun elemento da visualizzare.