diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index e24b90d8c6..3573bf1471 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -40,7 +40,6 @@ var d3Renderer = function (arg) { m_diagonal = null, m_scale = 1, m_transform = {dx: 0, dy: 0, rx: 0, ry: 0, rotation: 0}, - m_renderAnimFrameRef = null, m_renderIds = {}, m_removeIds = {}, m_svg = null, @@ -509,9 +508,7 @@ var d3Renderer = function (arg) { m_this._renderFeature(id, parentId); } else { m_renderIds[id] = true; - if (m_renderAnimFrameRef === null) { - m_renderAnimFrameRef = window.requestAnimationFrame(m_this._renderFrame); - } + m_this.layer().map().scheduleAnimationFrame(m_this._renderFrame); } }; @@ -524,7 +521,6 @@ var d3Renderer = function (arg) { m_removeIds = {}; var ids = m_renderIds; m_renderIds = {}; - m_renderAnimFrameRef = null; for (id in ids) { if (ids.hasOwnProperty(id)) { m_this._renderFeature(id); @@ -578,9 +574,7 @@ var d3Renderer = function (arg) { //////////////////////////////////////////////////////////////////////////// this._removeFeature = function (id) { m_removeIds[id] = true; - if (m_renderAnimFrameRef === null) { - m_renderAnimFrameRef = window.requestAnimationFrame(m_this._renderFrame); - } + m_this.layer().map().scheduleAnimationFrame(m_this._renderFrame); delete m_features[id]; if (m_renderIds[id]) { delete m_renderIds[id]; diff --git a/src/gl/polygonFeature.js b/src/gl/polygonFeature.js index 443827e3c7..8c12bd6b66 100644 --- a/src/gl/polygonFeature.js +++ b/src/gl/polygonFeature.js @@ -340,11 +340,11 @@ var gl_polygonFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// this._update = function (opts) { if (opts && opts.mayDelay) { - m_updateAnimFrameRef = window.requestAnimationFrame(this._update); + m_updateAnimFrameRef = m_this.layer().map().scheduleAnimationFrame(m_this._update); return; } if (m_updateAnimFrameRef) { - window.cancelAnimationFrame(m_updateAnimFrameRef); + m_this.layer().map().scheduleAnimationFrame(m_this._update, 'remove'); m_updateAnimFrameRef = null; } s_update.call(m_this); diff --git a/src/gl/quadFeature.js b/src/gl/quadFeature.js index e0f105362b..54c594daca 100644 --- a/src/gl/quadFeature.js +++ b/src/gl/quadFeature.js @@ -239,6 +239,7 @@ var gl_quadFeature = function (arg) { if (m_clrModelViewUniform) { m_clrModelViewUniform.setOrigin(m_quads.origin); } + m_this._updateTextures(); m_this.buildTime().modified(); }; @@ -330,8 +331,6 @@ var gl_quadFeature = function (arg) { opacity = 1, crop = {x: 1, y: 1}, quadcrop; - m_this._updateTextures(); - context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition); $.each(m_quads.imgQuads, function (idx, quad) { if (!quad.image) { diff --git a/src/gl/vglRenderer.js b/src/gl/vglRenderer.js index 5525748dc2..21288d1995 100644 --- a/src/gl/vglRenderer.js +++ b/src/gl/vglRenderer.js @@ -32,8 +32,8 @@ var vglRenderer = function (arg) { m_viewer = null, m_width = 0, m_height = 0, - m_renderAnimFrameRef = null, m_lastZoom, + m_updateCamera = false, s_init = this._init, s_exit = this._exit; @@ -121,7 +121,7 @@ var vglRenderer = function (arg) { m_this.canvas().attr('height', h); renderWindow.positionAndResize(x, y, w, h); - m_this._updateRendererCamera(); + m_updateCamera = true; m_this._render(); return m_this; @@ -133,10 +133,13 @@ var vglRenderer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._render = function () { - if (m_renderAnimFrameRef) { - window.cancelAnimationFrame(m_renderAnimFrameRef); - } - m_renderAnimFrameRef = window.requestAnimationFrame(this._renderFrame); + /* If we are already scheduled to render, don't schedule again. Rather, + * mark that we should render after other animation frame requests occur. + * It would be nice if we could just reschedule the call by removing and + * readding the animation frame request, but this doesn't work for if the + * reschedule occurs during another animation frame callback (it then waits + * until a subsequent frame). */ + m_this.layer().map().scheduleAnimationFrame(this._renderFrame, true); return m_this; }; @@ -144,7 +147,10 @@ var vglRenderer = function (arg) { * This clears the render timer and actually renders. */ this._renderFrame = function () { - m_renderAnimFrameRef = null; + if (m_updateCamera) { + m_updateCamera = false; + m_this._updateRendererCamera(); + } m_viewer.render(); }; @@ -229,7 +235,7 @@ var vglRenderer = function (arg) { // produce a pan m_this.layer().geoOn(geo_event.pan, function (evt) { void (evt); - m_this._updateRendererCamera(); + m_updateCamera = true; }); // Connect to parallelprojection event @@ -242,10 +248,11 @@ var vglRenderer = function (arg) { if (!vglRenderer || !vglRenderer.camera()) { console.log('Parallel projection event triggered on unconnected VGL ' + 'renderer.'); + return; } camera = vglRenderer.camera(); camera.setEnableParallelProjection(evt.parallelProjection); - m_this._updateRendererCamera(); + m_updateCamera = true; } }); diff --git a/src/map.js b/src/map.js index 6160de52b0..0e24298322 100644 --- a/src/map.js +++ b/src/map.js @@ -57,6 +57,9 @@ var sceneObject = require('./sceneObject'); * @param {geo.camera?} camera The camera to control the view * @param {geo.mapInteractor?} interactor The UI event handler * @param {geo.clock?} clock The clock used to synchronize time events + * @param {array} [animationQueue] An array used to synchonize animations. If + * specified, this should be an empty array or the same array as passed to + * other map instances. * @param {boolean} [autoResize=true] Adjust map size on window resize * @param {boolean} [clampBoundsX=false] Prevent panning outside of the * maximum bounds in the horizontal direction. @@ -127,6 +130,7 @@ var map = function (arg) { m_clampBoundsX, m_clampBoundsY, m_clampZoom, + m_animationQueue = arg.animationQueue || [], m_origin, m_scale = {x: 1, y: 1, z: 1}; // constant and ignored for the moment @@ -1229,7 +1233,7 @@ var map = function (arg) { } m_this.rotation(p[3], undefined, true); - window.requestAnimationFrame(anim); + m_this.scheduleAnimationFrame(anim); } m_this.geoTrigger(geo_event.transitionstart, opts); @@ -1245,7 +1249,7 @@ var map = function (arg) { } else if (animTime) { anim(animTime); } else { - window.requestAnimationFrame(anim); + m_this.scheduleAnimationFrame(anim); } return m_this; }; @@ -1566,6 +1570,56 @@ var map = function (arg) { return m_this; }; + /** + * Instead of each function using window.requestAnimationFrame, schedule all + * such frames here. This allows the callbacks to be reordered or removed as + * needed and reduces overhead in Chrome a small amount. Also, if the + * animation queue is shared between map instances, the callbacks will be + * called as one, providing better synchronization. + * + * @param {function} callback: function to call during the animation frame. + * It is called with an animation epoch, exactly as requestAnimationFrame. + * @param {string|boolean} action: falsy to only add the callback if it is + * not already scheduled. 'remove' to remove the callback (use this + * instead of cancelAnimationFrame). Any other truthy value moves the + * callback to the end of the list. + * @returns {integer} An integer as returned by window.requestAnimationFrame. + */ + this.scheduleAnimationFrame = function (callback, action) { + if (!m_animationQueue.length) { + /* By refering to requestAnimationFrame as a property of window, versus + * explicitly using window.requestAnimationFrame, we prevent the + * stripping of 'window' off of the reference and allow our tests to + * override this if needed. */ + m_animationQueue.push(window['requestAnimationFrame'](processAnimationFrame)); + } + var pos = m_animationQueue.indexOf(callback, 1); + if (pos >= 0) { + if (!action) { + return; + } + m_animationQueue.splice(pos, 1); + if (action === 'remove') { + return; + } + } + m_animationQueue.push(callback); + return m_animationQueue[0]; + }; + + /** + * Sevice the callback during an animation frame. This uses splice to modify + * the animationQueue to allow multiple map instances to share the queue. + */ + function processAnimationFrame() { + var queue = m_animationQueue.splice(0, m_animationQueue.length); + + /* The first entry is the reference to the window.requestAnimationFrame. */ + for (var i = 1; i < queue.length; i += 1) { + queue[i].apply(this, arguments); + } + } + //////////////////////////////////////////////////////////////////////////// // // The following are some private methods for interacting with the camera. diff --git a/src/mapInteractor.js b/src/mapInteractor.js index 82981f8904..fa7ffc64bc 100644 --- a/src/mapInteractor.js +++ b/src/mapInteractor.js @@ -1431,11 +1431,11 @@ var mapInteractor = function (args) { } if (m_state.handler) { - window.requestAnimationFrame(m_state.handler); + m_this.map().scheduleAnimationFrame(m_state.handler); } }; if (m_state.handler) { - window.requestAnimationFrame(m_state.handler); + m_this.map().scheduleAnimationFrame(m_state.handler); } }; diff --git a/src/util/init.js b/src/util/init.js index 5d0ab28c66..297c44d6c4 100644 --- a/src/util/init.js +++ b/src/util/init.js @@ -781,7 +781,7 @@ } else if (!stop && !m_originalRequestAnimationFrame) { m_originalRequestAnimationFrame = window.requestAnimationFrame; window.requestAnimationFrame = function (callback) { - m_originalRequestAnimationFrame.call(window, function (timestamp) { + return m_originalRequestAnimationFrame.call(window, function (timestamp) { var track = m_timingData.requestAnimationFrame, recent; /* Some environments have unsynchronized performance and time * counters. The nowDelta factor compensates for this. For diff --git a/tests/cases/d3GraphFeature.js b/tests/cases/d3GraphFeature.js index ee4ff01663..104b25392b 100644 --- a/tests/cases/d3GraphFeature.js +++ b/tests/cases/d3GraphFeature.js @@ -20,12 +20,12 @@ describe('d3 graph feature', function () { var map, layer, feature; it('Setup map', function () { + mockAnimationFrame(); map = geo.map({node: '#map-d3-graph-feature', center: [0, 0], zoom: 3}); layer = map.createLayer('feature', {'renderer': 'd3'}); }); it('Add features to a layer', function () { - mockAnimationFrame(); var selection, nodes; nodes = [ diff --git a/tests/cases/d3PointFeature.js b/tests/cases/d3PointFeature.js index d4ca8e5f5b..46a6bf4793 100644 --- a/tests/cases/d3PointFeature.js +++ b/tests/cases/d3PointFeature.js @@ -20,6 +20,7 @@ describe('d3 point feature', function () { var map, width = 800, height = 600, layer, feature1, feature2; it('Setup map', function () { + mockAnimationFrame(); map = geo.map({node: '#map-d3-point-feature', center: [0, 0], zoom: 3}); layer = map.createLayer('feature', {'renderer': 'd3'}); @@ -27,7 +28,6 @@ describe('d3 point feature', function () { }); it('Add features to a layer', function () { - mockAnimationFrame(); var selection; feature1 = layer.createFeature('point', {selectionAPI: true}) .data([{y: 0, x: 0}, {y: 10, x: 0}, {y: 0, x: 10}]) diff --git a/tests/cases/d3VectorFeature.js b/tests/cases/d3VectorFeature.js index aa3f65e500..357bf25cc5 100644 --- a/tests/cases/d3VectorFeature.js +++ b/tests/cases/d3VectorFeature.js @@ -10,6 +10,7 @@ describe('d3 vector feature', function () { var map, layer, feature1; it('Create a map with a d3 feature layer', function () { + mockAnimationFrame(); d3.select('body').append('div').attr('id', 'map-d3-vector'); map = geo.map({node: '#map-d3-vector', center: [0, 0], @@ -36,7 +37,6 @@ describe('d3 vector feature', function () { }); it('Add features to a layer', function () { - mockAnimationFrame(); var vectorLines, featureGroup, markers; feature1 = layer.createFeature('vector') .data([{y: 0, x: 0}, {y: 10, x: 0}, {y: 0, x: 10}]) diff --git a/tests/cases/map.js b/tests/cases/map.js index 3c9115ac7a..d470526a9d 100644 --- a/tests/cases/map.js +++ b/tests/cases/map.js @@ -390,8 +390,8 @@ describe('geo.core.map', function () { expect(closeToEqual(zc.center, {x: 0, y: 0})).toBe(true); }); it('transition', function () { - var m = create_map(), start, wasCalled; mockAnimationFrame(); + var m = create_map(), start, wasCalled; expect(m.transition()).toBe(null); start = new Date().getTime(); m.transition({ diff --git a/tests/cases/mapInteractor.js b/tests/cases/mapInteractor.js index 36fa490ebe..f3ab6585ff 100644 --- a/tests/cases/mapInteractor.js +++ b/tests/cases/mapInteractor.js @@ -136,6 +136,9 @@ describe('mapInteractor', function () { map.gcs = function (arg) { return 'EPSG:3857'; }; + map.scheduleAnimationFrame = function (callback) { + return window['requestAnimationFrame'](callback); + }; return map; } @@ -1327,6 +1330,7 @@ describe('mapInteractor', function () { }); it('Test momentum', function () { + mockAnimationFrame(); var map = mockedMap('#mapNode1'), start; var interactor = geo.mapInteractor({ @@ -1338,7 +1342,6 @@ describe('mapInteractor', function () { }], throttle: false }); - mockAnimationFrame(); mockDate(); // initiate a pan and release interactor.simulateEvent( @@ -1380,6 +1383,7 @@ describe('mapInteractor', function () { }); it('Test springback', function () { + mockAnimationFrame(); $('#mapNode1').css({width: '400px', height: '400px'}); var map = mockedMap('#mapNode1'), start; @@ -1393,7 +1397,6 @@ describe('mapInteractor', function () { }], throttle: false }); - mockAnimationFrame(); mockDate(); // pan past the max bounds interactor.simulateEvent( diff --git a/tests/test-utils.js b/tests/test-utils.js index b480ee64e3..49aecdcb35 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -341,6 +341,7 @@ module.exports.mockAnimationFrame = function (mockDate) { animFrameIndex += 1; var id = animFrameIndex; animFrameCallbacks.push({id: id, callback: callback}); + return id; } /* Replace window.cancelAnimationFrame with this function.