Source: movableCoord.js

  1. /**
  2. * Copyright (c) 2015 NAVER Corp.
  3. * egjs projects are licensed under the MIT license
  4. */
  5. // jscs:disable maximumLineLength
  6. eg.module("movableCoord", [eg, window, "Hammer"], function(ns, global, HM) {
  7. "use strict";
  8. var SUPPORT_TOUCH = "ontouchstart" in global;
  9. var assignFn = HM.assign || HM.merge;
  10. // jscs:enable maximumLineLength
  11. /**
  12. * A module used to change the information of user action entered by various input devices such as touch screen or mouse into logical coordinates within the virtual coordinate system. The coordinate information sorted by time events occurred is provided if animations are made by user actions. You can implement a user interface by applying the logical coordinates provided. For more information on the eg.MovableCoord module, see demos.
  13. * @group egjs
  14. * @ko 터치 입력 장치나 마우스와 같은 다양한 입력 장치로 전달 받은 사용자의 동작을 가상 좌표계의 논리적 좌표로 변경하는 모듈. 사용자의 동작으로 애니메이션이 일어나면 시간순으로 변경되는 좌표 정보도 제공한다. 변경된 논리적 좌표를 반영해 UI를 구현할 수 있다. eg.MovableCoord 모듈의 자세한 작동 방식은 데모를 참고한다.
  15. * @class
  16. * @name eg.MovableCoord
  17. * @extends eg.Component
  18. *
  19. * @param {Object} options The option object of the eg.MovableCoord module<ko>eg.MovableCoord 모듈의 옵션 객체</ko>
  20. * @param {Array} options.min The minimum value of X and Y coordinates <ko>좌표계의 최솟값</ko>
  21. * @param {Number} [options.min.0=0] The X coordinate of the minimum <ko>최소 x좌표</ko>
  22. * @param {Number} [options.min.1=0] The Y coordinate of the minimum <ko>최소 y좌표</ko>
  23. *
  24. * @param {Array} options.max The maximum value of X and Y coordinates <ko>좌표계의 최댓값</ko>
  25. * @param {Number} [options.max.0=100] The X coordinate of the maximum<ko>최대 x좌표</ko>
  26. * @param {Number} [options.max.1=100] The Y coordinate of the maximum<ko>최대 y좌표</ko>
  27. *
  28. * @param {Array} options.bounce The size of bouncing area. The coordinates can exceed the coordinate area as much as the bouncing area based on user action. If the coordinates does not exceed the bouncing area when an element is dragged, the coordinates where bouncing effects are applied are retuned back into the coordinate area<ko>바운스 영역의 크기. 사용자의 동작에 따라 좌표가 좌표 영역을 넘어 바운스 영역의 크기만큼 더 이동할 수 있다. 사용자가 끌어다 놓는 동작을 했을 때 좌표가 바운스 영역에 있으면, 바운스 효과가 적용된 좌표가 다시 좌표 영역 안으로 들어온다</ko>
  29. * @param {Boolean} [options.bounce.0=10] The size of top area <ko>위쪽 바운스 영역의 크기</ko>
  30. * @param {Boolean} [options.bounce.1=10] The size of right area <ko>오른쪽 바운스 영역의 크기</ko>
  31. * @param {Boolean} [options.bounce.2=10] The size of bottom area <ko>아래쪽 바운스 영역의 크기</ko>
  32. * @param {Boolean} [options.bounce.3=10] The size of left area <ko>왼쪽 바운스 영역의 크기</ko>
  33. *
  34. * @param {Array} options.margin The size of accessible space outside the coordinate area. If an element is dragged outside the coordinate area and then dropped, the coordinates of the element are returned back into the coordinate area. The size of margins that can be exceeded <ko>− 좌표 영역을 넘어 이동할 수 있는 바깥 영역의 크기. 사용자가 좌표를 바깥 영역까지 끌었다가 놓으면 좌표가 좌표 영역 안으로 들어온다.</ko>
  35. * @param {Boolean} [options.margin.0=0] The size of top margin <ko>위쪽 바깥 영역의 크기</ko>
  36. * @param {Boolean} [options.margin.1=0] The size of right margin <ko>오른쪽 바깥 영역의 크기</ko>
  37. * @param {Boolean} [options.margin.2=0] The size of bottom margin <ko>아래쪽 바깥 영역의 크기</ko>
  38. * @param {Boolean} [options.margin.3=0] The size of left margin <ko>왼쪽 바깥 영역의 크기</ko>
  39. * @param {Array} options.circular Indicates whether a circular element is available. If it is set to "true" and an element is dragged outside the coordinate area, the element will appear on the other side.<ko>순환 여부. 'true'로 설정한 방향의 좌표 영역 밖으로 엘리먼트가 이동하면 반대 방향에서 엘리먼트가 나타난다</ko>
  40. * @param {Boolean} [options.circular.0=false] Indicates whether to circulate to top <ko>위로 순환 여부</ko>
  41. * @param {Boolean} [options.circular.1=false] Indicates whether to circulate to right <ko>오른쪽으로 순환 여부</ko>
  42. * @param {Boolean} [options.circular.2=false] Indicates whether to circulate to bottom <ko>아래로 순환 여부</ko>
  43. * @param {Boolean} [options.circular.3=false] Indicates whether to circulate to left <ko>왼쪽으로 순환 여부</ko>
  44. *
  45. * @param {Function} [options.easing=easing.easeOutCubic] The easing function to apply to an animation <ko>애니메이션에 적용할 easing 함수</ko>
  46. * @param {Number} [options.maximumDuration=Infinity] Maximum duration of the animation <ko>가속도에 의해 애니메이션이 동작할 때의 최대 좌표 이동 시간</ko>
  47. * @param {Number} [options.deceleration=0.0006] Deceleration of the animation where acceleration is manually enabled by user. A higher value indicates shorter running time. <ko>사용자의 동작으로 가속도가 적용된 애니메이션의 감속도. 값이 높을수록 애니메이션 실행 시간이 짧아진다</ko>
  48. * @see HammerJS {@link http://hammerjs.github.io}
  49. * @see • Hammer.JS applies specific CSS properties by default when creating an instance (See {@link http://hammerjs.github.io/jsdoc/Hammer.defaults.cssProps.html}). The eg.MovableCoord module removes all default CSS properties provided by Hammer.JS <ko>Hammer.JS는 인스턴스를 생성할 때 기본으로 특정 CSS 속성을 적용한다(참고: @link{http://hammerjs.github.io/jsdoc/Hammer.defaults.cssProps.html}). 특정한 상황에서는 Hammer.JS의 속성 때문에 사용성에 문제가 있을 수 있다. eg.MovableCoord 모듈은 Hammer.JS의 기본 CSS 속성을 모두 제거했다</ko>
  50. *
  51. * @codepen {"id":"jPPqeR", "ko":"MovableCoord Cube 예제", "en":"MovableCoord Cube example", "collectionId":"AKpkGW", "height": 403}
  52. *
  53. * @see Easing Functions Cheat Sheet {@link http://easings.net/}
  54. * @see If you want to try a different easing function, use the jQuery easing plugin ({@link http://gsgd.co.uk/sandbox/jquery/easing}) or the jQuery UI easing library ({@link https://jqueryui.com/easing}) <ko>다른 easing 함수를 사용하려면 jQuery easing 플러그인({@link http://gsgd.co.uk/sandbox/jquery/easing})이나, jQuery UI easing 라이브러리({@lin https://jqueryui.com/easing})를 사용한다</ko>
  55. *
  56. * @support {"ie": "10+", "ch" : "latest", "ff" : "latest", "sf" : "latest", "edge" : "latest", "ios" : "7+", "an" : "2.3+ (except 3.x)"}
  57. */
  58. var MC = ns.MovableCoord = ns.Class.extend(ns.Component, {
  59. construct: function(options) {
  60. assignFn(this.options = {
  61. min: [0, 0],
  62. max: [100, 100],
  63. bounce: [10, 10, 10, 10],
  64. margin: [0,0,0,0],
  65. circular: [false, false, false, false],
  66. easing: function easeOutCubic(x) {
  67. return 1 - Math.pow(1 - x, 3);
  68. },
  69. maximumDuration: Infinity,
  70. deceleration: 0.0006
  71. }, options);
  72. this._reviseOptions();
  73. this._status = {
  74. grabOutside: false, // check whether user's action started on outside
  75. curHammer: null, // current hammer instance
  76. moveDistance: null, // a position of the first user's action
  77. animationParam: null, // animation information
  78. prevented: false // check whether the animation event was prevented
  79. };
  80. this._hammers = {};
  81. this._pos = this.options.min.concat();
  82. this._subOptions = {};
  83. this._raf = null;
  84. this._animationEnd = HM.bindFn(this._animationEnd, this); // for caching
  85. this._restore = HM.bindFn(this._restore, this); // for caching
  86. this._panmove = HM.bindFn(this._panmove, this); // for caching
  87. this._panend = HM.bindFn(this._panend, this); // for caching
  88. },
  89. /**
  90. * Registers an element to use the eg.MovableCoord module.
  91. * @ko eg.MovableCoord 모듈을 사용할 엘리먼트를 등록한다
  92. * @method eg.MovableCoord#bind
  93. * @param {HTMLElement|String|jQuery} element An element to use the eg.MovableCoord module<ko>− eg.MovableCoord 모듈을 사용할 엘리먼트</ko>
  94. * @param {Object} options The option object of the bind() method <ko>bind() 메서드의 옵션 객체</ko>
  95. * @param {Number} [options.direction=eg.MovableCoord.DIRECTION_ALL] Coordinate direction that a user can move<br>- eg.MovableCoord.DIRECTION_ALL: All directions available.<br>- eg.MovableCoord.DIRECTION_HORIZONTAL: Horizontal direction only.<br>- eg.MovableCoord.DIRECTION_VERTICAL: Vertical direction only<ko>사용자의 동작으로 움직일 수 있는 좌표의 방향.<br>- eg.MovableCoord.DIRECTION_ALL: 모든 방향으로 움직일 수 있다.<br>- eg.MovableCoord.DIRECTION_HORIZONTAL: 가로 방향으로만 움직일 수 있다.<br>- eg.MovableCoord.DIRECTION_VERTICAL: 세로 방향으로만 움직일 수 있다.</ko>
  96. * @param {Array} options.scale Coordinate scale that a user can move<ko>사용자의 동작으로 이동하는 좌표의 배율</ko>
  97. * @param {Number} [options.scale.0=1] X-axis scale <ko>x축 배율</ko>
  98. * @param {Number} [options.scale.1=1] Y-axis scale <ko>y축 배율</ko>
  99. * @param {Number} [options.thresholdAngle=45] The threshold value that determines whether user action is horizontal or vertical (0~90) <ko>사용자의 동작이 가로 방향인지 세로 방향인지 판단하는 기준 각도(0~90)</ko>
  100. * @param {Number} [options.interruptable=true] Indicates whether an animation is interruptible.<br>- true: It can be paused or stopped by user action or the API.<br>- false: It cannot be paused or stopped by user action or the API while it is running.<ko>진행 중인 애니메이션 중지 가능 여부.<br>- true: 사용자의 동작이나 API로 애니메이션을 중지할 수 있다.<br>- false: 애니메이션이 진행 중일 때는 사용자의 동작이나 API가 적용되지 않는다</ko>
  101. * @param {Array} [options.inputType] Types of input devices. (default: ["touch", "mouse"])<br>- touch: Touch screen<br>- mouse: Mouse <ko>입력 장치 종류.(기본값: ["touch", "mouse"])<br>- touch: 터치 입력 장치<br>- mouse: 마우스</ko>
  102. *
  103. * @return {eg.MovableCoord} An instance of a module itself <ko>모듈 자신의 인스턴스</ko>
  104. */
  105. bind: function(element, options) {
  106. var el = this._getEl(element);
  107. var keyValue = el[MC._KEY];
  108. var subOptions = {
  109. direction: MC.DIRECTION_ALL,
  110. scale: [ 1, 1 ],
  111. thresholdAngle: 45,
  112. interruptable: true,
  113. inputType: [ "touch", "mouse" ]
  114. };
  115. assignFn(subOptions, options);
  116. var inputClass = this._convertInputType(subOptions.inputType);
  117. if (!inputClass) {
  118. return this;
  119. }
  120. if (keyValue) {
  121. this._hammers[keyValue].inst.destroy();
  122. } else {
  123. keyValue = Math.round(Math.random() * new Date().getTime());
  124. }
  125. this._hammers[keyValue] = {
  126. inst: this._createHammer(
  127. el,
  128. subOptions,
  129. inputClass
  130. ),
  131. el: el,
  132. options: subOptions
  133. };
  134. el[MC._KEY] = keyValue;
  135. return this;
  136. },
  137. _createHammer: function(el, subOptions, inputClass) {
  138. try {
  139. // create Hammer
  140. var hammer = new HM.Manager(el, {
  141. recognizers: [
  142. [
  143. HM.Pan, {
  144. direction: subOptions.direction,
  145. threshold: 0
  146. }
  147. ]
  148. ],
  149. // css properties were removed due to usablility issue
  150. // http://hammerjs.github.io/jsdoc/Hammer.defaults.cssProps.html
  151. cssProps: {
  152. userSelect: "none",
  153. touchSelect: "none",
  154. touchCallout: "none",
  155. userDrag: "none"
  156. },
  157. inputClass: inputClass
  158. });
  159. return this._attachHammerEvents(hammer, subOptions);
  160. } catch (e) {}
  161. },
  162. _attachHammerEvents: function(hammer, options) {
  163. return hammer.on("hammer.input", HM.bindFn(function(e) {
  164. var enable = hammer.get("pan").options.enable;
  165. if (e.isFirst) {
  166. // apply options each
  167. this._subOptions = options;
  168. this._status.curHammer = hammer;
  169. enable && this._panstart(e);
  170. } else if (e.isFinal) {
  171. // substitute .on("panend tap", this._panend); Because it(tap, panend) cannot catch vertical(horizontal) movement on HORIZONTAL(VERTICAL) mode.
  172. enable && this._panend(e);
  173. }
  174. }, this))
  175. .on("panstart panmove", this._panmove);
  176. },
  177. _detachHammerEvents: function(hammer) {
  178. hammer.off("hammer.input panstart panmove panend");
  179. },
  180. _convertInputType: function(inputType) {
  181. var hasTouch = false;
  182. var hasMouse = false;
  183. inputType = inputType || [];
  184. inputType.forEach(function(v) {
  185. switch (v) {
  186. case "mouse" : hasMouse = true; break;
  187. case "touch" : hasTouch = SUPPORT_TOUCH;
  188. }
  189. });
  190. return hasTouch && HM.TouchInput || hasMouse && HM.MouseInput || null;
  191. },
  192. /**
  193. * Detaches an element using the eg.MovableCoord module.
  194. * @ko eg.MovableCoord 모듈을 사용하는 엘리먼트를 해제한다
  195. * @method eg.MovableCoord#unbind
  196. * @param {HTMLElement|String|jQuery} element An element from which the eg.MovableCoord module is detached<ko>eg.MovableCoord 모듈을 해제할 엘리먼트</ko>
  197. * @return {eg.MovableCoord} An instance of a module itself<ko>모듈 자신의 인스턴스</ko>
  198. */
  199. unbind: function(element) {
  200. var el = this._getEl(element);
  201. var key = el[MC._KEY];
  202. if (key) {
  203. this._hammers[key].inst.destroy();
  204. delete this._hammers[key];
  205. delete el[MC._KEY];
  206. }
  207. return this;
  208. },
  209. /**
  210. * get a hammer instance from elements using the eg.MovableCoord module.
  211. * @ko eg.MovableCoord 모듈을 사용하는 엘리먼트에서 hammer 객체를 얻는다
  212. * @method eg.MovableCoord#getHammer
  213. * @param {HTMLElement|String|jQuery} element An element from which the eg.MovableCoord module is using<ko>eg.MovableCoord 모듈을 사용하는 엘리먼트</ko>
  214. * @return {Hammer|null} An instance of Hammer.JS<ko>Hammer.JS의 인스턴스</ko>
  215. */
  216. getHammer: function(element) {
  217. var el = this._getEl(element);
  218. var key = el ? el[MC._KEY] : null;
  219. if (key && this._hammers[key]) {
  220. return this._hammers[key].inst;
  221. } else {
  222. return null;
  223. }
  224. },
  225. _grab: function() {
  226. if (this._status.animationParam) {
  227. this.trigger("animationEnd");
  228. var pos = this._getCircularPos(this._pos);
  229. if (pos[0] !== this._pos[0] || pos[1] !== this._pos[1]) {
  230. this._pos = pos;
  231. this._triggerChange(this._pos, true);
  232. }
  233. this._status.animationParam = null;
  234. this._raf && ns.cancelAnimationFrame(this._raf);
  235. this._raf = null;
  236. }
  237. },
  238. _getCircularPos: function(pos, min, max, circular) {
  239. min = min || this.options.min;
  240. max = max || this.options.max;
  241. circular = circular || this.options.circular;
  242. if (circular[0] && pos[1] < min[1]) { // up
  243. pos[1] = (pos[1] - min[1]) % (max[1] - min[1] + 1) + max[1];
  244. }
  245. if (circular[1] && pos[0] > max[0]) { // right
  246. pos[0] = (pos[0] - min[0]) % (max[0] - min[0] + 1) + min[0];
  247. }
  248. if (circular[2] && pos[1] > max[1]) { // down
  249. pos[1] = (pos[1] - min[1]) % (max[1] - min[1] + 1) + min[1];
  250. }
  251. if (circular[3] && pos[0] < min[0]) { // left
  252. pos[0] = (pos[0] - min[0]) % (max[0] - min[0] + 1) + max[0];
  253. }
  254. pos[0] = +pos[0].toFixed(5), pos[1] = +pos[1].toFixed(5);
  255. return pos;
  256. },
  257. // determine outside
  258. _isOutside: function(pos, min, max) {
  259. return pos[0] < min[0] || pos[1] < min[1] ||
  260. pos[0] > max[0] || pos[1] > max[1];
  261. },
  262. // from outside to outside
  263. _isOutToOut: function(pos, destPos) {
  264. var min = this.options.min;
  265. var max = this.options.max;
  266. return (pos[0] < min[0] || pos[0] > max[0] ||
  267. pos[1] < min[1] || pos[1] > max[1]) &&
  268. (destPos[0] < min[0] || destPos[0] > max[0] ||
  269. destPos[1] < min[1] || destPos[1] > max[1]);
  270. },
  271. // panstart event handler
  272. _panstart: function(e) {
  273. if (!this._subOptions.interruptable && this._status.prevented) {
  274. return;
  275. }
  276. this._setInterrupt(true);
  277. var pos = this._pos;
  278. this._grab();
  279. /**
  280. * This event is fired when a user holds an element on the screen of the device.
  281. * @ko 사용자가 기기의 화면에 손을 대고 있을 때 발생하는 이벤트
  282. * @name eg.MovableCoord#hold
  283. * @event
  284. * @param {Object} param The object of data to be sent when the event is fired<ko>이벤트가 발생할 때 전달되는 데이터 객체</ko>
  285. * @param {Array} param.pos coordinate <ko>좌표 정보</ko>
  286. * @param {Number} param.pos.0 The X coordinate<ko>x 좌표</ko>
  287. * @param {Number} param.pos.1 The Y coordinate<ko>y 좌표</ko>
  288. * @param {Object} param.hammerEvent The event information of Hammer.JS. It returns null if the event is fired through a call to the setTo() or setBy() method.<ko>Hammer.JS의 이벤트 정보. setTo() 메서드나 setBy() 메서드를 호출해 이벤트가 발생했을 때는 'null'을 반환한다.</ko>
  289. *
  290. */
  291. this.trigger("hold", {
  292. pos: pos.concat(),
  293. hammerEvent: e
  294. });
  295. this._status.moveDistance = pos.concat();
  296. this._status.grabOutside = this._isOutside(
  297. pos,
  298. this.options.min,
  299. this.options.max
  300. );
  301. },
  302. // panmove event handler
  303. _panmove: function(e) {
  304. if (!this._isInterrupting() || !this._status.moveDistance) {
  305. return;
  306. }
  307. var tv;
  308. var tn;
  309. var tx;
  310. var pos = this._pos;
  311. var min = this.options.min;
  312. var max = this.options.max;
  313. var bounce = this.options.bounce;
  314. var margin = this.options.margin;
  315. var direction = this._subOptions.direction;
  316. var scale = this._subOptions.scale;
  317. var userDirection = this._getDirection(e.angle);
  318. var out = [
  319. margin[0] + bounce[0],
  320. margin[1] + bounce[1],
  321. margin[2] + bounce[2],
  322. margin[3] + bounce[3]
  323. ];
  324. var prevent = false;
  325. // not support offset properties in Hammerjs - start
  326. var prevInput = this._status.curHammer.session.prevInput;
  327. if (prevInput) {
  328. e.offsetX = e.deltaX - prevInput.deltaX;
  329. e.offsetY = e.deltaY - prevInput.deltaY;
  330. } else {
  331. e.offsetX = e.offsetY = 0;
  332. }
  333. // not support offset properties in Hammerjs - end
  334. if (direction === MC.DIRECTION_ALL ||
  335. (direction & MC.DIRECTION_HORIZONTAL &&
  336. userDirection & MC.DIRECTION_HORIZONTAL)
  337. ) {
  338. this._status.moveDistance[0] += (e.offsetX * scale[0]);
  339. prevent = true;
  340. }
  341. if (direction === MC.DIRECTION_ALL ||
  342. (direction & MC.DIRECTION_VERTICAL &&
  343. userDirection & MC.DIRECTION_VERTICAL)
  344. ) {
  345. this._status.moveDistance[1] += (e.offsetY * scale[1]);
  346. prevent = true;
  347. }
  348. if (prevent) {
  349. e.srcEvent.preventDefault();
  350. e.srcEvent.stopPropagation();
  351. }
  352. e.preventSystemEvent = prevent;
  353. pos[0] = this._status.moveDistance[0];
  354. pos[1] = this._status.moveDistance[1];
  355. pos = this._getCircularPos(pos, min, max);
  356. // from outside to inside
  357. if (this._status.grabOutside && !this._isOutside(pos, min, max)) {
  358. this._status.grabOutside = false;
  359. }
  360. // when move pointer is held in outside
  361. if (this._status.grabOutside) {
  362. tn = min[0] - out[3], tx = max[0] + out[1], tv = pos[0];
  363. pos[0] = tv > tx ? tx : (tv < tn ? tn : tv);
  364. tn = min[1] - out[0], tx = max[1] + out[2], tv = pos[1];
  365. pos[1] = tv > tx ? tx : (tv < tn ? tn : tv);
  366. } else {
  367. // when start pointer is held in inside
  368. // get a initialization slope value to prevent smooth animation.
  369. var initSlope = this._easing(0.00001) / 0.00001;
  370. if (pos[1] < min[1]) { // up
  371. tv = (min[1] - pos[1]) / (out[0] * initSlope);
  372. pos[1] = min[1] - this._easing(tv) * out[0];
  373. } else if (pos[1] > max[1]) { // down
  374. tv = (pos[1] - max[1]) / (out[2] * initSlope);
  375. pos[1] = max[1] + this._easing(tv) * out[2];
  376. }
  377. if (pos[0] < min[0]) { // left
  378. tv = (min[0] - pos[0]) / (out[3] * initSlope);
  379. pos[0] = min[0] - this._easing(tv) * out[3];
  380. } else if (pos[0] > max[0]) { // right
  381. tv = (pos[0] - max[0]) / (out[1] * initSlope);
  382. pos[0] = max[0] + this._easing(tv) * out[1];
  383. }
  384. }
  385. this._triggerChange(pos, true, e);
  386. },
  387. // panend event handler
  388. _panend: function(e) {
  389. var pos = this._pos;
  390. if (!this._isInterrupting() || !this._status.moveDistance) {
  391. return;
  392. }
  393. // Abort the animating post process when "tap" occurs
  394. if (e.distance === 0 /*e.type === "tap"*/) {
  395. this._setInterrupt(false);
  396. this.trigger("release", {
  397. depaPos: pos.concat(),
  398. destPos: pos.concat(),
  399. hammerEvent: e || null
  400. });
  401. } else {
  402. var direction = this._subOptions.direction;
  403. var scale = this._subOptions.scale;
  404. var vX = Math.abs(e.velocityX);
  405. var vY = Math.abs(e.velocityY);
  406. !(direction & MC.DIRECTION_HORIZONTAL) && (vX = 0);
  407. !(direction & MC.DIRECTION_VERTICAL) && (vY = 0);
  408. var offset = this._getNextOffsetPos([
  409. vX * (e.deltaX < 0 ? -1 : 1) * scale[0],
  410. vY * (e.deltaY < 0 ? -1 : 1) * scale[1]
  411. ]);
  412. var destPos = [ pos[0] + offset[0], pos[1] + offset[1] ];
  413. destPos = this._getPointOfIntersection(pos, destPos);
  414. /**
  415. * This event is fired when a user release an element on the screen of the device.
  416. * @ko 사용자가 기기의 화면에서 손을 뗐을 때 발생하는 이벤트
  417. * @name eg.MovableCoord#release
  418. * @event
  419. *
  420. * @param {Object} param The object of data to be sent when the event is fired<ko>이벤트가 발생할 때 전달되는 데이터 객체</ko>
  421. * @param {Array} param.depaPos The coordinates when releasing an element<ko>손을 뗐을 때의 좌표현재 </ko>
  422. * @param {Number} param.depaPos.0 The X coordinate <ko> x 좌표</ko>
  423. * @param {Number} param.depaPos.1 The Y coordinate <ko> y 좌표</ko>
  424. * @param {Array} param.destPos The coordinates to move to after releasing an element<ko>손을 뗀 뒤에 이동할 좌표</ko>
  425. * @param {Number} param.destPos.0 The X coordinate <ko>x 좌표</ko>
  426. * @param {Number} param.destPos.1 The Y coordinate <ko>y 좌표</ko>
  427. * @param {Object} param.hammerEvent The event information of Hammer.JS. It returns null if the event is fired through a call to the setTo() or setBy() method.<ko>Hammer.JS의 이벤트 정보. setTo() 메서드나 setBy() 메서드를 호출해 이벤트가 발생했을 때는 'null'을 반환한다</ko>
  428. *
  429. */
  430. this.trigger("release", {
  431. depaPos: pos.concat(),
  432. destPos: destPos,
  433. hammerEvent: e || null
  434. });
  435. if (pos[0] !== destPos[0] || pos[1] !== destPos[1]) {
  436. this._animateTo(destPos, null, e || null);
  437. } else {
  438. this._setInterrupt(false);
  439. }
  440. }
  441. this._status.moveDistance = null;
  442. },
  443. _isInterrupting: function() {
  444. // when interruptable is 'true', return value is always 'true'.
  445. return this._subOptions.interruptable || this._status.prevented;
  446. },
  447. // get user's direction
  448. _getDirection: function(angle) {
  449. var thresholdAngle = this._subOptions.thresholdAngle;
  450. if (thresholdAngle < 0 || thresholdAngle > 90) {
  451. return MC.DIRECTION_NONE;
  452. }
  453. angle = Math.abs(angle);
  454. return angle > thresholdAngle && angle < 180 - thresholdAngle ?
  455. MC.DIRECTION_VERTICAL : MC.DIRECTION_HORIZONTAL;
  456. },
  457. _getNextOffsetPos: function(speeds) {
  458. var normalSpeed = Math.sqrt(
  459. speeds[0] * speeds[0] + speeds[1] * speeds[1]
  460. );
  461. var duration = Math.abs(normalSpeed / -this.options.deceleration);
  462. return [
  463. speeds[0] / 2 * duration,
  464. speeds[1] / 2 * duration
  465. ];
  466. },
  467. _getDurationFromPos: function(pos) {
  468. var normalPos = Math.sqrt(pos[0] * pos[0] + pos[1] * pos[1]);
  469. var duration = Math.sqrt(
  470. normalPos / this.options.deceleration * 2
  471. );
  472. // when duration is under 100, then value is zero
  473. return duration < 100 ? 0 : duration;
  474. },
  475. _getPointOfIntersection: function(depaPos, destPos) {
  476. var circular = this.options.circular;
  477. var bounce = this.options.bounce;
  478. var min = this.options.min;
  479. var max = this.options.max;
  480. var boxLT = [ min[0] - bounce[3], min[1] - bounce[0] ];
  481. var boxRB = [ max[0] + bounce[1], max[1] + bounce[2] ];
  482. var xd;
  483. var yd;
  484. destPos = [destPos[0], destPos[1]];
  485. xd = destPos[0] - depaPos[0], yd = destPos[1] - depaPos[1];
  486. if (!circular[3]) {
  487. destPos[0] = Math.max(boxLT[0], destPos[0]);
  488. } // left
  489. if (!circular[1]) {
  490. destPos[0] = Math.min(boxRB[0], destPos[0]);
  491. } // right
  492. destPos[1] = xd ?
  493. depaPos[1] + yd / xd * (destPos[0] - depaPos[0]) :
  494. destPos[1];
  495. if (!circular[0]) {
  496. destPos[1] = Math.max(boxLT[1], destPos[1]);
  497. } // up
  498. if (!circular[2]) {
  499. destPos[1] = Math.min(boxRB[1], destPos[1]);
  500. } // down
  501. destPos[0] = yd ?
  502. depaPos[0] + xd / yd * (destPos[1] - depaPos[1]) :
  503. destPos[0];
  504. return [
  505. Math.min(max[0], Math.max(min[0], destPos[0])),
  506. Math.min(max[1], Math.max(min[1], destPos[1]))
  507. ];
  508. },
  509. _isCircular: function(destPos) {
  510. var circular = this.options.circular;
  511. var min = this.options.min;
  512. var max = this.options.max;
  513. return (circular[0] && destPos[1] < min[1]) ||
  514. (circular[1] && destPos[0] > max[0]) ||
  515. (circular[2] && destPos[1] > max[1]) ||
  516. (circular[3] && destPos[0] < min[0]);
  517. },
  518. _prepareParam: function(absPos, duration, hammerEvent) {
  519. var pos = this._pos;
  520. var destPos = this._getPointOfIntersection(pos, absPos);
  521. destPos = this._isOutToOut(pos, destPos) ? pos : destPos;
  522. var distance = [
  523. Math.abs(destPos[0] - pos[0]),
  524. Math.abs(destPos[1] - pos[1])
  525. ];
  526. duration = duration == null ? this._getDurationFromPos(distance) : duration;
  527. duration = this.options.maximumDuration > duration ?
  528. duration : this.options.maximumDuration;
  529. return {
  530. depaPos: pos.concat(),
  531. destPos: destPos.concat(),
  532. isBounce: this._isOutside(destPos, this.options.min, this.options.max),
  533. isCircular: this._isCircular(absPos),
  534. duration: duration,
  535. distance: distance,
  536. hammerEvent: hammerEvent || null,
  537. done: this._animationEnd
  538. };
  539. },
  540. _restore: function(complete, hammerEvent) {
  541. var pos = this._pos;
  542. var min = this.options.min;
  543. var max = this.options.max;
  544. this._animate(this._prepareParam([
  545. Math.min(max[0], Math.max(min[0], pos[0])),
  546. Math.min(max[1], Math.max(min[1], pos[1]))
  547. ], null, hammerEvent), complete);
  548. },
  549. _animationEnd: function() {
  550. this._status.animationParam = null;
  551. this._pos = this._getCircularPos([
  552. Math.round(this._pos[0]),
  553. Math.round(this._pos[1])
  554. ]);
  555. this._setInterrupt(false);
  556. /**
  557. * This event is fired when animation ends.
  558. * @ko 에니메이션이 끝났을 때 발생한다.
  559. * @name eg.MovableCoord#animationEnd
  560. * @event
  561. */
  562. this.trigger("animationEnd");
  563. },
  564. _animate: function(param, complete) {
  565. param.startTime = new Date().getTime();
  566. this._status.animationParam = param;
  567. if (param.duration) {
  568. var info = this._status.animationParam;
  569. var self = this;
  570. (function loop() {
  571. self._raf = null;
  572. if (self._frame(info) >= 1) {
  573. // deferred.resolve();
  574. complete();
  575. return;
  576. } // animationEnd
  577. self._raf = ns.requestAnimationFrame(loop);
  578. })();
  579. } else {
  580. this._triggerChange(param.destPos, false);
  581. complete();
  582. }
  583. },
  584. _animateTo: function(absPos, duration, hammerEvent) {
  585. var param = this._prepareParam(absPos, duration, hammerEvent);
  586. var retTrigger = this.trigger("animationStart", param);
  587. // You can't stop the 'animationStart' event when 'circular' is true.
  588. if (param.isCircular && !retTrigger) {
  589. throw new Error(
  590. "You can't stop the 'animation' event when 'circular' is true."
  591. );
  592. }
  593. if (retTrigger) {
  594. var self = this;
  595. var queue = [];
  596. var dequeue = function() {
  597. var task = queue.shift();
  598. task && task.call(this);
  599. };
  600. if (param.depaPos[0] !== param.destPos[0] ||
  601. param.depaPos[1] !== param.destPos[1]) {
  602. queue.push(function() {
  603. self._animate(param, dequeue);
  604. });
  605. }
  606. if (this._isOutside(param.destPos, this.options.min, this.options.max)) {
  607. queue.push(function() {
  608. self._restore(dequeue, hammerEvent);
  609. });
  610. }
  611. queue.push(function() {
  612. self._animationEnd();
  613. });
  614. dequeue();
  615. }
  616. },
  617. // animation frame (0~1)
  618. _frame: function(param) {
  619. var curTime = new Date() - param.startTime;
  620. var easingPer = this._easing(curTime / param.duration);
  621. var pos = [ param.depaPos[0], param.depaPos[1] ];
  622. for (var i = 0; i < 2 ; i++) {
  623. (pos[i] !== param.destPos[i]) &&
  624. (pos[i] += (param.destPos[i] - pos[i]) * easingPer);
  625. }
  626. pos = this._getCircularPos(pos);
  627. this._triggerChange(pos, false);
  628. return easingPer;
  629. },
  630. // set up 'css' expression
  631. _reviseOptions: function() {
  632. var key;
  633. var self = this;
  634. (["bounce", "margin", "circular"]).forEach(function(v) {
  635. key = self.options[v];
  636. if (key != null) {
  637. if (key.constructor === Array) {
  638. self.options[v] = key.length === 2 ?
  639. key.concat(key) : key.concat();
  640. } else if (/string|number|boolean/.test(typeof key)) {
  641. self.options[v] = [ key, key, key, key ];
  642. } else {
  643. self.options[v] = null;
  644. }
  645. }
  646. });
  647. },
  648. // trigger 'change' event
  649. _triggerChange: function(pos, holding, e) {
  650. /**
  651. * This event is fired when coordinate changes.
  652. * @ko 좌표가 변경됐을 때 발생하는 이벤트
  653. * @name eg.MovableCoord#change
  654. * @event
  655. *
  656. * @param {Object} param The object of data to be sent when the event is fired <ko>이벤트가 발생할 때 전달되는 데이터 객체</ko>
  657. * @param {Array} param.pos departure coordinate <ko>좌표</ko>
  658. * @param {Number} param.pos.0 The X coordinate <ko>x 좌표</ko>
  659. * @param {Number} param.pos.1 The Y coordinate <ko>y 좌표</ko>
  660. * @param {Boolean} param.holding Indicates whether a user holds an element on the screen of the device.<ko>사용자가 기기의 화면을 누르고 있는지 여부</ko>
  661. * @param {Object} param.hammerEvent The event information of Hammer.JS. It returns null if the event is fired through a call to the setTo() or setBy() method.<ko>Hammer.JS의 이벤트 정보. setTo() 메서드나 setBy() 메서드를 호출해 이벤트가 발생했을 때는 'null'을 반환한다.</ko>
  662. *
  663. */
  664. this._pos = pos.concat();
  665. this.trigger("change", {
  666. pos: pos.concat(),
  667. holding: holding,
  668. hammerEvent: e || null
  669. });
  670. },
  671. /**
  672. * Returns the current position of the logical coordinates.
  673. * @ko 논리적 좌표의 현재 위치를 반환한다
  674. * @method eg.MovableCoord#get
  675. * @return {Array} pos <ko>좌표</ko>
  676. * @return {Number} pos.0 The X coordinate <ko>x 좌표</ko>
  677. * @return {Number} pos.1 The Y coordinate <ko>y 좌표</ko>
  678. */
  679. get: function() {
  680. return this._pos.concat();
  681. },
  682. /**
  683. * Moves an element to specific coordinates.
  684. * @ko 좌표를 이동한다.
  685. * @method eg.MovableCoord#setTo
  686. * @param {Number} x The X coordinate to move to <ko>이동할 x좌표</ko>
  687. * @param {Number} y The Y coordinate to move to <ko>이동할 y좌표</ko>
  688. * @param {Number} [duration=0] Duration of the animation (unit: ms) <ko>애니메이션 진행 시간(단위: ms)</ko>
  689. * @return {eg.MovableCoord} An instance of a module itself <ko>자신의 인스턴스</ko>
  690. */
  691. setTo: function(x, y, duration) {
  692. this._grab();
  693. var pos = this._pos.concat();
  694. var circular = this.options.circular;
  695. var min = this.options.min;
  696. var max = this.options.max;
  697. if (x === pos[0] && y === pos[1]) {
  698. return this;
  699. }
  700. this._setInterrupt(true);
  701. if (x !== pos[0]) {
  702. if (!circular[3]) {
  703. x = Math.max(min[0], x);
  704. }
  705. if (!circular[1]) {
  706. x = Math.min(max[0], x);
  707. }
  708. }
  709. if (y !== pos[1]) {
  710. if (!circular[0]) {
  711. y = Math.max(min[1], y);
  712. }
  713. if (!circular[2]) {
  714. y = Math.min(max[1], y);
  715. }
  716. }
  717. if (duration) {
  718. this._animateTo([ x, y ], duration);
  719. } else {
  720. this._pos = this._getCircularPos([ x, y ]);
  721. this._triggerChange(this._pos, false);
  722. this._setInterrupt(false);
  723. }
  724. return this;
  725. },
  726. /**
  727. * Moves an element from the current coordinates to specific coordinates. The change event is fired when the method is executed.
  728. * @ko 현재 좌표를 기준으로 좌표를 이동한다. 메서드가 실행되면 change 이벤트가 발생한다
  729. * @method eg.MovableCoord#setBy
  730. * @param {Number} x The X coordinate to move to <ko>이동할 x좌표</ko>
  731. * @param {Number} y The Y coordinate to move to <ko>이동할 y좌표</ko>
  732. * @param {Number} [duration=0] Duration of the animation (unit: ms) <ko>애니메이션 진행 시간(단위: ms)</ko>
  733. * @return {eg.MovableCoord} An instance of a module itself <ko>자신의 인스턴스</ko>
  734. */
  735. setBy: function(x, y, duration) {
  736. return this.setTo(
  737. x != null ? this._pos[0] + x : this._pos[0],
  738. y != null ? this._pos[1] + y : this._pos[1],
  739. duration
  740. );
  741. },
  742. _easing: function(p) {
  743. return p > 1 ? 1 : this.options.easing(p);
  744. },
  745. _setInterrupt: function(prevented) {
  746. !this._subOptions.interruptable &&
  747. (this._status.prevented = prevented);
  748. },
  749. _getEl: function(el) {
  750. if (typeof el === "string") {
  751. return document.querySelector(el);
  752. } else if (el instanceof jQuery && el.length > 0) {
  753. return el[0];
  754. }
  755. return el;
  756. },
  757. /**
  758. * Enables input devices
  759. * @ko 입력 장치를 사용할 수 있게 한다
  760. * @method eg.MovableCoord#enableInput
  761. * @param {HTMLElement|String|jQuery} [element] An element from which the eg.MovableCoord module is using (if the element parameter is not present, it applies to all binded elements)<ko>eg.MovableCoord 모듈을 사용하는 엘리먼트 (element 파라미터가 존재하지 않을 경우, 바인드된 모든 엘리먼트에 적용된다)</ko>
  762. * @return {eg.MovableCoord} An instance of a module itself <ko>자신의 인스턴스</ko>
  763. */
  764. enableInput: function(element) {
  765. return this._inputControl(true, element);
  766. },
  767. /**
  768. * Disables input devices
  769. * @ko 입력 장치를 사용할 수 없게 한다.
  770. * @method eg.MovableCoord#disableInput
  771. * @param {HTMLElement|String|jQuery} [element] An element from which the eg.MovableCoord module is using (if the element parameter is not present, it applies to all binded elements)<<ko>eg.MovableCoord 모듈을 사용하는 엘리먼트 (element 파라미터가 존재하지 않을 경우, 바인드된 모든 엘리먼트에 적용된다)</ko>
  772. * @return {eg.MovableCoord} An instance of a module itself <ko>자신의 인스턴스</ko>
  773. */
  774. disableInput: function(element) {
  775. return this._inputControl(false, element);
  776. },
  777. _inputControl: function(isEnable, element) {
  778. var option = { enable: isEnable };
  779. if (element) {
  780. var hammer = this.getHammer(element);
  781. hammer && hammer.get("pan").set(option);
  782. } else { // for multi
  783. for (var p in this._hammers) {
  784. this._hammers[p].inst.get("pan").set(option);
  785. }
  786. }
  787. return this;
  788. },
  789. /**
  790. * Destroys elements, properties, and events used in a module.
  791. * @ko 모듈에 사용한 엘리먼트와 속성, 이벤트를 해제한다.
  792. * @method eg.MovableCoord#destroy
  793. */
  794. destroy: function() {
  795. this.off();
  796. for (var p in this._hammers) {
  797. this._hammers[p].inst.destroy();
  798. delete this._hammers[p].el[MC._KEY];
  799. delete this._hammers[p];
  800. }
  801. }
  802. });
  803. MC._KEY = "__MOVABLECOORD__";
  804. /**
  805. * @name eg.MovableCoord.DIRECTION_NONE
  806. * @constant
  807. * @type {Number}
  808. */
  809. MC.DIRECTION_NONE = 1;
  810. /**
  811. * @name eg.MovableCoord.DIRECTION_LEFT
  812. * @constant
  813. * @type {Number}
  814. */
  815. MC.DIRECTION_LEFT = 2;
  816. /**
  817. * @name eg.MovableCoord.DIRECTION_RIGHT
  818. * @constant
  819. * @type {Number}
  820. */
  821. MC.DIRECTION_RIGHT = 4;
  822. /**
  823. * @name eg.MovableCoord.DIRECTION_UP
  824. * @constant
  825. * @type {Number}
  826. */
  827. MC.DIRECTION_UP = 8;
  828. /**
  829. * @name eg.MovableCoord.DIRECTION_DOWN
  830. * @constant
  831. * @type {Number}
  832. */
  833. MC.DIRECTION_DOWN = 16;
  834. /**
  835. * @name eg.MovableCoord.DIRECTION_HORIZONTAL
  836. * @constant
  837. * @type {Number}
  838. */
  839. MC.DIRECTION_HORIZONTAL = 2 | 4;
  840. /**
  841. * @name eg.MovableCoord.DIRECTION_VERTICAL
  842. * @constant
  843. * @type {Number}
  844. */
  845. MC.DIRECTION_VERTICAL = 8 | 16;
  846. /**
  847. * @name eg.MovableCoord.DIRECTION_ALL
  848. * @constant
  849. * @type {Number}
  850. */
  851. MC.DIRECTION_ALL = MC.DIRECTION_HORIZONTAL | MC.DIRECTION_VERTICAL;
  852. return {
  853. "MovableCoord": ns.MovableCoord,
  854. "assignFn": assignFn
  855. };
  856. });
comments powered by Disqus