使用fabric从零开始打造互动白板(一)
最近由于公司业务需要,原本使用第三方的互动白板功能,准备自行实现。一方面为了节省开支,另一方面自行实现定制化也更强一些,能够和现有业务更好的结合,用以满足第三方互动白板无法实现的一些功能。
一、功能整理
既然需求明确了,于是就开始着手整理白板所需的功能。由于我们的直播课都是大班课,只需要讲师在白板上操作,用户端只用来展示,也就不需要太多复杂的功能,结合几个第三方互动白板,归纳整理出了如下几个需要实现的功能点:
- 自由画笔
- 文字书写
- 橡皮擦
- 画三角、圆形、矩形
- 画直线和箭头
- 清空画布
- 撤销重做
- 画布缩放
- 插入PPT图片及切换控制
二、技术选择
观察了现有的互动白板,都是在Canvas
进行操作,为了节约开发时间于是找到了fabric
这个库,这个库本身就实现了不少功能,可以大大的减少开发时间。
结合我熟悉的技术栈,最终选定了使用Vite
+Vue3
+TypeScript
进行demo
版本的构建。
相关代码放在github
上,链接地址:使用vite+typescript+fabric创建的互动白板项目
三、页面结构
参考其他白板的布局进行了页面结构的搭建。白板使用一个容器进行包裹,左侧是工具区域,包括画笔、橡皮擦、画线、清空画布等各种绘制工具;左下角是撤销、重做和画布缩放控制区域;右上角是插入PPT
文件控制区域;右下角是PPT
控制区域。最后提供了一个容器进行的白板预览。
效果图如下:
页面结构代码如下:
<template>
<div>
<div class="canvas-wrap">
<div class="tool-box-out">
<ToolBox></ToolBox>
</div>
<div class="redo-undo-box">
<RedoUndo></RedoUndo>
</div>
<div class="zoom-controller-box">
<ZoomController></ZoomController>
</div>
<div class="room-controller-box" v-show="!isPreviewShow">
<div class="page-controller-mid-box">
<div className="page-preview-cell" @click="insertPPT">
<img style="width: 28px" :src="folder" alt="文件"/>
</div>
</div>
</div>
<div class="page-controller-box" v-show="isShowPPTControl">
<div className="page-controller-mid-box">
<PageController></PageController>
<div className="page-preview-cell" @click="handlePreviewState(true)">
<img :src="pages" alt="PPT预览"/>
</div>
</div>
</div>
<div class="preview-controller-box" v-show="isShowPPTControl&&isPreviewShow">
<PreviewController @handlePreviewState="handlePreviewState"></PreviewController>
</div>
<canvas id="canvas" width="800" height="450"></canvas>
</div>
<div class="canvas-wrap">
<canvas id="canvas2" width="800" height="450"></canvas>
</div>
</div>
</template>
四、初始化白板
为了方便后续使用,这里对fabric
进行封装,后续拓展也能更加灵活。相关代码如下:
import { fabric } from "fabric";
class FabricCanvas {
constructor(canvasId: string) {
// 初始化画布,默认可绘制
this.canvas = new fabric.Canvas(canvasId, {
isDrawingMode: true,
selection: false,
includeDefaultValues: false, // 转换成json对象,不包含默认值
});
}
}
使用示例:
const canvas = new FabricCanvas('canvas');
五、工具栏相关功能实现
页面框架搭建完成之后,就开始各种功能的开发。这里将fabric
封装成一个类,所有需要实现的方法在类中实现,方便后续灵活调用。
选择
选择功能实现比较简单,只需要关闭绘制模式,然后将画布设置可选中。相关代码如下:
this.canvas.isDrawingMode = false;
this.canvas.selection = true;
// 设置鼠标光标
this.canvas.defaultCursor = 'auto';
自由画笔
fabric
提供了各种丰富的笔刷,实现自由绘制这里调用了PencilBrush
类,并将isDrawingMode
设置为true
即可。相关代码如下:
public drawFreeDraw() {
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.canvas.freeDrawingBrush.color = '#ff0000'
this.canvas.freeDrawingBrush.width = 5
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}
使用示例:
const canvas = new FabricCanvas('canvas');
// 自由画笔
canvas.drawFreeDraw();
文字书写
文字输入使用fabric
提供的IText
方法,实现文字编辑和修改,并且让输入框自动获取焦点,方便输入。相关代码如下:
public drawText(text: string, options?: ITextOptions): void {
const textObj = new fabric.IText(text, {
editingBorderColor: '#ff0000',
padding: 5,
...options
});
this.canvas.add(textObj);
this.canvas.defaultCursor = 'text'
this.currentShape = textObj;
// 文本打开编辑模式
textObj.enterEditing();
// 文本编辑框获取焦点
textObj.hiddenTextarea.focus()
this.setActiveObject(textObj);
}
使用示例:
const canvas = new FabricCanvas('canvas');
// 绘制文本
canvas.drawText('Hello World!', { left: 50, top: 250, fontSize: 24, fill: 'red' })
橡皮擦
fabric
内置了EraserBrush
用来实现橡皮擦功能,不过默认构建排除了该功能,避免库文件过大。如需使用,需要进入node_modules/fabric
目录执行下面的命令重新构建:
node build.js modules=ALL exclude=gestures,accessors requirejs minifier=uglifyjs
构建完成之后就可以使用EraserBrush
来实现橡皮擦功能了,相关代码如下:
public eraser(options?: any): void {
this.canvas.freeDrawingBrush = new fabric.EraserBrush(this.canvas, options);
this.canvas.freeDrawingBrush.width = 10
this.canvas.freeDrawingCursor = 'default'
this.canvas.isDrawingMode = true;
}
使用示例:
const canvas = new FabricCanvas('canvas');
// 橡皮擦
canvas.erase({ width: 10 });
画三角、圆形、矩形
画三角形、圆形、矩形方法相似,直接调用fabric
封装的对应方法即可。
这里以绘制矩形为例,相关代码实现如下:
public drawRect(options: IRectOptions): void {
const rect = new fabric.Rect({ ...this.options, ...options });
this.canvas.add(rect);
this.currentShape = rect;
this.canvas.defaultCursor = 'crosshair'
}
使用示例:
const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawRect({ left: 50, top: 150, width: 100, height: 50, fill: 'green', stroke: 'black' });
画直线和箭头
画直线功能fabric
直接内置了,调用对应的方法即可,这里重点讲画箭头。画箭头的本质是在直线的一端加上一个三角形,根据起始点使用三角函数计算好三角形的方向,这样就组合成一个箭头了。这里搬运网友的画箭头方法,直接对画直线功能进行拓展,并封装成fabric
中的功能模块,方便后续调用。相关代码如下:
import { fabric } from 'fabric';
fabric.Arrow = fabric.util.createClass(fabric.Line, {
type: 'arrow',
superType: 'drawing',
initialize(points: number[], options: any) {
if (!points) {
const { x1, x2, y1, y2 } = options;
points = [x1, y1, x2, y2];
}
options = options || {};
this.callSuper('initialize', points, options);
},
_render(ctx: any) {
this.callSuper('_render', ctx);
ctx.save();
const xDiff = this.x2 - this.x1;
const yDiff = this.y2 - this.y1;
const angle = Math.atan2(yDiff, xDiff);
ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);
ctx.rotate(angle);
ctx.beginPath();
// Move 5px in front of line to start the arrow so it does not have the square line end showing in front (0,0)
ctx.moveTo(5, 0);
ctx.lineTo(-5, 5);
ctx.lineTo(-5, -5);
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
},
});
fabric.Arrow.fromObject = (options: any, callback: any) => {
const { x1, x2, y1, y2 } = options;
return callback(new fabric.Arrow([x1, y1, x2, y2], options));
};
export default fabric.Arrow;
封装好的代码,直接导入调用即可。相关代码如下:
import Arrow from "./objects/Arrow";
// 绘制箭头
public drawArrow(x1: number, y1: number, x2: number, y2: number, options?: ILineOptions) {
const arrow = new Arrow([x1, y1, x2, y2], { ...this.options, ...options });
this.canvas.add(arrow);
this.currentShape = arrow;
this.canvas.defaultCursor = 'crosshair'
}
使用示例:
const canvas = new FabricCanvas('canvas');
// 绘制矩形
canvas.drawArrow(10, 50, 100, 50, { stroke: 'blue', strokeWidth: 2 })
通过鼠标绘制图形
实际使用白板过程中,上面这些图形、线条的绘制都是通过鼠标拖动进行,这样更加灵活一些。
通过鼠标绘制图形,需要对鼠标的mouse:down
、mouse:move
、mouse:up
事件进行监听,相关代码如下:
// 监听鼠标事件
this.canvas.on("mouse:down", this.onMouseDown.bind(this));
this.canvas.on("mouse:move", this.onMouseMove.bind(this));
this.canvas.on("mouse:up", this.onMouseUp.bind(this));
这里以绘制矩形为例,实现通过通过鼠标绘制一个矩形框。
- 当鼠标按下时,在鼠标按下的地方绘制一个宽高为
0
的矩形。相关代码如下:
// 是否处于绘制状态
private isDrawing = false;
// 鼠标起点坐标x
private startX = 0;
// 鼠标起点多表y
private startY = 0;
// 鼠标按下事件处理函数
private onMouseDown(event: IEvent) {
// 如果当前有活动的元素则不进行后续绘制
const activeObject = this.canvas.getActiveObject();
if (!event.pointer || activeObject) return;
// 切换成绘制状态
this.isDrawing = true;
// 记录当前坐标点
const { x, y } = event.pointer;
this.startX = x;
this.startY = y;
// 在当前坐标绘制一个矩形
this.drawRect({
left: x,
top: y,
width: 0,
height: 0,
});
}
- 在鼠标移动的过程中,动态的修改矩形的宽高,并实时渲染。相关代码如下:
// 鼠标移动事件处理函数
private onMouseMove(event: IEvent) {
if (!this.isDrawing || !event.pointer || !this.currentShape) return;
// 计算宽高
const { x, y } = event.pointer;
const width = x - this.startX;
const height = y - this.startY;
// 设置宽高
this.currentShape.set({
width,
height,
});
// 更新画布
this.canvas.renderAll();
}
- 当鼠标抬起后,改变绘制状态。相关代码如下:
// 鼠标抬起事件处理函数
private onMouseUp() {
this.isDrawing = false;
this.currentShape = null;
}
如果想要更加灵活的在各种图形和线条中自由的进行切换,并通过鼠标绘制,在提供的demo
中也进行了对应的封装。
相关代码请在github
中进行查看,对fabric的各种功能封装。
清空画布
清空画布直接调用画布的清除方法即可,相关代码如下:
// 清空画布
public clearCanvas() {
this.canvas.clear();
}
不过该方法会清除画布上包含背景的所有内容,如果不想画布背景也被清除,可以遍历画布上的所有对象进行移除。相关代码如下:
// 移除所有对象
public removeAllObject() {
this.canvas.getObjects().forEach((obj) => {
this.canvas.remove(obj);
});
}
六、工具栏布局
将工具栏封装成ToolBox
组件,并在组件中实现各种工具的切换。
组件布局代码如下:
<template>
<div class="tool-mid-box-left">
<div class="tool-box-cell-box-left" v-for="item in tools" :key="item.shapeType">
<div class="tool-box-cell"
@click="clickAppliance(item.shapeType)">
<img :src="item.shapeType === currentShapType ? item.iconActive : item.icon" :alt="item.name"/>
</div>
</div>
<div class="tool-box-cell-box-left">
<div class="tool-box-cell"
@click="clickClear">
<img :src="clear" alt="清屏"/>
</div>
</div>
</div>
</template>
相关功能事件实现的代码如下:
const currentShapType = ref<string>("pencil")
// 设置当前工具
function clickAppliance(type: DrawingTool) {
currentShapType.value = type;
canvas?.value.setDrawingTool(type)
}
// 清屏事件处理
function clickClear() {
canvas?.value.clearCanvas()
}
设置当前绘制工具
// 设置绘图工具
public setDrawingTool(tool: DrawingTool) {
if(this.drawingTool === tool) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = false;
this.drawingTool = tool;
if (tool === "pencil") {
this.drawFreeDraw();
} else if (tool === "eraser") {
this.eraser();
} else if (tool === "select") {
this.canvas.selection = true;
this.canvas.defaultCursor = 'auto'
}
}
其他功能说明
为避免文章太长,撤销重做、画布缩放、插入PPT图片及切换控制等功能的实现在后续文章中介绍。
如果等不及,可以直接在github
上查看相关代码实现:使用vite+typescript+fabric创建的互动白板项目
六、参考资料
- Fabric.js Javascript Canvas Library (fabricjs.com)
- 使用fabric.js 快速开发一个图片编辑器
- netless-io/whiteboard-demo (github.com)
来源:juejin.cn/post/7221348552513077305