हमने यूज़र इंटरफ़ेस (यूआई) को बेहतर कैसे बनाया
परिचय
Chrome के साथ JAM, Google का बनाया गया एक वेब-आधारित म्यूज़िकल प्रोजेक्ट है. 'Chrome के साथ JAM करें' सुविधा की मदद से, दुनिया भर के लोग ब्राउज़र में रीयल टाइम में एक साथ संगीत बना सकते हैं. DinahMoe ने Chrome के वेब ऑडियो एपीआई की मदद से, कई नई चीज़ें की हैं. Tool of North America की हमारी टीम ने एक ऐसा इंटरफ़ेस बनाया है जिसकी मदद से, कंप्यूटर को किसी संगीत वाद्य की तरह इस्तेमाल किया जा सकता है. जैसे, स्ट्रिंग वाले वाद्य को बजाना, ड्रम बजाना वगैरह.
Google Creative Lab के क्रिएटिव डायरेक्शन के तहत, इलस्ट्रेटर रॉब बेली ने JAM के लिए उपलब्ध 19 इंस्ट्रूमेंट के हर एक के लिए, बेहतरीन इलस्ट्रेशन बनाए हैं. इन सुझावों के आधार पर, इंटरैक्टिव डायरेक्टर बेन ट्रिकलबैंक और Tool की हमारी डिज़ाइन टीम ने हर इंस्ट्रूमेंट के लिए आसान और बेहतर इंटरफ़ेस बनाया.
हर इंस्ट्रूमेंट का विज़ुअल अलग होता है. इसलिए, Tool के तकनीकी डायरेक्टर Bartek Drozdz और मैंने PNG इमेज, सीएसएस, SVG, और कैनवस एलिमेंट के कॉम्बिनेशन का इस्तेमाल करके, उन्हें एक साथ जोड़ा.
कई इंस्ट्रूमेंट के लिए, इंटरैक्शन के अलग-अलग तरीकों को हैंडल करना पड़ा. जैसे, क्लिक, खींचने और स्ट्रीम करने जैसे सभी काम, जो किसी इंस्ट्रूमेंट से किए जा सकते हैं. साथ ही, DinahMoe के साउंड इंजन के इंटरफ़ेस को भी पहले जैसा ही रखा गया. हमें पता चला कि गेम खेलने का बेहतर अनुभव देने के लिए, हमें JavaScript के mouseup और mousedown के अलावा और भी चीज़ों की ज़रूरत है.
इन सभी बदलावों को ध्यान में रखते हुए, हमने एक “स्टेज” एलिमेंट बनाया है. यह एलिमेंट, बजाए जा सकने वाले हिस्से को कवर करता है. साथ ही, सभी अलग-अलग इंस्ट्रूमेंट पर क्लिक, खींचने, और स्ट्रीम करने की सुविधा देता है.
The 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
};
एलिमेंट और माउस की पोज़िशन पाना
हमारा पहला काम, ब्राउज़र विंडो में माउस के निर्देशांक को हमारे स्टेज एलिमेंट के हिसाब से बदलना है. ऐसा करने के लिए, हमें यह ध्यान रखना था कि पेज में हमारा स्टेज कहां है.
हमें यह पता लगाना होता है कि एलिमेंट, सिर्फ़ अपने पैरंट एलिमेंट के हिसाब से नहीं, बल्कि पूरी विंडो के हिसाब से कहां है. इसलिए, एलिमेंट के 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);
};
माउस की गति को देखने के लिए, हम एक नया स्टेज ऑब्जेक्ट बनाएंगे और उसे उस डिव का आईडी पास करेंगे जिसका इस्तेमाल हमें स्टेज के तौर पर करना है.
//-- 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 फ़ंक्शन, कैनवस आईडी और 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 मिलीसेकंड के लिए mousemove इवेंट को अनदेखा करने के लिए, सिर्फ़ एक फ़्लैग सेट करेंगे.
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 का इस्तेमाल करने पर, अलग-अलग एलिमेंट से माउस इवेंट को अलग करने की सुविधा का फ़ायदा बहुत अच्छा हुआ. इसकी मदद से, हम इंटरफ़ेस डिज़ाइन के साथ ज़्यादा प्रयोग कर सकते हैं. साथ ही, एलिमेंट को ऐनिमेट करने के तरीकों के बीच स्विच कर सकते हैं. इसके अलावा, बुनियादी आकार की इमेज को बदलने के लिए एसवीजी का इस्तेमाल किया जा सकता है. साथ ही, हिट एरिया को आसानी से बंद किया जा सकता है.
ड्रम और स्टिंग को इस्तेमाल करते हुए देखने के लिए, अपना JAM शुरू करें और स्टैंडर्ड ड्रम या क्लासिक क्लीन इलेक्ट्रिक गिटार चुनें.