官网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 movelet 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>