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 thespeed
option.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: http://easings.net/)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_EVENTS
Result BREAK
Current transition will stop at the current state and next view state update is processed. SNAP_TO_END
Current transition will skip remaining transition steps and view state is updated to final value, transition is stopped and next view state update is processed. IGNORE
Any 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:
transitionDuration
transitionInterpolator
transitionEasing
transitionInterruption
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
number
ornumber[]
) 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_length
numbers, 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 theenter
callback), 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 duration
Number
0
Duration of the transition animation, in milliseconds easing
(t: number) => number
t => t
Easing function that maps a value from [0, 1] to [0, 1], see http://easings.net/ Additional fields for
type: 'spring'
:Key Type Default Description stiffness
Number
0.05
"Tension" factor for the spring damping
Number
0.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,
enter
callback is called at index 3 to backfill the position and color to transition from. The first argument is the "to" value. For position, the defaultenter
returns[2, 0, 0]
(same value), which will look like the new circle just appeared in place. For color, the user-suppliedenter
returns[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.