├── .gitignore ├── LICENSE ├── README.md ├── demo ├── bundle.js ├── index.html ├── index.js └── textures │ └── disturb.jpg ├── index.js ├── lib └── kinetic.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Andrei Kashcha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three.map.control 2 | 3 | Mobile friendly three.js camera that mimics 2d maps navigation with pan and zoom. 4 | 5 | [DEMO](https://anvaka.github.io/three.map.control/demo/) 6 | 7 | ## Features 8 | 9 | * **Touch friendly**. Drag scene around with single finger touch, or zoom it with standard 10 | pinch gesture. 11 | 12 | ![touch friendly](https://i.imgur.com/CL3inbB.gif) 13 | 14 | * **Zoom into point**. Use your mouse wheel to zoom into particular point on the scene. 15 | * **Easing**. When you pan around, the movement does not stop immediately. Smooth 16 | kinetic panning gives natural feel to it. 17 | 18 | ![easing](https://i.imgur.com/PSbGYp1.gif) 19 | * **Tiny**. It's less than `400` lines of documented code. 20 | 21 | # usage 22 | 23 | ``` js 24 | // let's say you have a standard THREE.js PerspectiveCamera: 25 | var camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 3000 ); 26 | 27 | // To turn on a map-like navigation: 28 | var createPanZoom = require('three.map.control'); 29 | 30 | // We assume that three.js scene is hosted inside DOM element `container` 31 | var panZoom = createPanZoom(camera, container); 32 | 33 | // That's it. panZoom wil now listen to events from `container`. You can pan and 34 | // zoom with your mouse or fingers (on touch device) 35 | 36 | // If you want to dispose three.js scene, make sure to call: 37 | panZoom.dispose(); 38 | ``` 39 | 40 | ## events 41 | 42 | ``` js 43 | // the panZoom api fires events when something happens, 44 | // so that you can react to user actions: 45 | panZoom.on('panstart', function() { 46 | // fired when users begins panning (dragging) the surface 47 | console.log('panstart fired'); 48 | }); 49 | 50 | panZoom.on('panend', function() { 51 | // fired when user stpos panning (dragging) the surface 52 | console.log('panend fired'); 53 | }); 54 | 55 | panZoom.on('beforepan', function(panPayload) { 56 | // fired when camera position will be changed. 57 | console.log('going to move camera.position.x by: ' + panPayload.dx); 58 | console.log('going to move camera.position.y by: ' + panPayload.dy); 59 | }); 60 | 61 | panZoom.on('beforezoom', function(panPayload) { 62 | // fired when befor zoom in/zoom out 63 | console.log('going to move camera.position.x by: ' + panPayload.dx); 64 | console.log('going to move camera.position.y by: ' + panPayload.dy); 65 | console.log('going to move camera.position.z by: ' + panPayload.dz); 66 | }); 67 | ``` 68 | 69 | # license 70 | 71 | MIT 72 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | three.map.control demo 5 | 6 | 7 | 8 | 36 | 37 | 38 |
39 |

40 | scroll or two fingers pinch to zoom
41 | drag to pan
42 | code 43 |

44 | 45 | 64 | 65 | 87 | 88 | 116 | 117 | 155 | 156 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | var THREE = require('three'); 2 | 3 | var container = document.getElementById('container'); 4 | 5 | var camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 3000 ); 6 | camera.position.z = 4; 7 | // This is how to use three.map.control: 8 | var createPanZoom = require('../'); 9 | var panZoom = createPanZoom(camera, container); 10 | // For convenience - move focus to the container: 11 | container.focus(); 12 | 13 | // That's it! Now you should be able to use mouse left button (or a single tap) to pan around 14 | // Use mouse wheel to zoom in/out (or two fingers pinch) 15 | // 16 | // When you dispose the three.js scene, don't forget to call: 17 | // panZoom.dispose(); 18 | 19 | // # EVENTS SUPPORT 20 | 21 | // the panZoom api fires events when something happens, 22 | // so that you can react to user actions: 23 | panZoom.on('panstart', function() { 24 | // fired when users begins panning (dragging) the surface 25 | console.log('panstart fired'); 26 | }); 27 | 28 | panZoom.on('panend', function() { 29 | // fired when user stpos panning (dragging) the surface 30 | console.log('panend fired'); 31 | }); 32 | 33 | panZoom.on('beforepan', function(panPayload) { 34 | // fired when camera position will be changed. 35 | console.log('going to move camera.position.x by: ' + panPayload.dx); 36 | console.log('going to move camera.position.y by: ' + panPayload.dy); 37 | }); 38 | 39 | panZoom.on('beforezoom', function(panPayload) { 40 | // fired when befor zoom in/zoom out 41 | console.log('going to move camera.position.x by: ' + panPayload.dx); 42 | console.log('going to move camera.position.y by: ' + panPayload.dy); 43 | console.log('going to move camera.position.z by: ' + panPayload.dz); 44 | }); 45 | 46 | // The rest of the code is just standard three.js demo from http://threejs.org/examples/webgl_shader2.html 47 | var scene, renderer; 48 | 49 | var uniforms1, uniforms2; 50 | 51 | var clock = new THREE.Clock(); 52 | 53 | init(); 54 | animate(); 55 | 56 | function init() { 57 | scene = new THREE.Scene(); 58 | 59 | var geometry = new THREE.BoxGeometry( 0.75, 0.75, 0.75 ); 60 | 61 | uniforms1 = { 62 | time: { value: 1.0 }, 63 | resolution: { value: new THREE.Vector2() } 64 | }; 65 | 66 | uniforms2 = { 67 | time: { value: 1.0 }, 68 | resolution: { value: new THREE.Vector2() }, 69 | texture: { value: new THREE.TextureLoader().load( "textures/disturb.jpg" ) } 70 | }; 71 | 72 | uniforms2.texture.value.wrapS = uniforms2.texture.value.wrapT = THREE.RepeatWrapping; 73 | 74 | var params = [ 75 | [ 'fragment_shader1', uniforms1 ], 76 | [ 'fragment_shader2', uniforms2 ], 77 | [ 'fragment_shader3', uniforms1 ], 78 | [ 'fragment_shader4', uniforms1 ] 79 | ]; 80 | 81 | for( var i = 0; i < params.length; i++ ) { 82 | 83 | var material = new THREE.ShaderMaterial( { 84 | 85 | uniforms: params[ i ][ 1 ], 86 | vertexShader: document.getElementById( 'vertexShader' ).textContent, 87 | fragmentShader: document.getElementById( params[ i ][ 0 ] ).textContent 88 | }); 89 | 90 | var mesh = new THREE.Mesh( geometry, material ); 91 | mesh.position.x = i - ( params.length - 1 ) / 2; 92 | mesh.position.y = i % 2 - 0.5; 93 | 94 | scene.add( mesh ); 95 | } 96 | 97 | renderer = new THREE.WebGLRenderer(); 98 | renderer.setPixelRatio( window.devicePixelRatio ); 99 | container.appendChild( renderer.domElement ); 100 | 101 | onWindowResize(); 102 | 103 | window.addEventListener( 'resize', onWindowResize, false ); 104 | 105 | function onWindowResize() { 106 | uniforms1.resolution.value.x = window.innerWidth; 107 | uniforms1.resolution.value.y = window.innerHeight; 108 | 109 | uniforms2.resolution.value.x = window.innerWidth; 110 | uniforms2.resolution.value.y = window.innerHeight; 111 | 112 | camera.aspect = window.innerWidth / window.innerHeight; 113 | camera.updateProjectionMatrix(); 114 | 115 | renderer.setSize( window.innerWidth, window.innerHeight ); 116 | 117 | } 118 | } 119 | 120 | function animate() { 121 | requestAnimationFrame(animate); 122 | render(); 123 | } 124 | 125 | function render() { 126 | var delta = clock.getDelta(); 127 | 128 | uniforms1.time.value += delta * 5; 129 | uniforms2.time.value = clock.elapsedTime; 130 | 131 | for ( var i = 0; i < scene.children.length; i ++ ) { 132 | 133 | var object = scene.children[ i ]; 134 | 135 | object.rotation.y += delta * 0.5 * ( i % 2 ? 1 : -1 ); 136 | object.rotation.x += delta * 0.5 * ( i % 2 ? -1 : 1 ); 137 | 138 | } 139 | 140 | renderer.render( scene, camera ); 141 | } 142 | 143 | -------------------------------------------------------------------------------- /demo/textures/disturb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anvaka/three.map.control/be77454f4e1c051e1ff474247a687a95b929dcb4/demo/textures/disturb.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var wheel = require('wheel') 2 | var eventify = require('ngraph.events') 3 | var kinetic = require('./lib/kinetic.js') 4 | var animate = require('amator'); 5 | 6 | module.exports = panzoom 7 | 8 | /** 9 | * Creates a new input controller. 10 | * 11 | * @param {Object} camera - a three.js perspective camera object. 12 | * @param {DOMElement+} owner - owner that should listen to mouse/keyboard/tap 13 | * events. This is optional, and defaults to document.body. 14 | * 15 | * @returns {Object} api for the input controller. It currently supports only one 16 | * method `dispose()` which should be invoked when you want to to release input 17 | * controller and all events. 18 | * 19 | * Consumers can listen to api's events via `api.on('change', function() {})` 20 | * interface. The change event will be fire every time when camera's position changed. 21 | */ 22 | function panzoom(camera, owner) { 23 | var isDragging = false 24 | var panstartFired = false 25 | var touchInProgress = false 26 | var lastTouchTime = new Date(0) 27 | var smoothZoomAnimation, smoothPanAnimation; 28 | var panPayload = { 29 | dx: 0, 30 | dy: 0 31 | } 32 | var zoomPayload = { 33 | dx: 0, 34 | dy: 0, 35 | dz: 0 36 | } 37 | 38 | var lastPinchZoomLength 39 | 40 | var mousePos = { 41 | x: 0, 42 | y: 0 43 | } 44 | 45 | owner = owner || document.body; 46 | owner.setAttribute('tabindex', 1); // TODO: not sure if this is really polite 47 | 48 | var smoothScroll = kinetic(getCameraPosition, { 49 | scrollCallback: onSmoothScroll 50 | }) 51 | 52 | wheel.addWheelListener(owner, onMouseWheel) 53 | 54 | var api = eventify({ 55 | dispose: dispose, 56 | speed: 0.03, 57 | min: 0.0001, 58 | max: Number.POSITIVE_INFINITY 59 | }) 60 | 61 | owner.addEventListener('mousedown', handleMouseDown) 62 | owner.addEventListener('touchstart', onTouch) 63 | owner.addEventListener('keydown', onKeyDown) 64 | 65 | return api; 66 | 67 | function onTouch(e) { 68 | var touchTime = new Date(); 69 | var timeBetweenTaps = touchTime - lastTouchTime; 70 | lastTouchTime = touchTime; 71 | 72 | var touchesCount = e.touches.length; 73 | 74 | if (timeBetweenTaps < 400 && touchesCount === 1) { 75 | handleDoubleTap(e); 76 | } else if (touchesCount < 3) { 77 | handleTouch(e) 78 | } 79 | } 80 | 81 | function onKeyDown(e) { 82 | var x = 0, y = 0, z = 0 83 | if (e.keyCode === 38) { 84 | y = 1 // up 85 | } else if (e.keyCode === 40) { 86 | y = -1 // down 87 | } else if (e.keyCode === 37) { 88 | x = 1 // left 89 | } else if (e.keyCode === 39) { 90 | x = -1 // right 91 | } else if (e.keyCode === 189 || e.keyCode === 109) { // DASH or SUBTRACT 92 | z = 1 // `-` - zoom out 93 | } else if (e.keyCode === 187 || e.keyCode === 107) { // EQUAL SIGN or ADD 94 | z = -1 // `=` - zoom in (equal sign on US layout is under `+`) 95 | } 96 | // TODO: Keypad keycodes are missing. 97 | 98 | if (x || y) { 99 | e.preventDefault() 100 | e.stopPropagation() 101 | smoothPanByOffset(5 * x, 5 * y) 102 | } 103 | 104 | if (z) { 105 | smoothZoom(owner.clientWidth/2, owner.clientHeight/2, z) 106 | } 107 | } 108 | 109 | function getPinchZoomLength(finger1, finger2) { 110 | return (finger1.clientX - finger2.clientX) * (finger1.clientX - finger2.clientX) + 111 | (finger1.clientY - finger2.clientY) * (finger1.clientY - finger2.clientY) 112 | } 113 | 114 | function handleTouch(e) { 115 | e.stopPropagation() 116 | e.preventDefault() 117 | 118 | setMousePos(e.touches[0]) 119 | 120 | if (!touchInProgress) { 121 | touchInProgress = true 122 | window.addEventListener('touchmove', handleTouchMove) 123 | window.addEventListener('touchend', handleTouchEnd) 124 | window.addEventListener('touchcancel', handleTouchEnd) 125 | } 126 | } 127 | 128 | function handleDoubleTap(e) { 129 | e.stopPropagation() 130 | e.preventDefault() 131 | 132 | var tap = e.touches[0]; 133 | 134 | smoothScroll.cancel(); 135 | 136 | smoothZoom(tap.clientX, tap.clientY, -1); 137 | } 138 | 139 | function smoothPanByOffset(x, y) { 140 | if (smoothPanAnimation) { 141 | smoothPanAnimation.cancel(); 142 | } 143 | 144 | var from = { x: x, y: y } 145 | var to = { x: 2 * x, y: 2 * y } 146 | smoothPanAnimation = animate(from, to, { 147 | easing: 'linear', 148 | duration: 200, 149 | step: function(d) { 150 | panByOffset(d.x, d.y) 151 | } 152 | }) 153 | } 154 | 155 | function smoothZoom(x, y, scale) { 156 | var from = { delta: scale } 157 | var to = { delta: scale * 2 } 158 | if (smoothZoomAnimation) { 159 | smoothZoomAnimation.cancel(); 160 | } 161 | 162 | smoothZoomAnimation = animate(from, to, { 163 | duration: 200, 164 | step: function(d) { 165 | var scaleMultiplier = getScaleMultiplier(d.delta); 166 | zoomTo(x, y, scaleMultiplier) 167 | } 168 | }) 169 | } 170 | 171 | function handleTouchMove(e) { 172 | triggerPanStart() 173 | 174 | if (e.touches.length === 1) { 175 | e.stopPropagation() 176 | var touch = e.touches[0] 177 | 178 | var dx = touch.clientX - mousePos.x 179 | var dy = touch.clientY - mousePos.y 180 | 181 | setMousePos(touch) 182 | 183 | panByOffset(dx, dy) 184 | } else if (e.touches.length === 2) { 185 | // it's a zoom, let's find direction 186 | var t1 = e.touches[0] 187 | var t2 = e.touches[1] 188 | var currentPinchLength = getPinchZoomLength(t1, t2) 189 | 190 | var delta = 0 191 | if (currentPinchLength < lastPinchZoomLength) { 192 | delta = 1 193 | } else if (currentPinchLength > lastPinchZoomLength) { 194 | delta = -1 195 | } 196 | 197 | var scaleMultiplier = getScaleMultiplier(delta) 198 | 199 | setMousePosFromTwoTouches(e); 200 | 201 | zoomTo(mousePos.x, mousePos.y, scaleMultiplier) 202 | 203 | lastPinchZoomLength = currentPinchLength 204 | 205 | e.stopPropagation() 206 | e.preventDefault() 207 | } 208 | } 209 | 210 | function setMousePosFromTwoTouches(e) { 211 | var t1 = e.touches[0] 212 | var t2 = e.touches[1] 213 | mousePos.x = (t1.clientX + t2.clientX)/2 214 | mousePos.y = (t1.clientY + t2.clientY)/2 215 | } 216 | 217 | function handleTouchEnd(e) { 218 | if (e.touches.length > 0) { 219 | setMousePos(e.touches[0]) 220 | } else { 221 | touchInProgress = false 222 | triggerPanEnd() 223 | disposeTouchEvents() 224 | } 225 | } 226 | 227 | function disposeTouchEvents() { 228 | window.removeEventListener('touchmove', handleTouchMove) 229 | window.removeEventListener('touchend', handleTouchEnd) 230 | window.removeEventListener('touchcancel', handleTouchEnd) 231 | } 232 | 233 | function getCameraPosition() { 234 | return camera.position 235 | } 236 | 237 | function onSmoothScroll(x, y) { 238 | camera.position.x = x 239 | camera.position.y = y 240 | 241 | api.fire('change') 242 | } 243 | 244 | function handleMouseDown(e) { 245 | isDragging = true 246 | setMousePos(e) 247 | 248 | window.addEventListener('mouseup', handleMouseUp, true) 249 | window.addEventListener('mousemove', handleMouseMove, true) 250 | } 251 | 252 | function handleMouseUp() { 253 | disposeWindowEvents() 254 | isDragging = false 255 | 256 | triggerPanEnd() 257 | } 258 | 259 | function setMousePos(e) { 260 | mousePos.x = e.clientX 261 | mousePos.y = e.clientY 262 | } 263 | 264 | function handleMouseMove(e) { 265 | if (!isDragging) return 266 | 267 | triggerPanStart() 268 | 269 | var dx = e.clientX - mousePos.x 270 | var dy = e.clientY - mousePos.y 271 | 272 | panByOffset(dx, dy) 273 | 274 | setMousePos(e) 275 | } 276 | 277 | function triggerPanStart() { 278 | if (!panstartFired) { 279 | api.fire('panstart') 280 | panstartFired = true 281 | smoothScroll.start() 282 | } 283 | } 284 | 285 | function triggerPanEnd() { 286 | if (panstartFired) { 287 | smoothScroll.stop() 288 | api.fire('panend') 289 | panstartFired = false 290 | } 291 | } 292 | 293 | function disposeWindowEvents() { 294 | window.removeEventListener('mouseup', handleMouseUp, true) 295 | window.removeEventListener('mousemove', handleMouseMove, true) 296 | } 297 | 298 | function dispose() { 299 | wheel.removeWheelListener(owner, onMouseWheel) 300 | disposeWindowEvents() 301 | disposeTouchEvents() 302 | 303 | smoothScroll.cancel() 304 | triggerPanEnd() 305 | 306 | owner.removeEventListener('mousedown', handleMouseDown) 307 | owner.removeEventListener('touchstart', onTouch) 308 | owner.removeEventListener('keydown', onKeyDown) 309 | } 310 | 311 | function panByOffset(dx, dy) { 312 | var currentScale = getCurrentScale() 313 | 314 | panPayload.dx = -dx/currentScale 315 | panPayload.dy = dy/currentScale 316 | 317 | // we fire first, so that clients can manipulate the payload 318 | api.fire('beforepan', panPayload) 319 | 320 | camera.position.x += panPayload.dx 321 | camera.position.y += panPayload.dy 322 | 323 | api.fire('change') 324 | } 325 | 326 | function onMouseWheel(e) { 327 | 328 | var scaleMultiplier = getScaleMultiplier(e.deltaY) 329 | 330 | smoothScroll.cancel() 331 | zoomTo(e.clientX, e.clientY, scaleMultiplier) 332 | } 333 | 334 | function zoomTo(offsetX, offsetY, scaleMultiplier) { 335 | var currentScale = getCurrentScale() 336 | 337 | var dx = (offsetX - owner.clientWidth / 2) / currentScale 338 | var dy = (offsetY - owner.clientHeight / 2) / currentScale 339 | 340 | var newZ = camera.position.z * scaleMultiplier 341 | if (newZ < api.min || newZ > api.max) { 342 | return 343 | } 344 | 345 | zoomPayload.dz = newZ - camera.position.z 346 | zoomPayload.dx = -(scaleMultiplier - 1) * dx 347 | zoomPayload.dy = (scaleMultiplier - 1) * dy 348 | 349 | api.fire('beforezoom', zoomPayload) 350 | 351 | camera.position.z += zoomPayload.dz 352 | camera.position.x -= (scaleMultiplier - 1) * dx 353 | camera.position.y += (scaleMultiplier - 1) * dy 354 | 355 | api.fire('change') 356 | } 357 | 358 | function getCurrentScale() { 359 | // TODO: This is the only code that depends on camera. Extract? 360 | var vFOV = camera.fov * Math.PI / 180 361 | var height = 2 * Math.tan( vFOV / 2 ) * camera.position.z 362 | var currentScale = owner.clientHeight / height 363 | 364 | return currentScale 365 | } 366 | 367 | function getScaleMultiplier(delta) { 368 | var scaleMultiplier = 1 369 | if (delta > 10) { 370 | delta = 10; 371 | } else if (delta < -10) { 372 | delta = -10; 373 | } 374 | scaleMultiplier = (1 + api.speed * delta) 375 | 376 | return scaleMultiplier 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /lib/kinetic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows smooth kinetic scrolling of the surface 3 | */ 4 | module.exports = kinetic; 5 | 6 | function kinetic(getPointCallback, options) { 7 | options = options || {}; 8 | 9 | var minVelocity = options.minVelocity || 10; 10 | var amplitude = options.amplitude || 0.42; 11 | var trackerSpeed = options.trackerSpeed || 100; 12 | var scrollCallback = options.scrollCallback || noop; 13 | 14 | var lastPoint = {x: 0, y: 0} 15 | var timestamp 16 | var timeConstant = 342 17 | 18 | var ticker 19 | var vx, targetX, ax; 20 | var vy, targetY, ay; 21 | 22 | var raf 23 | 24 | return { 25 | start: start, 26 | stop: stop, 27 | cancel: cancel, 28 | isStarted: isStarted 29 | } 30 | 31 | function isStarted() { 32 | return ticker !== 0; 33 | } 34 | 35 | function cancel() { 36 | window.clearInterval(ticker) 37 | window.cancelAnimationFrame(raf) 38 | } 39 | 40 | function start() { 41 | setInternalLastPoint(getPointCallback()) 42 | 43 | ax = ay = vx = vy = 0 44 | timestamp = new Date() 45 | 46 | window.clearInterval(ticker) 47 | window.cancelAnimationFrame(raf) 48 | 49 | ticker = window.setInterval(trackPointMovement, trackerSpeed); 50 | } 51 | 52 | function trackPointMovement() { 53 | var now = Date.now(); 54 | var elapsed = now - timestamp; 55 | timestamp = now; 56 | 57 | var point = getPointCallback() 58 | 59 | var dx = point.x - lastPoint.x 60 | var dy = point.y - lastPoint.y 61 | 62 | setInternalLastPoint(point); 63 | 64 | var dt = 1000 / (1 + elapsed) 65 | 66 | // moving average 67 | vx = 0.8 * dx * dt + 0.2 * vx 68 | vy = 0.8 * dy * dt + 0.2 * vy 69 | } 70 | 71 | function setInternalLastPoint(p) { 72 | lastPoint.x = p.x; 73 | lastPoint.y = p.y; 74 | } 75 | 76 | function stop() { 77 | window.clearInterval(ticker); 78 | window.cancelAnimationFrame(raf) 79 | ticker = 0; 80 | 81 | var point = getPointCallback() 82 | 83 | targetX = point.x 84 | targetY = point.y 85 | timestamp = Date.now() 86 | 87 | if (vx < -minVelocity || vx > minVelocity) { 88 | ax = amplitude * vx 89 | targetX += ax 90 | } 91 | 92 | if (vy < -minVelocity || vy > minVelocity) { 93 | ay = amplitude * vy 94 | targetY += ay 95 | } 96 | 97 | raf = window.requestAnimationFrame(autoScroll); 98 | } 99 | 100 | function autoScroll() { 101 | var elapsed = Date.now() - timestamp 102 | 103 | var moving = false 104 | var dx = 0 105 | var dy = 0 106 | 107 | if (ax) { 108 | dx = -ax * Math.exp(-elapsed / timeConstant) 109 | 110 | if (dx > 0.5 || dx < -0.5) moving = true 111 | else dx = ax = 0 112 | } 113 | 114 | if (ay) { 115 | dy = -ay * Math.exp(-elapsed / timeConstant) 116 | 117 | if (dy > 0.5 || dy < -0.5) moving = true 118 | else dy = ay = 0 119 | } 120 | 121 | if (moving) { 122 | scrollCallback(targetX + dx, targetY + dy) 123 | raf = window.requestAnimationFrame(autoScroll); 124 | } 125 | } 126 | } 127 | 128 | function noop() { } 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three.map.control", 3 | "version": "1.6.0", 4 | "description": "Mobile friendly three.js camera that mimics 2d maps navigation with pan and zoom", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "browserify demo/index.js > demo/bundle.js" 9 | }, 10 | "keywords": [ 11 | "three.js", 12 | "map", 13 | "control", 14 | "camera", 15 | "pan", 16 | "zoom", 17 | "scroll", 18 | "wheel" 19 | ], 20 | "author": "Andrei Kashcha", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/anvaka/three.map.control" 25 | }, 26 | "dependencies": { 27 | "amator": "^1.0.1", 28 | "ngraph.events": "0.0.4", 29 | "wheel": "0.0.4" 30 | }, 31 | "devDependencies": { 32 | "three": "^0.79.0" 33 | } 34 | } 35 | --------------------------------------------------------------------------------