在这之前,我写过一个vue版本的画板程序。最近因为 minicode的上线,就把画板用 react 重写了,其实思路什么的都是一样的,只是使用的前端框架不一样罢了。
github源码地址: https://github.com/pengfeiw/react-paint。
程序界面如图,上方是工具栏,下面是画布绘制区域。
主要功能使用html canvas实现,可以在 MDN canvas教程上学习 canvas 的二维图形绘制。
下面我说下部分的主要功能实现,你们可以在github上找我的源码进行参考。
主要是监听画板的鼠标事件实现的,这里我运用了面向对象的思想,写了一个Tool类,Pen、Shape、Eraser都是通过继承实现的。
class Tool {
public onMouseDown(event: MouseEvent): void {
//
}
public onMouseMove(event: MouseEvent): void {
//
}
public onMouseUp(event: MouseEvent): void {
//
}
}
class Pen extends Tool {
// 覆盖基类的三个鼠标事件方法
public onMouseDown(event: MouseEvent): void {
//
}
public onMouseMove(event: MouseEvent): void {
//
}
public onMouseUp(event: MouseEvent): void {
//
}
}
class Shape extends Tool {
...
}
// 由于橡皮擦和笔的实现基本一样,仅颜色和线宽不一样,所以我直接将Eraser继承Pen
class Eraser extends Pen {
...
}
从mousedown至mouseup这一个完整的过程,表示一段线段或者一个形状的绘制。具体实现可以看我的源码。
填充功能使用了图形学的flood fill算法,具体思路可以参考我的这篇文章高效率的种子填充算法。这个功能我花的时间是最多的,在算法上我花了很多时间去查阅资料和实践。
这个主要利用了双栈的思想,将每一步canvas的剪影(ImageData)存储在栈中。每次在canvas上绘制一个形状或者线段,又或者是擦除一条线段,都将此时的canvas图片(ImageData)存储在imageData1栈顶。back时将imageData1的栈顶元素弹栈,并将该元素push到imageData2中。forward的时候,如果imageData2非空,将imageData2执行弹栈操作,将该元素push到imageData1中。每次操作后,都将imageData1的栈顶ImageData加载到canvas上,即实现了撤销和回退功能。
class Snapshot {
private imageData1: ImageData[] = [];
private imageData2: ImageData[] = [];
public add(imageData: ImageData) {
this.imageData1.push(imageData);
}
public back() {
if (this.imageData1.length > 1) {
const imageData = this.imageData1.pop() as ImageData;
this.imageData2.push(imageData);
}
return this.imageData1.length > 0 ? this.imageData1[this.imageData1.length - 1] : null;
}
public forward() {
if (this.imageData2.length > 0) {
const imageData = this.imageData2.pop() as ImageData;
this.imageData1.push(imageData);
}
return this.imageData1.length > 0 ? this.imageData1[this.imageData1.length - 1] : null;
}
}
其他功能相对比较简单,就是更改一些线的样式,这里就不再详述了,更多细节请参考我的github。
如果你对该程序有疑问,可以在我的个人网站文章下方留言,也可以留下您的联系方式☺。
(完)