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ẻ cách tạo thành phần trình đơn trò chơi 3D. Hãy dùng thử 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 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 các 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.
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:
- Thiết lập thuộc tính tuỳ chỉnh.
- Bố cục hộp linh hoạt.
- Một nút tuỳ chỉnh có các phần tử giả trang trí.
- Đặ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ẻ 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í của chuột để tạo trình đơn trò chơi 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 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);
}
}
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 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, 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. 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
và --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%
và 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);
}
}
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
đượ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ả 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 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;
}
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, có đường viền.
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 transform
và background-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 khi di chuột và tập trung
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ó, 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 độ 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 thị sai chuột
Việc theo dõi chuột và nghiêng chuột để nghiêng trình đơn nhằm mô phỏng giao diện trò chơi điện tử 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 tôi sẽ đặt hoạt động tương tác phía 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í 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í x
và y
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í chuột để xác định cạnh bên trong 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 xem 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ớ từ đầu, chúng ta đã đặt các thuộc tính --x
và --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 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
Giờ thì bạn đã biết cách tôi thực hiện, còn bạn thì sao‽ 🙂 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!