केस-स्टडी - Chrome के साथ JAM

हमने यूज़र इंटरफ़ेस (यूआई) को शानदार कैसे बनाया

शुरुआती जानकारी

Chrome के साथ JAM, वेब पर आधारित एक म्यूज़िकल प्रोजेक्ट है, जिसे Google ने बनाया है. Chrome के साथ JAM, दुनिया भर के लोगों को ब्राउज़र में रीयल टाइम में बैंड और JAM बनाने की सुविधा देता है. DinahMoe ने Chrome के Web Audio API की मदद से, नई-नई संभावनाएं तलाशी हैं. Tool of North America की हमारी टीम ने इस डिवाइस के इंटरफ़ेस को इस तरह से बनाया है कि आपके कंप्यूटर में संगीत इंस्ट्रुमेंट की तरह ही ड्रम और ड्रम बजाई जा सकती है.

Google Creative Lab के क्रिएटिव निर्देशों का इस्तेमाल करके, इलस्ट्रेटर रॉब बेली ने JAM के लिए उपलब्ध 19 इंस्ट्रुमेंट में से हर एक के लिए मुश्किल इलस्ट्रेशन बनाए. इन वादियों की मदद से, इंटरैक्टिव डायरेक्टर बेन ट्रिकलबैंक और टूल की हमारी डिज़ाइन टीम ने हर इंस्ट्रुमेंट के लिए एक आसान और पेशेवर इंटरफ़ेस बनाया.

फ़ुल जैम मोंटाज

हर इंस्ट्रुमेंट विज़ुअल तौर पर यूनीक होता है. इसलिए, टूल के टेक्निकल डायरेक्टर Bartek Drozdz और मैंने उन्हें PNG इमेज, CSS, SVG, और कैनवस एलिमेंट के कॉम्बिनेशन का इस्तेमाल करके एक साथ बनाया.

कई इंस्ट्रुमेंट को 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
};

एलिमेंट और माउस की पोज़िशन हासिल करना

हमारा पहला काम है ब्राउज़र विंडो में माउस निर्देशांकों का अनुवाद करना, ताकि वे हमारे स्टेज एलिमेंट के बराबर हों. ऐसा करने के लिए, हमें यह देखना ज़रूरी था कि पेज में हमारा स्टेज कहां है.

हमारे लिए यह पता करना है कि एलिमेंट, सिर्फ़ उसके पैरंट एलिमेंट ही नहीं, बल्कि पूरी विंडो के किस हिस्से में है, इसलिए यह सिर्फ़ ऑफ़सेटटॉप और ऑफ़सेटलेफ़्ट को देखने से थोड़ा मुश्किल है. सबसे आसान विकल्प 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 का इस्तेमाल कर रहे हैं, तो ऑफ़सेट() फ़ंक्शन अलग-अलग प्लैटफ़ॉर्म पर जगह की जानकारी निकालने की जटिलता को संभालकर इस्तेमाल करता है, लेकिन आपको अब भी स्क्रोल की गई रकम को घटाना होगा.

जब भी पेज को स्क्रोल किया जाता है या उसका साइज़ बदला जाता है, तो हो सकता है कि एलिमेंट की जगह बदल गई हो. हम इन इवेंट को सुन सकते हैं और फिर से स्थिति का पता लगा सकते हैं. ये इवेंट सामान्य स्क्रोल या साइज़ बदलने पर कई बार ट्रिगर होते हैं. इसलिए, असल में यह सबसे अच्छा विकल्प है कि आप पोज़िशन को फिर से जांचें की संख्या को सीमित करें. ऐसा करने के कई तरीके हैं, लेकिन 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);
};

माउस की गतिविधि देखना शुरू करने के लिए, हम एक नया स्टेज ऑब्जेक्ट बनाएंगे और उसे उस div की आईडी को पास कर देंगे जिसे हम अपने स्टेज के रूप में इस्तेमाल करना चाहते हैं.

//-- Create a new Stage object, for a div with id of "stage"
var stage = new Stage("stage");

आसान हिट पहचान

Chrome के साथ JAM में सभी इंस्ट्रुमेंट इंटरफ़ेस जटिल नहीं होते. हमारे ड्रम मशीन पैड सिर्फ़ सामान्य रेक्टैंगल हैं. इनकी मदद से, यह आसानी से पता लगाया जा सकता है कि कोई क्लिक अपनी सीमा में आता है या नहीं.

ड्रम मशीन

आयतों से शुरू करते हुए, हम कुछ बुनियादी आकार सेट अप करेंगे. हर आकार ऑब्जेक्ट के लिए उसकी सीमाओं को जानना ज़रूरी है. साथ ही, उसके अंदर यह जांचने की क्षमता होनी चाहिए कि कोई बिंदु उसके अंदर है या नहीं.

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 इसके लिए हिट हैं और सही या गलत रिटर्न करते हैं.

हम स्टेज एलिमेंट में "ऐक्टिव" क्लास भी जोड़ सकते हैं. यह क्लास के ऊपर कर्सर घुमाने पर, माउस कर्सर को पॉइंटर में बदल देगा.

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 के कुछ सबसे अच्छे उदाहरण केविन लिंड्ज़ी की जियोमेट्री लाइब्रेरी से मिले हैं.

अच्छी बात यह है कि Chrome के साथ 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;
};

रंग बदलने के बजाय, हिट क्लास जोड़ने से 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 फ़ंक्शन, कैनवस आईडी और रेक्टैंगल ऑब्जेक्ट लेगा और उस रेक्टैंगल के बीच में एक लाइन बनाएगा.

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 मिलीसेकंड तक माउसmove इवेंट को अनदेखा करने के लिए, एक फ़्लैग सेट करेंगे.

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

आखिर में खास जानकारी

हमारे सभी इंटरैक्शन को हैंडल करने के लिए, एक ही स्टेज एलिमेंट का होना, इसकी कमियों के बिना नहीं है. यह कंप्यूटेशनल रूप से ज़्यादा मुश्किल है. साथ ही, कर्सर पॉइंटर इवेंट को बदलने के लिए उनमें अतिरिक्त कोड जोड़े बिना ही इन्हें बदला जा सकता है. हालांकि, Chrome के साथ JAM के लिए, अलग-अलग एलिमेंट से माउस इवेंट को अलग करके देख पाना, काफ़ी फ़ायदेमंद साबित हुआ. इसकी मदद से, हम इंटरफ़ेस के डिज़ाइन के साथ ज़्यादा प्रयोग कर सकते हैं, एलिमेंट को ऐनिमेट करने के तरीकों के बीच स्विच कर सकते हैं, बुनियादी आकार की इमेज बदलने के लिए SVG का इस्तेमाल कर सकते हैं, हिट एरिया को आसानी से बंद कर सकते हैं वगैरह.

ड्रम और स्टिंग को इस्तेमाल करते हुए देखने के लिए, खुद का JAM शुरू करें. इसके बाद, स्टैंडर्ड ड्रम या क्लासिक क्लीन इलेक्ट्रिक गिटार चुनें.

Jam का लोगो