赞
踩
上一篇博文中,我们引入了「逻辑画布」的概念,让整个工具的页面看起来 “专业” 了很多。这也为后续的很多实用的功能打下了基础,例如本篇博文要讲到的小地图MiniMap
。
如果你使用过市面上的一些图文编辑器,或者玩过一些游戏,就很容易理解「小地图」的作用。我们在屏幕中能看到的部分——前文中已经介绍过——叫做「视口」,视口之外的部分都看不到。小地图的作用是将全局视图按一定比例缩小,让我们能大体上总览全局的内容。
这篇博文是《前端canvas项目实战——在线图文编辑器》付费专栏系列博文的第十篇——小地图MiniMap(上),主要的内容有:
如有需要,你可以:
动手体验
CodeSandbox会自动对代码进行编译,并提供地址以供体验代码效果
由于CSDN的链接跳转有问题,会导致页面无法工作,请复制以下链接在浏览器打开:
https://zss88t.csb.app/
动态效果
这一节之后,我们就可以拖动小地图中的滑动窗口来改变画布的视口了。
动手实现之前,让我们先思考几个问题:
小地图使用什么方法实现?
经过调研,市面上的图文编辑器基本上都是通过堆叠div
标签实现小地图的。但是理性的思考告诉我们,这种方式实现的小地图可维护性很低,且实现起来很复杂。因此我决定再用一个canvas
来实现我们的小地图, 就好像「主画布」的一个缩小版。
小地图应该设计成什么形状,多大?
具体的来说,这个问题就是说我们的小地图,「长」和「宽」应该设置为多少,怎样的比例。经过思考,我认为小地图的长宽比应该和画布的「视口」一样。其中长和宽应该是视口的1/10
。这里引入一个变量canvasMiniMapRatio=10
,意为画布的长宽和小地图的长宽比为10:1
,这个变量会在下文中多次用到。
小地图中应该有哪些元素?
先看代码:
/**
* (根据画布中的对象变化)更新小地图的背景图
* @param canvas 画布实例
* @param miniMap 小地图实例
* @param canvasMiniMapRatio 画布和小地图尺寸的比例(默认为10)
*/
const updateMiniMapBackground = ({canvas, miniMap, canvasMiniMapRatio}) => {
// 1. 记录画布当前的viewportTransform数组
const viewportTransform = canvas.viewportTransform;
// 2. 缩放画布到适配窗口大小并居中
zoomAndCenterCanvas(canvas);
// 3. 获取当前画布截图
const canvasElement = canvas.toCanvasElement(1 / canvasMiniMapRatio);
const backgroundImage = new fabric.Image(canvasElement);
// 4. 恢复画布的viewportTransform为记录的值
canvas.setViewportTransform(viewportTransform);
// 5. 将得到的背景图赋值给小地图
miniMap.setBackgroundImage(backgroundImage);
miniMap.renderAll();
};
代码的逻辑很清晰,共分为5个步骤:
1) 记录画布当前的viewportTransform数组: 由于第2步会改变这个值,且后续的步骤还需要用到它来把画布恢复到当前位置,所以先用一个变量做记录。
2) 缩放画布到适配窗口大小并居中: 由于需要获取画布截图时,其视口可能已经不在正中央,且经过了缩放,大小发生了变化。所以在截图之前要先让画布缩放回适配窗口的大小且居中。zoomAndCenterCanvas
方法的代码逻辑我在上篇博文中有详细的讲解,如需回顾,点击前往
3) 获取当前画布的截图: 先获取1 / 10大小的画布DOM对象,然后通过new fabirc.Image
来创建截图。
4) 恢复画布的viewportTransform为记录的值: 把画布的缩放情况和视口恢复回获取截图前的状态。
5) 将得到的背景图赋值给小地图: 别忘了,miniMap也是一个fabric.Canvas
的实例,所以我们可以通过setBackgroundImage
为miniMap设置背景图。
滑动窗口小地图上的一个很实用的交互工具,用户可以通过拖动滑动窗口来改变画布的视口。当画布被放大时,滑动窗口所包围的区域就是当前视口所展现的,画布中我们正在看的部分;滑动窗口以外的部分,不在视口中,移动视口之后才可以看到。
代码如下:
/**
* 更新小地图的滑动窗口
* @param canvas 画布实例
* @param miniMap 小地图实例
* @param canvasMiniMapRatio 画布和小地图尺寸的比例(默认为10)
*/
const updateMiniMapSlideWindow = ({canvas, miniMap, canvasMiniMapRatio}) => {
...
// 1. 清除小地图中的所有对象(滑动窗口 + 4个遮罩)
const objects = miniMap.getObjects();
for (let i = 0; i < objects.length; i++) {
miniMap.remove(objects[i]);
}
// 2. 获取新的滑动窗口
let newSlideWindow = getNewSlideWindow({canvas, miniMap, canvasMiniMapRatio});
// 3. 添加新的滑动窗口到小地图中
miniMap.add(newSlideWindow);
miniMap.setActiveObject(newSlideWindow);
miniMap.renderAll();
};
这里的代码看起来很简单,注释很清楚,就不再赘述了。我们把getNewSlideWindow
方法展开讲解:
/**
* (根据画布的视口)获取小地图中新的滑动窗口实例
* @param canvas 画布实例
* @param miniMap 小地图实例
* @param canvasMiniMapRatio 画布宽高 : 小地图宽高(默认=10)
* @returns {fabric.Rect} 新的滑动窗口实例
*/
const getNewSlideWindow = ({canvas, miniMap, canvasMiniMapRatio}) => {
let canvasZoomRatio = canvas.getZoom() / calcZoomValueToFitWindow();
// 1. 获取新的滑动窗口宽高
const dimensions = _getNewSlideWindowDimensions({miniMap, canvasZoomRatio});
// 2. 获取新的滑动窗口位置
const positions = _getNewSlideWindowPositions({canvas, miniMap, canvasMiniMapRatio, canvasZoomRatio});
// 3. 实例化一个fabric.Rect作为滑动窗口,并返回
return new fabric.Rect({
fill: 'transparent',
stroke: "dodgerblue",
strokewidth: 1,
hasControls: false,
hasBorders: false,
...dimensions,
...positions
});
};
可以看到,「滑动窗口」实际上是一个fabric.Rect
的实例,通过hasControls: false
和hasBorders: false
设置了它没有选择框、也没有控制点。
重点在于1/2
两步,如何计算滑动窗口的宽高和位置, 这里我们再展开一层,看看计算这些数值的代码逻辑。
/**
* 计算得到新的滑动窗口宽高
* @param miniMap 小地图实例
* @param canvasZoomRatio 画布当前缩放值 : 画布适配窗口大小缩放值
* @returns {{width: number, height: number}} 新的滑动窗口宽高
* @private
*/
const _getNewSlideWindowDimensions = ({miniMap, canvasZoomRatio}) => {
if (canvasZoomRatio <= 1) {
canvasZoomRatio = 1;
}
return {
width: miniMap.width / canvasZoomRatio - 1,
height: miniMap.height / canvasZoomRatio - 1
};
};
这段代码包含以下两个部分:
zoom
成反比,画布被放大得越大,滑动窗口越小,其中的canvasZoomRatio=画布当前缩放值 : 画布适配窗口大小缩放值
,即上文getNewSlideWindow
方法中的canvasZoomRatio = canvas.getZoom() / calcZoomValueToFitWindow()
。canvasZoomRatio
最小为1
。 /**
* 计算得到新的滑动窗口位置
* @param canvas 画布实例
* @param miniMap 小地图实例
* @param canvasMiniMapRatio 画布宽高 : 小地图宽高(默认=10)
* @param canvasZoomRatio 画布当前缩放值 : 画布适配窗口大小缩放值
* @returns {{top: number, left: number}} 新的滑动窗口位置
* @private
*/
const _getNewSlideWindowPositions = ({canvas, miniMap, canvasMiniMapRatio, canvasZoomRatio}) => {
// 计算逻辑画布中心点在视口坐标系中的位置
const viewportTransform = canvas.viewportTransform;
const {logicCanvas} = store.getState();
const offsetX = ((logicCanvas.width / 2) * viewportTransform[0] + viewportTransform[4]) / canvasMiniMapRatio / canvasZoomRatio;
const offsetY = ((logicCanvas.height / 2) * viewportTransform[3] + viewportTransform[5]) / canvasMiniMapRatio / canvasZoomRatio;
const {x, y} = miniMap.getCenterPoint();
return {
left: x - offsetX,
top: y - offsetY
};
};
这段代码的核心在于offsetX
和offsetY
的计算公式,其目的在于将canvas
中视口的对应位置等比例缩小,求得在小地图中的坐标,offsetX
和offsetY
是滑动窗口的左上点距离小地图中心点的位移。
最后通过left: x - offsetX
和top: y - offsetY
得到小地图的位置信息。
遮罩用于降低小地图中,滑动窗口
之外区域的透明度,突出滑动窗口包围起来的区域。起初,我尝试了多种方式来实现它,包括给miniMap设置clipPath等,都不能完美实现。直到最后,想到了一个简单的方法。
我们先在概念上达成一致,下图中红色区域之内,蓝色区域之外的滑动窗口的部分就是「遮罩」:
这一片颜色较深的灰色区域,实际上是用4个梯形拼接而成的,为了便于说明,我将每个部分分别填充不同的颜色,加以区分:
这样,遮罩的实现就一目了然了,我们使用fabric.Polygon多边形
来实现每一个梯形。这里仅列举左侧的蓝色的梯形代码:
/**
* 更新小地图的遮罩区域
* @param miniMap 小地图实例
*/
const updateMiniMapMask = ({miniMap}) => {
// 1. 移除miniMap中所有的遮罩(fabric.Polygon)
let objects = miniMap.getObjects();
for (let i = 0; i < objects.length; i++) {
if (objects[i]?.type !== "rect") {
miniMap.remove(objects[i]);
}
}
// 2. 定义共有的属性
let slideWindow = miniMap.getObjects()[0];
const properties = {
opacity: 0.2,
hasControls: false,
hasBorders: false,
strokeWidth: 0,
selectable: false,
evented: false
}
// 3. 设置梯形顶点坐标
const polygonLeftPoints = [
{x: 0, y: 0},
{x: slideWindow.left, y: slideWindow.top},
{x: slideWindow.left, y: slideWindow.top + slideWindow.height},
{x: 0, y: miniMap.height}
];
// 4. 实例化梯形并添加到miniMap
let polygonLeft = new fabric.Polygon(polygonLeftPoints, properties);
miniMap.add(polygonLeft);
miniMap.renderAll();
};
这里的代码简单易懂,没有难点,只需要注意梯形的4个顶点坐标设置正确即可。
本片博文中,我们列举了小地图MiniMap,其4个核心方法中的3个。篇幅已经很长了,我把剩下的1个核心方法和将他们组合起来的部分放在下一篇博文。也希望大家在阅读和学习的过程中有个中途休息,我们缓一缓,下一篇博文再把他讲完。
如有需要,你可以:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。