Animations and Transitions
deck.gl provides several built-in animation/transition features.
- Camera Transitions - the camera can move smoothly from the current view state to the new view state.
- Layer Prop Transitions - when a layer prop is updated, it may animate from the old value to the new value.
Advanced motion effects can also be implemented using deck.gl in conjunction with other animation libraries, see the custom animations section.
Camera Transitions
Camera transitions provide smooth and visually appealing transitions when viewState change from one state to the other.
Transitions are performed when setting Deck's viewState or initialViewState prop to a new value with the following additional fields:
-
transitionInterpolator(object, optional, default:LinearInterpolator) - An interpolator object that defines the transition behavior between two view states. The choices are:- LinearInterpolator - a generic interpolator that works with all view types. This is the default.
- FlyToInterpolator - a "fly to" style camera transition for geospatial views. This is pretty useful when the camera center changes by long distance.
- Implement a custom interpolator. See TransitionInterpolator.
-
transitionDuration(number | string, optional, default: 0) - Transition duration in milliseconds, default value 0, implies no transition. When usingFlyToInterpolator, it can also be set to'auto'where actual duration is calculated based on the distance between the start and end states, and thespeedoption. -
transitionEasing(Function, optional, default:t => t) - Easing function that can be used to achieve effects like "Ease-In-Cubic", "Ease-Out-Cubic", etc. Default value performs Linear easing. (list of sample easing functions) -
transitionInterruption(number, optional, default:TRANSITION_EVENTS.BREAK) - This field controls how to process a new view state change that occurs while performing an existing transition. This field has no impact once transition is complete. Here is the list of all possible values with resulting behavior.TRANSITION_EVENTSResult BREAKCurrent transition will stop at the current state and next view state update is processed. SNAP_TO_ENDCurrent transition will skip remaining transition steps and view state is updated to final value, transition is stopped and next view state update is processed. IGNOREAny view state update is ignored until current transition is complete, this also includes view state changes due to user interaction. -
onTransitionStart(Functional, optional) - Callback fires when requested transition starts. -
onTransitionInterrupt(Functional, optional) - Callback fires when transition is interrupted. -
onTransitionEnd(Functional, optional) - Callback fires when transition ends.
Camera Transition Examples
This example provides flyTo style transition to move camera from current location to the requested city.
- JavaScript
- TypeScript
- React
import {Deck, FlyToInterpolator} from '@deck.gl/core';
const CITIES = {
SF: {
longitude: -122.4,
latitude: 37.8,
zoom: 10
},
NYC: {
longitude: -74.0,
latitude: 40.7,
zoom: 10
}
}
const deckInstance = new Deck({
initialViewState: CITIES.SF,
controller: true
});
for (const button of document.querySelectorAll('button')) {
button.onclick = () => flyToCity(button.id);
}
function flyToCity(name) {
deckInstance.setProps({
initialViewState: {
...CITIES[name],
transitionInterpolator: new FlyToInterpolator({speed: 2}),
transitionDuration: 'auto'
}
})
}
import {Deck, MapViewState, FlyToInterpolator} from '@deck.gl/core';
const CITIES: {[name: string]: MapViewState} = {
SF: {
longitude: -122.4,
latitude: 37.8,
zoom: 10
},
NYC: {
longitude: -74.0,
latitude: 40.7,
zoom: 10
}
}
const deckInstance = new Deck({
initialViewState: CITIES.SF,
controller: true
});
for (const button of document.querySelectorAll('button')) {
(button as HTMLButtonElement).onclick = () => flyToCity(button.id);
}
function flyToCity(name: string) {
deckInstance.setProps({
initialViewState: {
...CITIES[name],
transitionInterpolator: new FlyToInterpolator({speed: 2}),
transitionDuration: 'auto'
}
})
}
import React, {useState, useCallback} from 'react';
import {DeckGL} from '@deck.gl/react';
import {MapViewState, FlyToInterpolator} from '@deck.gl/core';
const CITIES: {[name: string]: MapViewState} = {
SF: {
longitude: -122.4,
latitude: 37.8,
zoom: 10
},
NYC: {
longitude: -74.0,
latitude: 40.7,
zoom: 10
}
}
function App() {
const [initialViewState, setInitialViewState] = useState<MapViewState>(CITIES.SF);
const flyToCity = useCallback(evt => {
setInitialViewState({
...CITIES[evt.target.id],
transitionInterpolator: new FlyToInterpolator({speed: 2}),
transitionDuration: 'auto'
});
}, [])
return <>
<DeckGL
initialViewState={initialViewState}
controller
/>;
{Object.keys(CITIES).map(name => <button id={name} onClick={flyToCity}>{name}</button>)}
</>;
}
This example continuously rotates the camera along the Z (vertical) axis until user interrupts the rotation by dragging. It uses LinearInterpolator and restricts transitions to bearing. Continuous transitions are achieved by triggering new transitions upon the onTransitionEnd callback.
- JavaScript
- TypeScript
- React
import {Deck, LinearInterpolator} from '@deck.gl/core';
let initialViewState = {
longitude: -122.4,
latitude: 37.8,
zoom: 12
};
const deckInstance = new Deck({
initialViewState,
controller: true,
onLoad: rotateCamera
});
function rotateCamera() {
initialViewState = {
...initialViewState,
bearing: initialViewState.bearing + 120,
transitionDuration: 1000,
transitionInterpolator: new LinearInterpolator(['bearing']),
onTransitionEnd: rotateCamera
};
deckInstance.setProps({initialViewState});
}
import {Deck, LinearInterpolator, MapViewState} from '@deck.gl/core';
let initialViewState: MapViewState = {
longitude: -122.4,
latitude: 37.8,
zoom: 12
};
const deckInstance = new Deck({
initialViewState,
controller: true,
onLoad: rotateCamera
});
function rotateCamera(): void {
initialViewState = {
...initialViewState,
bearing: initialViewState.bearing + 120,
transitionDuration: 1000,
transitionInterpolator: new LinearInterpolator(['bearing']),
onTransitionEnd: rotateCamera
};
deckInstance.setProps({initialViewState});
}
import React, {useState, useCallback} from 'react';
import {DeckGL} from '@deck.gl/react';
import {LinearInterpolator, MapViewState} from '@deck.gl/core';
function App() {
const [initialViewState, setInitialViewState] = useState<MapViewState>({
longitude: -122.4,
latitude: 37.8,
zoom: 12
});
const rotateCamera = useCallback(() => {
setInitialViewState(viewState => ({
...viewState,
bearing: viewState.bearing + 120,
transitionDuration: 1000,
transitionInterpolator: new LinearInterpolator(['bearing']),
onTransitionEnd: rotateCamera
}));
}, []);
return <DeckGL
initialViewState={initialViewState}
controller
onLoad={rotateCamera}
/>;
}
Remarks
Deck's view state transition model is "set and forget": the values of the following props at the start of a transition carry through the entire duration of the transition:
transitionDurationtransitionInterpolatortransitionEasingtransitionInterruption
The default transition behavior can always be intercepted and overwritten in the handler for onViewStateChange. However, if a transition is in progress, the properties that are being transitioned (e.g. longitude and latitude) should not be manipulated, otherwise the change will be interpreted as an interruption of the transition.
Layer Prop Transitions
Layer properties may smoothly transition from one value to the next if a transitions prop is configured. There are two categories of transition-enabled props, for each enabling transition has different implications regarding performance and complexity.
- Uniform prop (usually of type
numberornumber[]) transition is performed on the CPU. It only recomputes one single numeric value per update, and imposes virtually no cost on top of redrawing on each animation frame. - Attribute prop (usually named
get*) transition is performed on the GPU. Since it recomputes values forattribute_size * data_lengthnumbers, the ammount of data being updated per frame can be quite large for a big dataset. For example, animating the position of 1M point cloud involves 3M float64 or 6M float32 numbers. Performing the computation on the GPU means that they can be updated efficiently in parallel and without leaving the GPU memory. However, when such a transition is first triggered, some of the preparation is done on the CPU (specifically theentercallback), and it could potentially be expensive. See more discussions below.
To enable layer prop transitions, set the layer's transitions prop to an object that defines animation parameters by using the prop names as keys. The following example has the columns "grow" from the ground when data is loaded:
- JavaScript
- TypeScript
- React
import {Deck} from '@deck.gl/core';
import {HexagonLayer} from '@deck.gl/aggregation-layers';
const deckInstance = new Deck({
// ...
layer: getLayers(null)
});
const resp = await fetch('/path/to/data.json');
const data = await resp.json();
deckInstance.setProps({
layer: getLayers(data)
});
function getLayers(data) {
return [
new HexagonLayer({
id: '3d-heatmap',
data,
getPosition: d => [d.longitude, d.latitude],
getElevationWeight: d => d.count,
extruded: true,
elevationScale: data && data.length ? 50 : 0,
transitions: {
elevationScale: 3000
}
})
];
}
import {Deck} from '@deck.gl/core';
import {HexagonLayer} from '@deck.gl/aggregation-layers';
type DataType = {
longitude: number;
latitude: number;
count: number;
};
const deckInstance = new Deck({
// ...
layer: getLayers(null)
});
const resp = await fetch('/path/to/data.json');
const data = await resp.json() as DataType[];
deckInstance.setProps({
layer: getLayers(data)
});
function getLayers(data: DataType[] | null) {
return [
new HexagonLayer<DataType>({
id: '3d-heatmap',
data,
getPosition: (d: DataType) => [d.longitude, d.latitude],
getElevationWeight: (d: DataType) => d.count,
extruded: true,
elevationScale: data && data.length ? 50 : 0,
transitions: {
elevationScale: 3000
}
})
];
}
import React, {useEffect, useState} from 'react';
import {DeckGL} from '@deck.gl/react';
import {HexagonLayer} from '@deck.gl/aggregation-layers';
type DataType = {
longitude: number;
latitude: number;
count: number;
};
function App() {
const [data, setData] = useState<DataType[] | null>(null);
useEffect(() => {
(async () => {
const resp = await fetch('/path/to/data.json');
const data = await resp.json() as DataType[];
setData(data);
})();
}, []);
const layers = [
new HexagonLayer<DataType>({
id: '3d-heatmap',
data,
getPosition: (d: DataType) => [d.longitude, d.latitude],
getElevationWeight: (d: DataType) => d.count,
extruded: true,
elevationScale: data && data.length ? 50 : 0,
transitions: {
elevationScale: 3000
}
})
];
return <DeckGL
// ...
layers={layers}
/>;
}
In the transitions object, each prop name is mapped to a number or an object that is the transition setting. As a shorthand, if a prop name maps to a number, then the number is assigned to the duration parameter with an interpolation type transition. If an object is supplied, it may contain the following fields:
| Key | Type | Default | Description |
|---|---|---|---|
type | string | 'interpolation' | Type of the transition, currently supports 'interpolation' and 'spring' |
enter | Function | value => value | Callback to get the value that the entering vertices are transitioning from. See "attribute backfilling" below |
onStart | Function | null | Callback when the transition is started |
onEnd | Function | null | Callback when the transition is done |
onInterrupt | Function | null | Callback when the transition is interrupted |
-
Additional fields for
type: 'interpolation':Key Type Default Description durationNumber0Duration of the transition animation, in milliseconds easing(t: number) => numbert => tEasing function that maps a value from [0, 1] to [0, 1], see http://easings.net/ -
Additional fields for
type: 'spring':Key Type Default Description stiffnessNumber0.05"Tension" factor for the spring dampingNumber0.5"Friction" factor that counteracts the spring's acceleration
Attribute Backfilling
Consider the following setup, a ScatterplotLayer with transitions enabled for positions and fill colors:
import {ScatterplotLayer} from '@deck.gl/layers';
import type {Color} from '@deck.gl/core';
new ScatterplotLayer({
// ...
transitions: {
getPosition: {
type: 'spring',
damping: 0.2
},
getFillColor: {
duration: 600,
easing: (x: number) => -(Math.cos(Math.PI * x) - 1) / 2, // ease-in-out-sine
entry: ([r, g, b]: Color) => [r, g, b, 0]
}
}
});
When the layer's data updates from 3 elements to 4 elements, positions and colors also changed:
| Object index | Old getPosition result | New getPosition result | Old getFillColor result | New getFillColor result |
|---|---|---|---|---|
| 0 | [0, 0, 0] | [0, 3, 0] | [255, 0, 0, 255] | [255, 255, 0, 255] |
| 1 | [1, 0, 0] | [0, 0, 0] | [0, 255, 0, 255] | [255, 0, 0, 255] |
| 2 | [2, 0, 0] | [1, 0, 0] | [0, 0, 255, 255] | [0, 255, 0, 255] |
| 3 | - | [2, 0, 0] | - | [0, 0, 255, 255] |
- For index 0-2, transitions are performed from the old values to the new values at the same index.
- Because the new data is larger,
entercallback is called at index 3 to backfill the position and color to transition from. The first argument is the "to" value. For position, the defaultenterreturns[2, 0, 0](same value), which will look like the new circle just appeared in place. For color, the user-suppliedenterreturns[0, 0, 255, 0](same RGB and alpha=0), which will look like the new circle faded in.
When working with variable-length geometries, such as PathLayer and PolygonLayer, transitions are handled per geometry (path or polygon). For each geometry, transitions are performed between the old and new values at the same vertex index. If the new path/polygon has more vertices, enter is called to backfill the "from" value. In this case, enter also receives a second argument fromChunk representing the "from" value of the entire geometry.
The process of attribute backfilling may be expensive performance-wise because it calls enter for each index on the CPU, then upload the new data to the GPU. It only happens when data size/vertex count is growing.
Limitations
Between updates, objects are identified by their index in the data array. This means that if objects are inserted or removed, the transition will not look as expected. There is an open feature request for supporting custom object id.
Custom Animations
The most powerful way to create animations with deck.gl is to manage data and settings externally, and update the layers' props on every frame.
The following example shows the TripsLayer's currentTime prop animated by the popmotion library:
- JavaScript
- TypeScript
- React
import {Deck} from '@deck.gl/core';
import {TripsLayer} from '@deck.gl/geo-layers';
import {animate} from "popmotion";
const currentTimeAnimation = animate({
from: 0, // currentTime min value
to: 1800, // currentTime max value
duration: 5000, // over the course of 5 seconds
repeat: Infinity,
onUpdate: updateLayers
});
const deckInstance = new Deck({
initialViewState: {
longitude: -122.4,
latitude: 37.8,
zoom: 12
},
controller: true
});
function updateLayers(currentTime) {
const layers = [
new TripsLayer({
id: 'TripsLayer',
data: '/path/to/data.json',
getPath: d => d.waypoints.map(p => p.coordinates),
getTimestamps: d => d.waypoints.map(p => p.timestamp),
getColor: [253, 128, 93],
getWidth: 50,
currentTime,
trailLength: 600
})
];
deckInstance.setProps({layers});
}
import {Deck} from '@deck.gl/core';
import {TripsLayer} from '@deck.gl/geo-layers';
import {animate} from "popmotion";
const currentTimeAnimation = animate<number>({
from: 0, // currentTime min value
to: 1800, // currentTime max value
duration: 5000, // over the course of 5 seconds
repeat: Infinity,
onUpdate: updateLayers
});
const deckInstance = new Deck({
initialViewState: {
longitude: -122.4,
latitude: 37.8,
zoom: 12
},
controller: true
});
type TripData = {
coordinates: [longitude: number, latitude: number][];
timestamps: number[];
};
function updateLayers(currentTime: number) {
const layers = [
new TripsLayer<TripData>({
id: 'TripsLayer',
data: '/path/to/data.json',
getPath: (d: TripData) => d.waypoints.map(p => p.coordinates),
getTimestamps: (d: TripData) => d.waypoints.map(p => p.timestamp),
getColor: [253, 128, 93],
getWidth: 50,
currentTime,
trailLength: 600
})
];
deckInstance.setProps({layers});
}
import React, {useState, useEffect} from 'react';
import {DeckGL} from '@deck.gl/react';
import {MapViewState} from '@deck.gl/core';
import {TripsLayer} from '@deck.gl/geo-layers';
import {animate} from "popmotion";
const INITIAL_VIEW_STATE: MapViewState = {
longitude: -122.4,
latitude: 37.8,
zoom: 12
};
type TripData = {
coordinates: [longitude: number, latitude: number][];
timestamps: number[];
};
function App() {
const [currentTime, setCurrentTime] = useState<number>(0);
useEffect(() => {
const currentTimeAnimation = animate<number>({
from: 0, // currentTime min value
to: 1800, // currentTime max value
duration: 5000, // over the course of 5 seconds
repeat: Infinity,
onUpdate: setCurrentTime
});
return () => currentTimeAnimation.stop();
});
const layers = [
new TripsLayer<TripData>({
id: 'TripsLayer',
data: '/path/to/data.json',
getPath: (d: TripData) => d.waypoints.map(p => p.coordinates),
getTimestamps: (d: TripData) => d.waypoints.map(p => p.timestamp),
getColor: [253, 128, 93],
getWidth: 50,
currentTime,
trailLength: 600
})
];
return <DeckGL
initialViewState={INITIAL_VIEW_STATE}
controller
layers={layers}
/>;
}
Deck.gl is designed to handle layer updates very efficiently at high frame rate. An example of this kind of application is autonomous vehicle visualization, where car pose, LIDAR point clouds, camera imagery, as well as geometries outlining perception, prediction and planning decisions are streamed in from a server many times a second. The performance guide describes various techniques in optimizing for large datasets and frequent updates.
hubble.gl, a project by the vis.gl community, implements comprehensive keyframe-controlled animation of deck.gl layers for interactive storytelling and/or render to video.