【用canvas做一个2/4/8分的镜像画布】

1,053 阅读2分钟

记得以前遇到过一个手机绘图,就是将画笔画出的东西镜像对半分或者8分,这样随手画出来的东西有了对称也很有特色,就像这样:

image.png

想了想,觉得canvas应该是可以实现的。因此决定做一个试试。目标是做一个8分镜像的,但是直接上手没有思路,就先从左右镜像开始做起。不过最先开始还是需要实现canvas画笔的功能。

1. canvas画笔与画布功能

这里原本的思路是使用fillRect()一个点一个点的画出来连成线,但是当鼠标移动稍微快一点就会出现断层(mousemove的触发间隔不够短),想了半天没解决办法。后面偶然去了canvas官网,发现居然有现成的画笔示例,而且用的是stroke()实现的连线。。。

微信图片_20220714143730.jpg

想想也是,你滑的快,就直接用线连起来了。再快一点也就是一条直线了。

2. 镜像绘制

这个就比较简单,计算出当前点的中心y轴对称点的坐标,在绘制完当前点以后,再绘制对称点即可。

3. 四分镜像绘制

这里有点不同了,这里多了一个计算以中心点放射对称点坐标的方法(其实这里直接用两次镜面对称绘制更方便,但是当时思路是先想到了镜面对称,所以多了个计算放射对称点的方法)。思路也差不多,先绘制镜面对称的点;再绘制当前点和镜面对称点的x中心轴对称的点。

image.png

这两个方法有很多类似,因此准备后面优化。

4. 终极8分镜像

这里开始准备用旋转、变形操作实现,但是试了试不太好实现,后来还是用了镜像对称以及放射对称实现的。

主要的绘制顺序如下:

image.png

这里多了一个求当前点同一象限内对称点的操作,由于canvas画布是一个正方形,所以将当前点的(x,y)轴位置互换就是对称点的位置了。然后就接着按象限对称点继续执行放射对称和镜像对称的操作即可。

5.最终优化后的代码

最后将整个过程封装成了类,让代码更加直观,完整页面代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
</head>

<body>
  <canvas id="c"></canvas>
  <br />
  <button id="clear">清空</button>
  <input type="number" value="2" placeholder="画笔宽度" min="0" id="num-inp" />
