Una panoramica di base su come creare un menu di gioco 3D adattabile, reattivo e accessibile.
In questo post voglio condividere alcune idee su come creare un componente del menu di un 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 nello spazio 3D. Nei nuovi giochi AR/VR è molto comune fare in modo che il menu sembri fluttuare nello spazio. Oggi recrieremo gli elementi essenziali di questo effetto, ma con l'aggiunta di una combinazione di colori adattabile e di opzioni per gli utenti che preferiscono ridurre il movimento.
HTML
Un menu di gioco è un elenco di pulsanti. Il modo migliore per rappresentare questo in HTML è come segue:
<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 viene annunciato correttamente alle tecnologie degli 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 di elementi nello spazio 3D.
Panoramica delle proprietà personalizzate
Le proprietà personalizzate aiutano a distinguere i valori assegnando nomi significativi a valori apparentemente casuali, evitando il codice ripetuto e la condivisione dei valori tra i figli.
Di seguito sono riportate le query sui media salvate come variabili CSS, note anche come supporti personalizzati. Sono globali e verranno utilizzati in vari selettori per mantenere il codice conciso e leggibile. Il componente del menu del 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 memorizzano i valori di posizione del mouse per rendere interattivo il menu del gioco al passaggio del mouse. Assegnare un nome alle proprietà personalizzate contribuisce alla leggibilità del codice in quanto rivela il caso d'uso del valore o un nome facile da ricordare 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 un gradiente conico vivace da cyan
a deeppink
, mentre il tema scuro ha un gradiente conico scuro e delicato. 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 applicare la prospettiva all'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 la prospettiva.
Aggiungere stili all'elenco di pulsanti <ul>
Questo elemento è responsabile del layout complessivo della macro dell'elenco di pulsanti ed è una scheda popup interattiva e 3D. Ecco come fare.
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 relativi contenuti passando 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, definisci 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 intermedio per il limite è una proprietà personalizzata. Questi 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)
)
)
;
}
Poi, se il movimento è accettabile per l'utente che visita il sito, aggiungi un suggerimento al browser che informi che la trasformazione di questo elemento cambierà costantemente con will-change
.
Inoltre, attiva l'interpolazione impostando un transition
sulle trasformazioni. Questa transição si verifica quando il mouse interagisce con la scheda, consentendo transizioni плавные alle modifiche di rotazione. L'animazione è in esecuzione costante
e mostra lo spazio 3D in cui si trova la scheda, anche se un 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 il fotogramma chiave intermedio solo su 50%
, poiché il browser imposterà per impostazione predefinita 0%
e 100%
sullo stile predefinito dell'elemento. Si tratta di una scorciatoia per le animazioni che si alternano e devono iniziare e terminare nella stessa posizione. È un ottimo modo per creare animazioni infinite alternate.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
Definire lo stile 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 un ::marker
. Lo stile position
è impostato su relative
in modo che gli pseudo-elementi del pulsante successivi 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;
}
Definire lo stile degli elementi <button>
Lo stile dei pulsanti può essere un lavoro difficile, in quanto sono da tenere conto di molti stati e tipi di interazione. Questi pulsanti diventano rapidamente complessi a causa del bilanciamento di pseudo-elementi, animazioni e interazioni.
Stili iniziali di <button>
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;
}
Pseudo-elementi pulsante
I bordi del pulsante non sono bordi tradizionali, ma pseudo-elementi con bordi in posizione assoluta.
Questi elementi sono fondamentali per mostrare la prospettiva 3D che è stata stabilita. Uno di questi pseudo-elementi verrà allontanato dal pulsante e uno verrà avvicinato all'utente. L'effetto è più evidente nei pulsanti superiore e inferiore.
.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 distribuirsi sull'asse z
. transform
è impostato sulla proprietà personalizzata --distance
, che verrà aumentata al passaggio del mouse e al 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 accetta il movimento, il pulsante suggerisce al browser che la proprietà transform deve essere pronta per la modifica e che è impostata una transizione per le proprietà transform
e background-color
. Noti la differenza di durata? Ho pensato che fosse un effetto graduale molto piacevole.
.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 con il passaggio del mouse e lo stato attivo
Lo scopo dell'animazione di interazione è distribuire i livelli che compongono il pulsante con aspetto piatto. Per farlo, imposta inizialmente la variabile --distance
su 1px
. Il selettore mostrato nel seguente esempio di codice controlla se il pulsante è selezionato o se un dispositivo che dovrebbe visualizzare un indicatore di sfarfallamento lo sta passando sopra e non lo sta attivando. In questo caso, applica CSS per eseguire quanto segue:
- Applica il colore di sfondo al passaggio del mouse.
- Aumenta la distanza .
- Aggiungi un effetto di transizione graduale.
- Sposta 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 ancora molto interessante per la preferenza di movimento reduced
.
Gli elementi in alto e in basso mostrano l'effetto in modo discreto e gradevole.
Piccoli miglioramenti con JavaScript
L'interfaccia è già utilizzabile da tastiere, screen reader, gamepad, tocchi 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 pad direzionale o i joystick spostino lo stato attivo su un gamepad. La libreria roving-ux, spesso utilizzata per le interfacce GUI delle sfide, gestirà i tasti freccia per noi. Il codice riportato di seguito indica alla biblioteca di bloccare lo stato attivo in .threeD-button-set
e di inoltrarlo ai pulsanti secondari.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interazione con la parallasse del mouse
Il monitoraggio del mouse e l'inclinazione del menu sono pensati per imitare le interfacce dei videogiochi AR e VR, in cui al posto del mouse potresti avere un cursore virtuale. Può essere divertente quando gli elementi sono estremamente sensibili al cursore.
Poiché si tratta di una piccola funzionalità aggiuntiva, imposteremo l'interazione dietro una query relativa alle preferenze di movimento dell'utente. Inoltre, nell'ambito della configurazione, memorizza il componente dell'elenco di 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)'
)
A questo punto, 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 quale lato della casella si trova all'interno e di quanto. Il valore
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 delle proprietà personalizzate. Ho diviso per 20 per aumentare il valore della
variazione e renderla meno discontinua. Potrebbe esserci un modo migliore per farlo. Se ricordate, all'inizio abbiamo inserito gli elementi --x
e --y
nel mezzo 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
Si è verificato un problema durante il test del menu del gioco in altre modalità di scrittura e in altre lingue.
Gli elementi <button>
hanno uno stile !important
per writing-mode
nello stile
delle pagine dell'agente utente. Ciò significava che il codice HTML del menu del gioco doveva essere modificato per adattarsi al design desiderato. La modifica dell'elenco di 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 la rotazione dello smartphone ruoti il menu? Possiamo migliorare l'esperienza senza movimento?
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, twittami i link e io la aggiungerò alla sezione dei remix della community di seguito.
Remix della community
Ancora nessun elemento da visualizzare.