使用 Hand.js 创建跨浏览器触摸式操纵杆





5.00/5 (3投票s)
使用 Hand.js 创建跨浏览器触摸式操纵杆
我目前从事几个面向现代浏览器以及 Windows 应用商店应用的遊戲项目。其中一些项目基于 HTML5,以便简化多设备支持。然后,我开始寻找一种方法来处理所有平台的用户输入 - Windows 8 和 Windows RT、Windows Phone 8、iPad、Android 和 FirefoxOS。
正如您可能在我之前的文章 关于使用 Pointer Events 统一跨浏览器触摸和鼠标的文章 中所读到的,Windows 8、Windows RT 和 Windows Phone 8 上的 Internet Explorer 10 实现了我们提交给 W3C 的 Pointer Events 模型。Windows 7 上的 IE10 也对此有部分支持。为了以统一的方式处理这个 Pointer Events 模型以及 WebKit 浏览器中实现的模型,我们将使用 David Catuhe 的 HandJS 库。查看他的 关于 HandJS 作为 Polyfill 以支持所有浏览器指针事件的博文。其思想是针对 Pointer 模型,该库将把触摸事件传播到所有平台特定实现。
一旦我有了所有技术组件,我就在寻找一种实现虚拟触摸操纵杆的好方法。我不是非常喜欢触摸屏上的箭头键按钮。另一方面,虚拟模拟摇杆的位置通常不太好。但我最终发现 Seb Lee-Delisle 已经考虑过这个问题,并创造了一个很棒的概念,在他的文章 《用于 iPad 的 JavaScript/HTML5 多点触控游戏控制器》 中进行了描述。代码可在 GitHub 上找到:JSTouchController
我的想法是采用他的代码并重构其触摸组件,以针对 Pointer 模型而不是原始的 WebKit Touch 方法。几个月前在进行这项工作时,我发现 Google 的 Boris Smus 在他自己的库 Pointer.js 中已经开始做类似的事情,正如他在文章 《跨设备 Web 上的通用输入》 中所描述的那样。然而,当时 Boris 只是模仿了 IE10 Pointer Events 实现的一个旧版本,他的库在 IE10 中不起作用。这就是为什么我们决定自己开发版本。事实上,David 的库目前针对的是最新的、非常近期的 W3C 指针事件版本,该版本目前处于 最后呼吁草案状态。如果您查看这两个库,您还会发现 HandJS 在代码的几个部分使用了一些不同的方法。因此,在本文中我们将使用 HandJS 来构建我们的触摸操纵杆。
示例 1:指针跟踪器
此示例帮助您跟踪屏幕上的各种输入。它跟踪并跟随按下 canvas 元素的各种手指。它基于 Seb 在 GitHub 上的示例:Touches.html
有了 Hand.js,我们将使其兼容所有浏览器。它甚至还能跟踪触控笔和/或鼠标,具体取决于您当前正在测试的硬件类型!
在 Windows 8 或 iOS/Android/FirefoxOS 设备上(除了笔仅受 IE10 支持),在 Chrome 浏览器中,同一网页会提供相同的结果!有了 HandJS,只需编写一次,即可在任何地方运行!
正如您在视频中看到的,青色指针是“TOUCH”类型,而红色指针是“MOUSE”类型。如果您有触摸屏,可以通过在此 iframe 中嵌入的页面进行测试来获得相同的结果。
此示例在 Windows 8/RT 触摸设备、Windows Phone 8、iPad/iPhone 或 Android/FirefoxOS 设备上运行良好!如果您没有触摸设备,HandJS 将自动回退到鼠标。您应该能够使用鼠标至少跟踪 1 个指针。
让我们看看如何以统一的方式实现此结果。所有代码都位于 Touches.js 文件中。
在此代码中,我注册了 pointerdown/move/up 事件,正如我在 《MSPointer Events 入门文章》 中所述。在 pointerdown
处理程序中,我捕获了 ID、X 和 Y 坐标,以及指针类型(触摸、笔或鼠标),它们都包含在一个动态生成的对象中,该对象被推送到 pointers
集合对象中。此集合由指针的 ID 索引。集合对象在 Collection.js 中描述。然后,draw()
函数会枚举此集合,根据指针的位置以及其类型(触摸屏幕时)绘制一些青色/红色/亮绿色圆圈。它还在每个圆圈旁边添加一些文本以显示指针的详细信息。pointermove
处理程序负责更新集合中相关指针的坐标,而 pointerup
/out
则将其从集合中删除。Hand.JS 通过将 pointerdown/move/up/out 事件传播到 IE10 事件的相应 MSPointerDown/Move/Up/Out 以及 WebKit 浏览器的 touchstart/move/end 事件,使此代码兼容。
"use strict";
// shim layer with setTimeout fallback
// use this to requestAnimationFrame across browsers
window.requestAnimFrame = (function () {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
var pointers; // collections of pointers (regardless of input type)
var canvas,
c; // c is the canvas' context 2D
document.addEventListener("DOMContentLoaded", init);
// resize the canvas if user rotates slate or resizes browser window
window.onorientationchange = resetCanvas;
window.onresize = resetCanvas;
function init() {
setupCanvas();
pointers = new Collection();
canvas.addEventListener('pointerdown', onPointerDown, false);
canvas.addEventListener('pointermove', onPointerMove, false);
canvas.addEventListener('pointerup', onPointerUp, false);
canvas.addEventListener('pointerout', onPointerUp, false);
requestAnimFrame(draw);
}
function resetCanvas(e) {
// resize the canvas - but remember - this clears the canvas too.
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//make sure we scroll to the top left.
window.scrollTo(0, 0);
}
function draw() {
c.clearRect(0, 0, canvas.width, canvas.height);
pointers.forEach(function (pointer) {
c.beginPath();
c.fillStyle = "white";
c.fillText(pointer.type + " id : " + pointer.identifier + " x:" + pointer.x + " y:" +
pointer.y, pointer.x + 30, pointer.y - 30);
c.beginPath();
c.strokeStyle = pointer.color;
c.lineWidth = "6";
c.arc(pointer.x, pointer.y, 40, 0, Math.PI * 2, true);
c.stroke();
});
requestAnimFrame(draw);
}
// on detecting pointer events, create the pointer object to add to the collection
// for different input type, show different color and text
function createPointerObject(event) {
var type;
var color;
switch (event.pointerType) {
case event.POINTER_TYPE_MOUSE:
type = "MOUSE";
color = "red";
break;
case event.POINTER_TYPE_PEN:
type = "PEN";
color = "lime";
break;
case event.POINTER_TYPE_TOUCH:
type = "TOUCH";
color = "cyan";
break;
}
return { identifier: event.pointerId, x: event.clientX, y: event.clientY, type: type, color: color };
}
function onPointerDown(e) {
pointers.add(e.pointerId, createPointerObject(e));
}
function onPointerMove(e) {
if (pointers.item(e.pointerId)) {
pointers.item(e.pointerId).x = e.clientX;
pointers.item(e.pointerId).y = e.clientY;
}
}
function onPointerUp(e) {
pointers.remove(e.pointerId);
}
function setupCanvas() {
canvas = document.getElementById('canvasSurface');
c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
c.strokeStyle = "#ffffff";
c.lineWidth = 2;
}
如果您愿意,可以在 此处 查看完整的源代码。
示例 2:带简单宇宙飞船游戏的视频游戏控制器
现在让我们来看看我最感兴趣的示例——一个适用于 HTML5 游戏的模拟触摸板。用户应该能够在屏幕左侧的任意位置触摸。在该位置,canvas 将显示一个简单但非常有效的方向盘。移动您的手指(在按住的情况下)将更新虚拟触摸板并移动一架简单的宇宙飞船。触摸屏幕的右侧将显示一些红色的圆圈,这些圆圈将生成一些从宇宙飞船射出的子弹。再次强调,它基于 Seb 在 GitHub 上的示例:TouchControl.html。
如果您有触摸屏,您可以在下面的框中试玩这款游戏。
如果没有,您只能通过点击屏幕左侧来移动飞船,或者通过点击右侧进行射击,但您无法同时执行这两个操作。正如您所见,如果浏览器或平台不支持触摸,HandJS 会提供鼠标回退功能。
注意: iPad 似乎存在一个未知错误,导致第二个 iframe 无法正常工作。请在另一个标签页中直接打开示例,以便在您的 iPad 上正常工作。
让我们再次看看如何以统一的方式实现此结果。这次所有代码都位于 TouchControl.js 文件中,您可以在 此处 找到它。
// shim layer with setTimeout fallback
// use this to requestAnimationFrame across browsers
window.requestAnimFrame = (function () {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
var canvas,
c, // c is the canvas' context 2D
container,
halfWidth,
halfHeight,
leftPointerID = -1,
leftPointerPos = new Vector2(0, 0),
leftPointerStartPos = new Vector2(0, 0),
leftVector = new Vector2(0, 0);
// halfWidth and halfHeight are use to separate the screen
// so that we can decide whether to respond to touch joystick or bullet code
// Vector2 is a custom class for two-dimensional vectors
// the leftPointerStartPos is what we will us to place the joystick
// we will track joystick movement relative to it, using leftPointerPos
// in the Vector2 type object leftVector
// We will compute direction and speed of the spaceship based on value of leftVector
var pointers; // collections of pointers
var ship;
bullets = [],
spareBullets = [];
document.addEventListener("DOMContentLoaded", init);
// resize the canvas if user rotates slate or resizes browser window
window.onorientationchange = resetCanvas;
window.onresize = resetCanvas;
// associate the custom handJS events with their corresponding even listeners
function init() {
setupCanvas();
pointers = new Collection();
ship = new ShipMoving(halfWidth, halfHeight);
document.body.appendChild(ship.canvas);
canvas.addEventListener('pointerdown', onPointerDown, false);
canvas.addEventListener('pointermove', onPointerMove, false);
canvas.addEventListener('pointerup', onPointerUp, false);
canvas.addEventListener('pointerout', onPointerUp, false);
requestAnimFrame(draw);
}
function resetCanvas(e) {
// resize the canvas - but remember - this clears the canvas too.
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
halfWidth = canvas.width / 2;
halfHeight = canvas.height / 2;
//make sure we scroll to the top left.
window.scrollTo(0, 0);
}
function draw() {
c.clearRect(0, 0, canvas.width, canvas.height);
ship.targetVel.copyFrom(leftVector);
ship.targetVel.multiplyEq(0.15);
ship.update();
// if the ship moves offscreen left, redraw it re-entering from the right, and so on
with (ship.pos) {
if (x < 0) x = canvas.width;
else if (x > canvas.width) x = 0;
if (y < 0) y = canvas.height;
else if (y > canvas.height) y = 0;
}
ship.draw();
for (var i = 0; i < bullets.length; i++) {
var bullet = bullets[i];
if (!bullet.enabled) continue;
bullet.update();
bullet.draw(c);
if (!bullet.enabled) {
spareBullets.push(bullet);
}
}
pointers.forEach(function (pointer) {
if (pointer.identifier == leftPointerID) {
c.beginPath();
c.strokeStyle = "cyan";
c.lineWidth = 6;
c.arc(leftPointerStartPos.x, leftPointerStartPos.y, 40, 0, Math.PI * 2, true);
c.stroke();
c.beginPath();
c.strokeStyle = "cyan";
c.lineWidth = 2;
c.arc(leftPointerStartPos.x, leftPointerStartPos.y, 60, 0, Math.PI * 2, true);
c.stroke();
c.beginPath();
c.strokeStyle = "cyan";
c.arc(leftPointerPos.x, leftPointerPos.y, 40, 0, Math.PI * 2, true);
c.stroke();
} else {
c.beginPath();
c.fillStyle = "white";
c.fillText("type : " + pointer.type + " id : " + pointer.identifier + " x:" + pointer.x +
" y:" + pointer.y, pointer.x + 30, pointer.y - 30);
c.beginPath();
c.strokeStyle = "red";
c.lineWidth = "6";
c.arc(pointer.x, pointer.y, 40, 0, Math.PI * 2, true);
c.stroke();
}
});
requestAnimFrame(draw);
}
function makeBullet() {
var bullet;
if (spareBullets.length > 0) {
bullet = spareBullets.pop();
bullet.reset(ship.pos.x, ship.pos.y, ship.angle);
} else {
bullet = new Bullet(ship.pos.x, ship.pos.y, ship.angle);
bullets.push(bullet);
}
// make the bullet speed relative to the ship speed
bullet.vel.plusEq(ship.vel);
}
function givePointerType(event) {
switch (event.pointerType) {
case event.POINTER_TYPE_MOUSE:
return "MOUSE";
break;
case event.POINTER_TYPE_PEN:
return "PEN";
break;
case event.POINTER_TYPE_TOUCH:
return "TOUCH";
break;
}
}
// here is where we handle the logic for whether to present a joystick or bullet-firing experience
// only if there is no already existing joystick, and the user presses down on the left of the screen
// we should begin joystick calculations using leftVector
// else fire bullets
function onPointerDown(e) {
var newPointer = { identifier: e.pointerId, x: e.clientX, y: e.clientY, type: givePointerType(e) };
if ((leftPointerID < 0) && (e.clientX < halfWidth)) {
leftPointerID = e.pointerId;
leftPointerStartPos.reset(e.clientX, e.clientY);
leftPointerPos.copyFrom(leftPointerStartPos);
leftVector.reset(0, 0);
}
else {
makeBullet();
}
pointers.add(e.pointerId, newPointer);
}
// track motion of pointer to alter ships direction and speed
function onPointerMove(e) {
if (leftPointerID == e.pointerId) {
leftPointerPos.reset(e.clientX, e.clientY);
leftVector.copyFrom(leftPointerPos);
leftVector.minusEq(leftPointerStartPos);
}
else {
if (pointers.item(e.pointerId)) {
pointers.item(e.pointerId).x = e.clientX;
pointers.item(e.pointerId).y = e.clientY;
}
}
}
// release the joystick when user lifts up his finger/mouse
function onPointerUp(e) {
if (leftPointerID == e.pointerId) {
leftPointerID = -1;
leftVector.reset(0, 0);
}
leftVector.reset(0, 0);
pointers.remove(e.pointerId);
}
function setupCanvas() {
canvas = document.getElementById('canvasSurfaceGame');
c = canvas.getContext('2d');
resetCanvas();
c.strokeStyle = "#ffffff";
c.lineWidth = 2;
}
感谢 Seb Lee-Delisle 和 David Catuhe 的出色工作,您现在拥有了为您的 HTML5 游戏实现自己的虚拟触摸摇杆所需的所有组件。该结果将在所有支持 HTML5 的触摸设备上运行!
本文是 Internet Explorer 团队 HTML5 技术系列的一部分。通过 @ http://modern.IE 免费获得 3 个月的 BrowserStack 跨浏览器测试,亲身体验本文中的概念。
David Rousset 是微软的一名开发者布道师,专注于 HTML5 和 Web 开发。本文最初发布于他在 MSDN 上的博客 Coding4Fun,发布日期为 2013 年 2 月 22 日。您可以在 Twitter 上关注他:@davrous。