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

كيف صمّمنا واجهة المستخدم

مقدمة

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

باستخدام التوجيه الإبداعي في مختبر Google الإبداعي، أنشأ الرسام روب بيلي رسومات توضيحية معقدة لكل من الآلات الموسيقية التسع عشرة المتاحة لجلسات JAM. بناءً على ذلك، أنشأ مدير التفاعل بن Tricklebank وفريق التصميم في الأداة واجهة سهلة واحترافية لكلّ أداة من الآلات.

المونتاج الكامل للازدحام

وبما أنّ كلّ أداة تتميّز من جهة مرئية وفريدة من نوعها، تمكّنت من تركيب هذه الأدوات بالتعاون مع المدير التقني للأداة Bartek Drozdz باستخدام مجموعات من صور PNG وعناصر CSS وSVG وCanvas.

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

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

المسرح

المرحلة هي وحدة التحكم التي نستخدمها لإعداد الدالة عبر أي أداة. مثل إضافة أجزاء مختلفة من الأدوات التي سيتفاعل معها المستخدم. عندما نضيف المزيد من التفاعلات (مثل "النتيجة")، يمكننا إضافتها إلى النموذج الأولي للمرحلة.

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 (المرحلة). لإجراء ذلك، كان علينا مراعاة موضع المراحل في الصفحة.

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

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

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 مع 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 الذي تم تمريره يمثل نتيجة له ويقوم بإرجاع صحيح أو خطأ.

يمكننا أيضًا إضافة فئة "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;
}

وعندما نريد أن يهتز، فإننا نستدعي دالة العزف على الغيتار لضبط السلسلة على الحركة. سيقلل كل إطار نعرضه من قوة ضغطه قليلاً ويزيد من العدّاد الذي يؤدي إلى اهتزاز السلسلة ذهابًا وإيابًا.

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

التقاطعات والعزف على آلات موسيقية

ستكون منطقة النتيجة للسلسلة مربَّعًا مرة أخرى. من المفترض أن يؤدي النقر داخل هذا المربع إلى تشغيل الرسوم المتحركة للسلسلة. ولكن من يريد النقر فوق الغيتار؟

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

للحصول على مسافة كافية بين الموضع السابق والحالي للماوس، سنحتاج إلى إبطاء المعدل الذي نحصل به على أحداث حركة الماوس. في هذا المثال، سنقوم ببساطة بتعيين علامة لتجاهل أحداث تحريك الماوس لمدة 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);
  }
};

وأخيرًا، ستجري دالة عرض StringMachine خلال جميع السلاسل وتستدعي طرق العرض الخاصة بها. وتعمل هذه الميزة طوال الوقت، وبسرعة تناسب requestAnimationFrame. يمكنك قراءة المزيد حول requestAnimationFrame في مقالة "بول أيرش" بعنوان requestAnimationFrame for smart animating.

في التطبيق الحقيقي، قد ترغب في تعيين علامة عند عدم حدوث أي رسوم متحركة لتوقف رسم إطار لوحة رسم جديد.

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

الخاتمة

إن وجود عنصر مسرح مشترك للتعامل مع جميع تفاعلاتنا لا يخلو من عيوبه. وهي تكون أكثر تعقيدًا من الناحية الحسابية، كما أن أحداث مؤشر المؤشر تكون محدودة بدون إضافة رمز إضافي لتغييرها. ومع ذلك، بالنسبة إلى JAM في Chrome، نجحت الفوائد الناتجة عن إمكانية استخلاص أحداث الماوس بعيدًا عن العناصر الفردية بشكل جيد للغاية. فهذا يتيح لنا إجراء المزيد من التجارب على تصميم الواجهة، والتبديل بين طرق إنشاء العناصر المتحركة، واستخدام رسومات موجّهة يمكن تغيير حجمها (SVG) لاستبدال صور الأشكال الأساسية، وتعطيل مناطق النتائج وغير ذلك بسهولة.

لمشاهدة الطبول والعضلات على قيد الحياة، ابدأ تشغيل جهاز JAM الخاص بك واختَر الطبول العادية أو الغيتار الكهربائي الكلاسيكي النظيف.

شعار Jam