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

前言

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

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

  1. 实现用鼠标拖动滑动窗口,画布中的视口随之改变。
  2. 将4部分的核心代码组合起来,实现小地图的整个「生命周期」。

如有需要,你可以:

  • 点击这里,阅读序文《前端canvas项目实战——在线图文编辑器:序》
  • 点击这里,返回上一篇《前端canvas项目实战——在线图文编辑器(十):小地图MiniMap(上)》

二、 实现步骤

这里接着上文介绍小地图的实现步骤中,剩余的代码逻辑。如果想要回顾前面的代码,可以点击前往

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. 实例化miniMap
	    const miniMap = new fabric.Canvas('mini-map', {selection: false});
	
	    // 2. 设置miniMap的长和宽为画布的 1 / canvasMiniMapRatio
	    const 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中,点击前往,查看完整代码


后记

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

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

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

如有需要,你可以:

  • 点击这里,阅读序文《前端canvas项目实战——在线图文编辑器:序》
  • 点击这里,返回上一篇《前端canvas项目实战——在线图文编辑器(十):小地图MiniMap(上)》
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IMplementist

你的鼓励,是我继续写文章的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值