├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------