Duyarlı, uyarlanabilir ve erişilebilir bir 3D oyun menüsünün nasıl oluşturulacağına ilişkin temel bir genel bakış.
Bu yayında, 3D oyun menüsü bileşeni oluşturmanın bir yolunu paylaşmak istiyorum. Demoyu deneyin.
Videoyu tercih ediyorsanız bu yayının YouTube sürümünü burada bulabilirsiniz:
Genel Bakış
Video oyunları genellikle kullanıcılara animasyonlu ve 3D bir alanda yaratıcı ve sıra dışı bir menü sunar. Yeni AR/VR oyunlarında menünün havada süzülüyormuş gibi görünmesi popüler bir özelliktir. Bugün bu efektin temel özelliklerini yeniden oluşturacağız. Ancak bu kez, uyarlanabilir renk şeması ve hareketi azaltmayı tercih eden kullanıcılar için de düzenlemeler yapacağız.
HTML
Oyun menüsü, düğmelerin listesidir. Bunu HTML'de göstermenin en iyi yolu aşağıdaki gibidir:
<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>
Düğme listesi, ekran okuyucu teknolojilerine iyi bir şekilde duyurulur ve JavaScript veya CSS olmadan çalışır.
CSS
Düğme listesine stil uygulama işlemi aşağıdaki genel adımlara ayrılır:
- Özel mülkler oluşturma.
- Flexbox düzeni.
- Dekoratif sözde öğeler içeren özel bir düğme.
- Öğeleri 3D uzaya yerleştirme.
Özel özelliklere genel bakış
Özel mülkler, rastgele görünen değerlere anlamlı adlar vererek değerlerin anlamını netleştirmeye yardımcı olur, tekrarlanan koddan ve değerler arasında paylaşımdan kaçınır.
Aşağıda, özel medya olarak da bilinen CSS değişkenleri olarak kaydedilen medya sorguları verilmiştir. Bunlar geneldir ve kodun kısa ve okunaklı kalması için çeşitli seçicilerde kullanılır. Oyun menüsü bileşeni, hareket tercihlerini, sistem renk şemasını ve ekranın renk aralığı özelliklerini kullanır.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);
Aşağıdaki özel mülkler, renk düzenini yönetir ve fareyle üzerine gelindiğinde oyun menüsünü etkileşimli hale getirmek için fare konumsal değerlerini tutar. Özel mülkleri adlandırmak, değerin kullanım alanını veya değerin sonucu için kolay anlaşılır bir ad gösterdiğinden kodun okunabilirliğine yardımcı olur.
.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);
}
}
}
Açık ve koyu temalı arka plan koni arka planları
Açık temada canlı cyan
-deeppink
konik gradya, koyu temada ise koyu ve ince bir konik gradyan vardır. Konik degradelerle neler yapılabileceği hakkında daha fazla bilgi edinmek için conic.style konusuna bakın.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
3D perspektifi etkinleştirme
Öğelerin bir web sayfasının 3D alanında var olabilmesi için perspektif içeren bir görüntü alanının başlatılması gerekir. Perspektifi body
öğesine yerleştirmeyi seçtim ve beğendiğim stili oluşturmak için ekran alanı birimlerini kullandım.
body {
perspective: 40vw;
}
Bu, bakış açısının sahip olabileceği etki türüdür.
<ul>
düğme listesini biçimlendirme
Bu öğe, genel düğme listesi makro düzeninden ve etkileşimli ve 3D yüzen bir kart olmaktan sorumludur. Bunu aşağıdaki şekilde yapabilirsiniz.
Düğme grubu düzeni
Flexbox, kapsayıcı düzenini yönetebilir. flex-direction
kullanarak esnek özelliğin varsayılan yönünü satırlardan sütunlara çevirin ve align-items
için stretch
olan değeri start
olarak değiştirerek her bir öğenin içerik boyutunda olduğundan emin olun.
.threeD-button-set {
/* remove <ul> margins */
margin: 0;
/* vertical rag-right layout */
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2.5vh;
}
Ardından, kapsayıcıyı 3D alan bağlamı olarak oluşturun ve kartın okunabilir rotasyonlardan daha fazla dönmemesini sağlamak için CSS clamp()
işlevleri ayarlayın. Sabitleme için orta değerin özel bir özellik olduğuna dikkat edin. Bu --x
ve --y
değerleri daha sonra fare etkileşimi olduğunda JavaScript'ten ayarlanır.
.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)
)
)
;
}
Ardından, ziyaret eden kullanıcı hareketi kabul ediyorsa tarayıcıya bu öğenin dönüştürme işleminin will-change
ile sürekli değişeceğini belirten bir ipucu ekleyin.
Ayrıca, dönüşümlerde transition
ayarlayarak interpolasyonu etkinleştirin. Bu geçiş, fare kartla etkileşime geçtiğinde gerçekleşir ve döndürme değişikliklerine sorunsuz geçişler sağlar. Animasyon, farenin bileşenle etkileşimde bulunamadığı veya etkileşiminin olmadığı durumlarda bile kartın içinde bulunduğu 3D alanı gösteren, sürekli çalışan bir animasyondur.
@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;
}
}
Tarayıcı, 0%
ve 100%
'i varsayılan olarak öğenin varsayılan stiline ayarlayacağından rotate-y
animasyonu yalnızca orta animasyon karesini 50%
olarak ayarlar. Bu, aynı konumda başlaması ve bitmesi gereken, sırayla oynatılan animasyonlar için kısayoldur. Bu, sonsuz olarak değişen animasyonları ifade etmenin mükemmel bir yoludur.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
<li>
öğelerine stil uygulama
Her liste öğesi (<li>
), düğmeyi ve kenar öğelerini içerir. display
stili değiştirildiğinde öğede ::marker
gösterilmez. position
stili, sonraki düğme sözde öğelerinin kendilerini düğmenin kapladığı alanın tamamında konumlandırabilmesi için relative
olarak ayarlanır.
.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;
}
<button>
öğelerine stil uygulama
Düğmelerin stilini belirlemek zor olabilir. Hesaba katılacak birçok durum ve etkileşim türü vardır. Sözde öğeler, animasyonlar ve etkileşimlerin dengelenmesi nedeniyle bu düğmeler hızla karmaşık hale gelir.
İlk <button>
stilleri
Diğer eyaletleri destekleyecek temel stiller aşağıda verilmiştir.
.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;
}
Düğme sözde öğeleri
Düğmenin kenarları geleneksel kenarlık değildir, kenarlık içeren mutlak konumlu sözde öğelerdir.
Bu öğeler, oluşturulan 3D perspektifi göstermek için çok önemlidir. Bu sözde öğelerden biri düğmeden uzağa, diğeri ise kullanıcıya daha yakın çekilir. Bu etki en çok üst ve alt düğmelerde fark edilir.
.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);
}
}
}
3D dönüştürme stilleri
Çocukların z
ekseninde boşluk bırakabilmeleri için transform-style
altındaki değerin preserve-3d
değerine ayarlanması gerekir. transform
, --distance
özel mülkü olarak ayarlandı. Bu mülk, fareyle üzerine gelindiğinde ve odaklanıldığında artırılacak.
.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));
}
}
Koşullu animasyon stilleri
Kullanıcı hareketi kabul ederse düğme, tarayıcıya dönüştürme özelliğinin değişikliğe hazır olması gerektiğini ve transform
ile background-color
mülkleri için bir geçiş ayarlandığını belirtir. Süredeki farka dikkat edin. Bu, hoş ve zarif bir kademeli efekt oluşturdu.
.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 }
}
}
Fareyle üzerine gelme ve odaklanma etkileşim stilleri
Etkileşim animasyonunun amacı, düz görünen düğmeyi oluşturan katmanları yaymaktır. --distance
değişkenini başlangıçta 1px
değerine ayarlayarak bunu başarabilirsiniz. Aşağıdaki kod örneğinde gösterilen seçici, düğmenin fareyle üzerine gelinip gelinmediğini veya odak göstergesi görmesi gereken bir cihaz tarafından odaklanıp odaklanmadığını ve etkinleştirilip etkinleştirilmediğini kontrol eder. Bu durumda, aşağıdakileri yapmak için CSS uygular:
- Fareyle üzerine gelme arka plan rengini uygulayın.
- Mesafeyi artırın.
- Zıplama yumuşatma efekti ekleyin.
- Sanal öğe geçişlerini kademeli olarak ayarlayın.
.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 }
}
}
}
3D perspektif, reduced
hareket tercihi için yine de çok şıktı.
Üst ve alttaki öğeler, efekti hoş ve ince bir şekilde gösterir.
JavaScript ile küçük iyileştirmeler
Arayüz şu anda klavye, ekran okuyucu, oyun kumandası, dokunmatik ekran ve fare ile kullanılabilir. Ancak bazı senaryoları kolaylaştırmak için JavaScript'e bazı küçük dokunuşlar ekleyebiliriz.
Ok tuşlarını destekleme
Sekme tuşu, menüde gezinmek için iyi bir yöntemdir ancak gamepad'de odağı taşımak için yön çubuğu veya kontrol çubuklarının kullanılmasını beklerdim. GUI Challenge arayüzleri için genellikle kullanılan roving-ux kitaplığı, ok tuşlarını bizim için yönetir. Aşağıdaki kod, kitaplığa odağı .threeD-button-set
içinde yakalamasını ve odağı düğme alt öğelerine yönlendirmesini söyler.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Fare paralaks etkileşimi
Fareyi takip edip menüyü yatırmak, fare yerine sanal bir işaretçiniz olabileceği AR ve VR video oyunu arayüzlerini taklit etmeyi amaçlar. Öğeler işaretçinin çok farkında olduğunda bunu yapmak eğlenceli olabilir.
Bu küçük bir ek özellik olduğundan etkileşimi, kullanıcının hareket tercihine yönelik bir sorgunun arkasına yerleştireceğiz. Ayrıca, kurulumun bir parçası olarak düğme listesi bileşenini querySelector
ile belleğe kaydedin ve öğenin sınırlarını menuRect
üzerinde önbelleğe alın. Fare konumuna göre karta uygulanan döndürme ofsetini belirlemek için bu sınırları kullanın.
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Ardından, fare x
ve y
konumlarını kabul eden ve kartı döndürmek için kullanabileceğimiz bir değer döndüren bir işleve ihtiyacımız var. Aşağıdaki işlev, fare konumunu kullanarak farenin kutunun hangi tarafında olduğunu ve ne kadar uzakta olduğunu belirler. Delta, işlevden döndürülür.
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}
}
Son olarak, farenin hareketini izleyin, konumu getAngles()
işlevimize iletin ve delta değerlerini özel mülk stilleri olarak kullanın. 20'ye bölerek deltayı doldurmayı ve titremeyi azaltıyorum. Bunu yapmanın daha iyi bir yolu olabilir. Başlangıçta --x
ve --y
öğelerini bir clamp()
işlevinin ortasına koyduğumuzu hatırlıyorsanız. Bu, fare konumunun kartı okunamayacak bir konuma döndürmesini önler.
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`)
})
}
Çeviriler ve yol tarifleri
Oyun menüsünü diğer yazım modlarında ve dillerde test ederken bir sorunla karşılaştık.
<button>
öğeleri, kullanıcı aracısı stil sayfasında writing-mode
için !important
stiline sahiptir. Bu nedenle, oyun menüsü HTML'sinin istenen tasarıma uygun şekilde değiştirilmesi gerekiyordu. <a>
öğelerinin tarayıcı tarafından sağlanan bir !important
stili olmadığından, düğme listesini bir bağlantı listesiyle değiştirmek mantıksal özelliklerin menü yönünü değiştirebilmesini sağlar.
Sonuç
Bunu nasıl yaptığımı öğrendiğinize göre, siz nasıl yapardınız? 🙂 Telefonunuzu kaydırarak menüyü döndürmek için menüye ivmeölçer etkileşimi ekleyebilir misiniz? Hareketsiz görüntü deneyimini iyileştirebilir miyiz?
Yaklaşımlarımızı çeşitlendirelim ve web'de uygulama geliştirmenin tüm yollarını öğrenelim. Demo oluşturup beni tweet'le bağlantıları oluşturduğumda bunu aşağıdaki topluluk remiksleri bölümüne ekleyeceğim.
Topluluk remiksleri
Henüz burada görülecek bir şey yok.