├── .gitignore ├── .jshintrc ├── README.md ├── dist └── gamecontroller.min.js ├── gulpfile.js ├── license ├── package.json ├── src └── gamecontroller.js └── tests └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | "camelcase": true, 5 | "eqeqeq": true, 6 | "indent": 2, 7 | "latedef": true, 8 | "maxlen": 80, 9 | "newcap": true, 10 | "quotmark": "single", 11 | "strict": true, 12 | "undef": true, 13 | "unused": true 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HTML5 Virtual Game Controller 2 | ============================= 3 | 4 | About 5 | ----- 6 | **Author: [Clay.io](http://clay.io/development-tools) - Tools for HTML5 game developers** 7 | 8 | This library is for easy integration of a virtual game controller overlay for HTML5 games. With HTML5, it's easy to 9 | get your game to run on touch-screen devices like phones and tablets, but user-input is a whole different story. With 10 | just the accelerometer and touch to work with, it makes it hard to have a game's input pair well with the desktop version. 11 | 12 | The HTML5 Virtual Game Controller aims to alleviate the problem with a super-simple, yet customizable option for adding a 13 | touch-based gamepad to your game when touch is enabled. The controller will *only* be shown if touch is available on the device. 14 | 15 | **Watch a demo video [here](http://www.youtube.com/watch?v=XQKRYMjrp2Q), or [try the game](http://clay.io/plugins/controller/index.html) out** (if you have a touch-capable device). 16 | In Chrome, you can enable fake touch events with: ctrl+shift+i, then click the settings icon on the bottom right. 17 | Select the "Overrides" tab, and check "Emulate touch events" at the bottom). The demo game isn't the most efficient on 18 | mobile devices in it's current state, but iOS Safari should handle it. The game mentions to press the space key, the "B" button 19 | has been mapped to that functionality. This was a game that *completely* didn't work with touch prior to this library. 20 | 21 | As of January 20th 2013, tested in Chrome, Firefox, IE10, and Mobile Safari. 22 | 23 | Easy Setup 24 | ---------- 25 | ``` 26 | 27 | 32 | ``` 33 | 34 | If you are using node.js and something like [browserify](http://browserify.org/), you can install with `npm install game-controller` 35 | 36 | If you are still in the process of choosing an HTML5 Game Engine, see [this list - complete with reviews and details of popular HTML5 Game Engines](http://html5gameengine.com). 37 | 38 | Advanced Options 39 | ---------------- 40 | The entire customization for this library is done through the options object that is passed as a parameter to the `init` method. 41 | This can be as simple as passing nothing, are as advanced as passing dozens of options. 42 | 43 | ``` 44 | var options = {}; 45 | GameController.init( options ); 46 | ``` 47 | 48 | Below is a list of the possible options, and what each does. 49 | 50 | * **touchRadius** {int} - a faint glow for feedback will show when the screen is touched. Set this as the length of the radius of that glow circle in pixels. Set to false if you don't want this help to show 51 | * **forcePerformanceFriendly** {boolean} - the library auto-detects slower devices (phones) and changes a few things to make it not use as much CPU. You can force it to always use the performance-friendly mode by setting this to `true` 52 | * **left** {object} - options for the element you want on the left side of the game 53 | * **type** {string} - 'dpad', 'buttons', or 'joystick' depending on the mode you want. *Default: 'dpad'* 54 | * **position** {object} - positioning for this part of the controller 55 | * **left** {int/string} - the distance from the center point of this part to the left edge of the game's canvas. Can specify integer for number of pixels, and string: 'x%' for positioning relative to the canvas' size. *Default: 13%* 56 | * **right** {int/string} - same as *left*, just from the right side of the canvas. Only specify one of these two 57 | * **top** {int/string} - similar to *left* and *right*, just from the top side of the canvas 58 | * **bottom** {int/string} - same as *top*, just from the bottom side of the canvas. Only specify one of top or bottom. *Default: 22%* 59 | * **dpad** {object} - options pertaining to the dpad for this section (only has effect if *type* is set to 'dpad' 60 | * **up** {object} - options pertaining to the up direction of the dpad 61 | * **width** {int/string} - pixels (int) or percent ('x%') wide 62 | * **height** {int/string} - pixels (int) or percent ('x%') high 63 | * **stroke** {int} - thickness of stroke (in pixels) 64 | * **opacity** {float} - value from 0-1 for how opaque this should be 65 | * **touchStart** {function} - called when this direction is touched 66 | * **touchEnd** {function} - called when this direction is no longer touched 67 | * **touchMove** {function} - called on any movement while player is touching object 68 | * **right** {object} - the same object properties from *up* are available for *right* 69 | * **down** {object} - the same object properties from *up* are available for *down* 70 | * **left** {object} - the same object properties from *up* are available for *left* 71 | * **buttons** [] - array of button objects for this section (only has effect if type is set to 'buttons' 72 | * **button object** 73 | * **offset** {object} - offset for each button from the center position 74 | * **x** {int/string} - pixels (int) or percent ('x%') from center on x-axis 75 | * **y** {int/string} - pixels (int) or percent ('x%') from center on x-axis 76 | * **label** {string} - short label for this button 77 | * **radius** {int} - button radius in pixels 78 | * **stroke** {int} - stroke thickness in pixels 79 | * **backgroundColor** {string} - currently you have 5 options for this since gradients are used: 'blue', 'green', 'yellow', 'red', 'white' 80 | * **fontColor** {string} - hex code 81 | * **fontSize** {int} - size of the label font in pixels 82 | * **touchStart** {function} - called when this direction is touched 83 | * **touchEnd** {function} - called when this direction is no longer touched 84 | * **touchMove** {function} - called on any movement while player is touching object 85 | * **joystick** {object} - options pertaining to the dpad for this section (only has effect if *type* is set to 'dpad' 86 | * **radius** {int} joystick button radius in pixels 87 | * **touchStart** {function} - called when this direction is touched 88 | * **touchEnd** {function} - called when this direction is no longer touched 89 | * **touchMove** {function} - called on any movement while player is touching object. An object with the following properties is passed as the only parameter: 90 | * **dx** {float} - the distance on x axis the joystick is from the center 91 | * **dy** {float} - the distance on y axis the joystick is from the center 92 | * **max** {int} - the max distance the joystick can get from the center 93 | * **normalizedX** {float} - ranges from -1 to 1 where -1 is as far left as possible, and 1 is as far right as possible. 0 is center 94 | * **normalizedY** - ranges from -1 to 1 where -1 is as far up as possible, and 1 is as far down as possible. 0 is center 95 | 96 | Examples 97 | -------- 98 | **DPad on left, 2 buttons on right** 99 | ``` 100 | GameController.init(); 101 | ``` 102 | ![GamePad 1](http://clay.io/images/controller/1.png) 103 | 104 | **Joystick on left, 1 button on right** 105 | ``` 106 | GameController.init( { 107 | left: { 108 | type: 'joystick' 109 | }, 110 | right: { 111 | position: { 112 | right: '5%' 113 | }, 114 | type: 'buttons', 115 | buttons: [ 116 | { 117 | label: 'jump', fontSize: 13, touchStart: function() { 118 | // do something 119 | } 120 | }, 121 | false, false, false 122 | ] 123 | } 124 | } ); 125 | ``` 126 | ![GamePad 2](http://clay.io/images/controller/2.png) 127 | 128 | **Joysticks on both sides** 129 | ``` 130 | GameController.init( { 131 | left: { 132 | type: 'joystick', 133 | position: { left: '15%', bottom: '15%' }, 134 | joystick: { 135 | touchStart: function() { 136 | console.log('touch starts'); 137 | }, 138 | touchEnd: function() { 139 | console.log('touch ends'); 140 | }, 141 | touchMove: function( details ) { 142 | console.log( details.dx ); 143 | console.log( details.dy ); 144 | console.log( details.max ); 145 | console.log( details.normalizedX ); 146 | console.log( details.normalizedY ); 147 | } 148 | } 149 | }, 150 | right: { 151 | type: 'joystick', 152 | position: { right: '15%', bottom: '15%' } , 153 | joystick: { 154 | touchMove: function( details ) { 155 | // Do something... 156 | } 157 | } 158 | } 159 | }); 160 | ``` 161 | ![GamePad 3](http://clay.io/images/controller/3.png) 162 | 163 | **Two large buttons at bottom** 164 | ``` 165 | GameController.init( { 166 | left: { 167 | position: { left: '50%', bottom: '5%' }, 168 | dpad: { 169 | up: false, 170 | down: false, 171 | left: { width: '50%', height: '10%' }, 172 | right: { width: '50%', height: '10%' } 173 | } 174 | }, 175 | right: false 176 | } ); 177 | ``` 178 | ![GamePad 4](http://clay.io/images/controller/4.png) 179 | 180 | These examples are just the start - the customization allows for quite a bit to be done, 181 | and of course, the code can always be edited as well. 182 | -------------------------------------------------------------------------------- /dist/gamecontroller.min.js: -------------------------------------------------------------------------------- 1 | !function(t){"use strict";function e(t,i){var o,s,n,a,r=i,h=1,c=!0;if("boolean"==typeof t&&(c=t,h=2),"object"!=typeof t&&"function"!=typeof t&&(t={}),!r)return t;for(o in r)i=t[o],s=r[o],t!==s&&(n=Array.isArray(s),c&&("object"==typeof s||n)?(a=n?i&&Array.isArray(i)?i:[]:i&&"object"==typeof i?i:{},t[o]=e(a,s)):"undefined"!=typeof s&&(t[o]=s));return t}var i,o,s=2*Math.PI,n={}.hasOwnProperty,a=function(t,e){function i(){this.constructor=t}for(var o in e)n.call(e,o)&&(t[o]=e[o]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},r=t.GameController={options:{left:{type:"dpad",position:{left:"13%",bottom:"22%"},dpad:{up:{width:"7%",height:"15%",stroke:2,touchStart:function(){r.simulateKeyEvent("press",38),r.simulateKeyEvent("down",38)},touchEnd:function(){r.simulateKeyEvent("up",38)}},left:{width:"15%",height:"7%",stroke:2,touchStart:function(){r.simulateKeyEvent("press",37),r.simulateKeyEvent("down",37)},touchEnd:function(){r.simulateKeyEvent("up",37)}},down:{width:"7%",height:"15%",stroke:2,touchStart:function(){r.simulateKeyEvent("press",40),r.simulateKeyEvent("down",40)},touchEnd:function(){r.simulateKeyEvent("up",40)}},right:{width:"15%",height:"7%",stroke:2,touchStart:function(){r.simulateKeyEvent("press",39),r.simulateKeyEvent("down",39)},touchEnd:function(){r.simulateKeyEvent("up",39)}}},joystick:{radius:60,touchMove:function(t){console.log(t)}}},right:{type:"buttons",position:{right:"17%",bottom:"28%"},buttons:[{offset:{x:"-13%",y:0},label:"X",radius:"7%",stroke:2,backgroundColor:"blue",fontColor:"#fff",touchStart:function(){r.simulateKeyEvent("press",88),r.simulateKeyEvent("down",88)},touchEnd:function(){r.simulateKeyEvent("up",88)}},{offset:{x:0,y:"-11%"},label:"Y",radius:"7%",stroke:2,backgroundColor:"yellow",fontColor:"#fff",touchStart:function(){r.simulateKeyEvent("press",70),r.simulateKeyEvent("down",70)},touchEnd:function(){r.simulateKeyEvent("up",70)}},{offset:{x:"13%",y:0},label:"B",radius:"7%",stroke:2,backgroundColor:"red",fontColor:"#fff",touchStart:function(){r.simulateKeyEvent("press",90),r.simulateKeyEvent("down",90)},touchEnd:function(){r.simulateKeyEvent("up",90)}},{offset:{x:0,y:"11%"},label:"A",radius:"7%",stroke:2,backgroundColor:"green",fontColor:"#fff",touchStart:function(){r.simulateKeyEvent("press",67),r.simulateKeyEvent("down",67)},touchEnd:function(){r.simulateKeyEvent("up",67)}}],dpad:{up:{width:"7%",height:"15%",stroke:2},left:{width:"15%",height:"7%",stroke:2},down:{width:"7%",height:"15%",stroke:2},right:{width:"15%",height:"7%",stroke:2}},joystick:{radius:60,touchMove:function(t){console.log(t)}}},touchRadius:45},touchableAreas:[],touchableAreasCount:0,touches:[],offsetX:0,offsetY:0,bound:{left:!1,right:!1,top:!1,bottom:!1},cachedSprites:{},paused:!1,init:function(t){t=t||{},e(this.options,t);var i=navigator.userAgent.toLowerCase(),o=function(t){return-1!==i.indexOf(t)};this.performanceFriendly=["iphone","android"].filter(o)[0]||this.options.forcePerformanceFriendly;var s=document.getElementById(this.options.canvas);this.options.canvas&&s?s&&(this.options.canvas=s):this.options.canvas=document.getElementsByTagName("canvas")[0],this.options.ctx=this.options.canvas.getContext("2d"),this.createOverlayCanvas()},boundingSet:function(t){var e,i,o,s;if(t.width)e=this.getPixels(t.width),i=this.getPixels(t.height),o=this.getPixels(t.x),s=this.getPixels(t.y);else{var n=this.options.touchRadius?2*this.getPixels(t.radius)+this.getPixels(this.options.touchRadius)/2:t.radius;e=i=2*(n+this.getPixels(t.stroke)),o=this.getPixels(t.x)-e/2,s=this.getPixels(t.y)-i/2}var a=o+e,r=s+i;(!this.bound.left||othis.bound.right)&&(this.bound.right=a),(!this.bound.top||sthis.bound.bottom)&&(this.bound.bottom=r)},createOverlayCanvas:function(){var t=this;this.canvas=document.createElement("canvas"),this.resize(!0),document.body.appendChild(this.canvas),this.ctx=this.canvas.getContext("2d"),window.addEventListener("resize",function(){setTimeout(function(){r.resize.call(t)},10)}),this.setTouchEvents(),this.loadSide("left"),this.loadSide("right"),this.render(),this.touches&&this.touches.length||(this.paused=!0)},pixelRatio:1,resize:function(t){var e=r.options.canvas,i=this.options.canvas;this.canvas.width=i.width,this.canvas.height=i.height,this.offsetX=e.offsetLeft+document.body.scrollLeft,this.offsetY=e.offsetTop+document.body.scrollTop,i.style.width&&i.style.height&&-1!==i.style.height.indexOf("px")&&(this.canvas.style.width=i.style.width,this.canvas.style.height=i.style.height,this.pixelRatio=this.canvas.width/parseInt(this.canvas.style.width,10)),this.canvas.style.position="absolute",this.canvas.style.zIndex="5",this.canvas.style.left=i.offsetLeft+"px",this.canvas.style.top=i.offsetTop+"px";var o=this.canvas.getAttribute("style")+" -ms-touch-action: none;";this.canvas.setAttribute("style",o),t||(this.touchableAreas=[],this.cachedSprites=[],this.reloadSide("left"),this.reloadSide("right"))},getPixels:function(t,e){return t?"number"==typeof t?t:parseInt(t,10)/100*("x"===e?this.canvas.width:this.canvas.height):0},simulateKeyEvent:function(t,e){if("undefined"==typeof window.onkeydown)return!1;var i=document.createEvent("KeyboardEvent");-1!==navigator.userAgent.toLowerCase().indexOf("chrome")&&(Object.defineProperty(i,"keyCode",{get:function(){return this.keyCodeVal}}),Object.defineProperty(i,"which",{get:function(){return this.keyCodeVal}}));var o=i.initKeyboardEvent||i.initKeyEvent;o.call(i,"key"+t,!0,!0,document.defaultView,!1,!1,!1,!1,e,e),i.keyCodeVal=e},setTouchEvents:function(){var t=this,e=function(e){window.navigator.msPointerEnabled&&e.clientX&&e.pointerType===e.MSPOINTER_TYPE_TOUCH?t.touches[e.pointerId]={clientX:e.clientX,clientY:e.clientY}:t.touches=e.touches||[]},i=function(i){t.paused&&(t.paused=!1),i.preventDefault(),e(i)},o=function(e){e.preventDefault(),window.navigator.msPointerEnabled&&e.pointerType===e.MSPOINTER_TYPE_TOUCH?delete t.touches[e.pointerId]:t.touches=e.touches||[],e.touches&&e.touches.length||(t.render(),t.paused=!0)},s=function(t){t.preventDefault(),e(t)};this.canvas.addEventListener("touchstart",i,!1),this.canvas.addEventListener("touchend",o),this.canvas.addEventListener("touchmove",s),window.navigator.msPointerEnabled&&(this.canvas.addEventListener("MSPointerDown",i),this.canvas.addEventListener("MSPointerUp",o),this.canvas.addEventListener("MSPointerMove",s))},addTouchableDirection:function(t){var e=new c(t);e.id=this.touchableAreas.push(e),this.touchableAreasCount++,this.boundingSet(t)},addJoystick:function(t){var e=new u(t);e.id=this.touchableAreas.push(e),this.touchableAreasCount++,this.boundingSet(t)},addButton:function(t){var e=new d(t);e.id=this.touchableAreas.push(e),this.touchableAreasCount++,this.boundingSet(t)},addTouchableArea:function(){},loadButtons:function(t){for(var e=this.options[t].buttons,i=0,o=e.length;o>i;i++)if(e[i]&&"undefined"!=typeof e[i].offset){var s=this.getPositionX(t),n=this.getPositionY(t);e[i].x=s+this.getPixels(e[i].offset.x,"y"),e[i].y=n+this.getPixels(e[i].offset.y,"y"),this.addButton(e[i])}},loadDPad:function(t){var e=this.options[t].dpad||{},i=this.getPositionX(t),o=this.getPositionY(t);if(e.up&&e.left&&e.down&&e.right){var s={x:i,y:o,radius:e.right.height},n=new l(s);this.touchableAreas.push(n),this.touchableAreasCount++}var a=this.getPixels(e.left.height,"y")/2,r=this.getPixels(e.up.width,"y")/2;e.up||(e.up.x=i-r,e.up.y=o-this.getPixels(e.up.height,"y")-a,e.up.direction="up",this.addTouchableDirection(e.up)),e.left||(e.left.x=i-this.getPixels(e.left.width,"y")-r,e.left.y=o-a,e.left.direction="left",this.addTouchableDirection(e.left)),e.down||(e.down.x=i-this.getPixels(e.down.width,"y")/2,e.down.y=o+a,e.down.direction="down",this.addTouchableDirection(e.down)),e.right||(e.right.x=i+r,e.right.y=o-this.getPixels(e.right.height,"y")/2,e.right.direction="right",this.addTouchableDirection(e.right))},loadJoystick:function(t){var e=this.options[t].joystick;e.x=this.getPositionX(t),e.y=this.getPositionY(t),this.addJoystick(e)},reloadSide:function(t){this.loadSide(t)},loadSide:function(t){var e=this.options[t];"dpad"===e.type?this.loadDPad(t):"joystick"===e.type?this.loadJoystick(t):"buttons"===e.type&&this.loadButtons(t)},normalizeTouchPositionX:function(t){return(t-this.offsetX)*this.pixelRatio},normalizeTouchPositionY:function(t){return(t-this.offsetY)*this.pixelRatio},getXFromRight:function(t){return this.canvas.width-t},getYFromBottom:function(t){return this.canvas.height-t},getPositionX:function(t){var e=this.options[t].position;return"undefined"!=typeof e.left?this.getPixels(e.left,"x"):this.getXFromRight(this.getPixels(e.right,"x"))},getPositionY:function(t){var e=this.options[t].position;return"undefined"!=typeof e.top?this.getPixels(e.top,"y"):this.getYFromBottom(this.getPixels(e.bottom,"y"))},renderAreas:function(){for(var t=0,e=this.touchableAreasCount;e>t;t++){var i=this.touchableAreas[t];if("undefined"!=typeof i){i.draw();for(var o=!1,s=0,n=this.touches.length;n>s;s++){var a=this.touches[s];if("undefined"!=typeof a){var r=this.normalizeTouchPositionX(a.clientX),h=this.normalizeTouchPositionY(a.clientY);i.check(r,h)&&!o&&(o=this.touches[s])}}o?(i.active||i.touchStartWrapper(o),i.touchMoveWrapper(o)):i.active&&i.touchEndWrapper(o)}}},render:function(){var t=this.bound;if(this.paused&&this.performanceFriendly||this.ctx.clearRect(t.left,t.top,t.right-t.left,t.bottom-t.top),!this.paused&&!this.performanceFriendly){var e="touch-circle",o=this.cachedSprites[e],n=this.options.touchRadius;if(!o&&n){var a=document.createElement("canvas"),h=a.getContext("2d");a.width=2*n,a.height=2*n;var c=n,d=h.createRadialGradient(c,c,1,c,c,c);d.addColorStop(0,"rgba( 200, 200, 200, 1 )"),d.addColorStop(1,"rgba( 200, 200, 200, 0 )"),h.beginPath(),h.fillStyle=d,h.arc(c,c,c,0,s,!1),h.fill(),o=r.cachedSprites[e]=a}for(var u=0,l=this.touches.length;l>u;u++){var p=this.touches[u];if("undefined"!=typeof p){var f=this.normalizeTouchPositionX(p.clientX),y=this.normalizeTouchPositionY(p.clientY);f-n>this.bound.left&&f+nthis.bound.top&&y+nthis.x)&&(Math.abs(t-this.x-this.width)this.y)&&(Math.abs(e-this.y-this.height) this.bound.right ){ 287 | this.bound.right = right; 288 | } 289 | if( !this.bound.top || top < this.bound.top ) this.bound.top = top; 290 | if( !this.bound.bottom || bottom > this.bound.bottom ){ 291 | this.bound.bottom = bottom; 292 | } 293 | }, 294 | 295 | /** 296 | * Creates the canvas that sits on top of the game's canvas and 297 | * holds game controls 298 | */ 299 | createOverlayCanvas: function() { 300 | var _this = this; 301 | this.canvas = document.createElement('canvas'); 302 | // Scale to same size as original canvas 303 | this.resize(true); 304 | document.body.appendChild(this.canvas); 305 | this.ctx = this.canvas.getContext( '2d'); 306 | window.addEventListener( 'resize', function() { 307 | setTimeout(function(){ GameController.resize.call(_this); }, 10); 308 | }); 309 | 310 | // Set the touch events for this new canvas 311 | this.setTouchEvents(); 312 | 313 | // Load in the initial UI elements 314 | this.loadSide('left'); 315 | this.loadSide('right'); 316 | 317 | // Starts up the rendering / drawing 318 | this.render(); 319 | 320 | // pause until a touch event 321 | if( !this.touches || !this.touches.length ) this.paused = true; 322 | }, 323 | 324 | pixelRatio: 1, 325 | resize: function( firstTime ) { 326 | // Scale to same size as original canvas 327 | var gameCanvas = GameController.options.canvas; 328 | var canvas = this.options.canvas; 329 | this.canvas.width = canvas.width; 330 | this.canvas.height = canvas.height; 331 | this.offsetX = gameCanvas.offsetLeft + document.body.scrollLeft; 332 | this.offsetY = gameCanvas.offsetTop + document.body.scrollTop; 333 | 334 | // Get in on this retina action 335 | if( canvas.style.width && 336 | canvas.style.height && 337 | canvas.style.height.indexOf('px') !== -1) { 338 | this.canvas.style.width = canvas.style.width; 339 | this.canvas.style.height = canvas.style.height; 340 | this.pixelRatio = 341 | this.canvas.width / parseInt(this.canvas.style.width, 10); 342 | } 343 | 344 | this.canvas.style.position = 'absolute'; 345 | this.canvas.style.zIndex = '5'; 346 | this.canvas.style.left = canvas.offsetLeft + 'px'; 347 | this.canvas.style.top = canvas.offsetTop + 'px'; 348 | var style = this.canvas.getAttribute('style') +' -ms-touch-action: none;'; 349 | this.canvas.setAttribute('style', style); 350 | 351 | if( !firstTime ) { 352 | // Remove all current buttons 353 | this.touchableAreas = []; 354 | // Clear out the cached sprites 355 | this.cachedSprites = []; 356 | // Reload in the initial UI elements 357 | this.reloadSide( 'left'); 358 | this.reloadSide( 'right'); 359 | } 360 | }, 361 | 362 | /** 363 | * Returns the scaled pixels. Given the value passed 364 | * @param {int/string} value - either an integer for # of pixels, 365 | * or 'x%' for relative 366 | * @param {char} axis - x, y 367 | */ 368 | getPixels: function( value, axis ){ 369 | if( !value ) return 0; 370 | if( typeof value === 'number' ) return value; 371 | // a percentage 372 | return parseInt(value, 10) / 100 * 373 | (axis === 'x' ? this.canvas.width : this.canvas.height); 374 | }, 375 | 376 | /** 377 | * Simulates a key press 378 | * @param {string} eventName - 'down', 'up' 379 | * @param {char} character 380 | */ 381 | simulateKeyEvent: function( eventName, keyCode ) { 382 | // No keyboard, can't simulate... 383 | if( typeof window.onkeydown === 'undefined' ) return false; 384 | var oEvent = document.createEvent('KeyboardEvent'); 385 | 386 | // Chromium Hack 387 | if( navigator.userAgent.toLowerCase().indexOf( 'chrome' ) !== -1 ) { 388 | Object.defineProperty( oEvent, 'keyCode', { 389 | get: function() { return this.keyCodeVal; } 390 | }); 391 | Object.defineProperty( oEvent, 'which', { 392 | get: function() { return this.keyCodeVal; } 393 | }); 394 | } 395 | 396 | var initKeyEvent = oEvent.initKeyboardEvent || oEvent.initKeyEvent; 397 | initKeyEvent.call(oEvent, 398 | 'key' + eventName, 399 | true, 400 | true, 401 | document.defaultView, 402 | false, 403 | false, 404 | false, 405 | false, 406 | keyCode, 407 | keyCode 408 | ); 409 | 410 | oEvent.keyCodeVal = keyCode; 411 | }, 412 | 413 | setTouchEvents: function() { 414 | var _this = this; 415 | 416 | var setTouches = function(e){ 417 | // Microsoft always has to have their own stuff... 418 | if( window.navigator.msPointerEnabled && 419 | !! e.clientX && 420 | e.pointerType === e.MSPOINTER_TYPE_TOUCH 421 | ){ 422 | _this.touches[ e.pointerId ] = { 423 | clientX: e.clientX, 424 | clientY: e.clientY 425 | }; 426 | } else _this.touches = e.touches || []; 427 | }; 428 | 429 | var touchStart = function( e ) { 430 | if( _this.paused ) _this.paused = false; 431 | e.preventDefault(); 432 | setTouches(e); 433 | }; 434 | 435 | var touchEnd = function( e ) { 436 | e.preventDefault(); 437 | 438 | if( window.navigator.msPointerEnabled && 439 | e.pointerType === e.MSPOINTER_TYPE_TOUCH ) { 440 | delete _this.touches[ e.pointerId ]; 441 | } else _this.touches = e.touches || []; 442 | 443 | if( !e.touches || ! e.touches.length ) { 444 | // Draw once more to remove the touch area 445 | _this.render(); 446 | _this.paused = true; 447 | } 448 | }; 449 | 450 | var touchMove = function( e ) { 451 | e.preventDefault(); 452 | setTouches(e); 453 | }; 454 | 455 | this.canvas.addEventListener('touchstart', touchStart, false); 456 | this.canvas.addEventListener('touchend', touchEnd); 457 | this.canvas.addEventListener('touchmove', touchMove); 458 | 459 | if( window.navigator.msPointerEnabled ) { 460 | this.canvas.addEventListener('MSPointerDown', touchStart); 461 | this.canvas.addEventListener('MSPointerUp', touchEnd); 462 | this.canvas.addEventListener('MSPointerMove', touchMove); 463 | } 464 | }, 465 | 466 | /** 467 | * Adds the area to a list of touchable areas, draws 468 | * @param {object} options with properties: 469 | * x, y, width, height, touchStart, touchEnd, touchMove 470 | */ 471 | addTouchableDirection: function( options ) { 472 | var direction = new TouchableDirection( options); 473 | direction.id = this.touchableAreas.push( direction); 474 | this.touchableAreasCount++; 475 | this.boundingSet(options); 476 | }, 477 | 478 | /** 479 | * Adds the circular area to a list of touchable areas, draws 480 | * @param {object} options with properties: 481 | * x, y, width, height, touchStart, touchEnd, touchMove 482 | */ 483 | addJoystick: function( options ) { 484 | var joystick = new TouchableJoystick(options); 485 | joystick.id = this.touchableAreas.push( joystick); 486 | this.touchableAreasCount++; 487 | this.boundingSet(options); 488 | }, 489 | 490 | /** 491 | * Adds the circular area to a list of touchable areas, draws 492 | * @param {object} options with properties: 493 | * x, y, width, height, touchStart, touchEnd, touchMove 494 | */ 495 | addButton: function( options ) { 496 | var button = new TouchableButton(options); 497 | button.id = this.touchableAreas.push( button); 498 | this.touchableAreasCount++; 499 | this.boundingSet( options); 500 | }, 501 | 502 | addTouchableArea: function() {}, 503 | 504 | loadButtons: function( side ) { 505 | var buttons = this.options[ side ].buttons; 506 | for( var i = 0, j = buttons.length; i < j; i++ ) { 507 | if( !buttons[i] || typeof buttons[i].offset === 'undefined' ) continue; 508 | var posX = this.getPositionX( side); 509 | var posY = this.getPositionY( side); 510 | buttons[i].x = posX + this.getPixels( buttons[i].offset.x, 'y'); 511 | buttons[i].y = posY + this.getPixels( buttons[i].offset.y, 'y'); 512 | this.addButton( buttons[i]); 513 | } 514 | }, 515 | 516 | loadDPad: function( side ) { 517 | var dpad = this.options[ side ].dpad || {}; 518 | // Centered value is at this.options[ side ].position 519 | var posX = this.getPositionX( side); 520 | var posY = this.getPositionY( side); 521 | // If they have all 4 directions, add a circle to the center for looks 522 | if( dpad.up && dpad.left && dpad.down && dpad.right ) { 523 | var options = { 524 | x: posX, 525 | y: posY, 526 | radius: dpad.right.height 527 | }; 528 | var center = new TouchableCircle( options); 529 | this.touchableAreas.push( center); 530 | this.touchableAreasCount++; 531 | } 532 | 533 | var halfLeftHeight = this.getPixels(dpad.left.height, 'y') / 2; 534 | var halfUpWidth = this.getPixels(dpad.up.width, 'y') / 2; 535 | // Up arrow 536 | if( !dpad.up ) { 537 | dpad.up.x = posX - halfUpWidth; 538 | dpad.up.y = posY - this.getPixels(dpad.up.height, 'y') - halfLeftHeight; 539 | dpad.up.direction = 'up'; 540 | this.addTouchableDirection( dpad.up); 541 | } 542 | 543 | // Left arrow 544 | if( !dpad.left ) { 545 | dpad.left.x = posX - this.getPixels(dpad.left.width, 'y') - halfUpWidth; 546 | dpad.left.y = posY - halfLeftHeight; 547 | dpad.left.direction = 'left'; 548 | this.addTouchableDirection( dpad.left); 549 | } 550 | 551 | // Down arrow 552 | if( !dpad.down ) { 553 | dpad.down.x = posX - this.getPixels(dpad.down.width, 'y') / 2; 554 | dpad.down.y = posY + halfLeftHeight; 555 | dpad.down.direction = 'down'; 556 | this.addTouchableDirection( dpad.down); 557 | } 558 | 559 | // Right arrow 560 | if( !dpad.right ) { 561 | dpad.right.x = posX + halfUpWidth; 562 | dpad.right.y = posY - this.getPixels(dpad.right.height, 'y') / 2; 563 | dpad.right.direction = 'right'; 564 | this.addTouchableDirection( dpad.right); 565 | } 566 | }, 567 | 568 | loadJoystick: function( side ) { 569 | var joystick = this.options[ side ].joystick; 570 | joystick.x = this.getPositionX( side); 571 | joystick.y = this.getPositionY( side); 572 | this.addJoystick( joystick); 573 | }, 574 | 575 | /** 576 | * Used for resizing. Currently is just an alias for loadSide 577 | */ 578 | reloadSide: function( side ) { this.loadSide( side); }, 579 | 580 | loadSide: function( side ) { 581 | var o = this.options[ side ]; 582 | if( o.type === 'dpad' ) this.loadDPad( side); 583 | else if( o.type === 'joystick' ) this.loadJoystick( side); 584 | else if( o.type === 'buttons' ) this.loadButtons( side); 585 | }, 586 | 587 | /** 588 | * Normalize touch positions by the left and top offsets 589 | * @param {int} x 590 | */ 591 | normalizeTouchPositionX: function( x ) { 592 | return ( x - this.offsetX ) * this.pixelRatio; 593 | }, 594 | 595 | /** 596 | * Normalize touch positions by the left and top offsets 597 | * @param {int} y 598 | */ 599 | normalizeTouchPositionY: function( y ) { 600 | return ( y - this.offsetY ) * this.pixelRatio; 601 | }, 602 | 603 | /** 604 | * Returns the x position when given # of pixels from right 605 | * (based on canvas size) 606 | * @param {int} right 607 | */ 608 | getXFromRight: function( right ) { return this.canvas.width - right; }, 609 | 610 | /** 611 | * Returns the y position when given # of pixels from bottom 612 | * (based on canvas size) 613 | * @param {int} right 614 | */ 615 | getYFromBottom: function( bottom ) { return this.canvas.height - bottom; }, 616 | 617 | /** 618 | * Grabs the x position of either the left or right side/controls 619 | * @param {string} side - 'left', 'right' 620 | */ 621 | getPositionX: function( side ) { 622 | var position = this.options[side].position; 623 | return typeof position.left !== 'undefined' ? 624 | this.getPixels(position.left, 'x') : 625 | this.getXFromRight(this.getPixels(position.right, 'x')); 626 | }, 627 | 628 | /** 629 | * Grabs the y position of either the left or right side/controls 630 | * @param {string} side - 'left', 'right' 631 | */ 632 | getPositionY: function( side ) { 633 | var position = this.options[side].position; 634 | return typeof position.top !== 'undefined' ? 635 | this.getPixels(position.top, 'y') : 636 | this.getYFromBottom(this.getPixels(position.bottom, 'y')); 637 | }, 638 | 639 | /** 640 | * Processes the info for each touchableArea 641 | */ 642 | renderAreas: function() { 643 | for( var i = 0, j = this.touchableAreasCount; i < j; i++ ) { 644 | var area = this.touchableAreas[ i ]; 645 | if( typeof area === 'undefined' ) continue; 646 | area.draw(); 647 | // Go through all touches to see if any hit this area 648 | var touched = false; 649 | for( var k = 0, l = this.touches.length; k < l; k++ ) { 650 | var touch = this.touches[ k ]; 651 | if( typeof touch === 'undefined' ) continue; 652 | var x = this.normalizeTouchPositionX(touch.clientX), 653 | y = this.normalizeTouchPositionY(touch.clientY); 654 | // Check that it's in the bounding box/circle 655 | if( area.check(x, y) && !touched) touched = this.touches[k]; 656 | } 657 | 658 | if( touched ) { 659 | if( !area.active ) area.touchStartWrapper(touched); 660 | area.touchMoveWrapper(touched); 661 | } else if( area.active ) area.touchEndWrapper(touched); 662 | } 663 | }, 664 | 665 | render: function() { 666 | var bound = this.bound; 667 | if( ! this.paused || ! this.performanceFriendly ){ 668 | this.ctx.clearRect( 669 | bound.left, 670 | bound.top, 671 | bound.right - bound.left, 672 | bound.bottom - bound.top 673 | ); 674 | } 675 | 676 | // Draw feedback for when screen is being touched 677 | // When no touch events are happening, 678 | // this enables 'paused' mode, which skips running this. 679 | // This isn't run at all in performanceFriendly mode 680 | if( ! this.paused && ! this.performanceFriendly ) { 681 | var cacheId = 'touch-circle'; 682 | var cached = this.cachedSprites[ cacheId ]; 683 | var radius = this.options.touchRadius; 684 | if( ! cached && radius ) { 685 | var subCanvas = document.createElement('canvas'); 686 | var ctx = subCanvas.getContext('2d'); 687 | subCanvas.width = 2 * radius; 688 | subCanvas.height = 2 * radius; 689 | 690 | var center = radius; 691 | var gradient = ctx.createRadialGradient( 692 | center, 693 | center, 694 | 1, 695 | center, 696 | center, 697 | center 698 | ); 699 | 700 | gradient.addColorStop(0, 'rgba( 200, 200, 200, 1 )'); 701 | gradient.addColorStop(1, 'rgba( 200, 200, 200, 0 )'); 702 | ctx.beginPath(); 703 | ctx.fillStyle = gradient; 704 | ctx.arc(center, center, center, 0, PI2, false); 705 | ctx.fill(); 706 | 707 | cached = GameController.cachedSprites[ cacheId ] = subCanvas; 708 | } 709 | // Draw the current touch positions if any 710 | for( var i = 0, j = this.touches.length; i < j; i++ ) { 711 | var touch = this.touches[ i ]; 712 | if( typeof touch === 'undefined' ) continue; 713 | var x = this.normalizeTouchPositionX(touch.clientX), 714 | y = this.normalizeTouchPositionY(touch.clientY); 715 | if( x - radius > this.bound.left && 716 | x + radius < this.bound.right && 717 | y - radius > this.bound.top && 718 | y + radius < this.bound.bottom 719 | ){ 720 | this.ctx.drawImage(cached, x - radius, y - radius); 721 | } 722 | } 723 | } 724 | 725 | // Render if the game isn't paused, or we're not in performanceFriendly 726 | // mode (running when not paused keeps the semi-transparent gradients 727 | // looking better for some reason) 728 | // Process all the info for each touchable area 729 | if( !this.paused || !this.performanceFriendly ) this.renderAreas(); 730 | requestAnimationFrame(this.renderWrapper); 731 | }, 732 | /** 733 | * So we can keep scope, and don't have to create a new obj every 734 | * requestAnimationFrame (bad for garbage collection) 735 | */ 736 | renderWrapper: function() { GameController.render(); }, 737 | }; 738 | 739 | /** 740 | * Superclass for touchable stuff 741 | */ 742 | var TouchableArea = ( function() { 743 | function TouchableArea(){ } 744 | // Called when this direction is being touched 745 | TouchableArea.prototype.touchStart = null; 746 | // Called when this direction is being moved 747 | TouchableArea.prototype.touchMove = null; 748 | // Called when this direction is no longer being touched 749 | TouchableArea.prototype.touchEnd = null; 750 | TouchableArea.prototype.type = 'area'; 751 | TouchableArea.prototype.id = false; 752 | TouchableArea.prototype.active = false; 753 | 754 | /** 755 | * Sets the user-specified callback for this direction being touched 756 | * @param {function} callback 757 | */ 758 | TouchableArea.prototype.setTouchStart = function( callback ) { 759 | this.touchStart = callback; 760 | }; 761 | 762 | /** 763 | * Called when this direction is no longer touched 764 | */ 765 | TouchableArea.prototype.touchStartWrapper = function() { 766 | // Fire the user specified callback 767 | if( this.touchStart ) this.touchStart(); 768 | // Mark this direction as active 769 | this.active = true; 770 | }; 771 | 772 | /** 773 | * Sets the user-specified callback for this direction 774 | * no longer being touched 775 | * @param {function} callback 776 | */ 777 | TouchableArea.prototype.setTouchMove = function( callback ) { 778 | this.touchMove = callback; 779 | }; 780 | 781 | /** 782 | * Called when this direction is moved. Make sure it's actually changed 783 | * before passing to developer 784 | */ 785 | TouchableArea.prototype.lastPosX = 0; 786 | TouchableArea.prototype.lastPosY = 0; 787 | TouchableArea.prototype.touchMoveWrapper = function( e ) { 788 | // Fire the user specified callback 789 | if( this.touchMove && ( 790 | e.clientX !== TouchableArea.prototype.lastPosX || 791 | e.clientY !== TouchableArea.prototype.lastPosY) 792 | ){ 793 | this.touchMove(); 794 | this.lastPosX = e.clientX; 795 | this.lastPosY = e.clientY; 796 | } 797 | // Mark this direction as active 798 | this.active = true; 799 | }; 800 | 801 | /** 802 | * Sets the user-specified callback for this direction 803 | * no longer being touched 804 | * @param {function} callback 805 | */ 806 | TouchableArea.prototype.setTouchEnd = function( callback ) { 807 | this.touchEnd = callback; 808 | }; 809 | 810 | /** 811 | * Called when this direction is first touched 812 | */ 813 | TouchableArea.prototype.touchEndWrapper = function() { 814 | // Fire the user specified callback 815 | if( this.touchEnd ) this.touchEnd(); 816 | // Mark this direction as inactive 817 | this.active = false; 818 | GameController.render(); 819 | }; 820 | 821 | return TouchableArea; 822 | } )(); 823 | 824 | var TouchableDirection = ( function( __super ) { 825 | function TouchableDirection( options ) { 826 | for( var i in options ) { 827 | if( i === 'x' ) this[i] = GameController.getPixels(options[i], 'x'); 828 | else if( i === 'y' || i === 'height' || i === 'width' ) 829 | this[i] = GameController.getPixels(options[i], 'y'); 830 | else this[i] = options[i]; 831 | } 832 | this.draw(); 833 | } 834 | __extends( TouchableDirection, __super); 835 | 836 | TouchableDirection.prototype.type = 'direction'; 837 | 838 | /** 839 | * Checks if the touch is within the bounds of this direction 840 | */ 841 | TouchableDirection.prototype.check = function( touchX, touchY ) { 842 | var halfR = GameController.options.touchRadius / 2; 843 | return (Math.abs(touchX - this.x) < halfR || 844 | touchX > this.x ) && // left 845 | (Math.abs(touchX - this.x - this.width) < halfR || // right 846 | touchX < this.x + this.width) && 847 | (Math.abs(touchY - this.y) < halfR || touchY > this.y ) && // top 848 | (Math.abs(touchY - this.y - this.height) < halfR || // bottom 849 | touchY < this.y + this.height); 850 | }; 851 | 852 | TouchableDirection.prototype.draw = function() { 853 | var gradient; 854 | var cacheId = this.type + '' + this.id + '' + this.active; 855 | var cached = GameController.cachedSprites[ cacheId ]; 856 | if( ! cached ) { 857 | var subCanvas = document.createElement('canvas'); 858 | var ctx = subCanvas.getContext( '2d'); 859 | subCanvas.width = this.width + 2 * this.stroke; 860 | subCanvas.height = this.height + 2 * this.stroke; 861 | 862 | var opacity = this.opacity || 0.9; 863 | 864 | // Direction currently being touched 865 | if( ! this.active ) opacity *= 0.5; 866 | 867 | switch( this.direction ) { 868 | case 'up': 869 | gradient = ctx.createLinearGradient( 0, 0, 0, this.height); 870 | gradient.addColorStop(0, 'rgba(0, 0, 0, ' + (opacity * 0.5) + ')'); 871 | gradient.addColorStop(1, 'rgba(0, 0, 0, ' + opacity + ')'); 872 | break; 873 | case 'left': 874 | gradient = ctx.createLinearGradient( 0, 0, this.width, 0); 875 | gradient.addColorStop(0, 'rgba(0, 0, 0, ' + (opacity * 0.5) + ')'); 876 | gradient.addColorStop(1, 'rgba(0, 0, 0, ' + opacity + ')'); 877 | break; 878 | case 'right': 879 | gradient = ctx.createLinearGradient( 0, 0, this.width, 0); 880 | gradient.addColorStop(0, 'rgba(0, 0, 0, ' + opacity + ')'); 881 | gradient.addColorStop(1, 'rgba(0, 0, 0, ' + (opacity * 0.5) + ')'); 882 | break; 883 | default: 884 | case 'down': 885 | gradient = ctx.createLinearGradient( 0, 0, 0, this.height); 886 | gradient.addColorStop(0, 'rgba(0, 0, 0, ' + opacity + ')'); 887 | gradient.addColorStop(1, 'rgba(0, 0, 0, ' + (opacity * 0.5 ) + ')'); 888 | } 889 | ctx.fillStyle = gradient; 890 | ctx.fillRect(0, 0, this.width, this.height); 891 | ctx.lineWidth = this.stroke; 892 | ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; 893 | ctx.strokeRect(0, 0, this.width, this.height); 894 | cached = GameController.cachedSprites[ cacheId ] = subCanvas; 895 | } 896 | 897 | GameController.ctx.drawImage(cached, this.x, this.y); 898 | }; 899 | 900 | return TouchableDirection; 901 | } )( TouchableArea); 902 | 903 | var TouchableButton = ( function( __super ) { 904 | 905 | //x, y, radius, backgroundColor ) 906 | function TouchableButton( options ) { 907 | for( var i in options ) { 908 | if( i === 'x' ) this[i] = GameController.getPixels( options[i], 'x'); 909 | else if( i === 'y' || i === 'radius' ){ 910 | this[i] = GameController.getPixels(options[i], 'y'); 911 | } else this[i] = options[i]; 912 | } 913 | this.draw(); 914 | } 915 | __extends( TouchableButton, __super); 916 | 917 | TouchableButton.prototype.type = 'button'; 918 | 919 | /** 920 | * Checks if the touch is within the bounds of this direction 921 | */ 922 | TouchableButton.prototype.check = function( touchX, touchY ) { 923 | var radius = this.radius + GameController.options.touchRadius / 2; 924 | return Math.abs(touchX - this.x) < radius && 925 | Math.abs(touchY - this.y) < radius; 926 | }; 927 | 928 | TouchableButton.prototype.draw = function() { 929 | var cacheId = this.type + '' + this.id + '' + this.active, 930 | cached = GameController.cachedSprites[ cacheId ], 931 | r = this.radius; 932 | if( ! cached ){ 933 | var subCanvas = document.createElement('canvas'); 934 | var ctx = subCanvas.getContext( '2d'); 935 | ctx.lineWidth = this.stroke; 936 | subCanvas.width = subCanvas.height = 2 * (r + ctx.lineWidth); 937 | 938 | var gradient = ctx.createRadialGradient(r, r, 1, r, r, r); 939 | var textShadowColor; 940 | switch( this.backgroundColor ) { 941 | case 'blue': 942 | gradient.addColorStop(0, 'rgba(123, 181, 197, 0.6)'); 943 | gradient.addColorStop(1, '#105a78'); 944 | textShadowColor = '#0A4861'; 945 | break; 946 | case 'green': 947 | gradient.addColorStop(0, 'rgba(29, 201, 36, 0.6)'); 948 | gradient.addColorStop(1, '#107814'); 949 | textShadowColor = '#085C0B'; 950 | break; 951 | case 'red': 952 | gradient.addColorStop(0, 'rgba(165, 34, 34, 0.6)'); 953 | gradient.addColorStop(1, '#520101'); 954 | textShadowColor = '#330000'; 955 | break; 956 | case 'yellow': 957 | gradient.addColorStop(0, 'rgba(219, 217, 59, 0.6)'); 958 | gradient.addColorStop(1, '#E8E10E'); 959 | textShadowColor = '#BDB600'; 960 | break; 961 | default: 962 | case 'white': 963 | gradient.addColorStop(0, 'rgba( 255,255,255,.3 )'); 964 | gradient.addColorStop(1, '#eee'); 965 | break; 966 | } 967 | 968 | if( this.active ) ctx.fillStyle = textShadowColor; 969 | else ctx.fillStyle = gradient; 970 | 971 | ctx.strokeStyle = textShadowColor; 972 | ctx.beginPath(); 973 | //ctx.arc( this.x, this.y, r, 0 , PI2, false); 974 | var halfW = subCanvas.width / 2; 975 | ctx.arc(halfW, halfW, r, 0 , PI2, false); 976 | ctx.fill(); 977 | ctx.stroke(); 978 | 979 | if( this.label ) { 980 | // Text Shadow 981 | var fontSize = this.fontSize || subCanvas.height * 0.35, 982 | halfH = subCanvas.height / 2; 983 | ctx.fillStyle = textShadowColor; 984 | ctx.font = 'bold ' + fontSize + 'px Verdana'; 985 | ctx.textAlign = 'center'; 986 | ctx.textBaseline = 'middle'; 987 | ctx.fillText(this.label, halfH + 2, halfH + 2); 988 | 989 | ctx.fillStyle = this.fontColor; 990 | ctx.fillText(this.label, halfH, halfH); 991 | } 992 | 993 | cached = GameController.cachedSprites[ cacheId ] = subCanvas; 994 | } 995 | 996 | GameController.ctx.drawImage( cached, this.x, this.y); 997 | }; 998 | 999 | return TouchableButton; 1000 | } )( TouchableArea); 1001 | 1002 | var TouchableJoystick = ( function( __super ) { 1003 | function TouchableJoystick( options ) { 1004 | for( var i in options ) this[i] = options[i]; 1005 | this.currentX = this.currentX || this.x; 1006 | this.currentY = this.currentY || this.y; 1007 | } 1008 | __extends( TouchableJoystick, __super); 1009 | 1010 | TouchableJoystick.prototype.type = 'joystick'; 1011 | 1012 | /** 1013 | * Checks if the touch is within the bounds of this direction 1014 | */ 1015 | TouchableJoystick.prototype.check = function( touchX, touchY ) { 1016 | var edge = this.radius + 1017 | GameController.getPixels(GameController.options.touchRadius) / 2; 1018 | return Math.abs(touchX - this.x) < edge && 1019 | Math.abs(touchY - this.y) < edge; 1020 | }; 1021 | 1022 | /** 1023 | * details for the joystick move event, stored here so we're not 1024 | * constantly creating new objs for garbage. The object has params: 1025 | * dx - the number of pixels the current joystick center is from 1026 | * the base center in x direction 1027 | * dy - the number of pixels the current joystick center is from 1028 | * the base center in y direction 1029 | * max - the maximum number of pixels dx or dy can be 1030 | * normalizedX - a number between -1 and 1 relating to how far 1031 | * left or right the joystick is 1032 | * normalizedY - a number between -1 and 1 relating to how far 1033 | * up or down the joystick is 1034 | */ 1035 | TouchableJoystick.prototype.moveDetails = {}; 1036 | 1037 | /** 1038 | * Called when this joystick is moved 1039 | */ 1040 | TouchableJoystick.prototype.touchMoveWrapper = function( e ) { 1041 | this.currentX = GameController.normalizeTouchPositionX(e.clientX); 1042 | this.currentY = GameController.normalizeTouchPositionY(e.clientY); 1043 | 1044 | // Fire the user specified callback 1045 | if( this.touchMove && 1046 | this.moveDetails.dx !== this.currentX - this.x && 1047 | this.moveDetails.dy !== this.y - this.currentY 1048 | ){ 1049 | // reverse so right is positive 1050 | this.moveDetails.dx = this.currentX - this.x; 1051 | this.moveDetails.dy = this.y - this.currentY; 1052 | this.moveDetails.max = 1053 | this.radius + GameController.options.touchRadius / 2; 1054 | this.moveDetails.normalizedX = 1055 | this.moveDetails.dx / this.moveDetails.max; 1056 | this.moveDetails.normalizedY = 1057 | this.moveDetails.dy / this.moveDetails.max; 1058 | this.touchMove(this.moveDetails); 1059 | } 1060 | 1061 | // Mark this direction as inactive 1062 | this.active = true; 1063 | }; 1064 | 1065 | TouchableJoystick.prototype.draw = function() { 1066 | if( ! this.id ) return false; 1067 | var gradient, ctx, 1068 | cacheId = this.type + '' + this.id + '' + this.active, 1069 | cached = GameController.cachedSprites[ cacheId ], 1070 | r = this.radius; 1071 | if( ! cached ) { 1072 | var subCanvas = document.createElement('canvas'); 1073 | this.stroke = this.stroke || 2; 1074 | subCanvas.width = subCanvas.height = 1075 | 2 * (this.radius + GameController.options.touchRadius + this.stroke); 1076 | 1077 | ctx = subCanvas.getContext( '2d'); 1078 | ctx.lineWidth = this.stroke; 1079 | // Direction currently being touched 1080 | if( this.active ) { 1081 | gradient = ctx.createRadialGradient( 0, 0, 1, 0, 0, r); 1082 | gradient.addColorStop(0, 'rgba( 200,200,200,.5 )'); 1083 | gradient.addColorStop(1, 'rgba( 200,200,200,.9 )'); 1084 | ctx.strokeStyle = '#000'; 1085 | } else { 1086 | // STYLING FOR BUTTONS 1087 | gradient = ctx.createRadialGradient( 0, 0, 1, 0, 0, r); 1088 | gradient.addColorStop(0, 'rgba( 200,200,200,.2 )'); 1089 | gradient.addColorStop(1, 'rgba( 200,200,200,.4 )'); 1090 | ctx.strokeStyle = 'rgba( 0,0,0,.4 )'; 1091 | } 1092 | ctx.fillStyle = gradient; 1093 | // Actual joystick part that is being moved 1094 | ctx.beginPath(); 1095 | ctx.arc(r, r, r, 0 , PI2, false); 1096 | ctx.fill(); 1097 | ctx.stroke(); 1098 | cached = GameController.cachedSprites[ cacheId ] = subCanvas; 1099 | } 1100 | 1101 | // Draw the base that stays static 1102 | ctx = GameController.ctx; 1103 | ctx.fillStyle = '#444'; 1104 | ctx.beginPath(); 1105 | ctx.arc(this.x, this.y, r * 0.7, 0 , PI2, false); 1106 | ctx.fill(); 1107 | ctx.stroke(); 1108 | ctx.drawImage(cached, this.currentX - r, this.currentY - r); 1109 | }; 1110 | 1111 | return TouchableJoystick; 1112 | } )( TouchableArea); 1113 | 1114 | 1115 | var TouchableCircle = ( function( __super ) { 1116 | function TouchableCircle( options ) { 1117 | for( var i in options ) { 1118 | if( i === 'x' ) this[i] = GameController.getPixels( options[i], 'x'); 1119 | else if( i === 'y' || i === 'radius' ){ 1120 | this[i] = GameController.getPixels(options[i], 'y'); 1121 | } else this[i] = options[i]; 1122 | } 1123 | this.draw(); 1124 | } 1125 | 1126 | __extends( TouchableCircle, __super); 1127 | 1128 | /** 1129 | * No touch for this fella 1130 | */ 1131 | TouchableCircle.prototype.check = function(){ return false; }; 1132 | 1133 | TouchableCircle.prototype.draw = function() { 1134 | // STYLING FOR BUTTONS 1135 | GameController.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; 1136 | // Actual joystick part that is being moved 1137 | GameController.ctx.beginPath(); 1138 | GameController.ctx.arc(this.x, this.y, this.radius, 0 , PI2, false); 1139 | GameController.ctx.fill(); 1140 | }; 1141 | 1142 | return TouchableCircle; 1143 | } )( TouchableArea); 1144 | 1145 | /** 1146 | * Shim for requestAnimationFrame 1147 | */ 1148 | (function() { 1149 | if (typeof module !== 'undefined') return; 1150 | var lastTime = 0; 1151 | var vendors = ['ms', 'moz', 'webkit', 'o']; 1152 | requestAnimationFrame = window.requestAnimationFrame; 1153 | cancelAnimationFrame = window.cancelAnimationFrame; 1154 | for( var x = 0; x < vendors.length && !requestAnimationFrame; ++x ){ 1155 | requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 1156 | cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || 1157 | window[vendors[x]+'CancelRequestAnimationFrame']; 1158 | } 1159 | 1160 | if (!requestAnimationFrame){ 1161 | requestAnimationFrame = function(callback) { 1162 | var currTime = Date.now(); 1163 | var timeToCall = Math.max(10, 16 - currTime + lastTime); 1164 | lastTime = currTime + timeToCall; 1165 | return window.setTimeout(function() { 1166 | callback(currTime + timeToCall); 1167 | }, timeToCall); 1168 | }; 1169 | } 1170 | 1171 | if (!cancelAnimationFrame){ 1172 | cancelAnimationFrame = function(id){ clearTimeout( id); }; 1173 | } 1174 | }()); 1175 | })(typeof module !== 'undefined' ? module.exports : window); 1176 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 | 71 | 72 | 73 | --------------------------------------------------------------------------------