目录
- 前言
- 一、 效果展示
- 二、 实现步骤
- 1. 调整布局,最大化利用屏幕空间
- 2. 添加逻辑画布
- 3. 添加遮罩
- 4. 居中显示逻辑画布
- 5. 一个容易被忽视的bug点
- 三、Show u the code
- 后记
前言
上一篇博文中,我们实现了一组通用的功能按钮:复制、删除、锁定和层叠顺序。
这篇博文是《前端canvas项目实战——在线图文编辑器》付费专栏系列博文的第九篇——逻辑画布,主要的内容有:
- 调整页面布局,将画布区域扩展至整个屏幕的剩余空间中。
- 区分「物理画布」和「逻辑画布」,为实现「缩放」、「辅助线」等功能打基础。
如有需要,你可以:
- 点击这里,返回第一篇《前端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,
...
});
要居中显示逻辑画布,需要引入canvas
的viewport
「视口」概念。
视口: 可以理解为可视窗口。当逻辑画布很小时,我们可以看到它的全貌。反之,当它很大,或者被放大超出了窗口的大小,我们就只能看到它的局部。这个我们能看到的区域就称为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项目实战——在线图文编辑器(八):复制、删除、锁定、层叠顺序》