diff --git a/examples/webgl-layer-swipe.html b/examples/webgl-layer-swipe.html
new file mode 100644
index 0000000000..55867810a4
--- /dev/null
+++ b/examples/webgl-layer-swipe.html
@@ -0,0 +1,16 @@
+---
+layout: example.html
+title: Layer Swipe (WebGL)
+shortdesc: Cropping a WebGL tile layer
+docs: >
+ The prerender and postrender events on a WebGL tile layer can be
+ used to manipulate the WebGL context before and after rendering. In this case, the
+ gl.scissor()
+ method is called to clip the top layer based on the position of a slider.
+tags: "swipe, webgl"
+cloak:
+ - key: get_your_own_D6rA4zTHduk6KOKTXzGB
+ value: Get your own API key at https://www.maptiler.com/cloud/
+---
+
+
diff --git a/examples/webgl-layer-swipe.js b/examples/webgl-layer-swipe.js
new file mode 100644
index 0000000000..66da0b736c
--- /dev/null
+++ b/examples/webgl-layer-swipe.js
@@ -0,0 +1,61 @@
+import Map from '../src/ol/Map.js';
+import OSM from '../src/ol/source/OSM.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+import XYZ from '../src/ol/source/XYZ.js';
+import {getRenderPixel} from '../src/ol/render.js';
+
+const osm = new TileLayer({
+ source: new OSM({wrapX: true}),
+});
+
+const key = 'get_your_own_D6rA4zTHduk6KOKTXzGB';
+
+const imagery = new TileLayer({
+ source: new XYZ({
+ url: 'https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=' + key,
+ attributions:
+ '© MapTiler ' +
+ '© OpenStreetMap contributors ',
+ crossOrigin: '',
+ maxZoom: 20,
+ }),
+});
+
+const map = new Map({
+ layers: [osm, imagery],
+ target: 'map',
+ view: new View({
+ center: [0, 0],
+ zoom: 2,
+ }),
+});
+
+const swipe = document.getElementById('swipe');
+
+imagery.on('prerender', function (event) {
+ const gl = event.context;
+ gl.enable(gl.SCISSOR_TEST);
+
+ const mapSize = map.getSize(); // [width, height] in CSS pixels
+
+ // get render coordinates and dimensions given CSS coordinates
+ const bottomLeft = getRenderPixel(event, [0, mapSize[1]]);
+ const topRight = getRenderPixel(event, [mapSize[0], 0]);
+
+ const width = Math.round((topRight[0] - bottomLeft[0]) * (swipe.value / 100));
+ const height = topRight[1] - bottomLeft[1];
+
+ gl.scissor(bottomLeft[0], bottomLeft[1], width, height);
+});
+
+imagery.on('postrender', function (event) {
+ const gl = event.context;
+ gl.disable(gl.SCISSOR_TEST);
+});
+
+const listener = function () {
+ map.render();
+};
+swipe.addEventListener('input', listener);
+swipe.addEventListener('change', listener);
diff --git a/src/ol/render.js b/src/ol/render.js
index c9cd338c8d..6d7d12cd77 100644
--- a/src/ol/render.js
+++ b/src/ol/render.js
@@ -88,6 +88,10 @@ export function toContext(context, opt_options) {
* @api
*/
export function getVectorContext(event) {
+ if (!(event.context instanceof CanvasRenderingContext2D)) {
+ throw new Error('Only works for render events from Canvas 2D layers');
+ }
+
// canvas may be at a different pixel ratio than frameState.pixelRatio
const canvasPixelRatio = event.inversePixelTransform[0];
const frameState = event.frameState;
@@ -107,6 +111,7 @@ export function getVectorContext(event) {
frameState.viewState.projection
);
}
+
return new CanvasImmediateRenderer(
event.context,
canvasPixelRatio,
@@ -127,7 +132,5 @@ export function getVectorContext(event) {
* @api
*/
export function getRenderPixel(event, pixel) {
- const result = pixel.slice(0);
- applyTransform(event.inversePixelTransform.slice(), result);
- return result;
+ return applyTransform(event.inversePixelTransform, pixel.slice(0));
}
diff --git a/src/ol/render/Event.js b/src/ol/render/Event.js
index 7aa72a2961..ddccd6adac 100644
--- a/src/ol/render/Event.js
+++ b/src/ol/render/Event.js
@@ -10,7 +10,7 @@ class RenderEvent extends Event {
* @param {import("../transform.js").Transform} [opt_inversePixelTransform] Transform for
* CSS pixels to rendered pixels.
* @param {import("../PluggableMap.js").FrameState} [opt_frameState] Frame state.
- * @param {?CanvasRenderingContext2D} [opt_context] Context.
+ * @param {?(CanvasRenderingContext2D|WebGLRenderingContext)} [opt_context] Context.
*/
constructor(type, opt_inversePixelTransform, opt_frameState, opt_context) {
super(type);
@@ -31,9 +31,10 @@ class RenderEvent extends Event {
this.frameState = opt_frameState;
/**
- * Canvas context. Not available when the event is dispatched by the map. Only available
- * when a Canvas renderer is used, null otherwise.
- * @type {CanvasRenderingContext2D|null|undefined}
+ * Canvas context. Not available when the event is dispatched by the map. For Canvas 2D layers,
+ * the context will be the 2D rendering context. For WebGL layers, the context will be the WebGL
+ * context.
+ * @type {CanvasRenderingContext2D|WebGLRenderingContext|undefined}
* @api
*/
this.context = opt_context;
diff --git a/src/ol/renderer/webgl/Layer.js b/src/ol/renderer/webgl/Layer.js
index a492f9f11c..4b5780efb9 100644
--- a/src/ol/renderer/webgl/Layer.js
+++ b/src/ol/renderer/webgl/Layer.js
@@ -5,6 +5,10 @@ import LayerRenderer from '../Layer.js';
import RenderEvent from '../../render/Event.js';
import RenderEventType from '../../render/EventType.js';
import WebGLHelper from '../../webgl/Helper.js';
+import {
+ compose as composeTransform,
+ create as createTransform,
+} from '../../transform.js';
/**
* @enum {string}
@@ -58,6 +62,14 @@ class WebGLLayerRenderer extends LayerRenderer {
const options = opt_options || {};
+ /**
+ * The transform for viewport CSS pixels to rendered pixels. This transform is only
+ * set before dispatching rendering events.
+ * @private
+ * @type {import("../../transform.js").Transform}
+ */
+ this.inversePixelTransform_ = createTransform();
+
/**
* @type {WebGLHelper}
* @protected
@@ -84,32 +96,50 @@ class WebGLLayerRenderer extends LayerRenderer {
/**
* @param {import("../../render/EventType.js").default} type Event type.
+ * @param {WebGLRenderingContext} context The rendering context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @private
*/
- dispatchRenderEvent_(type, frameState) {
+ dispatchRenderEvent_(type, context, frameState) {
const layer = this.getLayer();
if (layer.hasListener(type)) {
- // RenderEvent does not get a context or an inversePixelTransform, because WebGL allows much less direct editing than Canvas2d does.
- const event = new RenderEvent(type, null, frameState, null);
+ composeTransform(
+ this.inversePixelTransform_,
+ 0,
+ 0,
+ frameState.pixelRatio,
+ -frameState.pixelRatio,
+ 0,
+ 0,
+ -frameState.size[1]
+ );
+
+ const event = new RenderEvent(
+ type,
+ this.inversePixelTransform_,
+ frameState,
+ context
+ );
layer.dispatchEvent(event);
}
}
/**
+ * @param {WebGLRenderingContext} context The rendering context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
- preRender(frameState) {
- this.dispatchRenderEvent_(RenderEventType.PRERENDER, frameState);
+ preRender(context, frameState) {
+ this.dispatchRenderEvent_(RenderEventType.PRERENDER, context, frameState);
}
/**
+ * @param {WebGLRenderingContext} context The rendering context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
- postRender(frameState) {
- this.dispatchRenderEvent_(RenderEventType.POSTRENDER, frameState);
+ postRender(context, frameState) {
+ this.dispatchRenderEvent_(RenderEventType.POSTRENDER, context, frameState);
}
}
diff --git a/src/ol/renderer/webgl/PointsLayer.js b/src/ol/renderer/webgl/PointsLayer.js
index 4b0d150746..2397e586cd 100644
--- a/src/ol/renderer/webgl/PointsLayer.js
+++ b/src/ol/renderer/webgl/PointsLayer.js
@@ -411,7 +411,8 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
* @return {HTMLElement} The rendered element.
*/
renderFrame(frameState) {
- this.preRender(frameState);
+ const gl = this.helper.getGL();
+ this.preRender(gl, frameState);
const renderCount = this.indicesBuffer_.getSize();
this.helper.drawElements(0, renderCount);
@@ -429,7 +430,7 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
this.hitRenderTarget_.clearCachedData();
}
- this.postRender(frameState);
+ this.postRender(gl, frameState);
return canvas;
}
diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js
index f17a8c2e6d..cc6f9c3f88 100644
--- a/src/ol/renderer/webgl/TileLayer.js
+++ b/src/ol/renderer/webgl/TileLayer.js
@@ -292,7 +292,8 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
* @return {HTMLElement} The rendered element.
*/
renderFrame(frameState) {
- this.preRender(frameState);
+ const gl = this.helper.getGL();
+ this.preRender(gl, frameState);
const viewState = frameState.viewState;
const layerState = frameState.layerStatesArray[frameState.layerIndex];
@@ -386,8 +387,6 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
.map(Number)
.sort(numberSafeCompareFunction);
- const gl = this.helper.getGL();
-
const centerX = viewState.center[0];
const centerY = viewState.center[1];
@@ -509,7 +508,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
frameState.postRenderFunctions.push(postRenderFunction);
- this.postRender(frameState);
+ this.postRender(gl, frameState);
return canvas;
}
diff --git a/test/browser/spec/ol/layer/WebGLTile.test.js b/test/browser/spec/ol/layer/WebGLTile.test.js
index 32cf772507..56f394885b 100644
--- a/test/browser/spec/ol/layer/WebGLTile.test.js
+++ b/test/browser/spec/ol/layer/WebGLTile.test.js
@@ -4,6 +4,7 @@ import View from '../../../../../src/ol/View.js';
import WebGLHelper from '../../../../../src/ol/webgl/Helper.js';
import WebGLTileLayer from '../../../../../src/ol/layer/WebGLTile.js';
import {createCanvasContext2D} from '../../../../../src/ol/dom.js';
+import {getRenderPixel} from '../../../../../src/ol/render.js';
describe('ol/layer/WebGLTile', function () {
/** @type {WebGLTileLayer} */
@@ -129,6 +130,46 @@ describe('ol/layer/WebGLTile', function () {
});
});
+ it('dispatches a prerender event with WebGL context and inverse pixel transform', (done) => {
+ let called = false;
+ layer.on('prerender', (event) => {
+ expect(event.context).to.be.a(WebGLRenderingContext);
+ const mapSize = event.frameState.size;
+ const bottomLeft = getRenderPixel(event, [0, mapSize[1]]);
+ expect(bottomLeft).to.eql([0, 0]);
+ called = true;
+ });
+
+ map.once('rendercomplete', () => {
+ expect(called).to.be(true);
+ done();
+ });
+
+ map.render();
+ });
+
+ it('dispatches a postrender event with WebGL context and inverse pixel transform', (done) => {
+ let called = false;
+ layer.on('postrender', (event) => {
+ expect(event.context).to.be.a(WebGLRenderingContext);
+ const mapSize = event.frameState.size;
+ const topRight = getRenderPixel(event, [mapSize[1], 0]);
+ const pixelRatio = event.frameState.pixelRatio;
+ expect(topRight).to.eql([
+ mapSize[0] * pixelRatio,
+ mapSize[1] * pixelRatio,
+ ]);
+ called = true;
+ });
+
+ map.once('rendercomplete', () => {
+ expect(called).to.be(true);
+ done();
+ });
+
+ map.render();
+ });
+
it('tries to expire the source tile cache', (done) => {
const source = layer.getSource();
const expire = sinon.spy(source, 'expireCache');