【gpt - 使用 canvas 实现 Gui - 01】

149 阅读5分钟

"gui" 是 Graphical User Interface 的缩写,指的是图形用户界面。它是一种让用户可以通过图形化的方式与计算机交互的界面,通过鼠标、键盘等输入设备和图形化的界面来完成各种操作。与之相对的是命令行界面(CLI),它是一种通过输入命令来与计算机

以下是一个使用 JavaScript Canvas 实现简单的 GUI 程序的例子。这个程序可以让用户通过点击鼠标来画点、画线、画矩形等等,同时还提供了清空画布的功能。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Canvas GUI</title>
  </head>
  <body>
    <canvas id="canvas" width="600" height="400" style="border:1px solid #000000;"></canvas>
    <br />
    <button onclick="clearCanvas()">Clear</button>
    <script>
      var canvas = document.getElementById('canvas');
      var context = canvas.getContext('2d');
      var isDrawing = false;
      var startX, startY;
      var currentX, currentY;

      canvas.addEventListener('mousedown', function(e) {
        startX = e.clientX - canvas.offsetLeft;
        startY = e.clientY - canvas.offsetTop;
        isDrawing = true;
      });

      canvas.addEventListener('mousemove', function(e) {
        if (isDrawing) {
          currentX = e.clientX - canvas.offsetLeft;
          currentY = e.clientY - canvas.offsetTop;
          draw();
        }
      });

      canvas.addEventListener('mouseup', function(e) {
        isDrawing = false;
      });

      function draw() {
        context.beginPath();
        context.moveTo(startX, startY);
        context.lineTo(currentX, currentY);
        context.stroke();
        context.closePath();
        startX = currentX;
        startY = currentY;
      }

      function clearCanvas() {
        context.clearRect(0, 0, canvas.width, canvas.height);
      }
    </script>
  </body>
</html>

这个程序使用了 Canvas 元素来创建一个画布,通过监听鼠标事件来实现画图功能。当用户点击鼠标时,程序会记录下起始点的坐标,并将 isDrawing 变量设为 true,表示正在画图。随后,当用户移动鼠标时,程序会根据当前鼠标位置和起始点的坐标,调用 draw() 函数来画出相应的图形。当用户释放鼠标时,程序将 isDrawing 变量设为 false,表示画图结束。

draw() 函数中,我们使用 context.beginPath()context.closePath() 方法来开始和结束路径,使用 context.moveTo()context.lineTo() 方法来画线。除了画线之外,我们还可以使用 context.fillRect()context.strokeRect() 方法来画矩形,使用 context.arc() 方法来画圆形等等。同时,我们还提供了一个 clearCanvas() 函数来清空画布,使用 context.clearRect() 方法来实现。

Q: 上面能否优化代码, 例如:可以分成

  • canvas 类,里面可以 drawPoint,drawLine drawReact 等
  • color 类,专门设置颜色
  • point 设置位置信息
  • size 设置宽高
class Canvas {
  constructor(id, width, height) {
    this.canvas = document.getElementById(id);
    this.ctx = this.canvas.getContext("2d");
    this.canvas.width = width;
    this.canvas.height = height;
    this.color = new Color(this.ctx);
  }

  clear() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  }

  drawPoint(point, size = 1) {
    this.ctx.beginPath();
    this.ctx.arc(point.x, point.y, size, 0, 2 * Math.PI);
    this.ctx.fill();
  }

  drawLine(point1, point2, width = 1) {
    this.ctx.beginPath();
    this.ctx.moveTo(point1.x, point1.y);
    this.ctx.lineTo(point2.x, point2.y);
    this.ctx.lineWidth = width;
    this.ctx.stroke();
  }

  drawRect(point, size, fill = false, width = 1) {
    if (fill) {
      this.ctx.fillRect(point.x, point.y, size.width, size.height);
    } else {
      this.ctx.strokeRect(point.x, point.y, size.width, size.height);
    }
  }
}

class Color {
  constructor(ctx) {
    this.ctx = ctx;
  }

  set(fillColor = "#000000", strokeColor = "#000000") {
    this.ctx.fillStyle = fillColor;
    this.ctx.strokeStyle = strokeColor;
  }
}

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class Size {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
}

在这个优化后的示例中,我们创建了 Canvas 类来处理所有与画布相关的操作,包括清除画布,画点,画线和画矩形。我们还创建了一个名为 Color 的新类来处理颜色,这个类被 Canvas 类引用。此外,我们还创建了两个辅助类 PointSize,它们用于存储有关点和大小的信息。

使用这些类,我们可以创建一个新的 Canvas 对象并调用其中的方法:

const canvas = new Canvas("myCanvas", 500, 500);
canvas.color.set("#ff0000", "#00ff00");
canvas.clear();
canvas.drawPoint(new Point(50, 50), 10);
canvas.drawLine(new Point(100, 100), new Point(200, 200), 5);
canvas.drawRect(new Point(300, 300), new Size(50, 50), true);

在这个例子中,我们首先创建了一个新的 Canvas 对象,并设置了它的大小和颜色。我们然后清除画布并使用 drawPointdrawLinedrawRect 方法来画点、画线和画矩形。

Q: 上面drawLine 能否不使用lineTo, 使用 drawPoint 来实现,同理 drawRect 使用 drawLine 实现

A: 可以使用一些基本的图形元素来构建其他高级图形元素。下面是一个使用 drawPoint 绘制线段的示例代码:

class Canvas {
  constructor(width, height) {
    this.canvas = document.createElement('canvas');
    this.canvas.width = width;
    this.canvas.height = height;
    this.ctx = this.canvas.getContext('2d');
  }

  setColor(color) {
    this.ctx.strokeStyle = color;
  }

  setLineWidth(width) {
    this.ctx.lineWidth = width;
  }

