دراسة حالة - JAM مع Chrome

كيف جعلنا واجهة المستخدم رائعة

مقدمة

JAM with Chrome هو مشروع موسيقي على الويب أنشأته Google. تتيح ميزة JAM with Chrome للمستخدمين من جميع أنحاء العالم تشكيل فرقة وإنشاء موسيقى في الوقت الفعلي داخل المتصفّح. تجاوزت DinahMoe الحدود الممكنة باستخدام Web Audio API في Chrome، وصمّم فريقنا في Tool of North America واجهة لعزف الوتريات والإيقاع والعزف على جهاز الكمبيوتر كما لو كان آلة موسيقية.

بتوجيه من فريق Google Creative Lab الإبداعي، أنشأ الرسام روب بايلي رسومًا توضيحية معقدة لكل آلة من الآلات الـ 19 المتاحة للعزف عليها في تطبيق JAM. استنادًا إلى هذه الملاحظات، أنشأ مدير التفاعل بن تريكليبانك وفريق التصميم في Tool واجهة سهلة الاستخدام ومناسبة للمحترفين لكل آلة موسيقية.

مقطع مجمّع كامل لمحتوى Jam

بما أنّ كل آلة موسيقية فريدة من حيث الشكل، جمعتُها معًا أنا والمدير الفني في Tool، بارتيك دروزز، باستخدام مجموعات من صور PNG وCSS وSVG وعناصر Canvas.

كان على العديد من الآلات التعامل مع طرق مختلفة للتفاعل (مثل النقرات والسحب والعزف - كل ما يمكن توقعه من آلة موسيقية) مع الحفاظ على واجهة محرّك الصوت في DinahMoe كما هي. تبيّن لنا أنّنا بحاجة إلى أكثر من مجرد mouseup وmousedown في JavaScript لنتمكّن من تقديم تجربة تشغيل رائعة.

للتعامل مع كل هذه الاختلافات، أنشأنا عنصر "المسرح" الذي يغطي المنطقة القابلة للتشغيل، ويعالج النقرات والسحب والعزف على جميع الآلات الموسيقية المختلفة.

The 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 الذي نريد استخدامه كمسرح.

//-- 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.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 التي رأيتها هي من مكتبة الهندسة التي أنشأها "كيفن ليندسي".

لحسن الحظ، لم نحتاج إلى استخدام أشكال أخرى غير الدوائر والمستطيلات عند إنشاء 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 وترسم خطًا في منتصف هذا المستطيل.

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

بعد ذلك، سنحتاج إلى الاعتماد على بعض رموز التقاطع التي كتبها "كيفن ليندسي" لمعرفة ما إذا كان خط حركة الماوس يتقاطع مع منتصف المستطيل.

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

أخيرًا، ستنتقل دالة عرض StringInstrument إلى جميع سلاسل الأدوات الموسيقية وستستدعي طرق عرضها. ويتم تشغيله طوال الوقت بالسرعة التي يراها 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