Tworzenie komponentu menu gry 3D

Podstawowe informacje o tworzeniu elastycznego, adaptacyjnego i łatwo dostępnego menu gry 3D.

W tym poście pokażę, jak można stworzyć element menu gry 3D. Zobacz prezentację.

Demonstracja

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

Przegląd

Gry wideo często przedstawiają kreatywne i nietypowe menu, animowane i w przestrzeni 3D. Jest ono popularne w nowych grach AR i VR, ponieważ w ten sposób menu wygląda na unoszące się w kosmosie. Dzisiaj zajmiemy się podstawami tego efektu, ale z dodatkowymi akcentami i dostosowanym schematem kolorów, który spodoba się użytkownikom, którzy wolą ograniczony ruch.

HTML

Menu gry to lista przycisków. Oto najlepszy sposób odzwierciedlenia tego zjawiska w kodzie 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 prezentować się w technologii czytników ekranu i działa bez JavaScriptu ani CSS.

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

CSS

Styl listy przycisków jest podzielony na następujące ogólne kroki:

  1. Konfiguruję właściwości niestandardowe.
  2. Układ Flexbox.
  3. Własny przycisk z ozdobnymi pseudoelementami.
  4. umieszczanie elementów w przestrzeni 3D,

Omówienie właściwości niestandardowych

Właściwości niestandardowe pomagają rozróżniać wartości przez nadawanie znaczących nazw wartościom, które w przeciwnym razie są losowe, co pozwala uniknąć powtarzania kodu i udostępniania wartości dzieciom.

Poniżej znajdziesz zapytania o media zapisane jako zmienne CSS, określane 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 systemu i zakresu 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 przytrzymują wartości pozycji myszy, aby umożliwić interaktywne najechanie kursorem w menu gry. Nazewnictwo właściwości niestandardowych zwiększa czytelność kodu, ponieważ pomaga ujawnić przypadek użycia danej wartości lub przyjazną nazwę dla 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);
    }
  }
}

Stożkowe tło z jasnym i ciemnym motywem

Jasny motyw ma jasny gradient stożkowy od cyan do deeppink, a ciemny – ciemny, subtelny stożek. Więcej o tym, co można zrobić z gradientami stożkowymi, 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 koloru tła: jasnego i ciemnego.

Włączanie perspektywy 3D

Aby elementy znajdowały się w przestrzeni 3D strony internetowej, musi zostać zainicjowany widoczny obszar z perspektywą. Zdecydowałam się na umieszczenie perspektywy w elemencie body, a jednostki widocznego obszaru do utworzenia stylu, który mi się podobał.

body {
  perspective: 40vw;
}

To właśnie perspektywa może mieć duży wpływ.

Styl listy przycisków <ul>

Odpowiada on za ogólny układ makra listy przycisków, a także za interaktywną kartę pływającą 3D. Oto jak to osiągnąć.

Układ grupy przycisków

Flexbox może zarządzać układem kontenera. Zmień domyślny kierunek Flex z wierszy na kolumny z flex-direction i upewnij się, że każdy element ma rozmiar zawartości, zmieniając wartość z stretch na start dla 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 określ kontener jako kontekst przestrzeni 3D i skonfiguruj funkcje CSS clamp(), aby karta nie wyświetlała się poza czytelnymi rotacjami. Zauważ, że środkowa wartość ograniczenia zakresu to właściwość niestandardowa. Te wartości --x i --y zostaną ustawione z kodu 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 dla użytkownika, dodaj do przeglądarki wskazówkę, że przekształcenie tego elementu będzie stale się zmieniało w elemencie will-change. Dodatkowo włącz interpolację, ustawiając dla przekształceń parametr transition. Przejście nastąpi, gdy mysz będzie wchodzić w interakcję z kartą, co umożliwi płynne przejście do zmian rotacji. Jest 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 środkową klatkę kluczową tylko w punkcie 50%, ponieważ przeglądarka domyślnie ustawi 0% i 100% na domyślny styl elementu. To skrót od animacji, które się zmieniają, a ich początek i koniec muszą się zaczynać i kończyć w tej samej pozycji. To świetny sposób na przedstawienie nieskończonych naprzemiennych animacji.

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

Styl elementów <li>

Każdy element listy (<li>) zawiera przycisk i jego elementy obramowania. Styl display został zmieniony, więc element nie wyświetla ::marker. Styl position jest ustawiony na relative, więc nadchodzące pseudoelementy przycisku mogą się znajdować w całym obszarze odczytywanym 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ą obróconą w przestrzeni 3D w celu pokazania perspektywy – żaden element listy nie ma już punktora.

Styl elementów <button>

Stosowanie stylów może być trudne, ponieważ trzeba uwzględnić wiele stanów i typów interakcji. Szybko się skomplikują z powodu równoważenia pseudoelementów, animacji i interakcji.

Początkowe style: <button>

Poniżej znajdziesz style podstawowe, które będą obsługiwać 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 stylowymi przyciskami.

Pseudoelementy przycisku

