In a WebGIS application, animations do not play serious roles; however, in some web mapping applications that are focused on user experience, they can really come in handy. In the last example, called ch06_animation, we will go through the process of making our own special camera effects. The theory behind animations is very important but quite mathematical, so try to keep up with me. First, we declare some CSS rules for our custom animation control:
.ol-rocket {
top: 20px;
right: 20px;
}
.ol-rocket button {
background-image: url(../../res/button_rocket.png);
background-size: contain;
background-repeat: no-repeat;
background-position: 50%;
}Next, we create a simple control, which will trigger the animation process. Let's make a little revision first. Animations can be triggered by calling the map object's beforeRender method with one or more animation functions. We can provide a start time and, occasionally, source, resolution, or rotation. These functions make sure that our view is animated before it switches to the next destination. After the first change in the view, the list of animations gets cleared and changes take place immediately later without animations. In this example, we make two custom animations: one for zooming into the maximum resolution of an OpenSteetMap layer (rocketTakeOff) and the other to zoom into my campus (rocketLanding):
ol.control.RocketFlight = function () {
var _this = this;
var controlDiv = document.createElement('div');
controlDiv.className = 'ol-rocket ol-unselectable ol-control';
var controlButton = document.createElement('button');
controlButton.title = 'Launch me';
controlButton.addEventListener('click', function () {
var view = _this.getMap().getView();
_this.getMap().beforeRender(
ol.animation.rocketTakeoff({
resolution: view.getResolution(),
rotation: view.getRotation()
})
);
view.setResolution(39135.75848201024);The first part, as mentioned previously, zooms into the maximum zoom level from the current resolution in an imaginary rocket. Next, we call an animation, which is designated to zoom into the final destination:
setTimeout(function () {
_this.getMap().beforeRender(
ol.animation.pan({
duration: 2000,
source: view.getCenter(),
easing: ol.easing.linear
}),
ol.animation.rocketLanding({
resolution: view.getResolution(),
rotation: view.getRotation()
})
);
view.setProperties({
center: [2026883.0676951527, 5792745.55306364],
resolution: 0.5971642834779395
});
}, 5100);
});
controlDiv.appendChild(controlButton);
ol.control.Control.call(this, {
element: controlDiv
});
};
ol.inherits(ol.control.RocketFlight, ol.control.Control);As we cannot trigger an animation before the previous one is finished, we delay it by a little more than five seconds. Our first animation will take exactly five seconds to complete, but we do not want the two animations to hook up.
Before we create our first custom animation, let's discuss how animations work in OpenLayers 3. Every animation function returns a function. The returned function receives two arguments. The first is the map object, which is negligible in most of the cases. The second one is the frame state. The returned function gets called in every frame until it returns false. From the frame state object, the most important properties that we can decipher is the current time and the view's updated state.
If you are a visual type, this might help you understand animations in OpenLayers 3 better. Imagine a spring. When we start an animation, we fixate one end in the new view. We hold the other end in the previous view where we start animating from. The spring will be in stationary state when both of its ends are in the new view. If we stop animating, we release the spring, which instantly jumps to the new view, as it wants to be in its stationary state. However, until we are animating, we move the end that we hold continuously to the fixated end for a fluent result. But how do we know how much it needs to be moved in a given time? We calculate it from the initial distance and the passed time. Finally, we move closer to the fixed end by the value it needs to be moved. Of course, every spring can be moved in two directions; thus, we can pull the spring in the opposite direction. If we do this, however, we have to calculate with an inverse distance.
There is a very important function, which we will call frequently, called easing. An easing function in the library expects a number between 0 and 1, representing the percentage value for which a movement in the animation is completed. It returns a scaling factor that's based on a function curve, with which a linear movement should be multiplied by in order to result in a tween. Now, we create our first custom animation function to zoom out of the map in the application:
ol.animation.rocketTakeoff = function (options) {
var now = +new Date();
return function (map, frameState) {
if (frameState.time < now + 5000) {
var delta = 1 - ol.easing.easeIn((frameState.time - now) / 5000);
var deltaResolution = options.resolution - frameState.viewState.resolution;
frameState.animate = true;
frameState.viewState.resolution += delta * deltaResolution;
if (frameState.time > now + 2000 && frameState.time < now + 3000) {
var rotateDelta = ol.easing.linear((frameState.time - now - 2000) / 1000);
var deltaRotation = options.rotation - 0.5;
frameState.viewState.rotation += deltaRotation * rotateDelta;
} else if (frameState.time >= now + 3000 && frameState.time < now + 4000) {
var rotateDelta = 1 - ol.easing.linear((frameState.time - now - 3000) / 1000);
var deltaRotation = options.rotation - 0.5;
frameState.viewState.rotation += deltaRotation * rotateDelta;
}
frameState.viewHints[0] += 1;
return true;
}
return false;
};
};As mentioned previously, every frame state comes with a timestamp. There is a twist in the timestamps, though, as it represents the current date in milliseconds.
This way, we have to adjust our timing with a new Date object, which we can cast to milliseconds by preceding it with + on construction. We save the starting time of our animation with this method and compare every frame state's timestamp to it. As we want our first animation to take 5 seconds, we update our frame states for this time, then return false, terminating the animation.
For the resolution change, we use the ol.easing.easeIn function, which starts fast and slows down at the end. We must provide the inverse value that's returned by the easing function, though, as we move toward our destination.
We increase our view state's resolution with the delta resolution between the initial and target value multiplied by the value that's returned by our easing function. This will make sure that our animation is fluent for the time it takes place.
If the animation is between 2 and 3 seconds, we rotate the view with a linear easing. We do not have to invert the returned value as we are moving from our destination (the updated rotation value). To turn back the rotation between the fourth and the fifth seconds, we take the same procedure with an inverse easing value.
As you may have noticed, we changed two values in the frame state's besides its view state's properties. Setting its animate property to true is mandatory; otherwise, the animation stops. Updating the 0 property of its viewHints object is optional, though. That property shows, how many animations are modifying the view at a current frame.
Next, we create our second animation to land in our imaginary rocket's shuttle:
ol.animation.rocketLanding = function (options) {
var now = +new Date();
var direction = Math.round(Math.random());
return function (map, frameState) {
if (frameState.time < now + 15000) {
var delta = 1 - ol.easing.parachuate((frameState.time - now) / 15000);
var deltaResolution = options.resolution - frameState.viewState.resolution;
frameState.animate = true;
frameState.viewState.resolution += delta * deltaResolution;
if (frameState.time > now + 5000 && frameState.time < now + 7000) {
var rotateDelta = ol.easing.linear((frameState.time - now - 5000) / 2000);
var deltaRotation = options.rotation + 2 * Math.PI;
frameState.viewState.rotation += deltaRotation * rotateDelta;
} else if (frameState.time > now + 7000 && frameState.time < now + 10000) {
var panDelta = ol.easing.linear((frameState.time - now - 7000) / 3000);
frameState.viewState.center[direction] += 500 * panDelta;
} else if (frameState.time >= now + 10000 && frameState.time < now + 12000) {
var panDelta = 1 - ol.easing.linear((frameState.time - now - 10000) / 2000);
frameState.viewState.center[direction] += 500 * panDelta;
}
frameState.viewHints[0] += 1;
return true;
}
return false;
};
};In this function, at the end of the landing, we swing our capsule on a random axis. The landing takes 15 seconds. For the duration of this period, we decrease the resolution with a custom easing function, which we will soon discuss. After 5 seconds, we make a 360 degree turn, which is exactly 2π radians. Between 7 and 10 seconds, we pan our view by 500 meters on the randomly generated axis, while between 10 and 12 seconds, we pan it back to its original place.
Next, we define our custom easing function:
ol.easing.parachuate = function (t) {
return 1 - Math.pow(1 - t, 7);
};The basic ol.easing.easeOut function is similar to custom easing function; however, it brings the inverted argument to its third power, making the slowdown faster. We slow down the effect drastically by bringing the inverted argument to its seventh power.
Finally, we include the control to the map and change the base layer back to OpenStreetMap:
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM({
wrapX: false
}),
name: 'OpenStreetMap'
}),
[…]
controls: [
[…]
new ol.control.RocketFlight()
[…]
});Now, if you save the code and open it, you can fly to my campus from any location.
