前端canvas项目实战——在线图文编辑器(九):逻辑画布

发布于:2024-04-26 ⋅ 阅读:(26) ⋅ 点赞:(0)

前言

上一篇博文中,我们实现了一组通用的功能按钮:复制、删除、锁定和层叠顺序

这篇博文是《前端canvas项目实战——在线图文编辑器》付费专栏系列博文的第九篇——逻辑画布,主要的内容有:

  1. 调整页面布局,将画布区域扩展至整个屏幕的剩余空间中。
  2. 区分「物理画布」和「逻辑画布」,为实现「缩放」、「辅助线」等功能打基础。

如有需要,你可以:

  • 点击这里,返回第一篇《前端canvas项目实战——在线图文编辑器(一)——左侧工具栏》
  • 点击这里,返回上一篇《前端canvas项目实战——在线图文编辑器(八):复制、删除、锁定、层叠顺序》

一、 效果展示

  • 动手体验
    CodeSandbox会自动对代码进行编译,并提供地址以供体验代码效果
    由于CSDN的链接跳转有问题,会导致页面无法工作,请复制以下链接在浏览器打开:
    https://5sd7gz.csb.app/

  • 效果演示


二、 实现步骤

1. 调整布局,最大化利用屏幕空间

在之前的博文中,我们的实现包含「左侧工具栏」、「画布」和「右侧属性栏」3个部分。他们是依次从左到右进行排的,因此我们可以看到屏幕的右侧和下方有空余的区域,既浪费,又不美观

要处理这个问题,我们需要修改canvas-page/index.js文件中的html部分,来充分利用屏幕空间:

	.content-container {
	  width: 100%;
	  height: 100vh;
	  display: flex;
	  flex-direction: row;
	  justify-content: space-between;
	}
	
	.left-side-tools-container {
	  width: 80px;
	  height: 100%;
	  ...
	}

	.right-side-props-container {
	  width: 16.25rem;
	  height: 100%;
	  ...
	}
	
	.scalable {
	  ...
	  position: absolute;
	  top: 0;
	  bottom: 0;
	  left: 80px;
	  right: 16.25rem;
	}
	
    <div className="content-container">
        <LeftSideTools canvas={canvas}/>
        <div className="scalable">
            <canvas id="canvas"/>
        </div>
        <RightSideProps w={canvasWidth} h={canvasHeight} u={canvasSizeUnit}/>
    </div>

可以看到,html标签的排布很简洁,下面对CSS中的样式进行说明:

  • .canvas-container: 作为父级容器
    • 首先通过width: 100%;height: 100vh;占满屏幕100%的宽度和高度,为3个子标签提供足够的空间。
    • 然后通过display: flex;flex-direction: row;justify-content: space-between;设置子标签在水平方向流式布局,并按等间距排列。
  • .left-side-tools-container: 占据80px的宽度并占满父标签100%的高度。
  • .right-side-props-container: 占据16.25rem的宽度并占满父标签100%的高度。
  • .scalable: 作为canvas的父级标签,采用绝对定位:
    • 上下两端都和父标签.canvas-container对齐。
    • 左侧从80px开始,即开始于工具栏右侧。
    • 右侧从16.25rem开始,即结束于属性栏左侧。

经过这样的调整,canvas就可以填充除工具栏和属性栏外的所有屏幕空间。

2. 添加逻辑画布

通常情况下,我们并不需要自己的画布填充满所有的空余区域,但又想让它居中显示在屏幕中央。这里,我们引入「逻辑画布」的概念来实现这样的需要,以下对几个概念做简要的说明:

  • 物理画布:canvas对象所占有的全部区域。
  • 逻辑画布: 一个相对于「物理画布canvas的概念。在canvas中,将我们关注的部分区域作为逻辑画布,在逻辑画布上,我们可以添加各种对象。
  • 背景区域: 可以理解为背景区域 = 物理画布 - 逻辑画布。即在画布中,但不在逻辑画布中的区域称之为「背景区域」。在背景区域中,只显示辅助线等少数的对象,其他对象都不会被显示出来。

可以通过下图来加深理解:

代码实现:

    useEffect(() => {
        const parentElement = document.getElementsByClassName("scalable")[0];
        let canvas = new fabric.Canvas("canvas", {
            width: parentElement.offsetWidth,
            height: parentElement.offsetHeight
        });
        ...
        addLogicCanvas(canvas, logicCanvasWidth, logicCanvasHeight);
        ...
    }, []);

	const addLogicCanvas = (canvas, width, height) => {
	    const logicCanvas = new fabric.Rect({
	        left: 0,
	        top: 0,
	        width,
	        height,
	        fill: "white",
	        stroke: "lightgray",
	        selectable: false,
	        evented: false,
	    });
	    canvas.add(logicCanvas);
	
	    // 逻辑画布永远置底
	    canvas.sendToBack(logicCanvas);
	    canvas.renderAll();
	    ...
	};

