canvas实现签字板功能

<!DOCTYPE html>
<html lang="en">
<head>
  <style>#canvas{border: 1px solid}</style>
</head>
<body>
  <div>
    <button class="clear">清空</button>
    <button class="revoke">上一笔</button>
    <button class="playback">播放</button>
  </div>
  <canvas id="canvas"></canvas>
</body>
<script src="./canvas.js"></script>
<script>
	const board = new DrawingBoard({
		selecter: '#canvas'
	});
	document.querySelector('.clear').onclick = () => {
		board.clear()
	};
	document.querySelector('.revoke').onclick = () => {
		board.revoke()
	};
	document.querySelector('.playback').onclick = () => {
		board.playback()
	};
</script>
</html>
class DrawingBoard {

  constructor(param = {}) {
    this.width = param?.width ?? 400;
    this.height = param?.height ?? 300;
    this.color = param?.color ?? 'red';
    this.size = param?.size ?? 2;
    this.speed = param?.speed ?? 10;
    this.isOutStop = param?.isOutStop ?? true;

    /**
     * drawRecord: 绘制历史列表中,即每一笔的记录集合
     *             这是一个二维数组,里面的每一条数据都是一次起笔到抬起的路径集合,
     *             二级数组中的数据为这一笔划过的路径的坐标信息
     * coords: 每一笔划过路径的坐标信息
     * timerId: 存放定时器ID,用于随时取消回放
     * isDrag: 设置开始绘画的开关
     */
    this.drawRecord = [];
    this.coords = [];
    this.timerId = null;
    this.isDrag = false;
    this.isPlay = false;

    /** @type {HTMLCanvasElement} */
    this.canvas = document.querySelector(param.selecter);
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    this.canvas.style.cursor = 'crosshair';

    this.ctx = this.canvas.getContext('2d');
		this.init();
  };

  /**
   * 初始化画笔样式
   * 为画布添加鼠标交互监听事件
   */
  init() {
    const {canvas, ctx, color, isOutStop, size} = this;

    ctx.lineWidth = size;
    ctx.lineJoin = 'round';
    ctx.strokeStyle = color;
    ctx.shadowColor = color;

    canvas.onmousedown = this.onmouseDown.bind(this);

    canvas.onmousemove = this.onmouseMove.bind(this);
    
    canvas.onmouseup = this.onmouseUp.bind(this);

    /**
     * 超出后停止绘画的前提是鼠标必须处于按下的状态
     */
    if (isOutStop && this.isDrag) {
      canvas.onmouseout = this.onmouseUp.bind(this)
    }
    
  };
  /**
   * 绘制线条
   */
  draw([x, y] = []) {
    this.ctx.lineTo(x, y)
    this.ctx.stroke();
  };

  onmouseDown() {
    if (this.isPlay) return;
    this.isDrag = true;
    this.ctx.beginPath();
  };

  onmouseMove(e) {
    if (this.isDrag) {
      const x = e.offsetX;
      const y = e.offsetY;

      this.draw([x, y])
      /**
       * 将当前画笔的路径坐标暂存起来,
       * 在鼠标抬起后将这一条绘制记录放入绘制历史列表中
       */
      this.coords.push([x, y])
    } 
  };

  onmouseUp() {
    this.isDrag = false;
    /**
     * 将当前绘画的这一笔存入到绘制历史记录中,
     * 并清空画笔坐标记录列表,避免下次重复记录到下一笔中
     */
    this.drawRecord.push([...this.coords]);
    this.coords = []
  };

  /**
   * 清空画布
   */
  clear(resetDrawRecord = true) {
    if (resetDrawRecord) {
      this.drawRecord = []
    };
    // 清除定时器,避免在点击清空是正在回放的绘制未结束,导致不能正常清空内容
    clearTimeout(this.timerId)

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

  /**
   * 播放绘制路径
   * @param { number } speed - 播放速度
   * 如果传入的speed为0,应当直接同步绘制,不需要定时器
   */
  playback(speed = this.speed) {
    // 播放前清空画布,但不清空记录
    this.clear(false);
    
    // 判断当前是否有绘制记录
    if (!this.drawRecord?.length) return;

    this.ctx.beginPath();
    
    const allCoord = this.collectAllCoords();

    this.isPlay = true;

    /**
     * 通过递归是便于控制播放的速度
     */
    const reDraw = (i) => {
      this.draw(allCoord[i]);
      
      clearTimeout(this.timerId);
      /**
       * 如果coord不存在说明已经超出数组范围了,则可以停止继续绘制了
       */
      const coord = allCoord[i + 1];
      if (!coord) {
        this.isPlay = false;
        return
      };
      /**
       * 当coord为reBeginPath时说明已经绘制到了新的一笔
       */
      if (coord === 'reBeginPath') {
        this.ctx.beginPath();
      };

      if (speed === 0) {
        reDraw(i + 1);
        return
      };

      // this.timerId = setTimeout(() => {
      //   reDraw(i + 1)
      // }, speed)
			
			window.requestAnimationFrame(()=>{
				reDraw(i + 1);
			})
    }

    reDraw(0)
  };

  /**
   * 将画笔历史记录中每一笔的路径坐标按顺序放在一个数组中
   * 在每一此记录被放入时在前面添加一个重新起笔的标识reBeginPath
   * 播放书写过程中,如果不设置重新起笔
   * 所有的笔画都将被连接起来
   */
  collectAllCoords() {
    const allCoord = [];

    this.drawRecord.forEach((coords) => {
      allCoord.push('reBeginPath');
      allCoord.push(...coords);
    });

    return allCoord
  };
  /**
   * 返回上一笔
   */
  revoke() {
    this.drawRecord.pop();
    this.playback(0)
  };

  /**
   * 利用requestAnimationFrame实现setTimeout功能
   */
  _setTimeout(callback, delay) {

    let start = 0, timeStamp = new Date().getTime();

    const implement = (t) => {

      if (start === 0) {
        start = t
      };

      if (t >= (delay + start)) {
        window.cancelAnimationFrame(window[timeStamp]);
        delete window[timeStamp];
        callback();
        return
      };

      window.cancelAnimationFrame(window[timeStamp]);
      delete window[timeStamp];

      window[timeStamp] = window.requestAnimationFrame(implement);
    };

    window[timeStamp] = window.requestAnimationFrame(implement);

    return timeStamp
  };

  _clearTimeout(id) {
    window.cancelAnimationFrame(window[id]);
    delete window[id]
  }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值