כשאתה מחבר בקר, אתה מועך כפתורים, מזיז את המקלות, מושך את ההדקים... וכמפתח, אתה לא רואה שום דבר מזה. הדפדפן קולט את זה, בטח, אבל אלא אם אתה רושם מספרים בקונסולה, זה בלתי נראה. זה כאב הראש עם ה-API של Gamepad. זה קיים כבר שנים, וזה למעשה די חזק. אתה יכול לקרוא כפתורים, מקלות, טריגרים, עבודות. אבל רוב האנשים לא נוגעים בזה. מַדוּעַ? כי אין משוב. אין פאנל בכלי מפתחים. אין דרך ברורה לדעת אם הבקר בכלל עושה מה שאתה חושב. זה מרגיש כמו לטוס עיוור. זה הציק לי מספיק כדי לבנות כלי קטן: Gamepad Cascade Debugger. במקום לבהות בפלט הקונסולה, אתה מקבל תצוגה חיה ואינטראקטיבית של הבקר. לחץ על משהו והוא מגיב על המסך. ועם CSS Cascade Layers, הסגנונות נשארים מסודרים, כך שיהיה נקי יותר לנפות באגים. בפוסט זה, אני אראה לך מדוע איתור באגים בבקרים הוא כל כך כאב, כיצד CSS עוזר לנקות אותו, וכיצד אתה יכול לבנות איתור באגים ויזואלי לשימוש חוזר עבור הפרויקטים שלך.
גם אם אתה מסוגל לרשום את כולם, אתה תסתיים במהירות עם דואר זבל בלתי קריא במסוף. לדוגמה: [0,0,1,0,0,0.5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
אתה יכול לדעת על איזה כפתור לחצו? אולי, אבל רק אחרי מאמץ את העיניים ופספס כמה כניסות. אז, לא, איתור באגים לא בא בקלות כשזה מגיע לקריאת קלט. בעיה 3: חוסר מבנה גם אם תחבר מכשיר חזותי מהיר, סגנונות יכולים להסתבך במהירות. מצבי ברירת מחדל, פעילים וניפוי באגים יכולים לחפוף, וללא מבנה ברור, ה-CSS שלך הופך שביר וקשה להרחבה. שכבות CSS Cascade יכולות לעזור. הם מקבצים סגנונות ל"שכבות" שמסודרות לפי עדיפות, אז אתה מפסיק להילחם בספציפיות ולנחש, "למה סגנון ניפוי הבאגים שלי לא מוצג?" במקום זאת, אתה שומר על חששות נפרדים:
בסיס: המראה הראשוני הסטנדרטי של הבקר. פעיל: הבהרה עבור כפתורים לחוץ ומקלות זזים. ניפוי באגים: שכבות על למפתחים (למשל, קריאות מספריות, מדריכים וכן הלאה).
אם היינו מגדירים שכבות ב-CSS לפי זה, היו לנו: /* בעדיפות הנמוכה עד הגבוהה ביותר */ @layer base, active, debug;
@layer base { /* ... */ }
@שכבה פעילה { /* ... */ }
@layer debug { /* ... */ }
מכיוון שכל שכבה נערמת באופן צפוי, אתה תמיד יודע אילו כללים מנצחים. יכולת הניבוי הזו הופכת את ניפוי הבאגים לא רק לקל יותר, אלא גם לניהול. כיסינו את הבעיה (קלט בלתי נראה, מבולגן) ואת הגישה (מאתר באגים חזותי שנבנה עם Cascade Layers). כעת נעבור על התהליך שלב אחר שלב לבניית מאתר הבאגים. קונספט ניפוי הבאגים הדרך הקלה ביותר להפוך קלט נסתר לגלוי היא פשוט לצייר אותו על המסך. זה מה שהגרגר הזה עושה. כפתורים, טריגרים וג'ויסטיקים מקבלים כולם ויזואלי.
לחץ על A: עיגול נדלק. דחף את המקל: העיגול מחליק מסביב. לחץ על הדק באמצע הדרך: סרגל מתמלא באמצע הדרך.
עכשיו אתה לא בוהה ב-0 ו-1, אלא בעצם צופה בבקר מגיב בשידור חי. כמובן, ברגע שתתחילו לצבור מצבים כמו ברירת מחדל, לחוץ, מידע על ניפוי באגים, אולי אפילו מצב הקלטה, ה-CSS מתחיל להיות גדול ומורכב יותר. זה המקום שבו שכבות אשד באות שימושיות. הנה דוגמה מופשטת: @layer base { .button { רקע: #222; רדיוס הגבול: 50%; רוחב: 40px; גובה: 40 פיקסלים; } }
@שכבה פעילה { .button.pressed { רקע: #0f0; /* ירוק עז */ } }
@layer debug { .button::after { content: attr(נתונים-ערך); גודל גופן: 12px; צבע: #fff; } }
סדר השכבות חשוב: בסיס → פעיל → איתור באגים.
הבסיס שואב את הבקר. ידיות אקטיביות במצבים לחוץ. ניפוי באגים זריקת שכבות-על.
לפרק את זה ככה אומר שאתה לא נלחם במלחמות ספציפיות מוזרות. לכל שכבה יש את המקום שלה, ואתה תמיד יודע מה מנצח. בונים את זה החוצה בוא נעלה קודם משהו על המסך. זה לא צריך להיראות טוב - רק צריך להתקיים כדי שיהיה לנו עם מה לעבוד.
מאתר באגים ב-Gamepad Cascade
זה ממש רק קופסאות. עדיין לא מרגש, אבל זה נותן לנו ידיות לתפוס מאוחר יותר עם CSS ו-JavaScript. אוקיי, אני משתמש כאן בשכבות אשד כי זה שומר דברים מסודרים ברגע שאתה מוסיף עוד מצבים. הנה מעבר גס:
/* ================================== הגדרת שכבות CASCADE סדר חשוב: בסיס → פעיל → ניפוי באגים ==================================== */
/* הגדר סדר שכבה מראש */ @layer base, active, debug;
/* שכבה 1: סגנונות בסיס - מראה ברירת מחדל */ @layer base { .button { רקע: #333; רדיוס הגבול: 50%; רוחב: 70px; גובה: 70 פיקסלים; תצוגה: flex; להצדיק-תוכן: מרכז; align-items: center; }
.pause { רוחב: 20 פיקסלים; גובה: 70 פיקסלים; רקע: #333; תצוגה: בלוק מוטבע; } }
/* שכבה 2: מצבים פעילים - מטפל בכפתורים שנלחצו */ @שכבה פעילה { .button.active { רקע: #0f0; /* ירוק בהיר בלחיצה על */ transform: scale(1.1); /* מגדיל מעט את הכפתור */ }
.pause.active { רקע: #0f0; transform: scaleY(1.1); /* נמתח אנכית בלחיצה */ } }
/* שכבה 3: ניפוי באגים בשכבות-על - מידע למפתחים */ @layer debug { .button::after { content: attr(נתונים-ערך); /* מציג את הערך המספרי */ גודל גופן: 12px; צבע: #fff; } }
היופי בגישה זו הוא שלכל שכבה יש מטרה ברורה. שכבת הבסיס לעולם לא יכולה לעקוף את הפעיל, והפעילה לעולם לא יכולה לעקוף את ניפוי הבאגים, ללא קשר לספציפיות. זה מבטל את מלחמות הספציפיות של CSS שבדרך כלל פוגעות בכלי איתור באגים. עכשיו זה נראה כאילו כמה אשכולות יושבים על רקע כהה. בכנות, לא נורא.
הוספת ה-JavaScript זמן JavaScript. זה המקום שבו הבקר למעשה עושה משהו. אנחנו נבנה את זה צעד אחר צעד. שלב 1: הגדר ניהול מצב ראשית, אנו זקוקים למשתנים כדי לעקוב אחר מצב ניפוי הבאגים: // ================================== // ניהול המדינה // ==================================
תן לרוץ = שקר; // עוקב אם מאתר הבאגים פעיל תן rafId; // מאחסן את מזהה requestAnimationFrame לביטול
משתנים אלה שולטים בלולאת האנימציה שקוראת באופן רציף את קלט המשחקים. שלב 2: תפוס הפניות DOM לאחר מכן, אנו מקבלים הפניות לכל רכיבי ה-HTML שנעדכן: // ================================== // DOM ELEMENT REFERENSS // ==================================
const btnA = document.getElementById("btn-a"); const btnB = document.getElementById("btn-b"); const btnX = document.getElementById("btn-x"); const pause1 = document.getElementById("pause1"); const pause2 = document.getElementById("pause2"); const status = document.getElementById("status");
אחסון הפניות אלה מראש יעיל יותר מאשר שאילתה חוזרת של ה-DOM. שלב 3: הוסף מקלדת סתירה לבדיקה ללא בקר פיזי, נמפה את מקשי המקלדת ללחצנים: // ================================== // KEYBOARD FALLBACK (לבדיקה ללא בקר) // ==================================
const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [pause1, pause2] // מקש 'p' שולט בשני פסי ההשהיה };
זה מאפשר לנו לבדוק את ממשק המשתמש על ידי לחיצה על מקשים במקלדת. שלב 4: צור את לולאת העדכון הראשית כאן מתרחש הקסם. פונקציה זו פועלת ברציפות וקוראת את מצב המשחקים: // ================================== // לולאת עדכון GAMEPAD הראשית // ==================================
function updateGamepad() { // קבל את כל משטחי המשחק המחוברים const gamepads = navigator.getGamepads(); if (!gamepads) חוזרים;
// השתמש ב-gamepad המחובר הראשון const gp = gamepads[0];
if (gp) { // עדכן מצבי כפתור על ידי החלפת מחלקה "פעילה". btnA.classList.toggle("active", gp.buttons[0].pressed); btnB.classList.toggle("active", gp.buttons[1].pressed); btnX.classList.toggle("active", gp.buttons[2].pressed);
// טיפול בלחצן השהייה (אינדקס לחצן 9 ברוב הבקרים) const pausePressed = gp.buttons[9].pressed; pause1.classList.toggle("active", pausePressed); pause2.classList.toggle("פעיל", pausePressed);
// בנה רשימה של כפתורים שנלחצים כעת לתצוגת מצב תן ללחוץ = []; gp.buttons.forEach((btn, i) => { if (btn.pressed)pressed.push("כפתור " + i); });
// עדכן טקסט סטטוס אם לחצנים כלשהם נלחצים if (pressed.length > 0) { status.textContent = "לחוץ: " + pressed.join(", "); } }
// המשך בלולאה אם מאתר הבאגים פועל if (פועל) { rafId = requestAnimationFrame(updateGamepad); } }
השיטה classList.toggle() מוסיפה או מסירה את המחלקה הפעילה בהתבסס על לחיצה על הכפתור, מה שמפעיל את סגנונות שכבת ה-CSS שלנו. שלב 5: טיפול באירועי מקלדת מאזיני האירועים האלה גורמים למקלדת הנפילה לעבוד: // ================================== // מטפלי אירועי מקלדת // ==================================
document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // טיפול באלמנטים בודדים או מרובים if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } אחר { keyMap[e.key].classList.add("active"); } status.textContent = "מקש לחוץ: " + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // הסר מצב פעיל כאשר המפתח משוחרר if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } אחר { keyMap[e.key].classList.remove("active"); } status.textContent = "מפתח שוחרר: " + e.key.toUpperCase(); } });
שלב 6: הוסף בקרת התחל/עצירה לבסוף, אנחנו צריכים דרך להפעיל ולכבות את ניפוי הבאגים: // ================================== // הפעל/כיבוי של ניפוי באגים // ==================================
document.getElementById("toggle").addEventListener("click", () => { ריצה = !ריצה; // הפוך את מצב הריצה
if (פועל) { status.textContent = "מאתר באגים פועל..."; updateGamepad(); // התחל את לולאת העדכון } אחר { status.textContent = "מאתר הבאגים לא פעיל"; cancelAnimationFrame(rafId); // עצור את הלולאה } });
אז כן, לחץ על כפתור והוא זוהר. דחוף את המקל והוא זז. זהו זה. דבר נוסף: ערכים גולמיים. לפעמים אתה רק רוצה לראות מספרים, לא אורות.
בשלב זה, אתה אמור לראות:
בקר פשוט על המסך, כפתורים שמגיבים בזמן שאתה מתקשר איתם, ו קריאת ניפוי באגים אופציונלית המציגה מדדי כפתורים לחוץ.
כדי להפוך את זה לפחות מופשט, הנה הדגמה מהירה של הבקר על המסך המגיב בזמן אמת:
כעת, לחיצה על התחל הקלטה רושמת הכל עד שתלחץ על עצור הקלטה. 2. ייצוא נתונים ל-CSV/JSON ברגע שיש לנו יומן, נרצה לשמור אותו.
שלב 1: צור את עוזר ההורדות ראשית, אנו זקוקים לפונקציית עוזר שמטפלת בהורדות קבצים בדפדפן: // ================================== // עוזר להורדת קבצים // ==================================
function downloadFile(שם קובץ, תוכן, סוג = "טקסט/רגיל") { // צור כתם מהתוכן const blob = new Blob([תוכן], { סוג }); const url = URL.createObjectURL(blob);
// צור קישור הורדה זמני ולחץ עליו const a = document.createElement("a"); a.href = url; a.download = שם קובץ; a.click();
// נקה את כתובת האתר של האובייקט לאחר ההורדה setTimeout(() => URL.revokeObjectURL(url), 100); }
פונקציה זו פועלת על ידי יצירת Blob (אובייקט גדול בינארי) מהנתונים שלך, יצירת כתובת URL זמנית עבורו ולחיצה תוכניתית על קישור הורדה. הניקוי מבטיח שלא נדלוף זיכרון. שלב 2: טפל בייצוא JSON JSON מושלם לשימור מבנה הנתונים המלא:
// ================================== // ייצוא כ-JSON // ==================================
document.getElementById("export-json").addEventListener("click", () => { // בדוק אם יש משהו לייצא if (!frames.length) { console.warn("אין הקלטה זמינה לייצוא."); חזרה; }
// צור מטען עם מטא נתונים ומסגרות const מטען = { createdAt: new Date().toISOString(), מסגרות };
// הורד כ-JSON מעוצב downloadFile( "gamepad-log.json", JSON.stringify(payload, null, 2), "application/json" ); });
פורמט JSON שומר על הכל מובנה וניתן לניתוח בקלות, מה שהופך אותו לאידיאלי לטעינה חזרה לכלי מפתחים או לשיתוף עם חברי צוות. שלב 3: טפל בייצוא CSV עבור ייצוא CSV, עלינו לשטח את הנתונים ההיררכיים לשורות ועמודות:
//================================== // ייצוא כ-CSV // ==================================
document.getElementById("export-csv").addEventListener("click", () => { // בדוק אם יש משהו לייצא if (!frames.length) { console.warn("אין הקלטה זמינה לייצוא."); חזרה; }
// בניית שורת כותרת CSV (עמודות עבור חותמת זמן, כל הלחצנים, כל הצירים) const headerButtons = frames[0].buttons.map((_, i) => btn${i}); const headerAxes = frames[0].axes.map((_, i) => axis${i}); const header = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// בניית שורות נתונים של CSV const rows = frames.map(f => { const btnVals = f.buttons.map(b => b.value); return [f.t, ...btnVals, ...f.axes].join(","); }).join("\n");
// הורד כ-CSV downloadFile("gamepad-log.csv", כותרת + שורות, "טקסט/csv"); });
CSV מבריק לניתוח נתונים מכיוון שהוא נפתח ישירות ב-Excel או ב-Google Sheets, ומאפשר לך ליצור תרשימים, לסנן נתונים או לזהות דפוסים ויזואלית. כעת כשלחצני הייצוא נכנסים, תראה שתי אפשרויות חדשות בחלונית: ייצוא JSON וייצוא CSV. JSON נחמד אם אתה רוצה לזרוק את היומן הגולמי בחזרה לתוך כלי הפיתוח שלך או לחטט במבנה. CSV, לעומת זאת, נפתח ישירות לתוך Excel או Google Sheets כך שתוכל לשרטט, לסנן או להשוות תשומות. האיור הבא מראה איך נראה הפאנל עם הפקדים הנוספים האלה.
3. מערכת סנאפצ'ט לפעמים אתה לא צריך הקלטה מלאה, רק "צילום מסך" מהיר של מצבי קלט. זה המקום בו כפתור צילום תמונת מצב עוזר.
וה-JavaScript:
// ================================== // צלם תמונת מצב // ==================================
document.getElementById("snapshot").addEventListener("click", () => { // קבל את כל משטחי המשחק המחוברים const pads = navigator.getGamepads(); const activePads = [];
// עברו בלולאה ותלכוד את המצב של כל משחק משטח מחובר for (const gp of pads) { אם (!gp) להמשיך; // דלג על משבצות ריקות
activePads.push({ id: gp.id, // שם הבקר/דגם חותמת זמן: performance.now(), buttons: gp.buttons.map(b => ({ לחוץ: ב.לחץ, ערך: b.value })), צירים: [...gp.axes] }); }
// בדוק אם נמצאו משטחי משחק if (!activePads.length) { console.warn("אין משטחי משחק מחוברים לצילום מצב."); alert("לא זוהה בקר!"); חזרה; }
// התחבר והודע למשתמש console.log("Snapshot:", activePads); התראה (תמונה צולמה! נתפסו ${activePads.length} בקר(ים).); });
צילומי מצב מקפיאים את המצב המדויק של הבקר שלך ברגע אחד. 4. שידור חוזר של קלט רפאים עכשיו למהנה: שידור חוזר של קלט רפאים. זה לוקח יומן ומשמיע אותו ויזואלית כאילו נגן פנטום משתמש בבקר.
JavaScript עבור שידור חוזר: // ================================== // שידור רפאים // ==================================
document.getElementById("replay").addEventListener("click", () => { // ודא שיש לנו הקלטה להשמעה חוזרת if (!frames.length) { alert("אין הקלטה להשמעה חוזרת!"); חזרה; }
console.log("התחלת שידור חוזר של רפאים...");
// תזמון רצועות עבור השמעה מסונכרנת let startTime = performance.now(); let frameIndex = 0;
// הפעל מחדש לולאת אנימציה function step() { const now = performance.now(); const elapsed = now - startTime;
// עבד את כל המסגרות שהיו אמורות להתרחש עד עכשיו while (frameIndex < frames.length && frames[frameIndex].t <= חלף) { const frame = frames[frameIndex];
// עדכן את ממשק המשתמש עם מצבי הכפתור המוקלטים btnA.classList.toggle("active", frame.buttons[0].pressed); btnB.classList.toggle("active", frame.buttons[1].pressed); btnX.classList.toggle("active", frame.buttons[2].pressed);
// תצוגת מצב עדכון תן ללחוץ = []; frame.buttons.forEach((btn, i) => { if (btn.pressed) pressed.push("כפתור " + i); }); if (pressed.length > 0) { status.textContent = "Ghost: " + pressed.join(", "); }
frameIndex++; }
// המשך לולאה אם יש יותר מסגרות if (frameIndex < frames.length) { requestAnimationFrame(step); } אחר { console.log("הפעל מחדשסיים."); status.textContent = "ההפעלה החוזרת הושלמה"; } }
// התחל את השידור החוזר step(); });
כדי להפוך את ניפוי הבאגים לקצת יותר מעשית, הוספתי שידור חוזר של רפאים. לאחר שהקלטת הפעלה, תוכל ללחוץ על הפעלה חוזרת ולראות את ממשק המשתמש פועל אותה, כמעט כמו נגן פנטום שמפעיל את הפנקס. לחצן Replay Ghost חדש מופיע בפאנל בשביל זה.
לחץ על הקלט, תתעסק קצת עם הבקר, עצור ואז הפעל מחדש. ממשק המשתמש פשוט מהדהד את כל מה שעשית, כמו רוח רפאים בעקבות הקלט שלך. למה להתעסק עם התוספות האלה?
הקלטה/ייצוא מקל על הבודקים להראות בדיוק מה קרה. צילומי מצב קופאים לרגע בזמן, שימושי במיוחד כשאתה רודף אחרי באגים מוזרים. שידור חוזר של Ghost נהדר להדרכות, בדיקות נגישות או סתם השוואה בין הגדרות בקרה זו לצד זו.
בשלב זה, זה כבר לא רק הדגמה מסודרת, אלא משהו שאתה באמת יכול להפעיל. מקרי שימוש בעולם האמיתי עכשיו יש לנו מאפר הבאגים שיכול לעשות הרבה. זה מציג קלט חי, מתעד יומנים, מייצא אותם ואפילו משמיע דברים חוזרים. אבל השאלה האמיתית היא: למי בעצם אכפת? למי זה שימושי? מפתחי משחקים בקרים הם חלק מהעבודה, אבל לנפות אותם? בדרך כלל כאב. תאר לעצמך שאתה בודק שילוב של משחקי לחימה, כמו ↓ → + אגרוף. במקום להתפלל, לחצת אותו באותה צורה פעמיים, אתה מקליט אותו פעם אחת ומשמיע אותו מחדש. בוצע. או שאתה מחליף יומני JSON עם חבר לצוות כדי לבדוק אם קוד מרובה המשתתפים שלך מגיב אותו דבר במחשב שלו. זה ענק. מתרגלי נגישות זה קרוב ללבי. לא כולם משחקים עם בקר "סטנדרטי". בקרים מסתגלים זורקים אותות מוזרים לפעמים. עם הכלי הזה, אתה יכול לראות בדיוק מה קורה. מורים, חוקרים, מי שלא יהיה. הם יכולים לתפוס יומנים, להשוות ביניהם או להשמיע קלט זה לצד זה. פתאום דברים בלתי נראים נהיים ברורים. בדיקת אבטחת איכות בודקים בדרך כלל כותבים הערות כמו "ריסקתי כאן כפתורים וזה נשבר." לא מאוד מועיל. עכשיו? הם יכולים ללכוד את הלחיצות המדויקות, לייצא את היומן ולשלוח אותו. בלי ניחושים. מחנכים אם אתה יוצר מדריכים או סרטוני YouTube, שידור חוזר של רפאים הוא זהב. אתה יכול לומר, פשוטו כמשמעו, "הנה מה שעשיתי עם הבקר", בזמן שממשק המשתמש מראה שזה קורה. הופך את ההסברים להרבה יותר ברורים. מעבר למשחקים וכן, זה לא רק על משחקים. אנשים השתמשו בבקרים עבור רובוטים, פרויקטי אמנות וממשקי נגישות. אותה בעיה בכל פעם: מה בעצם הדפדפן רואה? עם זה, אתה לא צריך לנחש. מסקנה איתור באגים בקלט של בקר תמיד הרגיש כמו לעוף עיוור. בניגוד ל-DOM או CSS, אין מפקח מובנה עבור משטחי משחק; זה רק מספרים גולמיים בקונסולה, אובדים בקלות ברעש. עם כמה מאות שורות של HTML, CSS ו-JavaScript, בנינו משהו אחר:
מאתר באגים ויזואלי שהופך קלט בלתי נראה לגלוי. מערכת CSS שכבתית השומרת על ממשק המשתמש נקי וניתן לניפוי באגים. קבוצה של שיפורים (הקלטה, ייצוא, צילומי מצב, שידור חוזר של רפאים) שמעלה אותו מהדגמה לכלי מפתחים.
פרויקט זה מראה כמה רחוק אתה יכול להגיע על ידי ערבוב הכוח של פלטפורמת האינטרנט עם מעט יצירתיות בשכבות CSS Cascade Layers. הכלי שהסברתי זה עתה בשלמותו הוא קוד פתוח. אתה יכול לשכפל את הריפו של GitHub ולנסות אותו בעצמך. אבל יותר חשוב, אתה יכול לעשות את זה שלך. הוסף שכבות משלך. בנו את היגיון השידור החוזר שלכם. שלב אותו עם אב הטיפוס של המשחק שלך. או אפילו להשתמש בו בדרכים שלא דמיינתי. להוראה, נגישות או ניתוח נתונים. בסופו של יום, זה לא רק על ניפוי באגים של משטחי משחק. מדובר על להאיר אור על תשומות נסתרות, ולהעניק למפתחים את הביטחון לעבוד עם חומרה שהרשת עדיין לא מאמצת במלואה. אז, חבר את הבקר שלך, פתח את העורך שלך והתחיל להתנסות. אתה עשוי להיות מופתע ממה שהדפדפן שלך וה-CSS שלך באמת יכולים להשיג.