Skip to content

Commit

Permalink
Refactor Drag{Pan,Rotate}Handler to use camera animation
Browse files Browse the repository at this point in the history
Instead of updating the transform directly within the mousemove handler,
we cede control to the render loop by doing our transform updates in the
callback we pass to `Camera#_startAnimation`.  This way, we synchronously
update the transform, render the map, and fire the `move` event (and
thus trigger any listeners that might perform DOM updates).
  • Loading branch information
Anand Thakker committed Jan 19, 2018
1 parent b1bb51a commit 9c46380
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 43 deletions.
2 changes: 1 addition & 1 deletion src/ui/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ class Camera extends Evented {
* @param finish A callback that is called when this animation is stopped (i.e., when `Camera#stop()` is called).
*/
_startAnimation(onFrame: (Transform) => void,
finish: () => void): this {
finish: () => void = () => {}): this {
this.stop();
this._onFrame = onFrame;
this._finishFn = finish;
Expand Down
58 changes: 42 additions & 16 deletions src/ui/handler/drag_pan.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const browser = require('../../util/browser');

import type Map from '../map';
import type Point from '@mapbox/point-geometry';
import type Transform from '../../geo/transform';

const inertiaLinearity = 0.3,
inertiaEasing = util.bezier(0, 0, inertiaLinearity, 1),
Expand All @@ -25,8 +26,10 @@ class DragPanHandler {
_enabled: boolean;
_active: boolean;
_pos: Point;
_previousPos: Point;
_startPos: Point;
_inertia: Array<[number, Point]>;
_lastMoveEvent: MouseEvent | TouchEvent | void;

constructor(map: Map) {
this._map = map;
Expand All @@ -37,7 +40,9 @@ class DragPanHandler {
'_onMove',
'_onUp',
'_onTouchEnd',
'_onMouseUp'
'_onMouseUp',
'_onDragFrame',
'_onDragFinished'
], this);
}

Expand Down Expand Up @@ -102,41 +107,58 @@ class DragPanHandler {
window.addEventListener('blur', this._onMouseUp);

this._active = false;
this._startPos = this._pos = DOM.mousePos(this._el, e);
this._startPos = this._previousPos = DOM.mousePos(this._el, e);
this._inertia = [[browser.now(), this._pos]];
}

_onMove(e: MouseEvent | TouchEvent) {
if (this._ignoreEvent(e)) return;
this._lastMoveEvent = e;
e.preventDefault();

this._pos = DOM.mousePos(this._el, e);
this._drainInertiaBuffer();
this._inertia.push([browser.now(), this._pos]);

if (!this.isActive()) {
// we treat the first move event (rather than the mousedown event)
// as the start of the drag
this._active = true;
this._map.moving = true;
this._fireEvent('dragstart', e);
this._fireEvent('movestart', e);
}

const pos = DOM.mousePos(this._el, e),
map = this._map;

map.stop();
this._drainInertiaBuffer();
this._inertia.push([browser.now(), pos]);
this._map._startAnimation(this._onDragFrame, this._onDragFinished);
}
}

map.transform.setLocationAtPoint(map.transform.pointLocation(this._pos), pos);
/**
* Called in each render frame while dragging is happening.
* @private
*/
_onDragFrame(tr: Transform) {
const e = this._lastMoveEvent;
if (!e) return;

tr.setLocationAtPoint(tr.pointLocation(this._previousPos), this._pos);
this._fireEvent('drag', e);
this._fireEvent('move', e);

this._pos = pos;

e.preventDefault();
this._previousPos = this._pos;
delete this._lastMoveEvent;
}

_onUp(e: MouseEvent | TouchEvent | FocusEvent) {
/**
* Called when dragging stops.
* @private
*/
_onDragFinished(e: MouseEvent | TouchEvent | FocusEvent | void) {
if (!this.isActive()) return;

this._active = false;
delete this._lastMoveEvent;
delete this._previousPos;

this._fireEvent('dragend', e);
this._drainInertiaBuffer();

Expand Down Expand Up @@ -180,6 +202,10 @@ class DragPanHandler {
}, { originalEvent: e });
}

_onUp(e: MouseEvent | TouchEvent | FocusEvent) {
this._onDragFinished(e);
}

_onMouseUp(e: MouseEvent | FocusEvent) {
if (this._ignoreEvent(e)) return;
this._onUp(e);
Expand All @@ -195,8 +221,8 @@ class DragPanHandler {
window.document.removeEventListener('touchend', this._onTouchEnd);
}

_fireEvent(type: string, e: Event) {
return this._map.fire(type, { originalEvent: e });
_fireEvent(type: string, e: ?Event) {
return this._map.fire(type, e ? { originalEvent: e } : {});
}

_ignoreEvent(e: any) {
Expand Down
63 changes: 42 additions & 21 deletions src/ui/handler/drag_rotate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const browser = require('../../util/browser');

import type Map from '../map';
import type Point from '@mapbox/point-geometry';
import type Transform from '../../geo/transform';

const inertiaLinearity = 0.25,
inertiaEasing = util.bezier(0, 0, inertiaLinearity, 1),
Expand All @@ -31,7 +32,10 @@ class DragRotateHandler {
_button: 'right' | 'left';
_bearingSnap: number;
_pitchWithRotate: boolean;

_lastMoveEvent: MouseEvent;
_pos: Point;
_previousPos: Point;
_startPos: Point;
_inertia: Array<[number, number]>;
_center: Point;
Expand All @@ -51,7 +55,9 @@ class DragRotateHandler {
util.bindAll([
'_onDown',
'_onMove',
'_onUp'
'_onUp',
'_onDragFrame',
'_onDragFinished'
], this);
}

Expand Down Expand Up @@ -126,13 +132,16 @@ class DragRotateHandler {

this._active = false;
this._inertia = [[browser.now(), this._map.getBearing()]];
this._startPos = this._pos = DOM.mousePos(this._el, e);
this._startPos = this._previousPos = DOM.mousePos(this._el, e);
this._center = this._map.transform.centerPoint; // Center of rotation

e.preventDefault();
}

_onMove(e: MouseEvent) {
this._lastMoveEvent = e;
this._pos = DOM.mousePos(this._el, e);

if (!this.isActive()) {
this._active = true;
this._map.moving = true;
Expand All @@ -141,45 +150,57 @@ class DragRotateHandler {
if (this._pitchWithRotate) {
this._fireEvent('pitchstart', e);
}

this._map._startAnimation(this._onDragFrame, this._onDragFinished);
}
}

const map = this._map;
map.stop();
_onUp(e: MouseEvent | FocusEvent) {
window.document.removeEventListener('mousemove', this._onMove, {capture: true});
window.document.removeEventListener('mouseup', this._onUp);
window.removeEventListener('blur', this._onUp);

const p1 = this._pos,
p2 = DOM.mousePos(this._el, e),
DOM.enableDrag();

this._onDragFinished(e);
}

_onDragFrame(tr: Transform) {
const e = this._lastMoveEvent;
if (!e) return;

const p1 = this._previousPos,
p2 = this._pos,
bearingDiff = (p1.x - p2.x) * 0.8,
pitchDiff = (p1.y - p2.y) * -0.5,
bearing = map.getBearing() - bearingDiff,
pitch = map.getPitch() - pitchDiff,
bearing = tr.bearing - bearingDiff,
pitch = tr.pitch - pitchDiff,
inertia = this._inertia,
last = inertia[inertia.length - 1];

this._drainInertiaBuffer();
inertia.push([browser.now(), map._normalizeBearing(bearing, last[1])]);
inertia.push([browser.now(), this._map._normalizeBearing(bearing, last[1])]);

map.transform.bearing = bearing;
tr.bearing = bearing;
if (this._pitchWithRotate) {
this._fireEvent('pitch', e);
map.transform.pitch = pitch;
tr.pitch = pitch;
}

this._fireEvent('rotate', e);
this._fireEvent('move', e);

this._pos = p2;
delete this._lastMoveEvent;
this._previousPos = this._pos;
}

_onUp(e: MouseEvent | FocusEvent) {
window.document.removeEventListener('mousemove', this._onMove, {capture: true});
window.document.removeEventListener('mouseup', this._onUp);
window.removeEventListener('blur', this._onUp);

DOM.enableDrag();

_onDragFinished(e: MouseEvent | FocusEvent | void) {
if (!this.isActive()) return;

this._active = false;
delete this._lastMoveEvent;
delete this._previousPos;

this._fireEvent('rotateend', e);
this._drainInertiaBuffer();

Expand Down Expand Up @@ -236,8 +257,8 @@ class DragRotateHandler {
}, { originalEvent: e });
}

_fireEvent(type: string, e: Event) {
return this._map.fire(type, { originalEvent: e });
_fireEvent(type: string, e: ?Event) {
return this._map.fire(type, e ? { originalEvent: e } : {});
}

_drainInertiaBuffer() {
Expand Down
27 changes: 22 additions & 5 deletions test/unit/ui/handler/drag_rotate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ test('DragRotateHandler rotates in response to a right-click drag', (t) => {

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
map._updateCamera();

t.ok(rotatestart.calledOnce);
t.ok(rotate.calledOnce);

Expand All @@ -44,16 +46,19 @@ test('DragRotateHandler rotates in response to a right-click drag', (t) => {
test('DragRotateHandler stops rotating after mouseup', (t) => {
const map = createMap();

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
simulate.mouseup(map.getCanvas(), {bubbles: true, buttons: 0, button: 2});

const spy = t.spy();

map.on('rotatestart', spy);
map.on('rotate', spy);
map.on('rotateend', spy);

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
map._updateCamera();
simulate.mouseup(map.getCanvas(), {bubbles: true, buttons: 0, button: 2});

t.ok(spy.calledThrice);

spy.reset();
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 0});

t.ok(spy.notCalled);
Expand All @@ -73,6 +78,7 @@ test('DragRotateHandler rotates in response to a control-left-click drag', (t) =

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 1, button: 0, ctrlKey: true});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 1, ctrlKey: true});
map._updateCamera();
t.ok(rotatestart.calledOnce);
t.ok(rotate.calledOnce);

Expand All @@ -95,6 +101,7 @@ test('DragRotateHandler pitches in response to a right-click drag by default', (

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
map._updateCamera();
t.ok(pitchstart.calledOnce);
t.ok(pitch.calledOnce);

Expand All @@ -117,6 +124,7 @@ test('DragRotateHandler pitches in response to a control-left-click drag', (t) =

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 1, button: 0, ctrlKey: true});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 1, ctrlKey: true});
map._updateCamera();
t.ok(pitchstart.calledOnce);
t.ok(pitch.calledOnce);

Expand All @@ -137,10 +145,12 @@ test('DragRotateHandler does not pitch if given pitchWithRotate: false', (t) =>

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
map._updateCamera();
simulate.mouseup(map.getCanvas(), {bubbles: true, buttons: 0, button: 2});

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 1, button: 0, ctrlKey: true});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 1, ctrlKey: true});
map._updateCamera();
simulate.mouseup(map.getCanvas(), {bubbles: true, buttons: 0, button: 0, ctrlKey: true});

t.ok(spy.notCalled);
Expand All @@ -163,6 +173,7 @@ test('DragRotateHandler does not rotate or pitch when disabled', (t) => {

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
map._updateCamera();
simulate.mouseup(map.getCanvas(), {bubbles: true, buttons: 0, button: 2});

t.ok(spy.notCalled);
Expand Down Expand Up @@ -197,6 +208,7 @@ test('DragRotateHandler fires move events', (t) => {

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
map._updateCamera();
t.ok(movestart.calledOnce);
t.ok(move.calledOnce);

Expand Down Expand Up @@ -233,6 +245,7 @@ test('DragRotateHandler includes originalEvent property in triggered events', (t

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
map._updateCamera();
simulate.mouseup(map.getCanvas(), {bubbles: true, buttons: 0, button: 2});

t.ok(rotatestart.firstCall.args[0].originalEvent.type, 'mousemove');
Expand Down Expand Up @@ -263,6 +276,7 @@ test('DragRotateHandler responds to events on the canvas container (#1301)', (t)

simulate.mousedown(map.getCanvasContainer(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvasContainer(), {bubbles: true, buttons: 2});
map._updateCamera();
t.ok(rotatestart.calledOnce);
t.ok(rotate.calledOnce);

Expand All @@ -280,6 +294,7 @@ test('DragRotateHandler prevents mousemove events from firing during a drag (#15

simulate.mousedown(map.getCanvasContainer(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvasContainer(), {bubbles: true, buttons: 2});
map._updateCamera();
simulate.mouseup(map.getCanvasContainer(), {bubbles: true, buttons: 0, button: 2});

t.ok(mousemove.notCalled);
Expand All @@ -299,6 +314,7 @@ test('DragRotateHandler ends a control-left-click drag on mouseup even when the

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 1, button: 0, ctrlKey: true});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 1, ctrlKey: true});
map._updateCamera();
t.ok(rotatestart.calledOnce);
t.ok(rotate.calledOnce);

Expand All @@ -321,6 +337,7 @@ test('DragRotateHandler ends rotation if the window blurs (#3389)', (t) => {

simulate.mousedown(map.getCanvas(), {bubbles: true, buttons: 2, button: 2});
simulate.mousemove(map.getCanvas(), {bubbles: true, buttons: 2});
map._updateCamera();
t.ok(rotatestart.calledOnce);
t.ok(rotate.calledOnce);

Expand Down

0 comments on commit 9c46380

Please sign in to comment.