├── .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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------