diff --git a/src/ol/css.js b/src/ol/css.js index 0e5b6e709d..be8954e834 100644 --- a/src/ol/css.js +++ b/src/ol/css.js @@ -114,11 +114,3 @@ export const getFontParameters = function (fontSpec) { style.families = style.family.split(/,\s?/); return style; }; - -/** - * @param {number} opacity Opacity (0..1). - * @return {string} CSS opacity. - */ -export function cssOpacity(opacity) { - return opacity === 1 ? '' : String(Math.round(opacity * 100) / 100); -} diff --git a/src/ol/renderer/canvas/ImageLayer.js b/src/ol/renderer/canvas/ImageLayer.js index 13ad732b37..6fb8c770e6 100644 --- a/src/ol/renderer/canvas/ImageLayer.js +++ b/src/ol/renderer/canvas/ImageLayer.js @@ -195,12 +195,7 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer { const canvasTransform = toTransformString(this.pixelTransform); - this.useContainer( - target, - canvasTransform, - layerState.opacity, - this.getBackground(frameState) - ); + this.useContainer(target, canvasTransform, this.getBackground(frameState)); const context = this.context; const canvas = context.canvas; diff --git a/src/ol/renderer/canvas/Layer.js b/src/ol/renderer/canvas/Layer.js index 99e69b5e36..7852bcf6cb 100644 --- a/src/ol/renderer/canvas/Layer.js +++ b/src/ol/renderer/canvas/Layer.js @@ -20,6 +20,11 @@ import { import {createCanvasContext2D} from '../../dom.js'; import {equals} from '../../array.js'; +/** + * @type {Array} + */ +export const canvasPool = []; + /** * @type {CanvasRenderingContext2D} */ @@ -143,17 +148,14 @@ class CanvasLayerRenderer extends LayerRenderer { * Get a rendering container from an existing target, if compatible. * @param {HTMLElement} target Potential render target. * @param {string} transform CSS Transform. - * @param {number} opacity Opacity. * @param {string} [opt_backgroundColor] Background color. */ - useContainer(target, transform, opacity, opt_backgroundColor) { + useContainer(target, transform, opt_backgroundColor) { const layerClassName = this.getLayer().getClassName(); let container, context; if ( target && target.className === layerClassName && - target.style.opacity === '' && - opacity === 1 && (!opt_backgroundColor || (target && target.style.backgroundColor && diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index c08bd5274a..1ee94a26f3 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -25,7 +25,6 @@ import { getWidth, intersects, } from '../../extent.js'; -import {cssOpacity} from '../../css.js'; import {fromUserExtent} from '../../proj.js'; import {getUid} from '../../util.js'; import {numberSafeCompareFunction} from '../../array.js'; @@ -330,7 +329,12 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { const uid = getUid(this); if (tile.getState() == TileState.LOADED) { tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; - const inTransition = tile.inTransition(uid); + let inTransition = tile.inTransition(uid); + if (inTransition && layerState.opacity !== 1) { + // Skipping transition when layer is not fully opaque avoids visual artifacts. + tile.endTransition(uid); + inTransition = false; + } if ( !this.newTiles_ && (inTransition || this.renderedTiles.indexOf(tile) === -1) @@ -381,12 +385,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { const canvasTransform = toTransformString(this.pixelTransform); - this.useContainer( - target, - canvasTransform, - layerState.opacity, - this.getBackground(frameState) - ); + this.useContainer(target, canvasTransform, this.getBackground(frameState)); const context = this.context; const canvas = context.canvas; @@ -572,11 +571,6 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { if (canvasTransform !== canvas.style.transform) { canvas.style.transform = canvasTransform; } - const opacity = cssOpacity(layerState.opacity); - const container = this.container; - if (opacity !== container.style.opacity) { - container.style.opacity = opacity; - } return this.container; } @@ -597,7 +591,10 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { return; } const uid = getUid(this); - const alpha = transition ? tile.getAlpha(uid, frameState.time) : 1; + const layerState = frameState.layerStatesArray[frameState.layerIndex]; + const alpha = + layerState.opacity * + (transition ? tile.getAlpha(uid, frameState.time) : 1); const alphaChanged = alpha !== this.context.globalAlpha; if (alphaChanged) { this.context.save(); @@ -618,7 +615,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { if (alphaChanged) { this.context.restore(); } - if (alpha !== 1) { + if (alpha !== layerState.opacity) { frameState.animate = true; } else if (transition) { tile.endTransition(uid); diff --git a/src/ol/renderer/canvas/VectorImageLayer.js b/src/ol/renderer/canvas/VectorImageLayer.js index 57071f4ecc..75948f710c 100644 --- a/src/ol/renderer/canvas/VectorImageLayer.js +++ b/src/ol/renderer/canvas/VectorImageLayer.js @@ -105,8 +105,11 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { !hints[ViewHint.INTERACTING] && !isEmpty(renderedExtent) ) { - vectorRenderer.useContainer(null, null, 1); + vectorRenderer.useContainer(null, null); const context = vectorRenderer.context; + const layerState = frameState.layerStatesArray[frameState.layerIndex]; + context.globalAlpha = layerState.opacity; + const imageLayerState = assign({}, layerState, {opacity: 1}); const imageFrameState = /** @type {import("../../PluggableMap.js").FrameState} */ ( assign({}, frameState, { @@ -118,6 +121,8 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { rotation: 0, }) ), + layerStatesArray: [imageLayerState], + layerIndex: 0, }) ); let emptyImage = true; diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 1e495ed189..da5e41e56f 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -2,7 +2,7 @@ * @module ol/renderer/canvas/VectorLayer */ import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; -import CanvasLayerRenderer from './Layer.js'; +import CanvasLayerRenderer, {canvasPool} from './Layer.js'; import ExecutorGroup from '../../render/canvas/ExecutorGroup.js'; import ViewHint from '../../ViewHint.js'; import { @@ -24,7 +24,7 @@ import { intersects as intersectsExtent, wrapX as wrapExtentX, } from '../../extent.js'; -import {cssOpacity} from '../../css.js'; +import {createCanvasContext2D, releaseCanvas} from '../../dom.js'; import { defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, @@ -142,6 +142,18 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { * @type {boolean} */ this.clipping = true; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.compositionContext_ = null; + + /** + * @private + * @type {number} + */ + this.opacity_ = 1; } /** @@ -163,7 +175,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const snapToPixel = !( viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING] ); - const context = this.context; + const context = this.compositionContext_; const width = Math.round(frameState.size[0] * pixelRatio); const height = Math.round(frameState.size[1] * pixelRatio); @@ -197,17 +209,44 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } while (++world < endWorld); } + setupCompositionContext_() { + if (this.opacity_ !== 1) { + const compositionContext = createCanvasContext2D( + this.context.canvas.width, + this.context.canvas.height, + canvasPool + ); + this.compositionContext_ = compositionContext; + } else { + this.compositionContext_ = this.context; + } + } + + releaseCompositionContext_() { + if (this.opacity_ !== 1) { + const alpha = this.context.globalAlpha; + this.context.globalAlpha = this.opacity_; + this.context.drawImage(this.compositionContext_.canvas, 0, 0); + this.context.globalAlpha = alpha; + releaseCanvas(this.compositionContext_); + canvasPool.push(this.compositionContext_.canvas); + this.compositionContext_ = null; + } + } + /** * Render declutter items for this layer * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. */ renderDeclutter(frameState) { if (this.declutterExecutorGroup) { + this.setupCompositionContext_(); this.renderWorlds( this.declutterExecutorGroup, frameState, frameState.declutterTree ); + this.releaseCompositionContext_(); } } @@ -227,12 +266,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const canvasTransform = transformToString(this.pixelTransform); - this.useContainer( - target, - canvasTransform, - layerState.opacity, - this.getBackground(frameState) - ); + this.useContainer(target, canvasTransform, this.getBackground(frameState)); const context = this.context; const canvas = context.canvas; @@ -263,6 +297,9 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const viewState = frameState.viewState; const projection = viewState.projection; + this.opacity_ = layerState.opacity; + this.setupCompositionContext_(); + // clipped rendering if layer extent is set let clipped = false; let render = true; @@ -271,7 +308,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { render = intersectsExtent(layerExtent, frameState.extent); clipped = render && !containsExtent(layerExtent, frameState.extent); if (clipped) { - this.clipUnrotated(context, frameState, layerExtent); + this.clipUnrotated(this.compositionContext_, frameState, layerExtent); } } @@ -280,17 +317,13 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { } if (clipped) { - context.restore(); + this.compositionContext_.restore(); } + this.releaseCompositionContext_(); + this.postRender(context, frameState); - const opacity = cssOpacity(layerState.opacity); - const container = this.container; - if (opacity !== container.style.opacity) { - container.style.opacity = opacity; - } - if (this.renderedRotation_ !== viewState.rotation) { this.renderedRotation_ = viewState.rotation; this.hitDetectionImageData_ = null; diff --git a/test/node/ol/css.test.js b/test/node/ol/css.test.js index 05d441ee98..90c51d572e 100644 --- a/test/node/ol/css.test.js +++ b/test/node/ol/css.test.js @@ -1,13 +1,7 @@ import expect from '../expect.js'; -import {cssOpacity, getFontParameters} from '../../../src/ol/css.js'; +import {getFontParameters} from '../../../src/ol/css.js'; describe('ol.css', function () { - describe('cssOpacity()', function () { - it('converts number to string, 1 to ""', function () { - expect(cssOpacity(0.5)).to.eql('0.5'); - expect(cssOpacity(1)).to.eql(''); - }); - }); describe('getFontParameters()', function () { const cases = [ {