如何讓 UI 更出色
簡介
JAM with Chrome 是 Google 推出的網路音樂專案,透過 Chrome 進行 JAM 時,來自世界各地的使用者可以組成樂團,並在瀏覽器中即時進行 JAM。DinahMoe 團隊突破了 Chrome 的 Web Audio API 的限制,Tool of North America 團隊打造了這個介面,讓使用者可以彈撥、敲打和演奏電腦,就像是樂器一樣。
在 Google Creative Lab 的創意指導下,插畫家 Rob Bailey 為 JAM 可使用的 19 種樂器,各自繪製精緻的插圖。根據這些資訊,互動式總監 Ben Tricklebank 和我們的工具設計團隊為每個樂器建立了簡單專業的介面。
由於每個樂器的視覺效果都各不相同,因此 Tool 的技術總監 Bartek Drozdz 和我使用 PNG 圖片、CSS、SVG 和 Canvas 元素的組合,將這些樂器拼接在一起。
許多樂器都必須處理不同的互動方式 (例如點擊、拖曳和撥弦 - 所有您預期樂器會執行的操作),同時保持與 DinahMoe 音效引擎的介面相同。我們發現,除了 JavaScript 的 mouseup 和 mousedown 之外,還需要其他元素才能提供絕佳的遊戲體驗。
為了處理所有這些變化,我們建立了涵蓋可播放區域的「舞台」元素,處理所有不同樂器的點擊、拖曳和撥弦動作。
階段
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」元素的座標。為此,我們需要考量頁面中的 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 debounce 捲動事件,這在本例中會很實用。
在處理任何觸發偵測之前,這個第一個範例會在滑鼠在「Stage」區域移動時,輸出相對的 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 物件,並將要用於 Stage 的 div ID 傳遞給該物件。
//-- 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 物件中加入函式,才能將其登錄為命中區域。
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);
其他形狀
形狀越複雜,用來判斷點是否位於形狀內的數學運算就會越複雜。不過,這些等式已建立且在許多線上資源中都有詳細記錄。我看過的最佳 JavaScript 範例之一,就是 Kevin Lindsey 的幾何學程式庫。
很幸的是,在使用 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 函式會採用畫布 ID 和 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;
};
交集和彈撥
字串的觸發區域又會變成方塊。按一下該方塊即可觸發字串動畫。不過,誰會想按吉他呢?
如要加入撥弦效果,我們需要檢查弦樂盒與使用者滑鼠移動路徑的交點。
為了讓滑鼠先前和目前的位置有足夠的距離,我們需要放慢取得滑鼠移動事件的速度。在本例中,我們只需設定標記,即可在 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);
};
接下來,我們需要使用 Kevin Lindsey 編寫的部分交錯程式碼,查看滑鼠移動的線是否與矩形中間交錯。
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;
};
最後,我們會新增一個函式來建立弦樂器。它會建立新的 Stage、設定多個字串,並取得繪製作業的 Canvas 內容。
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 with Chrome 來說,能夠將滑鼠事件抽象化,不必與個別元素互動,這項做法非常實用。這讓我們可以進一步嘗試介面設計、切換元素動畫方法、使用 SVG 取代基本圖形的圖片,以及輕鬆停用觸發區等等。
如要瞭解鼓和弦樂器的實際運作情形,請建立自己的JAM,然後選取「標準鼓」或「經典清晰電吉他」。