├── README.md ├── .gitignore ├── package.json ├── LICENSE └── index.js /README.md: -------------------------------------------------------------------------------- 1 | Simulator 2 | ========= 3 | 4 | Simulate gestures 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hammer-simulator", 3 | "version": "0.0.0", 4 | "description": "Simulator\r =========", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/hammerjs/simulator.git" 12 | }, 13 | "keywords": [ 14 | "hammer", 15 | "gestures", 16 | "simulator" 17 | ], 18 | "author": "J. Tangelder", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/hammerjs/simulator/issues" 22 | }, 23 | "homepage": "https://github.com/hammerjs/simulator" 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Hammer.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | /** 3 | * return unchanged if array passed, otherwise wrap in an array 4 | * @param {Object|Array|Null} arr any object 5 | * @return {Array} 6 | */ 7 | function ensureAnArray (arr) { 8 | if (Object.prototype.toString.call(arr) === '[object Array]') { 9 | return arr; 10 | } else if (arr === null || arr === void 0) { 11 | return []; 12 | } else { 13 | return [arr]; 14 | } 15 | } 16 | 17 | var Simulator = { 18 | type: 'touch', 19 | 20 | /** 21 | * set type 22 | * @param type 23 | */ 24 | setType: function(type) { 25 | if(!Simulator.events[type]) { 26 | throw new Error(type + " is not a valid event type."); 27 | } 28 | return this.type = type; 29 | } 30 | }; 31 | 32 | 33 | // simple easing methods 34 | // found at the source of velocity.js 35 | Simulator.easings = { 36 | linear: function(p) { return p; }, 37 | swing: function(p) { return 0.5 - Math.cos(p * Math.PI) / 2; }, 38 | quad: function(p) { return Math.pow(p, 2); }, 39 | cubic: function(p) { return Math.pow(p, 3); }, 40 | quart: function(p) { return Math.pow(p, 4); }, 41 | quint: function(p) { return Math.pow(p, 5); }, 42 | expo: function(p) { return Math.pow(p, 6); } 43 | }; 44 | 45 | Simulator.events = { 46 | /** 47 | * pointer events 48 | */ 49 | pointer: { 50 | fakeSupport: function() { 51 | if(!("PointerEvent" in window)) { 52 | navigator.maxTouchPoints = 10; 53 | window.PointerEvent = function () {}; 54 | } 55 | }, 56 | 57 | typeMap: { 58 | start: 'pointerdown', 59 | move: 'pointermove', 60 | end: 'pointerup', 61 | cancel: 'pointercancel' 62 | }, 63 | 64 | trigger: function(touches, element, type) { 65 | touches.forEach(function (touch, i) { 66 | var x = Math.round(touch.x), 67 | y = Math.round(touch.y); 68 | 69 | var eventType = this.typeMap[type]; 70 | // ie10 style events 71 | var msEventType = window.MSPointerEvent && eventType.replace(/pointer([a-z])/, function(_, a) { 72 | return 'MSPointer'+ a.toUpperCase(); 73 | }); 74 | 75 | var event = document.createEvent('Event'); 76 | event.initEvent(msEventType || eventType, true, true); 77 | 78 | event.getCurrentPoint = function() { return touch; }; 79 | event.setPointerCapture = event.releasePointerCapture = function() { }; 80 | 81 | event.pointerId = i; 82 | event.buttons = 1; 83 | event.pageX = x; 84 | event.pageY = y; 85 | event.clientX = x; 86 | event.clientY = y; 87 | event.screenX = x; 88 | event.screenY = y; 89 | event.target = element; 90 | event.pointerType = 'touch'; 91 | event.identifier = i; 92 | 93 | element.dispatchEvent(event); 94 | }, this); 95 | 96 | renderTouches(touches, element); 97 | } 98 | }, 99 | 100 | /** 101 | * touch events 102 | */ 103 | touch: { 104 | fakeSupport: function() { 105 | if(!("ontouchstart" in window)) { 106 | window.ontouchstart = function () {}; 107 | } 108 | }, 109 | 110 | emptyTouchList: function() { 111 | var touchList = []; 112 | touchList.identifiedTouch = touchList.item = function(index) { 113 | return this[index] || {}; 114 | }; 115 | return touchList; 116 | }, 117 | 118 | trigger: function (touches, element, type) { 119 | var touchList = this.emptyTouchList(); 120 | touches.forEach(function (touch, i) { 121 | var x = Math.round(touch.x), 122 | y = Math.round(touch.y); 123 | 124 | touchList.push({ 125 | pageX: x, 126 | pageY: y, 127 | clientX: x, 128 | clientY: y, 129 | screenX: x, 130 | screenY: y, 131 | target: touch.target, 132 | identifier: i 133 | }); 134 | }); 135 | 136 | var event = document.createEvent('Event'); 137 | event.initEvent('touch' + type, true, true); 138 | 139 | if (type !== 'end') { 140 | var targetTouches = touchList.filter(function(touch){ 141 | return touch.target === element; 142 | }) 143 | 144 | event.changedTouches = targetTouches; 145 | } else { 146 | // assume that last touch is released touch. Pop it out from list of touches 147 | event.changedTouches = [touchList.pop()]; 148 | 149 | var targetTouches = touchList.filter(function(touch){ 150 | return touch.target === element; 151 | }) 152 | } 153 | 154 | event.touches = touchList; 155 | event.targetTouches = targetTouches; 156 | element.dispatchEvent(event); 157 | 158 | renderTouches(touches, element); 159 | } 160 | } 161 | }; 162 | 163 | /** 164 | * merge objects 165 | * @param dest 166 | * @param src 167 | * @returns dest 168 | */ 169 | function merge(dest, src) { 170 | dest = dest || {}; 171 | src = src || {}; 172 | for (var key in src) { 173 | if (src.hasOwnProperty(key) && dest[key] === undefined) { 174 | dest[key] = src[key]; 175 | } 176 | } 177 | return dest; 178 | } 179 | 180 | /** 181 | * generate a list of x/y around the center 182 | * @param center 183 | * @param countTouches 184 | * @param [radius=100] 185 | * @param [rotation=0] 186 | */ 187 | function getTouches(center, elements, countTouches, radius, rotation) { 188 | var cx = center[0], 189 | cy = center[1], 190 | touches = [], 191 | slice, i, angle; 192 | 193 | elements = ensureAnArray(elements); 194 | 195 | // just one touch, at the center 196 | if (countTouches === 1) { 197 | if (elements.length) { 198 | return [{ x: cx, y: cy, target: elements[0] }]; 199 | } else { 200 | return [{ x: cx, y: cy }]; 201 | } 202 | } 203 | 204 | radius = radius || 100; 205 | rotation = (rotation * Math.PI / 180) || 0; 206 | slice = 2 * Math.PI / countTouches; 207 | 208 | for (i = 0; i < countTouches; i++) { 209 | angle = (slice * i) + rotation; 210 | touches.push({ 211 | x: (cx + radius * Math.cos(angle)), 212 | y: (cy + radius * Math.sin(angle)), 213 | target: elements[i % elements.length] 214 | }); 215 | } 216 | 217 | return touches; 218 | } 219 | 220 | /** 221 | * render the touches 222 | * @param touches 223 | * @param element 224 | * @param type 225 | */ 226 | function renderTouches(touches, element) { 227 | touches.forEach(function(touch) { 228 | var el = document.createElement('div'); 229 | el.style.width = '20px'; 230 | el.style.height = '20px'; 231 | el.style.background = 'red'; 232 | el.style.position = 'fixed'; 233 | el.style.top = touch.y +'px'; 234 | el.style.left = touch.x +'px'; 235 | el.style.borderRadius = '100%'; 236 | el.style.border = 'solid 2px #000'; 237 | el.style.zIndex = 6000; 238 | 239 | element.appendChild(el); 240 | setTimeout(function() { 241 | el && el.parentNode && el.parentNode.removeChild(el); 242 | el = null; 243 | }, 100); 244 | }); 245 | } 246 | 247 | /** 248 | * trigger the touch events 249 | * @param touches 250 | * @param element 251 | * @param type 252 | * @returns {*} 253 | */ 254 | function trigger(touches, element, type) { 255 | return Simulator.events[Simulator.type].trigger(touches, element, type); 256 | } 257 | 258 | /** 259 | * trigger a gesture 260 | * @param elements 261 | * @param startTouches 262 | * @param options 263 | * @param done 264 | */ 265 | function triggerGesture(elements, startTouches, options, done) { 266 | var interval = 10, 267 | loops = Math.ceil(options.duration / interval), 268 | loop = 1; 269 | 270 | elements = ensureAnArray(elements); 271 | 272 | options = merge(options, { 273 | pos: [10, 10], 274 | duration: 250, 275 | touches: 1, 276 | deltaX: 0, 277 | deltaY: 0, 278 | radius: 100, 279 | scale: 1, 280 | rotation: 0, 281 | easing: 'swing' 282 | }); 283 | 284 | function gestureLoop() { 285 | // calculate the radius 286 | // this is for scaling and multiple touches 287 | var radius = options.radius; 288 | if (options.scale !== 1) { 289 | radius = options.radius - (options.radius * (1 - options.scale) * (1 / loops * loop)); 290 | } 291 | 292 | // calculate new position/rotation 293 | var easing = Simulator.easings[options.easing](1 / loops * loop), 294 | posX = options.pos[0] + (options.deltaX / loops * loop) * easing, 295 | posY = options.pos[1] + (options.deltaY / loops * loop) * easing, 296 | rotation = options.rotation / loops * loop, 297 | touches = getTouches([posX, posY], elements, startTouches.length, radius, rotation), 298 | isFirst = (loop == 1), 299 | isLast = (loop == loops); 300 | 301 | for (var t = touches.length - 1; t >= 0; t--) { 302 | if (isFirst) { 303 | trigger(touches, touches[t].target, 'start'); 304 | } else if (isLast) { 305 | trigger(touches, touches[t].target, 'end'); 306 | 307 | // Remove processed touch 308 | touches.pop() 309 | 310 | if (touches.length === 0) { 311 | return done(); 312 | } 313 | } else { 314 | trigger(touches, touches[t].target, 'move'); 315 | } 316 | } 317 | 318 | setTimeout(gestureLoop, interval); 319 | loop++; 320 | } 321 | gestureLoop(); 322 | } 323 | 324 | Simulator.gestures = { 325 | /** 326 | * press 327 | * @param element 328 | * @param options 329 | * @param done 330 | */ 331 | press: function(element, options, done) { 332 | options = merge(options, { 333 | pos: [10, 10], 334 | duration: 500, 335 | touches: 1 336 | }); 337 | 338 | var touches = getTouches(options.pos, element, 1); 339 | 340 | trigger(touches, element, 'start'); 341 | setTimeout(function() { 342 | trigger(touches, element, 'end'); 343 | done && setTimeout(done, 25); 344 | }, options.duration); 345 | }, 346 | 347 | /** 348 | * tap 349 | * @param element 350 | * @param options 351 | * @param done 352 | */ 353 | tap: function(element, options, done) { 354 | options = merge(options, { 355 | pos: [10, 10], 356 | duration: 100, 357 | touches: 1 358 | }); 359 | 360 | var touches = getTouches(options.pos, element, 1); 361 | trigger(touches, element, 'start'); 362 | setTimeout(function() { 363 | trigger(touches, element, 'end'); 364 | done && setTimeout(done, 25); 365 | }, options.duration); 366 | }, 367 | 368 | /** 369 | * double tap 370 | * @param element 371 | * @param options 372 | * @param done 373 | */ 374 | doubleTap: function(element, options, done) { 375 | options = merge(options, { 376 | pos: [10, 10], 377 | pos2: [11, 11], 378 | duration: 100, 379 | interval: 200, 380 | touches: 1 381 | }); 382 | 383 | Simulator.gestures.tap(element, options, function() { 384 | setTimeout(function() { 385 | options.pos = options.pos2; 386 | Simulator.gestures.tap(element, options, done); 387 | }, options.interval); 388 | }); 389 | }, 390 | 391 | /** 392 | * pan 393 | * @param element 394 | * @param options 395 | * @param done 396 | */ 397 | pan: function(element, options, done) { 398 | options = merge(options, { 399 | pos: [10, 10], 400 | deltaX: 300, 401 | deltaY: 150, 402 | duration: 250, 403 | touches: 1 404 | }); 405 | 406 | var touches = getTouches(options.pos, element, options.touches); 407 | triggerGesture(element, touches, options, function() { 408 | done && setTimeout(done, 25); 409 | }); 410 | }, 411 | 412 | /** 413 | * swipe 414 | * @param element 415 | * @param options 416 | * @param done 417 | */ 418 | swipe: function(element, options, done) { 419 | options = merge(options, { 420 | pos: [10, 10], 421 | deltaX: 300, 422 | deltaY: 150, 423 | duration: 250, 424 | touches: 1, 425 | easing: 'cubic' 426 | }); 427 | 428 | var touches = getTouches(options.pos, element, options.touches); 429 | triggerGesture(element, touches, options, function() { 430 | done && setTimeout(done, 25); 431 | }); 432 | }, 433 | 434 | /** 435 | * pinch 436 | * @param {HTMLElement|Array} elements 437 | * @param options 438 | * @param done 439 | */ 440 | pinch: function(elements, options, done) { 441 | elements = ensureAnArray(elements); 442 | options = merge(options, { 443 | pos: [300, 300], 444 | scale: 2, 445 | duration: 250, 446 | radius: 100, 447 | touches: 2 448 | }); 449 | 450 | var touches = getTouches(options.pos, elements, options.touches); 451 | triggerGesture(elements, touches, options, function() { 452 | done && setTimeout(done, 25); 453 | }); 454 | }, 455 | 456 | /** 457 | * rotate 458 | * @param {HTMLElement|Array} elements 459 | * @param options 460 | * @param done 461 | */ 462 | rotate: function(elements, options, done) { 463 | elements = ensureAnArray(elements); 464 | options = merge(options, { 465 | pos: [300, 300], 466 | rotation: 180, 467 | duration: 250, 468 | touches: 2 469 | }); 470 | 471 | var touches = getTouches(options.pos, elements, options.touches); 472 | triggerGesture(elements, touches, options, function() { 473 | done && setTimeout(done, 25); 474 | }); 475 | }, 476 | 477 | /** 478 | * combination of pinch and rotate 479 | * @param {HTMLElement|Array} elements 480 | * @param options 481 | * @param done 482 | */ 483 | pinchRotate: function(elements, options, done) { 484 | elements = ensureAnArray(elements); 485 | options = merge(options, { 486 | pos: [300, 300], 487 | rotation: 180, 488 | radius: 100, 489 | scale: .5, 490 | duration: 250, 491 | touches: 2 492 | }); 493 | 494 | var touches = getTouches(options.pos, elements, options.touches); 495 | triggerGesture(elements, touches, options, function() { 496 | done && setTimeout(done, 25); 497 | }); 498 | } 499 | }; 500 | 501 | // initial 502 | if(window.PointerEvent || window.MSPointerEvent) { 503 | Simulator.setType('pointer'); 504 | } else { 505 | Simulator.setType('touch'); 506 | Simulator.events.touch.fakeSupport(); 507 | } 508 | 509 | window.Simulator = Simulator; 510 | })(); 511 | --------------------------------------------------------------------------------