前端canvas项目实战——在线图文编辑器(十一):小地图MiniMap(下)

embedded/2024/9/24 13:31:42/

目录

  • 前言
  • 二、 实现步骤
    • 4. 根据滑动窗口的位置和大小改变画布的视口
    • 5. 组装起来:实现小地图的整个生命周期
      • 5.0 初始化小地图
      • 5.1 小地图监听事件,滑动窗口被拖动时,更新画布视口和小地图的遮罩
      • 5.2 画布监听事件:画布的视口发生变化时,更新小地图滑动窗口大小、位置和遮罩
      • 5.3 画布监听事件:画布中的对象被添加、删除、修改时,更新小地图背景图
      • 5.n 页面销毁时:注销事件监听器,避免内存泄漏
    • 6. 小地图受控组件
  • 三、Show u the code
  • 后记

前言

上一篇博文中,我们列举了小地图MiniMap,其4个核心方法中的3个。由于篇幅所限,在这篇博文中继续介绍剩余的内容。

这篇博文是《前端canvas项目实战——在线图文编辑器》付费专栏系列博文的第十一篇——小地图MiniMap(下),主要的内容有:

  1. 实现用鼠标拖动滑动窗口,画布中的视口随之改变。
  2. 将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);

当用户用鼠标拖动小地图中的滑动窗口时,应该更新画布的视口和小地图的遮罩。这里监听了miniMapobject: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], scaleXviewportTransform[3], scaleY: 表示画布在「水平」和「垂直」两个方向上的缩放比例,值为0.5表示画布在一个方向上被缩小到了原来的50%.
  • viewportTransform[1], skewXviewportTransform[2], skewY: 表示画布在「水平」和「垂直」两个方向上的倾斜程度,一般不会修改这两个值。
  • viewportTransform[4], translateXviewportTransform[5], translateY: 表示画布在「水平」和「垂直」两个方向上视口的平移值,单位是像素px

这里监听了canvasviewportTransform: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);});

这里很好理解,当画布上有任何的“风吹草动”,小地图都要跟随着发生变化,因为小地图存在的意义就是“画布的缩小版”,所以这里监听了canvasobject:added, object:removedobject:modified3个事件,当画布中「添加」、「移除」和「修改对象属性」时,都更新小地图的背景图

来看看效果:

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中,点击前往,查看完整代码


后记

实现和优化小地图的代码,用掉了我两周多的空余时间。再打磨出来这上下两篇博文,近一个月过去了。不过一切都是值得的。

经过这两篇博文,我们实现了单方面操作小地图,使画布的视口发生变化,并部分了解了如何监听画布的事件,动态更新小地图的样式。

后面两篇博文,主要介绍用鼠标滚轮缩放画布、按住空格键,用鼠标拖动画布等功能,敬请期待!

如有需要,你可以:


http://www.ppmy.cn/embedded/44298.html

相关文章

【调试笔记-20240522-Windows-WSL 修改已安装发行版名称】

调试笔记-系列文章目录 调试笔记-20240522-Windows-WSL 修改已安装发行版名称 文章目录 调试笔记-系列文章目录调试笔记-20240522-Windows-WSL 修改已安装发行版名称 前言一、调试环境操作系统&#xff1a;Windows 10 专业版调试环境调试目标 二、调试步骤方法一&#xff1a;修…

Mysql教程(0):学习框架

1、Mysql简介 MySQL 是一个开放源代码的、免费的关系型数据库管理系统。在 Web 开发领域&#xff0c;MySQL 是最流行、使用最广泛的关系数据库。MySql 分为社区版和商业版&#xff0c;社区版完全免费&#xff0c;并且几乎能满足全部的使用场景。由于 MySQL 是开源的&#xff0…

HaloDB 的 Oracle 兼容模式

↑ 关注“少安事务所”公众号&#xff0c;欢迎⭐收藏&#xff0c;不错过精彩内容~ 前倾回顾 前面介绍了“光环”数据库的基本情况和安装办法。 哈喽&#xff0c;国产数据库&#xff01;Halo DB! 三步走&#xff0c;Halo DB 安装指引 ★ HaloDB是基于原生PG打造的新一代高性能安…

应急响应流程

应急响应的流程&#xff08;Linux 和 Windows&#xff09; 1、收集信息&#xff1a;搜集客户信息和中毒信息&#xff0c;备份 2、判断类型&#xff1a;判断是否是安全事件、是何种安全事件&#xff08;勒索病毒、挖矿、断网、ddos等&#xff09; 3、深入分析&#xff1a;日志分…

【亲测,安卓版】快速将网页网址打包成安卓app,一键将网页打包成app,免安装纯绿色版本,快速将网页网址打包成安卓apk

背景&#xff1a;部分客户需求将自己网站打包成app&#xff0c;供用户在浏览器安装使用、 网页网址快速生成app 准备材料操作流程第一步&#xff1a;打开HBuilder X新建项目第二步创建Wap2App项目第三步修改App图标第四步发布app第五步查看apk 准备材料 1.需要打包的网页 2.ap…

macbook中foxmail的通讯录迁移

之前windows中用习惯了foxmail,换成macbook后,还是沿用foxmail。使用一段时间后,确实受不了foxmail的不便:1、版本比较低1.5.6,很多windows新版的功能都没有;2、动不动莫名其妙崩溃,写了半天的邮件,点击发送就直接崩了,又得重新写。 忍耐了几个月后,下定决心换成网易…

Kotlin学习笔记 泛型

在 Kotlin 中&#xff0c;T 通常用作类型参数的占位符&#xff0c;它在实例化或传递参数时会被替换成具体的类型。 Kotlin 支持泛型&#xff0c;这意味着您可以编写可以与多种数据类型一起工作的代码&#xff0c;而不必为每种数据类型编写单独的代码。 ### 泛型类和函数 在 …

深入解析Web前端三大主流框架:Angular、React和Vue

Web前端三大主流框架分别是Angular、React和Vue。下面我将为您详细介绍这三大框架的特点和使用指南。 Angular 核心概念: 组件(Components): 组件是Angular应用的构建块,每个组件由一个带有装饰器的类、一个HTML模板、一个CSS样式表组成。组件通过输入(@Input)和输出(…