Xây dựng thành phần trình đơn trò chơi 3D

Tổng quan cơ bản về cách tạo trình đơn trò chơi 3D có tính thích ứng, thích ứng và dễ tiếp cận.

Trong bài đăng này, tôi muốn chia sẻ cách tạo thành phần trình đơn trò chơi 3D. Xem bản minh hoạ.

Bản minh hoạ

Nếu bạn thích xem video hơn, sau đây là phiên bản của bài đăng này trên YouTube:

Tổng quan

Trò chơi điện tử thường hiển thị cho người dùng một trình đơn sáng tạo và khác lạ, dạng ảnh động và trong không gian 3D. Cách này phổ biến trong các trò chơi thực tế tăng cường/thực tế ảo mới để làm cho trình đơn trông như trôi nổi trong không gian. Hôm nay, chúng tôi sẽ tái tạo những yếu tố cơ bản của hiệu ứng này nhưng với sự tinh tế hơn của bảng phối màu thích ứng và tính năng điều chỉnh cho những người dùng muốn giảm chuyển động.

HTML

Trình đơn trò chơi là một danh sách các nút. Cách tốt nhất để trình bày nội dung này trong HTML như sau:

<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>

Danh sách các nút sẽ tự thông báo rõ ràng cho các công nghệ trình đọc màn hình và hoạt động mà không cần JavaScript hoặc CSS.

một danh sách dấu đầu dòng có vẻ rất chung chung với các nút thông thường dưới dạng các mục.

CSS

Việc tạo kiểu cho danh sách nút được chia thành các bước tổng quát sau đây:

  1. Thiết lập thuộc tính tuỳ chỉnh.
  2. Bố cục hộp linh hoạt.
  3. Một nút tuỳ chỉnh có các phần tử giả trang trí.
  4. Đặt các thành phần vào không gian 3D.

Tổng quan về thuộc tính tuỳ chỉnh

Thuộc tính tuỳ chỉnh giúp phân biệt các giá trị bằng cách đặt tên có ý nghĩa cho các giá trị có vẻ ngẫu nhiên khác, tránh sử dụng mã lặp lại và chia sẻ giá trị giữa các phần tử con.

Dưới đây là các truy vấn nội dung nghe nhìn được lưu dưới dạng biến CSS, còn gọi là nội dung nghe nhìn tuỳ chỉnh. Đây là các tập lệnh chung và sẽ được dùng trên nhiều bộ chọn để đảm bảo mã ngắn gọn và dễ đọc. Thành phần trình đơn trò chơi sử dụng lựa chọn ưu tiên về chuyển động, lược đồ màu của hệ thống và chức năng phạm vi màu của màn hình.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);

Các thuộc tính tuỳ chỉnh sau đây quản lý bảng phối màu và giữ các giá trị vị trí của chuột để trình đơn trò chơi có khả năng tương tác khi di chuột. Việc đặt tên thuộc tính tuỳ chỉnh giúp mã dễ đọc vì thuộc tính này cho thấy trường hợp sử dụng của giá trị hoặc một tên thân thiện cho kết quả của giá trị.

.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);
    }
  }
}

Nền hình tròn theo giao diện sáng và tối

Giao diện sáng có độ dốc từ cyan đến deeppink hiệu ứng chuyển màu conic, trong khi giao diện tối có độ dốc màu conic tinh tế. Để xem thêm về những việc có thể làm với hiệu ứng chuyển màu conic, hãy xem conic.style.

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

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
Hình minh hoạ sự thay đổi nền giữa các lựa chọn ưu tiên về màu sáng và tối.

Bật phối cảnh 3D

Để các phần tử tồn tại trong không gian 3D của trang web, bạn cần khởi chạy một khung nhìn có góc nhìn. Tôi chọn đặt phối cảnh trên phần tử body và sử dụng các đơn vị khung nhìn để tạo kiểu mà tôi thích.

body {
  perspective: 40vw;
}

Đây là loại quan điểm tác động có thể có.

Tạo kiểu danh sách nút <ul>

Phần tử này chịu trách nhiệm về bố cục macro danh sách nút tổng thể, đồng thời là một thẻ nổi 3D và có tính tương tác. Sau đây là cách để đạt được điều đó.

Bố cục nhóm nút

Hộp linh hoạt có thể quản lý bố cục vùng chứa. Thay đổi hướng linh hoạt mặc định từ các hàng thành cột bằng flex-direction và đảm bảo mỗi mục đều có kích thước của nội dung bằng cách thay đổi từ stretch thành start cho 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;
}

