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 Wami sposobem na tworzenie 3D komponentu menu gry. Wypróbuj wersję demonstracyjną.

Demonstracja

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 tworzenie menu, które wydaje się unosić w powietrzu. Dziś odtworzymy najważniejsze elementy tego efektu, ale z dodatkowym urozmaiceniem w postaci dostosowania schematu kolorów i dostosowania do użytkowników, którzy wolą ograniczone ruchy.

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 z zwykłymi przyciskami jako elementami.

CSS

Nadawanie stylów liście przycisków można podzielić 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 można uniknąć powtarzania kodu i dzielenia wartości między elementy potomne.

Poniżej znajdują się zapytania o multimedia 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 gradient od koloru cyan 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);
  }
}
Demonstracja zmiany tła między jasnymi a ciemnymi ustawieniami kolorów.

Włączanie perspektywy 3D

Aby elementy istniały w przestrzeni 3D na stronie internetowej, należy zainicjować widok z perspektywą. Zastosowałam perspektywę do elementu body i użyłam jednostek widoku, aby utworzyć 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 jest interaktywną, unoszoną 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ść ogranicznika jest właściwością niestandardową, a wartości --x--y zostaną ustawione z JavaScriptu po interakcji z myszką.

.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 użytkownik zgadza się na ruch, dodaj do przeglądarki podpowiedź, że transformacja tego elementu będzie się stale zmieniać w zależności od will-change. Dodatkowo włącz interpolację, ustawiając wartość transition w transformacjach. Ten przejście nastąpi, gdy mysz będzie oddziaływać na kartę, umożliwiając płynne przechodzenie do zmian 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 został zmieniony, aby element nie wyświetlał się z ::marker. Styl position jest ustawiony na relative, więc nadchodzące pseudoelementy przycisku mogą się pozycjonować w całym obszarze, który zajmuje 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. Te przyciski szybko stają się skomplikowane ze względu na konieczność równoważenia pseudoelementów, animacji i interakcji.

Wstępne style <button>

Poniżej znajdziesz style podstawowe, które obsługują inne stany.

.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 z listą przycisków w perspektywie 3D, tym razem ze stylizowanymi przyciskami

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 są kluczowe w prezentowaniu ustalonej perspektywy 3D. Jedno z tych pseudoelementów zostanie odsunięte od przycisku, a drugie przyciągnięte 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 zgadza się na ruch, przycisk informuje przeglądarkę, że właściwość transform powinna być gotowa do zmiany, a właściwości transformbackground-color mają ustawione przejście. 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 o płaskim wyglądzie. 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, to powoduje to zastosowanie CSS do:

  • Zastosuj kolor tła po najechaniu kursorem.
  • Zwiększ odległość .
  • Dodaj efekt łagodnego odbicia.
  • Przesuń 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 reduced. Elementy u góry i u dołu pokazują efekt w przyjemny, subtelny sposób.

drobne ulepszenia za pomocą JavaScriptu,

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

Obsługa klawiszy strzałek

Klawisz Tab jest dobrym sposobem na poruszanie się po menu, ale spodziewam się, że na kontrolerze sterowanie kursorem będzie możliwe za pomocą panelu kierunkowego lub joysticków. 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 AR i VR, w których zamiast myszy może być używany wskaźnik wirtualny. Może to być zabawne, gdy elementy są bardzo ś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. Na podstawie tych wartości określa się przesunięcie obracania stosowane do karty na podstawie położenia 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 przyjmuje pozycje myszy xy i zwraca wartość, której możemy użyć do obrócenia karty. 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 przez 20, aby zwiększyć wartość delta i ustabilizować ją. Być może istnieje lepszy sposób na to. 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 i językach wystąpił jeden problem.

Elementy <button> mają styl !important dla atrybutu writing-mode w arkuszu stylów użytkownika. 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

Teraz, gdy już wiesz, jak to zrobić, jak Ty to zrobisz? 🙂 Czy możesz dodać do menu interakcję z akcelerometrem, aby obracanie telefonu powodowało obracanie 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