Obramowanie przycisku nie jest tradycyjnym obramowaniem, a jedynie pseudoelementy położenia bezwzględnego.

Zrzut ekranu przedstawiający panel elementów w Chrome Devtools Elements z przyciskiem „before” i ::after.

Te elementy są kluczowe w prezentowaniu przyjętej perspektywy 3D. Jeden z tych pseudoelementów zostanie odsunięty od przycisku, a jeden przybliżony do użytkownika. Efekt jest najbardziej widoczny przy przyciskach 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łcenia 3D

Poniżej transform-style jest ustawiony na preserve-3d, więc dzieci mogą się rozmieścić wokół osi z. transform jest ustawiony na właściwość niestandardową --distance, która będzie zwiększana po najechaniu 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));
  }
}

Warunkowe style animacji

Jeśli użytkownik zgadza się na ruch, przycisk informuje przeglądarkę, że właściwość transform powinna być gotowa do zmiany, a przejście jest ustawione dla właściwości transform i background-color. Zwróć uwagę na różnicę w czasie trwania. Mam wrażenie, że daje to przyjemny, subtelny efekt opóźnienia.

.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

Celem animacji interakcji jest nałożenie warstw, które składają się na płaski przycisk. Aby to zrobić, ustaw zmienną --distance na początku wartości 1px. Selektor widoczny w poniższym przykładowym kodzie sprawdza, czy urządzenie, na którym wyświetla się wskaźnik zaznaczenia, znajduje się na nim i czy przycisk nie został aktywowany. W takim przypadku używa CSS do wykonywania tych czynności:

  • Zastosuj kolor tła po najechaniu kursorem.
  • Zwiększ odległość .
  • Dodaj efekt ułatwiania odbijania e-maili.
  • 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 dobrze sprawdzała się w przypadku ruchu w reduced. Górne i dolne elementy pokazują efekt w subtelny sposób.

Niewielkie ulepszenia w języku JavaScript

Interfejs ten można już używać np. za pomocą klawiatur, czytników ekranu, padów do gier, dotyku i myszy, ale możemy z łatwością dodać kilka drobnych zmian w języku JavaScript.

Obsługiwane klawisze strzałek

Klawisz Tab to dobry sposób na poruszanie się po menu, ale powinien on regulować fokus na gamepadzie za pomocą pada kierunkowego lub joysticka. Biblioteka roving-ux często używana w interfejsach GUI Challenge obsługuje za nas klawisze strzałek. Poniższy kod informuje bibliotekę, że ma usuwać zaznaczenie w obrębie elementu .threeD-button-set i przekierowywać go do elementów podrzędnych przycisku.

import {rovingIndex} from 'roving-ux'

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

Interakcja z paralaksą myszą

Śledzenie myszy i przechylanie jej menu ma naśladować interfejsy gier wideo AR i VR, gdzie zamiast myszy może znajdować się wirtualny wskaźnik. Może być świetna zabawa, gdy elementy mają nadmierną świadomość wskaźnika.

To niewielka, dodatkowa funkcja, dlatego interakcja z nią będzie zależeć od preferencji użytkownika dotyczących ruchu. Podczas konfiguracji zapisz komponent listy przycisków w pamięci za pomocą funkcji querySelector i zapisz w pamięci podręcznej granice elementu w: menuRect. Te progi służą do określania przesunięcia obrotu zastosowanego do karty na podstawie 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 potrzebna jest funkcja, która akceptuje pozycje x i y myszy oraz zwraca wartość, której możemy użyć do obrócenia karty. Poniższa funkcja wykorzystuje pozycję myszy, aby określić, w jakiej stronie znajduje się pole i w jakim stopniu. 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 obserwuj ruch myszy, przekazuj pozycję do funkcji getAngles() i używaj wartości delta jako stylów właściwości niestandardowych. Podzieliłem wynik przez 20, żeby wygładzić delta i zmniejszyć jej drgania. Może być lepszy sposób. Jeśli pamiętasz je od początku, umieszczamy właściwości --x i --y w środku funkcji clamp(), dzięki czemu karta nie może zbyt obracać 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 dojazdu

Podczas testowania menu gry w innych trybach pisania i językach coś się nie udało.

Elementy <button> mają styl !important dla writing-mode w arkuszu stylów klienta użytkownika. Oznaczało to konieczność zmiany kodu HTML menu gry, aby dostosować go do oczekiwanego 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 dostarczonego przez przeglądarkę.

Podsumowanie

Wiesz już, jak to zrobiłem. 🙂 Czy możesz dodać do menu interakcję z akcelerometrem, tak aby kafelki na telefonie obróciły menu? Czy możemy poprawić wrażenia z ruchu?

Stwórzmy różne metody i nauczmy się wszystkiego, jak rozwijać się w internecie. Utwórz demonstrację i udostępnię mi linki na Twitterze, a dodam ją do sekcji remiksów w ramach społeczności poniżej.

Remiksy społeczności

Nic tu jeszcze nie ma.