Tworzenie komponentu menu gry 3D

Podstawowy przegląd tworzenia responsywnego, elastycznego i dostępnego menu gry 3D.

W tym poście chcę podzielić się z Tobą sposobem na tworzenie 3D komponentów menu gry. Wypróbuj wersję demonstracyjną.

Prezentacja

Jeśli wolisz film, oto wersja tego posta w YouTube:

Omówienie

Gry wideo często oferują użytkownikom kreatywne i niezwykłe menu, które jest animowane i wyświetlane w przestrzeni 3D. W przypadku nowych gier AR/VR popularne jest wyświetlanie menu w przestrzeni. 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. Najlepszy sposób przedstawienia tego w HTML:

<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 odczytywana przez czytniki ekranu i działa bez JavaScriptu ani CSS.

bardzo ogólna lista punktowana
ze zwykłymi przyciskami jako elementami.

CSS

Styl listy przycisków dzieli się na te ogólne kroki:

  1. Konfigurowanie właściwości niestandardowych.
  2. Układ Flexbox.
  3. Przycisk niestandardowy z ozdobnymi pseudoelementami.
  4. Umieszczanie elementów w przestrzeni 3D.

Omówienie właściwości niestandardowych

Własne właściwości pomagają rozróżniać wartości, nadając im znaczące nazwy, dzięki czemu unikniesz powtarzania kodu i dzielenia wartości między elementy potomne.

Poniżej znajdują się zapytania dotyczące multimediów zapisane jako zmienne w usłudze porównywania cen, czyli niestandardowe multimedia. 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 dotyczących animacji, schematu kolorów systemu oraz 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);

Te właściwości niestandardowe zarządzają schematem kolorów i przechowują wartości pozycji kursora, aby menu gry było interaktywne po najechaniu kursorem. Nazywanie właściwości niestandardowych ułatwia czytelność kodu, ponieważ ujawniają one przypadek użycia wartości lub przyjazną nazwę wyniku 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ła trójkątne w jasnym i ciemnym motywie

Jasny motyw ma żywy gradientcyan do deeppink, a ciemny motyw ma ciemny subtelny gradient. Więcej informacji o tym, co można zrobić za pomocą gradientów stożkowych, znajdziesz w artykule conic.style.

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

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
Prezentacja zmiany tła między jasnym i ciemnym kolorem.

Włączanie perspektywy 3D

Aby elementy mogły istnieć w przestrzeni 3D na stronie internetowej, należy zainicjować widok 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;
}

To jest rodzaj wpływu, jaki może mieć perspektywa.

Nadawanie stylu liście przycisków <ul>

Ten element odpowiada za ogólny układ makra listy przycisków, a także za interaktywną i wyłanianą kartę 3D. Oto jak to zrobić.

Układ grupy przycisków

Flexbox może zarządzać układem kontenera. Zmień domyślny kierunek elastyczności z wierszy na kolumny za pomocą flex-direction i upewnij się, że każdy element ma rozmiar swoich treści, zmieniając z stretch na start w przypadku 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(), aby zapewnić, że karta nie będzie obracana 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. Dodatkowo włącz interpolację, ustawiając wartość transition w transformacjach. Ten przejście nastąpi, gdy mysz będzie wchodzić w interakcję z kartą, umożliwiając płynne przejścia między zmianami orientacji. Animacja jest animacją ciągłą, która pokazuje 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ą na 50%, ponieważ przeglądarka domyślnie ustawia wartości 0%100% na domyślny styl elementu. Jest to skrót do animacji, które się naprzemiennie wyświetlają i muszą zaczynać się i kończyć w tym samym położeniu. To świetny sposób na tworzenie nieskończonych animacji naprzemiennych.

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

Stylizacja elementów <li>

Każdy element listy (<li>) zawiera przycisk i jego elementy obwiedni. 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;
}

Zrzut ekranu z listą, która została obrócona w przestrzeni 3D, aby pokazać perspektywę. Każda pozycja na liście nie ma już punktora.

Stylizacja elementów <button>

Nadawanie stylów przyciskom może być trudne, ponieważ trzeba uwzględnić 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;
}

Zrzut ekranu przedstawiający listę przycisków w widoku 3D, tym razem z wybranymi stylami.

Pseudoelementy przycisku

