;(function(undefined) { 'use strict'; if (typeof sigma === 'undefined') throw 'sigma is not declared'; // Initialize packages: sigma.utils.pkg('sigma.captors'); /** * The user inputs default captor. It deals with mouse events, keyboards * events and touch events. * * @param {DOMElement} target The DOM element where the listeners will be * bound. * @param {camera} camera The camera related to the target. * @param {configurable} settings The settings function. * @return {sigma.captor} The fresh new captor instance. */ sigma.captors.touch = function(target, camera, settings) { var _self = this, _target = target, _camera = camera, _settings = settings, // CAMERA MANAGEMENT: // ****************** // The camera position when the user starts dragging: _startCameraX, _startCameraY, _startCameraAngle, _startCameraRatio, // The latest stage position: _lastCameraX, _lastCameraY, _lastCameraAngle, _lastCameraRatio, // TOUCH MANAGEMENT: // ***************** // Touches that are down: _downTouches = [], _startTouchX0, _startTouchY0, _startTouchX1, _startTouchY1, _startTouchAngle, _startTouchDistance, _touchMode, _isMoving, _doubleTap, _movingTimeoutId; sigma.classes.dispatcher.extend(this); sigma.utils.doubleClick(_target, 'touchstart', _doubleTapHandler); _target.addEventListener('touchstart', _handleStart, false); _target.addEventListener('touchend', _handleLeave, false); _target.addEventListener('touchcancel', _handleLeave, false); _target.addEventListener('touchleave', _handleLeave, false); _target.addEventListener('touchmove', _handleMove, false); function position(e) { var offset = sigma.utils.getOffset(_target); return { x: e.pageX - offset.left, y: e.pageY - offset.top }; } /** * This method unbinds every handlers that makes the captor work. */ this.kill = function() { sigma.utils.unbindDoubleClick(_target, 'touchstart'); _target.addEventListener('touchstart', _handleStart); _target.addEventListener('touchend', _handleLeave); _target.addEventListener('touchcancel', _handleLeave); _target.addEventListener('touchleave', _handleLeave); _target.addEventListener('touchmove', _handleMove); }; // TOUCH EVENTS: // ************* /** * The handler listening to the 'touchstart' event. It will set the touch * mode ("_touchMode") and start observing the user touch moves. * * @param {event} e A touch event. */ function _handleStart(e) { if (_settings('touchEnabled')) { var x0, x1, y0, y1, pos0, pos1; _downTouches = e.touches; switch (_downTouches.length) { case 1: _camera.isMoving = true; _touchMode = 1; _startCameraX = _camera.x; _startCameraY = _camera.y; _lastCameraX = _camera.x; _lastCameraY = _camera.y; pos0 = position(_downTouches[0]); _startTouchX0 = pos0.x; _startTouchY0 = pos0.y; break; case 2: _camera.isMoving = true; _touchMode = 2; pos0 = position(_downTouches[0]); pos1 = position(_downTouches[1]); x0 = pos0.x; y0 = pos0.y; x1 = pos1.x; y1 = pos1.y; _lastCameraX = _camera.x; _lastCameraY = _camera.y; _startCameraAngle = _camera.angle; _startCameraRatio = _camera.ratio; _startCameraX = _camera.x; _startCameraY = _camera.y; _startTouchX0 = x0; _startTouchY0 = y0; _startTouchX1 = x1; _startTouchY1 = y1; _startTouchAngle = Math.atan2( _startTouchY1 - _startTouchY0, _startTouchX1 - _startTouchX0 ); _startTouchDistance = Math.sqrt( (_startTouchY1 - _startTouchY0) * (_startTouchY1 - _startTouchY0) + (_startTouchX1 - _startTouchX0) * (_startTouchX1 - _startTouchX0) ); e.preventDefault(); return false; } } } /** * The handler listening to the 'touchend', 'touchcancel' and 'touchleave' * event. It will update the touch mode if there are still at least one * finger, and stop dragging else. * * @param {event} e A touch event. */ function _handleLeave(e) { if (_settings('touchEnabled')) { _downTouches = e.touches; var inertiaRatio = _settings('touchInertiaRatio'); if (_movingTimeoutId) { _isMoving = false; clearTimeout(_movingTimeoutId); } switch (_touchMode) { case 2: if (e.touches.length === 1) { _handleStart(e); e.preventDefault(); break; } /* falls through */ case 1: _camera.isMoving = false; _self.dispatchEvent('stopDrag'); if (_isMoving) { _doubleTap = false; sigma.misc.animation.camera( _camera, { x: _camera.x + inertiaRatio * (_camera.x - _lastCameraX), y: _camera.y + inertiaRatio * (_camera.y - _lastCameraY) }, { easing: 'quadraticOut', duration: _settings('touchInertiaDuration') } ); } _isMoving = false; _touchMode = 0; break; } } } /** * The handler listening to the 'touchmove' event. It will effectively drag * the graph, and eventually zooms and turn it if the user is using two * fingers. * * @param {event} e A touch event. */ function _handleMove(e) { if (!_doubleTap && _settings('touchEnabled')) { var x0, x1, y0, y1, cos, sin, end, pos0, pos1, diff, start, dAngle, dRatio, newStageX, newStageY, newStageRatio, newStageAngle; _downTouches = e.touches; _isMoving = true; if (_movingTimeoutId) clearTimeout(_movingTimeoutId); _movingTimeoutId = setTimeout(function() { _isMoving = false; }, _settings('dragTimeout')); switch (_touchMode) { case 1: pos0 = position(_downTouches[0]); x0 = pos0.x; y0 = pos0.y; diff = _camera.cameraPosition( x0 - _startTouchX0, y0 - _startTouchY0, true ); newStageX = _startCameraX - diff.x; newStageY = _startCameraY - diff.y; if (newStageX !== _camera.x || newStageY !== _camera.y) { _lastCameraX = _camera.x; _lastCameraY = _camera.y; _camera.goTo({ x: newStageX, y: newStageY }); _self.dispatchEvent('mousemove', sigma.utils.mouseCoords(e, pos0.x, pos0.y)); _self.dispatchEvent('drag'); } break; case 2: pos0 = position(_downTouches[0]); pos1 = position(_downTouches[1]); x0 = pos0.x; y0 = pos0.y; x1 = pos1.x; y1 = pos1.y; start = _camera.cameraPosition( (_startTouchX0 + _startTouchX1) / 2 - sigma.utils.getCenter(e).x, (_startTouchY0 + _startTouchY1) / 2 - sigma.utils.getCenter(e).y, true ); end = _camera.cameraPosition( (x0 + x1) / 2 - sigma.utils.getCenter(e).x, (y0 + y1) / 2 - sigma.utils.getCenter(e).y, true ); dAngle = Math.atan2(y1 - y0, x1 - x0) - _startTouchAngle; dRatio = Math.sqrt( (y1 - y0) * (y1 - y0) + (x1 - x0) * (x1 - x0) ) / _startTouchDistance; // Translation: x0 = start.x; y0 = start.y; // Homothetic transformation: newStageRatio = _startCameraRatio / dRatio; x0 = x0 * dRatio; y0 = y0 * dRatio; // Rotation: newStageAngle = _startCameraAngle - dAngle; cos = Math.cos(-dAngle); sin = Math.sin(-dAngle); x1 = x0 * cos + y0 * sin; y1 = y0 * cos - x0 * sin; x0 = x1; y0 = y1; // Finalize: newStageX = x0 - end.x + _startCameraX; newStageY = y0 - end.y + _startCameraY; if ( newStageRatio !== _camera.ratio || newStageAngle !== _camera.angle || newStageX !== _camera.x || newStageY !== _camera.y ) { _lastCameraX = _camera.x; _lastCameraY = _camera.y; _lastCameraAngle = _camera.angle; _lastCameraRatio = _camera.ratio; _camera.goTo({ x: newStageX, y: newStageY, angle: newStageAngle, ratio: newStageRatio }); _self.dispatchEvent('drag'); } break; } e.preventDefault(); return false; } } /** * The handler listening to the double tap custom event. It will * basically zoom into the graph. * * @param {event} e A touch event. */ function _doubleTapHandler(e) { var pos, ratio, animation; if (e.touches && e.touches.length === 1 && _settings('touchEnabled')) { _doubleTap = true; ratio = 1 / _settings('doubleClickZoomingRatio'); pos = position(e.touches[0]); _self.dispatchEvent('doubleclick', sigma.utils.mouseCoords(e, pos.x, pos.y)); if (_settings('doubleClickEnabled')) { pos = _camera.cameraPosition( pos.x - sigma.utils.getCenter(e).x, pos.y - sigma.utils.getCenter(e).y, true ); animation = { duration: _settings('doubleClickZoomDuration'), onComplete: function() { _doubleTap = false; } }; sigma.utils.zoomTo(_camera, pos.x, pos.y, ratio, animation); } if (e.preventDefault) e.preventDefault(); else e.returnValue = false; e.stopPropagation(); return false; } } }; }).call(this);