1 /** 2 * arcade-controls.js 3 * Copyright (c) 2010-2011, Martin Wendt (http://wwWendt.de) 4 * 5 * Dual licensed under the MIT or GPL Version 2 licenses. 6 * http://code.google.com/p/arcade-js/wiki/LicenseInfo 7 * 8 * A current version and some documentation is available at 9 * http://arcade-js.googlecode.com/ 10 * 11 * @fileOverview Controls and tools for ArcadeJS on mobile devices. 12 * 13 * @author Martin Wendt 14 * @version 0.0.1 15 */ 16 17 function _getTouchWithId(touchList, id){ 18 // `0` is a valid id on Android! 19 if(id !== null && touchList && touchList.length){ 20 for(var i=0; i<touchList.length; i++) { 21 var touch = touchList[i]; 22 if(touch.identifier === id){ 23 return touch; 24 } 25 } 26 } 27 return null; 28 } 29 30 /*----------------------------------------------------------------------------*/ 31 32 /**Base class for screen controls. 33 * 34 * As opposed to a 'normal' Movable, this class sets pos to `undefined` and uses 35 * posCC instead. 36 * 37 * @class 38 * @extends Movable 39 */ 40 41 var CanvasObject = Movable.extend( 42 /** @lends CanvasObject.prototype */ 43 { 44 init: function(type, opts) { 45 this._super(type, $.extend({ 46 useCC: true, 47 isBackground: true, 48 onClick: function() { alert("onClick is mandatory"); } 49 }, opts)); 50 // Copy selected options as object attributes 51 ArcadeJS.extendAttributes(this, this.opts, "onClick onResize useCC isBackground"); 52 // ArcadeJS.assertAttributes(this, this.opts, "onClick pos useCC"); 53 }, 54 /**Return true, if point hits this object. 55 * @param {Point2} pt Point in canvas coordinates 56 * @returns {boolean} 57 */ 58 containsCC: undefined, 59 contains: undefined, // in useCC mode, this shouldn't be called 60 /**@function Called when window is resized (and on start). 61 * The default processing depends on the 'resizeMode' option. 62 * @param {Int} width 63 * @param {Int} height 64 * @param {Event} e 65 * @returns false to prevent default handling 66 */ 67 onResize: undefined, 68 /**Called when button was clicked (i.e. pushed and released). */ 69 onClick: undefined, 70 // --- end of class 71 __lastentry: undefined 72 }); 73 74 /*----------------------------------------------------------------------------*/ 75 76 /**Button for mouse touch screen devices. 77 * @class 78 * @extends CanvasObject 79 */ 80 81 var TouchButton = CanvasObject.extend( 82 /** @lends TouchButton.prototype */ 83 { 84 init: function(opts) { 85 this._super("button", $.extend({ 86 r: 20, 87 r3: 40//, 88 // onClick: function() { alert("onClick is mandatory"); } 89 }, opts)); 90 // Copy selected options as object attributes 91 ArcadeJS.extendAttributes(this, this.opts, "r r3 onClick"); 92 this.touchDownId = null; 93 this.clicked = false; 94 this.down = false; 95 }, 96 containsCC: function(ptCC) { 97 return this.pos.distanceTo(ptCC) <= this.r3; 98 }, 99 render: function(ctx) { 100 // Draw gray sphere 101 var gradient = ctx.createRadialGradient(0, 0, 0, 5, -5, this.r); 102 gradient.addColorStop(0, "rgba(255, 255, 255, 0.7)"); 103 if(!this.down){ 104 gradient.addColorStop(0.7, "rgba(192, 192, 192, 0.7)"); 105 } 106 gradient.addColorStop(1, "rgba(80, 80, 80, 0.7)"); 107 ctx.fillStyle = gradient; 108 ctx.fillCircle2(0, 0, this.r); 109 }, 110 onMousedown: function(e) { 111 this.down = this.clicked = this.containsCC(this.game.mousePosCC); 112 }, 113 onMousemove: function(e) { 114 this.down = this.clicked && this.containsCC(this.game.mousePosCC); 115 }, 116 onMouseup: function(e) { 117 if(this.clicked && this.containsCC(this.game.mousePosCC)){ 118 this.onClick.call(this); 119 } 120 this.down = this.clicked = false; 121 }, 122 onTouchevent: function(e, orgEvent) { 123 var touch = null, 124 game = this.game; 125 if(this.touchDownId !== null){ 126 touch = _getTouchWithId(orgEvent.changedTouches, this.touchDownId); 127 }else if(e.type == "touchstart" && orgEvent.changedTouches.length == 1) { 128 touch = orgEvent.changedTouches[0]; 129 } 130 // alert("e.type: " + e.type + ", id=" + (touch ? touch.identifier : "?")); 131 // Ignore event, if touch identifier is different from start event 132 if(!touch){ 133 return; 134 } 135 // Otherwise, prevent default handling 136 orgEvent.preventDefault(); 137 138 var touchPos = new Point2( 139 touch.pageX - game.canvas.offsetLeft, 140 touch.pageY - game.canvas.offsetTop); 141 var isInside = this.containsCC(touchPos); 142 143 // TODO: seems that we get touchend for both fingers, even if only the other 144 // finger was lifted! 145 // http://stackoverflow.com/questions/3695128/webkit-iphone-ipad-issue-with-mutl-touch 146 // if(e.type!="touchmove"){ 147 // game.debug("button " + e.type + " - isInside: " + isInside + ", drag: " + this.touchDragOffset + ", id=" + touch.identifier); 148 // } 149 150 switch (e.type) { 151 case "touchstart": 152 case "touchmove": 153 this.down = isInside; 154 if(isInside){ 155 this.touchDownId = touch.identifier; 156 } 157 break; 158 case "touchend": 159 if(this.down && isInside){ 160 this.onClick.call(this); 161 } 162 this.touchDownId = null; 163 this.down = false; 164 break; 165 case "touchcancel": 166 this.touchDownId = null; 167 this.down = false; 168 break; 169 default: 170 alert("not handled " + e.type); 171 } 172 }, 173 /**Return true if button is down (but mouse key is also still down). */ 174 isDown: function() { 175 return this.down === true; 176 }, 177 /**Called when button was clicked (i.e. pushed and released). */ 178 onClick: undefined, 179 // --- end of class 180 __lastentry: undefined 181 }); 182 183 /*----------------------------------------------------------------------------*/ 184 185 /**Joystick emulation for mouse and touch screen devices. 186 * @class 187 * @extends CanvasObject 188 */ 189 var TouchStick = CanvasObject.extend( 190 /** @lends TouchStick.prototype */ 191 { 192 init: function(opts) { 193 this._super("joystick", $.extend({ 194 r1: 10, 195 r2: 30, 196 r3: 50 197 }, opts)); 198 // Copy selected options as object attributes 199 ArcadeJS.extendAttributes(this, this.opts, "r1 r2 r3"); 200 this.active = false; 201 this.touchDownPos = null; 202 this.touchDownId = null; 203 this.touchDragOffset = null; 204 }, 205 containsCC: function(ptCC) { 206 return this.pos.distanceTo(ptCC) <= this.r3; 207 }, 208 render: function(ctx) { 209 // Draw gray sphere 210 var gradient = ctx.createRadialGradient(0, 0, this.r1, 5, -5, this.r2); 211 gradient.addColorStop(0, "rgba(255, 255, 255, 0.7)"); 212 gradient.addColorStop(0.7, "rgba(192, 192, 192, 0.7)"); 213 gradient.addColorStop(1, "rgba(80, 80, 80, 0.7)"); 214 ctx.fillStyle = gradient; 215 ctx.fillCircle2(0, 0, this.r2); 216 // with the dragged stick 217 var pos2 = new Point2(0, 0); 218 if(this.touchDragOffset){ 219 pos2.translate(this.touchDragOffset.limit(this.r2)); 220 } 221 var gradient = ctx.createRadialGradient(pos2.x, pos2.y, 0, pos2.x+3, pos2.y-3, this.r1); 222 gradient.addColorStop(0, "#fff"); 223 gradient.addColorStop(0.7, "#ccc"); 224 gradient.addColorStop(1, "#555"); 225 ctx.fillStyle = gradient; 226 ctx.fillCircle2(pos2.x, pos2.y, this.r1); 227 }, 228 onDragstart: function(clickPos) { 229 if(this.containsCC(this.game.mousePosCC)){ 230 this.touchDownPos = this.pos; 231 this.touchDragOffset = new Vec2(0, 0); 232 return true; 233 } 234 this.touchDownPos = null; 235 return false; 236 }, 237 onDrag: function(dragOffset) { 238 if(this.touchDownPos){ 239 this.touchDragOffset = dragOffset.copy(); 240 } 241 }, 242 onDragcancel: function(dragOffset) { 243 this.touchDownPos = this.touchDownId = this.touchDragOffset = null; 244 }, 245 onDrop: function(dragOffset) { 246 this.touchDownPos = this.touchDownId = this.touchDragOffset = null; 247 }, 248 onTouchevent: function(e, orgEvent) { 249 // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html#//apple_ref/doc/uid/TP40006511-SW1 250 // http://www.sitepen.com/blog/2008/07/10/touching-and-gesturing-on-the-iphone/ 251 var touch = null, 252 game = this.game; 253 if(this.touchDownId !== null){ 254 touch = _getTouchWithId(orgEvent.changedTouches, this.touchDownId); 255 }else if(e.type == "touchstart" && orgEvent.changedTouches.length == 1) { 256 touch = orgEvent.changedTouches[0]; 257 } 258 // Ignore event, if touch identifier is different from start event 259 if(!touch){ 260 return; 261 } 262 263 // Otherwise, prevent default handling 264 orgEvent.preventDefault(); 265 266 var touchPos = new Point2( 267 touch.pageX - game.canvas.offsetLeft, 268 touch.pageY - game.canvas.offsetTop); 269 // if(e.type!="touchmove"){ 270 // game.debug("stick " + e.type + " - isInside: " + this.contains(touchPos) + ", drag: " + this.touchDragOffset + ", id=" + touch.identifier); 271 // } 272 // TODO: seems that we get touchend for both fingers, even if only the other 273 // finger was lifted! 274 // http://stackoverflow.com/questions/3695128/webkit-iphone-ipad-issue-with-mutl-touch 275 switch (e.type) { 276 case "touchstart": 277 if(this.containsCC(touchPos)){ 278 this.touchDownPos = touchPos; 279 this.touchDragOffset = new Vec2(0, 0); 280 this.touchDownId = touch.identifier; 281 } 282 break; 283 case "touchmove": 284 // Drag vector is always relative to controls center 285 this.touchDragOffset = new Vec2( 286 touchPos.x - this.pos.x, 287 touchPos.y - this.pos.y); 288 break; 289 case "touchend": 290 case "touchcancel": 291 this.touchDownPos = this.touchDownId = this.touchDragOffset = null; 292 break; 293 } 294 }, 295 /**Return true if joystick is currently used. */ 296 isActive: function() { 297 return !!this.touchDownPos; 298 }, 299 /**Return x deflection [-1.0 .. +1.0]. */ 300 getX: function() { 301 return this.isActive() ? this.touchDragOffset.dx / this.r2 : 0; 302 }, 303 /**Return y deflection [-1.0 .. +1.0]. */ 304 getY: function() { 305 return this.isActive() ? this.touchDragOffset.dy / this.r2 : 0; 306 }, 307 /**Return deflection vector with length [0..r2]. */ 308 getDeflection: function() { 309 return this.isActive() ? this.touchDragOffset.copy().normalize() : 0; 310 }, 311 /**Called when button was clicked (i.e. pushed and released). */ 312 onClick: undefined, 313 // --- end of class 314 __lastentry: undefined 315 }); 316 317 /* *Text area with click event. 318 * @class 319 * @extends CanvasObject 320 */ 321 /* 322 var TouchArea = CanvasObject.extend( 323 / ** @lends TouchArea.prototype * / 324 { 325 init: function(opts) { 326 this._super("touchArea", $.extend({ 327 width: 20, 328 height: 40, 329 text: "Ok\nsoweit?", 330 border: true, 331 onResize: function(width, height){ 332 this.textWidth = this.game.context.measureText(this.text).width; 333 this.textHeight = this.game.context.measureText("M").width; 334 this.box = { 335 top: 0.5 * (height - this.textHeight) - this.padding, 336 left: 0.5 * (width - this.textWidth) - this.padding, 337 width: this.textWidth + 2 * this.padding, 338 height: this.textHeight + 2 * this.padding 339 }; 340 this.pos.set(this.box.left + this.padding, this.box.top + this.padding + this.textHeight); 341 }, 342 padding: 5 343 // onClick: function() { alert("onClick is mandatory"); } 344 }, opts)); 345 // Copy selected options as object attributes 346 ArcadeJS.extendAttributes(this, this.opts, "width height padding text onClick"); 347 this.touchDownId = null; 348 this.clicked = false; 349 this.down = false; 350 }, 351 containsCC: function(ptCC) { 352 var box = this.box; 353 return ptCC.x >= box.left && ptCC.x <= box.left + box.width 354 ptCC.y >= box.top && ptCC.y <= box.top + box.height; 355 }, 356 render: function(ctx) { 357 ctx.save(); 358 ctx.resetTransform(); // TODO: why is this required? 359 var box = this.box; 360 ctx.strokeRect(box.left, box.top, box.width, box.height); 361 ctx.strokeText(this.text, this.pos.x, this.pos.y); 362 ctx.restore(); 363 }, 364 onMousedown: function(e) { 365 this.down = this.clicked = this.containsCC(this.game.mousePosCC); 366 }, 367 onMousemove: function(e) { 368 this.down = this.clicked && this.containsCC(this.game.mousePosCC); 369 }, 370 onMouseup: function(e) { 371 if(this.clicked && this.containsCC(this.game.mousePosCC)){ 372 this.onClick.call(this); 373 } 374 this.down = this.clicked = false; 375 }, 376 onTouchevent: function(e, orgEvent) { 377 var touch = null, 378 game = this.game; 379 if(this.touchDownId !== null){ 380 touch = _getTouchWithId(orgEvent.changedTouches, this.touchDownId); 381 }else if(e.type == "touchstart" && orgEvent.changedTouches.length == 1) { 382 touch = orgEvent.changedTouches[0]; 383 } 384 // Ignore event, if touch identifier is different from start event 385 if(!touch){ 386 return; 387 } 388 // Otherwise, prevent default handling 389 orgEvent.preventDefault(); 390 391 var touchPos = new Point2( 392 touch.pageX - game.canvas.offsetLeft, 393 touch.pageY - game.canvas.offsetTop); 394 var isInside = this.containsCC(touchPos); 395 396 switch (e.type) { 397 case "touchstart": 398 case "touchmove": 399 this.down = isInside; 400 if(isInside){ 401 this.touchDownId = touch.identifier; 402 } 403 break; 404 case "touchend": 405 if(this.down && isInside){ 406 this.onClick.call(this); 407 } 408 this.touchDownId = null; 409 this.down = false; 410 break; 411 case "touchcancel": 412 this.touchDownId = null; 413 this.down = false; 414 break; 415 default: 416 alert("not handled " + e.type); 417 } 418 }, 419 / **Return true if button is down (but mouse key is also still down). * / 420 isDown: function() { 421 return this.down === true; 422 }, 423 / **Called when button was clicked (i.e. pushed and released). * / 424 onClick: undefined, 425 // --- end of class 426 __lastentry: undefined 427 }); 428 */ 429 430 /**HTML overlay, attached to canvas. 431 * @class 432 * @extends Class 433 */ 434 435 var HtmlOverlay = Class.extend( 436 /** @lends HtmlOverlay.prototype */ 437 { 438 _defaultCss: { 439 position: "absolute", 440 // display: "none", 441 left: 0, 442 top: 0, 443 display: "none", 444 padding: 10, 445 border: "1px solid white", 446 zIndex: 1000, 447 color: "black", 448 // backgroundColor: "transparent", 449 backgroundColor: "#f1f3f2", 450 // filter: "alpha(opacity=20)", 451 // opacity: 0.2, 452 borderRadius: "5px", 453 "-moz-border-radius": "5px", 454 "-webkit-border-radius": "5px", 455 boxShadow: "3px 3px 10px #bfbfbf", 456 "-moz-box-shadow": "3px 3px 10px #bfbfbf", 457 "-webkit-box-shadow": "3px 3px 10px #bfbfbf" 458 }, 459 init: function(opts){ 460 // Define and override default properties 461 ArcadeJS.guerrillaDerive(this, { 462 game: undefined, // (mandatory) parent canvas element 463 pos: {x: 0, y: 0}, // Centered on screen 464 // title: null, // Title 465 html: undefined, // (mandatory) Box content 466 onClick: null, // Called when box is clicked or touched 467 onClose: null, // Called after box was closed 468 closeOnClick: false, 469 blockClicks: true, 470 showSpeed: "normal", 471 hideSpeed: "normal", 472 timeout: 0 // [ms], 0: never 473 }, opts); 474 this.css = $.extend({}, this._defaultCss, opts.css); 475 var self = this; 476 this.canvas = this.game.canvas; 477 this.down = false; 478 this.inside = false; 479 this.touchDownId = null; 480 481 this.$div = $("<div class='arcadePopup'>" + this.html + "</div>") 482 .hide(0) 483 .css(this.css) 484 .appendTo("body") 485 .click(function(e){ 486 if(e.target.nodeName == "A"){ return; } // Allow <a> tags to work 487 self._clicked(e); 488 }).bind("mousedown", function(e){ 489 if(e.target.nodeName == "A"){ return; } // Allow <a> tags to work 490 self.inside = self.down = true; 491 // 'eat' this event, so it isn't dispatched to the parent canvas 492 return !self.blockClicks; 493 }).bind("mouseup", function(e){ 494 self.down = false; 495 }).bind("touchstart", function(e){ 496 self.game.debug("HtmlOverlay got " + e.type + ", node:" + e.target.nodeName); 497 if(e.target.nodeName == "A"){ return; } // Allow <a> tags to work 498 // if(e.target.nodeName == "CANVAS"){ return; } // Allow <a> tags to work 499 if(self.touchDownId !== null && e.originalEvent.changedTouches.length > 1){ return; } // Already have a touch 500 e.originalEvent.preventDefault(); 501 self.touchDownId = e.originalEvent.changedTouches[0].identifier; 502 self.game.debug(" nChanged:" + e.originalEvent.changedTouches.length + ", id=" + self.touchDownId); 503 // $(this).css("backgroundColor", "red"); 504 self.down = true; 505 self._clicked(e); 506 // }).bind("touchenter", function(e){ 507 // self.inside = false; 508 // }).bind("touchleave", function(e){ 509 // self.inside = false; 510 }).bind("touchend touchcancel", function(e){ 511 self.game.debug("HtmlOverlay got " + e.type + ", node:" + e.target.nodeName); 512 self.game.debug(" id=" + self.touchDownId + ", t=" + _getTouchWithId(e.originalEvent.changedTouches, self.touchDownId)); 513 e.originalEvent.preventDefault(); 514 if(!_getTouchWithId(e.originalEvent.changedTouches, self.touchDownId)){ 515 return; // touch event was for another target 516 } 517 self.touchDownId = null; 518 // $(this).css("backgroundColor", "white"); 519 self.inside = self.down = false; 520 }); 521 $(window).resize(function(e){ 522 self._resized(e); 523 }); 524 // $("button", this.$div).click(function(e){ 525 // alert(e); 526 // }); 527 this._resized(null); 528 this.$div.show("normal"); 529 }, 530 /**Hide and remove this box (triggers onClose callback).*/ 531 close: function() { 532 var self = this; 533 this.$div.hide(this.hideSpeed, function(){ 534 self.$div.remove(); 535 if(self.onClose){ 536 self.onClose(); 537 } 538 }); 539 }, 540 /**Return true if button is down (but mouse key / finger is also still down). */ 541 isDown: function() { 542 return this.down === true; 543 }, 544 /**Called when element was clicked or touched (triggers onClick callback).*/ 545 _clicked: function(e){ 546 var oe = e.originalEvent, 547 res = true; 548 if(this.onClick){ 549 res = this.onClick(e); 550 } 551 if(this.closeOnClick){ 552 this.close(); 553 } 554 return res; 555 }, 556 /**Called when element was clicked or touched. */ 557 _closed: function(e){ 558 var oe = e.originalEvent; 559 if(this.closeOnClick){ 560 this.close(); 561 } 562 }, 563 /**Adjust element relative to canvas center / borders.*/ 564 _resized: function(e){ 565 var x, y, 566 $c = $(this.canvas), 567 cx = $c.offset().left, 568 cy = $c.offset().top; 569 570 if(this.pos.x === 0){ 571 x = cx + 0.5 * ($c.width() - this.$div.outerWidth()); 572 }else if(this.pos.x < 0){ 573 x = cx + ($c.width() - this.$div.outerWidth() + this.pos.x + 1); 574 }else{ 575 x = cx + this.pos.x - 1; 576 } 577 if(this.pos.y === 0){ 578 y = cy + 0.5 * ($c.height() - this.$div.outerHeight()); 579 }else if(this.pos.y < 0){ 580 // alert("cy:" + cy + ", ch:" + $c.height() + ", dh:" + this.$div.outerHeight() + ", py:" + this.pos.y); 581 y = cy + ($c.height() - this.$div.outerHeight() + this.pos.y + 1); 582 }else{ 583 y = cy + this.pos.y - 1; 584 } 585 this.$div.css({left: x, top: y}); 586 }, 587 // --- end of class 588 __lastentry: undefined 589 }); 590