סקירה כללית בסיסית על יצירת תפריט משחק תלת-ממדי רספונסיבי, מותאם ונגיש.
בפוסט הזה אני רוצה לשתף אתכם במחשבה על דרך לפיתוח רכיב בתפריט משחקים בתלת-ממד. נסו את ההדגמה.
אם אתם מעדיפים סרטון, הנה גרסה של הפוסט הזה ב-YouTube:
סקירה כללית
במשחקי וידאו, המשתמשים מוצגים לעיתים קרובות לתפריט יצירתי ויוצא דופן, מונפש ומרחבי תלת-ממד. במשחקי AR/VR חדשים, נהוג ליצור תפריט שנראה כאילו הוא צף בחלל. היום נציג את העיקר של האפקט הזה, אבל עם תוספת של ערכת צבעים דינמית והתאמות למשתמשים שמעדיפים תנועה מופחתת.
HTML
תפריט משחק הוא רשימת לחצנים. הדרך הטובה ביותר לייצג את זה ב-HTML היא:
<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>
רשימה של לחצנים תקריא את עצמה בצורה טובה לטכנולוגיות של קוראי מסך, ותעבוד ללא JavaScript או CSS.
CSS
עיצוב רשימת הלחצנים מחולק לשלבים הבאים ברמה גבוהה:
- הגדרת מאפיינים מותאמים אישית.
- פריסת Flexbox.
- לחצן בהתאמה אישית עם פסאודו-רכיבים דקורטיביים.
- מיקום רכיבים במרחב תלת-ממדי.
סקירה כללית על מאפיינים מותאמים אישית
מאפיינים מותאמים אישית עוזרים להבדיל בין ערכים על ידי מתן שמות משמעותיים לערכים שנראות אקראיים, בלי צורך בקוד חוזר ובשיתוף ערכים עם ילדים.
בהמשך מפורטות שאילתות מדיה שנשמרו כמשתני CSS, שנקראים גם מדיה בהתאמה אישית. אלה ערכים גלובליים, והם ישמשו בבוררים שונים כדי לשמור על הקוד תמציתי וקריא. רכיב תפריט המשחק משתמש בהעדפות התנועה, בערכת הצבעים וביכולות של טווח הצבעים של המסך.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);
המאפיינים המותאמים אישית הבאים מנהלים את ערכת הצבעים ומחזיקים את ערכי המיקום של העכבר כדי שתפריט המשחק יהיה אינטראקטיבי כשמעבירים את העכבר מעליו. מתן שמות למאפיינים מותאמים אישית עוזר לקריאות הקוד, כי הוא חושף את התרחיש לדוגמה שבו נעשה שימוש בערך, או שם ידידותי לתוצאה של הערך.
.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);
}
}
}
רקעים חצי-אליפטיים בעיצוב בהיר ועיצוב כהה
בעיצוב הבהיר יש שיפוע conic תוסס מ-cyan
ל-deeppink
, ובעיצוב הכהה יש שיפוע conic עדין כהה. מידע נוסף על מה שאפשר לעשות עם מעברי צבע אליפסואידיים זמין במאמר conic.style.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
הפעלת נקודת מבט תלת-ממדית
כדי שרכיבים יופיעו במרחב התלת-ממדי של דף אינטרנט, צריך לאתחל אזור תצוגה עם נקודת מבט. בחרתי להציב את הפרספקטיבה ברכיב body
, והשתמשתי ביחידות של חלון התצוגה כדי ליצור את הסגנון שרציתי.
body {
perspective: 40vw;
}
זהו סוג ההשפעה שיכולה להיות לנקודת המבט.
עיצוב רשימת הלחצנים <ul>
האלמנט הזה אחראי על הפריסה הכוללת של רשימת הלחצנים ברמת המאקרו, וגם על כרטיס צף אינטראקטיבי תלת-ממדי. כך עושים את זה.
פריסת קבוצת לחצנים
Flexbox יכול לנהל את הפריסה של הקונטיינר. משנים את כיוון ברירת המחדל של flex משורות לעמודות באמצעות flex-direction
, ומוודאים שכל פריט בגודל התוכן שלו על ידי שינוי הערך של align-items
מ-stretch
ל-start
.
.threeD-button-set {
/* remove <ul> margins */
margin: 0;
/* vertical rag-right layout */
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2.5vh;
}
בשלב הבא, מגדירים את הקונטיינר כקונטקסט של מרחב תלת-ממדי ומגדירים פונקציות CSS מסוג clamp()
כדי לוודא שהכרטיס לא יתרווח מעבר לתנועות שאפשר לקרוא. שימו לב שהערך האמצעי של ההצמדה הוא מאפיין מותאם אישית. הערכים האלה של --x
ו---y
יוגדרו מ-JavaScript אחרי אינטראקציה עם העכבר מאוחר יותר.
.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)
)
)
;
}
בשלב הבא, אם התנועה מתאימה למשתמש שמבקר, מוסיפים רמז לדפדפן שהטרנספורמציה של הפריט הזה משתנה כל הזמן בעזרת will-change
.
בנוסף, מפעילים את האינטרפולציה על ידי הגדרת transition
בטרנספורמציות. המעבר יתבצע כשהעכבר מקיים אינטראקציה עם הכרטיס, וכך אפשר לעבור בצורה חלקה לשינויים בסיבוב. האנימציה פועלת באופן קבוע וממחישה את השטח התלת-ממדי שבו נמצא הכרטיס, גם אם העכבר לא יכול או לא יכול לנהל אינטראקציה עם הרכיב.
@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;
}
}
אנימציית rotate-y
מגדירה רק את תמונה המפתח האמצעית ב-50%
, כי הדפדפן מגדיר כברירת מחדל את 0%
ואת 100%
לסגנון ברירת המחדל של הרכיב. זוהי קיצור דרך לאנימציות שמחליפות זו את זו, והן צריכות להתחיל ולסתיים באותה מיקום. זו דרך מצוינת להציג אנימציות לסירוגין ללא הגבלה.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
עיצוב הרכיבים <li>
כל פריט ברשימה (<li>
) מכיל את הלחצן ואת רכיבי השוליים שלו. הסגנון display
השתנה כך שבפריט לא מוצג ::marker
. הסגנון position
מוגדר ל-relative
כדי שפסאודו-האלמנטים הבאים של הלחצן יוכלו למקם את עצמם בתוך האזור המלא שהלחצן תופס.
.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>
עיצוב לחצנים יכול להיות משימה קשה, כי יש הרבה מצבים וסוגים של אינטראקציה שצריך להביא בחשבון. הלחצנים האלה הופכים למסובכים במהירות בגלל האיזון בין רכיבי פסאודו, אנימציות ואינטראקציות.
<button>
סגנונות ראשוניים
בהמשך מפורטים הסגנונות הבסיסיים שתומכים בסטטוסים האחרים.
.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;
}
פסאודו-אלמנטים של לחצנים
השוליים של הלחצן הם לא שוליים מסורתיים, אלא אלמנטים פסאודו עם שוליים במיקום מוחלט.
הרכיבים האלה חיוניים להצגת נקודת המבט התלת-ממדית שהוגדרה. אחד מהפסאודו-רכיבים האלה יודחק מהלחצן, והשני יימשך קרוב יותר למשתמש. ההשפעה בולטת במיוחד בלחצנים העליונים והתחתונים.
.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);
}
}
}
סגנונות של טרנספורמציות תלת-ממדיות
מתחת ל-transform-style
מוגדר preserve-3d
כדי שהילדים יוכלו לפזר את עצמם בציר z
. הערך של transform
מוגדר למאפיין המותאם אישית --distance
, והוא יגדל בהעברת העכבר מעל הרכיב והתמקדות בו.
.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));
}
}
סגנונות אנימציה מותנים
אם המשתמש מסכים לתנועה, הלחצן נותן לגלישה רמוזות לכך שמאפיין הטרנספורמציה צריך להיות מוכן לשינוי, ומוגדר מעבר למאפיינים transform
ו-background-color
. שימו לב להבדל באורך, לדעתי זה יוצר אפקט מרווח עדין ונעים.
.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 }
}
}
סגנונות אינטראקציה של העברת עכבר והתמקדות
המטרה של אנימציית האינטראקציה היא לפזר את השכבות שמהן מורכב הלחצן השטוח שמופיע. כדי לעשות זאת, מגדירים את המשתנה --distance
, בהתחלה ל-1px
. הבורר שמוצג בדוגמת הקוד הבאה בודק אם המכשיר שמוגדר להציג אינדיקטור של מיקוד מעביר את העכבר מעל הלחצן או מתמקד בו, אבל לא מפעיל אותו. אם כן, הוא מחיל CSS כדי לבצע את הפעולות הבאות:
- מחילים את צבע הרקע של העברת העכבר.
- מגדילים את המרחק.
- הוספת אפקט של קלי קלות.
- מניעים את המעברים המדומים.
.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 }
}
}
}
הפרספקטיבה התלת-ממדית עדיין הייתה מאורגנת למדי להעדפת התנועה של reduced
.
הרכיבים העליונים והתחתונים מציגים את האפקט בצורה עדינה ונעימה.
שיפורים קטנים באמצעות JavaScript
אפשר להשתמש בממשק ממקלדות, קוראי מסך, גיימפאד, מסך מגע ועכבר, אבל אפשר להוסיף כמה נגיעות קלות של JavaScript כדי להקל על כמה תרחישים.
מקשי החיצים תומכים
מקש ה-Tab הוא דרך טובה לנווט בתפריט, אבל ציפיתי שהמקלדת או הג'ויסטיקים יזיזו את המיקוד לבקר משחקים. הספרייה roving-ux, שמשמשת לעתים קרובות לממשקי GUI של אתגרים, תעזור לנו לטפל במקשות החיצים. הקוד הבא מורה לספרייה לקבץ את המיקוד ב-.threeD-button-set
ולהעביר אותו לילדים של הלחצן.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
אינטראקציה של תזוזת העכבר
המעקב אחרי העכבר וההטיה שלו כדי להזיז את התפריט נועדו לחקות ממשקי משחקי וידאו ב-AR וב-VR, שבהם במקום עכבר יכול להיות סמן וירטואלי. יכול להיות שזה יהיה כיף אם הרכיבים יהיו מודעים מאוד לסמן.
מכיוון שזו תכונה נוספת קטנה, נוסיף את האינטראקציה לשאילתה לגבי העדפות התנועה של המשתמש. כמו כן, כחלק מההגדרה, שומרים את הרכיב של רשימת הלחצנים בזיכרון באמצעות querySelector
ומאחסנים את גבולות הרכיב במטמון באמצעות menuRect
. משתמשים בגבולות האלה כדי לקבוע את ההיסט של הסיבוב שיוחל על הכרטיס על סמך מיקום העכבר.
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
בשלב הבא, נצטרך פונקציה שתקבל את המיקומים x
ו-y
של העכבר ותחזיר ערך שאפשר להשתמש בו כדי לסובב את הכרטיס. הפונקציה הבאה משתמשת במיקום העכבר כדי לקבוע באיזה צד של התיבה הוא נמצא ובכמה. הפונקציה מחזירה את הערך של הדלתה.
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}
}
לבסוף, בודקים את תנועת העכבר, מעבירים את המיקום לפונקציה getAngles()
ומשתמשים בערכי הדלתה כסגנונות מותאמים אישית של נכסים. חילקתי ב-20 כדי להגדיל את הערך של הדלתה ולמנוע תנודות חדות, יכול להיות שיש דרך טובה יותר לעשות את זה. אם זכור לכם מההתחלה, שמנו את ה-props --x
ו---y
באמצע פונקציית clamp()
. כך מיקום העכבר לא גורם לסיבוב יתר של הכרטיס למצב שלא ניתן לקרוא אותו.
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`)
})
}
תרגומים ומסלולים
גילינו בעיה אחת כשבדקנו את תפריט המשחק בשפות ובמצב כתיבה אחרים.
לרכיבי <button>
יש סגנון !important
עבור writing-mode
בגיליון הסגנונות של סוכן המשתמש. לכן, היה צריך לשנות את ה-HTML של תפריט המשחק כדי להתאים אותו לעיצוב הרצוי. שינוי רשימת הלחצנים לרשימת קישורים מאפשר לנכסים לוגיים לשנות את כיוון התפריט, כי לרכיבי <a>
אין סגנון !important
שסופק על ידי הדפדפן.
סיכום
עכשיו אחרי שסיפרתי לך איך עשיתי את זה, איך היית עושה את זה? 🙂 אפשר להוסיף לממשק אינטראקציה עם תאוצה למסך, כך ששינוי המצב של הטלפון יביא לתנועה של התפריט? איך אפשר לשפר את חוויית השימוש במצב ללא תנועה?
נרחיב את הגישות שלנו ונלמד את כל הדרכים לפיתוח באינטרנט. אפשר ליצור הדגמה, לשלוח ציוץ לי ולהוסיף אותה לקטע 'רמיקסים של הקהילה' בהמשך!
רמיקסים של הקהילה
אין כאן שום דבר עדיין לראות!