Podstawowe informacje o tym, jak stworzyć elastyczne, elastyczne i łatwo dostępne menu gier 3D.
W tym poście chcę pokazać, jak stworzyć komponent menu gry 3D. Zobacz prezentację.
Jeśli wolisz film, oto wersja tego posta w YouTube:
Przegląd
Gry wideo często mają kreatywne, nietypowe menu, animowane i w 3D. W nowych grach AR/VR to menu popularne jest właśnie w tych grach, które sprawiają wrażenie unoszących się w przestrzeni kosmicznej. Dziś przypomnimy podstawy tego efektu, ale uzupełnimy go o adaptacyjną kolorystykę i dostosowujemy go do potrzeb użytkowników, którzy preferują ograniczenie ruchu.
HTML
Menu gry to lista przycisków. Najlepszym sposobem odzwierciedlenia tego w kodzie HTML jest następujący:
<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>
Lista przycisków będzie dobrze widoczna dla technologii czytników ekranu i będzie działać bez JavaScriptu ani CSS.
CSS
Styl listy przycisków dzieli się na te ogólne kroki:
- Konfiguruję właściwości niestandardowe.
- Układ flexbox.
- Własny przycisk z ozdobnymi pseudoelementami.
- Umieszczanie elementów w przestrzeni 3D.
Omówienie właściwości niestandardowych
Właściwości niestandardowe pomagają identyfikować wartości, nadając im rozpoznawalne nazwy, które wyglądają losowo. Pozwala to uniknąć powtarzania kodu i udostępniania wartości przez elementy podrzędne.
Poniżej znajdziesz zapytania o media zapisane jako zmienne CSS, znane też jako niestandardowe media. Są one globalne i będą używane w różnych selektorach, aby kod był zwięzły i czytelny. Komponent menu gry korzysta z preferencji ruchu, schematu kolorów i zakresu kolorów wyświetlacza.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);
Poniższe właściwości niestandardowe zarządzają schematem kolorów i przytrzymują wartości pozycji kursora, aby menu gry było interaktywne i najeżdżało na nie kursorem. Nazwy właściwości niestandardowych ułatwiają czytelność kodu, ponieważ ujawniają przypadek użycia wartości lub przyjazną nazwę wyniku tej wartości.
.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);
}
}
}
Tło w kształcie stożka w jasnym i ciemnym motywie
Jasny motyw ma jasny gradient stożkowy od cyan
do deeppink
, a ciemny – ciemny, subtelny stożkowy gradient. Więcej informacji o tym, co można zrobić przy użyciu gradientów stożkowych, znajdziesz na stronie conic.style.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
Włączanie perspektywy 3D
Aby elementy pojawiły się w przestrzeni 3D strony internetowej, trzeba zainicjować widoczny obszar z perspektywą. Postawiłem perspektywę na element body
i użyłem jednostek widocznego obszaru, by stworzyć styl, który mi się podoba.
body {
perspective: 40vw;
}
Właśnie taki wpływ może wywrzeć ta perspektywa.
Określanie stylu listy przycisków <ul>
Ten element odpowiada za ogólny układ makra listy przycisków, a także za interaktywną i pływającą kartę 3D. Oto sposób, aby to osiągnąć.
Układ grupy przycisków
Flexbox może zarządzać układem kontenera. Zmień domyślny kierunek układu elastycznego z wierszy na kolumny z atrybutem flex-direction
i upewnij się, że każdy element ma taki sam rozmiar jak jego zawartość, zmieniając wartość z stretch
na start
w kolumnie 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;
}
Następnie ustaw kontener jako kontekst przestrzeni 3D i skonfiguruj funkcje CSS clamp()
, by karta nie obracała się poza czytelne obroty. Zwróć uwagę, że środkowa wartość ograniczenia jest właściwością niestandardową. Te wartości --x
i --y
zostaną ustawione przez JavaScript po późniejszej interakcji myszą.
.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)
)
)
;
}
Następnie, jeśli ruch jest prawidłowy u odwiedzającego użytkownika, dodaj wskazówkę do przeglądarki, że przekształcenie tego elementu będzie się stale zmieniać za pomocą will-change
.
Aby włączyć interpolację, ustaw też transition
na przekształceniach. Przejście to nastąpi, gdy mysz wejdzie w interakcję z kartą, umożliwiając płynne przejście do zmian obrotu. Animacja to ciągła animacja pokazująca przestrzeń 3D, w której znajduje się karta, nawet jeśli mysz nie może lub nie wchodzi w interakcję z komponentem.
@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;
}
}
Animacja rotate-y
ustawia tylko środkową klatkę kluczową w miejscu 50%
, bo przeglądarka ustawi domyślny styl 0%
i 100%
elementu. Jest to skrót od naprzemiennych animacji, które muszą zaczynać się i kończyć w tym samym miejscu. To świetny sposób na przedstawienie nieskończonych naprzemiennych animacji.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
Określanie stylu elementów <li>
Każdy element listy (<li>
) zawiera przycisk i jego elementy obramowania. Styl display
zostanie zmieniony i element nie wyświetla ::marker
. Styl position
ma wartość relative
, więc pseudoelementy kolejnych przycisków mogą się umieszczać w całym obszarze zajmowanym przez przycisk.
.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;
}
Określanie stylu elementów <button>
Stylizowanie przycisków może być pracochłonne, trzeba wziąć pod uwagę wiele stanów i typów interakcji. Przyciski te szybko się komplikują dzięki równoważeniu pseudoelementów, animacji i interakcji.
Początkowe style (<button>
)
Poniżej znajduje się lista stylów podstawowych, które będą obsługiwane w innych stanach.
.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;
}
Pseudoelementy przycisku
Obramowanie przycisku to nie tradycyjne obramowanie, tylko pseudoelementy o pozycji bezwzględnej z obramowaniem.
Te elementy mają kluczowe znaczenie w prezentowaniu dotychczasowej perspektywy 3D. Jeden z tych pseudoelementów zostanie odsunięty od przycisku, a jeden – bliżej użytkownika. Efekt jest najbardziej zauważalny w przypadku przycisków u góry i na dole.
.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);
}
}
}
Style przekształceń 3D
Opcja poniżej transform-style
ma wartość preserve-3d
, aby dzieci mogły się znajdować na osi z
. Pole transform
ma ustawioną właściwość niestandardową --distance
, która zmienia się po najechaniu kursorem i zaznaczeniu.
.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));
}
}
Style animacji warunkowych
Jeśli użytkownik wyraża zgodę na ruch, przycisk podpowiada przeglądarce, że właściwość przekształcenia powinna być gotowa do zmiany i ustawione jest przejście we właściwościach transform
i background-color
. Zwróć uwagę na różnicę w czasie trwania.
Wydawało mi się to subtelny, rozłożony efekt.
.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 }
}
}
Style interakcji z najeżdżaniem kursorem i zaznaczeniem
Celem animacji interakcji jest rozłożenie warstw składających się na płaski przycisk. Aby to zrobić, ustaw zmienną --distance
na początku na 1px
. Selektor widoczny w przykładowym kodzie sprawdza, czy urządzenie nie powoduje najechania kursorem albo zaznaczenia przycisku przez urządzenie, które powinno widzieć wskaźnik ostrości, i czy nie został on aktywowany. Jeśli tak jest, korzysta z CSS,
by wykonać te czynności:
- Zastosuj kolor tła po najechaniu kursorem.
- Zwiększ dystans .
- Dodaj efekt łatwego odbijania odbijania.
- Rozłóż przejścia pseudoelementów.
.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 }
}
}
}
Perspektywa 3D nadal świetnie wyglądała w przypadku preferencji ruchu w usłudze reduced
.
Elementy górne i dolne prezentują efekt w subtelny sposób.
Małe ulepszenia w języku JavaScript
Interfejs jest obsługiwany przez klawiatury, czytniki ekranu, pady do gier, dotykowe i myszki, ale dla ułatwienia w kilku sytuacjach możemy dodać kilka drobnych zmian w JavaScripcie.
Dodatkowe klawisze strzałek
Klawisz tabulacji sprawdza się w poruszaniu się po menu, ale powinien obsługiwać pad kierunkowy lub joysticki na padzie do gier. Biblioteka roving-ux często używana w interfejsach GUI Challenge będzie obsługiwać klawisze strzałek. Poniższy kod informuje bibliotekę, aby utrzymać fokus w obrębie elementu .threeD-button-set
i przekierować go na elementy podrzędne przycisków.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interakcja z paralaksą kursora myszy
Śledzenie myszy i pochylanie jej menu ma naśladować interfejsy AR i VR, w których zamiast myszy może znajdować się wirtualny wskaźnik. Może być zabawnie, gdy elementy są zbyt świadome wskaźnika.
Ze względu na to, że jest to niewielka funkcja, za pomocą zapytań określamy preferencje użytkownika dotyczące ruchu. Dodatkowo w ramach konfiguracji zapisz w pamięci komponent listy przycisków w pamięci za pomocą funkcji querySelector
i umieść granice elementu w pamięci podręcznej w menuRect
. Te granice pozwalają określić opóźnienie obrotu karty w zależności od pozycji myszy.
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Następnie potrzebujemy funkcji, która akceptuje pozycje x
i y
myszy oraz zwraca wartość, której możemy użyć, aby obrócić kartę. Ta funkcja korzysta z pozycji kursora myszy, aby określić, po której stronie ramki się znajduje i o ile się na nią znajduje. Wartość delta jest zwracana z funkcji.
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}
}
Na koniec spójrz na ruch myszą, przekaż położenie do naszej funkcji getAngles()
i użyj wartości delta jako stylów właściwości niestandardowych. Podzieliłem delta przez 20, aby zmniejszyć drgania i to może być lepsze rozwiązanie. Jeśli zapamiętasz je od początku, umieszczamy rekwizyty --x
i --y
w środku funkcji clamp()
, aby zapobiec nadmiernym obróceniu karty w nieczytelną pozycję, gdy mysz znajduje się w pozycji myszki.
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`)
})
}
Tłumaczenia i wskazówki dojazdu
Podczas testowania menu gry w różnych trybach pisania i językach wystąpiła pewna niedogodność.
Elementy <button>
mają w arkuszu stylów klienta użytkownika styl !important
dla writing-mode
. Oznaczało to, że trzeba było zmienić kod HTML menu gry, aby dostosować go do pożądanego projektu. Zmiana listy przycisków na listę linków umożliwia właściwościom logicznym zmianę kierunku menu, ponieważ elementy <a>
nie mają stylu !important
obsługiwanego przez przeglądarkę.
Podsumowanie
Wiesz już, jak to robiłem. Jak to zrobisz‽ 🙂 Czy możesz dodać do menu interakcję z akcelerometrem, aby ułożenie kafelków na telefonie spowodowało obrót menu? Czy możemy poprawić doświadczenie bez ruchu?
Stosujmy różne podejścia i poznajmy sposoby budowania obecności w internecie. Przygotuj wersję demonstracyjną, a potem dodam linki do tweetów, a ja dodam ją do poniższej sekcji na temat remiksów na karcie Społeczność.
Remiksy utworzone przez społeczność
Na razie nic tu nie ma.