סקירה כללית בסיסית על בניית תפריט תלת-ממדי רספונסיבי, מותאם ונגיש למשחק.
בפוסט הזה אני רוצה לשתף את המחשבות שלי על דרך לבנות רכיב תפריט למשחק תלת-ממדי. כדאי לנסות את ההדגמה.
אם אתם מעדיפים לצפות בסרטון, הנה גרסת 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. המשתנים האלה הם גלובליים וישמשו בכל מיני בוררים כדי לשמור על קוד תמציתי וקריא. רכיב תפריט המשחק משתמש בהעדפות תנועה, בערכת הצבעים של המערכת וביכולות טווח הצבעים של התצוגה.
@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);
}
}
}
רקעים קוניים לעיצוב בהיר וכהה
העיצוב הבהיר כולל cyan עד deeppink מעבר צבע קוני תוסס, והעיצוב הכהה כולל מעבר צבע קוני כהה ועדין. מידע נוסף על האפשרויות של מעברי צבע קוניים זמין במאמר conic.style.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
הפעלת פרספקטיבה תלת-ממדית
כדי שרכיבים יופיעו במרחב התלת-ממדי של דף אינטרנט, צריך לאתחל אזור תצוגה עם פרספקטיבה. בחרתי להוסיף את המאפיין perspective לרכיב 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() כדי לוודא שהכרטיס לא יסתובב מעבר לסיבובים שניתן לקרוא. שימו לב שהערך האמצעי של ה-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 צריך להיות מוכן לשינוי, ומוגדר מעבר למאפיינים 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 כדי להוסיף מרווח לשינוי ולמנוע תנודות חדות מדי, אבל יכול להיות שיש דרך טובה יותר לעשות את זה. אם אתם זוכרים מההתחלה, הוספנו את המאפיינים --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 שמוגדר בדפדפן.
סיכום
עכשיו שאתה יודע איך עשיתי את זה, איך היית עושה את זה‽ 🙂 אפשר להוסיף אינטראקציה עם מד תאוצה לתפריט, כך שהתפריט יסתובב כשמטים את הטלפון? יש דרך לשפר את חוויית השימוש כשאין תנועה?
בואו נגוון את הגישות שלנו ונלמד את כל הדרכים לבנות אתרים. אפשר ליצור סרטון הדגמה, לצייץ לי קישורים, ואוסיף אותו לקטע של רמיקסים מהקהילה שבהמשך!
רמיקסים מהקהילה
עדיין אין מה לראות כאן!