mapbox-gl
Version:
A WebGL interactive maps library
671 lines (547 loc) • 28.1 kB
JavaScript
// @flow
import {Event} from '../util/evented.js';
import DOM from '../util/dom.js';
import type Map from './map.js';
import HandlerInertia from './handler_inertia.js';
import {MapEventHandler, BlockableMapEventHandler} from './handler/map_event.js';
import BoxZoomHandler from './handler/box_zoom.js';
import TapZoomHandler from './handler/tap_zoom.js';
import {MousePanHandler, MouseRotateHandler, MousePitchHandler} from './handler/mouse.js';
import TouchPanHandler from './handler/touch_pan.js';
import {TouchZoomHandler, TouchRotateHandler, TouchPitchHandler} from './handler/touch_zoom_rotate.js';
import KeyboardHandler from './handler/keyboard.js';
import ScrollZoomHandler from './handler/scroll_zoom.js';
import DoubleClickZoomHandler from './handler/shim/dblclick_zoom.js';
import ClickZoomHandler from './handler/click_zoom.js';
import TapDragZoomHandler from './handler/tap_drag_zoom.js';
import DragPanHandler from './handler/shim/drag_pan.js';
import DragRotateHandler from './handler/shim/drag_rotate.js';
import TouchZoomRotateHandler from './handler/shim/touch_zoom_rotate.js';
import {bindAll, extend} from '../util/util.js';
import window from '../util/window.js';
import Point from '@mapbox/point-geometry';
import assert from 'assert';
import {vec3} from 'gl-matrix';
import MercatorCoordinate, {altitudeFromMercatorZ} from '../geo/mercator_coordinate.js';
export type InputEvent = MouseEvent | TouchEvent | KeyboardEvent | WheelEvent;
const isMoving = p => p.zoom || p.drag || p.pitch || p.rotate;
class RenderFrameEvent extends Event {
type: 'renderFrame';
timeStamp: number;
}
class TrackingEllipsoid {
constants: Array<number>;
radius: number;
constructor() {
// a, b, c in the equation x²/a² + y²/b² + z²/c² = 1
this.constants = [1, 1, 0.01];
this.radius = 0;
}
setup(center: vec3, pointOnSurface: vec3) {
const centerToSurface = vec3.sub([], pointOnSurface, center);
if (centerToSurface[2] < 0) {
this.radius = vec3.length(vec3.div([], centerToSurface, this.constants));
} else {
// The point on surface is above the center. This can happen for example when the camera is
// below the clicked point (like a mountain) Use slightly shorter radius for less aggressive movement
this.radius = vec3.length([centerToSurface[0], centerToSurface[1], 0]);
}
}
// Cast a ray from the center of the ellipsoid and the intersection point.
projectRay(dir: vec3): vec3 {
// Perform the intersection test against a unit sphere
vec3.div(dir, dir, this.constants);
vec3.normalize(dir, dir);
vec3.mul(dir, dir, this.constants);
const intersection = vec3.scale([], dir, this.radius);
if (intersection[2] > 0) {
// The intersection point is above horizon so special handling is required.
// Otherwise direction of the movement would be inverted due to the ellipsoid shape
const h = vec3.scale([], [0, 0, 1], vec3.dot(intersection, [0, 0, 1]));
const r = vec3.scale([], vec3.normalize([], [intersection[0], intersection[1], 0]), this.radius);
const p = vec3.add([], intersection, vec3.scale([], vec3.sub([], vec3.add([], r, h), intersection), 2));
intersection[0] = p[0];
intersection[1] = p[1];
}
return intersection;
}
}
// Handlers interpret dom events and return camera changes that should be
// applied to the map (`HandlerResult`s). The camera changes are all deltas.
// The handler itself should have no knowledge of the map's current state.
// This makes it easier to merge multiple results and keeps handlers simpler.
// For example, if there is a mousedown and mousemove, the mousePan handler
// would return a `panDelta` on the mousemove.
export interface Handler {
enable(): void;
disable(): void;
isEnabled(): boolean;
isActive(): boolean;
// `reset` can be called by the manager at any time and must reset everything to it's original state
reset(): void;
// Handlers can optionally implement these methods.
// They are called with dom events whenever those dom evens are received.
+touchstart?: (e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) => HandlerResult | void;
+touchmove?: (e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) => HandlerResult | void;
+touchend?: (e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) => HandlerResult | void;
+touchcancel?: (e: TouchEvent, points: Array<Point>, mapTouches: Array<Touch>) => HandlerResult | void;
+mousedown?: (e: MouseEvent, point: Point) => HandlerResult | void;
+mousemove?: (e: MouseEvent, point: Point) => HandlerResult | void;
+mouseup?: (e: MouseEvent, point: Point) => HandlerResult | void;
+dblclick?: (e: MouseEvent, point: Point) => HandlerResult | void;
+wheel?: (e: WheelEvent, point: Point) => HandlerResult | void;
+keydown?: (e: KeyboardEvent) => HandlerResult | void;
+keyup?: (e: KeyboardEvent) => HandlerResult | void;
// `renderFrame` is the only non-dom event. It is called during render
// frames and can be used to smooth camera changes (see scroll handler).
+renderFrame?: () => HandlerResult | void;
}
// All handler methods that are called with events can optionally return a `HandlerResult`.
export type HandlerResult = {|
panDelta?: Point,
zoomDelta?: number,
bearingDelta?: number,
pitchDelta?: number,
// the point to not move when changing the camera
around?: Point | null,
// same as above, except for pinch actions, which are given higher priority
pinchAround?: Point | null,
// the point to not move when changing the camera in mercator coordinates
aroundCoord?: MercatorCoordinate | null,
// A method that can fire a one-off easing by directly changing the map's camera.
cameraAnimation?: (map: Map) => any;
// The last three properties are needed by only one handler: scrollzoom.
// The DOM event to be used as the `originalEvent` on any camera change events.
originalEvent?: any,
// Makes the manager trigger a frame, allowing the handler to return multiple results over time (see scrollzoom).
needsRenderFrame?: boolean,
// The camera changes won't get recorded for inertial zooming.
noInertia?: boolean
|};
function hasChange(result: HandlerResult) {
return (result.panDelta && result.panDelta.mag()) || result.zoomDelta || result.bearingDelta || result.pitchDelta;
}
class HandlerManager {
_map: Map;
_el: HTMLElement;
_handlers: Array<{ handlerName: string, handler: Handler, allowed: any }>;
_eventsInProgress: Object;
_frameId: number;
_inertia: HandlerInertia;
_bearingSnap: number;
_handlersById: { [string]: Handler };
_updatingCamera: boolean;
_changes: Array<[HandlerResult, Object, any]>;
_previousActiveHandlers: { [string]: Handler };
_listeners: Array<[HTMLElement, string, void | {passive?: boolean, capture?: boolean}]>;
_trackingEllipsoid: TrackingEllipsoid;
_dragOrigin: ?vec3;
constructor(map: Map, options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number, bearingSnap: number}) {
this._map = map;
this._el = this._map.getCanvasContainer();
this._handlers = [];
this._handlersById = {};
this._changes = [];
this._inertia = new HandlerInertia(map);
this._bearingSnap = options.bearingSnap;
this._previousActiveHandlers = {};
this._trackingEllipsoid = new TrackingEllipsoid();
this._dragOrigin = null;
// Track whether map is currently moving, to compute start/move/end events
this._eventsInProgress = {};
this._addDefaultHandlers(options);
bindAll(['handleEvent', 'handleWindowEvent'], this);
const el = this._el;
this._listeners = [
// This needs to be `passive: true` so that a double tap fires two
// pairs of touchstart/end events in iOS Safari 13. If this is set to
// `passive: false` then the second pair of events is only fired if
// preventDefault() is called on the first touchstart. Calling preventDefault()
// undesirably prevents click events.
[el, 'touchstart', {passive: true}],
// This needs to be `passive: false` so that scrolls and pinches can be
// prevented in browsers that don't support `touch-actions: none`, for example iOS Safari 12.
[el, 'touchmove', {passive: false}],
[el, 'touchend', undefined],
[el, 'touchcancel', undefined],
[el, 'mousedown', undefined],
[el, 'mousemove', undefined],
[el, 'mouseup', undefined],
// Bind window-level event listeners for move and up/end events. In the absence of
// the pointer capture API, which is not supported by all necessary platforms,
// window-level event listeners give us the best shot at capturing events that
// fall outside the map canvas element. Use `{capture: true}` for the move event
// to prevent map move events from being fired during a drag.
[window.document, 'mousemove', {capture: true}],
[window.document, 'mouseup', undefined],
[el, 'mouseover', undefined],
[el, 'mouseout', undefined],
[el, 'dblclick', undefined],
[el, 'click', undefined],
[el, 'keydown', {capture: false}],
[el, 'keyup', undefined],
[el, 'wheel', {passive: false}],
[el, 'contextmenu', undefined],
[window, 'blur', undefined]
];
for (const [target, type, listenerOptions] of this._listeners) {
DOM.addEventListener(target, type, target === window.document ? this.handleWindowEvent : this.handleEvent, listenerOptions);
}
}
destroy() {
for (const [target, type, listenerOptions] of this._listeners) {
DOM.removeEventListener(target, type, target === window.document ? this.handleWindowEvent : this.handleEvent, listenerOptions);
}
}
_addDefaultHandlers(options: { interactive: boolean, pitchWithRotate: boolean, clickTolerance: number }) {
const map = this._map;
const el = map.getCanvasContainer();
this._add('mapEvent', new MapEventHandler(map, options));
const boxZoom = map.boxZoom = new BoxZoomHandler(map, options);
this._add('boxZoom', boxZoom);
const tapZoom = new TapZoomHandler();
const clickZoom = new ClickZoomHandler();
map.doubleClickZoom = new DoubleClickZoomHandler(clickZoom, tapZoom);
this._add('tapZoom', tapZoom);
this._add('clickZoom', clickZoom);
const tapDragZoom = new TapDragZoomHandler();
this._add('tapDragZoom', tapDragZoom);
const touchPitch = map.touchPitch = new TouchPitchHandler();
this._add('touchPitch', touchPitch);
const mouseRotate = new MouseRotateHandler(options);
const mousePitch = new MousePitchHandler(options);
map.dragRotate = new DragRotateHandler(options, mouseRotate, mousePitch);
this._add('mouseRotate', mouseRotate, ['mousePitch']);
this._add('mousePitch', mousePitch, ['mouseRotate']);
const mousePan = new MousePanHandler(options);
const touchPan = new TouchPanHandler(options);
map.dragPan = new DragPanHandler(el, mousePan, touchPan);
this._add('mousePan', mousePan);
this._add('touchPan', touchPan, ['touchZoom', 'touchRotate']);
const touchRotate = new TouchRotateHandler();
const touchZoom = new TouchZoomHandler();
map.touchZoomRotate = new TouchZoomRotateHandler(el, touchZoom, touchRotate, tapDragZoom);
this._add('touchRotate', touchRotate, ['touchPan', 'touchZoom']);
this._add('touchZoom', touchZoom, ['touchPan', 'touchRotate']);
this._add('blockableMapEvent', new BlockableMapEventHandler(map));
const scrollZoom = map.scrollZoom = new ScrollZoomHandler(map, this);
this._add('scrollZoom', scrollZoom, ['mousePan']);
const keyboard = map.keyboard = new KeyboardHandler();
this._add('keyboard', keyboard);
for (const name of ['boxZoom', 'doubleClickZoom', 'tapDragZoom', 'touchPitch', 'dragRotate', 'dragPan', 'touchZoomRotate', 'scrollZoom', 'keyboard']) {
if (options.interactive && (options: any)[name]) {
(map: any)[name].enable((options: any)[name]);
}
}
}
_add(handlerName: string, handler: Handler, allowed?: Array<string>) {
this._handlers.push({handlerName, handler, allowed});
this._handlersById[handlerName] = handler;
}
stop(allowEndAnimation: boolean) {
// do nothing if this method was triggered by a gesture update
if (this._updatingCamera) return;
for (const {handler} of this._handlers) {
handler.reset();
}
this._inertia.clear();
this._fireEvents({}, {}, allowEndAnimation);
this._changes = [];
}
isActive() {
for (const {handler} of this._handlers) {
if (handler.isActive()) return true;
}
return false;
}
isZooming() {
return !!this._eventsInProgress.zoom || this._map.scrollZoom.isZooming();
}
isRotating() {
return !!this._eventsInProgress.rotate;
}
isMoving() {
return Boolean(isMoving(this._eventsInProgress)) || this.isZooming();
}
_blockedByActive(activeHandlers: { [string]: Handler }, allowed: Array<string>, myName: string) {
for (const name in activeHandlers) {
if (name === myName) continue;
if (!allowed || allowed.indexOf(name) < 0) {
return true;
}
}
return false;
}
handleWindowEvent(e: InputEvent) {
this.handleEvent(e, `${e.type}Window`);
}
_getMapTouches(touches: TouchList) {
const mapTouches = [];
for (const t of touches) {
const target = ((t.target: any): Node);
if (this._el.contains(target)) {
mapTouches.push(t);
}
}
return ((mapTouches: any): TouchList);
}
handleEvent(e: InputEvent | RenderFrameEvent, eventName?: string) {
if (e.type === 'blur') {
this.stop(true);
return;
}
this._updatingCamera = true;
assert(e.timeStamp !== undefined);
const isRenderFrame = e.type === 'renderFrame';
const inputEvent = isRenderFrame ? undefined : ((e: any): InputEvent);
/*
* We don't call e.preventDefault() for any events by default.
* Handlers are responsible for calling it where necessary.
*/
const mergedHandlerResult: HandlerResult = {needsRenderFrame: false};
const eventsInProgress = {};
const activeHandlers = {};
const mapTouches = e.touches ? this._getMapTouches(((e: any): TouchEvent).touches) : undefined;
const points = mapTouches ? DOM.touchPos(this._el, mapTouches) :
isRenderFrame ? undefined : // renderFrame event doesn't have any points
DOM.mousePos(this._el, ((e: any): MouseEvent));
for (const {handlerName, handler, allowed} of this._handlers) {
if (!handler.isEnabled()) continue;
let data: HandlerResult | void;
if (this._blockedByActive(activeHandlers, allowed, handlerName)) {
handler.reset();
} else {
if ((handler: any)[eventName || e.type]) {
data = (handler: any)[eventName || e.type](e, points, mapTouches);
this.mergeHandlerResult(mergedHandlerResult, eventsInProgress, data, handlerName, inputEvent);
if (data && data.needsRenderFrame) {
this._triggerRenderFrame();
}
}
}
if (data || handler.isActive()) {
activeHandlers[handlerName] = handler;
}
}
const deactivatedHandlers = {};
for (const name in this._previousActiveHandlers) {
if (!activeHandlers[name]) {
deactivatedHandlers[name] = inputEvent;
}
}
this._previousActiveHandlers = activeHandlers;
if (Object.keys(deactivatedHandlers).length || hasChange(mergedHandlerResult)) {
this._changes.push([mergedHandlerResult, eventsInProgress, deactivatedHandlers]);
this._triggerRenderFrame();
}
if (Object.keys(activeHandlers).length || hasChange(mergedHandlerResult)) {
this._map._stop(true);
}
this._updatingCamera = false;
const {cameraAnimation} = mergedHandlerResult;
if (cameraAnimation) {
this._inertia.clear();
this._fireEvents({}, {}, true);
this._changes = [];
cameraAnimation(this._map);
}
}
mergeHandlerResult(mergedHandlerResult: HandlerResult, eventsInProgress: Object, handlerResult: HandlerResult, name: string, e?: InputEvent) {
if (!handlerResult) return;
extend(mergedHandlerResult, handlerResult);
const eventData = {handlerName: name, originalEvent: handlerResult.originalEvent || e};
// track which handler changed which camera property
if (handlerResult.zoomDelta !== undefined) {
eventsInProgress.zoom = eventData;
}
if (handlerResult.panDelta !== undefined) {
eventsInProgress.drag = eventData;
}
if (handlerResult.pitchDelta !== undefined) {
eventsInProgress.pitch = eventData;
}
if (handlerResult.bearingDelta !== undefined) {
eventsInProgress.rotate = eventData;
}
}
_applyChanges() {
const combined = {};
const combinedEventsInProgress = {};
const combinedDeactivatedHandlers = {};
for (const [change, eventsInProgress, deactivatedHandlers] of this._changes) {
if (change.panDelta) combined.panDelta = (combined.panDelta || new Point(0, 0))._add(change.panDelta);
if (change.zoomDelta) combined.zoomDelta = (combined.zoomDelta || 0) + change.zoomDelta;
if (change.bearingDelta) combined.bearingDelta = (combined.bearingDelta || 0) + change.bearingDelta;
if (change.pitchDelta) combined.pitchDelta = (combined.pitchDelta || 0) + change.pitchDelta;
if (change.around !== undefined) combined.around = change.around;
if (change.aroundCoord !== undefined) combined.aroundCoord = change.aroundCoord;
if (change.pinchAround !== undefined) combined.pinchAround = change.pinchAround;
if (change.noInertia) combined.noInertia = change.noInertia;
extend(combinedEventsInProgress, eventsInProgress);
extend(combinedDeactivatedHandlers, deactivatedHandlers);
}
this._updateMapTransform(combined, combinedEventsInProgress, combinedDeactivatedHandlers);
this._changes = [];
}
_updateMapTransform(combinedResult: any, combinedEventsInProgress: Object, deactivatedHandlers: Object) {
const map = this._map;
const tr = map.transform;
const eventStarted = (type) => {
const newEvent = combinedEventsInProgress[type];
return newEvent && !this._eventsInProgress[type];
};
const eventEnded = (type) => {
const event = this._eventsInProgress[type];
return event && !this._handlersById[event.handlerName].isActive();
};
const toVec3 = (p: MercatorCoordinate): vec3 => [p.x, p.y, p.z];
if (eventEnded("drag") && !hasChange(combinedResult)) {
const preZoom = tr.zoom;
tr.cameraElevationReference = "sea";
tr.recenterOnTerrain();
tr.cameraElevationReference = "ground";
// Map zoom might change during the pan operation due to terrain elevation.
if (preZoom !== tr.zoom) this._map._update(true);
}
if (!hasChange(combinedResult)) {
return this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true);
}
let {panDelta, zoomDelta, bearingDelta, pitchDelta, around, aroundCoord, pinchAround} = combinedResult;
if (pinchAround !== undefined) {
around = pinchAround;
}
if (eventStarted("drag") && around) {
this._dragOrigin = toVec3(tr.pointCoordinate3D(around));
// Construct the tracking ellipsoid every time user changes the drag origin.
// Direction of the ray will define size of the shape and hence defining the available range of movement
this._trackingEllipsoid.setup(tr._camera.position, this._dragOrigin);
}
// All movement of the camera is done relative to the sea level
tr.cameraElevationReference = "sea";
// stop any ongoing camera animations (easeTo, flyTo)
map._stop(true);
around = around || map.transform.centerPoint;
if (bearingDelta) tr.bearing += bearingDelta;
if (pitchDelta) tr.pitch += pitchDelta;
tr._updateCameraState();
// Compute Mercator 3D camera offset based on screenspace panDelta
const panVec = [0, 0, 0];
if (panDelta) {
assert(this._dragOrigin, '_dragOrigin should have been setup with a previous dragstart');
const startRay = tr.screenPointToMercatorRay(around);
const endRay = tr.screenPointToMercatorRay(around.sub(panDelta));
const startPoint = this._trackingEllipsoid.projectRay(startRay.dir);
const endPoint = this._trackingEllipsoid.projectRay(endRay.dir);
panVec[0] = endPoint[0] - startPoint[0];
panVec[1] = endPoint[1] - startPoint[1];
}
const originalZoom = tr.zoom;
// Compute Mercator 3D camera offset based on screenspace requested ZoomDelta
const zoomVec = [0, 0, 0];
if (zoomDelta) {
// Zoom value has to be computed relative to a secondary map plane that is created from the terrain position below the cursor.
// This way the zoom interpolation can be kept linear and independent of the (possible) terrain elevation
const pickedPosition: vec3 = aroundCoord ? toVec3(aroundCoord) : toVec3(tr.pointCoordinate3D(around));
const aroundRay = {dir: vec3.normalize([], vec3.sub([], pickedPosition, tr._camera.position))};
const centerRay = tr.screenPointToMercatorRay(tr.centerPoint);
if (aroundRay.dir[2] < 0) {
// Compute center point on the elevated map plane by casting a ray from the center of the screen.
// ZoomDelta is then subtracted from the relative zoom value and converted to a movement vector
const pickedAltitude = altitudeFromMercatorZ(pickedPosition[2], pickedPosition[1]);
const centerOnTargetPlane = tr.rayIntersectionCoordinate(tr.pointRayIntersection(tr.centerPoint, pickedAltitude));
const movement = tr.zoomDeltaToMovement(toVec3(centerOnTargetPlane), zoomDelta) * (centerRay.dir[2] / aroundRay.dir[2]);
vec3.scale(zoomVec, aroundRay.dir, movement);
} else if (tr._terrainEnabled()) {
// Special handling is required if the ray created from the cursor is heading up.
// This scenario is possible if user is trying to zoom towards e.g. a hill or a mountain.
// Convert zoomDelta to a movement vector as if the camera would be orbiting around the picked point
const movement = tr.zoomDeltaToMovement(pickedPosition, zoomDelta);
vec3.scale(zoomVec, aroundRay.dir, movement);
}
}
// Mutate camera state via CameraAPI
const translation = vec3.add(panVec, panVec, zoomVec);
tr._translateCameraConstrained(translation);
if (zoomDelta && Math.abs(tr.zoom - originalZoom) > 0.0001) {
tr.recenterOnTerrain();
}
tr.cameraElevationReference = "ground";
this._map._update();
if (!combinedResult.noInertia) this._inertia.record(combinedResult);
this._fireEvents(combinedEventsInProgress, deactivatedHandlers, true);
}
_fireEvents(newEventsInProgress: { [string]: Object }, deactivatedHandlers: Object, allowEndAnimation: boolean) {
const wasMoving = isMoving(this._eventsInProgress);
const nowMoving = isMoving(newEventsInProgress);
const startEvents = {};
for (const eventName in newEventsInProgress) {
const {originalEvent} = newEventsInProgress[eventName];
if (!this._eventsInProgress[eventName]) {
startEvents[`${eventName}start`] = originalEvent;
}
this._eventsInProgress[eventName] = newEventsInProgress[eventName];
}
// fire start events only after this._eventsInProgress has been updated
if (!wasMoving && nowMoving) {
this._fireEvent('movestart', nowMoving.originalEvent);
}
for (const name in startEvents) {
this._fireEvent(name, startEvents[name]);
}
if (nowMoving) {
this._fireEvent('move', nowMoving.originalEvent);
}
for (const eventName in newEventsInProgress) {
const {originalEvent} = newEventsInProgress[eventName];
this._fireEvent(eventName, originalEvent);
}
const endEvents = {};
let originalEndEvent;
for (const eventName in this._eventsInProgress) {
const {handlerName, originalEvent} = this._eventsInProgress[eventName];
if (!this._handlersById[handlerName].isActive()) {
delete this._eventsInProgress[eventName];
originalEndEvent = deactivatedHandlers[handlerName] || originalEvent;
endEvents[`${eventName}end`] = originalEndEvent;
}
}
for (const name in endEvents) {
this._fireEvent(name, endEvents[name]);
}
const stillMoving = isMoving(this._eventsInProgress);
if (allowEndAnimation && (wasMoving || nowMoving) && !stillMoving) {
this._updatingCamera = true;
const inertialEase = this._inertia._onMoveEnd(this._map.dragPan._inertiaOptions);
const shouldSnapToNorth = bearing => bearing !== 0 && -this._bearingSnap < bearing && bearing < this._bearingSnap;
if (inertialEase) {
if (shouldSnapToNorth(inertialEase.bearing || this._map.getBearing())) {
inertialEase.bearing = 0;
}
this._map.easeTo(inertialEase, {originalEvent: originalEndEvent});
} else {
this._map.fire(new Event('moveend', {originalEvent: originalEndEvent}));
if (shouldSnapToNorth(this._map.getBearing())) {
this._map.resetNorth();
}
}
this._updatingCamera = false;
}
}
_fireEvent(type: string, e: *) {
this._map.fire(new Event(type, e ? {originalEvent: e} : {}));
}
_requestFrame() {
this._map.triggerRepaint();
return this._map._renderTaskQueue.add(timeStamp => {
delete this._frameId;
this.handleEvent(new RenderFrameEvent('renderFrame', {timeStamp}));
this._applyChanges();
});
}
_triggerRenderFrame() {
if (this._frameId === undefined) {
this._frameId = this._requestFrame();
}
}
}
export default HandlerManager;