Obramowanie przycisku to nie tradycyjne obramowanie, tylko pseudoelementy o pozycji bezwzględnej z obramowaniem.

Zrzut ekranu panelu Elementy w Narzędziach dla programistów w Chrome z przyciskiem zawierającym elementy ::before i ::after.

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 na górze 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 transformacji 3D

W ustawieniu poniżej transform-style jest ustawiona wartość preserve-3d, dzięki czemu dzieci mogą rozmieścić się na osi z. Wartość transform jest ustawiona na wartość właściwości niestandardowej --distance, która będzie zwiększana po najechaniu kursorem i po wybraniu.

.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. Uważam, że dzięki temu udało mi się uzyskać przyjemny, subtelny 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 po najechaniu kursorem i po nakierowaniu

Celem animacji interakcji jest rozłożenie warstw, z których składa się przycisk, który pojawia się jako płaski. Aby to zrobić, ustaw zmienną --distance na 1px. Selektor pokazany w tym przykładzie kodu sprawdza, czy kursor znajduje się nad przyciskiem lub czy urządzenie, na którym wyświetla się wskaźnik fokusu, nie aktywuje go. Jeśli tak, stosuje ona CSS do:

  • Zastosuj kolor tła po najechaniu kursorem.
  • Zwiększ dystans .
  • Dodaj efekt łagodnego odbicia.
  • 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 była nadal bardzo przydatna w przypadku preferencji animacji reduced. Elementy górne i dolne prezentują efekt w subtelny sposób.

Małe ulepszenia w języku JavaScript

Interfejs jest już dostępny za pomocą klawiatur, czytników ekranu, kontrolerów, ekranów dotykowych i myszy, ale możemy dodać trochę kodu JavaScript, aby ułatwić kilka scenariuszy.

Obsługa klawiszy 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, która jest często używana do obsługi interfejsów GUI, będzie obsługiwać za nas klawisze strzałek. Podany niżej kod informuje bibliotekę, aby przechwyciła fokus w komponencie .threeD-button-set i przekazała go do elementów podrzędnych przycisku.

import {rovingIndex} from 'roving-ux'

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

Interakcja z paralaksą myszy

Śledzenie myszy i przechylanie menu ma naśladować interfejsy gier wideo AR i VR, w których zamiast myszy może być używany wskaźnik wirtualny. Może być zabawnie, gdy elementy są zbyt świadome wskaźnika.

Ponieważ jest to niewielka dodatkowa funkcja, interakcji będzie towarzyszyć zapytanie o preferencje dotyczące ruchu użytkownika. W ramach konfiguracji przechowuj też komponent listy przycisków w pamięci za pomocą funkcji querySelector i przechowuj w pamięci podręcznej granice elementu za pomocą funkcji 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ę. Funkcja ta korzysta z pozycji myszy, aby określić, po której stronie pola znajduje się mysz i o ile. Funkcja zwraca wartość delta.

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 obserwuj ruch kursora, przekaż pozycję do funkcji getAngles() i użyj wartości delta jako niestandardowych stylów właściwości. Podzieliłem delta przez 20, aby zmniejszyć drgania i to może być lepsze rozwiązanie. Pamiętasz, że na początku umieszczamy elementy --x--y w środku funkcji clamp(), aby nie można było przesunąć myszy w taki sposób, aby karta znalazła się w nieczytelnej pozycji.

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

Podczas testowania menu gry w innych 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 kod HTML menu gry musiał zostać zmieniony, aby dostosować go do wybranego projektu. Zmiana listy przycisków na listę linków umożliwia zmianę kierunku menu za pomocą właściwości logicznych, ponieważ elementy <a> nie mają stylu !important dostarczanego przez przeglądarkę.

Podsumowanie

Skoro już wiesz, jak to robiłem, jak to działa‽ 🙂 Czy możesz dodać do menu interakcję z akcelerometrem, aby ułożenie kafelków na telefonie obróciło menu? Czy możemy poprawić działanie usługi w przypadku braku ruchu?

Zróżnicujemy nasze podejścia i poznamy wszystkie sposoby tworzenia stron internetowych. Utwórz wersję demonstracyjną, wyślij mi linki, a ja dodam je do sekcji z remiksami społeczności.

Remiksy społeczności

Na razie jest tu pusto