  drawPoint(x, y) {
    this.ctx.beginPath();
    this.ctx.moveTo(x, y);
    this.ctx.lineTo(x + 1, y + 1);
    this.ctx.stroke();
  }

  drawLine(x1, y1, x2, y2) {
    this.ctx.beginPath();
    this.ctx.moveTo(x1, y1);
    this.drawPoint(x2, y2);
    this.ctx.stroke();
  }

  drawRect(x, y, width, height) {
    this.drawLine(x, y, x + width, y);
    this.drawLine(x + width, y, x + width, y + height);
    this.drawLine(x + width, y + height, x, y + height);
    this.drawLine(x, y + height, x, y);
  }

  addToDOM(element) {
    element.appendChild(this.canvas);
  }
}

const canvas = new Canvas(400, 400);
canvas.setColor('#FF0000');
canvas.setLineWidth(5);
canvas.drawLine(100, 100, 300, 300);
canvas.addToDOM(document.body);

这里使用了 drawPoint 方法来实现绘制线段,通过在线段的两个端点绘制一个非常小的点来实现。同样,可以使用 drawLine 方法来实现绘制矩形。

Q: 过改变像素信息来实现 画点效果

A:

好的,下面是分别基于像素操作实现的 drawLinedrawRect 方法:

class Canvas {
  constructor(canvasId) {
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext("2d");
    this.w = this.canvas.width;
    this.h = this.canvas.height;
    this.pixels = this.context.getImageData(0, 0, this.w, this.h);
  }

  drawPoint(x, y, color) {
    const index = (y * this.w + x) * 4;
    this.pixels.data[index] = color.r;
    this.pixels.data[index + 1] = color.g;
    this.pixels.data[index + 2] = color.b;
    this.pixels.data[index + 3] = 255;
    this.context.putImageData(this.pixels, 0, 0);
  }

  drawLine(x1, y1, x2, y2, color) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    const steps = Math.max(Math.abs(dx), Math.abs(dy));
    const xIncrement = dx / steps;
    const yIncrement = dy / steps;

    let x = x1;
    let y = y1;
    for (let i = 0; i <= steps; i++) {
      this.drawPoint(Math.round(x), Math.round(y), color);
      x += xIncrement;
      y += yIncrement;
    }
  }

  drawRect(x, y, width, height, color) {
    this.drawLine(x, y, x + width, y, color);
    this.drawLine(x, y, x, y + height, color);
    this.drawLine(x, y + height, x + width, y + height, color);
    this.drawLine(x + width, y, x + width, y + height, color);
  }

  setColor(color) {
    this.color = color;
  }
}

class Color {
  constructor(r, g, b) {
    this.r = r;
    this.g = g;
    this.b = b;
  }
}

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class Size {
  constructor(w, h) {
    this.w = w;
    this.h = h;
  }
}

这里的 drawLine 方法使用的是 Bresenham 直线算法,而 drawRect 方法则是基于 drawLine 方法实现的。这两个方法与之前的 drawPoint 方法一样,都是基于像素操作实现的。

Q: 加 clear

A:

以下是修改后的完整代码:

class Color {
  static white() {
    return { r: 255, g: 255, b: 255, a: 255 };
  }

  static black() {
    return { r: 0, g: 0, b: 0, a: 255 };
  }
}

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class Size {
  constructor(w, h) {
    this.w = w;
    this.h = h;
  }
}

class Canvas {
  constructor(canvasId, size) {
    this.canvas = document.getElementById(canvasId);
    this.context = this.canvas.getContext('2d');
    this.size = size;
    this.canvas.width = size.w;
    this.canvas.height = size.h;
    this.pixels = this.context.getImageData(0, 0, size.w, size.h);

    this.clear();
  }

  clear() {
    const clearColor = Color.white();
    for (let i = 0; i < this.size.w; i++) {
      for (let j = 0; j < this.size.h; j++) {
        const index = (i + j * this.size.w) * 4;
        this.pixels.data[index] = clearColor.r;
        this.pixels.data[index + 1] = clearColor.g;
        this.pixels.data[index + 2] = clearColor.b;
        this.pixels.data[index + 3] = clearColor.a;
      }
    }
    this.context.putImageData(this.pixels, 0, 0);
  }

  drawPoint(point, color) {
    const index = (point.x + point.y * this.size.w) * 4;
    this.pixels.data[index] = color.r;
    this.pixels.data[index + 1] = color.g;
    this.pixels.data[index + 2] = color.b;
    this.pixels.data[index + 3] = color.a;

    this.context.putImageData(this.pixels, 0, 0);
  }

  drawLine(start, end, color) {
    const dx = end.x - start.x;
    const dy = end.y - start.y;
    const steps = Math.max(Math.abs(dx), Math.abs(dy));
    const xIncrement = dx / steps;
    const yIncrement = dy / steps;

    let x = start.x;
    let y = start.y;

    for (let i = 0; i <= steps; i++) {
      const point = new Point(Math.round(x), Math.round(y));
      this.drawPoint(point, color);
      x += xIncrement;
      y += yIncrement;
    }
  }

  drawRect(point, size, color) {
    const p1 = point;
    const p2 = new Point(point.x + size.w, point.y);
    const p3 = new Point(point.x + size.w, point.y + size.h);
    const p4 = new Point(point.x, point.y + size.h);
    this.drawLine(p1, p2, color);
    this.drawLine(p2, p3, color);
    this.drawLine(p3, p4, color);
    this.drawLine(p4, p1, color);
  }
}

const canvas = new Canvas('myCanvas', new Size(400, 400));
canvas.drawLine(new Point(50, 50), new Point(350, 350), Color.black());
canvas.drawRect(new Point(100, 100), new Size(200, 100), Color.black());
canvas.clear();