mirror of
https://github.com/visgl/react-map-gl.git
synced 2026-01-18 15:54:22 +00:00
644 lines
19 KiB
JavaScript
644 lines
19 KiB
JavaScript
// Copyright (c) 2015 Uber Technologies, Inc.
|
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
/* eslint-disable complexity, max-statements */
|
|
import MapState from './map-state';
|
|
import {LinearInterpolator} from './transition';
|
|
import TransitionManager, {TRANSITION_EVENTS} from './transition-manager';
|
|
|
|
const NO_TRANSITION_PROPS = {
|
|
transitionDuration: 0
|
|
};
|
|
export const LINEAR_TRANSITION_PROPS = {
|
|
transitionDuration: 300,
|
|
transitionEasing: t => t,
|
|
transitionInterpolator: new LinearInterpolator(),
|
|
transitionInterruption: TRANSITION_EVENTS.BREAK
|
|
};
|
|
|
|
// EVENT HANDLING PARAMETERS
|
|
const DEFAULT_INERTIA = 300;
|
|
const INERTIA_EASING = t => 1 - (1 - t) * (1 - t);
|
|
|
|
const EVENT_TYPES = {
|
|
WHEEL: ['wheel'],
|
|
PAN: ['panstart', 'panmove', 'panend'],
|
|
PINCH: ['pinchstart', 'pinchmove', 'pinchend'],
|
|
TRIPLE_PAN: ['tripanstart', 'tripanmove', 'tripanend'],
|
|
DOUBLE_TAP: ['doubletap'],
|
|
KEYBOARD: ['keydown']
|
|
};
|
|
|
|
/**
|
|
* @classdesc
|
|
* A class that handles events and updates mercator style viewport parameters
|
|
*/
|
|
export default class MapController {
|
|
events = [];
|
|
scrollZoom = true;
|
|
dragPan = true;
|
|
dragRotate = true;
|
|
doubleClickZoom = true;
|
|
touchZoom = true;
|
|
touchRotate = false;
|
|
keyboard = true;
|
|
|
|
_interactionState = {
|
|
isDragging: false
|
|
};
|
|
_events = {};
|
|
|
|
constructor() {
|
|
this.handleEvent = this.handleEvent.bind(this);
|
|
this._transitionManager = new TransitionManager({
|
|
onViewportChange: this._onTransition,
|
|
onStateChange: this._setInteractionState
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Callback for events
|
|
* @param {object} event - a mjolnir.js Event
|
|
*/
|
|
handleEvent(event) {
|
|
this.mapState = this.getMapState();
|
|
const eventStartBlocked = this._eventStartBlocked;
|
|
|
|
switch (event.type) {
|
|
case 'panstart':
|
|
return eventStartBlocked ? false : this._onPanStart(event);
|
|
case 'panmove':
|
|
return this._onPan(event);
|
|
case 'panend':
|
|
return this._onPanEnd(event);
|
|
case 'pinchstart':
|
|
return eventStartBlocked ? false : this._onPinchStart(event);
|
|
case 'pinchmove':
|
|
return this._onPinch(event);
|
|
case 'pinchend':
|
|
return this._onPinchEnd(event);
|
|
case 'tripanstart':
|
|
return eventStartBlocked ? false : this._onTriplePanStart(event);
|
|
case 'tripanmove':
|
|
return this._onTriplePan(event);
|
|
case 'tripanend':
|
|
return this._onTriplePanEnd(event);
|
|
case 'doubletap':
|
|
return this._onDoubleTap(event);
|
|
case 'wheel':
|
|
return this._onWheel(event);
|
|
case 'keydown':
|
|
return this._onKeyDown(event);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* Event utils */
|
|
// Event object: http://hammerjs.github.io/api/#event-object
|
|
getCenter(event) {
|
|
const {
|
|
offsetCenter: {x, y}
|
|
} = event;
|
|
return [x, y];
|
|
}
|
|
|
|
isFunctionKeyPressed(event) {
|
|
const {srcEvent} = event;
|
|
return Boolean(srcEvent.metaKey || srcEvent.altKey || srcEvent.ctrlKey || srcEvent.shiftKey);
|
|
}
|
|
|
|
// When a multi-touch event ends, e.g. pinch, not all pointers are lifted at the same time.
|
|
// This triggers a brief `pan` event.
|
|
// Calling this method will temporarily disable *start events to avoid conflicting transitions.
|
|
blockEvents(timeout) {
|
|
/* global setTimeout */
|
|
const timer = setTimeout(() => {
|
|
if (this._eventStartBlocked === timer) {
|
|
this._eventStartBlocked = null;
|
|
}
|
|
}, timeout);
|
|
this._eventStartBlocked = timer;
|
|
}
|
|
|
|
/* Callback util */
|
|
// formats map state and invokes callback function
|
|
updateViewport(newMapState, extraProps, interactionState) {
|
|
// Always trigger callback on initial update (resize)
|
|
const oldViewport =
|
|
this.mapState instanceof MapState ? this.mapState.getViewportProps() : this.mapState;
|
|
const newViewport = {...newMapState.getViewportProps(), ...extraProps};
|
|
|
|
const viewStateChanged = Object.keys(newViewport).some(
|
|
key => oldViewport[key] !== newViewport[key]
|
|
);
|
|
|
|
this._state = newMapState.getState();
|
|
this._setInteractionState(interactionState);
|
|
|
|
// viewState has changed
|
|
if (viewStateChanged) {
|
|
this.onViewportChange(newViewport, this._interactionState, oldViewport);
|
|
}
|
|
}
|
|
|
|
_setInteractionState = newState => {
|
|
Object.assign(this._interactionState, newState);
|
|
if (this.onStateChange) {
|
|
this.onStateChange(this._interactionState);
|
|
}
|
|
};
|
|
|
|
_onTransition = (newViewport, oldViewport) => {
|
|
this.onViewportChange(newViewport, this._interactionState, oldViewport);
|
|
};
|
|
|
|
getMapState(overrides) {
|
|
return new MapState({...this.mapStateProps, ...this._state, ...overrides});
|
|
}
|
|
|
|
isDragging() {
|
|
return this._interactionState.isDragging;
|
|
}
|
|
|
|
/**
|
|
* Extract interactivity options
|
|
*/
|
|
setOptions(options) {
|
|
const {
|
|
onViewportChange,
|
|
onStateChange,
|
|
eventManager = this.eventManager,
|
|
|
|
isInteractive = true,
|
|
scrollZoom = this.scrollZoom,
|
|
dragPan = this.dragPan,
|
|
dragRotate = this.dragRotate,
|
|
doubleClickZoom = this.doubleClickZoom,
|
|
touchZoom = this.touchZoom,
|
|
touchRotate = this.touchRotate,
|
|
keyboard = this.keyboard
|
|
} = options;
|
|
|
|
this.onViewportChange = onViewportChange;
|
|
this.onStateChange = onStateChange;
|
|
|
|
const prevOptions = this.mapStateProps || {};
|
|
const dimensionChanged =
|
|
prevOptions.height !== options.height || prevOptions.width !== options.width;
|
|
|
|
this.mapStateProps = options;
|
|
|
|
if (dimensionChanged) {
|
|
// Dimensions changed, normalize the props and fire change event
|
|
this.mapState = prevOptions;
|
|
this.updateViewport(new MapState(options));
|
|
}
|
|
// Update transition
|
|
this._transitionManager.processViewportChange(options);
|
|
|
|
if (this.eventManager !== eventManager) {
|
|
// EventManager has changed
|
|
this.eventManager = eventManager;
|
|
this._events = {};
|
|
this.toggleEvents(this.events, true);
|
|
}
|
|
|
|
// Register/unregister events
|
|
this.toggleEvents(EVENT_TYPES.WHEEL, isInteractive && Boolean(scrollZoom));
|
|
this.toggleEvents(EVENT_TYPES.PAN, isInteractive && Boolean(dragPan || dragRotate));
|
|
this.toggleEvents(EVENT_TYPES.PINCH, isInteractive && Boolean(touchZoom || touchRotate));
|
|
this.toggleEvents(EVENT_TYPES.TRIPLE_PAN, isInteractive && Boolean(touchRotate));
|
|
this.toggleEvents(EVENT_TYPES.DOUBLE_TAP, isInteractive && Boolean(doubleClickZoom));
|
|
this.toggleEvents(EVENT_TYPES.KEYBOARD, isInteractive && Boolean(keyboard));
|
|
|
|
// Interaction toggles
|
|
this.scrollZoom = scrollZoom;
|
|
this.dragPan = dragPan;
|
|
this.dragRotate = dragRotate;
|
|
this.doubleClickZoom = doubleClickZoom;
|
|
this.touchZoom = touchZoom;
|
|
this.touchRotate = touchRotate;
|
|
this.keyboard = keyboard;
|
|
}
|
|
|
|
toggleEvents(eventNames, enabled) {
|
|
if (this.eventManager) {
|
|
eventNames.forEach(eventName => {
|
|
if (this._events[eventName] !== enabled) {
|
|
this._events[eventName] = enabled;
|
|
if (enabled) {
|
|
this.eventManager.on(eventName, this.handleEvent);
|
|
} else {
|
|
this.eventManager.off(eventName, this.handleEvent);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/* Event handlers */
|
|
// Default handler for the `panstart` event.
|
|
_onPanStart(event) {
|
|
const pos = this.getCenter(event);
|
|
this._panRotate = this.isFunctionKeyPressed(event) || event.rightButton;
|
|
const newMapState = this._panRotate
|
|
? this.mapState.rotateStart({pos})
|
|
: this.mapState.panStart({pos});
|
|
this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isDragging: true});
|
|
return true;
|
|
}
|
|
|
|
// Default handler for the `panmove` event.
|
|
_onPan(event) {
|
|
if (!this.isDragging()) {
|
|
return false;
|
|
}
|
|
return this._panRotate ? this._onPanRotate(event) : this._onPanMove(event);
|
|
}
|
|
|
|
// Default handler for the `panend` event.
|
|
_onPanEnd(event) {
|
|
if (!this.isDragging()) {
|
|
return false;
|
|
}
|
|
return this._panRotate ? this._onPanRotateEnd(event) : this._onPanMoveEnd(event);
|
|
}
|
|
|
|
// Default handler for panning to move.
|
|
// Called by `_onPan` when panning without function key pressed.
|
|
_onPanMove(event) {
|
|
if (!this.dragPan) {
|
|
return false;
|
|
}
|
|
const pos = this.getCenter(event);
|
|
const newMapState = this.mapState.pan({pos});
|
|
this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isPanning: true});
|
|
return true;
|
|
}
|
|
|
|
_onPanMoveEnd(event) {
|
|
if (this.dragPan) {
|
|
const {inertia = DEFAULT_INERTIA} = this.dragPan;
|
|
if (inertia && event.velocity) {
|
|
const pos = this.getCenter(event);
|
|
const endPos = [
|
|
pos[0] + (event.velocityX * inertia) / 2,
|
|
pos[1] + (event.velocityY * inertia) / 2
|
|
];
|
|
const newControllerState = this.mapState.pan({pos: endPos}).panEnd();
|
|
this.updateViewport(
|
|
newControllerState,
|
|
{
|
|
...LINEAR_TRANSITION_PROPS,
|
|
transitionDuration: inertia,
|
|
transitionEasing: INERTIA_EASING
|
|
},
|
|
{
|
|
isDragging: false,
|
|
isPanning: true
|
|
}
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
const newMapState = this.mapState.panEnd();
|
|
this.updateViewport(newMapState, null, {
|
|
isDragging: false,
|
|
isPanning: false
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Default handler for panning to rotate.
|
|
// Called by `_onPan` when panning with function key pressed.
|
|
_onPanRotate(event) {
|
|
if (!this.dragRotate) {
|
|
return false;
|
|
}
|
|
|
|
const pos = this.getCenter(event);
|
|
const newMapState = this.mapState.rotate({pos});
|
|
this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isRotating: true});
|
|
return true;
|
|
}
|
|
|
|
_onPanRotateEnd(event) {
|
|
if (this.dragRotate) {
|
|
const {inertia = DEFAULT_INERTIA} = this.dragRotate;
|
|
if (inertia && event.velocity) {
|
|
const pos = this.getCenter(event);
|
|
const endPos = [
|
|
pos[0] + (event.velocityX * inertia) / 2,
|
|
pos[1] + (event.velocityY * inertia) / 2
|
|
];
|
|
const newControllerState = this.mapState.rotate({pos: endPos}).rotateEnd();
|
|
this.updateViewport(
|
|
newControllerState,
|
|
{
|
|
...LINEAR_TRANSITION_PROPS,
|
|
transitionDuration: inertia,
|
|
transitionEasing: INERTIA_EASING
|
|
},
|
|
{
|
|
isDragging: false,
|
|
isRotating: true
|
|
}
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
const newMapState = this.mapState.panEnd();
|
|
this.updateViewport(newMapState, null, {
|
|
isDragging: false,
|
|
isRotating: false
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Default handler for the `wheel` event.
|
|
_onWheel(event) {
|
|
if (!this.scrollZoom) {
|
|
return false;
|
|
}
|
|
|
|
const {speed = 0.01, smooth = false} = this.scrollZoom;
|
|
|
|
event.preventDefault();
|
|
|
|
const pos = this.getCenter(event);
|
|
const {delta} = event;
|
|
|
|
// Map wheel delta to relative scale
|
|
let scale = 2 / (1 + Math.exp(-Math.abs(delta * speed)));
|
|
if (delta < 0 && scale !== 0) {
|
|
scale = 1 / scale;
|
|
}
|
|
|
|
const newMapState = this.mapState.zoom({pos, scale});
|
|
this.updateViewport(
|
|
newMapState,
|
|
{
|
|
...LINEAR_TRANSITION_PROPS,
|
|
transitionInterpolator: new LinearInterpolator({around: pos}),
|
|
transitionDuration: smooth ? 250 : 1
|
|
},
|
|
{
|
|
isPanning: true,
|
|
isZooming: true
|
|
}
|
|
);
|
|
return true;
|
|
}
|
|
|
|
// Default handler for the `pinchstart` event.
|
|
_onPinchStart(event) {
|
|
const pos = this.getCenter(event);
|
|
const newMapState = this.mapState.zoomStart({pos}).rotateStart({pos});
|
|
// hack - hammer's `rotation` field doesn't seem to produce the correct angle
|
|
this._startPinchRotation = event.rotation;
|
|
this._lastPinchEvent = event;
|
|
this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isDragging: true});
|
|
return true;
|
|
}
|
|
|
|
// Default handler for the `pinch` event.
|
|
_onPinch(event) {
|
|
if (!this.isDragging()) {
|
|
return false;
|
|
}
|
|
if (!this.touchZoom && !this.touchRotate) {
|
|
return false;
|
|
}
|
|
|
|
let newMapState = this.mapState;
|
|
if (this.touchZoom) {
|
|
const {scale} = event;
|
|
const pos = this.getCenter(event);
|
|
newMapState = newMapState.zoom({pos, scale});
|
|
}
|
|
if (this.touchRotate) {
|
|
const {rotation} = event;
|
|
newMapState = newMapState.rotate({
|
|
deltaAngleX: this._startPinchRotation - rotation
|
|
});
|
|
}
|
|
|
|
this.updateViewport(newMapState, NO_TRANSITION_PROPS, {
|
|
isDragging: true,
|
|
isPanning: Boolean(this.touchZoom),
|
|
isZooming: Boolean(this.touchZoom),
|
|
isRotating: Boolean(this.touchRotate)
|
|
});
|
|
this._lastPinchEvent = event;
|
|
return true;
|
|
}
|
|
|
|
// Default handler for the `pinchend` event.
|
|
_onPinchEnd(event) {
|
|
if (!this.isDragging()) {
|
|
return false;
|
|
}
|
|
if (this.touchZoom) {
|
|
const {inertia = DEFAULT_INERTIA} = this.touchZoom;
|
|
const {_lastPinchEvent} = this;
|
|
if (inertia && _lastPinchEvent && event.scale !== _lastPinchEvent.scale) {
|
|
const pos = this.getCenter(event);
|
|
let newMapState = this.mapState.rotateEnd();
|
|
const z = Math.log2(event.scale);
|
|
const velocityZ =
|
|
(z - Math.log2(_lastPinchEvent.scale)) / (event.deltaTime - _lastPinchEvent.deltaTime);
|
|
const endScale = Math.pow(2, z + (velocityZ * inertia) / 2);
|
|
newMapState = newMapState.zoom({pos, scale: endScale}).zoomEnd();
|
|
|
|
this.updateViewport(
|
|
newMapState,
|
|
{
|
|
...LINEAR_TRANSITION_PROPS,
|
|
transitionInterpolator: new LinearInterpolator({around: pos}),
|
|
transitionDuration: inertia,
|
|
transitionEasing: INERTIA_EASING
|
|
},
|
|
{
|
|
isDragging: false,
|
|
isPanning: Boolean(this.touchZoom),
|
|
isZooming: Boolean(this.touchZoom),
|
|
isRotating: false
|
|
}
|
|
);
|
|
this.blockEvents(inertia);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const newMapState = this.mapState.zoomEnd().rotateEnd();
|
|
this._state.startPinchRotation = 0;
|
|
this.updateViewport(newMapState, null, {
|
|
isDragging: false,
|
|
isPanning: false,
|
|
isZooming: false,
|
|
isRotating: false
|
|
});
|
|
this._startPinchRotation = null;
|
|
this._lastPinchEvent = null;
|
|
return true;
|
|
}
|
|
|
|
_onTriplePanStart(event) {
|
|
const pos = this.getCenter(event);
|
|
const newMapState = this.mapState.rotateStart({pos});
|
|
this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isDragging: true});
|
|
return true;
|
|
}
|
|
|
|
_onTriplePan(event) {
|
|
if (!this.isDragging()) {
|
|
return false;
|
|
}
|
|
if (!this.touchRotate) {
|
|
return false;
|
|
}
|
|
|
|
const pos = this.getCenter(event);
|
|
pos[0] -= event.deltaX;
|
|
|
|
const newMapState = this.mapState.rotate({pos});
|
|
this.updateViewport(newMapState, NO_TRANSITION_PROPS, {isRotating: true});
|
|
return true;
|
|
}
|
|
|
|
_onTriplePanEnd(event) {
|
|
if (!this.isDragging()) {
|
|
return false;
|
|
}
|
|
if (this.touchRotate) {
|
|
const {inertia = DEFAULT_INERTIA} = this.touchRotate;
|
|
if (inertia && event.velocityY) {
|
|
const pos = this.getCenter(event);
|
|
const endPos = [pos[0], (pos[1] += (event.velocityY * inertia) / 2)];
|
|
const newMapState = this.mapState.rotate({pos: endPos});
|
|
this.updateViewport(
|
|
newMapState,
|
|
{
|
|
...LINEAR_TRANSITION_PROPS,
|
|
transitionDuration: inertia,
|
|
transitionEasing: INERTIA_EASING
|
|
},
|
|
{
|
|
isDragging: false,
|
|
isRotating: true
|
|
}
|
|
);
|
|
this.blockEvents(inertia);
|
|
return false;
|
|
}
|
|
}
|
|
const newMapState = this.mapState.rotateEnd();
|
|
this.updateViewport(newMapState, null, {
|
|
isDragging: false,
|
|
isRotating: false
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// Default handler for the `doubletap` event.
|
|
_onDoubleTap(event) {
|
|
if (!this.doubleClickZoom) {
|
|
return false;
|
|
}
|
|
const pos = this.getCenter(event);
|
|
const isZoomOut = this.isFunctionKeyPressed(event);
|
|
|
|
const newMapState = this.mapState.zoom({pos, scale: isZoomOut ? 0.5 : 2});
|
|
this.updateViewport(
|
|
newMapState,
|
|
Object.assign({}, LINEAR_TRANSITION_PROPS, {
|
|
transitionInterpolator: new LinearInterpolator({around: pos})
|
|
}),
|
|
{isZooming: true}
|
|
);
|
|
return true;
|
|
}
|
|
|
|
// Default handler for the `keydown` event
|
|
_onKeyDown(event) {
|
|
if (!this.keyboard) {
|
|
return false;
|
|
}
|
|
const funcKey = this.isFunctionKeyPressed(event);
|
|
const {zoomSpeed = 2, moveSpeed = 100, rotateSpeedX = 15, rotateSpeedY = 10} = this.keyboard;
|
|
|
|
const {mapStateProps} = this;
|
|
let newMapState;
|
|
|
|
switch (event.srcEvent.keyCode) {
|
|
case 189: // -
|
|
if (funcKey) {
|
|
newMapState = this.getMapState({zoom: mapStateProps.zoom - Math.log2(zoomSpeed) - 1});
|
|
} else {
|
|
newMapState = this.getMapState({zoom: mapStateProps.zoom - Math.log2(zoomSpeed)});
|
|
}
|
|
break;
|
|
case 187: // +
|
|
if (funcKey) {
|
|
newMapState = this.getMapState({zoom: mapStateProps.zoom + Math.log2(zoomSpeed) + 1});
|
|
} else {
|
|
newMapState = this.getMapState({zoom: mapStateProps.zoom + Math.log2(zoomSpeed)});
|
|
}
|
|
break;
|
|
case 37: // left
|
|
if (funcKey) {
|
|
newMapState = this.getMapState({
|
|
bearing: mapStateProps.bearing - rotateSpeedX
|
|
});
|
|
} else {
|
|
newMapState = this.mapState.pan({pos: [moveSpeed, 0], startPos: [0, 0]});
|
|
}
|
|
break;
|
|
case 39: // right
|
|
if (funcKey) {
|
|
newMapState = this.getMapState({
|
|
bearing: mapStateProps.bearing + rotateSpeedX
|
|
});
|
|
} else {
|
|
newMapState = this.mapState.pan({pos: [-moveSpeed, 0], startPos: [0, 0]});
|
|
}
|
|
break;
|
|
case 38: // up
|
|
if (funcKey) {
|
|
newMapState = this.getMapState({pitch: mapStateProps.pitch + rotateSpeedY});
|
|
} else {
|
|
newMapState = this.mapState.pan({pos: [0, moveSpeed], startPos: [0, 0]});
|
|
}
|
|
break;
|
|
case 40: // down
|
|
if (funcKey) {
|
|
newMapState = this.getMapState({pitch: mapStateProps.pitch - rotateSpeedY});
|
|
} else {
|
|
newMapState = this.mapState.pan({pos: [0, -moveSpeed], startPos: [0, 0]});
|
|
}
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return this.updateViewport(newMapState, LINEAR_TRANSITION_PROPS);
|
|
}
|
|
}
|