重构代码,让代码变得可维护
在这里,我们将会使用面向对象的思想。有java/c++基础的,会很容易接受。如果没有了解过,建议先了解面向对象的编程思想。 JavaScript在ES6引入了关键字class,让我们可以按照java/c++的方式来编写class了,从而摆脱难以理解的原型链。 首先,在之前的实现中,我们有不同的工具,例如铅笔,荧光笔,矩形等。那么,我们可以抽象出一个tool,它代表了一个工具。这个工具,用于接收事件,然后进行显示。定义如下:
class DrawingTool {
constructor(ctx, lineWidth, strokeStyle) {
this._ctx = ctx;
ctx.lineWidth = lineWidth;
ctx.strokeStyle = strokeStyle;
}
//
handleMouseDown(x, y) { throw new Error('handleMouseDown not implemented'); }
handleMouseMove(x, y) { throw new Error('handleMouseMove not implemented'); }
handleMouseUp(x, y) { throw new Error('handleMouseUp not implemented'); }
}
复制代码
接下来,在鼠标起来的时候,完成了绘制。此时,我们还需要记住用户的操作结果,并在undo/redo的时候,去进行重绘。我们可以把这部分数据,保存在tool里面,也就是在actions里面,记录每一个tool。在我们目前的需求中,这个并没有任何问题。 不过,我们可以考虑的长远一些:假如我们以后增加了一个功能,用户直接手绘一个三角形,我们用程序识别出开,并且准确找出三角形的三个顶点,然后用直线绘制出一个漂亮的三角形。那么,我们如果直接在actions里面直接记录tool,就不太容易处理后续的undo/redo。 因此,我们可以抽象出一个操作数据类型,它记录了每一个图形绘制的结果数据。例如,铅笔,荧光笔等,他们实际上都是一个路径,一个颜色,一个粗细。他们就可以使用相同的数据类型。而对于矩形,则有自己的数据类型。当我们发现用户手绘了一个三角形的时候,我们可以直接让tool返回一个三角形数据,然后加入到actions里面。 因此我们定义一个ActionData类型:
class ActionData {
//
constructor(type, lineWidth, strokeStyle) {
this._type = type;
this._lineWidth = lineWidth;
this._strokeStyle = strokeStyle;
}
//
draw(ctx) { throw new Error('draw not implemented'); }
}
复制代码
这个类型,目前只有一个方法共外部调用,就是draw。也就是在undo/redo的时候,我们将会调用draw来让每一个数据显示自己。 因为目前铅笔,荧光笔和橡皮擦,他们之间除了属性外,没有任何不同,因此,我们暂时只需要定一个PathTool,就可以实现这三个工具:
class PathTool extends DrawingTool {
//
constructor(ctx, lineWidth, strokeStyle) {
super(ctx, lineWidth, strokeStyle);
this._points = [];
}
//
handleMouseDown(x, y) {
//
const ctx = this.ctx;
ctx.beginPath();
ctx.moveTo(x, y);
//
this._points = [{x, y}];
}
//
handleMouseMove(x, y) {
//
const ctx = this.ctx;
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, y);
this._points.push({x, y});
}
//
handleMouseUp() {
return new PathData(this._points, this.lineWidth, this.strokeStyle);
}
}
复制代码
而对于矩形工具,我们的实现如下:
class RectTool extends DrawingTool {
//
constructor(ctx, lineWidth, strokeStyle) {
super(ctx, lineWidth, strokeStyle);
}
//
handleMouseDown(x, y) {
//
const ctx = this.ctx;
this._cloneCanvas(ctx);
this._topLeft = {x, y};
}
//
handleMouseMove(x, y) {
if (this._tempImageData) {
ctx.putImageData(this._tempImageData, 0, 0);
}
//
this._bottomRight = {x, y};
//
ctx.beginPath();
ctx.rect(this._topLeft.x, this._topLeft.y, x - this._topLeft.x, y - this._topLeft.y);
ctx.stroke();
}
//
handleMouseUp() {
return new RectData(this._topLeft, this._bottomRight, this.lineWidth, this.strokeStyle);
}
//
_cloneCanvas(ctx) {
const canvas = ctx.canvas;
this._tempImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
}
}
复制代码
而对于PathData和RectData来说,也很简单:
class PathData extends ActionData {
//
constructor(points, lineWidth, strokeStyle) {
super('path', lineWidth, strokeStyle);
//
this._points = points;
}
//
draw(ctx) {
//
ctx.beginPath();
ctx.lineWidth = this._lineWidth;
ctx.strokeStyle = this._strokeStyle;
//
let points = this._points;
if (points.length == 0) {
return;
}
//
let firstPoint = points[0];
ctx.moveTo(firstPoint.x, firstPoint.y);
for (let j = 1; j < points.length; j++) {
const point = points[j];
ctx.lineTo(point.x, point.y);
}
ctx.stroke();
//
}
}
//
class RectData extends ActionData {
constructor(topLeft, bottomRight, lineWidth, strokeStyle) {
super('rect', lineWidth, strokeStyle);
//
this._topLeft = topLeft;
this._bottomRight = bottomRight;
}
//
draw(ctx) {
//
ctx.beginPath();
ctx.lineWidth = this._lineWidth;
ctx.strokeStyle = this._strokeStyle;
//
const pt1 = this._topLeft;
const pt2 = this._bottomRight;
ctx.rect(pt1.x, pt1.y, pt2.x - pt1.x, pt2.y - pt1.y);
ctx.stroke();
//
}
}
复制代码
在我们把工具和数据部分代码分离出来之后,整个逻辑也变得简单了:
let tool = new PathTool(ctx, 2, 'blue');
//
function handleMouseDown(event) {
//
if (undoCursor != -1) {
actions = actions.slice(0, undoCursor);
}
undoCursor = -1;
//
pad.addEventListener('mousemove', handleMouseMove);
pad.addEventListener('mouseup', handleMouseUp);
//
tool.handleMouseDown(event.offsetX, event.offsetY);
}
//
function handleMouseMove(event) {
tool.handleMouseMove(event.offsetX, event.offsetY);
}
//
function handleMouseUp(event) {
pad.removeEventListener('mousemove', handleMouseMove);
pad.removeEventListener('mouseup', handleMouseUp);
//
let actionData = tool.handleMouseUp(event.offsetX, event.offsetY);
actions.push(actionData);
//
updateButtonStatus();
}
//
function handleChoosePencil(event) {
tool = new PathTool(ctx, 2, 'blue');
}
//
function handleChooseHighlighter(event) {
tool = new PathTool(ctx, 8, 'rgba(255, 255, 0, 0.5)');
}
//
function handleChooseEraser(event) {
tool = new PathTool(ctx, 8, 'white');
}
//
function handleDrawRect(event) {
tool = new RectTool(ctx, 2, 'red');
}
...
function repaint() {
ctx.clearRect(0, 0, pad.width, pad.height);
//
let toIndex = undoCursor == -1 ? actions.length : undoCursor;
for (let i = 0; i < toIndex; i++) {
//
let actionData = actions[i];
actionData.draw(ctx);
}
}
复制代码
可以看到,鼠标响应代码以及重绘代码里面,已经没有if/else了。 接下来,我们将在新的代码基础上,实现一个圆形/椭圆的绘制工具。