Tiếp theo, thiết lập vùng chứa làm ngữ cảnh không gian 3D và thiết lập các hàm clamp() CSS để đảm bảo thẻ không xoay ngoài các chế độ xoay dễ đọc. Xin lưu ý rằng giá trị ở giữa của kẹp là một thuộc tính tuỳ chỉnh, các giá trị --x--y này sẽ được đặt từ JavaScript khi tương tác sau đó bằng chuột.

.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)
      )
    )
  ;
}

Tiếp theo, nếu người dùng truy cập chấp nhận chuyển động, hãy thêm gợi ý vào trình duyệt rằng biến đổi của mục này sẽ liên tục thay đổi bằng will-change. Ngoài ra, bật nội suy bằng cách đặt transition trên phép biến đổi. Hiệu ứng chuyển đổi này sẽ xảy ra khi chuột tương tác với thẻ, cho phép chuyển đổi mượt mà sang các thay đổi về chế độ xoay. Ảnh động là một ảnh động chạy liên tục minh hoạ không gian 3D chứa thẻ, ngay cả khi chuột không thể hoặc không tương tác với thành phần.

@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;
  }
}

Ảnh động rotate-y chỉ đặt khung hình chính ở giữa tại 50% vì trình duyệt sẽ mặc định 0%100% thành kiểu mặc định của phần tử. Đây là cách viết tắt của các ảnh động thay đổi, cần bắt đầu và kết thúc ở cùng một vị trí. Đây là một cách hay để tạo hiệu ứng chuyển động xen kẽ vô hạn.

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

Tạo kiểu cho các phần tử <li>

Mỗi mục trong danh sách (<li>) chứa nút và các phần tử đường viền của nút đó. Kiểu display đã thay đổi nên mục không hiển thị ::marker. Kiểu position được đặt thành relative để các phần tử giả nút sắp tới có thể tự định vị trong toàn bộ khu vực mà nút sử dụng.

.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;
}

Ảnh chụp màn hình danh sách được xoay trong không gian 3D để hiển thị phối cảnh và mỗi mục danh sách không còn dấu đầu dòng nữa.

Tạo kiểu cho các phần tử <button>

Nút tạo kiểu có thể là công việc khó khăn, cần giải thích nhiều trạng thái và loại tương tác. Các nút này sẽ nhanh chóng trở nên phức tạp do cân bằng giữa các phần tử giả, ảnh động và hoạt động tương tác.

Kiểu <button> ban đầu

Dưới đây là các kiểu cơ bản sẽ hỗ trợ các trạng thái khác.

.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;
}

Ảnh chụp màn hình danh sách nút ở phối cảnh 3D, lần này với các nút được tạo kiểu.

Phần tử giả dạng nút

Đường viền của nút không phải là đường viền truyền thống mà là các phần tử giả có vị trí tuyệt đối, có đường viền.

Ảnh chụp màn hình bảng điều khiển Phần tử Chrome Devtools với một nút hiển thị có các phần tử ::before và ::after.

Những yếu tố này đóng vai trò quan trọng trong việc hiển thị phối cảnh 3D đã được thiết lập. Một trong các phần tử giả này sẽ được đẩy ra khỏi nút và một phần tử sẽ được kéo gần người dùng hơn. Hiệu ứng này dễ nhận thấy nhất ở các nút trên cùng và dưới cùng.

.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);
    }
  }
}

Kiểu biến đổi 3D

transform-style bên dưới được đặt thành preserve-3d để các phần tử con có thể tự giãn cách trên trục z. transform được đặt thành thuộc tính tuỳ chỉnh --distance, sẽ được tăng lên khi di chuột và lấy nét.

.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));
  }
}

Kiểu ảnh động có điều kiện

Nếu người dùng vẫn chấp nhận chuyển động, nút này sẽ gợi ý cho trình duyệt biết rằng thuộc tính biến đổi phải sẵn sàng thay đổi và hiệu ứng chuyển đổi sẽ được thiết lập cho các thuộc tính transformbackground-color. Hãy chú ý đến sự khác biệt về thời lượng, tôi cảm thấy thời lượng này tạo ra một hiệu ứng so le tinh tế.

.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 }
  }
}

Kiểu tương tác khi di chuột và tập trung

Mục tiêu của ảnh động tương tác là trải các lớp tạo nên nút phẳng. Hãy hoàn thành việc này bằng cách đặt biến --distance, ban đầu thành 1px. Bộ chọn hiển thị trong các mã ví dụ sau đây sẽ kiểm tra để xem nút đang được di chuột hay lấy làm tâm điểm bởi một thiết bị sẽ nhìn thấy chỉ báo lấy nét và không được kích hoạt. Nếu có, CSS sẽ thực hiện những việc sau:

  • Áp dụng màu nền khi di chuột.
  • Tăng khoảng cách .
  • Thêm hiệu ứng nhẹ nhàng.
  • Xoay vòng chuyển đổi phần tử giả.
