目录
- 前言
- 二、 实现步骤
- 4. 根据滑动窗口的位置和大小改变画布的视口
- 5. 组装起来:实现小地图的整个生命周期
- 5.0 初始化小地图
- 5.1 小地图监听事件,滑动窗口被拖动时,更新画布视口和小地图的遮罩
- 5.2 画布监听事件:画布的视口发生变化时,更新小地图滑动窗口大小、位置和遮罩
- 5.3 画布监听事件:画布中的对象被添加、删除、修改时,更新小地图背景图
- 5.n 页面销毁时:注销事件监听器,避免内存泄漏
- 6. 小地图受控组件
- 三、Show u the code
- 后记
前言
上一篇博文中,我们列举了小地图MiniMap,其4个核心方法中的3个。由于篇幅所限,在这篇博文中继续介绍剩余的内容。
这篇博文是《前端canvas项目实战——在线图文编辑器》付费专栏系列博文的第十一篇——小地图MiniMap(下),主要的内容有:
- 实现用鼠标拖动滑动窗口,画布中的视口随之改变。
- 将4部分的核心代码组合起来,实现小地图的整个「生命周期」。
如有需要,你可以:
二、 实现步骤
这里接着上文介绍小地图的实现步骤中,剩余的代码逻辑。如果想要回顾前面的代码,可以点击前往。
4. 根据滑动窗口的位置和大小改变画布的视口
上文中有介绍到,用户可以用鼠标拖动滑动窗口来改变画布的视口,以看到视口以外的画布内容。
/*** (根据小地图中滑动窗口的大小和位置)更新画布视口* @param canvas 画布实例* @param miniMap 小地图实例* @param canvasMiniMapRatio 画布和小地图尺寸的比例(默认为10)*/const updateCanvasViewport = ({canvas, miniMap, canvasMiniMapRatio}) => {const objects = miniMap.getObjects();if (!objects || objects.length === 0) {throw new ReferenceError("[-] Slide window is not shown in mini-map!");}const {x, y} = miniMap.getCenterPoint();const slideWindow = miniMap.getObjects()[0];const viewportTransform = canvas.viewportTransform;const canvasZoomRatio = viewportTransform[0] / calcZoomValueToFitWindow();const offsetX = (x - slideWindow.left) * canvasZoomRatio * canvasMiniMapRatio;const offsetY = (y - slideWindow.top) * canvasZoomRatio * canvasMiniMapRatio;const {logicCanvas} = store.getState();viewportTransform[4] = offsetX - (logicCanvas.width / 2) * viewportTransform[0];viewportTransform[5] = offsetY - (logicCanvas.height / 2) * viewportTransform[3];canvas.setViewportTransform(viewportTransform, false);canvas.renderAll();};
这里的代码不做冗余的介绍,其中的计算公式和上一篇博文中 2.2 获取新的滑动窗口「位置」小节中的公式恒等。
5. 组装起来:实现小地图的整个生命周期
HTML
部分和画布canvas
类似:
<div className="mini-map-container"><canvas id="mini-map"/></div>
我们创建一个名为useMiniMap
的自定义Hook
,使小地图的生命周期可以自行闭环,避免和canvas
互相耦合:
/*** 小地图逻辑的Hook* @param canvas 画布实例* @param canvasMiniMapRatio 画布和小地图尺寸的比例(默认为10)*/
const useMiniMap = (canvas, canvasMiniMapRatio) => {useEffect(() => {if (!canvas) {return;}// 0. 初始化小地图// 1. 小地图监听事件:滑动窗口被拖动时,更新画布视口和小地图的遮罩// 2. 画布监听事件:画布的视口发生变化时,更新小地图滑动窗口大小、位置和遮罩// 3. 画布监听事件:画布中的对象被添加、删除、修改时,更新小地图背景图// n. 页面销毁时:注销事件监听器,避免内存泄漏}, [canvas]);
};
这里一共分为5
个部分。为了减少篇幅中的冗余,我先把各个部分的代码都删去了。接下来我逐一进行介绍,聪明的你,可以在看完一个部分之后,把那里的代码填回以上注释下的位置,完整的代码就组装好了!
5.0 初始化小地图
const miniMap = initMiniMap({canvas, canvasMiniMapRatio});
小地图的初始化依赖于canvas
实例,我们来看看initMiniMap
方法的细节:
/*** 初始化小地图* @param canvas 画布实例* @param canvasMiniMapRatio 画布和小地图尺寸的比例(默认为10)* @returns {*} 小地图实例*/const initMiniMap = ({canvas, canvasMiniMapRatio}) => {if (!canvas) {return;}// 1. 实例化miniMapconst miniMap = new fabric.Canvas('mini-map', {selection: false});// 2. 设置miniMap的长和宽为画布的 1 / canvasMiniMapRatioconst rootElement = document.getElementsByClassName("scalable")[0];miniMap.setDimensions({width: rootElement.offsetWidth / canvasMiniMapRatio,height: rootElement.offsetHeight / canvasMiniMapRatio});// 3. 设置miniMap的背景图、滑动窗口和遮罩updateMiniMapBackground({canvas, miniMap, canvasMiniMapRatio});updateMiniMapSlideWindow({canvas, miniMap, canvasMiniMapRatio});updateMiniMapMask({miniMap});miniMap.renderAll();return miniMap;};
initMiniMap
方法的逻辑很简洁,分为以下3步:
- 1) 实例化miniMap: 通过
new fabric.Canvas
实例化小地图,与画布canvas
异曲同工。 - 2) 设置小地图大小: 上文中有介绍过,这里的
canvasMiniMapRatio=10
,即小地图的宽高是画布的1 / 10
。 - 3) 设置miniMap的背景图、滑动窗口和遮罩: 调用上文中介绍过的3个核心方法依次设置。
5.1 小地图监听事件,滑动窗口被拖动时,更新画布视口和小地图的遮罩
const handleMiniMapSlideWindowMovingEvent = () => {updateCanvasViewport({canvas, miniMap, canvasMiniMapRatio});updateMiniMapMask({miniMap});};miniMap.on('object:moving', handleMiniMapSlideWindowMovingEvent);
当用户用鼠标拖动小地图中的滑动窗口时,应该更新画布的视口和小地图的遮罩。这里监听了miniMap
的object:moving
事件,即小地图中有对象发生位移时(只有滑动窗口,遮罩被设置了selectable: false
,无法被鼠标拖动),触发上述动作。
5.2 画布监听事件:画布的视口发生变化时,更新小地图滑动窗口大小、位置和遮罩
const handleViewportTransformUpdatedEvent = () => {updateMiniMapSlideWindow({canvas, miniMap, canvasMiniMapRatio});updateMiniMapMask({miniMap});};canvas.on('viewportTransform:updated', handleViewportTransformUpdatedEvent);
鉴于画布丰富的操作性,有很多可能会导致画布的viewportTransform
值发生变化的情形,为了便于说明,这里再次介绍一下viewportTransform
的值是怎么样的:
它是一个固定长度为6
的列表,每一位分别代表[scaleX, skewX, skewY, scaleY, translateX, translateY]
viewportTransform[0], scaleX
和viewportTransform[3], scaleY
: 表示画布在「水平」和「垂直」两个方向上的缩放比例,值为0.5
表示画布在一个方向上被缩小到了原来的50%.viewportTransform[1], skewX
和viewportTransform[2], skewY
: 表示画布在「水平」和「垂直」两个方向上的倾斜程度,一般不会修改这两个值。viewportTransform[4], translateX
和viewportTransform[5], translateY
: 表示画布在「水平」和「垂直」两个方向上视口的平移值,单位是像素px
。
这里监听了canvas
的viewportTransform:updated
,只要viewportTransform
值被更新(包括窗口被缩放、画布被缩放等多种情况),就会立即更新滑动窗口,并同时更新遮罩。
需要注意的是: 这里的viewportTransform:updated
并不是fabric.Canvas
本身具有的,而是一个我们自定义的监听事件,具体的实现会放在下一篇博文中进行介绍。
最后,让我们来看看监听了viewportTransform:updated
之后的效果:
5.3 画布监听事件:画布中的对象被添加、删除、修改时,更新小地图背景图
const events = ["object:added","object:removed","object:modified"];const handleCanvasObjectEvent = () => {updateMiniMapBackground({canvas, miniMap, canvasMiniMapRatio});};events.forEach(event => {canvas.on(event, handleCanvasObjectEvent);});
这里很好理解,当画布上有任何的“风吹草动”,小地图都要跟随着发生变化,因为小地图存在的意义就是“画布的缩小版”,所以这里监听了canvas
的object:added
, object:removed
和object:modified
3个事件,当画布中「添加」、「移除」和「修改对象属性」时,都更新小地图的背景图。
来看看效果:
5.n 页面销毁时:注销事件监听器,避免内存泄漏
return () => {events.forEach(event => {canvas.off(event, handleCanvasObjectEvent);});canvas.off('viewportTransform:updated', handleViewportTransformUpdatedEvent);miniMap.off('object:moving', handleMiniMapSlideWindowMovingEvent);}
出于代码洁癖和严谨的态度,我们在小地图被页面销毁时,主动注销掉1/2/3
步注册的「所有监听器」,避免以后还要回过头来查内存泄漏的问题。
6. 小地图受控组件
组合了上述所有的代码,来看看我们实现的受控组件<MiniMap />
:
const MiniMap = (props) => {const {canvas} = props;const canvasMiniMapRatio = 10;useMiniMap(canvas, canvasMiniMapRatio);return (<div className="mini-map-container"><canvas id="mini-map"/></div>)};
在父级组件中,只需要传入画布canvas
的实例,就可以使用我们的小地图miniMap
了。
三、Show u the code
按照惯例,本节的完整代码我也托管在了CodeSandbox中,点击前往,查看完整代码
后记
实现和优化小地图的代码,用掉了我两周多的空余时间。再打磨出来这上下两篇博文,近一个月过去了。不过一切都是值得的。
经过这两篇博文,我们实现了单方面操作小地图,使画布的视口发生变化,并部分了解了如何监听画布的事件,动态更新小地图的样式。
后面两篇博文,主要介绍用鼠标滚轮缩放画布、按住空格键,用鼠标拖动画布等功能,敬请期待!
如有需要,你可以: