官网demo地址:
Magnify
这篇讲了如何在地图上添加放大镜效果。
首先加载底图
const layer = new TileLayer({
source: new StadiaMaps({
layer: "stamen_terrain_background",
}),
});
const container = document.getElementById("map");
const map = new Map({
layers: [layer],
target: container,
view: new View({
center: fromLonLat([-109, 46.5]),
zoom: 6,
}),
});
鼠标移动的时候,调用render方法,触发postrender事件。
container.addEventListener("mousemove", function (event) {
mousePosition = map.getEventPixel(event);
map.render();
});
container.addEventListener("mouseout", function () {
mousePosition = null;
map.render();
});
postrender事件中可以获取到鼠标移动的位置,实时绘制圆形和放大后的图像。
先用getRenderPixel将地理坐标转换为屏幕坐标,通过勾股定理(直角三角形的两条直角边的平方和等于斜边的平方)算出半径。
layer.on("postrender", function (event) {
if (mousePosition) {
const pixel = getRenderPixel(event, mousePosition);
const offset = getRenderPixel(event, [
mousePosition[0] + radius,
mousePosition[1],
]);
//计算半径
const half = Math.sqrt(
Math.pow(offset[0] - pixel[0], 2) + Math.pow(offset[1] - pixel[1], 2)
);
}
});
获取放大镜范围内所需要的图像。
//从画布上下文中提取放大镜区域的图像数据:
const context = event.context;
const centerX = pixel[0];
const centerY = pixel[1];
//正方形左边的顶点
const originX = centerX - half;
const originY = centerY - half;
//计算直径
const size = Math.round(2 * half + 1);
const sourceData = context.getImageData(
originX,
originY,
size,
size
).data;
//获取正方形范围下所有的像素点
const dest = context.createImageData(size, size);
const destData = dest.data;
然后开始创建放大后的图像数据。
// 创建放大后的图像数据
for (let j = 0; j < size; ++j) {
for (let i = 0; i < size; ++i) {
//dI 和 dJ 是相对于中心的偏移
const dI = i - half;
const dJ = j - half;
//点到中心的距离
const dist = Math.sqrt(dI * dI + dJ * dJ);
let sourceI = i;
let sourceJ = j;
//如果 dist 小于 half,根据偏移和缩放因子计算新的像素位置
if (dist < half) {
sourceI = Math.round(half + dI / 2);
sourceJ = Math.round(half + dJ / 2);
}
const destOffset = (j * size + i) * 4;
const sourceOffset = (sourceJ * size + sourceI) * 4;
destData[destOffset] = sourceData[sourceOffset];
destData[destOffset + 1] = sourceData[sourceOffset + 1];
destData[destOffset + 2] = sourceData[sourceOffset + 2];
destData[destOffset + 3] = sourceData[sourceOffset + 3];
}
}
要看懂这段代码我们需要来好好分析一下。
放大的关键在于 dI / 2
和 dJ / 2
的计算。实际上是将像素距离中心点的偏移量减半,从而将像素“拉近”到中心点。放大镜区域内的像素将被集中在更小的区域内,看起来像是被放大了 。
简单来说,如果我们的圆形下本来有16个像素格子,每个格子展示不同的像素,放大效果就是让两个、三个、或者四个格子都展示同一个像素,那看起来中间部分就会比较大。
我们通过一个简单的 4x4 像素的例子来详细说明这段代码是如何实现放大镜效果的。
假设这是图像的像素点。
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
每个点用坐标表示就是这样:
(0,0) (1,0) (2,0) (3,0)
(0,1) (1,1) (2,1) (3,1)
(0,2) (1,2) (2,2) (3,2)
(0,3) (1,3) (2,3) (3,3)
for (let j = 0; j < size; ++j) {
for (let i = 0; i < size; ++i) {
const dI = i - half;
const dJ = j - half;
const dist = Math.sqrt(dI * dI + dJ * dJ);
let sourceI = i;
let sourceJ = j;
if (dist < half) {
sourceI = Math.round(half + dI / 2);
sourceJ = Math.round(half + dJ / 2);
}
}
}
假设 half
是 2,我们要遍历 4x4 区域的所有像素,计算每个像素在放大镜效果下的新位置。
循环第一行 (i = 0, j = 0 到 3)
-
(0, 0)
:dI = 0 - 2 = -2
dJ = 0 - 2 = -2
dist = Math.sqrt((-2)^2 + (-2)^2) = Math.sqrt(8) ≈ 2.83
- 因为
dist > 2
,所以sourceI = 0
,sourceJ = 0
- 拷贝
(0, 0)
位置的像素数据
-
(1, 0)
:dI = 1 - 2 = -1
dJ = 0 - 2 = -2
dist = Math.sqrt((-1)^2 + (-2)^2) = Math.sqrt(5) ≈ 2.24
- 因为
dist > 2
,所以sourceI = 1
,sourceJ = 0
- 拷贝
(1, 0)
位置的像素数据
-
(2, 0)
:dI = 2 - 2 = 0
dJ = 0 - 2 = -2
dist = Math.sqrt(0^2 + (-2)^2) = Math.sqrt(4) = 2
- 因为
dist <= 2
,所以sourceI = Math.round(2 + 0 / 2) = 2
sourceJ = Math.round(2 + (-2) / 2) = 1
- 拷贝
(2, 1)
位置的像素数据
-
(3, 0)
:dI = 3 - 2 = 1
dJ = 0 - 2 = -2
dist = Math.sqrt(1^2 + (-2)^2) = Math.sqrt(5) ≈ 2.24
- 因为
dist > 2
,所以sourceI = 3
,sourceJ = 0
- 拷贝
(3, 0)
位置的像素数据
循环第二行 (i = 0, j = 1 到 3)
-
(0, 1)
:dI = 0 - 2 = -2
dJ = 1 - 2 = -1
dist = Math.sqrt((-2)^2 + (-1)^2) = Math.sqrt(5) ≈ 2.24
- 因为
dist > 2
,所以sourceI = 0
,sourceJ = 1
- 拷贝
(0, 1)
位置的像素数据
-
(1, 1)
:dI = 1 - 2 = -1
dJ = 1 - 2 = -1
dist = Math.sqrt((-1)^2 + (-1)^2) = Math.sqrt(2) ≈ 1.41
- 因为
dist <= 2
,所以sourceI = Math.round(2 + (-1) / 2) = 1.5 ≈ 2
sourceJ = Math.round(2 + (-1) / 2) = 1.5 ≈ 2
- 拷贝
(2, 2)
位置的像素数据
-
(2, 1)
:dI = 2 - 2 = 0
dJ = 1 - 2 = -1
dist = Math.sqrt(0^2 + (-1)^2) = Math.sqrt(1) = 1
- 因为
dist <= 2
,所以sourceI = Math.round(2 + 0 / 2) = 2
sourceJ = Math.round(2 + (-1) / 2) = 1.5 ≈ 2
- 拷贝
(2, 2)
位置的像素数据
-
(3, 1)
:dI = 3 - 2 = 1
dJ = 1 - 2 = -1
dist = Math.sqrt(1^2 + (-1)^2) = Math.sqrt(2) ≈ 1.41
- 因为
dist <= 2
,所以sourceI = Math.round(2 + 1 / 2) = 2.5 ≈ 3
sourceJ = Math.round(2 + (-1) / 2) = 1.5 ≈ 2
- 拷贝
(3, 2)
位置的像素数据
通过这种方式,我们得到新的像素点坐标
(0,0) (1,0) (2,1) (3,0)
(0,1) (2,2) (2,2) (3,2)
(1,2) (2,2) (2,2) (3,2)
(0,3) (2,3) (2,3) (3,3)
跟原坐标对比下:
(0,0) (1,0) (2,0) (3,0)
(0,1) (1,1) (2,1) (3,1)
(0,2) (1,2) (2,2) (3,2)
(0,3) (1,3) (2,3) (3,3)
对比之下发现(2,2)坐标下的像素由原本的一个点展示变成了四个点展示,周围的像素点也发生了一些变化,由此,中间部分就被放大了。
接下里就是把像素点放进新数组中。
const destOffset = (j * size + i) * 4;
const sourceOffset = (sourceJ * size + sourceI) * 4;
destData[destOffset] = sourceData[sourceOffset]; //r
destData[destOffset + 1] = sourceData[sourceOffset + 1]; //g
destData[destOffset + 2] = sourceData[sourceOffset + 2]; //b
destData[destOffset + 3] = sourceData[sourceOffset + 3]; //a
因为图像数据在数组中的存储规则是:
[r,g,b,a,r,g,b,a,r,g,b,a,r,g,b,a...]
因此通过计算得到像素点在数组中的位置destOffset,而sourceOffset 则是计算的偏移后的数组位置。
最后再将放大镜的圆形绘制到地图上就可以了。
//绘制圆形
context.beginPath();
context.arc(centerX, centerY, half, 0, 2 * Math.PI);
context.lineWidth = (3 * half) / radius;
context.strokeStyle = "rgba(255,255,255,0.5)";
context.putImageData(dest, originX, originY);
context.stroke();
context.restore();
完整代码:
<template>
<div class="box">
<h1>Magnify</h1>
<div id="map" class="map"></div>
</div>
</template>
<script>
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";
import StadiaMaps from "ol/source/StadiaMaps.js";
export default {
name: "",
components: {},
data() {
return {
map: null,
};
},
computed: {},
created() {},
mounted() {
const layer = new TileLayer({
source: new StadiaMaps({
layer: "stamen_terrain_background",
}),
});
const container = document.getElementById("map");
const map = new Map({
layers: [layer],
target: container,
view: new View({
center: fromLonLat([-109, 46.5]),
zoom: 6,
}),
});
let radius = 75;
document.addEventListener("keydown", function (evt) {
if (evt.key === "ArrowUp") {
radius = Math.min(radius + 5, 150);
map.render();
evt.preventDefault();
} else if (evt.key === "ArrowDown") {
radius = Math.max(radius - 5, 25);
map.render();
evt.preventDefault();
}
});
// get the pixel position with every move
let mousePosition = null;
container.addEventListener("mousemove", function (event) {
mousePosition = map.getEventPixel(event);
map.render();
});
container.addEventListener("mouseout", function () {
mousePosition = null;
map.render();
});
layer.on("postrender", function (event) {
if (mousePosition) {
const pixel = getRenderPixel(event, mousePosition);
const offset = getRenderPixel(event, [
mousePosition[0] + radius,
mousePosition[1],
]);
//计算半径
const half = Math.sqrt(
Math.pow(offset[0] - pixel[0], 2) + Math.pow(offset[1] - pixel[1], 2)
);
//从画布上下文中提取放大镜区域的图像数据:
const context = event.context;
const centerX = pixel[0];
const centerY = pixel[1];
//正方形左边的顶点
const originX = centerX - half;
const originY = centerY - half;
//计算直径
const size = Math.round(2 * half + 1);
const sourceData = context.getImageData(
originX,
originY,
size,
size
).data;
//获取正方形范围下所有的像素点
const dest = context.createImageData(size, size);
const destData = dest.data;
// 创建放大后的图像数据
for (let j = 0; j < size; ++j) {
for (let i = 0; i < size; ++i) {
//dI 和 dJ 是相对于中心的偏移
const dI = i - half;
const dJ = j - half;
//点到中心的距离
const dist = Math.sqrt(dI * dI + dJ * dJ);
let sourceI = i;
let sourceJ = j;
//如果 dist 小于 half,根据偏移和缩放因子计算新的像素位置
if (dist < half) {
sourceI = Math.round(half + dI / 2);
sourceJ = Math.round(half + dJ / 2);
}
const destOffset = (j * size + i) * 4;
const sourceOffset = (sourceJ * size + sourceI) * 4;
destData[destOffset] = sourceData[sourceOffset];
destData[destOffset + 1] = sourceData[sourceOffset + 1];
destData[destOffset + 2] = sourceData[sourceOffset + 2];
destData[destOffset + 3] = sourceData[sourceOffset + 3];
}
}
//绘制圆形
context.beginPath();
context.arc(centerX, centerY, half, 0, 2 * Math.PI);
context.lineWidth = (3 * half) / radius;
context.strokeStyle = "rgba(255,255,255,0.5)";
context.putImageData(dest, originX, originY);
context.stroke();
context.restore();
}
});
},
methods: {},
};
</script>
<style lang="scss" scoped>
#map {
width: 100%;
height: 500px;
position: relative;
}
.box {
height: 100%;
}
</style>