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

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

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách xây dựng thành phần trình đơn trò chơi 3D. Hãy thử bản minh hoạ.

Bản minh hoạ

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

Tổng quan

Trò chơi điện tử thường cung cấp cho người dùng một trình đơn sáng tạo và độc đáo, dạng ảnh động và trong không gian 3D. Phương pháp 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ư đang lơ lửng trong không gian. Hôm nay, chúng ta sẽ tái tạo các yếu tố cần thiết của hiệu ứng này nhưng với sự tinh tế bổ sung của bảng phối màu thích ứng và các tuỳ chọn điều chỉnh cho những người dùng thích 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 để thể hiện điều 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 với 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 có dấu đầu dòng trông 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 nhỏ thành các bước cấp cao sau đây:

  1. Đang thiết lập thuộc tính tuỳ chỉnh.
  2. Bố cục hộp linh hoạt.
  3. Nút tuỳ chỉnh với 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

Các 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ị trông có vẻ ngẫu nhiên, nhờ đó tránh việc lặp lại mã và dùng chung 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 được gọi là nội dung nghe nhìn tuỳ chỉnh. Đây là các tham số chung và sẽ được dùng trên nhiều bộ chọn để giữ cho mã ngắn gọn và dễ đọc. Thành phần trình đơn trò chơi sử dụng các lựa chọn ưu tiên chuyển động, bảng phối màu của hệ thống và chức năng phạm vi màu của chế độ hiển thị.

@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ó thể tương tác khi di chuột. Việc đặt tên cho các thuộc tính tuỳ chỉnh giúp bạn dễ đọc mã vì điều này cho biết trường hợp sử dụng của giá trị hoặc một tên phù hợp 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 conic trong nền giao diện sáng và tối

Giao diện sáng có hiệu ứng chuyển màu âm thanh từ cyan đến deeppink rực rỡ trong khi giao diện tối có độ dốc conic tối tinh tế. Để xem thêm về những việc có thể làm với hiệu ứng chuyển màu dạng 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);
  }
}
Bản minh hoạ việc 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 phối cảnh vào 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à kiểu từ góc nhìn có thể gây ra tác động.

Tạo kiểu cho 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ể, cũng như là một thẻ nổi 3D và tương tác. Sau đây là một cách để làm được việc đó.

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 mặc định của linh hoạt từ hàng thành cột có flex-direction và đảm bảo mỗi mục đều có kích thước 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, hãy 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 vượt quá 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 với chuột sau này.

.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 chuyển động ổn với người dùng truy cập, hãy thêm một gợi ý vào trình duyệt rằng phép biến đổi của mục này sẽ liên tục thay đổi khi sử dụng will-change. Ngoài ra, hãy bật loại nội suy bằng cách đặt transition trên các phép biến đổi. Quá trình 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à ả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% về 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 thế, cần phải bắt đầu và kết thúc ở cùng một vị trí. Đó là một cách tuyệt vời để sắp xếp rõ ràng các hoạt ảnh xen kẽ vô hạn.

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

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

Mỗi mục trong danh sách (<li>) chứa nút và các thành phần đường viền của nút. Kiểu display được thay đổi để mục không hiển thị ::marker. Kiểu position được đặt thành relative để các phần tử giả của 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 có dấu đầu dòng.

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

Tạo kiểu cho các nút có thể là công việc khó khăn, có rất nhiều trạng thái và loại tương tác cần lưu ý. Các nút này trở nên phức tạp một cách nhanh chóng 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 ở góc nhìn 3D, lần này với các nút được tạo kiểu.

Phần tử giả của nút

Đường viền của nút không phải là đường viền truyền thống, mà là thành phần 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ử của Chrome Devtools với một nút hiển thị có các phần tử
::before và ::after.

Các thành phần này đóng vai trò quan trọng trong việc thể hiện góc nhìn 3D đã có từ lâu. 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 lại gần người dùng hơn. Hiệu ứng đáng chú ý nhất là ở 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

Bên dưới transform-style được thiết lập thành preserve-3d để các thành phần 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. Thuộc tính này sẽ tăng lên khi di chuột và lấy tiêu điểm.

.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 ổn với chuyển động, nút sẽ gợi ý cho trình duyệt rằng thuộc tính biến đổi đã sẵn sàng để thay đổi và hiệu ứng chuyển đổi sẽ được đặt cho các thuộc tính transformbackground-color. Để ý đến sự khác biệt về thời lượng, tôi cảm thấy tính năng này tạo ra một hiệu ứng so le rất 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 di chuột và lấy tiêu điểm

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 xuất hiện phẳng. Hãy thực hiện việc này bằng cách đặt biến --distance, ban đầu là 1px. Bộ chọn hiển thị trong ví dụ về mã sau đây sẽ kiểm tra xem nút có đang được di chuột hoặc lấy tiêu điểm bởi một thiết bị sẽ thấy chỉ báo lấy nét và không được kích hoạt. Nếu có, hệ thống sẽ áp dụng CSS để 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 dễ dàng thoát.
  • Bố trí các quá trình chuyển đổi giả phần tử.
.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 }
    }
  }
}

Phối cảnh 3D vẫn thực sự gọn gàng đối với lựa chọn ưu tiên về 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à đẹp mắt.

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, nhưng chúng ta có thể thêm một số thao tác chạm vào JavaScript để giảm bớt một số trường hợp.

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

Phím tab là một cách hiệu quả để di chuyển trên trình đơn nhưng tôi cho rằng bàn phím di chuyển hoặc cá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 Thử thách GUI 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 giữ tiêu điểm trong .threeD-button-set và chuyển tiếp tiêu điểm đến các nút con.

import {rovingIndex} from 'roving-ux'

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

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

Thao tá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 (AR) và thực tế ảo (VR), trong đó thay vì dùng chuột, bạn có thể có một con trỏ ảo. Sẽ rất thú vị khi các phần tử nhận biết được con trỏ.

Vì đây là một tính năng bổ sung nhỏ, nên chúng ta sẽ đặt tương tác sau một truy vấn tuỳ chọn 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 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 các 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 xem nằm bên trong hộp nào và bao nhiêu. delta được hàm trả về.

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 để bù đắp cho delta và làm cho nó bớt giật, có thể có cách tốt hơn để làm điều đó. Nếu bạn còn nhớ từ đầu, chúng ta đã đặt đạo cụ --x--y vào giữa hàm clamp(). Điều này ngăn vị trí chuột xoay quá mức thẻ thành một 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

Đã xảy ra một vấn đề khi thử nghiệm trình đơn trò chơi ở các chế độ và ngôn ngữ viết 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. Điều này có nghĩa là HTML của trình đơn trò chơi cần được 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 của 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 làm điều đó, bạn sẽ làm như thế nào 🙂 Bạn có thể thêm tương tác gia tốc kế vào menu để ô điện thoại xoay trình đơn không? Chúng tôi có thể cải thiện trải nghiệm không có chuyển động không?

Hãy đa dạng hoá phương pháp tiếp cận của chúng ta và tìm hiểu tất cả các cách xây dựng trên web. Hãy tạo một bản minh hoạ, đường liên kết tweet me và tôi sẽ thêm bản phối lại đó 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ó nội dung nào để xem ở đây!