├── .editorconfig ├── .gitignore ├── demos ├── assets │ ├── knob-bg.png │ └── knob.png ├── demo1.html ├── demo2.html ├── demo3-2.html ├── demo3.html ├── demo4.html ├── demo5.html ├── demo6.html ├── demo7.html └── demo8.html ├── dist ├── debug │ ├── momentum.min.js │ └── momentum.min.js.gz ├── index.js ├── momentum.min.js └── momentum.min.js.gz ├── etc ├── momentum.deps.js └── momentum.externs.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── readme.md └── src ├── momentum ├── Coordinate.js ├── Draggable.js ├── Handler.js ├── HandlerComponent.js ├── Rotatable.js ├── TrackingPoint.js ├── index.js ├── index.m.js └── utils.js └── momentumdebug ├── Configurator.js ├── ScriptLoader.js └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | 6 | [*.js] 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | .history -------------------------------------------------------------------------------- /demos/assets/knob-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davideperozzi/momentum-js/c8ba6dc1e28baea9ca2ccf18f721735d0ce5edcf/demos/assets/knob-bg.png -------------------------------------------------------------------------------- /demos/assets/knob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davideperozzi/momentum-js/c8ba6dc1e28baea9ca2ccf18f721735d0ce5edcf/demos/assets/knob.png -------------------------------------------------------------------------------- /demos/demo1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 1) 5 | 6 | 67 | 68 | 69 |
70 |
71 |
72 | 73 |
74 |
75 |
76 | 77 | 78 | 79 | 97 | 98 | -------------------------------------------------------------------------------- /demos/demo2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 2) 5 | 6 | 30 | 31 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 50 | 51 | -------------------------------------------------------------------------------- /demos/demo3-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 3 - 2) 5 | 6 | 49 | 50 | 51 |
52 |
53 |
54 |
0.0%
55 |
56 |
57 |
58 | 59 | 60 | 61 | 77 | 78 | -------------------------------------------------------------------------------- /demos/demo3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 3) 5 | 6 | 49 | 50 | 51 |
52 |
53 |
54 |
0.0%
55 |
56 |
57 |
58 | 59 | 60 | 61 | 78 | 79 | -------------------------------------------------------------------------------- /demos/demo4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 4) 5 | 6 | 33 | 34 | 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | 56 | 57 | -------------------------------------------------------------------------------- /demos/demo5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 5) 5 | 6 | 51 | 52 | 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 | 65 | 70 | 71 | -------------------------------------------------------------------------------- /demos/demo6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 6) 5 | 6 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 79 | 80 | -------------------------------------------------------------------------------- /demos/demo7.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 7) 5 | 6 | 84 | 85 | 86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aut, eum. Soluta et veritatis dolorem, magni quae maiores autem. Magni amet vitae provident distinctio deleniti modi sit, accusantium alias? Quis, id. 96 |
97 | 98 | 99 | 100 | 101 | 109 | 110 | -------------------------------------------------------------------------------- /demos/demo8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | momentum.js (Demo 8) 5 | 6 | 106 | 107 | 108 |
109 |
110 |
111 |
112 | 113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | 179 |
180 |
181 | 182 | 183 | 184 | 185 | 186 | 200 | 201 | -------------------------------------------------------------------------------- /dist/debug/momentum.min.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var e="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)},g="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this;function h(a){if(a){for(var b=g,c=["String","prototype","repeat"],d=0;da||1342177279>>=1)b+=b;return d}});var k=this;function n(a){var b=typeof a;return"object"==b&&null!=a||"function"==b}function p(a,b,c){return a.call.apply(a.bind,arguments)} 4 | function q(a,b,c){if(!a)throw Error();if(2=this.F||Math.abs(this.c.y)>=this.F||0!=this.h.x||0!=this.h.y)&&this.ia();a=0;for(var b=this.Z.length;a=a.f.I)a.c.x=-1*a.c.x*a.i.x}else{0!=a.h.x?(a.m.x=a.w.x,0!=a.h.x&&(b=a.i.x,a.o&&(b/=2),a.a.x+=L(a.h.x*(1+b)))):a.m.x=0;b=K(a.f.K-a.a.x);var c=K(a.f.I-a.a.x);a.h.x=0c?c:0}if(a.W)if(0<=a.i.y){if(b=a.a,b.y=Math.min(Math.max(b.y,a.f.L),a.f.J),a.a.y<=a.f.L||a.a.y>=a.f.J)a.c.y=-1*a.c.y*a.i.y}else 0!=a.h.y?(a.m.y=a.w.y,0!=a.h.y&&(b=a.i.x,a.o&&(b/=2),a.a.y+=L(a.h.y*(1+b)))):a.m.y=0,b= 20 | K(a.f.L-a.a.y),c=K(a.f.J-a.a.y),a.h.y=0c?c:0}if(a.a.x!=a.C.x||a.a.y!=a.C.y){b=0;for(c=a.X.length;ba.width,c=this.f.height>a.height,this.a.oa(b?this.c.x+a.width-this.f.width:this.b.bounds.x+this.c.x,b?this.c.x:this.b.bounds.x+this.b.bounds.width-(this.f.width-this.c.x),c?this.c.y+a.height-this.f.height:this.b.bounds.y+this.c.y,c?this.c.y:this.b.bounds.y+this.b.bounds.height-(this.f.height-this.c.y)))};N.prototype.updateBounds=N.prototype.R; 27 | N.prototype.S=function(){this.i.x=0;for(var a=this.i.y=0,b=this.j.length;ad.height&&(this.i.y+=c.scrollTop);c.scrollWidth>d.width&&(this.i.x+=c.scrollLeft)}};N.prototype.updateScrollPositions=N.prototype.S;N.prototype.H=function(){return this.a}; 28 | N.prototype.update=function(a){(a=a&&!0===a)||this.ga();this.f=this.g.getBoundingClientRect();this.b.autoAnchor||(this.l.x=void 0!==this.b.anchorX?this.b.anchorX:this.l.x,this.l.y=void 0!==this.b.anchorY?this.b.anchorY:this.l.y,this.c.x=this.f.width*this.l.x,this.c.y=this.f.height*this.l.y);this.h.x=this.g.offsetLeft;this.h.y=this.g.offsetTop;for(var b=[],c=new v,d=this.g.parentElement;d&&d!=this.a.g;){var f=window.getComputedStyle(d).position,l=window.getComputedStyle(d).overflow;if("relative"== 29 | f||"absolute"==f)f=F(d),f.x>c.x&&(c.x=f.x),f.y>c.y&&(c.y=f.y);"auto"!=l&&"scroll"!=l||b.push(d);d=d.parentElement}this.h.x+=c.x;this.h.y+=c.y;c=this.j.slice(0);this.j=[];d=0;for(l=c.length;d=d&&a=f&&ba&&"none"==f.style.display?f.style.display="block":0<=a&&"block"==f.style.display&&(f.style.display="none")}.bind(a));var l=d.add(c,"offsetFriction",0,1).onFinishChange(function(a){b.da(a)}.bind(a)).name("offset friction");f=E(l.domElement);0<=c.restitution&&(f.style.display="none");l=d.add(c,"maxVelocity", 39 | 0,100);d.add(c,"threshold",0).onFinishChange(function(a){b.fa(a)});var m=c.maxVelocity,x=d.add(c,"velocityX",-m,m).step(.05).name("X-Velocity").listen(),aa=d.add(c,"velocityY",-m,m).step(.05).name("Y-Velocity").listen();l.onFinishChange(function(a){x.min(-a).max(a);aa.min(-a).max(a);b.ca(a)});b.onMove(function(a,b,d,f){c.velocityX=d;c.velocityY=f});d.open();a.a.push(d)} 40 | function Z(a,b){var c=new U(b),d=a.c.addFolder("Draggable"),f=null,l=null;d.add(c,"autoAnchor").onFinishChange(function(a){b.b.autoAnchor=a;f&&l&&(f.style.display=a?"none":"",l.style.display=a?"none":"");a||void 0!==b.b.anchorX||(b.b.anchorX=c.anchorX);a||void 0!==b.b.anchorY||(b.b.anchorY=c.anchorY);b.update(!0)});var m=d.add(c,"anchorX",0,1).onFinishChange(function(a){b.b.anchorX=a;b.update(!0)}),x=d.add(c,"anchorY",0,1).onFinishChange(function(a){b.b.anchorY=a;b.update(!0)});f=E(m.domElement); 41 | l=E(x.domElement);b.b.autoAnchor&&(f.style.display="none",l.style.display="none");d.add(c,"lockAxisX").onFinishChange(function(a){b.b.lockAxis={x:a,y:c.lockAxisY};b.update(!0)});d.add(c,"lockAxisY").onFinishChange(function(a){b.b.lockAxis={x:c.lockAxisX,y:a};b.update(!0)});d.add(c,"reset").name("Reset draggable").onFinishChange(function(){b.reset()});a.a.push(d)};t("momentumdebug.Configurator",W); 42 | })() 43 | -------------------------------------------------------------------------------- /dist/debug/momentum.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davideperozzi/momentum-js/c8ba6dc1e28baea9ca2ccf18f721735d0ce5edcf/dist/debug/momentum.min.js.gz -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var d="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)},f="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this;function h(a){if(a){for(var b=f,c=["String","prototype","repeat"],e=0;ea||1342177279>>=1)b+=b;return e}});function l(a){var b=typeof a;return"object"==b&&null!=a||"function"==b} 4 | function m(a,b){function c(){}c.prototype=b.prototype;a.ya=b.prototype;a.prototype=new c;a.prototype.constructor=a;a.xa=function(a,c,k){for(var e=Array(arguments.length-2),g=2;g=this.O||Math.abs(this.c.y)>=this.O||0!=this.h.x||0!=this.h.y)&&this.da();a=0;for(var b=this.Z.length;a=a.f.G)a.c.x=-1*a.c.x*a.j.x}else{0!=a.h.x?(a.m.x=a.A.x,0!=a.h.x&&(b=a.j.x,a.o&&(b/=2),a.a.x+=D(a.h.x*(1+b)))):a.m.x=0;b=C(a.f.I-a.a.x);var c=C(a.f.G-a.a.x);a.h.x=0c?c:0}if(a.W)if(0<=a.j.y){if(b=a.a,b.y=Math.min(Math.max(b.y,a.f.J),a.f.H),a.a.y<=a.f.J||a.a.y>=a.f.H)a.c.y=-1*a.c.y*a.j.y}else 0!=a.h.y?(a.m.y=a.A.y,0!=a.h.y&&(b=a.j.x,a.o&&(b/=2),a.a.y+=D(a.h.y*(1+b)))):a.m.y=0,b= 19 | C(a.f.J-a.a.y),c=C(a.f.H-a.a.y),a.h.y=0c?c:0}if(a.a.x!=a.C.x||a.a.y!=a.C.y){b=0;for(c=a.X.length;ba.width,c=this.f.height>a.height,this.a.ia(b?this.c.x+a.width-this.f.width:this.b.bounds.x+this.c.x,b?this.c.x:this.b.bounds.x+this.b.bounds.width-(this.f.width-this.c.x),c?this.c.y+a.height-this.f.height:this.b.bounds.y+this.c.y,c?this.c.y:this.b.bounds.y+this.b.bounds.height-(this.f.height-this.c.y)))};F.prototype.updateBounds=F.prototype.R; 26 | F.prototype.S=function(){this.i.x=0;for(var a=this.i.y=0,b=this.j.length;ae.height&&(this.i.y+=c.scrollTop);c.scrollWidth>e.width&&(this.i.x+=c.scrollLeft)}};F.prototype.updateScrollPositions=F.prototype.S;F.prototype.P=function(){return this.a}; 27 | F.prototype.update=function(a){(a=a&&!0===a)||this.ba();this.f=this.g.getBoundingClientRect();this.b.autoAnchor||(this.l.x=void 0!==this.b.anchorX?this.b.anchorX:this.l.x,this.l.y=void 0!==this.b.anchorY?this.b.anchorY:this.l.y,this.c.x=this.f.width*this.l.x,this.c.y=this.f.height*this.l.y);this.h.x=this.g.offsetLeft;this.h.y=this.g.offsetTop;for(var b=[],c=new n,e=this.g.parentElement;e&&e!=this.a.g;){var g=window.getComputedStyle(e).position,k=window.getComputedStyle(e).overflow;if("relative"== 28 | g||"absolute"==g)g=x(e),g.x>c.x&&(c.x=g.x),g.y>c.y&&(c.y=g.y);"auto"!=k&&"scroll"!=k||b.push(e);e=e.parentElement}this.h.x+=c.x;this.h.y+=c.y;c=this.j.slice(0);this.j=[];e=0;for(k=c.length;e=e&&a=g&&ba||1342177279>>=1)b+=b;return e}});var l=this; 4 | function m(a,b){a=a.split(".");var c=l;a[0]in c||"undefined"==typeof c.execScript||c.execScript("var "+a[0]);for(var e;a.length&&(e=a.shift());)a.length||void 0===b?c[e]&&c[e]!==Object.prototype[e]?c=c[e]:c=c[e]={}:c[e]=b}function n(a){var b=typeof a;return"object"==b&&null!=a||"function"==b} 5 | function p(a,b){function c(){}c.prototype=b.prototype;a.ya=b.prototype;a.prototype=new c;a.prototype.constructor=a;a.xa=function(a,c,k){for(var e=Array(arguments.length-2),g=2;g=this.O||Math.abs(this.c.y)>=this.O||0!=this.h.x||0!=this.h.y)&&this.da();a=0;for(var b=this.Z.length;a=a.f.G)a.c.x=-1*a.c.x*a.j.x}else{0!=a.h.x?(a.m.x=a.A.x,0!=a.h.x&&(b=a.j.x,a.o&&(b/=2),a.a.x+=F(a.h.x*(1+b)))):a.m.x=0;b=E(a.f.I-a.a.x);var c=E(a.f.G-a.a.x);a.h.x=0c?c:0}if(a.W)if(0<=a.j.y){if(b=a.a,b.y=Math.min(Math.max(b.y,a.f.J),a.f.H),a.a.y<=a.f.J||a.a.y>=a.f.H)a.c.y=-1*a.c.y*a.j.y}else 0!=a.h.y?(a.m.y=a.A.y,0!=a.h.y&&(b=a.j.x,a.o&&(b/=2),a.a.y+=F(a.h.y*(1+b)))):a.m.y=0,b= 20 | E(a.f.J-a.a.y),c=E(a.f.H-a.a.y),a.h.y=0c?c:0}if(a.a.x!=a.C.x||a.a.y!=a.C.y){b=0;for(c=a.X.length;ba.width,c=this.f.height>a.height,this.a.ia(b?this.c.x+a.width-this.f.width:this.b.bounds.x+this.c.x,b?this.c.x:this.b.bounds.x+this.b.bounds.width-(this.f.width-this.c.x),c?this.c.y+a.height-this.f.height:this.b.bounds.y+this.c.y,c?this.c.y:this.b.bounds.y+this.b.bounds.height-(this.f.height-this.c.y)))};H.prototype.updateBounds=H.prototype.R; 27 | H.prototype.S=function(){this.i.x=0;for(var a=this.i.y=0,b=this.j.length;ae.height&&(this.i.y+=c.scrollTop);c.scrollWidth>e.width&&(this.i.x+=c.scrollLeft)}};H.prototype.updateScrollPositions=H.prototype.S;H.prototype.P=function(){return this.a}; 28 | H.prototype.update=function(a){(a=a&&!0===a)||this.ba();this.f=this.g.getBoundingClientRect();this.b.autoAnchor||(this.l.x=void 0!==this.b.anchorX?this.b.anchorX:this.l.x,this.l.y=void 0!==this.b.anchorY?this.b.anchorY:this.l.y,this.c.x=this.f.width*this.l.x,this.c.y=this.f.height*this.l.y);this.h.x=this.g.offsetLeft;this.h.y=this.g.offsetTop;for(var b=[],c=new q,e=this.g.parentElement;e&&e!=this.a.g;){var g=window.getComputedStyle(e).position,k=window.getComputedStyle(e).overflow;if("relative"== 29 | g||"absolute"==g)g=z(e),g.x>c.x&&(c.x=g.x),g.y>c.y&&(c.y=g.y);"auto"!=k&&"scroll"!=k||b.push(e);e=e.parentElement}this.h.x+=c.x;this.h.y+=c.y;c=this.j.slice(0);this.j=[];e=0;for(k=c.length;e=e&&a=g&&b closure.distCompile(mainBuildConfig)); 60 | gulp.task('js-dist-module-build', () => closure.distCompile(moduleBuildConfig)); 61 | gulp.task('js-dist-debug-build', () => closure.distCompile(debugBuildConfig)); 62 | gulp.task('js-deps-build', () => closure.depsBuild(depsConfig)); 63 | gulp.task('js-deps-watch', ['js-deps-build'], () => { 64 | return closure.depsWatch(depsConfig, () => gulp.start('js-deps-build')) 65 | }); 66 | 67 | // Tasks 68 | gulp.task('compress-js', () => { 69 | gulp.src('./dist/momentum.min.js') 70 | .pipe(gzip()) 71 | .pipe(gulp.dest('./dist/')); 72 | 73 | gulp.src('./dist/debug/momentum.min.js') 74 | .pipe(gzip()) 75 | .pipe(gulp.dest('./dist/debug/')); 76 | }); 77 | 78 | gulp.task('build', gulpSequence( 79 | 'js-dist-main-build', 80 | 'js-dist-module-build', 81 | 'js-dist-debug-build', 82 | 'compress-js' 83 | )); 84 | 85 | gulp.task('server', () => { 86 | return connect.server({ 87 | host: '0.0.0.0', 88 | root: ['./demos', './'], 89 | livereload: true, 90 | port: 8000 91 | }); 92 | }); 93 | 94 | gulp.task('start', () => { 95 | gulp.start('js-deps-watch', 'server'); 96 | }); 97 | 98 | gulp.task('default', ['build']); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "momentum.js", 3 | "version": "1.2.2", 4 | "description": "Add a momentum effect to elements", 5 | "main": "dist/index.js", 6 | "keywords": [ 7 | "javascript", 8 | "momentum", 9 | "draggable", 10 | "throwable", 11 | "throw", 12 | "kinetic" 13 | ], 14 | "homepage": "https://github.com/davideperozzi/momentum-js", 15 | "repository": "https://github.com/davideperozzi/momentum-js", 16 | "contributors": [ 17 | { 18 | "name": "Davide Perozzi", 19 | "email": "info@davide-perozzi.de", 20 | "url": "http://davide-perozzi.de/" 21 | } 22 | ], 23 | "license": "MIT", 24 | "devDependencies": { 25 | "compression": "^1.7.2", 26 | "dat.gui": "^0.7.2", 27 | "dj-gulp-tasks": "^1.1.0", 28 | "google-closure-compiler": "^20180506.0.0", 29 | "google-closure-library": "^20180506.0.0", 30 | "gulp": "^3.9.1", 31 | "gulp-closure-deps": "^0.4.0", 32 | "gulp-connect": "^5.5.0", 33 | "gulp-gzip": "^1.4.2", 34 | "gulp-sequence": "^1.0.0", 35 | "natives": "^1.1.6" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # momentum.js 2 | 3 | momenutm.js is a small (plainly written) JavaScript plugin, giving you the ability to add a "throw momentum effect" to anything displayed on your webpage. It can be a handy alternative to the Draggable extension of the greensock animation framework. Besides desktop devices it also works for mobile and touch devices. 4 | 5 | ## Installation 6 | 7 | To use momentum.js you have to include the momentum.js file from the dist folder. If your server supports serving compressed files you can also put the compressed **gz** file in the same directory as the js file. This improves the loading time by **more than 50%**. 8 | It also comes as a **npm** module: 9 | ```bash 10 | npm install --save momentum.js 11 | ``` 12 | 13 | ## Usage 14 | You can use this package by simply including it like this: 15 | 16 | ```js 17 | import { Draggable, Handler, Rotatable } from 'momentum.js'; 18 | ``` 19 | 20 | ## How to use the Draggable 21 | 22 | The Draggable component allows your elements to be dragged around. 23 | 24 | ```javascript 25 | new momentum.Draggable(element, config); 26 | ``` 27 | 28 | This example shows how to create a basic draggable element: 29 | 30 | ```javascript 31 | new momentum.Draggable(dragElement, { 32 | container: containerElement, 33 | containerBounds: true, 34 | resizeUpdate: true, 35 | autoAnchor: true 36 | }); 37 | ``` 38 | 39 | ### Some Draggable Demos 40 | http://momentum.davide-perozzi.de/ 41 | 42 | ### The Draggable configuration 43 | 44 | | Option | Type | Default | Description | 45 | | ------------- | ------------- | ------------- | ------------- | 46 | | container | Element | null | Container of the draggable element. Also the target for the "drag events" | 47 | | elementBounds | Element|string | null | Determines if the bounds of a element should be used. As a shortcut you can pass 'container' or 'parent' as a string | 48 | | bounds | Object | null | Set the bounds manually {x: number y: number, width: number, height: number} | 49 | | autoAnchor | boolean | false | Determines if the anchor should be set on the start position the user has clicked | 50 | | anchorX | number | 0.5 | The anchor point on the horizontal axis. | 51 | | anchorY | number | 0.5 | The anchor point on the vertical axis. | 52 | | threshold | number| 5 | The minimum velocity the element needs to reach to trigger the throw animation | 53 | | restitution | number | 0 | The bounciness of the element if it hits the bounds. This will be multiplicated with the velocity. You can use negative values to let the element bounce out of the bounds. Numbers **from -1 to 1** are **valid**. 54 | | friction | number | 0.035 | The friction of the element. Lower values make the elements decelerate longer. Numbers **from 0 to 1** are **valid** | 55 | | offsetFriction | number | 0.1 | The friction used out of bounds. This will be included in the calculations if you used a negative restitution. Numbers **from 0 to 1** are **valid** | 56 | | maxVelocity | number | 70 | The maximum velocity the element can reach. Numbers **greater than 0** are **valid**. | 57 | | resizeUpdate | boolean | false | Determines whether the draggable should be updated automatically after the browser is resized. | 58 | | lockAxis | Object | null | Locked axis will be excluded from the translation. For example: {x: false, y: true}. This will lock the y axis. | 59 | | onUp | Function | null | Callback which will be called if the user released the element. | 60 | | onDown | Function | null | Callback which will be called if the user hits the element before the drag. Whether you return true or false determimes if the drag will be accepted. If you want to **preserve the default behaviour** you should **return the "hit" parameter**. Parameter list: hit, cursorX, cursorY, elementX, elementY, elementWidth, elementHeight | 61 | | onMove | Function | null | This will be triggered before the element is going to be moved. At this point the element does **not** have the latest translation. You can return an coordinate object like "{x: number, y: number}" to manipulate the position of the element. Parameter list: posX, posY, velX, velY. | 62 | | onTranslate | Function | null | This will be called if the translation settled. Parameter list: elementX, elementY, elementWidth, elementHeight, elementBounds | 63 | | preventMove | Function | null | A function which needs to return wheter true or false to prevent the movement. This can be useful to add a move threshold. Paramter list: movedX, movedY, isTouchDevice 64 | 65 | ## License 66 | momentum.js is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). 67 | -------------------------------------------------------------------------------- /src/momentum/Coordinate.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum.Coordinate'); 2 | 3 | /** 4 | * @struct 5 | * @constructor 6 | * @param {number=} optX 7 | * @param {number=} optY 8 | */ 9 | momentum.Coordinate = function(optX, optY) { 10 | /** 11 | * @public 12 | * @type {number} 13 | */ 14 | this.x = optX || 0; 15 | 16 | /** 17 | * @public 18 | * @type {number} 19 | */ 20 | this.y = optY || 0; 21 | }; 22 | 23 | /** 24 | * @public 25 | * @return {momentum.Coordinate} 26 | */ 27 | momentum.Coordinate.prototype.clone = function() { 28 | return new momentum.Coordinate(this.x, this.y); 29 | }; 30 | 31 | /** 32 | * @public 33 | */ 34 | momentum.Coordinate.prototype.clamp = function(min, max) { 35 | this.clampX(min, max); 36 | this.clampY(min, max); 37 | }; 38 | 39 | /** 40 | * @public 41 | */ 42 | momentum.Coordinate.prototype.clampX = function(min, max) { 43 | this.x = Math.min(Math.max(this.x, min), max); 44 | }; 45 | 46 | /** 47 | * @public 48 | */ 49 | momentum.Coordinate.prototype.clampY = function(min, max) { 50 | this.y = Math.min(Math.max(this.y, min), max); 51 | }; -------------------------------------------------------------------------------- /src/momentum/Draggable.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum.Draggable'); 2 | 3 | // momentum 4 | goog.require('momentum.Handler'); 5 | goog.require('momentum.HandlerComponent'); 6 | goog.require('momentum.Coordinate'); 7 | goog.require('momentum.utils'); 8 | 9 | /** 10 | * @constructor 11 | * @param {Element} element 12 | * @param {MomentumDraggableConfig} optConfig 13 | * @extends {momentum.HandlerComponent} 14 | */ 15 | momentum.Draggable = function(element, optConfig) { 16 | momentum.Draggable.base(this, 'constructor'); 17 | 18 | /** 19 | * @private 20 | * @type {Element} 21 | */ 22 | this.element_ = element; 23 | 24 | /** 25 | * @public 26 | * @type {MomentumDraggableConfig} 27 | */ 28 | this.defaults_ = { 29 | // @deprecated: containerBounds is deprecated and will be removed in the future. 30 | // Use elementBounds instead 31 | // 32 | // containerBounds: true, 33 | container: document.documentElement, 34 | elementBounds: 'container', 35 | restitution: -0.6, 36 | resizeUpdate: true, 37 | autoAnchor: true 38 | }; 39 | 40 | /** 41 | * @public 42 | * @type {MomentumDraggableConfig} 43 | */ 44 | this.config = optConfig || {}; 45 | 46 | /** 47 | * @private 48 | * @type {ClientRect} 49 | */ 50 | this.elementBounds_ = this.element_.getBoundingClientRect(); 51 | 52 | /** 53 | * @private 54 | * @type {momentum.Coordinate} 55 | */ 56 | this.anchorPoint_ = new momentum.Coordinate(.5, .5); 57 | 58 | /** 59 | * @private 60 | * @type {momentum.Coordinate} 61 | */ 62 | this.lastTranslation_ = new momentum.Coordinate(); 63 | 64 | /** 65 | * @private 66 | * @type {momentum.Coordinate} 67 | */ 68 | this.positionOffset_ = new momentum.Coordinate(); 69 | 70 | /** 71 | * @private 72 | * @type {momentum.Coordinate} 73 | */ 74 | this.scrollOffset_ = new momentum.Coordinate(); 75 | 76 | /** 77 | * @private 78 | * @type {momentum.Coordinate} 79 | */ 80 | this.startPosition_ = new momentum.Coordinate(); 81 | 82 | /** 83 | * @private 84 | * @type {momentum.Handler} 85 | */ 86 | this.handler_ = null; 87 | 88 | /** 89 | * @private 90 | * @type {boolean} 91 | */ 92 | this.destroyed_ = false; 93 | 94 | /** 95 | * @private 96 | * @type {boolean} 97 | */ 98 | this.hasTranslation_ = false; 99 | 100 | /** 101 | * @private 102 | * @type {Array} 103 | */ 104 | this.scrollContainers_ = []; 105 | 106 | /** 107 | * Initialize self 108 | */ 109 | this.init_(); 110 | }; 111 | 112 | goog.inherits( 113 | momentum.Draggable, 114 | momentum.HandlerComponent 115 | ); 116 | 117 | /** 118 | * @export 119 | * @type {MomentumDraggableConfig} 120 | */ 121 | momentum.Draggable.prototype.config = {}; 122 | 123 | /** 124 | * @export 125 | */ 126 | momentum.Draggable.prototype.updateSettings = function() { 127 | // Merge config 128 | var config = this.defaults_; 129 | 130 | momentum.utils.extendObject(config, this.config); 131 | 132 | this.config = config; 133 | 134 | // Notify handler about the new settings 135 | if (this.config.restitution && !isNaN(this.config.restitution)) { 136 | this.handler_.setRestitution(this.config.restitution); 137 | } 138 | 139 | if (this.config.friction && !isNaN(this.config.friction)) { 140 | this.handler_.setFriction(this.config.friction); 141 | } 142 | 143 | if (this.config.offsetFriction && !isNaN(this.config.offsetFriction)) { 144 | this.handler_.setOffsetFriction(this.config.offsetFriction); 145 | } 146 | 147 | if (this.config.threshold && !isNaN(this.config.threshold)) { 148 | this.handler_.setThreshold(this.config.threshold); 149 | } 150 | 151 | if (this.config.maxVelocity && !isNaN(this.config.maxVelocity)) { 152 | this.handler_.setMaxVelocity(this.config.maxVelocity); 153 | } 154 | 155 | if (this.config.preventMove && typeof this.config.preventMove === 'function') { 156 | this.handler_.setPreventMoveCheck(this.config.preventMove); 157 | } 158 | }; 159 | 160 | /** 161 | * @export 162 | * @param {boolean=} optNoCache 163 | */ 164 | momentum.Draggable.prototype.updateBounds = function(optNoCache) { 165 | if (this.config.elementBounds) { 166 | if (optNoCache || ! this.config.bounds) { 167 | if (goog.isString(this.config.elementBounds)) { 168 | switch (this.config.elementBounds) { 169 | case 'parent': 170 | var parentNode = /** @type {Element} */ (this.element_.parentNode); 171 | 172 | if (parentNode != this.element_) { 173 | this.config.elementBounds = parentNode; 174 | } 175 | else { 176 | this.config.elementBounds = this.config.container; 177 | } 178 | break; 179 | case 'container': 180 | this.config.elementBounds = this.config.container || document.documentElement; 181 | break; 182 | } 183 | 184 | if (this.config.elementBounds.nodeType === Node.DOCUMENT_NODE) { 185 | this.config.elementBounds = document.documentElement; 186 | } 187 | } 188 | 189 | if (this.config.elementBounds.nodeType === Node.ELEMENT_NODE) { 190 | // Use the bounds of the element and change the position of the object. 191 | // This ensures the scroll position is included in the calculations 192 | var bounds = this.config.elementBounds.getBoundingClientRect(); 193 | var targetOffset = momentum.utils.getPageOffset(this.handler_.getTarget()); 194 | var elementOffset = momentum.utils.getPageOffset(/** @type {Element} */ ( 195 | this.config.elementBounds 196 | )); 197 | 198 | // Getting relative position to the handler target 199 | elementOffset.x -= targetOffset.x; 200 | elementOffset.y -= targetOffset.y; 201 | 202 | this.config.bounds = { 203 | x: elementOffset.x, 204 | y: elementOffset.y, 205 | width: bounds.width, 206 | height: bounds.height 207 | }; 208 | } 209 | } 210 | } 211 | 212 | if (this.config.bounds) { 213 | var containerBounds = this.handler_.getTargetBounds(optNoCache); 214 | var overflowX = this.elementBounds_.width > containerBounds.width; 215 | var overflowY = this.elementBounds_.height > containerBounds.height; 216 | 217 | // @todo: Make inverting bounds for different bound targets possible 218 | 219 | this.handler_.setBounds( 220 | // min x 221 | overflowX 222 | ? this.positionOffset_.x + containerBounds.width - this.elementBounds_.width 223 | : this.config.bounds.x + this.positionOffset_.x, 224 | 225 | // max x 226 | overflowX 227 | ? this.positionOffset_.x 228 | : (this.config.bounds.x + this.config.bounds.width) - (this.elementBounds_.width - this.positionOffset_.x), 229 | 230 | // min y 231 | overflowY 232 | ? this.positionOffset_.y + containerBounds.height - this.elementBounds_.height 233 | : this.config.bounds.y + this.positionOffset_.y, 234 | 235 | // max y 236 | overflowY 237 | ? this.positionOffset_.y 238 | : (this.config.bounds.y + this.config.bounds.height) - (this.elementBounds_.height - this.positionOffset_.y) 239 | ); 240 | } 241 | }; 242 | 243 | /** 244 | * @export 245 | */ 246 | momentum.Draggable.prototype.updateScrollPositions = function() 247 | { 248 | this.scrollOffset_.x = 0; 249 | this.scrollOffset_.y = 0; 250 | 251 | for (var i = 0, len = this.scrollContainers_.length; i < len; i++) { 252 | var container = this.scrollContainers_[i]; 253 | var bounds = container.getBoundingClientRect(); 254 | 255 | if (container.scrollHeight > bounds.height) { 256 | this.scrollOffset_.y += container.scrollTop; 257 | } 258 | 259 | if (container.scrollWidth > bounds.width) { 260 | this.scrollOffset_.x += container.scrollLeft; 261 | } 262 | } 263 | }; 264 | 265 | /** 266 | * {@inheritDoc} 267 | */ 268 | momentum.Draggable.prototype.getHandler = function() 269 | { 270 | return this.handler_; 271 | }; 272 | 273 | /** 274 | * @export 275 | * @param {boolean=} optPreventHandler 276 | */ 277 | momentum.Draggable.prototype.update = function(optPreventHandler) { 278 | optPreventHandler = optPreventHandler && optPreventHandler === true; 279 | 280 | if ( ! optPreventHandler) { 281 | this.updateSettings(); 282 | } 283 | 284 | // Update element bounds and offsets 285 | this.elementBounds_ = this.element_.getBoundingClientRect(); 286 | 287 | // Update anchor points 288 | if ( ! this.config.autoAnchor) { 289 | this.anchorPoint_.x = goog.isDef(this.config.anchorX) ? this.config.anchorX : this.anchorPoint_.x; 290 | this.anchorPoint_.y = goog.isDef(this.config.anchorY) ? this.config.anchorY : this.anchorPoint_.y; 291 | this.positionOffset_.x = this.elementBounds_.width * this.anchorPoint_.x; 292 | this.positionOffset_.y = this.elementBounds_.height * this.anchorPoint_.y; 293 | } 294 | 295 | // Set start position 296 | this.startPosition_.x = this.element_.offsetLeft; 297 | this.startPosition_.y = this.element_.offsetTop; 298 | 299 | var scrollContainers = []; 300 | var containerOffset = new momentum.Coordinate(); 301 | var parentElement = this.element_.parentElement; 302 | 303 | while (parentElement) { 304 | if (parentElement == this.handler_.getTarget()) { 305 | break; 306 | } 307 | 308 | var position = momentum.utils.getStyle(parentElement, 'position'); 309 | var overflow = momentum.utils.getStyle(parentElement, 'overflow'); 310 | 311 | if (position == 'relative' || position == 'absolute') { 312 | var offset = momentum.utils.getPageOffset(parentElement); 313 | 314 | if (offset.x > containerOffset.x) { 315 | containerOffset.x = offset.x; 316 | } 317 | 318 | if (offset.y > containerOffset.y) { 319 | containerOffset.y = offset.y; 320 | } 321 | } 322 | 323 | if (overflow == 'auto' || overflow == 'scroll') { 324 | scrollContainers.push(parentElement); 325 | } 326 | 327 | parentElement = parentElement.parentElement; 328 | } 329 | 330 | this.startPosition_.x += containerOffset.x; 331 | this.startPosition_.y += containerOffset.y; 332 | 333 | // Remove old scroll containers 334 | var oldContainers = this.scrollContainers_.slice(0); 335 | this.scrollContainers_ = []; 336 | 337 | for (var i = 0, len = oldContainers.length; i < len; i++) { 338 | if (scrollContainers.indexOf(oldContainers[i]) === -1) { 339 | oldContainers[i].removeEventListener('scroll', 340 | this.handleContainerScroll_.bind(this), false); 341 | } else { 342 | this.scrollContainers_.push(oldContainers[i]); 343 | } 344 | } 345 | 346 | // Add new scroll containers 347 | for (var i = 0, len = scrollContainers.length; i < len; i++) { 348 | if (this.scrollContainers_.indexOf(scrollContainers[i]) === -1) { 349 | this.scrollContainers_.push(scrollContainers[i]); 350 | 351 | scrollContainers[i].addEventListener('scroll', 352 | this.handleContainerScroll_.bind(this), false); 353 | } 354 | } 355 | 356 | // Update scroll positions 357 | this.updateScrollPositions(); 358 | 359 | // Update bounds with a refresh 360 | this.updateBounds(true); 361 | 362 | // Update handler 363 | if ( ! optPreventHandler) { 364 | this.handler_.update(); 365 | } 366 | }; 367 | 368 | /** 369 | * @private 370 | */ 371 | momentum.Draggable.prototype.handleContainerScroll_ = function() 372 | { 373 | setTimeout(this.updateScrollPositions.bind(this), 0); 374 | }; 375 | 376 | /** 377 | * @export 378 | */ 379 | momentum.Draggable.prototype.reset = function() { 380 | this.destroy(); 381 | this.restore(); 382 | }; 383 | 384 | /** 385 | * @export 386 | */ 387 | momentum.Draggable.prototype.destroy = function() { 388 | this.destroyed_ = true; 389 | 390 | this.handler_.destroy(); 391 | delete this.handler_; 392 | this.handler_ = null; 393 | 394 | momentum.utils.removeStyle(this.element_, 'transform', true); 395 | }; 396 | 397 | /** 398 | * @export 399 | */ 400 | momentum.Draggable.prototype.restore = function() { 401 | this.destroyed_ = false; 402 | 403 | this.init_(); 404 | this.handlerChanged(); 405 | }; 406 | 407 | /** 408 | * @private 409 | */ 410 | momentum.Draggable.prototype.init_ = function() { 411 | // Setup handler 412 | this.handler_ = new momentum.Handler(this.config.container); 413 | this.handler_.onUp(this.handleUp_, this); 414 | this.handler_.onDown(this.handleDown_, this); 415 | this.handler_.onMove(this.handleMove_, this); 416 | 417 | // Update settings 418 | this.updateSettings(); 419 | 420 | // Set the initial position 421 | this.setInitialPosition_(); 422 | 423 | // Initial update 424 | this.update(); 425 | 426 | // Init handler 427 | this.handler_.init(); 428 | 429 | // Disable translation 430 | this.hasTranslation_ = false; 431 | 432 | // Reset last translation 433 | this.lastTranslation_.x = 0; 434 | this.lastTranslation_.y = 0; 435 | 436 | // Listen for browser events 437 | if (this.config.resizeUpdate) { 438 | window.addEventListener('resize', function(){ 439 | setTimeout(function(){ 440 | this.setInitialPosition_(); 441 | this.update(); 442 | }.bind(this), 0); 443 | }.bind(this), false); 444 | } 445 | 446 | // Scroll elements 447 | window.addEventListener('scroll', function(){ 448 | setTimeout(function(){ 449 | this.updateScrollPositions(); 450 | this.updateBounds(true); 451 | }.bind(this), 0); 452 | }.bind(this), false); 453 | }; 454 | 455 | /** 456 | * @private 457 | */ 458 | momentum.Draggable.prototype.setInitialPosition_ = function() { 459 | var initialPosition = this.handler_.getRelativeElementPosition(this.element_); 460 | 461 | this.handler_.setPosition( 462 | initialPosition.x + this.positionOffset_.x, 463 | initialPosition.y + this.positionOffset_.y, 464 | true 465 | ); 466 | }; 467 | 468 | /** 469 | * @private 470 | * @param {number} x 471 | * @param {number} y 472 | */ 473 | momentum.Draggable.prototype.translate_ = function(x, y) { 474 | if (this.destroyed_) { 475 | return; 476 | } 477 | 478 | x = x - this.positionOffset_.x - this.startPosition_.x + this.scrollOffset_.x; 479 | y = y - this.positionOffset_.y - this.startPosition_.y + this.scrollOffset_.y; 480 | 481 | if (this.config.lockAxis && this.hasTranslation_) { 482 | if (goog.isObject(this.config.lockAxis)) { 483 | if (true == this.config.lockAxis.x) { 484 | x = this.lastTranslation_.x; 485 | } 486 | 487 | if (true == this.config.lockAxis.y) { 488 | y = this.lastTranslation_.y; 489 | } 490 | } 491 | } 492 | 493 | momentum.utils.setTranslation(this.element_, x, y); 494 | 495 | this.lastTranslation_.x = x; 496 | this.lastTranslation_.y = y; 497 | this.hasTranslation_ = true; 498 | 499 | if (this.config.onTranslate) { 500 | this.config.onTranslate( 501 | x, 502 | y, 503 | this.elementBounds_.width, 504 | this.elementBounds_.height, 505 | this.config.bounds 506 | ? this.config.bounds 507 | : this.handler_.getTargetBounds() 508 | ); 509 | } 510 | }; 511 | 512 | /** 513 | * @private 514 | * @param {number} x1 515 | * @param {number} x2 516 | * @param {number} y1 517 | * @param {number} y2 518 | * @param {number} width 519 | * @param {number} height 520 | * @return {boolean} 521 | */ 522 | momentum.Draggable.prototype.hitTest_ = function(x1, y1, x2, y2, width, height) { 523 | return x1 >= x2 && x1 < x2 + width && y1 >= y2 && y1 < y2 + height; 524 | }; 525 | 526 | /** 527 | * @private 528 | * @param {number} x 529 | * @param {number} y 530 | * @return {boolean} 531 | */ 532 | momentum.Draggable.prototype.handleDown_ = function(x, y) { 533 | if (this.destroyed_) { 534 | return false; 535 | } 536 | 537 | var elementPosition = this.handler_.getRelativeElementPosition(this.element_); 538 | var containsPoint = this.hitTest_(x, y, elementPosition.x, elementPosition.y, 539 | this.elementBounds_.width, this.elementBounds_.height); 540 | 541 | if (this.config.autoAnchor && containsPoint) { 542 | this.positionOffset_.x = x - elementPosition.x; 543 | this.positionOffset_.y = y - elementPosition.y; 544 | this.updateBounds(); 545 | } 546 | 547 | if (this.config.onDown) { 548 | var customHit = this.config.onDown(containsPoint, x, y, elementPosition.x, elementPosition.y, 549 | this.elementBounds_.width, this.elementBounds_.height); 550 | 551 | if (goog.isBoolean(customHit)) { 552 | return customHit; 553 | } 554 | } 555 | 556 | return containsPoint; 557 | }; 558 | 559 | /** 560 | * @private 561 | */ 562 | momentum.Draggable.prototype.handleUp_ = function() 563 | { 564 | if (this.destroyed_) { 565 | return; 566 | } 567 | 568 | if (this.config.onUp) { 569 | this.config.onUp(); 570 | } 571 | }; 572 | 573 | /** 574 | * @private 575 | * @param {number} posX 576 | * @param {number} posY 577 | * @param {number} velX 578 | * @param {number} velY 579 | */ 580 | momentum.Draggable.prototype.handleMove_ = function(posX, posY, velX, velY) { 581 | if (this.destroyed_) { 582 | return; 583 | } 584 | 585 | if (this.config.onMove) { 586 | var newPosition = this.config.onMove(posX, posY, velX, velY); 587 | 588 | if (goog.isObject(newPosition)) { 589 | if (newPosition.hasOwnProperty('x')) { 590 | posX = parseFloat(newPosition['x']); 591 | } 592 | 593 | if (newPosition.hasOwnProperty('y')) { 594 | posY = parseFloat(newPosition['y']); 595 | } 596 | } 597 | } 598 | 599 | this.translate_(posX, posY); 600 | }; -------------------------------------------------------------------------------- /src/momentum/Handler.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum.Handler'); 2 | 3 | // momentum 4 | goog.require('momentum.Coordinate'); 5 | goog.require('momentum.TrackingPoint'); 6 | goog.require('momentum.utils'); 7 | 8 | /** 9 | * @constructor 10 | * @param {Element=} optTarget 11 | */ 12 | momentum.Handler = function(optTarget) { 13 | /** 14 | * @private 15 | * @type {Element} 16 | */ 17 | this.target_ = optTarget || document.documentElement; 18 | 19 | /** 20 | * @private 21 | * @type {ClientRect} 22 | */ 23 | this.targetBounds_ = this.target_.getBoundingClientRect(); 24 | 25 | /** 26 | * @private 27 | * @type {momentum.Coordinate} 28 | */ 29 | this.position_ = new momentum.Coordinate(); 30 | 31 | /** 32 | * @private 33 | * @type {Array} 34 | */ 35 | this.trackingPoints_ = []; 36 | 37 | /** 38 | * @private 39 | * @type {momentum.Coordinate} 40 | */ 41 | this.startPosition_ = new momentum.Coordinate(); 42 | 43 | /** 44 | * @private 45 | * @type {momentum.Coordinate} 46 | */ 47 | this.lastDownPosition_ = new momentum.Coordinate(); 48 | 49 | /** 50 | * @private 51 | * @type {momentum.Coordinate} 52 | */ 53 | this.lastPosition_ = new momentum.Coordinate(); 54 | 55 | /** 56 | * @private 57 | * @type {momentum.Coordinate} 58 | */ 59 | this.lastVelocity_ = new momentum.Coordinate(); 60 | 61 | /** 62 | * @private 63 | * @type {Object} 64 | */ 65 | this.boundOverflow_ = new momentum.Coordinate(); 66 | 67 | /** 68 | * @private 69 | * @type {{ 70 | * minX: number, 71 | * maxX: number, 72 | * minY: number, 73 | * maxY: number 74 | * }} 75 | */ 76 | this.bounds_ = { 77 | minX: 0, 78 | maxX: 0, 79 | minY: 0, 80 | maxY: 0 81 | }; 82 | 83 | /** 84 | * @private 85 | * @type {boolean} 86 | */ 87 | this.animationsStopped_ = false; 88 | 89 | /** 90 | * @private 91 | * @type {boolean} 92 | */ 93 | this.hasBounds_ = false; 94 | 95 | /** 96 | * @private 97 | * @type {boolean} 98 | */ 99 | this.hasBoundsX_ = false; 100 | 101 | /** 102 | * @private 103 | * @type {boolean} 104 | */ 105 | this.hasBoundsY_ = false; 106 | 107 | /** 108 | * @private 109 | * @type {boolean} 110 | */ 111 | this.dragging_ = false; 112 | 113 | /** 114 | * @private 115 | * @type {boolean} 116 | */ 117 | this.movedSinceUserDown_ = false; 118 | 119 | /** 120 | * @private 121 | * @type {boolean} 122 | */ 123 | this.allowDecelerating_ = false; 124 | 125 | /** 126 | * @private 127 | * @type {boolean} 128 | */ 129 | this.decelerating_ = false; 130 | 131 | /** 132 | * @private 133 | * @type {number} 134 | */ 135 | this.resetTimerId_ = -1; 136 | 137 | /** 138 | * @private 139 | * @type {number} 140 | */ 141 | this.startTime_ = 0; 142 | 143 | /** 144 | * @private 145 | * @type {Array} 146 | */ 147 | this.moveCallbacks_ = []; 148 | 149 | /** 150 | * @private 151 | * @type {Array} 152 | */ 153 | this.upCallbacks_ = []; 154 | 155 | /** 156 | * @private 157 | * @type {Function} 158 | */ 159 | this.downCallback_ = null; 160 | 161 | /** 162 | * @private 163 | * @type {Function} 164 | */ 165 | this.preventMoveCheck_ = null; 166 | 167 | /** 168 | * @private 169 | * @type {number} 170 | */ 171 | this.precision_ = 3; 172 | 173 | /** 174 | * @private 175 | * @type {momentum.Coordinate} 176 | */ 177 | this.friction_ = new momentum.Coordinate(0.035, 0.035); 178 | 179 | /** 180 | * @private 181 | * @type {momentum.Coordinate} 182 | */ 183 | this.activeOffsetFriction_ = new momentum.Coordinate(0, 0); 184 | 185 | /** 186 | * @private 187 | * @type {momentum.Coordinate} 188 | */ 189 | this.offsetFriction_ = new momentum.Coordinate(0.1, 0.1); 190 | 191 | /** 192 | * @private 193 | * @type {momentum.Coordinate} 194 | */ 195 | this.restitution_ = new momentum.Coordinate(0, 0); 196 | 197 | /** 198 | * @private 199 | * @type {number} 200 | */ 201 | this.threshold_ = 5; 202 | 203 | /** 204 | * @private 205 | * @type {number} 206 | */ 207 | this.resetMs_ = 50; 208 | 209 | /** 210 | * @private 211 | * @type {number} 212 | */ 213 | this.maxVelocity_ = 70; 214 | 215 | /** 216 | * @private 217 | * @type {Object} 218 | */ 219 | this.currentListenerMap_ = {}; 220 | 221 | /** 222 | * @private 223 | * @type {Object} 224 | */ 225 | this.listenerOptions_ = { 226 | 'passive': false, 227 | 'useCapture': false 228 | }; 229 | }; 230 | 231 | /** 232 | * @export 233 | * @param {!Function} callback 234 | * @param {Object=} optCtx 235 | */ 236 | momentum.Handler.prototype.onMove = function(callback, optCtx) { 237 | this.moveCallbacks_.push(callback.bind(optCtx || this)); 238 | }; 239 | 240 | /** 241 | * @export 242 | * @param {!Function} callback 243 | * @param {Object=} optCtx 244 | */ 245 | momentum.Handler.prototype.onDown = function(callback, optCtx) { 246 | this.downCallback_ = callback.bind(optCtx || this); 247 | }; 248 | 249 | 250 | /** 251 | * @export 252 | * @param {!Function} callback 253 | * @param {Object=} optCtx 254 | */ 255 | momentum.Handler.prototype.onUp = function(callback, optCtx) { 256 | this.upCallbacks_.push(callback.bind(optCtx || this)); 257 | }; 258 | 259 | /** 260 | * @export 261 | * @param {number} friction 262 | */ 263 | momentum.Handler.prototype.setFriction = function(friction) { 264 | this.friction_.x = this.friction_.y = friction; 265 | this.friction_.clamp(0, 1); 266 | }; 267 | 268 | /** 269 | * @export 270 | * @param {number} friction 271 | */ 272 | momentum.Handler.prototype.setOffsetFriction = function(friction) { 273 | this.offsetFriction_.x = this.offsetFriction_.y = friction; 274 | this.offsetFriction_.clamp(0, 1); 275 | }; 276 | 277 | /** 278 | * @export 279 | * @param {number} threshold 280 | */ 281 | momentum.Handler.prototype.setThreshold = function(threshold) { 282 | this.threshold_ = threshold; 283 | }; 284 | 285 | /** 286 | * @export 287 | * @param {number} restitution 288 | */ 289 | momentum.Handler.prototype.setRestitution = function(restitution) { 290 | this.restitution_.x = this.restitution_.y = restitution; 291 | this.restitution_.clamp(-1, 1); 292 | }; 293 | 294 | /** 295 | * @export 296 | * @param {number} maxVelocity 297 | */ 298 | momentum.Handler.prototype.setMaxVelocity = function(maxVelocity) { 299 | this.maxVelocity_ = Math.max(maxVelocity, 0); 300 | }; 301 | 302 | /** 303 | * @export 304 | * @param {Function} fnc 305 | */ 306 | momentum.Handler.prototype.setPreventMoveCheck = function(fnc) { 307 | this.preventMoveCheck_ = fnc; 308 | }; 309 | 310 | /** 311 | * @export 312 | * @return {momentum.Coordinate} 313 | */ 314 | momentum.Handler.prototype.getFriction = function() { 315 | return this.friction_; 316 | }; 317 | 318 | /** 319 | * @export 320 | * @return {momentum.Coordinate} 321 | */ 322 | momentum.Handler.prototype.getOffsetFriction = function() { 323 | return this.offsetFriction_; 324 | }; 325 | 326 | /** 327 | * @export 328 | * @return {number} 329 | */ 330 | momentum.Handler.prototype.getThreshold = function() { 331 | return this.threshold_; 332 | }; 333 | 334 | /** 335 | * @export 336 | * @return {momentum.Coordinate} 337 | */ 338 | momentum.Handler.prototype.getRestitution = function() { 339 | return this.restitution_; 340 | }; 341 | 342 | /** 343 | * @export 344 | * @return {number} 345 | */ 346 | momentum.Handler.prototype.getMaxVelocity = function() { 347 | return this.maxVelocity_; 348 | }; 349 | 350 | /** 351 | * @export 352 | * @return {Function} 353 | */ 354 | momentum.Handler.prototype.getPreventMoveCheck = function() { 355 | return this.preventMoveCheck_; 356 | }; 357 | 358 | /** 359 | * @export 360 | * @return {Element} 361 | */ 362 | momentum.Handler.prototype.getTarget = function() { 363 | return this.target_; 364 | }; 365 | 366 | /** 367 | * @export 368 | * @param {boolean=} optUpdate 369 | * @return {ClientRect} 370 | */ 371 | momentum.Handler.prototype.getTargetBounds = function(optUpdate) { 372 | if (optUpdate) { 373 | this.targetBounds_ = this.target_.getBoundingClientRect(); 374 | } 375 | 376 | return this.targetBounds_; 377 | }; 378 | 379 | /** 380 | * @export 381 | * @param {number} x 382 | * @param {number} y 383 | * @param {boolean=} optReset 384 | */ 385 | momentum.Handler.prototype.setPosition = function(x, y, optReset) { 386 | this.position_.x = x; 387 | this.position_.y = y; 388 | 389 | if (optReset) { 390 | this.lastPosition_.x = x; 391 | this.lastPosition_.y = y; 392 | 393 | if (this.decelerating_) { 394 | this.lastVelocity_.x = 0; 395 | this.lastVelocity_.y = 0; 396 | } 397 | } 398 | }; 399 | 400 | /** 401 | * @private 402 | * @return {boolean} 403 | */ 404 | momentum.Handler.prototype.hasTouch_ = function() { 405 | return !!('ontouchstart' in window || navigator.msMaxTouchPoints); 406 | }; 407 | 408 | /** 409 | * @export 410 | * @suppress {checkTypes} 411 | */ 412 | momentum.Handler.prototype.init = function() { 413 | if (this.hasTouch_()) { 414 | this.currentListenerMap_ = { 415 | 'touchend': { 416 | 'target': this.target_, 417 | 'listener': this.handleUserUp_.bind(this) 418 | }, 419 | 'touchcancel': { 420 | 'target': this.target_, 421 | 'listener': this.handleUserUp_.bind(this) 422 | }, 423 | 'touchstart': { 424 | 'target': this.target_, 425 | 'listener': this.handleUserDown_.bind(this) 426 | }, 427 | 'touchmove': { 428 | 'target': this.target_, 429 | 'listener': this.handleUserMove_.bind(this) 430 | } 431 | }; 432 | } else { 433 | this.currentListenerMap_ = { 434 | 'mouseup': { 435 | 'target': this.target_, 436 | 'listener': this.handleUserUp_.bind(this) 437 | }, 438 | 'mouseleave': { 439 | 'target': this.target_, 440 | 'listener': this.handleUserUp_.bind(this) 441 | }, 442 | 'mousedown': { 443 | 'target': this.target_, 444 | 'listener': this.handleUserDown_.bind(this) 445 | }, 446 | 'mousemove': { 447 | 'target': this.target_, 448 | 'listener': this.handleUserMove_.bind(this) 449 | } 450 | }; 451 | } 452 | 453 | this.currentListenerMap_['scroll'] = { 454 | 'target': window, 455 | 'listener': this.update.bind(this) 456 | }; 457 | 458 | this.applyListenerMap_(); 459 | }; 460 | 461 | /** 462 | * @export 463 | */ 464 | momentum.Handler.prototype.destroy = function() 465 | { 466 | this.removeListenerMap_(); 467 | this.stop(); 468 | }; 469 | 470 | /** 471 | * @export 472 | */ 473 | momentum.Handler.prototype.stop = function() 474 | { 475 | this.animationsStopped_ = true; 476 | this.allowDecelerating_ = false; 477 | }; 478 | 479 | /** 480 | * @export 481 | */ 482 | momentum.Handler.prototype.start = function() 483 | { 484 | this.animationsStopped_ = false; 485 | this.allowDecelerating_ = true; 486 | }; 487 | 488 | /** 489 | * @export 490 | * @param {number=} optMinX 491 | * @param {number=} optMaxX 492 | * @param {number=} optMinY 493 | * @param {number=} optMaxY 494 | */ 495 | momentum.Handler.prototype.setBounds = function(optMinX, optMaxX, optMinY, optMaxY) { 496 | this.bounds_.minX = optMinX || 0; 497 | this.bounds_.maxX = optMaxX || 0; 498 | this.bounds_.minY = optMinY || 0; 499 | this.bounds_.maxY = optMaxY || 0; 500 | 501 | this.hasBoundsX_ = this.bounds_.minX != 0 || this.bounds_.maxX != 0; 502 | this.hasBoundsY_ = this.bounds_.minY != 0 || this.bounds_.maxY != 0; 503 | this.hasBounds_ = this.hasBoundsX_ || this.hasBoundsY_; 504 | }; 505 | 506 | /** 507 | * @export 508 | */ 509 | momentum.Handler.prototype.update = function() { 510 | this.targetBounds_ = this.target_.getBoundingClientRect(); 511 | this.positionUpdated_(); 512 | }; 513 | 514 | /** 515 | * @public 516 | * @param {Element} element 517 | * @return {momentum.Coordinate} 518 | */ 519 | momentum.Handler.prototype.getRelativeElementPosition = function(element) { 520 | var bounds = element.getBoundingClientRect(); 521 | 522 | return new momentum.Coordinate( 523 | bounds.left - this.targetBounds_.left, 524 | bounds.top - this.targetBounds_.top 525 | ); 526 | }; 527 | 528 | /** 529 | * @private 530 | */ 531 | momentum.Handler.prototype.applyListenerMap_ = function() 532 | { 533 | for (var type in this.currentListenerMap_) { 534 | var target = this.currentListenerMap_[type]['target']; 535 | 536 | target.addEventListener( 537 | type, 538 | this.currentListenerMap_[type]['listener'], 539 | this.listenerOptions_ 540 | ); 541 | } 542 | }; 543 | 544 | /** 545 | * @private 546 | */ 547 | momentum.Handler.prototype.removeListenerMap_ = function() 548 | { 549 | for (var type in this.currentListenerMap_) { 550 | var target = this.currentListenerMap_[type]['target']; 551 | 552 | target.removeEventListener( 553 | type, 554 | this.currentListenerMap_[type]['listener'], 555 | this.listenerOptions_ 556 | ); 557 | } 558 | 559 | this.currentListenerMap_ = {}; 560 | }; 561 | 562 | /** 563 | * @param {Event} event 564 | * @return {momentum.Coordinate} 565 | */ 566 | momentum.Handler.prototype.getEventPosition_ = function(event) { 567 | var position = new momentum.Coordinate(); 568 | 569 | if (event.touches) { 570 | position.x = event.touches[0].clientX - this.targetBounds_.left; 571 | position.y = event.touches[0].clientY - this.targetBounds_.top; 572 | } else { 573 | position.x = event.clientX - this.targetBounds_.left; 574 | position.y = event.clientY - this.targetBounds_.top; 575 | } 576 | 577 | return position; 578 | }; 579 | 580 | /** 581 | * @private 582 | * @param {Event} event 583 | */ 584 | momentum.Handler.prototype.handleUserDown_ = function(event) { 585 | var position = this.getEventPosition_(event); 586 | 587 | if (this.downCallback_ && !this.downCallback_(position.x, position.y)) { 588 | return; 589 | } 590 | 591 | this.dragging_ = true; 592 | this.allowDecelerating_ = false; 593 | 594 | // Set initial start values 595 | this.lastDownPosition_.x = position.x; 596 | this.lastDownPosition_.y = position.y; 597 | this.startPosition_.x = this.position_.x = position.x; 598 | this.startPosition_.y = this.position_.y = position.y; 599 | this.startTime_ = Date.now(); 600 | 601 | // Reset velocity 602 | this.lastVelocity_.x = 0; 603 | this.lastVelocity_.y = 0; 604 | 605 | this.positionUpdated_(); 606 | this.collectTrackingPoints_(); 607 | }; 608 | 609 | /** 610 | * @private 611 | */ 612 | momentum.Handler.prototype.collectTrackingPoints_ = function() { 613 | this.addTrackingPoint_(); 614 | this.updateTrackingPoints_(); 615 | 616 | if (this.dragging_) { 617 | this.requestAnimationFrame_(this.collectTrackingPoints_, this); 618 | } 619 | }; 620 | 621 | /** 622 | * @private 623 | */ 624 | momentum.Handler.prototype.addTrackingPoint_ = function() { 625 | this.trackingPoints_.push( 626 | new momentum.TrackingPoint(this.position_.clone()) 627 | ); 628 | }; 629 | 630 | /** 631 | * @private 632 | * @param {number} movedX 633 | * @param {number} movedY 634 | * @return {boolean} 635 | */ 636 | momentum.Handler.prototype.preventMove_ = function(movedX, movedY) { 637 | return this.preventMoveCheck_ 638 | ? this.preventMoveCheck_(movedX, movedY, this.hasTouch_()) 639 | : false; 640 | }; 641 | 642 | /** 643 | * @private 644 | * @param {Event} event 645 | */ 646 | momentum.Handler.prototype.handleUserMove_ = function(event) { 647 | if (this.dragging_) { 648 | var position = this.getEventPosition_(event); 649 | var movedX = Math.abs(this.lastDownPosition_.x - position.x); 650 | var movedY = Math.abs(this.lastDownPosition_.y - position.y); 651 | 652 | if ( ! this.preventMove_(movedX, movedY) || this.movedSinceUserDown_) { 653 | this.movedSinceUserDown_ = true; 654 | event.stopPropagation(); 655 | event.preventDefault(); 656 | 657 | this.position_.x = position.x; 658 | this.position_.y = position.y; 659 | 660 | this.positionUpdated_(); 661 | } 662 | } 663 | }; 664 | 665 | /** 666 | * @private 667 | */ 668 | momentum.Handler.prototype.updateTrackingPoints_ = function() { 669 | var timestamp = Date.now(); 670 | var removeIndicies = []; 671 | 672 | for (var i = 0, len = this.trackingPoints_.length; i < len; i++) { 673 | if (timestamp - this.trackingPoints_[i].timestamp >= this.resetMs_) { 674 | removeIndicies.push(i); 675 | } 676 | } 677 | 678 | for (var i = 0, len = removeIndicies.length; i < len; i++) { 679 | this.trackingPoints_.splice(removeIndicies[i], 1); 680 | } 681 | 682 | if (this.trackingPoints_.length > 0) { 683 | var lastTrackingPoint = this.trackingPoints_[0]; 684 | 685 | this.startPosition_ = lastTrackingPoint.position; 686 | this.startTime_ = lastTrackingPoint.timestamp; 687 | } 688 | }; 689 | 690 | /** 691 | * @private 692 | * @param {Event} event 693 | */ 694 | momentum.Handler.prototype.handleUserUp_ = function(event) { 695 | this.lastDownPosition_.x = 0; 696 | this.lastDownPosition_.y = 0; 697 | this.movedSinceUserDown_ = false; 698 | 699 | if (this.dragging_) { 700 | this.dragging_ = false; 701 | this.allowDecelerating_ = true; 702 | 703 | // Calculate the velocity the object reached before the user 704 | // released the trigger. Depending on the start time. 705 | var timeDelta = (Date.now() - this.startTime_) / 15; 706 | 707 | this.lastVelocity_.x = (this.position_.x - this.startPosition_.x) / timeDelta; 708 | this.lastVelocity_.y = (this.position_.y - this.startPosition_.y) / timeDelta; 709 | 710 | // Clamp velocities to the max value 711 | this.lastVelocity_.clamp(-this.maxVelocity_, this.maxVelocity_); 712 | 713 | // Clear the start proeprties, so they won't mess up any 714 | // further calculations 715 | this.clearStartProperties_(); 716 | 717 | // Clear previous tracking points. At this point all calculations which 718 | // are including the tracking points should be already made. 719 | this.trackingPoints_ = []; 720 | 721 | // Check if the velocity is greater than the threshold to enable 722 | // the decelerating from the calculated velocity 723 | if (Math.abs(this.lastVelocity_.x) >= this.threshold_ || 724 | Math.abs(this.lastVelocity_.y) >= this.threshold_ || 725 | this.boundOverflow_.x != 0 || 726 | this.boundOverflow_.y != 0) { 727 | this.decelerate_(); 728 | } 729 | 730 | // Call up callbacks 731 | for (var i = 0, len = this.upCallbacks_.length; i < len; i++) { 732 | this.upCallbacks_[i](); 733 | } 734 | } 735 | }; 736 | 737 | /** 738 | * @private 739 | */ 740 | momentum.Handler.prototype.clearStartProperties_ = function() { 741 | this.startPosition_.x = 0; 742 | this.startPosition_.y = 0; 743 | this.startTime_ = 0; 744 | }; 745 | 746 | /** 747 | * @private 748 | * @param {number} num 749 | * @param {number} dec 750 | * @return {number} 751 | */ 752 | momentum.Handler.prototype.roundDecimal_ = function(num, dec) { 753 | var desc = parseInt(1 + '0'.repeat(dec), 10); 754 | 755 | return Math.round(num * desc) / desc; 756 | }; 757 | 758 | /** 759 | * @private 760 | */ 761 | momentum.Handler.prototype.applyBounds_ = function() { 762 | if (this.hasBounds_) { 763 | if (this.hasBoundsX_) { 764 | if (this.restitution_.x >= 0) { 765 | this.position_.clampX(this.bounds_.minX, this.bounds_.maxX); 766 | 767 | // Handle bounce by inverting the velocity for each axis 768 | if (this.position_.x <= this.bounds_.minX || 769 | this.position_.x >= this.bounds_.maxX) { 770 | this.lastVelocity_.x = (this.lastVelocity_.x * -1) * this.restitution_.x; 771 | } 772 | } 773 | else { 774 | if (this.boundOverflow_.x != 0) { 775 | this.activeOffsetFriction_.x = this.offsetFriction_.x; 776 | this.deflowBoundX_(); 777 | } 778 | else { 779 | this.activeOffsetFriction_.x = 0; 780 | } 781 | 782 | var boundDiffMinX = this.roundDecimal_(this.bounds_.minX - this.position_.x, 2); 783 | var boundDiffMaxX = this.roundDecimal_(this.bounds_.maxX - this.position_.x, 2); 784 | 785 | if (boundDiffMinX > 0) { 786 | this.boundOverflow_.x = boundDiffMinX; 787 | } 788 | else if (boundDiffMaxX < 0) { 789 | this.boundOverflow_.x = boundDiffMaxX; 790 | } 791 | else { 792 | this.boundOverflow_.x = 0; 793 | } 794 | } 795 | } 796 | 797 | if (this.hasBoundsY_) { 798 | if (this.restitution_.y >= 0) { 799 | this.position_.clampY(this.bounds_.minY, this.bounds_.maxY); 800 | 801 | // Handle bounce by inverting the velocity for each axis 802 | if (this.position_.y <= this.bounds_.minY || 803 | this.position_.y >= this.bounds_.maxY) { 804 | this.lastVelocity_.y = (this.lastVelocity_.y * -1) * this.restitution_.y; 805 | } 806 | } 807 | else { 808 | if (this.boundOverflow_.y != 0) { 809 | this.activeOffsetFriction_.y = this.offsetFriction_.y; 810 | this.deflowBoundY_(); 811 | } 812 | else { 813 | this.activeOffsetFriction_.y = 0; 814 | } 815 | 816 | var boundDiffMinY = this.roundDecimal_(this.bounds_.minY - this.position_.y, 2); 817 | var boundDiffMaxY = this.roundDecimal_(this.bounds_.maxY - this.position_.y, 2); 818 | 819 | if (boundDiffMinY > 0) { 820 | this.boundOverflow_.y = boundDiffMinY; 821 | } 822 | else if (boundDiffMaxY < 0) { 823 | this.boundOverflow_.y = boundDiffMaxY; 824 | } 825 | else { 826 | this.boundOverflow_.y = 0; 827 | } 828 | } 829 | } 830 | } 831 | }; 832 | 833 | /** 834 | * @private 835 | */ 836 | momentum.Handler.prototype.positionUpdated_ = function() { 837 | this.applyBounds_(); 838 | 839 | if (this.position_.x != this.lastPosition_.x || this.position_.y != this.lastPosition_.y) { 840 | for (var i = 0, len = this.moveCallbacks_.length; i < len; i++) { 841 | this.moveCallbacks_[i]( 842 | this.position_.x, 843 | this.position_.y, 844 | this.lastVelocity_.x, 845 | this.lastVelocity_.y 846 | ); 847 | } 848 | 849 | this.lastPosition_.x = this.position_.x; 850 | this.lastPosition_.y = this.position_.y; 851 | } 852 | }; 853 | 854 | /** 855 | * @param {number} num 856 | * @param {number} precision 857 | * @return {number} 858 | */ 859 | momentum.Handler.prototype.getPrecisionNumber_ = function(num, precision) { 860 | var number = num.toString(); 861 | return parseFloat(number.substring(0, number.indexOf('.') + (1 + precision))) || 0; 862 | }; 863 | 864 | /** 865 | * @private 866 | */ 867 | momentum.Handler.prototype.deflowBoundX_ = function() { 868 | if (this.boundOverflow_.x != 0) { 869 | var restitution = this.restitution_.x; 870 | 871 | if (this.dragging_) { 872 | restitution /= 2; 873 | } 874 | 875 | this.position_.x += this.getPrecisionNumber_(this.boundOverflow_.x * (1 + restitution), this.precision_); 876 | } 877 | }; 878 | 879 | /** 880 | * @private 881 | */ 882 | momentum.Handler.prototype.deflowBoundY_ = function() { 883 | if (this.boundOverflow_.y != 0) { 884 | var restitution = this.restitution_.x; 885 | 886 | if (this.dragging_) { 887 | restitution /= 2; 888 | } 889 | 890 | this.position_.y += this.getPrecisionNumber_(this.boundOverflow_.y * (1 + restitution), this.precision_); 891 | } 892 | } 893 | 894 | /** 895 | * @private 896 | */ 897 | momentum.Handler.prototype.decelerate_ = function() { 898 | if (!this.allowDecelerating_) { 899 | return; 900 | } 901 | 902 | this.decelerating_ = true; 903 | 904 | if (Math.abs(this.lastVelocity_.x) > 0) { 905 | var friction = this.activeOffsetFriction_.x > 0 ? this.activeOffsetFriction_.x : this.friction_.x; 906 | this.lastVelocity_.x = this.getPrecisionNumber_(this.lastVelocity_.x * (1 - friction), this.precision_); 907 | this.position_.x += this.lastVelocity_.x; 908 | this.position_.x = parseFloat(this.position_.x.toFixed(this.precision_)); 909 | } 910 | 911 | if (Math.abs(this.lastVelocity_.y) > 0) { 912 | var friction = this.activeOffsetFriction_.y > 0 ? this.activeOffsetFriction_.y : this.friction_.y; 913 | this.lastVelocity_.y = this.getPrecisionNumber_(this.lastVelocity_.y * (1 - friction), this.precision_); 914 | this.position_.y += this.lastVelocity_.y; 915 | this.position_.y = parseFloat(this.position_.y.toFixed(this.precision_)); 916 | } 917 | 918 | // Clamp velocity in case it changed during deceleration 919 | this.lastVelocity_.clamp(-this.maxVelocity_, this.maxVelocity_); 920 | 921 | // Notify listeners and apply bounds 922 | this.positionUpdated_(); 923 | 924 | // Round velocity, so it gets canceled. This is done solarge decimal numbers 925 | // will be sorted out. The user wouldn't even notice translation with decimal 926 | // digits this large. 927 | var velX = this.roundDecimal_(Math.abs(this.lastVelocity_.x), 2); 928 | var velY = this.roundDecimal_(Math.abs(this.lastVelocity_.y), 2); 929 | 930 | if (velX > 0 || velX > 0 || this.boundOverflow_.x != 0 || this.boundOverflow_.y != 0) { 931 | this.requestAnimationFrame_(this.decelerate_, this); 932 | } 933 | else { 934 | this.decelerating_ = false; 935 | } 936 | }; 937 | 938 | /** 939 | * @param {Function} callback 940 | * @param {Object} ctx 941 | * @private 942 | */ 943 | momentum.Handler.prototype.requestAnimationFrame_ = function(callback, ctx) { 944 | if ( ! this.animationsStopped_) { 945 | (window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame)(callback.bind(ctx)); 946 | } 947 | }; -------------------------------------------------------------------------------- /src/momentum/HandlerComponent.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum.HandlerComponent'); 2 | 3 | /** 4 | * @constructor 5 | */ 6 | momentum.HandlerComponent = function() 7 | { 8 | /** 9 | * @private 10 | * @type {Array} 11 | */ 12 | this.handlerChangeCallbacks_ = []; 13 | }; 14 | 15 | /** 16 | * @export 17 | * @return {momentum.Handler} 18 | */ 19 | momentum.HandlerComponent.prototype.getHandler = function() 20 | { 21 | return goog.abstractMethod(); 22 | }; 23 | 24 | /** 25 | * @export 26 | * @param {Function} callback 27 | */ 28 | momentum.HandlerComponent.prototype.onHandlerChange = function(callback) 29 | { 30 | this.handlerChangeCallbacks_.push(callback); 31 | }; 32 | 33 | /** 34 | * @protected 35 | */ 36 | momentum.HandlerComponent.prototype.handlerChanged = function() 37 | { 38 | for (var i = 0, len = this.handlerChangeCallbacks_.length; i < len; i++) { 39 | this.handlerChangeCallbacks_[i](this.getHandler()); 40 | } 41 | }; -------------------------------------------------------------------------------- /src/momentum/Rotatable.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum.Rotatable'); 2 | 3 | // momentum 4 | goog.require('momentum.Handler'); 5 | goog.require('momentum.utils'); 6 | goog.require('momentum.HandlerComponent'); 7 | 8 | /** 9 | * @constructor 10 | * @param {Element} element 11 | * @extends {momentum.HandlerComponent} 12 | */ 13 | momentum.Rotatable = function(element) { 14 | /** 15 | * @private 16 | * @type {Element} 17 | */ 18 | this.element_ = element; 19 | 20 | /** 21 | * @private 22 | * @type {ClientRect} 23 | */ 24 | this.elementBounds_ = this.element_.getBoundingClientRect(); 25 | 26 | /** 27 | * @private 28 | * @type {momentum.Handler} 29 | */ 30 | this.handler_ = null; 31 | 32 | /** 33 | * @private 34 | * @type {number} 35 | */ 36 | this.lastRotation_ = 0; 37 | 38 | /** 39 | * @private 40 | * @type {number} 41 | */ 42 | this.startRotation_ = 0; 43 | 44 | /** 45 | * Init rotatable 46 | */ 47 | this.init_(); 48 | }; 49 | 50 | goog.inherits( 51 | momentum.Rotatable, 52 | momentum.HandlerComponent 53 | ); 54 | 55 | /** 56 | * @private 57 | */ 58 | momentum.Rotatable.prototype.init_ = function() { 59 | this.handler_ = new momentum.Handler(this.element_.parentElement); 60 | this.handler_.onDown(this.handleDown_, this); 61 | this.handler_.onMove(this.handleMove_, this); 62 | this.handler_.init(); 63 | }; 64 | 65 | /** 66 | * @private 67 | * @param {number} x 68 | * @param {number} y 69 | * @param {number} r 70 | * @return {number} 71 | */ 72 | momentum.Rotatable.prototype.getRotationDegree_ = function(x, y, r) { 73 | return 180 - Math.atan2(x - r, y - r) * (180 / Math.PI); 74 | }; 75 | 76 | /** 77 | * @private 78 | * @param {number} x 79 | * @param {number} y 80 | * @return {boolean} 81 | */ 82 | momentum.Rotatable.prototype.handleDown_ = function(x, y) { 83 | var rotation = this.getRotationDegree_(x, y, this.elementBounds_.width / 2); 84 | 85 | this.startRotation_ = rotation - this.lastRotation_; 86 | 87 | return true; 88 | }; 89 | 90 | /** 91 | * @private 92 | * @param {number} x 93 | * @param {number} y 94 | */ 95 | momentum.Rotatable.prototype.handleMove_ = function(x, y) { 96 | var rotation = this.getRotationDegree_(x, y, this.elementBounds_.width / 2); 97 | 98 | this.lastRotation_ = rotation - this.startRotation_; 99 | this.lastRotation_ %= 360; 100 | 101 | momentum.utils.setStyle(this.element_, 'transform', 'rotate(' + this.lastRotation_ + 'deg)'); 102 | }; 103 | 104 | /** 105 | * {@inheritDoc} 106 | */ 107 | momentum.Rotatable.prototype.getHandler = function() { 108 | return this.handler_; 109 | }; -------------------------------------------------------------------------------- /src/momentum/TrackingPoint.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum.TrackingPoint'); 2 | 3 | // momentum 4 | goog.require('momentum.Coordinate'); 5 | 6 | /** 7 | * @struct 8 | * @constructor 9 | * @param {momentum.Coordinate} position 10 | */ 11 | momentum.TrackingPoint = function(position) { 12 | /** 13 | * @public 14 | * @type {momentum.Coordinate} 15 | */ 16 | this.position = position; 17 | 18 | /** 19 | * @public 20 | * @type {number} 21 | */ 22 | this.timestamp = Date.now(); 23 | }; -------------------------------------------------------------------------------- /src/momentum/index.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum'); 2 | 3 | // momentum 4 | goog.require('momentum.Draggable'); 5 | goog.require('momentum.Rotatable'); 6 | goog.require('momentum.Handler'); 7 | 8 | // Export usable classes 9 | goog.exportSymbol('momentum.Handler', momentum.Handler); 10 | goog.exportSymbol('momentum.Draggable', momentum.Draggable); 11 | goog.exportSymbol('momentum.Rotatable', momentum.Rotatable); -------------------------------------------------------------------------------- /src/momentum/index.m.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum.module'); 2 | 3 | goog.require('momentum.Draggable'); 4 | goog.require('momentum.Rotatable'); 5 | goog.require('momentum.Handler'); 6 | 7 | module.exports = { 8 | 'Draggable': momentum.Draggable, 9 | 'Rotatable': momentum.Rotatable, 10 | 'Handler': momentum.Handler 11 | }; 12 | -------------------------------------------------------------------------------- /src/momentum/utils.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentum.utils'); 2 | 3 | // momentum 4 | goog.require('momentum.Coordinate'); 5 | 6 | /** 7 | * @private 8 | * @type {string} 9 | */ 10 | momentum.utils.cachedVendor_ = ''; 11 | 12 | /** 13 | * @private 14 | * @type {Object} 15 | */ 16 | momentum.utils.cachedVendorProps_ = {}; 17 | 18 | /** 19 | * @param {string=} optProp 20 | * @return {string} 21 | */ 22 | momentum.utils.getVendor = function(optProp) { 23 | var property = ''; 24 | var prefix = ''; 25 | 26 | if (optProp && momentum.utils.cachedVendorProps_.hasOwnProperty(optProp)) { 27 | return momentum.utils.cachedVendorProps_[optProp]; 28 | } 29 | 30 | if (momentum.utils.cachedVendor_ != '') { 31 | prefix = momentum.utils.cachedVendor_; 32 | } 33 | else { 34 | var styles = window.getComputedStyle(document.documentElement, ''); 35 | prefix = momentum.utils.cachedVendor_ = (Array.prototype.slice.call(styles).join('').match(/-(moz|webkit|ms)-/) || 36 | (styles.OLink && styles.OLink === '' && ['', 'o']))[1]; 37 | } 38 | 39 | var vendorPrefix = prefix[0].toUpperCase() + prefix.substr(1); 40 | 41 | if (prefix.length > 0 && optProp) { 42 | property = optProp[0].toUpperCase() + optProp.substr(1); 43 | momentum.utils.cachedVendorProps_[optProp] = vendorPrefix + property; 44 | } 45 | 46 | return vendorPrefix + property; 47 | }; 48 | 49 | /** 50 | * @param {Element} element 51 | * @param {string} style 52 | * @param {boolean=} optVendorize 53 | * @return {string} 54 | */ 55 | momentum.utils.getStyle = function(element, style, optVendorize) { 56 | return window.getComputedStyle(element)[optVendorize ? momentum.utils.getVendor(style) : style]; 57 | }; 58 | 59 | /** 60 | * @param {Element} element 61 | * @param {string} property 62 | * @param {string} value 63 | * @param {boolean=} optVendorize 64 | */ 65 | momentum.utils.setStyle = function(element, property, value, optVendorize) { 66 | element.style[property] = value; 67 | 68 | if (optVendorize) { 69 | element.style[momentum.utils.getVendor(property)] = value; 70 | } 71 | }; 72 | 73 | /** 74 | * @public 75 | * @param {Element} element 76 | * @param {string} property 77 | * @param {boolean=} optVendorize 78 | */ 79 | momentum.utils.removeStyle = function(element, property, optVendorize) 80 | { 81 | element.style[property] = ''; 82 | 83 | if (optVendorize) { 84 | element.style[momentum.utils.getVendor(property)] = ''; 85 | } 86 | }; 87 | 88 | /** 89 | * @param {Element} element 90 | * @param {number} x 91 | * @param {number} y 92 | */ 93 | momentum.utils.setTranslation = function(element, x, y) { 94 | momentum.utils.setStyle(element, 'transform', 'translate3d(' + x + 'px,' + y + 'px,0)', true); 95 | }; 96 | 97 | /** 98 | * @public 99 | * @see {goog.object.extend} 100 | * @param {Object} target 101 | * @param {...Object} var_args 102 | */ 103 | momentum.utils.extendObject = function(target, var_args) { 104 | var key; 105 | var source; 106 | var prorotypeFields = [ 107 | 'constructor', 108 | 'hasOwnProperty', 109 | 'isPrototypeOf', 110 | 'propertyIsEnumerable', 111 | 'toLocaleString', 112 | 'toString', 113 | 'valueOf' 114 | ]; 115 | 116 | for (var i = 1; i < arguments.length; i++) { 117 | source = arguments[i]; 118 | 119 | for (key in source) { 120 | target[key] = source[key]; 121 | } 122 | 123 | for (var j = 0; j < prorotypeFields.length; j++) { 124 | key = prorotypeFields[j]; 125 | 126 | if (Object.prototype.hasOwnProperty.call(source, key)) { 127 | target[key] = source[key]; 128 | } 129 | } 130 | } 131 | }; 132 | 133 | /** 134 | * @public 135 | * @see {goog.dom.getAncestor} 136 | * @param {Node} element 137 | * @param {function(Node) : boolean} matcher 138 | * @param {boolean=} opt_includeNode 139 | * @param {number=} opt_maxSearchSteps 140 | * @return {Node} 141 | */ 142 | momentum.utils.getAncestor = function(element, matcher, opt_includeNode, opt_maxSearchSteps) { 143 | if (element && ! opt_includeNode) { 144 | element = element.parentNode; 145 | } 146 | 147 | var steps = 0; 148 | 149 | while (element && (opt_maxSearchSteps == null || steps <= opt_maxSearchSteps)) { 150 | if (matcher(element)) { 151 | return element; 152 | } 153 | 154 | element = element.parentNode; 155 | steps++; 156 | } 157 | 158 | return null; 159 | }; 160 | 161 | /** 162 | * @public 163 | * @see {goog.dom.getAncestorByTagNameAndClass} 164 | * @param {Node} element 165 | * @param {string=} opt_tag 166 | * @return {?R} 167 | * @template T 168 | * @template R := cond(isUnknown(T), 'Element', T) =: 169 | */ 170 | momentum.utils.getAncestorByTagName = function(element, opt_tag) { 171 | if ( ! opt_tag) { 172 | return null; 173 | } 174 | 175 | var tagName = opt_tag ? String(opt_tag).toUpperCase() : null; 176 | 177 | return /** @type {Element} */ (momentum.utils.getAncestor(element, function(node) { 178 | return !tagName || node.nodeName == tagName; 179 | }, true)); 180 | }; 181 | 182 | /** 183 | * @public 184 | * @see {goog.dom.getOwnerDocument} 185 | * @param {Node|Window} node 186 | * @return {!Document} 187 | */ 188 | momentum.utils.getOwnerDocument = function(node) { 189 | return /** @type {!Document} */ ( 190 | node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument || node.document); 191 | }; 192 | 193 | /** 194 | * @public 195 | * @see {goog.dom.getDocumentScroll} 196 | * @return {momentum.Coordinate} 197 | */ 198 | momentum.utils.getDocumentScroll = function() { 199 | var element = document.scrollingElement || document.documentElement; 200 | 201 | return new momentum.Coordinate( 202 | window.pageXOffset || element.scrollLeft, 203 | window.pageYOffset || element.scrollTop 204 | ); 205 | }; 206 | 207 | /** 208 | * @public 209 | * @see {gogo.style.getPageOffset} 210 | * @param {Element} element 211 | * @return {momentum.Coordinate} 212 | */ 213 | momentum.utils.getPageOffset = function(element) { 214 | var ownerDoc = momentum.utils.getOwnerDocument(element); 215 | var offsetPosition = new momentum.Coordinate(); 216 | var boundingRect = element.getBoundingClientRect(); 217 | var scrollPosition = momentum.utils.getDocumentScroll(); 218 | 219 | offsetPosition.x = boundingRect.left + scrollPosition.x; 220 | offsetPosition.y = boundingRect.top + scrollPosition.y; 221 | 222 | return offsetPosition; 223 | }; -------------------------------------------------------------------------------- /src/momentumdebug/Configurator.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentumdebug.Configurator'); 2 | 3 | // momentum 4 | goog.require('momentum.Draggable'); 5 | goog.require('momentum.utils'); 6 | 7 | // momentumdebug 8 | goog.require('momentumdebug.ScriptLoader'); 9 | 10 | /** 11 | * @private 12 | * @constructor 13 | * @param {momentum.Handler} handler 14 | */ 15 | momentumdebug.HandlerInterface_ = function(handler) 16 | { 17 | /** 18 | * @public 19 | * @type {number} 20 | */ 21 | this['friction'] = handler.getFriction().x; 22 | 23 | /** 24 | * @public 25 | * @type {number} 26 | */ 27 | this['restitution'] = handler.getRestitution().x; 28 | 29 | /** 30 | * @public 31 | * @type {number} 32 | */ 33 | this['offsetFriction'] = handler.getOffsetFriction().x; 34 | 35 | /** 36 | * @public 37 | * @type {number} 38 | */ 39 | this['maxVelocity'] = handler.getMaxVelocity(); 40 | 41 | /** 42 | * @public 43 | * @type {number} 44 | */ 45 | this['threshold'] = handler.getThreshold(); 46 | 47 | /** 48 | * @public 49 | * @type {number} 50 | */ 51 | this['velocityX'] = 0; 52 | 53 | /** 54 | * @public 55 | * @type {number} 56 | */ 57 | this['velocityY'] = 0; 58 | }; 59 | 60 | /** 61 | * @private 62 | * @constructor 63 | * @param {momentum.Draggable} draggable 64 | */ 65 | momentumdebug.DraggableInterface_ = function(draggable) 66 | { 67 | /** 68 | * @public 69 | * @type {boolean} 70 | */ 71 | this['autoAnchor'] = !!draggable.config.autoAnchor; 72 | 73 | /** 74 | * @public 75 | * @type {number} 76 | */ 77 | this['anchorX'] = draggable.config.anchorX || 0.5; 78 | 79 | /** 80 | * @public 81 | * @type {number} 82 | */ 83 | this['anchorY'] = draggable.config.anchorY || 0.5; 84 | 85 | /** 86 | * @public 87 | * @type {boolean} 88 | */ 89 | this['lockAxisX'] = draggable.config.lockAxis ? !!draggable.config.lockAxis.x : false; 90 | 91 | /** 92 | * @public 93 | * @type {boolean} 94 | */ 95 | this['lockAxisY'] = draggable.config.lockAxis ? !!draggable.config.lockAxis.y : false; 96 | 97 | /** 98 | * @public 99 | * @type {Function} 100 | */ 101 | this['reset'] = function(){}; 102 | }; 103 | 104 | /** 105 | * @static 106 | * @type {momentumdebug.ScriptLoader} 107 | */ 108 | momentumdebug.datGuiLoader = momentumdebug.ScriptLoader.create( 109 | 'datgui', 'https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.0/dat.gui.min.js' 110 | ); 111 | 112 | /** 113 | * @constructor 114 | * @param {momentum.HandlerComponent} component 115 | * @param {Element=} optParentElement 116 | */ 117 | momentumdebug.Configurator = function(component, optParentElement) 118 | { 119 | if ( ! (component instanceof momentum.HandlerComponent)) { 120 | throw new Error('Invalid handler component given'); 121 | } 122 | 123 | /** 124 | * @private 125 | * @type {momentum.HandlerComponent} 126 | */ 127 | this.component_ = component; 128 | 129 | /** 130 | * @private 131 | * @type {Element} 132 | */ 133 | this.parent_ = optParentElement || null; 134 | 135 | /** 136 | * @private 137 | * @type {dat.GUI} 138 | */ 139 | this.datGui_ = null; 140 | 141 | /** 142 | * @private 143 | * @type {Array} 144 | */ 145 | this.folders_ = []; 146 | 147 | // dat.GUI checks and loading 148 | this.load_(); 149 | }; 150 | 151 | /** 152 | * @private 153 | */ 154 | momentumdebug.Configurator.prototype.load_ = function() 155 | { 156 | if (momentumdebug.datGuiLoader.loaded) { 157 | this.init_(); 158 | } 159 | else if ( 160 | ! window.hasOwnProperty('dat') || ( 161 | window.hasOwnProperty('dat') && ! goog.isDefAndNotNull(dat.GUI)) 162 | ) { 163 | momentumdebug.datGuiLoader.load().onComplete(this.init_, this); 164 | } 165 | else { 166 | this.init_(); 167 | } 168 | }; 169 | 170 | /** 171 | * @private 172 | */ 173 | momentumdebug.Configurator.prototype.init_ = function() 174 | { 175 | // Init dat.GUI 176 | this.datGui_ = new dat.GUI({ autoPlace: !!!this.parent_ }); 177 | 178 | // Place element 179 | if (this.parent_) { 180 | this.parent_.appendChild(this.datGui_.domElement); 181 | } 182 | 183 | // Detect handler change 184 | this.component_.onHandlerChange(this.handleHandlerChange_.bind(this)); 185 | 186 | // Initial field rendering 187 | this.renderFields_(); 188 | }; 189 | 190 | /** 191 | * @export 192 | */ 193 | momentumdebug.Configurator.prototype.close = function() 194 | { 195 | this.datGui_.close(); 196 | }; 197 | 198 | /** 199 | * @export 200 | */ 201 | momentumdebug.Configurator.prototype.open = function() 202 | { 203 | this.datGui_.open(); 204 | }; 205 | 206 | 207 | /** 208 | * @export 209 | */ 210 | momentumdebug.Configurator.prototype.closeFolders = function() 211 | { 212 | for (var i = 0, len = this.folders_.length; i < len; i++) { 213 | this.folders_[i].close(); 214 | } 215 | }; 216 | 217 | /** 218 | * @export 219 | */ 220 | momentumdebug.Configurator.prototype.openFolders = function() 221 | { 222 | for (var i = 0, len = this.folders_.length; i < len; i++) { 223 | this.folders_[i].open(); 224 | } 225 | }; 226 | 227 | /** 228 | * @export 229 | * @param {string} name 230 | * @return {boolean} 231 | */ 232 | momentumdebug.Configurator.prototype.closeFolder = function(name) 233 | { 234 | for (var i = 0, len = this.folders_.length; i < len; i++) { 235 | if (this.folders_[i].name == name) { 236 | this.folders_[i].close(); 237 | return true; 238 | } 239 | } 240 | 241 | return false; 242 | }; 243 | 244 | /** 245 | * @export 246 | * @param {string} name 247 | * @return {boolean} 248 | */ 249 | momentumdebug.Configurator.prototype.openFolder = function(name) 250 | { 251 | for (var i = 0, len = this.folders_.length; i < len; i++) { 252 | if (this.folders_[i].name == name) { 253 | this.folders_[i].open(); 254 | return true; 255 | } 256 | } 257 | 258 | return false; 259 | }; 260 | 261 | /** 262 | * @private 263 | */ 264 | momentumdebug.Configurator.prototype.handleHandlerChange_ = function() 265 | { 266 | // Remove all registered folders 267 | for (var i = 0, len = this.folders_.length; i < len; i++) { 268 | this.datGui_.removeFolder(/** @type {dat.GUI.Folder} */ (this.folders_[i])); 269 | } 270 | 271 | this.folders_ = []; 272 | 273 | // Re-render all fields 274 | this.renderFields_(); 275 | }; 276 | 277 | /** 278 | * @private 279 | */ 280 | momentumdebug.Configurator.prototype.renderFields_ = function() 281 | { 282 | // Set handler fields 283 | this.setHandlerFields_(this.component_.getHandler()); 284 | 285 | // Set dragable fields 286 | if (this.component_ instanceof momentum.Draggable) { 287 | this.setDraggableFields_(this.component_); 288 | } 289 | }; 290 | 291 | /** 292 | * @private 293 | * @param {momentum.Handler} handler 294 | */ 295 | momentumdebug.Configurator.prototype.setHandlerFields_ = function(handler) 296 | { 297 | var controlInterface = new momentumdebug.HandlerInterface_(handler); 298 | var folder = this.datGui_.addFolder('Dynamics'); 299 | 300 | // Friction 301 | folder.add(controlInterface, 'friction', 0, 1).onFinishChange(function(value){ 302 | handler.setFriction(value); 303 | }.bind(this)); 304 | 305 | // Restitution + Offset friction 306 | var offsetFrictionElement = null; 307 | 308 | folder.add(controlInterface, 'restitution', -1, 1).onFinishChange(function(value){ 309 | handler.setRestitution(value); 310 | 311 | if (value < 0 && offsetFrictionElement.style.display == 'none') { 312 | offsetFrictionElement.style.display = 'block'; 313 | } 314 | else if (value >= 0 && offsetFrictionElement.style.display == 'block') { 315 | offsetFrictionElement.style.display = 'none'; 316 | } 317 | }.bind(this)); 318 | 319 | var offsetFrictionItem = folder.add(controlInterface, 'offsetFriction', 0, 1).onFinishChange(function(value){ 320 | handler.setOffsetFriction(value); 321 | }.bind(this)).name('offset friction'); 322 | 323 | offsetFrictionElement = momentum.utils.getAncestorByTagName(offsetFrictionItem.domElement, 'li'); 324 | 325 | if (controlInterface['restitution'] >= 0) { 326 | offsetFrictionElement.style.display = 'none'; 327 | } 328 | 329 | // Max velocity 330 | var maxVelocityItem = folder.add(controlInterface, 'maxVelocity', 0, 100); 331 | 332 | // Threshold 333 | folder.add(controlInterface, 'threshold', 0).onFinishChange(function(value){ 334 | handler.setThreshold(value); 335 | }); 336 | 337 | // Velocity info 338 | var maxVelocity = controlInterface['maxVelocity']; 339 | var velItemX = folder.add(controlInterface, 'velocityX', -maxVelocity, maxVelocity).step(.05).name('X-Velocity').listen(); 340 | var velItemY = folder.add(controlInterface, 'velocityY', -maxVelocity, maxVelocity).step(.05).name('Y-Velocity').listen(); 341 | 342 | maxVelocityItem.onFinishChange(function(value){ 343 | velItemX.min(-value).max(value); 344 | velItemY.min(-value).max(value); 345 | 346 | handler.setMaxVelocity(value); 347 | }); 348 | 349 | handler.onMove(function(posX, posY, velX, velY){ 350 | controlInterface['velocityX'] = velX; 351 | controlInterface['velocityY'] = velY; 352 | }); 353 | 354 | // Open folder on default 355 | folder.open(); 356 | 357 | // Add folder to registry 358 | this.folders_.push(folder); 359 | }; 360 | 361 | /** 362 | * @private 363 | * @param {momentum.Draggable} draggable 364 | */ 365 | momentumdebug.Configurator.prototype.setDraggableFields_ = function(draggable) 366 | { 367 | var controlInterface = new momentumdebug.DraggableInterface_(draggable); 368 | var folder = this.datGui_.addFolder('Draggable'); 369 | 370 | // Auto anchor + anchor 371 | var anchorElementX = null; 372 | var anchorElementY = null; 373 | 374 | folder.add(controlInterface, 'autoAnchor').onFinishChange(function(value){ 375 | draggable.config.autoAnchor = value; 376 | 377 | if (anchorElementX && anchorElementY){ 378 | anchorElementX.style.display = value ? 'none' : ''; 379 | anchorElementY.style.display = value ? 'none' : ''; 380 | } 381 | 382 | if ( ! value && ! goog.isDef(draggable.config.anchorX)) { 383 | draggable.config.anchorX = controlInterface['anchorX']; 384 | } 385 | 386 | if ( ! value && ! goog.isDef(draggable.config.anchorY)) { 387 | draggable.config.anchorY = controlInterface['anchorY']; 388 | } 389 | 390 | draggable.update(true); 391 | }); 392 | 393 | var anchorItemX = folder.add(controlInterface, 'anchorX', 0, 1).onFinishChange(function(value){ 394 | draggable.config.anchorX = value; 395 | draggable.update(true); 396 | }); 397 | 398 | var anchorItemY = folder.add(controlInterface, 'anchorY', 0, 1).onFinishChange(function(value){ 399 | draggable.config.anchorY = value; 400 | draggable.update(true); 401 | }) 402 | 403 | anchorElementX = momentum.utils.getAncestorByTagName(anchorItemX.domElement, 'li'); 404 | anchorElementY = momentum.utils.getAncestorByTagName(anchorItemY.domElement, 'li'); 405 | 406 | if (!!draggable.config.autoAnchor) { 407 | anchorElementX.style.display = 'none'; 408 | anchorElementY.style.display = 'none'; 409 | } 410 | 411 | // Lock axis 412 | folder.add(controlInterface, 'lockAxisX').onFinishChange(function(value){ 413 | draggable.config.lockAxis = { 414 | x: value, 415 | y: controlInterface['lockAxisY'] 416 | }; 417 | 418 | draggable.update(true); 419 | }); 420 | 421 | folder.add(controlInterface, 'lockAxisY').onFinishChange(function(value){ 422 | draggable.config.lockAxis = { 423 | x: controlInterface['lockAxisX'], 424 | y: value, 425 | }; 426 | 427 | draggable.update(true); 428 | }); 429 | 430 | // Reset 431 | folder.add(controlInterface, 'reset').name('Reset draggable').onFinishChange(function(){ 432 | draggable.reset(); 433 | }); 434 | 435 | // Add folder to registry 436 | this.folders_.push(folder); 437 | }; -------------------------------------------------------------------------------- /src/momentumdebug/ScriptLoader.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentumdebug.ScriptLoader'); 2 | 3 | /** 4 | * @constructor 5 | * @param {string} id 6 | * @param {string=} optSrc 7 | */ 8 | momentumdebug.ScriptLoader = function(id, optSrc) 9 | { 10 | /** 11 | * @public 12 | * @type {string} 13 | */ 14 | this.id = id; 15 | 16 | /** 17 | * @public 18 | * @type {string} 19 | */ 20 | this.src = optSrc || ''; 21 | 22 | /** 23 | * @public 24 | * @type {boolean} 25 | */ 26 | this.loaded = false; 27 | 28 | /** 29 | * @public 30 | * @type {boolean} 31 | */ 32 | this.loading = false; 33 | 34 | /** 35 | * @private 36 | * @type {Array} 37 | */ 38 | this.completeCallbacks_ = []; 39 | 40 | /** 41 | * @private 42 | * @type {Element} 43 | */ 44 | this.element_ = document.createElement('script'); 45 | 46 | /** 47 | * @private 48 | * @type {Element} 49 | */ 50 | this.target_ = document.body; 51 | }; 52 | 53 | /** 54 | * @public 55 | * @return {momentumdebug.ScriptLoader} 56 | */ 57 | momentumdebug.ScriptLoader.prototype.load = function() 58 | { 59 | if ( ! this.loading && ! this.loaded) { 60 | this.loading = true; 61 | this.element_.onload = this.handleComplete_.bind(this); 62 | this.element_.src = this.src; 63 | 64 | this.target_.appendChild(this.element_); 65 | } 66 | 67 | return this; 68 | }; 69 | 70 | /** 71 | * @private 72 | */ 73 | momentumdebug.ScriptLoader.prototype.handleComplete_ = function() 74 | { 75 | this.loaded = true; 76 | 77 | for (var i = 0, len = this.completeCallbacks_.length; i < len; i++) { 78 | this.completeCallbacks_[i](); 79 | } 80 | }; 81 | 82 | /** 83 | * @public 84 | * @param {Function} callback 85 | * @param {Object=} optCtx 86 | */ 87 | momentumdebug.ScriptLoader.prototype.onComplete = function(callback, optCtx) 88 | { 89 | if (optCtx) { 90 | callback = goog.bind(callback, optCtx); 91 | } 92 | 93 | this.completeCallbacks_.push(callback); 94 | }; 95 | 96 | /** 97 | * @type {Object} 98 | */ 99 | momentumdebug.ScriptLoader.Instances = {}; 100 | 101 | /** 102 | * @public 103 | * @param {string} id 104 | * @param {string=} optSrc 105 | * @return {momentumdebug.ScriptLoader} 106 | */ 107 | momentumdebug.ScriptLoader.create = function(id, optSrc) 108 | { 109 | if ( ! momentumdebug.ScriptLoader.Instances.hasOwnProperty(id)) { 110 | momentumdebug.ScriptLoader.Instances[id] = new momentumdebug.ScriptLoader(id, optSrc); 111 | } 112 | 113 | return momentumdebug.ScriptLoader.Instances[id]; 114 | }; -------------------------------------------------------------------------------- /src/momentumdebug/index.js: -------------------------------------------------------------------------------- 1 | goog.provide('momentumdebug'); 2 | 3 | // momentum 4 | goog.require('momentum'); 5 | 6 | // momentumdebug 7 | goog.require('momentumdebug.Configurator'); 8 | 9 | // Export usable classes 10 | goog.exportSymbol('momentumdebug.Configurator', momentumdebug.Configurator); --------------------------------------------------------------------------------