</body>
<script>
  class Artboard {
    constructor(c, cwh) {
      this.c = c
      this.ctx = c.getContext('2d')
      this.cwh = cwh // 画布宽高
      this.lineWidth = 2 // 画笔宽度
      this.lastTimePoint = [] // 上一次mouseMove的(x, y)位置
      this.isLeftDown = false // 鼠标是否按下
      this.middlePosition = this.cwh / 2 + 0.05
    }

    init() {
      this.c.width = this.c.height = this.cwh
      // this.drawMarkLine()
    }

    // 绘制
    drawLine(x1, y1, x2, y2, w) {
      this.ctx.beginPath();
      this.ctx.lineWidth = w;
      this.ctx.moveTo(x1, y1);
      this.ctx.lineTo(x2, y2);
      this.ctx.closePath();
      this.ctx.stroke();
    }

    // 初始化绑定事件
    initEventListener() {
      // 其他绑定事件
      document.getElementById("num-inp").addEventListener('change', (e) => {
        this.lineWidth = e.target.value
      })

      document.getElementById("clear").addEventListener("click", () => {
        this.ctx.clearRect(0, 0, this.cwh, this.cwh);
        this.drawMarkLine();
      });
      // 鼠标按下
      this.c.addEventListener("mousedown", (e) => {
        const x = e.offsetX;
        const y = e.offsetY;

        this.lastTimePoint = [x, y];
        this.isLeftDown = true;
      });

      // 鼠标抬起
      this.c.addEventListener("mouseup", () => {
        this.isLeftDown = false;
      });

      // 鼠标离开画布
      this.c.addEventListener("mouseleave", (e) => {
        if (!this.isLeftDown) return;
        const x = e.offsetX >= 0 ? e.offsetX : 0;
        const y = e.offsetY >= 0 ? e.offsetY : 0;
        this.isLeftDown = false;

        this.drawLine(this.lastTimePoint[0], this.lastTimePoint[1], x, y, this.lineWidth);
      });

      // 鼠标移动
      this.c.addEventListener("mousemove", (e) => {
        if (!this.isLeftDown) return;
        const x = e.offsetX;
        const y = e.offsetY;

        this.drawAllLines(x, y)

        // 这一步至关重要,需要保存上一个mousemove事件时的鼠标位置
        this.lastTimePoint = [x, y];
      });
    }

    // 绘制每个分区的点
    drawAllLines(x, y) {
      // 1 - 绘制当前鼠标位置的点
      this.drawLine(this.lastTimePoint[0], this.lastTimePoint[1], x, y, this.lineWidth);
      // 2 - 绘制中心点放射对称位置的点
      const xy1 = this.getRadialSymmetryPoint(this.lastTimePoint[0], this.lastTimePoint[1]); // 
      const xy2 = this.getRadialSymmetryPoint(x, y);
      const revertXY = this.drawLine(...xy1, ...xy2, this.lineWidth);
      // 3 - 绘制当前鼠标y轴镜像对称的点
      const xy11 = this.getMirrorSymmetryPoint(this.lastTimePoint[0], this.lastTimePoint[1], "x");
      const xy22 = this.getMirrorSymmetryPoint(x, y, "x");
      this.drawLine(...xy11, ...xy22, this.lineWidth);
      // 4 - 绘制当前鼠标x轴镜像对称的点
      const xy111 = this.getMirrorSymmetryPoint(this.lastTimePoint[0], this.lastTimePoint[1], "y");
      const xy222 = this.getMirrorSymmetryPoint(x, y, "y");
      this.drawLine(...xy111, ...xy222, this.lineWidth);
      // 5 - 绘制当前点同一象限内的对称点
      const xy1111 = this.getSameQuadrantPoint(this.lastTimePoint[0], this.lastTimePoint[1]);
      const xy2222 = this.getSameQuadrantPoint(x, y);
      this.drawLine(...xy1111, ...xy2222, this.lineWidth);
      // 6 - 绘制象限对称点的放射对成点
      const xy11111 = this.getRadialSymmetryPoint(...xy1111);
      const xy22222 = this.getRadialSymmetryPoint(...xy2222);
      this.drawLine(...xy11111, ...xy22222, this.lineWidth);
      // 7 - 绘制象限内对称点的y轴纵向向镜面对称点
      const xy111111 = this.getMirrorSymmetryPoint(...xy1111, 'x')
      const xy222222 = this.getMirrorSymmetryPoint(...xy2222, 'x')
      this.drawLine(...xy111111, ...xy222222, this.lineWidth);
      // 8 - 绘制象限内对称点的x轴横向镜面对称点
      const xy1111111 = this.getMirrorSymmetryPoint(...xy1111, 'y')
      const xy2222222 = this.getMirrorSymmetryPoint(...xy2222, 'y')
      this.drawLine(...xy1111111, ...xy2222222, this.lineWidth);
    }

    // 绘制辅助线
    drawMarkLine() {
      this.drawLine(this.middlePosition + 0.5, 0, this.middlePosition + 0.5, this.cwh, 0.5);
      this.drawLine(0, this.middlePosition + 0.5, this.cwh, this.middlePosition + 0.5, 0.5);
      this.drawLine(0, 0, this.cwh, this.cwh, 0.5);
      this.drawLine(this.cwh, 0, 0, this.cwh, 0.5);
    }

    // 计算与中心轴偏移量的方法
    calcNewPosit(val, offset) {
      let newVal = val
      if (val < this.middlePosition) { // 点在中心轴左侧/上方
        newVal = offset > 0 ? this.middlePosition + offset : this.middlePosition - offset;
      } else { // 点在中心轴右侧/下方
        newVal = offset < 0 ? this.middlePosition + offset : this.middlePosition - offset;
      }

      return newVal
    }

    // 计算这个点同一象限内的那个对称点。由于是正方形,所以直接将其x,y轴位置对调即可
    getSameQuadrantPoint(x, y) {
      return [y, x];
    }

    // 计算中心放射对称的点
    getRadialSymmetryPoint(x, y) {
      const xl = this.middlePosition - x;
      const yl = this.middlePosition - y;
      let nx = x, ny = y;

      nx = this.calcNewPosit(x, xl)

      ny = this.calcNewPosit(y, yl)
      return [nx, ny];
    }

    // 计算镜面对称位置的点
    getMirrorSymmetryPoint(x, y, type) {
      const xl = this.middlePosition - x; // 与中心轴的偏移量
      const yl = this.middlePosition - y;
      let nx = x,
        ny = y; // nx,ny是与对称轴对称的点x,y轴坐标
      // 根据type决定是返回当前点的纵向镜面对称还是横向镜面对称
      switch (type) {
        // 以y轴镜面对称
        case "x":
          nx = this.calcNewPosit(x, xl)
          break;
        // 以x轴镜面对称
        case "y":
          ny = this.calcNewPosit(y, yl)
          break;
        default:
          break;
      }

      return [nx, ny];
    }
  }

  window.onload = function () {
    const c = document.getElementById('c')
    const c1 = new Artboard(c, 400)

    c1.initEventListener()
    c1.init()
  }
</script>

<style>
  #c {
    border: 1px solid #ddd;
    margin-left: 100px;
  }
</style>

</html>

“我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!