מקרה לדוגמה – JAM עם Chrome

איך יצרנו את ממשק המשתמש

מבוא

JAM עם Chrome הוא פרויקט מוזיקלי מבוסס-אינטרנט שנוצר על ידי Google. JAM עם Chrome מאפשר לאנשים מכל העולם להקים להקה ו-JAM בזמן אמת בתוך הדפדפן. DinahMoe פרצה את הגבולות למה שאפשר היה לעשות עם ה-Web Audio API של Chrome, הצוות שלנו ב-Tool of West America יצר את הממשק עבור החבטה, תיפוף ונגינה במחשב כאילו הוא כלי נגינה.

עם המנהל היצירתי של Google Creative Lab, המאייר רוב ביילי יצר איורים מורכבים לכל אחד מ-19 כלי הנגינה הזמינים ל-JAM. בנוסף לכל אלה, המנהל האינטראקטיבי בן טריקלבנק וצוות העיצוב שלנו ב-tool יצרו ממשק קל ומקצועי לכל כלי.

סרטון מונטאז' מלא של ג'אם

מכיוון שכל כלי נגינה הוא ייחודי מבחינה חזותית, המנהל הטכני של הכלי בארטק דרוז'ד ותפרתי אותם בעזרת שילובים של תמונות PNG, רכיבי CSS, SVG ולוח הציור.

רבים מכלי הנגינה היו צריכים לטפל בשיטות אינטראקציה שונות (כמו קליקים, גרירה ופרקים - כל הדברים שהייתם מצפים לעשות עם כלי נגינה) תוך שמירה על הממשק עם מנוע הקול של DinahMoe ללא שינוי. גילינו שאנחנו צריכים יותר מהתכונה 'העברת העכבר מעל העכבר' והתכונה 'mousedown' ב-JavaScript כדי לספק חוויית משחק יפה.

כדי להתמודד עם כל הווריאציות האלה, יצרנו רכיב "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
};

קבלת מיקום הרכיב והעכבר

המשימה הראשונה שלנו היא לתרגם את הקואורדינטות של העכבר בחלון הדפדפן כך שיהיו יחסיות לרכיב ה-Stage. כדי לעשות זאת, היה עלינו לקחת בחשבון את המיקום של השלב.

מכיוון שאנחנו צריכים למצוא את המיקום ביחס לחלון כולו, לא רק לאלמנט ההורה שלו, הוא קצת יותר מסובך מאשר רק להביט באלמנטים מסוגsetTop ו-offLeft. האפשרות הקלה ביותר היא להשתמש ב-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, הפונקציה reset() מצליחה להתמודד עם המורכבות של זיהוי המיקום בפלטפורמות שונות, אך עדיין יהיה עליך להחסיר את הסכום שגלל.

בכל פעם שגוללים בדף או משנים את הגודל שלו, יכול להיות שמיקום הרכיב השתנה. אנחנו יכולים להאזין לאירועים האלה ולבדוק שוב את המיקום. האירועים האלה מופעלים פעמים רבות במהלך גלילה או שינוי גודל טיפוסיים, לכן באפליקציה אמיתית כדאי להגביל את התדירות שבה בודקים שוב את המיקום. יש הרבה דרכים לעשות זאת, אבל ב-HTML5 Rocks יש מאמר בנושא ביטול של אירועי גלילה באמצעות requestAnimationFrame, שמתאימה מאוד כאן.

לפני שנטפל בזיהוי התאמות, הדוגמה הראשונה תציג פלט של x ו-y היחסי בכל פעם שהעכבר ינוע באזור Stage.

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 עם 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'.

כמו כן, נוכל להוסיף מחלקה "active" לרכיב השלב. שינוי זה ישנה את סמן העכבר למצב מצביע בעת ריחוף מעל הריבוע.

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 שראיתי הן מספריית הגאומטריה של קווין לינדזי.

למרבה המזל, במהלך פיתוח 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. גודל הרקע מאפשר לנו לשנות במהירות את תמונת התוף מבלי להשפיע על מיקום התוף. לצורך עבודה זו תצטרכו להוסיף קידומות של דפדפנים אחרים (-מוזה, -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 ותצייר קו לאורך מרכז המלבן.

function GuitarString(rect) {
  this.x = rect.x;
  this.y = rect.y + rect.height / 2;
  this.width = rect.width;
  this._strumForce = 0;
  this.a = 0;
}

כשנרצה שהוא ירטוט, נקרא לפונקציית ה-Strum כדי להגדיר את המחרוזת בתנועה. כל פריים שנעבד יפחית את הכוח שבו הוא נחתך מעט ויגדיל את המונה שיגרום למחרוזת לתנודה קדימה ואחורה.

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);
};

בשלב הבא נצטרך להסתמך על קוד צומת שקווין לינדזי כתב כדי לראות אם הקו של תנועת העכבר חוצה את אמצע המלבן.

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;
}

בשלב הבא נמקם את אזורי ההיט של המחרוזות ולאחר מכן נוסיף אותן לרכיב ה-Stage.

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);
  }
};

לבסוף, פונקציית העיבוד של כלי הנגינה תעבור בלולאה בין כל המחרוזות שלנו ותקרא לשיטות העיבוד שלהן. היא פועלת כל הזמן, במהירות שמתאימה ל- requestAnimationFrame. תוכל לקרוא מידע נוסף על requestAnimationFrame במאמר של פול אירלנד בנושא 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 שלכם ובחרו באפשרות תופים רגילים או בגיטרה חשמלית קלאסית.

הלוגו של Jam