From 40a824fc97cab99547850d75c088aae677472ae6 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 20 Feb 2013 18:40:00 +0200 Subject: [PATCH] refactor TileLayer animation, fix #1140, #1437, #52, #1442 Refactored TileLayer animation so that it happens for each tile layer independently instead of animating the parent of all tile layers. Moved TileLayer animation code into a separate file (TileLayer.Anim.js). Fixes loads of bugs and makes the code easier to understand. --- build/deps.js | 2 +- src/layer/tile/TileLayer.Anim.js | 117 ++++++++++++++++++++++++++++ src/layer/tile/TileLayer.js | 58 +++++++++----- src/map/Map.js | 7 +- src/map/anim/Map.ZoomAnimation.js | 122 ++++-------------------------- src/map/handler/Map.TouchZoom.js | 30 ++------ 6 files changed, 185 insertions(+), 151 deletions(-) create mode 100644 src/layer/tile/TileLayer.Anim.js diff --git a/build/deps.js b/build/deps.js index 696a5502..ee4972fd 100644 --- a/build/deps.js +++ b/build/deps.js @@ -245,7 +245,7 @@ var deps = { }, AnimationZoom: { - src: ['map/anim/Map.ZoomAnimation.js'], + src: ['map/anim/Map.ZoomAnimation.js', 'layer/tile/TileLayer.Anim.js'], deps: ['AnimationPan'], desc: 'Smooth zooming animation. Works only on browsers that support CSS3 Transitions.' }, diff --git a/src/layer/tile/TileLayer.Anim.js b/src/layer/tile/TileLayer.Anim.js new file mode 100644 index 00000000..c0ef4c59 --- /dev/null +++ b/src/layer/tile/TileLayer.Anim.js @@ -0,0 +1,117 @@ +/* + Zoom animation logic for L.TileLayer. +*/ + +L.TileLayer.include({ + _animateZoom: function (e) { + var firstFrame = false; + + if (!this._animating) { + this._animating = true; + firstFrame = true; + } + + if (firstFrame) { + this._prepareBgBuffer(); + } + + var transform = L.DomUtil.TRANSFORM, + bg = this._bgBuffer; + + if (firstFrame) { + //prevent bg buffer from clearing right after zoom + clearTimeout(this._clearBgBufferTimer); + + // hack to make sure transform is updated before running animation + L.Util.falseFn(bg.offsetWidth); + } + + var scaleStr = L.DomUtil.getScaleString(e.scale, e.origin), + oldTransform = bg.style[transform]; + + bg.style[transform] = e.backwards ? + (e.delta ? L.DomUtil.getTranslateString(e.delta) : oldTransform) + ' ' + scaleStr : + scaleStr + ' ' + oldTransform; + }, + + _endZoomAnim: function () { + var front = this._tileContainer, + bg = this._bgBuffer; + + front.style.visibility = ''; + front.style.zIndex = 2; + + bg.style.zIndex = 1; + + // force reflow + L.Util.falseFn(bg.offsetWidth); + + this._animating = false; + }, + + _clearBgBuffer: function () { + var map = this._map; + + if (!map._animatingZoom && !map.touchZoom._zooming) { + this._bgBuffer.innerHTML = ''; + this._bgBuffer.style[L.DomUtil.TRANSFORM] = ''; + } + }, + + _prepareBgBuffer: function () { + + var front = this._tileContainer, + bg = this._bgBuffer; + + // if foreground layer doesn't have many tiles but bg layer does, + // keep the existing bg layer and just zoom it some more + + if (bg && this._getLoadedTilesPercentage(bg) > 0.5 && + this._getLoadedTilesPercentage(front) < 0.5) { + + front.style.visibility = 'hidden'; + this._stopLoadingImages(front); + return; + } + + // prepare the buffer to become the front tile pane + bg.style.visibility = 'hidden'; + bg.style[L.DomUtil.TRANSFORM] = ''; + + // switch out the current layer to be the new bg layer (and vice-versa) + this._tileContainer = bg; + bg = this._bgBuffer = front; + + this._stopLoadingImages(bg); + }, + + _getLoadedTilesPercentage: function (container) { + var tiles = container.getElementsByTagName('img'), + i, len, count = 0; + + for (i = 0, len = tiles.length; i < len; i++) { + if (tiles[i].complete) { + count++; + } + } + return count / len; + }, + + // stops loading all tiles in the background layer + _stopLoadingImages: function (container) { + var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')), + i, len, tile; + + for (i = 0, len = tiles.length; i < len; i++) { + tile = tiles[i]; + + if (!tile.complete) { + tile.onload = L.Util.falseFn; + tile.onerror = L.Util.falseFn; + tile.src = L.Util.emptyImageUrl; + + tile.parentNode.removeChild(tile); + } + } + } +}); diff --git a/src/layer/tile/TileLayer.js b/src/layer/tile/TileLayer.js index 25c655c5..1e8fdf20 100644 --- a/src/layer/tile/TileLayer.js +++ b/src/layer/tile/TileLayer.js @@ -53,6 +53,7 @@ L.TileLayer = L.Class.extend({ onAdd: function (map) { this._map = map; + this._animated = map.options.zoomAnimation && L.Browser.any3d; // create a container div for tiles this._initContainer(); @@ -62,10 +63,17 @@ L.TileLayer = L.Class.extend({ // set up events map.on({ - 'viewreset': this._resetCallback, + 'viewreset': this._reset, 'moveend': this._update }, this); + if (this._animated) { + map.on({ + 'zoomanim': this._animateZoom, + 'zoomend': this._endZoomAnim + }, this); + } + if (!this.options.updateWhenIdle) { this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this); map.on('move', this._limitedUpdate, this); @@ -84,10 +92,17 @@ L.TileLayer = L.Class.extend({ this._container.parentNode.removeChild(this._container); map.off({ - 'viewreset': this._resetCallback, + 'viewreset': this._reset, 'moveend': this._update }, this); + if (this._animated) { + map.off({ + 'zoomanim': this._animateZoom, + 'zoomend': this._endZoomAnim + }, this); + } + if (!this.options.updateWhenIdle) { map.off('move', this._limitedUpdate, this); } @@ -151,8 +166,7 @@ L.TileLayer = L.Class.extend({ redraw: function () { if (this._map) { - this._map._panes.tilePane.empty = false; - this._reset(true); + this._reset({hard: true}); this._update(); } return this; @@ -212,11 +226,20 @@ L.TileLayer = L.Class.extend({ _initContainer: function () { var tilePane = this._map._panes.tilePane; - if (!this._container || tilePane.empty) { + if (!this._container) { this._container = L.DomUtil.create('div', 'leaflet-layer'); this._updateZIndex(); + if (this._animated) { + var className = 'leaflet-tile-container leaflet-zoom-animated'; + + this._bgBuffer = L.DomUtil.create('div', className, this._container); + this._tileContainer = L.DomUtil.create('div', className, this._container); + } else { + this._tileContainer = this._container; + } + tilePane.appendChild(this._container); if (this.options.opacity < 1) { @@ -225,11 +248,7 @@ L.TileLayer = L.Class.extend({ } }, - _resetCallback: function (e) { - this._reset(e.hard); - }, - - _reset: function (clearOldContainer) { + _reset: function (e) { var tiles = this._tiles; for (var key in tiles) { @@ -245,8 +264,10 @@ L.TileLayer = L.Class.extend({ this._unusedTiles = []; } - if (clearOldContainer && this._container) { - this._container.innerHTML = ""; + this._tileContainer.innerHTML = ""; + + if (this._animated && e && e.hard) { + this._clearBgBuffer(); } this._initContainer(); @@ -319,7 +340,7 @@ L.TileLayer = L.Class.extend({ this._addTile(queue[i], fragment); } - this._container.appendChild(fragment); + this._tileContainer.appendChild(fragment); }, _tileShouldBeLoaded: function (tilePoint) { @@ -365,8 +386,8 @@ L.TileLayer = L.Class.extend({ L.DomUtil.removeClass(tile, 'leaflet-tile-loaded'); this._unusedTiles.push(tile); - } else if (tile.parentNode === this._container) { - this._container.removeChild(tile); + } else if (tile.parentNode === this._tileContainer) { + this._tileContainer.removeChild(tile); } // for https://github.com/CloudMade/Leaflet/issues/137 @@ -386,7 +407,6 @@ L.TileLayer = L.Class.extend({ /* Chrome 20 layouts much faster with top/left (verify with timeline, frames) Android 4 browser has display issues with top/left and requires transform instead - Android 3 browser not tested Android 2 browser requires top/left or tiles disappear on load or first drag (reappear after zoom) https://github.com/CloudMade/Leaflet/issues/866 (other browsers don't currently care) - see debug/hacks/jitter.html for an example @@ -397,7 +417,7 @@ L.TileLayer = L.Class.extend({ this._loadTile(tile, tilePoint); - if (tile.parentNode !== this._container) { + if (tile.parentNode !== this._tileContainer) { container.appendChild(tile); } }, @@ -499,6 +519,10 @@ L.TileLayer = L.Class.extend({ this._tilesToLoad--; if (!this._tilesToLoad) { this.fire('load'); + + // clear scaled tiles after all new tiles are loaded (for performance) + clearTimeout(this._clearBgBufferTimer); + this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 500); } }, diff --git a/src/map/Map.js b/src/map/Map.js index f1f41647..c681d6ed 100644 --- a/src/map/Map.js +++ b/src/map/Map.js @@ -628,12 +628,9 @@ L.Map = L.Class.extend({ }, _onTileLayerLoad: function () { - // TODO super-ugly, refactor!!! - // clear scaled tiles after all new tiles are loaded (for performance) this._tileLayersToLoad--; - if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) { - clearTimeout(this._clearTileBgTimer); - this._clearTileBgTimer = setTimeout(L.bind(this._clearTileBg, this), 500); + if (this._tileLayersNum && !this._tileLayersToLoad) { + this.fire('tilelayersload'); } }, diff --git a/src/map/anim/Map.ZoomAnimation.js b/src/map/anim/Map.ZoomAnimation.js index 87387701..f15fc574 100644 --- a/src/map/anim/Map.ZoomAnimation.js +++ b/src/map/anim/Map.ZoomAnimation.js @@ -26,32 +26,21 @@ L.Map.include(!L.DomUtil.TRANSITION ? {} : { // if offset does not exceed half of the view if (!this._offsetIsWithinView(offset, 1)) { return false; } - L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim'); - this .fire('movestart') .fire('zoomstart'); - this.fire('zoomanim', { - center: center, - zoom: zoom - }); - var origin = this._getCenterLayerPoint().add(offset); - this._prepareTileBg(); - this._runAnimation(center, zoom, scale, origin); + this._animateZoom(center, zoom, origin, scale); return true; }, - _catchTransitionEnd: function () { - if (this._animatingZoom) { - this._onZoomTransitionEnd(); - } - }, + _animateZoom: function (center, zoom, origin, scale, delta, backwards) { + + L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim'); - _runAnimation: function (center, zoom, scale, origin, backwardsTransform) { this._animateToCenter = center; this._animateToZoom = zoom; this._animatingZoom = true; @@ -60,110 +49,31 @@ L.Map.include(!L.DomUtil.TRANSITION ? {} : { L.Draggable._disabled = true; } - var transform = L.DomUtil.TRANSFORM, - tileBg = this._tileBg; - - clearTimeout(this._clearTileBgTimer); - - L.Util.falseFn(tileBg.offsetWidth); //hack to make sure transform is updated before running animation - - var scaleStr = L.DomUtil.getScaleString(scale, origin), - oldTransform = tileBg.style[transform]; - - tileBg.style[transform] = backwardsTransform ? - oldTransform + ' ' + scaleStr : - scaleStr + ' ' + oldTransform; + this.fire('zoomanim', { + center: center, + zoom: zoom, + origin: origin, + scale: scale, + delta: delta, + backwards: backwards + }); }, - _prepareTileBg: function () { - var tilePane = this._tilePane, - tileBg = this._tileBg; - - // If foreground layer doesn't have many tiles but bg layer does, keep the existing bg layer and just zoom it some more - if (tileBg && this._getLoadedTilesPercentage(tileBg) > 0.5 && - this._getLoadedTilesPercentage(tilePane) < 0.5) { - - tilePane.style.visibility = 'hidden'; - tilePane.empty = true; - this._stopLoadingImages(tilePane); - return; - } - - if (!tileBg) { - tileBg = this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane); - tileBg.style.zIndex = 1; - } - - // prepare the background pane to become the main tile pane - tileBg.style[L.DomUtil.TRANSFORM] = ''; - tileBg.style.visibility = 'hidden'; - - // tells tile layers to reinitialize their containers - tileBg.empty = true; //new FG - tilePane.empty = false; //new BG - - //Switch out the current layer to be the new bg layer (And vice-versa) - this._tilePane = this._panes.tilePane = tileBg; - var newTileBg = this._tileBg = tilePane; - - L.DomUtil.addClass(newTileBg, 'leaflet-zoom-animated'); - - this._stopLoadingImages(newTileBg); - }, - - _getLoadedTilesPercentage: function (container) { - var tiles = container.getElementsByTagName('img'), - i, len, count = 0; - - for (i = 0, len = tiles.length; i < len; i++) { - if (tiles[i].complete) { - count++; - } - } - return count / len; - }, - - // stops loading all tiles in the background layer - _stopLoadingImages: function (container) { - var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')), - i, len, tile; - - for (i = 0, len = tiles.length; i < len; i++) { - tile = tiles[i]; - - if (!tile.complete) { - tile.onload = L.Util.falseFn; - tile.onerror = L.Util.falseFn; - tile.src = L.Util.emptyImageUrl; - - tile.parentNode.removeChild(tile); - } + _catchTransitionEnd: function () { + if (this._animatingZoom) { + this._onZoomTransitionEnd(); } }, _onZoomTransitionEnd: function () { - this._restoreTileFront(); L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim'); - L.Util.falseFn(this._tileBg.offsetWidth); // force reflow + this._animatingZoom = false; this._resetView(this._animateToCenter, this._animateToZoom, true, true); if (L.Draggable) { L.Draggable._disabled = false; } - }, - - _restoreTileFront: function () { - this._tilePane.innerHTML = ''; - this._tilePane.style.visibility = ''; - this._tilePane.style.zIndex = 2; - this._tileBg.style.zIndex = 1; - }, - - _clearTileBg: function () { - if (!this._animatingZoom && !this.touchZoom._zooming) { - this._tileBg.innerHTML = ''; - } } }); diff --git a/src/map/handler/Map.TouchZoom.js b/src/map/handler/Map.TouchZoom.js index adb5301c..9f3c56a3 100644 --- a/src/map/handler/Map.TouchZoom.js +++ b/src/map/handler/Map.TouchZoom.js @@ -57,12 +57,11 @@ L.Map.TouchZoom = L.Handler.extend({ if (this._scale === 1) { return; } if (!this._moved) { - L.DomUtil.addClass(map._mapPane, 'leaflet-zoom-anim leaflet-touching'); + L.DomUtil.addClass(map._mapPane, 'leaflet-touching'); map .fire('movestart') - .fire('zoomstart') - ._prepareTileBg(); + .fire('zoomstart'); this._moved = true; } @@ -77,19 +76,10 @@ L.Map.TouchZoom = L.Handler.extend({ _updateOnMove: function () { var map = this._map, origin = this._getScaleOrigin(), - center = map.layerPointToLatLng(origin); + center = map.layerPointToLatLng(origin), + zoom = map.getScaleZoom(this._scale); - map.fire('zoomanim', { - center: center, - zoom: map.getScaleZoom(this._scale) - }); - - // Used 2 translates instead of transform-origin because of a very strange bug - - // it didn't count the origin on the first touch-zoom but worked correctly afterwards - - map._tileBg.style[L.DomUtil.TRANSFORM] = - L.DomUtil.getTranslateString(this._delta) + ' ' + - L.DomUtil.getScaleString(this._scale, this._startCenter); + map._animateZoom(center, zoom, this._startCenter, this._scale, this._delta, true); }, _onTouchEnd: function () { @@ -112,14 +102,10 @@ L.Map.TouchZoom = L.Handler.extend({ roundZoomDelta = (floatZoomDelta > 0 ? Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)), - zoom = map._limitZoom(oldZoom + roundZoomDelta); + zoom = map._limitZoom(oldZoom + roundZoomDelta), + scale = map.getZoomScale(zoom) / this._scale; - map.fire('zoomanim', { - center: center, - zoom: zoom - }); - - map._runAnimation(center, zoom, map.getZoomScale(zoom) / this._scale, origin, true); + map._animateZoom(center, zoom, origin, scale, null, true); }, _getScaleOrigin: function () {