javascript全栈开发实践-web-6

重构代码,让代码变得可维护

在这里,我们将会使用面向对象的思想。有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了。 接下来,我们将在新的代码基础上,实现一个圆形/椭圆的绘制工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值