مطالعه موردی - JAM با Chrome

چگونه ما رابط کاربری را سنگ تمام کردیم

مقدمه

JAM with Chrome یک پروژه موسیقی مبتنی بر وب است که توسط Google ایجاد شده است. JAM با Chrome به مردم از سرتاسر جهان اجازه می‌دهد تا در مرورگر باند و JAM تشکیل دهند. DinahMoe مرزهای آنچه را که با Chrome's Web Audio API امکان پذیر بود، جابه جا کرد، تیم ما در Tool of North America رابطی را برای کوبیدن، طبل زدن و نواختن رایانه شما طوری ایجاد کرد که گویی یک ساز موسیقی است.

راب بیلی، تصویرگر، با هدایت خلاقانه Google Creative Lab، تصاویر پیچیده ای را برای هر یک از 19 ابزار موجود در JAM ایجاد کرد. در کنار اینها، مدیر تعاملی بن تریکلبنک و تیم طراحی ما در Tool یک رابط کاربری آسان و حرفه ای برای هر ابزار ایجاد کردند.

جم کامل مونتاژ

از آنجایی که هر ساز از نظر بصری منحصر به فرد است، مدیر فنی Tool، Bartek Drozdz و من آنها را با استفاده از ترکیبی از تصاویر PNG، CSS، SVG و عناصر Canvas به هم دوختیم.

بسیاری از سازها مجبور بودند از روش‌های مختلف تعامل (مانند کلیک‌ها، کشیدن‌ها و استرام‌ها - همه کارهایی که انتظار دارید با یک ساز انجام دهید) استفاده می‌کردند، در حالی که رابط کاربری با موتور صوتی DinahMoe یکسان بود. متوجه شدیم که برای ارائه یک تجربه بازی زیبا به چیزی بیش از موس و جاوا اسکریپت نیاز داریم.

برای مقابله با همه این تغییرات، ما یک عنصر "Stage" ایجاد کردیم که منطقه قابل پخش را پوشش می داد، کلیک ها، درگ ها و استرام ها را در تمام سازهای مختلف مدیریت می کرد.

صحنه

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

گرفتن عنصر و موقعیت ماوس

اولین وظیفه ما این است که مختصات ماوس را در پنجره مرورگر ترجمه کنیم تا نسبت به عنصر Stage ما باشد. برای انجام این کار باید در نظر بگیریم که مرحله ما در کجای صفحه قرار دارد.

از آنجایی که باید مکان عنصر نسبت به کل پنجره را پیدا کنیم، نه فقط عنصر اصلی آن، کمی پیچیده تر از نگاه کردن به عناصر 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 حرکت می‌کند، خروجی می‌دهد.

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

شکل های بیشتر

همانطور که اشکال پیچیده تر می شوند، ریاضیات برای یافتن اینکه آیا یک نقطه در داخل آنها قرار دارد پیچیده تر می شود. با این حال، این معادلات به خوبی تثبیت شده اند و در بسیاری از مکان های آنلاین با جزئیات بسیار مستند شده اند. برخی از بهترین نمونه های جاوا اسکریپت که من دیده ام از کتابخانه هندسه کوین لیندزی است.

خوشبختانه در ساختن JAM با کروم، هرگز مجبور نبودیم از دایره ها و مستطیل ها فراتر برویم، و با تکیه بر ترکیبی از اشکال و لایه بندی برای رسیدگی به هر گونه پیچیدگی اضافی.

اشکال طبل

حلقه ها

برای بررسی اینکه آیا یک نقطه در داخل یک درام دایره ای قرار دارد، باید یک شکل پایه دایره ای ایجاد کنیم. اگرچه کاملاً شبیه به مستطیل است، اما روش‌های خاص خود را برای تعیین کرانه‌ها و بررسی اینکه آیا نقطه در داخل دایره است یا خیر، دارد.

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

به جای تغییر رنگ، افزودن کلاس hit باعث ایجاد یک انیمیشن 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;
}

هنگامی که می خواهیم آن را ارتعاش کنیم، تابع 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;
};

تقاطع ها و ضربه زدن

منطقه ضربه ما برای رشته فقط یک جعبه خواهد بود. کلیک کردن در آن کادر باید انیمیشن رشته را فعال کند. اما چه کسی می خواهد روی یک گیتار کلیک کند؟

برای اضافه کردن strumming باید محل تقاطع کادر رشته ها و خطی که ماوس کاربر در حال حرکت است را بررسی کنیم.

برای به دست آوردن فاصله کافی بین موقعیت قبلی و فعلی ماوس، باید سرعتی را که با آن رویدادهای حرکت ماوس را دریافت می کنیم، کاهش دهیم. برای این مثال، ما به سادگی یک پرچم برای نادیده گرفتن رویدادهای ماوس به مدت 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);
  }
};

در نهایت تابع رندر StringInstrument در تمام رشته های ما حلقه زده و متدهای رندر آنها را فراخوانی می کند. آن را همیشه اجرا می کند، به سرعت به عنوان 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 با کروم، مزایای امکان انتزاع رویدادهای ماوس به دور از عناصر منفرد واقعاً خوب عمل کرد. این به ما اجازه می دهد طراحی رابط را بیشتر آزمایش کنیم، بین روش های متحرک سازی عناصر تغییر دهیم، از SVG برای جایگزینی تصاویر اشکال اصلی استفاده کنیم، مناطق ضربه را به راحتی غیرفعال کنیم و موارد دیگر.

برای مشاهده درام ها و نیش ها در حال اجرا، JAM خود را شروع کنید و درام های استاندارد یا گیتار الکتریک کلاسیک را انتخاب کنید.

لوگو جام