代码逻辑比较简单,以下做简要说明:

  • useEffect: 画布页面初始化的阶段,根据父级标签scalable的宽度和高度实例化「物理画布canvas对象,并向其中添加「逻辑画布logicCanvas
  • addLogicCanvas: 创建并添加逻辑画布,有以下要点:
    • 逻辑画布」实际上是一个填充色为白色的fabric.Rect矩形对象。
    • 它不可以通过鼠标点击被用户选择,不参与任何监听事件
    • 通过canvas.sendToBack方法使逻辑画布用于置于所有对象的最底层,否则会遮盖住其他对象。

3. 添加遮罩

有了逻辑画布,我们可以在其上添加和拖动各种各样的对象。但有一种情况的表现还不尽如人意,见下面的动图:

当我们把一个对象拖出逻辑画布时,预期它应该被遮盖或隐藏,但实际的表现是:它仍然显示在那里。

为了实现这个小需求点,我们可以为画布添加clipPath,即「遮罩范围」:

	const updateClipPath = (canvas, logicCanvas) => {
	    const {left, top, width, height} = logicCanvas;
	    canvas.clipPath = new fabric.Rect({
	        left,
	        top,
	        width,
	        height,
	        absolutePositioned: true,
	        selectable: false,
	        evented: false,
	    });
	};

和逻辑画布类似,遮罩是一个和逻辑画布相同位置、相同大小fabric.Rect矩形区域,同样不可以被选中,不参与任何监听事件。

设置了clipPath之后,我们来看看效果:

可以看到,对象被拖出逻辑画布的区域被隐藏了,只有选择框的控制线和控制点仍可以显示。

4. 居中显示逻辑画布

前面几个小节中,为了美观和便于说明,我直接使用了居中后的页面进行截图。实际上,我们在初始化时设置了逻辑画布的坐标为(0, 0):

    const logicCanvas = new fabric.Rect({
        left: 0,
        top: 0,
        ...
    });

要居中显示逻辑画布,需要引入canvasviewport视口」概念。

视口: 可以理解为可视窗口。当逻辑画布很小时,我们可以看到它的全貌。反之,当它很大,或者被放大超出了窗口的大小,我们就只能看到它的局部。这个我们能看到的区域就称为viewport视口。

先看下面一张图:

红色方框的区域就是上述的「视口」, 起初,视口的中心在「红色十字」的位置,我们看到的逻辑画布就在屏幕的左上方。想要看到逻辑画布处于视口正中间,就需要把视口向左上角移动一定的距离,使得视口中心和「蓝色十字」重合。

那么问题就简化为,如何计算出水平和竖直两个方向上的位移量。由图中可以很方便的计算出:

  • 水平方向的偏移量 = (canvas.width - logicCanvas.width) / 2;
  • 竖直方向的偏移量 = (canvas.height - logicCanvas.height) / 2;

因此有以下代码:

    let panX = -(canvas.width - logicCanvas.width) / 2;
    let panY = -(canvas.height - logicCanvas.height) / 2;

    ...
    canvas.absolutePan(new fabric.Point(panX, panY));
    ...

canvas.absolutePan方法的作用即对当前画布的视口做绝对的平移。 如此,我们的逻辑画布就可以居中显示在屏幕中央了。

5. 一个容易被忽视的bug点

由于「逻辑画布」也是canvas中的一个fabric.Rect对象,且在Z轴上必须永远置底,所以会造成原有的「移至底层」功能出现bug,具体的表现就是:将一个对象移至底层之后,由于被逻辑画布完全遮挡,这个对象就再也看不到,也无法选中了

具体的表现如下图:

这个问题修复的逻辑很简单:把一个对象移至底层之后,再向上移动一层,使它在Z轴上必然比逻辑画布高即可:

	if (selectedItem.key === "toBottom") {
		canvas.sendToBack(object);
		// 逻辑画布应该永远置底,所有对象都应该高于逻辑画布
		canvas.bringForward(object);
	}

问题修复后的效果,不再截图徒增篇幅。


三、Show u the code

按照惯例,本节的完整代码我也托管在了CodeSandbox中,点击前往,查看完整代码


后记

这篇博文中对画布进行调整的内容不算太多,但十分重要,会作为后续多篇博文的基础。后续的博文中,我们会依次实现通过工具缩放画布、通过鼠标滚轮缩放画布、鼠标拖动移动视口等功能。

如有需要,你可以:

  • 点击这里,返回第一篇《前端canvas项目实战——在线图文编辑器(一)——左侧工具栏》
  • 点击这里,返回上一篇《前端canvas项目实战——在线图文编辑器(八):复制、删除、锁定、层叠顺序》