Merge pull request #6079 from tschaub/animate

Add view animation
This commit is contained in:
Tim Schaub
2016-11-07 09:50:41 -07:00
committed by GitHub
23 changed files with 794 additions and 310 deletions

View File

@@ -1,5 +1,40 @@
## Upgrade notes
### Next release
#### Use `view.animate()` instead of `map.beforeRender()` and `ol.animation` functions
The `map.beforeRender()` and `ol.animation` functions have been deprecated in favor of a new `view.animate()` function. Use of the deprecated functions will result in a warning during development. These functions are subject to removal in an upcoming release.
For details on the `view.animate()` method, see the API docs and the view animation example. Upgrading should be relatively straightforward. For example, if you wanted to have an animated pan, zoom, and rotation previously, you might have done this:
```js
var zoom = ol.animation.zoom({
resolution: view.getResolution()
});
var pan = ol.animation.pan({
source: view.getCenter()
});
var rotate = ol.animation.rotate({
rotation: view.getRotation()
});
map.beforeRender(zoom, pan, rotate);
map.setZoom(1);
map.setCenter([0, 0]);
map.setRotation(Math.PI);
```
Now, the same can be accomplished with this:
```js
view.animate({
zoom: 1,
center: [0, 0],
rotation: Math.PI
});
```
### v3.19.1
#### `ol.style.Fill` with `CanvasGradient` or `CanvasPattern`

View File

@@ -34,7 +34,7 @@ The library is intended for use on both desktop/laptop and mobile devices.
OL3 uses a similar object hierarchy to the Closure library. There is a top-level `ol` namespace (basically, `var ol = {};`). Subdivisions of this are:
* further namespaces, such as `ol.layer`; these have a lower-case initial
* simple objects containing static properties and methods, such as `ol.animation`; these also have a lower-case initial
* simple objects containing static properties and methods, such as `ol.easing`; these also have a lower-case initial
* types, which have an upper-case initial. These are mainly 'classes', which here means a constructor function with prototypal inheritance, such as `ol.Map` or `ol.layer.Vector` (the Vector class within the layer namespace). There are however other, simpler, types, such as `ol.Extent`, which is an array.
Class namespaces, such as `ol.layer` have a base class type with the same name, such as `ol.layer.Layer`. These are mainly abstract classes, from which the other subclasses inherit.

View File

@@ -3,17 +3,17 @@ layout: example.html
title: View Animation
shortdesc: Demonstrates animated pan, zoom, and rotation.
docs: >
This example shows how to use the beforeRender function on the Map to run one
or more animations.
This example shows how to use the <code>view.animate()</code> method to run
one or more animations.
tags: "animation"
---
<div id="map" class="map"></div>
<button id="rotate-left" title="Rotate clockwise"></button>
<button id="rotate-right" title="Rotate counterclockwise"></button>
<button id="rotate-around-rome">Rotate around Rome</button>
<button id="pan-to-london">Pan to London</button>
<button id="elastic-to-moscow">Elastic to Moscow</button>
<button id="bounce-to-istanbul">Bounce to Istanbul</button>
<button id="spin-to-rome">Spin to Rome</button>
<button id="fly-to-bern">Fly to Bern</button>
<button id="spiral-to-madrid">Spiral to Madrid</button>
<button id="rotate-around-rome">Rotate around Rome</button>
<button id="tour">Take a tour</button>

View File