.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 }
    }
  }
}

Góc nhìn 3D vẫn rất gọn gàng đối với lựa chọn chuyển động reduced. Các phần tử trên cùng và dưới cùng thể hiện hiệu ứng một cách tinh tế và thú vị.

Các cải tiến nhỏ với JavaScript

Bạn đã có thể sử dụng giao diện này từ bàn phím, trình đọc màn hình, tay điều khiển trò chơi, thao tác chạm và chuột. Tuy nhiên, chúng ta có thể thêm một số thao tác đơn giản cho JavaScript để đơn giản hoá một vài tình huống.

Các phím mũi tên hỗ trợ

Phím Tab là một cách hay để di chuyển trình đơn, nhưng tôi muốn bàn phím di chuyển hoặc cần điều khiển sẽ di chuyển tiêu điểm trên tay điều khiển trò chơi. Thư viện roving-ux thường dùng cho giao diện GUI Thử thách sẽ xử lý các phím mũi tên cho chúng ta. Mã dưới đây yêu cầu thư viện bẫy tiêu điểm trong .threeD-button-set và chuyển tiếp tiêu điểm đến nút con.

import {rovingIndex} from 'roving-ux'

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

Tương tác thị sai chuột

Việc theo dõi chuột và nghiêng trình đơn nhằm mô phỏng giao diện trò chơi điện tử thực tế tăng cường và thực tế ảo, trong đó bạn có thể dùng một con trỏ ảo thay vì dùng chuột. Điều này có thể thú vị khi các phần tử nhận biết rõ ràng về con trỏ.

Vì đây là một tính năng bổ sung nhỏ, nên chúng tôi sẽ đặt hoạt động tương tác sau một truy vấn về lựa chọn ưu tiên về chuyển động của người dùng. Ngoài ra, trong quá trình thiết lập, hãy lưu trữ thành phần danh sách nút vào bộ nhớ bằng querySelector và lưu các giới hạn của phần tử vào bộ nhớ đệm vào menuRect. Sử dụng các giới hạn này để xác định độ lệch xoay áp dụng cho thẻ dựa trên vị trí chuột.

const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

Tiếp theo, chúng ta cần một hàm chấp nhận vị trí xy của chuột, đồng thời trả về một giá trị có thể sử dụng để xoay thẻ. Hàm sau đây sử dụng vị trí chuột để xác định cạnh nào của hộp và giá trị của nó. delta được trả về từ hàm.

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}
}

Cuối cùng, hãy quan sát thao tác di chuyển chuột, truyền vị trí vào hàm getAngles() và sử dụng các giá trị delta làm kiểu thuộc tính tuỳ chỉnh. Tôi đã chia cho 20 để đệm delta và làm cho nó ít giật hơn, có thể có cách tốt hơn để làm việc đó. Nếu bạn còn nhớ ngay từ đầu, chúng ta sẽ đặt thuộc tính --x--y ở giữa hàm clamp() để ngăn vị trí chuột xoay thẻ quá mức đến vị trí không đọc được.

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`)
  })
}

Bản dịch và hướng dẫn

Có một lỗi xảy ra khi kiểm thử trình đơn trò chơi ở các chế độ viết và ngôn ngữ khác.

Các phần tử <button> có kiểu !important cho writing-mode trong biểu định kiểu tác nhân người dùng. Tức là HTML của trình đơn trò chơi cần thay đổi để phù hợp với thiết kế mong muốn. Việc thay đổi danh sách nút thành danh sách đường liên kết sẽ cho phép các thuộc tính logic thay đổi hướng trình đơn, vì các phần tử <a> không có kiểu !important do trình duyệt cung cấp.

Kết luận

Bây giờ, bạn đã biết cách tôi thực hiện điều đó, bạn sẽ như thế nào‽ 🙂 Bạn có thể thêm tương tác gia tốc kế vào trình đơn, để điện thoại của bạn xoay trình đơn không? Chúng tôi có thể cải thiện trải nghiệm không chuyển động không?

Hãy đa dạng hoá phương pháp tiếp cận và tìm hiểu tất cả các cách xây dựng ứng dụng trên web. Tạo một bản minh hoạ, tweet cho tôi các đường liên kết và tôi sẽ thêm vào phần bản phối lại của cộng đồng ở bên dưới!

Bản phối lại của cộng đồng

Chưa có gì để xem ở đây!