Layer Swipe Mixing WebGL and Vector layers in OpenLayers
The official examples don’t show you how to create a layer swipe effect in OpenLayers mixing WebGL and Vector layers.
Compare
// compare.ts
import type VectorLayer from "ol/layer/Vector";
import type RenderEvent from "ol/render/Event";
import { getRenderPixel } from "ol/render";
import type Map from "ol/Map";
import { Feature } from "ol";
import WebGlTileLayer from "ol/layer/WebGLTile";
export function compare(
map: Map,
getPositionPx: () => number,
left?: VectorLayer<Feature> | WebGlTileLayer,
right?: VectorLayer<Feature> | WebGlTileLayer
) {
function onPreRender(opts?: {
left?: boolean;
bothWebGl?: boolean;
isRightWebGL?: boolean;
}) {
return function (event: RenderEvent) {
const ctx = event.context as CanvasRenderingContext2D;
if (!ctx) return;
const mapSize = map.getSize();
if (!mapSize) return;
//
const width = getPositionPx();
// @ts-expect-error -- This only works if you are bundling ol yourself
const devicePixelRatio = map.pixelRatio_;
const bbox = opts?.left
? {
tl: [0, 0],
tr: [width, 0],
bl: [0, mapSize[1]],
br: [width, mapSize[1]],
}
: {
tl: [width, 0],
tr: [mapSize[0], 0],
bl: [width, mapSize[1]],
br: [mapSize[0], mapSize[1]],
};
const tl = getRenderPixel(event, bbox.tl);
const tr = getRenderPixel(event, bbox.tr);
const bl = getRenderPixel(event, bbox.bl);
const br = getRenderPixel(event, bbox.br);
if (ctx instanceof WebGLRenderingContext) {
if (opts?.left) {
if (!opts.bothWebGl) {
ctx.clear(ctx.COLOR_BUFFER_BIT);
}
ctx.enable(ctx.SCISSOR_TEST);
ctx.scissor(
bl[0],
bl[1],
width * devicePixelRatio,
mapSize[1] * devicePixelRatio
);
} else {
ctx.enable(ctx.SCISSOR_TEST);
ctx.clear(ctx.COLOR_BUFFER_BIT);
ctx.scissor(
bl[0],
bl[1],
(mapSize[0] - width) * devicePixelRatio,
mapSize[1] * devicePixelRatio
);
}
return;
}
if (opts?.isRightWebGL) {
ctx.clearRect(0, 0, br[0], br[1]);
}
ctx.save();
ctx.beginPath();
ctx.moveTo(tl[0], tl[1]);
ctx.lineTo(bl[0], bl[1]);
ctx.lineTo(br[0], br[1]);
ctx.lineTo(tr[0], tr[1]);
ctx.closePath();
ctx.clip();
};
}
function onPostRender(event: RenderEvent) {
const ctx = event.context as CanvasRenderingContext2D;
if (!ctx) {
return;
}
if (ctx instanceof WebGLRenderingContext) {
ctx.disable(ctx.SCISSOR_TEST);
return;
}
ctx.restore();
}
// store your own discriminator on the OpenLayers Layers when you create them
// see: https://openlayers.org/en/latest/apidoc/module-ol_layer_Layer-Layer.html#set
const isLeftWebGL = left?.get("type") === "webgl";
const isRightWebGL = right?.get("type") === "webgl";
const bothWebGl = isLeftWebGL && isRightWebGL;
const leftHandler = onPreRender({ left: true, bothWebGl, isRightWebGL });
left?.on("prerender", leftHandler);
left?.on("postrender", onPostRender);
const rightHandler = onPreRender({ isRightWebGL, bothWebGl });
right?.on("prerender", rightHandler);
right?.on("postrender", onPostRender);
map.render();
return function off() {
left?.un("prerender", leftHandler);
left?.un("postrender", onPostRender);
right?.un("prerender", rightHandler);
right?.un("prerender", onPostRender);
map.render();
};
}
Usage
import "./styles.css";
import Map from "ol/Map.js";
import OSM from "ol/source/OSM.js";
import WebGLTileLayer from "ol/layer/WebGLTile";
import View from "ol/View.js";
import "ol/ol.css";
import VectorSource from "ol/source/Vector";
import { GeoJSON } from "ol/format";
import VectorLayer from "ol/layer/Vector";
import { compare } from "./compare";
const left = new WebGLTileLayer({
source: new OSM(),
properties: {
type: "webgl",
},
});
const polygon = {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [
[
[-5e6, -1e6],
[-3e6, -1e6],
[-4e6, 1e6],
[-5e6, -1e6],
],
],
},
};
const vectorSource = new VectorSource({
features: new GeoJSON().readFeatures(polygon),
});
const right = new VectorLayer({
source: vectorSource,
properties: {
type: "vector",
},
});
const map = new Map({
target: "map",
layers: [left, right],
view: new View({
center: [0, 0],
zoom: 2,
}),
});
const checkbox = document.getElementById("toggle-swipe") as HTMLInputElement;
const slider = document.getElementById("slider") as HTMLInputElement;
const leftOnCheckbox = document.getElementById("left-on") as HTMLInputElement;
const rightOnCheckbox = document.getElementById("right-on") as HTMLInputElement;
let endCompare: ReturnType<typeof compare> | undefined;
let px = slider.valueAsNumber * map.getSize()![0];
let on = true;
let leftOn = true;
let rightOn = true;
function startCompare() {
const getOff = () =>
compare(
map,
() => px,
leftOn ? left : undefined,
rightOn ? right : undefined
);
if (endCompare) {
endCompare();
endCompare = getOff();
} else {
endCompare = getOff();
}
}
checkbox.onchange = function (ev) {
on = (ev.target as HTMLInputElement).checked;
if (on) {
startCompare();
} else {
endCompare?.();
endCompare = undefined;
}
map.render();
};
slider.oninput = function (ev) {
px = (ev.target as HTMLInputElement).valueAsNumber * map.getSize()![0];
map.render();
};
leftOnCheckbox.onchange = function (ev) {
leftOn = (ev.target as HTMLInputElement).checked;
startCompare();
};
rightOnCheckbox.onchange = function (ev) {
rightOn = (ev.target as HTMLInputElement).checked;
startCompare();
};
if (on) {
startCompare();
}