@@ -1,13 +1,35 @@
goog.require('ol.Map');
goog.require('ol.View');
goog.require('ol.animation');
goog.require('ol.control');
goog.require('ol.layer.Tile');
goog.require('ol.proj');
goog.require('ol.source.OSM');
var london = ol.proj.fromLonLat([-0.12755, 51.507222]);
var moscow = ol.proj.fromLonLat([37.6178, 55.7517]);
var istanbul = ol.proj.fromLonLat([28.9744, 41.0128]);
var rome = ol.proj.fromLonLat([12.5, 41.9]);
var bern = ol.proj.fromLonLat([7.4458, 46.95]);
// from https://github.com/DmitryBaranovskiy/raphael
var view = new ol.View({
center: istanbul,
zoom: 6
});
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
preload: 4,
source: new ol.source.OSM()
})
],
// Improve user experience by loading tiles while animating. Will make
// animations stutter on mobile or slow devices.
loadTilesWhileAnimating: true,
view: view
});
// A bounce easing method (from https://github.com/DmitryBaranovskiy/raphael).
function bounce(t) {
var s = 7.5625, p = 2.75, l;
if (t < (1 / p)) {
@@ -29,159 +51,116 @@ function bounce(t) {
return l;
}
// from https://github.com/DmitryBaranovskiy/raphael
// An elastic easing method (from https://github.com/DmitryBaranovskiy/raphael).
function elastic(t) {
return Math.pow(2, -10 * t) * Math.sin((t - 0.075) * (2 * Math.PI) / 0.3) + 1;
}
var london = ol.proj.fromLonLat([-0.12755, 51.507222]);
var moscow = ol.proj.fromLonLat([37.6178, 55.7517]);
var istanbul = ol.proj.fromLonLat([28.9744, 41.0128]);
var rome = ol.proj.fromLonLat([12.5, 41.9]);
var bern = ol.proj.fromLonLat([7.4458, 46.95]);
var madrid = ol.proj.fromLonLat([-3.683333, 40.4]);
function onClick(id, callback) {
document.getElementById(id).addEventListener('click', callback);
}
var view = new ol.View({
// the view's initial state
center: istanbul,
zoom: 6
onClick('rotate-left', function() {
view.animate({
rotation: view.getRotation() + Math.PI / 2
});
});
var map = new ol.Map({
layers: [
new ol.layer.Tile({
preload: 4,
source: new ol.source.OSM()
})
],
// Improve user experience by loading tiles while animating. Will make
// animations stutter on mobile or slow devices.
loadTilesWhileAnimating: true,
target: 'map',
controls: ol.control.defaults({
attributionOptions: /** @type {olx.control.AttributionOptions} */ ({
collapsible: false
})
}),
view: view
onClick('rotate-right', function() {
view.animate({
rotation: view.getRotation() - Math.PI / 2
});
});
var rotateLeft = document.getElementById('rotate-left');
rotateLeft.addEventListener('click', function() {
var rotateLeft = ol.animation.rotate({
duration: 2000,
rotation: -4 * Math.PI
onClick('rotate-around-rome', function() {
view.animate({
rotation: view.getRotation() + 2 * Math.PI,
anchor: rome
});
map.beforeRender(rotateLeft);
}, false);
var rotateRight = document.getElementById('rotate-right');
rotateRight.addEventListener('click', function() {
var rotateRight = ol.animation.rotate({
duration: 2000,
rotation: 4 * Math.PI
});
map.beforeRender(rotateRight);
}, false);
});
var rotateAroundRome = document.getElementById('rotate-around-rome');
rotateAroundRome.addEventListener('click', function() {
var currentRotation = view.getRotation();
var rotateAroundRome = ol.animation.rotate({
anchor: rome,
duration: 1000,
rotation: currentRotation
onClick('pan-to-london', function() {
view.animate({
center: london,
duration: 2000
});
map.beforeRender(rotateAroundRome);
view.rotate(currentRotation + (Math.PI / 2), rome);
}, false);
});
var panToLondon = document.getElementById('pan-to-london');
panToLondon.addEventListener('click', function() {
var pan = ol.animation.pan({
onClick('elastic-to-moscow', function() {
view.animate({
center: moscow,
duration: 2000,
source: /** @type {ol.Coordinate} */ (view.getCenter())
easing: elastic
});
map.beforeRender(pan);
view.setCenter(london);
}, false);
});
var elasticToMoscow = document.getElementById('elastic-to-moscow');
elasticToMoscow.addEventListener('click', function() {
var pan = ol.animation.pan({
onClick('bounce-to-istanbul', function() {
view.animate({
center: istanbul,
duration: 2000,
easing: elastic,
source: /** @type {ol.Coordinate} */ (view.getCenter())
easing: bounce
});
map.beforeRender(pan);
view.setCenter(moscow);
}, false);
});
var bounceToIstanbul = document.getElementById('bounce-to-istanbul');
bounceToIstanbul.addEventListener('click', function() {
var pan = ol.animation.pan({
duration: 2000,
easing: bounce,
source: /** @type {ol.Coordinate} */ (view.getCenter())
});
map.beforeRender(pan);
view.setCenter(istanbul);
}, false);
var spinToRome = document.getElementById('spin-to-rome');
spinToRome.addEventListener('click', function() {
var duration = 2000;
var start = +new Date();
var pan = ol.animation.pan({
duration: duration,
source: /** @type {ol.Coordinate} */ (view.getCenter()),
start: start
});
var rotate = ol.animation.rotate({
duration: duration,
onClick('spin-to-rome', function() {
view.animate({
center: rome,
rotation: 2 * Math.PI,
start: start
duration: 2000
});
map.beforeRender(pan, rotate);
view.setCenter(rome);
}, false);
});
var flyToBern = document.getElementById('fly-to-bern');
flyToBern.addEventListener('click', function() {
function flyTo(location, done) {
var duration = 2000;
var start = +new Date();
var pan = ol.animation.pan({
duration: duration,
source: /** @type {ol.Coordinate} */ (view.getCenter()),
start: start
});
var bounce = ol.animation.bounce({
duration: duration,
resolution: 4 * view.getResolution(),
start: start
});
map.beforeRender(pan, bounce);
view.setCenter(bern);
}, false);
var zoom = view.getZoom();
var parts = 2;
var called = false;
function callback(complete) {
--parts;
if (called) {
return;
}
if (parts === 0 || !complete) {
called = true;
done(complete);
}
}
view.animate({
center: location,
duration: duration
}, callback);
view.animate({
zoom: zoom - 1,
duration: duration / 2
}, {
zoom: zoom,
duration: duration / 2
}, callback);
}
var spiralToMadrid = document.getElementById('spiral-to-madrid');
spiralToMadrid.addEventListener('click', function() {
var duration = 2000;
var start = +new Date();
var pan = ol.animation.pan({
duration: duration,
source: /** @type {ol.Coordinate} */ (view.getCenter()),
start: start
});
var bounce = ol.animation.bounce({
duration: duration,
resolution: 2 * view.getResolution(),
start: start
});
var rotate = ol.animation.rotate({
duration: duration,
rotation: -4 * Math.PI,
start: start
});
map.beforeRender(pan, bounce, rotate);
view.setCenter(madrid);
}, false);
onClick('fly-to-bern', function() {
flyTo(bern, function() {});
});
function tour() {
var locations = [london, bern, rome, moscow, istanbul];
var index = -1;
function next(more) {
if (more) {
++index;
if (index < locations.length) {
var delay = index === 0 ? 0 : 750;
setTimeout(function() {
flyTo(locations[index], next);
}, delay);
} else {
alert('Tour complete');
}
} else {
alert('Tour cancelled');
}
}
next(true);
}
onClick('tour', tour);

View File

@@ -130,26 +130,6 @@ function addPosition(position, heading, m, speed) {
}
}
var previousM = 0;
// change center and rotation before render
map.beforeRender(function(map, frameState) {
if (frameState !== null) {
// use sampling period to get a smooth transition
var m = frameState.time - deltaMean * 1.5;
m = Math.max(m, previousM);
previousM = m;
// interpolate position along positions LineString
var c = positions.getCoordinateAtM(m, true);
var view = frameState.viewState;
if (c) {
view.center = getCenterWithHeading(c, -c[2], view.resolution);
view.rotation = -c[2];
marker.setPosition(c);
}
}
return true; // Force animation to continue
});
// recenters the view by putting the given coordinates at 3/4 from the top or
// the screen
function getCenterWithHeading(position, rotation, resolution) {
@@ -162,9 +142,19 @@ function getCenterWithHeading(position, rotation, resolution) {
];
}
// postcompose callback
function render() {
map.render();
var previousM = 0;
function updateView() {
// use sampling period to get a smooth transition
var m = Date.now() - deltaMean * 1.5;
m = Math.max(m, previousM);
previousM = m;
// interpolate position along positions LineString
var c = positions.getCoordinateAtM(m, true);
if (c) {
view.setCenter(getCenterWithHeading(c, -c[2], view.getResolution()));
view.setRotation(-c[2]);
marker.setPosition(c);
}
}
// geolocate device
@@ -172,7 +162,7 @@ var geolocateBtn = document.getElementById('geolocate');
geolocateBtn.addEventListener('click', function() {
geolocation.setTracking(true); // Start position tracking
map.on('postcompose', render);
map.on('postcompose', updateView);
map.render();
disableButtons();
@@ -214,7 +204,7 @@ simulateBtn.addEventListener('click', function() {
}
geolocate();
map.on('postcompose', render);
map.on('postcompose', updateView);
map.render();
disableButtons();

View File

@@ -0,0 +1,7 @@
@media (min-width: 800px) {
.half {
padding: 0 10px;
width: 50%;
float: left;
}
}

View File

@@ -6,10 +6,14 @@ docs: >
Two maps (one with the Canvas renderer, one with the WebGL renderer) share the same center, resolution, rotation and layers.
tags: "side-by-side, canvas, webgl"
---
<h4>Canvas</h4>
<div id="canvasMap" class="map"></div>
<h4>WebGL</h4>
<div id="webglMap" class="map"></div>
<div id="no-webgl" class="alert alert-danger" style="display: none">
This map requires a browser that supports <a href="http://get.webgl.org/">WebGL</a>.
<div class="half">
<h4>Canvas</h4>
<div id="canvasMap" class="map"></div>
</div>
<div class="half">
<h4>WebGL</h4>
<div id="webglMap" class="map"></div>
<div id="no-webgl" class="alert alert-danger" style="display: none">
This map requires a browser that supports <a href="http://get.webgl.org/">WebGL</a>.
</div>
</div>

View File

@@ -312,7 +312,7 @@ olx.MapOptions.prototype.view;
* stopEvent: (boolean|undefined),
* insertFirst: (boolean|undefined),
* autoPan: (boolean|undefined),
* autoPanAnimation: (olx.animation.PanOptions|undefined),
* autoPanAnimation: (olx.OverlayPanOptions|undefined),
* autoPanMargin: (number|undefined)}}
*/
olx.OverlayOptions;
@@ -398,10 +398,10 @@ olx.OverlayOptions.prototype.autoPan;
/**
* The options used to create a `ol.animation.pan` animation. This animation
* is only used when `autoPan` is enabled. By default the default options for
* `ol.animation.pan` are used. If set to `null` the panning is not animated.
* @type {olx.animation.PanOptions|undefined}
* The animation options used to pan the overlay into view. This animation
* is only used when `autoPan` is enabled. A `duration` and `easing` may be
* provided to customize the animation.
* @type {olx.OverlayPanOptions|undefined}
* @api
*/
olx.OverlayOptions.prototype.autoPanAnimation;
@@ -416,6 +416,32 @@ olx.OverlayOptions.prototype.autoPanAnimation;
olx.OverlayOptions.prototype.autoPanMargin;
/**
* @typedef {{
* duration: (number|undefined),
* easing: (function(number):number|undefined)
* }}
*/
olx.OverlayPanOptions;
/**
* The duration of the animation in milliseconds. Default is `1000`.
* @type {number|undefined}
* @api
*/
olx.OverlayPanOptions.prototype.duration;
/**
* The easing function to use. Can be an {@link ol.easing} or a custom function.
* Default is {@link ol.easing.inAndOut}.
* @type {function(number):number|undefined}
* @api
*/
olx.OverlayPanOptions.prototype.easing;
/**
* Object literal with config options for the projection.
* @typedef {{code: string,
@@ -661,6 +687,81 @@ olx.ViewOptions.prototype.zoom;
olx.ViewOptions.prototype.zoomFactor;
/**
* @typedef {{
* center: (ol.Coordinate|undefined),
* zoom: (number|undefined),
* resolution: (number|undefined),
* rotation: (number|undefined),
* anchor: (ol.Coordinate|undefined),
* duration: (number|undefined),
* easing: (function(number):number|undefined)
* }}
*/
olx.AnimationOptions;
/**
* The center of the view at the end of the animation.
* @type {ol.Coordinate|undefined}
* @api
*/
olx.AnimationOptions.prototype.center;
/**
* The zoom level of the view at the end of the animation. This takes
* precedence over `resolution`.
* @type {number|undefined}
* @api
*/
olx.AnimationOptions.prototype.zoom;
/**
* The resolution of the view at the end of the animation. If `zoom` is also
* provided, this option will be ignored.
* @type {number|undefined}
* @api
*/
olx.AnimationOptions.prototype.resolution;
/**
* The rotation of the view at the end of the animation.
* @type {number|undefined}
* @api
*/
olx.AnimationOptions.prototype.rotation;
/**
* Optional anchor to remained fixed during a rotation or resolution animation.
* @type {ol.Coordinate|undefined}
* @api
*/
olx.AnimationOptions.prototype.anchor;
/**
* The duration of the animation in milliseconds (defaults to `1000`).
* @type {number|undefined}
* @api
*/
olx.AnimationOptions.prototype.duration;
/**
* The easing function used during the animation (defaults to {@link ol.easing.inAndOut}).
* The function will be called for each frame with a number representing a
* fraction of the animation's duration. The function should return a number
* between 0 and 1 representing the progress toward the destination state.
* @type {function(number):number|undefined}
* @api
*/
olx.AnimationOptions.prototype.easing;
/**
* Namespace.
* @type {Object}

View File

@@ -91,7 +91,8 @@
2,
{
"allow": [
"assert"
"assert",
"warn"
]
}
],

View File

@@ -7,6 +7,7 @@ goog.require('ol.easing');
/**
* Deprecated (use {@link ol.View#animate} instead).
* Generate an animated transition that will "bounce" the resolution as it
* approaches the final value.
* @param {olx.animation.BounceOptions} options Bounce options.
@@ -14,6 +15,7 @@ goog.require('ol.easing');
* @api
*/
ol.animation.bounce = function(options) {
ol.DEBUG && console.warn('ol.animation.bounce() is deprecated. Use view.animate() instead.');
var resolution = options.resolution;
var start = options.start ? options.start : Date.now();
var duration = options.duration !== undefined ? options.duration : 1000;
@@ -45,12 +47,14 @@ ol.animation.bounce = function(options) {
/**
* Deprecated (use {@link ol.View#animate} instead).
* Generate an animated transition while updating the view center.
* @param {olx.animation.PanOptions} options Pan options.
* @return {ol.PreRenderFunction} Pre-render function.
* @api
*/
ol.animation.pan = function(options) {
ol.DEBUG && console.warn('ol.animation.pan() is deprecated. Use view.animate() instead.');
var source = options.source;
var start = options.start ? options.start : Date.now();
var sourceX = source[0];
@@ -86,12 +90,14 @@ ol.animation.pan = function(options) {
/**
* Deprecated (use {@link ol.View#animate} instead).
* Generate an animated transition while updating the view rotation.
* @param {olx.animation.RotateOptions} options Rotate options.
* @return {ol.PreRenderFunction} Pre-render function.
* @api
*/
ol.animation.rotate = function(options) {
ol.DEBUG && console.warn('ol.animation.rotate() is deprecated. Use view.animate() instead.');
var sourceRotation = options.rotation ? options.rotation : 0;
var start = options.start ? options.start : Date.now();
var duration = options.duration !== undefined ? options.duration : 1000;
@@ -133,12 +139,14 @@ ol.animation.rotate = function(options) {
/**
* Deprecated (use {@link ol.View#animate} instead).
* Generate an animated transition while updating the view resolution.
* @param {olx.animation.ZoomOptions} options Zoom options.
* @return {ol.PreRenderFunction} Pre-render function.
* @api
*/
ol.animation.zoom = function(options) {
ol.DEBUG && console.warn('ol.animation.zoom() is deprecated. Use view.animate() instead.');
var sourceResolution = options.resolution;
var start = options.start ? options.start : Date.now();
var duration = options.duration !== undefined ? options.duration : 1000;

View File

@@ -3,7 +3,6 @@ goog.provide('ol.control.Rotate');
goog.require('ol.events');
goog.require('ol.events.EventType');
goog.require('ol');
goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');
@@ -131,13 +130,14 @@ ol.control.Rotate.prototype.resetNorth_ = function() {
if (currentRotation > Math.PI) {
currentRotation -= 2 * Math.PI;
}
map.beforeRender(ol.animation.rotate({
rotation: currentRotation,
view.animate({
rotation: 0,
duration: this.duration_,
easing: ol.easing.easeOut
}));
});
} else {
view.setRotation(0);
}
view.setRotation(0);
}
};

View File

@@ -3,7 +3,6 @@ goog.provide('ol.control.Zoom');
goog.require('ol');
goog.require('ol.events');
goog.require('ol.events.EventType');
goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');
@@ -105,14 +104,15 @@ ol.control.Zoom.prototype.zoomByDelta_ = function(delta) {
}
var currentResolution = view.getResolution();
if (currentResolution) {
var newResolution = view.constrainResolution(currentResolution, delta);
if (this.duration_ > 0) {
map.beforeRender(ol.animation.zoom({
resolution: currentResolution,
view.animate({
resolution: newResolution,
duration: this.duration_,
easing: ol.easing.easeOut
}));
});
} else {
view.setResolution(newResolution);
}
var newResolution = view.constrainResolution(currentResolution, delta);
view.setResolution(newResolution);
}
};

View File

@@ -4,7 +4,6 @@ goog.provide('ol.control.ZoomSlider');
goog.require('ol');
goog.require('ol.View');
goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');
@@ -233,19 +232,19 @@ ol.control.ZoomSlider.render = function(mapEvent) {
* @private
*/
ol.control.ZoomSlider.prototype.handleContainerClick_ = function(event) {
var map = this.getMap();
var view = map.getView();
var currentResolution = view.getResolution();
map.beforeRender(ol.animation.zoom({
resolution: /** @type {number} */ (currentResolution),
duration: this.duration_,
easing: ol.easing.easeOut
}));
var view = this.getMap().getView();
var relativePosition = this.getRelativePosition_(
event.offsetX - this.thumbSize_[0] / 2,
event.offsetY - this.thumbSize_[1] / 2);
var resolution = this.getResolutionForPosition_(relativePosition);
view.setResolution(view.constrainResolution(resolution));
view.animate({
resolution: view.constrainResolution(resolution),
duration: this.duration_,
easing: ol.easing.easeOut
});
};
@@ -306,16 +305,15 @@ ol.control.ZoomSlider.prototype.handleDraggerDrag_ = function(event) {
*/
ol.control.ZoomSlider.prototype.handleDraggerEnd_ = function(event) {
if (this.dragging_) {
var map = this.getMap();
var view = map.getView();
var view = this.getMap().getView();
view.setHint(ol.View.Hint.INTERACTING, -1);
map.beforeRender(ol.animation.zoom({
resolution: /** @type {number} */ (this.currentResolution_),
view.animate({
resolution: view.constrainResolution(this.currentResolution_),
duration: this.duration_,
easing: ol.easing.easeOut
}));
var resolution = view.constrainResolution(this.currentResolution_);
view.setResolution(resolution);
});
this.dragging_ = false;
this.previousX_ = undefined;
this.previousY_ = undefined;

View File

@@ -1,4 +1,4 @@
/**
* Easing functions for {@link ol.animation}.
* Easing functions for {@link ol.View#animate}.
* @namespace ol.easing
*/

View File

@@ -3,6 +3,7 @@ goog.provide('ol.interaction.DragPan');
goog.require('ol');
goog.require('ol.View');
goog.require('ol.coordinate');
goog.require('ol.easing');
goog.require('ol.events.condition');
goog.require('ol.functions');
goog.require('ol.interaction.Pointer');
@@ -33,12 +34,6 @@ ol.interaction.DragPan = function(opt_options) {
*/
this.kinetic_ = options.kinetic;
/**
* @private
* @type {?ol.PreRenderFunction}
*/
this.kineticPreRenderFn_ = null;
/**
* @type {ol.Pixel}
*/
@@ -105,15 +100,16 @@ ol.interaction.DragPan.handleUpEvent_ = function(mapBrowserEvent) {
var distance = this.kinetic_.getDistance();
var angle = this.kinetic_.getAngle();
var center = /** @type {!ol.Coordinate} */ (view.getCenter());
this.kineticPreRenderFn_ = this.kinetic_.pan(center);
map.beforeRender(this.kineticPreRenderFn_);
var centerpx = map.getPixelFromCoordinate(center);
var dest = map.getCoordinateFromPixel([
centerpx[0] - distance * Math.cos(angle),
centerpx[1] - distance * Math.sin(angle)
]);
dest = view.constrainCenter(dest);
view.setCenter(dest);
view.animate({
center: view.constrainCenter(dest),
duration: 500,
easing: ol.easing.easeOut
});
} else {
// the view is not updated, force a render
map.render();
@@ -141,11 +137,8 @@ ol.interaction.DragPan.handleDownEvent_ = function(mapBrowserEvent) {
if (!this.handlingDownUpSequence) {
view.setHint(ol.View.Hint.INTERACTING, 1);
}
if (this.kineticPreRenderFn_ &&
map.removePreRenderFunction(this.kineticPreRenderFn_)) {
view.setCenter(mapBrowserEvent.frameState.viewState.center);
this.kineticPreRenderFn_ = null;
}
// stop any current animation
view.setCenter(mapBrowserEvent.frameState.viewState.center);
if (this.kinetic_) {
this.kinetic_.begin();
}

View File

@@ -1,7 +1,6 @@
goog.provide('ol.interaction.DragZoom');
goog.require('ol');
goog.require('ol.animation');
goog.require('ol.easing');
goog.require('ol.events.condition');
goog.require('ol.extent');
@@ -75,21 +74,11 @@ ol.interaction.DragZoom.prototype.onBoxEnd = function() {
var resolution = view.constrainResolution(
view.getResolutionForExtent(extent, size));
var currentResolution = /** @type {number} */ (view.getResolution());
var currentCenter = /** @type {!ol.Coordinate} */ (view.getCenter());
map.beforeRender(ol.animation.zoom({
resolution: currentResolution,
view.animate({
resolution: resolution,
center: ol.extent.getCenter(extent),
duration: this.duration_,
easing: ol.easing.easeOut
}));
map.beforeRender(ol.animation.pan({
source: currentCenter,
duration: this.duration_,
easing: ol.easing.easeOut
}));
});
view.setCenter(ol.extent.getCenter(extent));
view.setResolution(resolution);
};

View File

@@ -4,7 +4,6 @@ goog.provide('ol.interaction.Interaction');
goog.require('ol');
goog.require('ol.Object');
goog.require('ol.animation');
goog.require('ol.easing');
@@ -99,16 +98,17 @@ ol.interaction.Interaction.prototype.setMap = function(map) {
ol.interaction.Interaction.pan = function(map, view, delta, opt_duration) {
var currentCenter = view.getCenter();
if (currentCenter) {
if (opt_duration && opt_duration > 0) {
map.beforeRender(ol.animation.pan({
source: currentCenter,
duration: opt_duration,
easing: ol.easing.linear
}));
}
var center = view.constrainCenter(
[currentCenter[0] + delta[0], currentCenter[1] + delta[1]]);
view.setCenter(center);
if (opt_duration) {
view.animate({
duration: opt_duration,
easing: ol.easing.linear,
center: center
});
} else {
view.setCenter(center);
}
}
};
@@ -138,22 +138,16 @@ ol.interaction.Interaction.rotateWithoutConstraints = function(map, view, rotati
if (rotation !== undefined) {
var currentRotation = view.getRotation();
var currentCenter = view.getCenter();
if (currentRotation !== undefined && currentCenter &&
opt_duration && opt_duration > 0) {
map.beforeRender(ol.animation.rotate({
rotation: currentRotation,
if (currentRotation !== undefined && currentCenter && opt_duration > 0) {
view.animate({
rotation: rotation,
anchor: opt_anchor,
duration: opt_duration,
easing: ol.easing.easeOut
}));
if (opt_anchor) {
map.beforeRender(ol.animation.pan({
source: currentCenter,
duration: opt_duration,
easing: ol.easing.easeOut
}));
}
});
} else {
view.rotate(rotation, opt_anchor);
}
view.rotate(rotation, opt_anchor);
}
};
@@ -207,26 +201,20 @@ ol.interaction.Interaction.zoomWithoutConstraints = function(map, view, resoluti
var currentResolution = view.getResolution();
var currentCenter = view.getCenter();
if (currentResolution !== undefined && currentCenter &&
resolution !== currentResolution &&
opt_duration && opt_duration > 0) {
map.beforeRender(ol.animation.zoom({
resolution: currentResolution,
resolution !== currentResolution && opt_duration) {
view.animate({
resolution: resolution,
anchor: opt_anchor,
duration: opt_duration,
easing: ol.easing.easeOut
}));
});
} else {
if (opt_anchor) {
map.beforeRender(ol.animation.pan({
source: currentCenter,
duration: opt_duration,
easing: ol.easing.easeOut
}));
var center = view.calculateCenterZoom(resolution, opt_anchor);
view.setCenter(center);
}
view.setResolution(resolution);
}
if (opt_anchor) {
var center = view.calculateCenterZoom(resolution, opt_anchor);
view.setCenter(center);
}
view.setResolution(resolution);
}
};

View File

@@ -1,7 +1,5 @@
goog.provide('ol.Kinetic');
goog.require('ol.animation');
/**
* @classdesc
@@ -105,32 +103,6 @@ ol.Kinetic.prototype.end = function() {
};
/**
* @param {ol.Coordinate} source Source coordinate for the animation.
* @return {ol.PreRenderFunction} Pre-render function for kinetic animation.
*/
ol.Kinetic.prototype.pan = function(source) {
var decay = this.decay_;
var initialVelocity = this.initialVelocity_;
var velocity = this.minVelocity_ - initialVelocity;
var duration = this.getDuration_();
var easingFunction = (
/**
* @param {number} t T.
* @return {number} Easing.
*/
function(t) {
return initialVelocity * (Math.exp((decay * t) * duration) - 1) /
velocity;
});
return ol.animation.pan({
source: source,
duration: duration,
easing: easingFunction
});
};
/**
* @private
* @return {number} Duration of animation (milliseconds).

View File

@@ -512,6 +512,7 @@ ol.Map.prototype.addOverlayInternal_ = function(overlay) {
/**
* Deprecated (use {@link ol.View#animate} instead).
* Add functions to be called before rendering. This can be used for attaching
* animations before updating the map's view. The {@link ol.animation}
* namespace provides several static methods for creating prerender functions.
@@ -519,6 +520,7 @@ ol.Map.prototype.addOverlayInternal_ = function(overlay) {
* @api
*/
ol.Map.prototype.beforeRender = function(var_args) {
ol.DEBUG && console.warn('map.beforeRender() is deprecated. Use view.animate() instead.');
this.render();
Array.prototype.push.apply(this.preRenderFunctions_, arguments);
};

View File

@@ -3,7 +3,6 @@ goog.provide('ol.Overlay');
goog.require('ol');
goog.require('ol.MapEvent');
goog.require('ol.Object');
goog.require('ol.animation');
goog.require('ol.dom');
goog.require('ol.events');
goog.require('ol.extent');
@@ -69,10 +68,10 @@ ol.Overlay = function(options) {
/**
* @private
* @type {olx.animation.PanOptions}
* @type {olx.OverlayPanOptions}
*/
this.autoPanAnimation_ = options.autoPanAnimation !== undefined ?
options.autoPanAnimation : /** @type {olx.animation.PanOptions} */ ({});
this.autoPanAnimation_ = options.autoPanAnimation ||
/** @type {olx.OverlayPanOptions} */ ({});
/**
* @private
@@ -380,11 +379,11 @@ ol.Overlay.prototype.panIntoView_ = function() {
centerPx[1] + delta[1]
];
if (this.autoPanAnimation_) {
this.autoPanAnimation_.source = center;
map.beforeRender(ol.animation.pan(this.autoPanAnimation_));
}
map.getView().setCenter(map.getCoordinateFromPixel(newCenterPx));
map.getView().animate({
center: map.getCoordinateFromPixel(newCenterPx),
duration: this.autoPanAnimation_.duration,
easing: this.autoPanAnimation_.easing
});
}
}
};

View File

@@ -652,6 +652,27 @@ ol.TileUrlFunctionType;
ol.TransformFunction;
/**
* An animation configuration
*
* @typedef {{
* sourceCenter: (ol.Coordinate|undefined),
* targetCenter: (ol.Coordinate|undefined),
* sourceResolution: (number|undefined),
* targetResolution: (number|undefined),
* sourceRotation: (number|undefined),
* targetRotation: (number|undefined),
* anchor: (ol.Coordinate|undefined),
* start: number,
* duration: number,
* complete: boolean,
* easing: function(number):number,
* callback: (function(boolean)|undefined)
* }}
*/
ol.ViewAnimation;
/**
* @typedef {{buf: ol.webgl.Buffer,
* buffer: WebGLBuffer}}

View File

@@ -9,6 +9,7 @@ goog.require('ol.RotationConstraint');
goog.require('ol.array');
goog.require('ol.asserts');
goog.require('ol.coordinate');
goog.require('ol.easing');
goog.require('ol.extent');
goog.require('ol.geom.Polygon');
goog.require('ol.geom.SimpleGeometry');
@@ -84,6 +85,20 @@ ol.View = function(opt_options) {
*/
this.hints_ = [0, 0];
/**
* @private
* @type {Array.<Array.<ol.ViewAnimation>>}
*/
this.animations_ = [];
/**
* @private
* @type {number|undefined}
*/
this.updateAnimationKey_;
this.updateAnimations_ = this.updateAnimations_.bind(this);
/**
* @type {Object.<string, *>}
*/
@@ -155,6 +170,194 @@ ol.View = function(opt_options) {
ol.inherits(ol.View, ol.Object);
/**
* Animate the view. The view's center, zoom (or resolution), and rotation
* can be animated for smooth transitions between view states. For example,
* to animate the view to a new zoom level:
*
* view.animate({zoom: view.getZoom() + 1});
*
* By default, the animation lasts one second and uses in-and-out easing. You
* can customize this behavior by including `duration` (in milliseconds) and
* `easing` options (see {@link ol.easing}).
*
* To chain together multiple animations, call the method with multiple
* animation objects. For example, to first zoom and then pan:
*
* view.animate({zoom: 10}, {center: [0, 0]});
*
* If you provide a function as the last argument to the animate method, it
* will get called at the end of an animation series. The callback will be
* called with `true` if the animation series completed on its own or `false`
* if it was cancelled.
*
* Animations are cancelled by user interactions (e.g. dragging the map) or by
* calling `view.setCenter()`, `view.setResolution()`, or `view.setRotation()`
* (or another method that calls one of these).
*
* @param {...(olx.AnimationOptions|function(boolean))} var_args Animation
* options. Multiple animations can be run in series by passing multiple
* options objects. To run multiple animations in parallel, call the method
* multiple times. An optional callback can be provided as a final
* argument. The callback will be called with a boolean indicating whether
* the animation completed without being cancelled.
* @api
*/
ol.View.prototype.animate = function(var_args) {
var start = Date.now();
var center = this.getCenter().slice();
var resolution = this.getResolution();
var rotation = this.getRotation();
var animationCount = arguments.length;
var callback;
if (animationCount > 1 && typeof arguments[animationCount - 1] === 'function') {
callback = arguments[animationCount - 1];
--animationCount;
}
var series = [];
for (var i = 0; i < animationCount; ++i) {
var options = /** @type olx.AnimationOptions */ (arguments[i]);
var animation = /** @type {ol.ViewAnimation} */ ({
start: start,
complete: false,
anchor: options.anchor,
duration: options.duration || 1000,
easing: options.easing || ol.easing.inAndOut
});
if (options.center) {
animation.sourceCenter = center;
animation.targetCenter = options.center;
center = animation.targetCenter;
}
if (options.zoom !== undefined) {
animation.sourceResolution = resolution;
animation.targetResolution = this.constrainResolution(
this.maxResolution_, options.zoom - this.minZoom_, 0);
resolution = animation.targetResolution;
} else if (options.resolution) {
animation.sourceResolution = this.getResolution();
animation.targetResolution = options.resolution;
resolution = animation.targetResolution;
}
if (options.rotation !== undefined) {
animation.sourceRotation = rotation;
animation.targetRotation = options.rotation;
rotation = animation.targetRotation;
}
animation.callback = callback;
start += animation.duration;
series.push(animation);
}
this.animations_.push(series);
this.setHint(ol.View.Hint.ANIMATING, 1);
this.updateAnimations_();
};
/**
* Determine if the view is being animated.
* @return {boolean} The view is being animated.
*/
ol.View.prototype.getAnimating = function() {
return this.getHints()[ol.View.Hint.ANIMATING] > 0;
};
/**
* Cancel any ongoing animations.
*/
ol.View.prototype.cancelAnimations_ = function() {
for (var i = 0, ii = this.animations_.length; i < ii; ++i) {
var series = this.animations_[i];
if (series[0].callback) {
series[0].callback(false);
}
}
this.animations_.length = 0;
this.setHint(ol.View.Hint.ANIMATING, -this.getHints()[ol.View.Hint.ANIMATING]);
};
/**
* Update all animations.
*/
ol.View.prototype.updateAnimations_ = function() {
if (this.updateAnimationKey_ !== undefined) {
cancelAnimationFrame(this.updateAnimationKey_);
this.updateAnimationKey_ = undefined;
}
if (!this.getAnimating()) {
return;
}
var now = Date.now();
var more = false;
for (var i = this.animations_.length - 1; i >= 0; --i) {
var series = this.animations_[i];
var seriesComplete = true;
for (var j = 0, jj = series.length; j < jj; ++j) {
var animation = series[j];
if (animation.complete) {
continue;
}
var elapsed = now - animation.start;
var fraction = elapsed / animation.duration;
if (fraction > 1) {
animation.complete = true;
fraction = 1;
} else {
seriesComplete = false;
}
var progress = animation.easing(fraction);
if (animation.sourceCenter) {
var x0 = animation.sourceCenter[0];
var y0 = animation.sourceCenter[1];
var x1 = animation.targetCenter[0];
var y1 = animation.targetCenter[1];
var x = x0 + progress * (x1 - x0);
var y = y0 + progress * (y1 - y0);
this.set(ol.View.Property.CENTER, [x, y]);
}
if (animation.sourceResolution) {
var resolution = animation.sourceResolution +
progress * (animation.targetResolution - animation.sourceResolution);
if (animation.anchor) {
this.set(ol.View.Property.CENTER,
this.calculateCenterZoom(resolution, animation.anchor));
}
this.set(ol.View.Property.RESOLUTION, resolution);
}
if (animation.sourceRotation !== undefined) {
var rotation = animation.sourceRotation +
progress * (animation.targetRotation - animation.sourceRotation);
if (animation.anchor) {
this.set(ol.View.Property.CENTER,
this.calculateCenterRotate(rotation, animation.anchor));
}
this.set(ol.View.Property.ROTATION, rotation);
}
more = true;
if (!animation.complete) {
break;
}
}
if (seriesComplete) {
this.setHint(ol.View.Hint.ANIMATING, -1);
var completed = this.animations_.pop();
var callback = completed[0].callback;
if (callback) {
callback(true);
}
}
}
if (more && this.updateAnimationKey_ === undefined) {
this.updateAnimationKey_ = requestAnimationFrame(this.updateAnimations_);
}
};
/**
* @param {number} rotation Target rotation.
* @param {ol.Coordinate} anchor Rotation anchor.
@@ -603,6 +806,9 @@ ol.View.prototype.rotate = function(rotation, opt_anchor) {
*/
ol.View.prototype.setCenter = function(center) {
this.set(ol.View.Property.CENTER, center);
if (this.getAnimating()) {
this.cancelAnimations_();
}
};
@@ -629,6 +835,9 @@ ol.View.prototype.setHint = function(hint, delta) {
*/
ol.View.prototype.setResolution = function(resolution) {
this.set(ol.View.Property.RESOLUTION, resolution);
if (this.getAnimating()) {
this.cancelAnimations_();
}
};
@@ -640,6 +849,9 @@ ol.View.prototype.setResolution = function(resolution) {
*/
ol.View.prototype.setRotation = function(rotation) {
this.set(ol.View.Property.ROTATION, rotation);
if (this.getAnimating()) {
this.cancelAnimations_();
}
};

View File

@@ -301,6 +301,191 @@ describe('ol.View', function() {
});
describe('#animate()', function() {
var originalRequestAnimationFrame = window.requestAnimationFrame;
var originalCancelAnimationFrame = window.cancelAnimationFrame;
beforeEach(function() {
window.requestAnimationFrame = function(callback) {
return setTimeout(callback, 1);
};
window.cancelAnimationFrame = function(key) {
return clearTimeout(key);
};
});
afterEach(function() {
window.requestAnimationFrame = originalRequestAnimationFrame;
window.cancelAnimationFrame = originalCancelAnimationFrame;
});
it('can be called to animate view properties', function(done) {
var view = new ol.View({
center: [0, 0],
zoom: 5
});
view.animate({
zoom: 4,
duration: 25
});
expect(view.getAnimating()).to.eql(true);
setTimeout(function() {
expect(view.getCenter()).to.eql([0, 0]);
expect(view.getZoom()).to.eql(4);
expect(view.getAnimating()).to.eql(false);
done();
}, 50);
});
it('prefers zoom over resolution', function(done) {
var view = new ol.View({
center: [0, 0],
zoom: 5
});
view.animate({
zoom: 4,
resolution: view.getResolution() * 3,
duration: 25
}, function(complete) {
expect(complete).to.be(true);
expect(view.getZoom()).to.be(4);
done();
});
});
it('calls a callback when animation completes', function(done) {
var view = new ol.View({
center: [0, 0],
zoom: 0
});
view.animate({
zoom: 1,
duration: 25
}, function(complete) {
expect(complete).to.be(true);
done();
});
});
it('calls callback with false when animation is interrupted', function(done) {
var view = new ol.View({
center: [0, 0],
zoom: 0
});
view.animate({
zoom: 1,
duration: 25
}, function(complete) {
expect(complete).to.be(false);
done();
});
view.setCenter([1, 2]); // interrupt the animation
});
it('can run multiple animations in series', function(done) {
var view = new ol.View({
center: [0, 0],
zoom: 0
});
var checked = false;
view.animate({
zoom: 2,
duration: 25
}, {
center: [10, 10],
duration: 25
}, function(complete) {
expect(checked).to.be(true);
expect(view.getZoom()).to.roughlyEqual(2, 1e-5);
expect(view.getCenter()).to.eql([10, 10]);
expect(complete).to.be(true);
done();
});
setTimeout(function() {
expect(view.getCenter()).to.eql([0, 0]);
checked = true;
}, 10);
});
it('properly sets the ANIMATING hint', function(done) {
var view = new ol.View({
center: [0, 0],
zoom: 0,
rotation: 0
});
var count = 3;
function decrement() {
--count;
if (count === 0) {
expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(0);
done();
}
}
view.animate({
center: [1, 2],
duration: 25
}, decrement);
expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(1);
view.animate({
zoom: 1,
duration: 25
}, decrement);
expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(2);
view.animate({
rotate: Math.PI,
duration: 25
}, decrement);
expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(3);
});
it('clears the ANIMATING hint when animations are cancelled', function() {
var view = new ol.View({
center: [0, 0],
zoom: 0,
rotation: 0
});
view.animate({
center: [1, 2],
duration: 25
});
expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(1);
view.animate({
zoom: 1,
duration: 25
});
expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(2);
view.animate({
rotate: Math.PI,
duration: 25
});
expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(3);
// cancel animations
view.setCenter([10, 20]);
expect(view.getHints()[ol.View.Hint.ANIMATING]).to.be(0);
});
});
describe('#getResolutions', function() {
var view;
var resolutions = [512, 256, 128, 64, 32, 16];