Ringkasan dasar tentang cara membuat menu game 3D yang responsif, adaptif, dan mudah diakses.
Dalam postingan ini, saya ingin membagikan pemikiran tentang cara membuat komponen menu game 3D. Coba demo.
Jika Anda lebih suka video, berikut versi YouTube dari postingan ini:
Ringkasan
Video game sering kali menampilkan menu yang kreatif dan tidak biasa, yang dianimasikan dan dalam ruang 3D. Ini populer di game AR/VR baru untuk membuat menu tampak melayang di ruang. Hari ini kita akan membuat ulang elemen penting dari efek ini, tetapi dengan tambahan skema warna adaptif dan penyesuaian untuk pengguna yang lebih memilih gerakan yang dikurangi.
HTML
Menu game adalah daftar tombol. Cara terbaik untuk merepresentasikannya dalam HTML adalah sebagai berikut:
<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>
Daftar tombol akan diumumkan dengan baik ke teknologi pembaca layar dan berfungsi tanpa JavaScript atau CSS.
CSS
Gaya daftar tombol dibagi menjadi beberapa langkah tingkat tinggi berikut:
- Menyiapkan properti kustom.
- Tata letak flexbox.
- Tombol kustom dengan pseudo-elemen dekoratif.
- Menempatkan elemen ke dalam ruang 3D.
Ringkasan properti kustom
Properti kustom membantu membedakan nilai dengan memberikan nama yang bermakna ke nilai yang terlihat acak, menghindari kode berulang, dan berbagi nilai di antara turunan.
Di bawah ini adalah kueri media yang disimpan sebagai variabel CSS, yang juga dikenal sebagai media kustom. Ini bersifat global dan akan digunakan di berbagai pemilih agar kode tetap ringkas dan mudah dibaca. Komponen menu game menggunakan preferensi gerakan, skema warna sistem, dan kemampuan rentang warna dari layar.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);
Properti kustom berikut mengelola skema warna dan menyimpan nilai posisi mouse untuk membuat menu game interaktif saat diarahkan kursor. Memberi nama properti kustom membantu keterbacaan kode karena mengungkapkan kasus penggunaan untuk nilai atau nama yang mudah dipahami untuk hasil nilai.
.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);
}
}
}
Latar belakang kerucut latar belakang tema terang dan gelap
Tema terang memiliki gradien
konik cyan
hingga deeppink
yang cerah,
sedangkan tema gelap memiliki gradien konik halus yang gelap. Untuk melihat lebih lanjut hal-hal
yang dapat dilakukan dengan gradien konik, lihat conic.style.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
Mengaktifkan perspektif 3D
Agar elemen ada di ruang 3D halaman web, area pandang dengan
perspektif
harus diinisialisasi. Saya memilih untuk menempatkan perspektif pada elemen body
dan menggunakan unit area pandang untuk membuat gaya yang saya sukai.
body {
perspective: 40vw;
}
Ini adalah jenis dampak yang dapat ditimbulkan oleh perspektif.
Menata gaya daftar tombol <ul>
Elemen ini bertanggung jawab atas tata letak makro daftar tombol secara keseluruhan serta menjadi kartu mengambang 3D dan interaktif. Berikut cara melakukannya.
Tata letak grup tombol
Flexbox dapat mengelola tata letak penampung. Ubah arah default flex
dari baris ke kolom dengan flex-direction
dan pastikan setiap item adalah ukuran
kontennya dengan mengubah dari stretch
ke start
untuk 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;
}
Selanjutnya, buat penampung sebagai konteks ruang 3D dan siapkan fungsi clamp()
CSS untuk memastikan kartu tidak berputar di luar rotasi yang dapat dibaca. Perhatikan bahwa nilai tengah untuk clamp adalah properti kustom, nilai --x
dan --y
ini akan ditetapkan dari JavaScript setelah interaksi mouse nanti.
.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)
)
)
;
}
Selanjutnya, jika gerakan tidak masalah bagi pengguna yang berkunjung, tambahkan petunjuk ke browser bahwa
transformasi item ini akan terus berubah dengan
will-change
.
Selain itu, aktifkan interpolasi dengan menetapkan transition
pada transformasi. Transisi
ini akan terjadi saat mouse berinteraksi dengan kartu, sehingga memungkinkan transisi
yang lancar ke perubahan rotasi. Animasi ini adalah animasi yang berjalan terus-menerus
yang menunjukkan ruang 3D tempat kartu berada, meskipun mouse tidak dapat atau
tidak berinteraksi dengan komponen.
@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;
}
}
Animasi rotate-y
hanya menetapkan keyframe tengah di 50%
karena browser akan menetapkan 0%
dan 100%
secara default ke gaya default elemen. Ini
adalah singkatan untuk animasi yang bergantian, yang harus dimulai dan diakhiri pada posisi
yang sama. Ini adalah cara yang bagus untuk mengartikulasikan animasi bergantian tanpa batas.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
Menata gaya elemen <li>
Setiap item daftar (<li>
) berisi tombol dan elemen batasnya. Gaya
display
diubah sehingga item tidak menampilkan
::marker
. Gaya position
ditetapkan ke relative
sehingga pseudo-elemen tombol mendatang dapat memosisikan
dirinya dalam area penuh yang digunakan tombol.
.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;
}
Menata gaya elemen <button>
Menata gaya tombol bisa menjadi pekerjaan yang sulit, karena ada banyak status dan jenis interaksi yang harus diperhitungkan. Tombol ini menjadi kompleks dengan cepat karena menyeimbangkan pseudo-elemen, animasi, dan interaksi.
Gaya <button>
awal
Berikut adalah gaya dasar yang akan mendukung status lainnya.
.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;
}
Pseudo-elemen tombol
Batas tombol bukanlah batas tradisional, melainkan elemen pseudo posisi absolut dengan batas.
Elemen-elemen ini sangat penting dalam menampilkan perspektif 3D yang telah ditetapkan. Salah satu elemen pseudo ini akan didorong menjauh dari tombol, dan satu elemen akan ditarik lebih dekat ke pengguna. Efeknya paling terlihat di tombol atas dan bawah.
.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);
}
}
}
Gaya transformasi 3D
Di bawah transform-style
ditetapkan ke preserve-3d
sehingga turunan dapat mengatur jarak
sendiri pada sumbu z
. transform
ditetapkan ke properti kustom --distance
, yang akan ditingkatkan saat mengarahkan kursor dan
fokus.
.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));
}
}
Gaya animasi bersyarat
Jika pengguna mengizinkan gerakan, tombol akan memberi tahu browser bahwa
properti transformasi harus siap untuk diubah dan transisi ditetapkan untuk
properti transform
dan background-color
. Perhatikan perbedaan durasi, saya merasa hal ini menghasilkan efek berurutan yang halus dan bagus.
.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 }
}
}
Gaya interaksi pengarahan kursor dan fokus
Tujuan animasi interaksi adalah untuk menyebarkan lapisan yang membentuk
tombol yang tampak datar. Lakukan ini dengan menetapkan variabel --distance
,
awalnya ke 1px
. Pemilih yang ditampilkan dalam contoh kode berikut memeriksa
apakah tombol sedang diarahkan atau difokuskan oleh perangkat yang akan melihat
indikator fokus, dan tidak diaktifkan. Jika demikian, CSS akan diterapkan untuk melakukan
hal berikut:
- Terapkan warna latar belakang saat kursor diarahkan.
- Meningkatkan jarak .
- Menambahkan efek kemudahan pantulan.
- Menyusun transisi elemen pseudo secara bertahap.
.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 }
}
}
}
Perspektif 3D masih sangat rapi untuk preferensi gerakan reduced
.
Elemen atas dan bawah menunjukkan efek dengan cara yang halus dan bagus.
Peningkatan kecil dengan JavaScript
Antarmuka ini sudah dapat digunakan dari keyboard, pembaca layar, gamepad, sentuh, dan mouse, tetapi kita dapat menambahkan beberapa sentuhan ringan JavaScript untuk memudahkan beberapa skenario.
Mendukung tombol panah
Tombol tab adalah cara yang bagus untuk menavigasi menu, tetapi saya berharap tombol directional
pad atau joystick untuk memindahkan fokus pada gamepad. Library
roving-ux yang sering digunakan untuk antarmuka
Tantangan GUI akan menangani tombol panah untuk kita. Kode di bawah memberi tahu
library untuk menjebak fokus dalam .threeD-button-set
dan meneruskan fokus ke
turunan tombol.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interaksi paralaks mouse
Melacak mouse dan membuatnya memiringkan menu dimaksudkan untuk meniru antarmuka game video AR dan VR, dengan pointer virtual sebagai pengganti mouse. Hal ini bisa menjadi menyenangkan jika elemen sangat menyadari kursor.
Karena ini adalah fitur tambahan kecil, kita akan menempatkan interaksi di balik kueri
preferensi gerakan pengguna. Selain itu, sebagai bagian dari penyiapan, simpan komponen daftar tombol
ke dalam memori dengan querySelector
dan simpan cache batas elemen ke dalam
menuRect
. Gunakan batas ini untuk menentukan offset rotasi yang diterapkan ke kartu
berdasarkan posisi mouse.
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Selanjutnya, kita memerlukan fungsi yang menerima posisi x
dan y
mouse serta menampilkan
nilai yang dapat kita gunakan untuk memutar kartu. Fungsi berikut menggunakan posisi mouse untuk menentukan sisi kotak yang di dalamnya dan seberapa banyak. Delta
ditampilkan dari fungsi.
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}
}
Terakhir, perhatikan gerakan mouse, teruskan posisi ke fungsi getAngles()
kita,
dan gunakan nilai delta sebagai gaya properti kustom. Saya membagi dengan 20 untuk mengisi
delta dan membuatnya tidak terlalu berkedip, mungkin ada cara yang lebih baik untuk melakukannya. Jika Anda
ingat dari awal, kita menempatkan properti --x
dan --y
di tengah
fungsi clamp()
, hal ini mencegah posisi mouse memutar kartu
terlalu banyak ke posisi yang tidak terbaca.
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`)
})
}
Terjemahan dan rute
Ada satu masalah saat menguji menu game dalam mode penulisan dan bahasa lain.
Elemen <button>
memiliki gaya !important
untuk writing-mode
dalam stylesheet agen
pengguna. Artinya, HTML menu game perlu diubah untuk mengakomodasi desain yang diinginkan. Mengubah daftar tombol menjadi daftar link memungkinkan properti
logis untuk mengubah arah menu, karena elemen <a>
tidak memiliki gaya
!important
yang disediakan browser.
Kesimpulan
Setelah Anda mengetahui cara saya melakukannya, bagaimana Anda melakukannya‽ 🙂 Dapatkah Anda menambahkan interaksi akselerometer ke menu, sehingga menyusun ponsel Anda akan memutar menu? Bisakah kami meningkatkan pengalaman tanpa gerakan?
Mari kita diversifikasi pendekatan dan pelajari semua cara untuk mem-build di web. Buat demo, tweet link-nya, dan saya akan menambahkannya ke bagian remix komunitas di bawah.
Remix komunitas
Belum ada apa-apa di sini.