我们如何打造出出色的界面
简介
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 和画布元素的组合将它们拼接在一起。
许多乐器都必须处理不同的互动方式(例如点击、拖动和弹拨 - 您预计使用乐器执行的所有操作),同时保持 DinahMoe 音效引擎的界面不变。我们发现,仅使用 JavaScript 的 mouseup 和 mousedown 并不足以提供出色的游戏体验。
为了处理所有这些变化,我们创建了一个覆盖可玩区域的“舞台”元素,用于处理所有不同乐器的点击、拖动和弹拨操作。
舞台
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 取消滚动事件延迟的文章,非常适合此处使用。
在处理任何点击检测之前,此第一个示例只会在鼠标在 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、设置多个字符串,并获取将在其上绘制的画布的上下文。
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 的判断。您可以参阅 Paul Irish 的文章 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);
}
};
小结
使用一个通用的 Stage 元素来处理所有交互并非没有缺点。计算更为复杂,而且如果不添加额外的代码来更改它们,光标指针事件会受到限制。不过,对于 JAM with Chrome,能够将鼠标事件从各个元素中提取出来非常有用。借助它,我们可以更多地尝试界面设计、在元素动画方法之间切换、使用 SVG 替换基本形状的图片、轻松停用感应区域等。
如需查看鼓和刺耳音效的效果,请开始创作自己的JAM,然后选择标准鼓或经典干净电吉他。