官网demo地址:
这篇示例讲了卷帘效果。
所谓的卷帘效果实际上是在地图上添加了两个不同的图层,通过滑块滑动实时剪裁上层图层的图像,漏出下层图层。方便观察对比两个图层的不同。
首先先初始化地图,并加载底层图层
initMap() {
this.map = new Map({
target: "map",
view: new View({
center: [0, 0],
zoom: 2,
projection: "EPSG:4326",
}),
});
},
addUnderLayer() {
let underLayer = new TileLayer({
source: new StadiaMaps({
layer: "outdoors",
}),
});
this.map.addLayer(underLayer);
},
然后加载上层图层
const layer = new TileLayer({
source: new StadiaMaps({
layer: "stamen_terrain_background",
}),
});
this.map.addLayer(layer);
上层图层的裁剪效果主要依靠滑块滑动时调用this.map.render()触发两个事件prerender(图层渲染前)和postrender(图层渲染后)来实现的。
swipeChange() {
this.map.render();
},
layer.on("prerender", (event) => {
const ctx = event.context;
const mapSize = this.map.getSize();
const width = mapSize[0] * (this.swipeVal / 100);
const tl = getRenderPixel(event, [width, 0]);
const tr = getRenderPixel(event, [mapSize[0], 0]);
const bl = getRenderPixel(event, [width, mapSize[1]]);
const br = getRenderPixel(event, mapSize);
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();
});
layer.on("postrender", function (event) {
const ctx = event.context;
ctx.restore();
});
代码看着不多,但要真正理解为啥要这么写我们不妨先来看一个小示例
使用canvas加载一张图片,并在其上层绘制一个圆形,裁剪下方的图片。
window.onload = function () {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const image = new Image();
image.src = 'https://t7.baidu.com/it/u=2604797219,1573897854&fm=193&f=GIF'; // 替换为您的图片路径
image.onload = function () {
// 保存当前绘图状态
ctx.save();
// 定义圆形剪切路径
let cx = 200;
let cy = 200;
let radius = 100;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2, false);
ctx.clip();
// 绘制图像到裁剪路径内
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
}
};
这个顺序非要重要,是先绘制圆形剪裁区,再添加图片。如果把顺序换过来,将会是这样:
image.onload = function () {
// 保存当前绘图状态
ctx.save();
// 定义圆形剪切路径
// 绘制图像到裁剪路径内
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
let cx = 200;
let cy = 200;
let radius = 100;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2, false);
ctx.clip();
}
可以看到图片没有被剪切。
这也是为什么我们的卷帘裁剪是在 prerender(图层渲染前)进行,而不是图层渲染后或其他时机。
卷帘效果绘制的过程和绘制圆形裁剪区的代码类似,卷帘是以滑块的值为界线将左边的图像进行裁剪,而且绘制的时候要先使用getRenderPixel把地理坐标转换为屏幕坐标。
layer.on("prerender", (event) => {
const ctx = event.context;
const mapSize = this.map.getSize();
const width = mapSize[0] * (this.swipeVal / 100);
const tl = getRenderPixel(event, [width, 0]);
const tr = getRenderPixel(event, [mapSize[0], 0]);
const bl = getRenderPixel(event, [width, mapSize[1]]);
const br = getRenderPixel(event, mapSize);
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();
});
接下来来看postrender渲染后
layer.on("postrender", function (event) {
const ctx = event.context;
ctx.restore();
});
这段代码非常重要,如果不写,卷帘效果会是这样:
左边的图像全都不见了,这是为什么呢?ctx.restore()为什么如此重要?
继续回到canvas圆形裁剪图形的例子,剪切完之后,继续绘制一个圆形
image.onload = function () {
// 保存当前绘图状态
ctx.save();
// 定义圆形剪切路径
let cx = 200;
let cy = 200;
let radius = 100;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2, false);
ctx.clip();
// 绘制图像到裁剪路径内
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// 绘制圆形边框
ctx.beginPath();
ctx.strokeStyle = '#ccc';
ctx.fillStyle = 'rgba(255,255,24,0.5)';
ctx.lineWidth = 10;
ctx.arc(300, 300, radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fill()
}
这时发现圆形只绘制出来了一小部分,这是因为绘制剪切圆形路径之后,将只能在圆形上绘制图像,如果绘制的图形超出了圆形范围,将不会显示出来。
而加上 ctx.restore()之后:
image.onload = function () {
// 保存当前绘图状态
ctx.save();
// 定义圆形剪切路径
let cx = 200;
let cy = 200;
let radius = 100;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2, false);
ctx.clip();
// 绘制图像到裁剪路径内
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// 恢复剪切路径,以便绘制边框
ctx.restore();
// 绘制圆形边框
ctx.beginPath();
ctx.strokeStyle = '#ccc';
ctx.fillStyle = 'rgba(255,255,24,0.5)';
ctx.lineWidth = 10;
ctx.arc(300, 300, radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fill()
}
才可以继续绘制图形。
所以绘制卷帘时要在 postrender事件中将路径恢复到原本状态。
layer.on("postrender", function (event) {
const ctx = event.context;
ctx.restore();
});
完整代码:
canvas小示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Image Cropping</title>
<style>
#canvas-container {
display: flex;
}
canvas {
border: 1px solid black;
margin-right: 10px;
}
</style>
</head>
<body>
<div id="canvas-container">
<canvas id="canvas" width="600" height="600"></canvas>
</div>
<script>
window.onload = function () {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const image = new Image();
image.src = 'https://t7.baidu.com/it/u=2604797219,1573897854&fm=193&f=GIF'; // 替换为您的图片路径
image.onload = function () {
// 保存当前绘图状态
ctx.save();
// 定义圆形剪切路径
let cx = 200;
let cy = 200;
let radius = 100;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2, false);
ctx.clip();
// 绘制图像到裁剪路径内
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
// 恢复剪切路径,以便绘制边框
ctx.restore();
// 绘制圆形边框
ctx.beginPath();
ctx.strokeStyle = '#ccc';
ctx.fillStyle = 'rgba(255,255,24,0.5)';
ctx.lineWidth = 10;
ctx.arc(300, 300, radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fill()
}
};
</script>
</body>
</html>
卷帘效果:
<template>
<div class="box">
<h1>Layer Swipe</h1>
<div id="map" class="map"></div>
<input
id="swipe"
type="range"
v-model="swipeVal"
style="width: 100%"
@input="swipeChange"
/>
</div>
</template>
<script>
import StadiaMaps from "ol/source/StadiaMaps.js";
import Map from "ol/Map.js";
import TileLayer from "ol/layer/Tile.js";
import View from "ol/View.js";
import XYZ from "ol/source/XYZ.js";
import { fromLonLat } from "ol/proj.js";
import { getRenderPixel } from "ol/render.js";
export default {
name: "",
components: {},
data() {
return {
map: null,
swipeVal: 50,
};
},
computed: {},
created() {},
mounted() {
this.initMap();
this.addUnderLayer()
this.addClipLayer();
},
methods: {
swipeChange() {
this.map.render();
},
addClipLayer() {
const layer = new TileLayer({
source: new StadiaMaps({
layer: "stamen_terrain_background",
}),
});
this.map.addLayer(layer);
layer.on("prerender", (event) => {
const ctx = event.context;
const mapSize = this.map.getSize();
const width = mapSize[0] * (this.swipeVal / 100);
const tl = getRenderPixel(event, [width, 0]);
const tr = getRenderPixel(event, [mapSize[0], 0]);
const bl = getRenderPixel(event, [width, mapSize[1]]);
const br = getRenderPixel(event, mapSize);
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();
});
layer.on("postrender", function (event) {
const ctx = event.context;
ctx.restore();
});
},
initMap() {
this.map = new Map({
target: "map",
view: new View({
center: [0, 0],
zoom: 2,
projection: "EPSG:4326",
}),
});
},
addUnderLayer() {
let underLayer = new TileLayer({
source: new StadiaMaps({
layer: "outdoors",
}),
});
this.map.addLayer(underLayer);
},
},
};
</script>
<style lang="scss" scoped>
#map {
width: 100%;
height: 500px;
}
.box {
height: 100%;
}
</style>