diff --git a/src/ol/color/color.js b/src/ol/color/color.js index c322252c37..84ba8e6694 100644 --- a/src/ol/color/color.js +++ b/src/ol/color/color.js @@ -183,6 +183,16 @@ ol.color.fromStringInternal_ = function(s) { }; +/** + * @param {ol.ColorLike|string} color Color. + * @return {boolean} Is rgba. + */ +ol.color.isRgba = function(color) { + return Array.isArray(color) && color.length == 4 || + typeof color == 'string' && ol.color.rgbaColorRe_.test(color); +}; + + /** * @param {ol.Color} color Color. * @return {boolean} Is valid. diff --git a/src/ol/render/canvas/canvasreplay.js b/src/ol/render/canvas/canvasreplay.js index d1bca4fcbc..20066e4c56 100644 --- a/src/ol/render/canvas/canvasreplay.js +++ b/src/ol/render/canvas/canvasreplay.js @@ -71,6 +71,12 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution) { */ this.maxExtent = maxExtent; + /** + * @protected + * @type {boolean} + */ + this.transparency = false; + /** * @private * @type {ol.Extent} @@ -257,6 +263,10 @@ ol.render.canvas.Replay.prototype.replay_ = function( var localTransform = this.tmpLocalTransform_; var localTransformInv = this.tmpLocalTransformInv_; var prevX, prevY, roundX, roundY; + var pendingFill = 0; + var pendingStroke = 0; + var batchSize = + this.instructions != instructions || this.transparency ? 0 : 200; while (i < ii) { var instruction = instructions[i]; var type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]); @@ -276,7 +286,17 @@ ol.render.canvas.Replay.prototype.replay_ = function( } break; case ol.render.canvas.Instruction.BEGIN_PATH: - context.beginPath(); + if (pendingFill > batchSize) { + context.fill(); + pendingFill = 0; + } + if (pendingStroke > batchSize) { + context.stroke(); + pendingStroke = 0; + } + if (!pendingFill && !pendingStroke) { + context.beginPath(); + } ++i; break; case ol.render.canvas.Instruction.CIRCLE: @@ -290,6 +310,7 @@ ol.render.canvas.Replay.prototype.replay_ = function( var dx = x2 - x1; var dy = y2 - y1; var r = Math.sqrt(dx * dx + dy * dy); + context.moveTo(x2, y2); context.arc(x1, y1, r, 0, 2 * Math.PI, true); ++i; break; @@ -438,7 +459,11 @@ ol.render.canvas.Replay.prototype.replay_ = function( ++i; break; case ol.render.canvas.Instruction.FILL: - context.fill(); + if (batchSize) { + pendingFill++; + } else { + context.fill(); + } ++i; break; case ol.render.canvas.Instruction.MOVE_TO_LINE_TO: @@ -475,6 +500,11 @@ ol.render.canvas.Replay.prototype.replay_ = function( ol.colorlike.isColorLike(instruction[1]), '2nd instruction should be a string, ' + 'CanvasPattern, or CanvasGradient'); + if (pendingFill) { + context.fill(); + pendingFill = 0; + } + context.fillStyle = /** @type {ol.ColorLike} */ (instruction[1]); ++i; break; @@ -494,6 +524,10 @@ ol.render.canvas.Replay.prototype.replay_ = function( var usePixelRatio = instruction[7] !== undefined ? instruction[7] : true; var lineWidth = /** @type {number} */ (instruction[2]); + if (pendingStroke) { + context.stroke(); + pendingStroke = 0; + } context.strokeStyle = /** @type {string} */ (instruction[1]); context.lineWidth = usePixelRatio ? lineWidth * pixelRatio : lineWidth; context.lineCap = /** @type {string} */ (instruction[3]); @@ -519,7 +553,11 @@ ol.render.canvas.Replay.prototype.replay_ = function( ++i; break; case ol.render.canvas.Instruction.STROKE: - context.stroke(); + if (batchSize) { + pendingStroke++; + } else { + context.stroke(); + } ++i; break; default: @@ -528,6 +566,12 @@ ol.render.canvas.Replay.prototype.replay_ = function( break; } } + if (pendingFill) { + context.fill(); + } + if (pendingStroke) { + context.stroke(); + } // assert that all instructions were consumed goog.asserts.assert(i == instructions.length, 'all instructions should be consumed'); @@ -547,7 +591,7 @@ ol.render.canvas.Replay.prototype.replay = function( context, pixelRatio, transform, viewRotation, skippedFeaturesHash) { var instructions = this.instructions; this.replay_(context, pixelRatio, transform, viewRotation, - skippedFeaturesHash, instructions, undefined); + skippedFeaturesHash, instructions, undefined, undefined); }; @@ -1413,6 +1457,10 @@ ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle var fillStyleColor = fillStyle.getColor(); state.fillStyle = ol.colorlike.asColorLike(fillStyleColor ? fillStyleColor : ol.render.canvas.defaultFillStyle); + if (!this.transparency && ol.color.isRgba(state.fillStyle)) { + this.transparency = ol.color.asArray( + /** @type {ol.Color|string} */ (state.fillStyle))[0] != 1; + } } else { state.fillStyle = undefined; } @@ -1420,6 +1468,9 @@ ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle var strokeStyleColor = strokeStyle.getColor(); state.strokeStyle = ol.color.asString(strokeStyleColor ? strokeStyleColor : ol.render.canvas.defaultStrokeStyle); + if (!this.transparency && ol.color.isRgba(state.strokeStyle)) { + this.transparency = ol.color.asArray(state.strokeStyle)[3] != 1; + } var strokeStyleLineCap = strokeStyle.getLineCap(); state.lineCap = strokeStyleLineCap !== undefined ? strokeStyleLineCap : ol.render.canvas.defaultLineCap; diff --git a/test/spec/ol/color.test.js b/test/spec/ol/color.test.js index d15e761d15..12bcbdb3d5 100644 --- a/test/spec/ol/color.test.js +++ b/test/spec/ol/color.test.js @@ -103,6 +103,20 @@ describe('ol.color', function() { }); + describe('ol.color.isRgba', function() { + it('identifies rgba arrays', function() { + expect(ol.color.isRgba([255, 255, 255, 1])).to.be(true); + expect(ol.color.isRgba([255, 255, 255])).to.be(false); + }); + it('identifies rgba strings', function() { + expect(ol.color.isRgba('rgba(255,255,255,1)')).to.be(true); + expect(ol.color.isRgba('rgb(255,255,255)')).to.be(false); + expect(ol.color.isRgba('#FFF')).to.be(false); + expect(ol.color.isRgba('#FFFFFF')).to.be(false); + expect(ol.color.isRgba('red')).to.be(false); + }); + }); + describe('ol.color.isValid', function() { it('identifies valid colors', function() { diff --git a/test/spec/ol/renderer/canvas/canvasreplay.test.js b/test/spec/ol/renderer/canvas/canvasreplay.test.js index b36074bb42..1f30b88ebc 100644 --- a/test/spec/ol/renderer/canvas/canvasreplay.test.js +++ b/test/spec/ol/renderer/canvas/canvasreplay.test.js @@ -1,5 +1,139 @@ goog.provide('ol.test.renderer.canvas.Replay'); +describe('ol.render.canvas.ReplayGroup', function() { + + describe('#replay', function() { + + var context, replay, fillCount, strokeCount, beginPathCount; + var feature1, feature2, feature3, style1, style2, style3, transform; + + beforeEach(function() { + transform = goog.vec.Mat4.createNumber(); + replay = new ol.render.canvas.ReplayGroup(1, [-180, -90, 180, 90], 1); + feature1 = new ol.Feature(new ol.geom.Polygon( + [[[-90, -45], [-90, 0], [0, 0], [0, -45], [-90, -45]]])); + feature2 = new ol.Feature(new ol.geom.Polygon( + [[[90, 45], [90, 0], [0, 0], [0, 45], [90, 45]]])); + feature3 = new ol.Feature(new ol.geom.Polygon( + [[[-90, -45], [-90, 45], [90, 45], [90, -45], [-90, -45]]])); + style1 = new ol.style.Style({ + fill: new ol.style.Fill({color: 'black'}), + stroke: new ol.style.Stroke({color: 'white', width: 1}) + }); + style2 = new ol.style.Style({ + fill: new ol.style.Fill({color: 'white'}), + stroke: new ol.style.Stroke({color: 'black', width: 1}) + }); + style3 = new ol.style.Style({ + fill: new ol.style.Fill({color: 'rgba(255,255,255,0.8)'}), + stroke: new ol.style.Stroke({color: 'rgba(0,0,0,0.8)', width: 1}) + }); + fillCount = 0; + strokeCount = 0; + beginPathCount = 0; + context = { + fill: function() { + fillCount++; + }, + stroke: function() { + strokeCount++; + }, + beginPath: function() { + beginPathCount++; + }, + clip: function() { + beginPathCount--; + }, + save: function() {}, + moveTo: function() {}, + lineTo: function() {}, + closePath: function() {}, + setLineDash: function() {}, + restore: function() {} + } + + }) + + it('batches fill and stroke instructions for same style', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style1, 1); + replay.replay(context, 1, transform, 0, {}); + expect(fillCount).to.be(1); + expect(strokeCount).to.be(1); + expect(beginPathCount).to.be(1); + }); + + it('batches fill and stroke instructions for different styles', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style2, 1); + replay.replay(context, 1, transform, 0, {}); + expect(fillCount).to.be(2); + expect(strokeCount).to.be(2); + expect(beginPathCount).to.be(2); + }); + + it('batches fill and stroke instructions for changing styles', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style2, 1); + ol.renderer.vector.renderFeature(replay, feature3, style1, 1); + replay.replay(context, 1, transform, 0, {}); + expect(fillCount).to.be(3); + expect(strokeCount).to.be(3); + expect(beginPathCount).to.be(3); + }); + + it('batches fill and stroke instructions for skipped feature at the beginning', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style2, 1); + ol.renderer.vector.renderFeature(replay, feature3, style2, 1); + var skippedUids = {}; + skippedUids[goog.getUid(feature1)] = true; + replay.replay(context, 1, transform, 0, skippedUids); + expect(fillCount).to.be(1); + expect(strokeCount).to.be(1); + expect(beginPathCount).to.be(1); + }); + + it('batches fill and stroke instructions for skipped feature at the end', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style2, 1); + var skippedUids = {}; + skippedUids[goog.getUid(feature3)] = true; + replay.replay(context, 1, transform, 0, skippedUids); + expect(fillCount).to.be(1); + expect(strokeCount).to.be(1); + expect(beginPathCount).to.be(1); + }); + + it('batches fill and stroke instructions for skipped features', function() { + ol.renderer.vector.renderFeature(replay, feature1, style1, 1); + ol.renderer.vector.renderFeature(replay, feature2, style1, 1); + ol.renderer.vector.renderFeature(replay, feature3, style2, 1); + var skippedUids = {}; + skippedUids[goog.getUid(feature1)] = true; + skippedUids[goog.getUid(feature2)] = true; + replay.replay(context, 1, transform, 0, skippedUids); + expect(fillCount).to.be(1); + expect(strokeCount).to.be(1); + expect(beginPathCount).to.be(1); + }); + + it('does not batch when transparent fills/strokes are used', function() { + ol.renderer.vector.renderFeature(replay, feature1, style3, 1); + ol.renderer.vector.renderFeature(replay, feature2, style3, 1); + ol.renderer.vector.renderFeature(replay, feature3, style3, 1); + replay.replay(context, 1, transform, 0, {}); + expect(fillCount).to.be(3); + expect(strokeCount).to.be(3); + expect(beginPathCount).to.be(3); + }) + }); + +}); + describe('ol.render.canvas.Replay', function() { describe('constructor', function() { @@ -122,7 +256,14 @@ describe('ol.render.canvas.PolygonReplay', function() { }); +goog.require('goog.vec.Mat4'); +goog.require('ol.Feature'); +goog.require('ol.geom.Polygon'); goog.require('ol.render.canvas.LineStringReplay'); goog.require('ol.render.canvas.PolygonReplay'); goog.require('ol.render.canvas.Replay'); +goog.require('ol.render.canvas.ReplayGroup'); +goog.require('ol.renderer.vector'); +goog.require('ol.style.Fill'); goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); diff --git a/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png b/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png new file mode 100644 index 0000000000..5781ffd7ed Binary files /dev/null and b/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png differ diff --git a/test_rendering/spec/ol/layer/vector.test.js b/test_rendering/spec/ol/layer/vector.test.js index 95a7d5a678..f8e4c6bf8b 100644 --- a/test_rendering/spec/ol/layer/vector.test.js +++ b/test_rendering/spec/ol/layer/vector.test.js @@ -47,7 +47,7 @@ describe('ol.rendering.layer.Vector', function() { disposeMap(map); }); - it('renders correctly with the canvas renderer', function(done) { + it('renders opacity correctly with the canvas renderer', function(done) { map = createMap('canvas'); var smallLine = new ol.Feature(new ol.geom.LineString([ [center[0], center[1] - 1], @@ -73,6 +73,29 @@ describe('ol.rendering.layer.Vector', function() { }); }); + it('renders fill/stroke batches correctly with the canvas renderer', function(done) { + map = createMap('canvas'); + addPolygon(100); + addCircle(200); + addPolygon(250); + addCircle(500); + addPolygon(600); + addPolygon(720); + map.addLayer(new ol.layer.Vector({ + source: source, + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: '#3399CC', + width: 1.25 + }) + }) + })); + map.once('postrender', function() { + expectResemble(map, 'spec/ol/layer/expected/vector-canvas-opaque.png', + 17, done); + }) + }); + }); }); @@ -84,3 +107,5 @@ goog.require('ol.geom.Circle'); goog.require('ol.geom.Polygon'); goog.require('ol.layer.Vector'); goog.require('ol.source.Vector'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style');