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');