Handle opacity on canvas instead of dom

This commit is contained in:
Andreas Hocevar
2022-07-10 15:30:28 +02:00
parent 239487e9f0
commit 6086459cba
7 changed files with 76 additions and 58 deletions

View File

@@ -114,11 +114,3 @@ export const getFontParameters = function (fontSpec) {
style.families = style.family.split(/,\s?/); style.families = style.family.split(/,\s?/);
return style; 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);
}

View File

@@ -195,12 +195,7 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer {
const canvasTransform = toTransformString(this.pixelTransform); const canvasTransform = toTransformString(this.pixelTransform);
this.useContainer( this.useContainer(target, canvasTransform, this.getBackground(frameState));
target,
canvasTransform,
layerState.opacity,
this.getBackground(frameState)
);
const context = this.context; const context = this.context;
const canvas = context.canvas; const canvas = context.canvas;

View File

@@ -20,6 +20,11 @@ import {
import {createCanvasContext2D} from '../../dom.js'; import {createCanvasContext2D} from '../../dom.js';
import {equals} from '../../array.js'; import {equals} from '../../array.js';
/**
* @type {Array<HTMLCanvasElement>}
*/
export const canvasPool = [];
/** /**
* @type {CanvasRenderingContext2D} * @type {CanvasRenderingContext2D}
*/ */
@@ -143,17 +148,14 @@ class CanvasLayerRenderer extends LayerRenderer {
* Get a rendering container from an existing target, if compatible. * Get a rendering container from an existing target, if compatible.
* @param {HTMLElement} target Potential render target. * @param {HTMLElement} target Potential render target.
* @param {string} transform CSS Transform. * @param {string} transform CSS Transform.
* @param {number} opacity Opacity.
* @param {string} [opt_backgroundColor] Background color. * @param {string} [opt_backgroundColor] Background color.
*/ */
useContainer(target, transform, opacity, opt_backgroundColor) { useContainer(target, transform, opt_backgroundColor) {
const layerClassName = this.getLayer().getClassName(); const layerClassName = this.getLayer().getClassName();
let container, context; let container, context;
if ( if (
target && target &&
target.className === layerClassName && target.className === layerClassName &&
target.style.opacity === '' &&
opacity === 1 &&
(!opt_backgroundColor || (!opt_backgroundColor ||
(target && (target &&
target.style.backgroundColor && target.style.backgroundColor &&

View File

@@ -25,7 +25,6 @@ import {
getWidth, getWidth,
intersects, intersects,
} from '../../extent.js'; } from '../../extent.js';
import {cssOpacity} from '../../css.js';
import {fromUserExtent} from '../../proj.js'; import {fromUserExtent} from '../../proj.js';
import {getUid} from '../../util.js'; import {getUid} from '../../util.js';
import {numberSafeCompareFunction} from '../../array.js'; import {numberSafeCompareFunction} from '../../array.js';
@@ -330,7 +329,12 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer {
const uid = getUid(this); const uid = getUid(this);
if (tile.getState() == TileState.LOADED) { if (tile.getState() == TileState.LOADED) {
tilesToDrawByZ[z][tile.tileCoord.toString()] = tile; 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 ( if (
!this.newTiles_ && !this.newTiles_ &&
(inTransition || this.renderedTiles.indexOf(tile) === -1) (inTransition || this.renderedTiles.indexOf(tile) === -1)
@@ -381,12 +385,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer {
const canvasTransform = toTransformString(this.pixelTransform); const canvasTransform = toTransformString(this.pixelTransform);
this.useContainer( this.useContainer(target, canvasTransform, this.getBackground(frameState));
target,
canvasTransform,
layerState.opacity,
this.getBackground(frameState)
);
const context = this.context; const context = this.context;
const canvas = context.canvas; const canvas = context.canvas;
@@ -572,11 +571,6 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer {
if (canvasTransform !== canvas.style.transform) { if (canvasTransform !== canvas.style.transform) {
canvas.style.transform = canvasTransform; 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; return this.container;
} }
@@ -597,7 +591,10 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer {
return; return;
} }
const uid = getUid(this); 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; const alphaChanged = alpha !== this.context.globalAlpha;
if (alphaChanged) { if (alphaChanged) {
this.context.save(); this.context.save();
@@ -618,7 +615,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer {
if (alphaChanged) { if (alphaChanged) {
this.context.restore(); this.context.restore();
} }
if (alpha !== 1) { if (alpha !== layerState.opacity) {
frameState.animate = true; frameState.animate = true;
} else if (transition) { } else if (transition) {
tile.endTransition(uid); tile.endTransition(uid);

View File

@@ -105,8 +105,11 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
!hints[ViewHint.INTERACTING] && !hints[ViewHint.INTERACTING] &&
!isEmpty(renderedExtent) !isEmpty(renderedExtent)
) { ) {
vectorRenderer.useContainer(null, null, 1); vectorRenderer.useContainer(null, null);
const context = vectorRenderer.context; const context = vectorRenderer.context;
const layerState = frameState.layerStatesArray[frameState.layerIndex];
context.globalAlpha = layerState.opacity;
const imageLayerState = assign({}, layerState, {opacity: 1});
const imageFrameState = const imageFrameState =
/** @type {import("../../PluggableMap.js").FrameState} */ ( /** @type {import("../../PluggableMap.js").FrameState} */ (
assign({}, frameState, { assign({}, frameState, {
@@ -118,6 +121,8 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
rotation: 0, rotation: 0,
}) })
), ),
layerStatesArray: [imageLayerState],
layerIndex: 0,
}) })
); );
let emptyImage = true; let emptyImage = true;

View File

@@ -2,7 +2,7 @@
* @module ol/renderer/canvas/VectorLayer * @module ol/renderer/canvas/VectorLayer
*/ */
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; 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 ExecutorGroup from '../../render/canvas/ExecutorGroup.js';
import ViewHint from '../../ViewHint.js'; import ViewHint from '../../ViewHint.js';
import { import {
@@ -24,7 +24,7 @@ import {
intersects as intersectsExtent, intersects as intersectsExtent,
wrapX as wrapExtentX, wrapX as wrapExtentX,
} from '../../extent.js'; } from '../../extent.js';
import {cssOpacity} from '../../css.js'; import {createCanvasContext2D, releaseCanvas} from '../../dom.js';
import { import {
defaultOrder as defaultRenderOrder, defaultOrder as defaultRenderOrder,
getTolerance as getRenderTolerance, getTolerance as getRenderTolerance,
@@ -142,6 +142,18 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
* @type {boolean} * @type {boolean}
*/ */
this.clipping = true; 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 = !( const snapToPixel = !(
viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING] viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]
); );
const context = this.context; const context = this.compositionContext_;
const width = Math.round(frameState.size[0] * pixelRatio); const width = Math.round(frameState.size[0] * pixelRatio);
const height = Math.round(frameState.size[1] * pixelRatio); const height = Math.round(frameState.size[1] * pixelRatio);
@@ -197,17 +209,44 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
} while (++world < endWorld); } 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 * Render declutter items for this layer
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
*/ */
renderDeclutter(frameState) { renderDeclutter(frameState) {
if (this.declutterExecutorGroup) { if (this.declutterExecutorGroup) {
this.setupCompositionContext_();
this.renderWorlds( this.renderWorlds(
this.declutterExecutorGroup, this.declutterExecutorGroup,
frameState, frameState,
frameState.declutterTree frameState.declutterTree
); );
this.releaseCompositionContext_();
} }
} }
@@ -227,12 +266,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
const canvasTransform = transformToString(this.pixelTransform); const canvasTransform = transformToString(this.pixelTransform);
this.useContainer( this.useContainer(target, canvasTransform, this.getBackground(frameState));
target,
canvasTransform,
layerState.opacity,
this.getBackground(frameState)
);
const context = this.context; const context = this.context;
const canvas = context.canvas; const canvas = context.canvas;
@@ -263,6 +297,9 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
const viewState = frameState.viewState; const viewState = frameState.viewState;
const projection = viewState.projection; const projection = viewState.projection;
this.opacity_ = layerState.opacity;
this.setupCompositionContext_();
// clipped rendering if layer extent is set // clipped rendering if layer extent is set
let clipped = false; let clipped = false;
let render = true; let render = true;
@@ -271,7 +308,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
render = intersectsExtent(layerExtent, frameState.extent); render = intersectsExtent(layerExtent, frameState.extent);
clipped = render && !containsExtent(layerExtent, frameState.extent); clipped = render && !containsExtent(layerExtent, frameState.extent);
if (clipped) { if (clipped) {
this.clipUnrotated(context, frameState, layerExtent); this.clipUnrotated(this.compositionContext_, frameState, layerExtent);
} }
} }
@@ -280,17 +317,13 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
} }
if (clipped) { if (clipped) {
context.restore(); this.compositionContext_.restore();
} }
this.releaseCompositionContext_();
this.postRender(context, frameState); 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) { if (this.renderedRotation_ !== viewState.rotation) {
this.renderedRotation_ = viewState.rotation; this.renderedRotation_ = viewState.rotation;
this.hitDetectionImageData_ = null; this.hitDetectionImageData_ = null;

View File

@@ -1,13 +1,7 @@
import expect from '../expect.js'; 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('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 () { describe('getFontParameters()', function () {
const cases = [ const cases = [
{ {