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 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 tạo thành phần trình đơn trò chơi 3D. Hãy dùng thử bản minh hoạ.

Bản minh hoạ

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

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à khác thường, có ảnh động và trong không gian 3D. Đây là một tính năng phổ biến trong các trò chơi AR/VR mới để làm cho trình đơn có vẻ như đang trôi nổi trong không gian. Hôm nay, chúng ta sẽ tạo lại những yếu tố cơ bản của hiệu ứng này nhưng thêm vào đó là bảng phối màu thích ứng và các biện pháp đ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 để biểu thị điều này trong HTML là 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 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 đầu dòng trông rất chung chung với các nút thông thường làm mục.

CSS

Việc tạo kiểu cho danh sách nút được chia thành các bước cấp cao sau:

  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 phần tử 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ị trông ngẫu nhiên, tránh mã lặp lại và chia sẻ các 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 biến toàn cục và sẽ được sử dụng trong nhiều bộ chọn để giúp 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, bảng phối màu của hệ thống và khả năng về dải 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í chuột để tạo trình đơn trò chơi 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 mã dễ đọc vì nó cho biết trường hợp sử dụng của giá trị hoặc 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 nón của giao diện sáng và tối

Giao diện sáng có độ dốc hình nón cyan đến deeppink sống động, còn giao diện tối có độ dốc hình nón tối tinh tế. Để xem thêm về những gì bạn có thể làm với hiệu ứng chuyển màu hình nón, 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);
  }
}
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 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 tác động mà quan điểm có thể gây ra.

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à có khả năng tương tác. Sau đây là một cách để thực hiện việc đó.

Bố cục nhóm nút

Flexbox có thể quản lý bố cục vùng chứa. Thay đổi hướng mặc định của flex từ hàng thành cột bằng flex-direction và đảm bảo mỗi mục có kích thước bằng nội dung của mục đó 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 dưới dạng 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 quá mức xoay có thể đọc được. Lưu ý rằng giá trị giữa của điểm 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 người dùng truy cập không gặp vấn đề gì với chuyển động, hãy thêm 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 bằng will-change. Ngoài ra, hãy bật tính năng 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 suôn sẻ sang các thay đổi về độ xoay. Ảnh động này là một ảnh động chạy liên tục minh hoạ không gian 3D mà thẻ nằm trong đó, 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 ở 50% vì trình duyệt sẽ đặt mặc định 0%100% thành kiểu mặc định của phần tử. Đây là viết tắt của ảnh động luân phiên, cần bắt đầu và kết thúc ở cùng một vị trí. Đây là một cách hay để thể hiện ảnh động luân phiên vô hạn.

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

Định 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 đượ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ả lập 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.

Định kiểu cho các phần tử <button>

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

Phần tử giả lập 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 với đường viền.

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

Những thành phần này rất quan trọng trong việc thể hiện 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, còn một phần tử sẽ được kéo lại 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);
    }
  }
}

Phong cách biến đổi 3D

Bên dưới transform-style được đặt thành preserve-3d để các thành phần con có thể tự tạo khoảng trống 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 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 đồng ý với chuyển động, nút này sẽ gợi ý cho trình duyệt rằng thuộc tính biến đổi phải sẵn sàng thay đổi và chuyển đổi được đặt cho thuộc tính transformbackground-color. Hãy lưu ý sự khác biệt về thời lượng, tôi cảm thấy điều này tạo ra hiệu ứng lồng ghép tinh tế và đẹp mắ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 rộng các lớp tạo nên nút xuất hiện phẳng. Thực hiện việc này bằng cách đặt biến --distance ban đầu thành 1px. Bộ chọn hiển thị trong ví dụ 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 tiêu điểm và không được kích hoạt hay không. Nếu có, lớp này sẽ áp dụng CSS để thực hiện các thao tác sau:

  • Áp dụng màu nền khi di chuột.
  • Tăng khoảng cách .
  • Thêm hiệu ứng độ nảy dễ dàng.
  • Tăng dần các hiệu ứ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 }
    }
  }
}

Chế độ phối cảnh 3D vẫn rất gọn gàng cho tuỳ chọn chuyển động reduced. Các phần tử trên cùng và dưới cùng hiển thị hiệu ứng một cách tinh tế và đẹp mắt.

Các điểm cải tiến nhỏ bằng JavaScript

Giao diện này có thể sử dụng được 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 nhẹ bằng JavaScript để dễ dàng xử lý một số trường hợp.

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

Phím tab là một cách tốt để điều hướng trình đơn, nhưng tôi muốn bàn di chuột hoặc cần điều khiển di chuyển tiêu điểm trên tay điều khiển trò chơi. Thư viện roving-ux thường được 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ã bên dưới yêu cầu thư viện thu thập 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 với hiệu ứng thị sai của chuột

Việc theo dõi chuột và nghiêng trình đơn bằng chuột nhằm mô phỏng giao diện trò chơi video AR và VR, trong đó thay vì chuột, bạn có thể có con trỏ ảo. Điều này có thể 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 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 trong 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í của 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 và trả về một giá trị mà chúng ta có thể sử dụng để xoay thẻ. Hàm sau đây sử dụng vị trí con chuột để xác định bên nào của hộp và vị trí của con chuột so với bên đó. Hàm này sẽ trả về 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}
}

Cuối cùng, hãy xem con trỏ di chuyển, truyền vị trí đó đến 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 để tăng delta và làm cho delta í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ớ từ đầu, chúng ta đã đặt các thuộc tính --x--y vào giữa hàm clamp(). Điều này giúp vị trí con chuột không xoay thẻ quá mức thành 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à chỉ dẫn

Có một vấn đề 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 tệp định kiểu của 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 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 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

Giờ bạn đã biết cách tôi thực hiện, bạn sẽ làm như thế nào‽ 🙂 Bạn có thể thêm hoạt động tương tác với gia tốc kế vào trình đơn để xoay trình đơn khi xếp kề điện thoại 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á các phương pháp và tìm hiểu tất cả các cách xây dựng trên web. Hãy tạo bản minh hoạ, gửi đường liên kết cho tôi trên Twitter và tôi sẽ thêm bản minh hoạ đó vào phầ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!