איך שיפרנו את ממשק המשתמש
מבוא
JAM with Chrome הוא פרויקט מוזיקלי מבוסס-אינטרנט שנוצר על ידי Google. JAM with Chrome מאפשר לאנשים מכל העולם ליצור להקה ולנגן בה בזמן אמת בדפדפן. DinahMoe דחפו את הגבולות של מה שאפשר לעשות באמצעות Web Audio API של Chrome, והצוות שלנו ב-Tool of North America יצר את הממשק לנגינה בגיטרה, בתופים ובמחשב כאילו מדובר בכלי מוזיקלי.
בהנחיית צוות Google Creative Lab, המאייר רוב באילי (Rob Bailey) יצר איורים מורכבים לכל אחד מ-19 הכלים שזמינים ליצירת JAM. על סמך הנתונים האלה, הבמאי האינטראקטיבי בן טריקלבנק וצוות העיצוב שלנו ב-Tool יצרו ממשק קל ומקצועי לכל מכשיר.
כל כלי הוא ייחודי מבחינה ויזואלית, ולכן Bartek Drozdz, מנהל הטכני של Tool, ואני צירפנו אותם יחד באמצעות שילובים של תמונות PNG, רכיבי CSS, SVG ו-Canvas.
רבים מהכלים היו צריכים לטפל בשיטות אינטראקציה שונות (כמו קליקים, גרירה ונגינה בפריט – כל מה שאפשר לעשות עם כלי), תוך שמירה על הממשק עם מנוע הקול של DinahMoe. הבנו שאנחנו צריכים יותר מאשר רק את האירועים mouseup ו-mousedown ב-JavaScript כדי לספק חוויית משחק יפה.
כדי להתמודד עם כל המגוון הזה, יצרנו רכיב 'במה' שכיסה את אזור הנגינה, טיפל בלחיצות, בגרירה ובנגינה בכל הכלים השונים.
The Stage
Stage הוא הבקר שלנו שמשמש להגדרת פונקציות בכלי. לדוגמה, הוספת חלקים שונים של המכשירים שבהם המשתמש יתנהל. ככל שנוסיף עוד אינטראקציות (כמו 'היט'), נוכל להוסיף אותן לאב טיפוס של השלב.
function Stage(el) {
// Grab the elements from the dom
this.el = document.getElementById(el);
this.elOutput = document.getElementById("output-1");
// Find the position of the stage element
this.position();
// Listen for events
this.listeners();
return this;
}
Stage.prototype.position = function() {
// Get the position
};
Stage.prototype.offset = function() {
// Get the offset of the element in the window
};
Stage.prototype.listeners = function() {
// Listen for Resizes or Scrolling
// Listen for Mouse events
};
אחזור המיקום של הרכיב והעכבר
המשימה הראשונה שלנו היא לתרגם את קואורדינטות העכבר בחלון הדפדפן כך שיהיו יחסיות לרכיב הבמה שלנו. כדי לעשות זאת, נדרשנו להביא בחשבון את המיקום של הבמה בדף.
מכיוון שאנחנו צריכים למצוא את המיקום של הרכיב ביחס לחלון כולו, ולא רק ביחס לרכיב ההורה שלו, התהליך קצת יותר מורכב מאשר פשוט לבדוק את הרכיבים offsetTop ו-offsetLeft. האפשרות הקלה ביותר היא להשתמש ב-getBoundingClientRect, שמציג את המיקום ביחס לחלון, בדיוק כמו אירועי עכבר, ויש לו תמיכה טובה בדפדפנים חדשים יותר.
Stage.prototype.offset = function() {
var _x, _y,
el = this.el;
// Check to see if bouding is available
if (typeof el.getBoundingClientRect !== "undefined") {
return el.getBoundingClientRect();
} else {
_x = 0;
_y = 0;
// Go up the chain of parents of the element
// and add their offsets to the offset of our Stage element
while (el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
_x += el.offsetLeft;
_y += el.offsetTop;
el = el.offsetParent;
}
// Subtract any scrolling movment
return {top: _y - window.scrollY, left: _x - window.scrollX};
}
};
אם getBoundingClientRect לא קיים, יש לנו פונקציה פשוטה שפשוט תוסיף את ההיסטים, תוך תנועה למעלה בשרשרת של הרכיבים ההורים עד שהיא תגיע לגוף. לאחר מכן אנחנו מחסירים את המרחק שבו החלון גלול כדי לקבל את המיקום ביחס לחלון. אם אתם משתמשים ב-jQuery, הפונקציה offset() עושה עבודה מצוינת בטיפול במורכבות של חישוב המיקום בפלטפורמות שונות, אבל עדיין תצטרכו לחסר את כמות הגלילה.
בכל פעם שגוללים בדף או משנים את הגודל שלו, יכול להיות שמיקום הרכיב השתנה. אנחנו יכולים להאזין לאירועים האלה ולבדוק שוב את המיקום. האירועים האלה מופעלים פעמים רבות בזמן גלילה או שינוי גודל רגילים, לכן באפליקציה אמיתית מומלץ להגביל את תדירות הבדיקה מחדש של המיקום. יש הרבה דרכים לעשות זאת, אבל ב-HTML5 Rocks יש מאמר בנושא ביטול הרטט של אירועי גלילה באמצעות requestAnimationFrame, שיעבוד טוב כאן.
לפני שנטפל בזיהוי היטים, הדוגמה הראשונה הזו פשוט תציג את הערכים היחסיים של x ו-y בכל פעם שהעכבר יזוז באזור הבמה.
Stage.prototype.listeners = function() {
var output = document.getElementById("output");
this.el.addEventListener('mousemove', function(e) {
// Subtract the elements position from the mouse event's x and y
var x = e.clientX - _self.positionLeft,
y = e.clientY - _self.positionTop;
// Print out the coordinates
output.innerHTML = (x + "," + y);
}, false);
};
כדי להתחיל לעקוב אחרי תנועת העכבר, נוצר אובייקט Stage חדש ומעבירים לו את המזהה של ה-div שאנחנו רוצים להשתמש בו בתור Stage.
//-- Create a new Stage object, for a div with id of "stage"
var stage = new Stage("stage");
זיהוי פשוט של היטים
ב-JAM with Chrome, לא כל ממשקי המכשירים מורכבים. הלחצנים במכונות התופים שלנו הם פשוט מלבנים פשוטים, כך שקל לזהות אם לחיצה נמצאת בתוך גבולותיהם.
נתחיל עם מלבנים, ונגדיר כמה סוגים בסיסיים של צורות. כל אובייקט של צורה צריך לדעת מהם הגבולות שלו ויכול לבדוק אם נקודה נמצאת בתוכו.
function Rect(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
return this;
}
Rect.prototype.inside = function(x, y) {
return x >= this.x && y >= this.y
&& x <= this.x + this.width
&& y <= this.y + this.height;
};
לכל סוג צורה חדש שנוסף תצטרך להיות פונקציה באובייקט Stage שלנו כדי לרשום אותו כתחום היעד.
Stage.prototype.addRect = function(id) {
var el = document.getElementById(id),
rect = new Rect(
el.offsetLeft,
el.offsetTop,
el.offsetWidth,
el.offsetHeight
);
rect.el = el;
this.hitZones.push(rect);
return rect;
};
באירועי עכבר, כל מופע של צורה יטפל בבדיקת אם הקואורדינטות x ו-y של העכבר שהועברו הן היטים שלו, ויחזיר את הערכים true או false.
אפשר גם להוסיף לרכיב הבמה את הכיתה 'פעיל', וכך סמן העכבר ישתנה למצב של כוונן כשתעבירו אותו מעל הריבוע.
this.el.addEventListener ('mousemove', function(e) {
var x = e.clientX - _self.positionLeft,
y = e.clientY - _self.positionTop;
_self.hitZones.forEach (function(zone){
if (zone.inside(x, y)) {
// Add class to change colors
zone.el.classList.add('hit');
// change cursor to pointer
this.el.classList.add('active');
} else {
zone.el.classList.remove('hit');
this.el.classList.remove('active');
}
});
}, false);
צורות נוספות
ככל שהצורות מורכבות יותר, כך החישובים שצריך לבצע כדי לבדוק אם נקודה נמצאת בתוכן שלהן נעשים מורכבים יותר. עם זאת, המשוואות האלה מקובלות ומתוועדות בפירוט רב במקומות רבים באינטרנט. חלק מהדוגמאות הטובות ביותר ל-JavaScript שראיתי הן מספריית הגיאומטריה של Kevin Lindsey.
למרבה המזל, כשפיתחנו את JAM ב-Chrome, אף פעם לא היינו צריכים לחרוג ממעגלים ומריבועים, והסתמכנו על שילובים של צורות ועל שכבות כדי להתמודד עם כל מורכבות נוספת.
מעגלים
כדי לבדוק אם נקודה נמצאת בתוך תוף עגול, נצטרך ליצור צורה של בסיס עגול. הוא דומה מאוד למלבן, אבל יהיו לו שיטות משלו לקביעת גבולות ולבדיקה אם הנקודה נמצאת בתוך המעגל.
function Circle(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
return this;
}
Circle.prototype.inside = function(x, y) {
var dx = x - this.x,
dy = y - this.y,
r = this.radius;
return dx * dx + dy * dy <= r * r;
};
במקום לשנות את הצבע, הוספת סיווג ההיט תפעיל אנימציה של CSS3. הגודל של הרקע מאפשר לנו לשנות במהירות את הגודל של התמונה של התוף, בלי להשפיע על המיקום שלה. כדי לעבוד עם דפדפנים אחרים, תצטרכו להוסיף להם קידומות (-moz, -o ו-ms), וייתכן שתרצו להוסיף גם גרסה ללא קידומת.
#snare.hit{
{ % mixin animation: drumHit .15s linear infinite; % }
}
@{ % mixin keyframes drumHit % } {
0% { background-size: 100%;}
10% { background-size: 95%; }
30% { background-size: 97%; }
50% { background-size: 100%;}
60% { background-size: 98%; }
70% { background-size: 100%;}
80% { background-size: 99%; }
100% { background-size: 100%;}
}
מחרוזת
הפונקציה GuitarString שלנו תקבל מזהה של לוח ציור ואובייקט Rect ותצייר קו במרכז המת rectangle.
function GuitarString(rect) {
this.x = rect.x;
this.y = rect.y + rect.height / 2;
this.width = rect.width;
this._strumForce = 0;
this.a = 0;
}
כשנרצה שהיא תרטוט, נפעיל את פונקציית הנגינה כדי להניע את המחרוזת. בכל פריים שאנחנו מריצים, אנחנו מפחיתים מעט את הכוח שבו ניגנו עליו ומגדילים את המונה שגורם לתנודות של המיתר הלוך ושוב.
GuitarString.prototype.strum = function() {
this._strumForce = 5;
};
GuitarString.prototype.render = function(ctx, canvas) {
ctx.strokeStyle = "#000000";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.bezierCurveTo(
this.x, this.y + Math.sin(this.a) * this._strumForce,
this.x + this.width, this.y + Math.sin(this.a) * this._strumForce,
this.x + this.width, this.y);
ctx.stroke();
this._strumForce *= 0.99;
this.a += 0.5;
};
צמתים ופריטה
אזור ההיט של המחרוזת יהיה שוב תיבת חיפוש. לחיצה בתוך התיבה הזו אמורה להפעיל את אנימציית המחרוזת. אבל מי רוצה ללחוץ על גיטרה?
כדי להוסיף פריטה, צריך לבדוק את הצטלבות התיבה של המיתרים עם הקו שבו העכבר של המשתמש נע.
כדי ליצור מספיק מרחק בין המיקום הקודם של העכבר לבין המיקום הנוכחי שלו, נצטרך להאט את הקצב שבו אנחנו מקבלים את אירועי העכבר. בדוגמה הזו, פשוט נגדיר דגל כדי להתעלם מאירועי mousemove למשך 50 אלפיות השנייה.
document.addEventListener('mousemove', function(e) {
var x, y;
if (!this.dragging || this.limit) return;
this.limit = true;
this.hitZones.forEach(function(zone) {
this.checkIntercept(
this.prev[0],
this.prev[1],
x,
y,
zone
);
});
this.prev = [x, y];
setInterval(function() {
this.limit = false;
}, 50);
};
בשלב הבא נצטרך להשתמש בקוד של צומת שכתב Kevin Lindsey כדי לבדוק אם הקו של תנועת העכבר חוצה את מרכז המלבן.
Rect.prototype.intersectLine = function(a1, a2, b1, b2) {
//-- http://www.kevlindev.com/gui/math/intersection/Intersection.js
var result,
ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
if (u_b != 0) {
var ua = ua_t / u_b;
var ub = ub_t / u_b;
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
result = true;
} else {
result = false; //-- No Intersection
}
} else {
if (ua_t == 0 || ub_t == 0) {
result = false; //-- Coincident
} else {
result = false; //-- Parallel
}
}
return result;
};
לסיום, נוסיף פונקציה חדשה ליצירת כלי מיתר. הפונקציה תיצור את הבמה החדשה, תגדיר מספר מחרוזות ותקבל את ההקשר של לוח הציור שבו תתבצע הציור.
function StringInstrument(stageID, canvasID, stringNum){
this.strings = [];
this.canvas = document.getElementById(canvasID);
this.stage = new Stage(stageID);
this.ctx = this.canvas.getContext('2d');
this.stringNum = stringNum;
this.create();
this.render();
return this;
}
בשלב הבא נמקם את אזורי ההקשה של המיתרים ולאחר מכן נוסיף אותם לרכיב הבמה.
StringInstrument.prototype.create = function() {
for (var i = 0; i < this.stringNum; i++) {
var srect = new Rect(10, 90 + i * 15, 380, 5);
var s = new GuitarString(srect);
this.stage.addString(srect, s);
this.strings.push(s);
}
};
לבסוף, פונקציית ה-render של StringInstrument תעבור בחזרה על כל המחרוזות ותפעיל את שיטות ה-render שלהן. הוא פועל כל הזמן, במהירות שמתאימה ל-requestAnimationFrame. מידע נוסף על requestAnimationFrame זמין במאמר של Paul Irish, requestAnimationFrame ליצירת אנימציות חכמות.
באפליקציה אמיתית, כדאי להגדיר דגל כשאין אנימציה כדי להפסיק את הציור של מסגרת חדשה בקנבס.
StringInstrument.prototype.render = function() {
var _self = this;
requestAnimFrame(function(){
_self.render();
});
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (var i = 0; i < this.stringNum; i++) {
this.strings[i].render(this.ctx);
}
};
סיכום
יש חסרונות לשימוש ברכיב Stage משותף לטיפול בכל האינטראקציות שלנו. האירועים מורכבים יותר מבחינה חישובית, ואירועי הסמן של הסמן מוגבלים בלי הוספת קוד נוסף כדי לשנות אותם. עם זאת, ב-JAM עם Chrome, היתרונות של היכולת להפריד אירועי עכבר מהרכיבים הנפרדים עבדו מצוין. היא מאפשרת לנו להתנסות יותר בעיצוב הממשק, לעבור בין שיטות של אנימציה של רכיבים, להשתמש ב-SVG כדי להחליף תמונות של צורות בסיסיות, להשבית בקלות אזורים של אירועים ועוד.
כדי לראות את התכונות 'תופים' ו'קטעים קצרים' בפעולה, פותחים JAM משלכם ובוחרים באפשרות תופים רגילים או גיטרה חשמלית קלאסית נקייה.