You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Canvas切换绘图模式后绘制内容被图片遮挡且出现重复图形问题

Angular Canvas绘图功能问题及解决方案

问题描述

  • 在Canvas底图上绘制线条后,点击矩形绘制按钮切换绘图模式,此前绘制的内容会被置于底图下方被遮挡。
  • 在Canvas上绘制线条、矩形时,会生成多个重复的对应图形。

问题示意图

原有代码

HTML代码

<div class="container">
          <div class="row">
            <canvas id="canvasImage"></canvas>
          </div>
          <div class="tools" style="margin-top: 20px;">
            <button type="button" class="btn " (click)="createLines()">Line</button>
            <button type="button" class="btn " (click)="createPencil()">Pencil</button>
            <button type="button" class="btn " (click)="createRectangle()">Rectangle</button>
            <div (click)="change_color_red()" class="color-field" style="background-color: red;"></div>
            <div (click)="change_color_blue()" class="color-field" style="background-color: blue;"></div>
            <div (click)="change_color_green()" class="color-field" style="background-color: green;"></div>
          </div>
        </div>

Angular TypeScript代码

this.canvasEle = <HTMLCanvasElement> document.getElementById('canvasImage');
this.context = this.canvasEle.getContext("2d");
this.canvasEle.width = window.innerWidth-500;
this.canvasEle.height = window.innerHeight -200;
 
 createPencil(){
    this.mode = "Pencil";
    this.drawOnCanvas();
  }
  createLines(){
    this.mode = "Line";
    this.drawOnCanvas();
  }
  createRectangle(){
    this.mode = "Rectangle";
    this.drawOnCanvas();
  }
  drawOnCanvas(){
    var offsetX,offsetY;
    this.img = new Image();
    this.img.width = 800;
    this.img.height = 600;
    this.img.onload = () =>
    {         
      // this.context.drawImage(this.img,0,0,this.img.width, this.img.height,
      //     0,0, this.canvasEle.width,this.canvasEle.height);    
      loadImage();
    };
    this.img.src = this.imageList['0'].LINK;
    let startPosition = {x: 0, y: 0};
    let lineCoordinates = {x: 0, y: 0};
    let isDrawStart = false;
    this.draw_color = "black";
    
    const loadImage = () => {
      this.context.drawImage(this.img,10,10,this.img.width, this.img.height,
        20,20, this.canvasEle.width,this.canvasEle.height);    
    }
    const getClientOffset = (event) => {
        const {pageX, pageY} = event.touches ? event.touches[0] : event;
        var BB= this.canvasEle.getBoundingClientRect();
        const x = pageX - BB.left;
        const y = pageY - BB.top;
  
        return {
          x,
          y
        } 
    }
    const reOffset = () => {
      var BB= this.canvasEle.getBoundingClientRect();
      offsetX=BB.left;
      offsetY=BB.top;        
    }
    const drawRectangle = (e) => {
      this.context.beginPath();
      this.context.lineWidth=2;
      this.context.rect(startPosition.x, startPosition.y,lineCoordinates.x, lineCoordinates.y);
      this.context.strokeStyle = this.draw_color;
      this.context.stroke();
      e.preventDefault();
    }
    const drawPencil = (e) => {
      this.context.lineWidth = 2;
      this.context.moveTo(e.X,e.Y);
      this.context.lineTo(e.clientX- offsetX, e.clientY- offsetY);
      this.context.strokeStyle = this.draw_color;
      this.context.lineCap = "round";
      this.context.lineJoin = "round";
      this.context.stroke();
      e.preventDefault();
    }
    const drawLine = (e) => {
      this.context.beginPath();
      this.context.lineWidth=2;
      this.context.moveTo(startPosition.x, startPosition.y);
      this.context.lineTo(lineCoordinates.x, lineCoordinates.y);
      this.context.strokeStyle=this.draw_color;
      this.context.stroke();
      this.context.closePath();
      e.preventDefault();
    }
  
    const mouseDownListener = (event) => {
      startPosition = getClientOffset(event);
      isDrawStart = true;
      //takesnapshot();
      event.preventDefault();
    }
  
    const mouseMoveListener = (event) => {
      if(!isDrawStart) return;
      if(this.mode == 'Line'){
        //restoresnapshot();
        reOffset();
        lineCoordinates = getClientOffset(event);
        drawLine(event);
      }else if(this.mode == 'Rectangle'){
        //restoresnapshot();
        lineCoordinates = getClientOffset(event);
        drawRectangle(event);
      }else{
        //restoresnapshot();
        // lineCoordinates = getClientOffset(event);
        drawPencil(event);
      }
     
    }
  
    const mouseupListener = (event) => {
      isDrawStart = false;
      this.context.beginPath();
      event.preventDefault();
    }
  
    const takesnapshot = () => {
      this.snapshot = this.context.getImageData(0, 0, this.canvasEle.width, this.canvasEle.height);
    }
  
    const restoresnapshot = () => {
      this.context.putImageData(this.snapshot,0, 0);
    }
  
    this.canvasEle.addEventListener('mousedown', mouseDownListener);
    this.canvasEle.addEventListener('mousemove', mouseMoveListener);
    this.canvasEle.addEventListener('mouseup', mouseupListener);
  }
  change_color_red(){
    this.draw_color = "red";
  }
  change_color_blue(){
    this.draw_color = "blue";
  }
  change_color_green(){
    this.draw_color = "green";
  }

问题原因&修复方案

问题1:切换模式后原有绘制内容被底图遮挡

原因:每次切换模式调用drawOnCanvas时,都会重新加载底图并调用drawImage把底图画在最上层,直接覆盖了之前绘制的所有图形。
修复:

  1. 底图加载逻辑单独抽离,只在组件初始化的时候执行一次,不要放在drawOnCanvas里重复执行。
  2. 如果业务需要动态更新底图,更新前先保存当前画布的完整快照,底图绘制完成后再把快照绘制到底图上方即可。

问题2:绘制线条、矩形时出现多个重复图形

原因:

  1. 你注释掉了绘制过程中的快照恢复逻辑,每次鼠标移动绘制新的预览图形时,没有清空上一次鼠标移动时绘制的旧预览图形,导致所有移动过程中的图形都留在了画布上。
  2. 每次调用drawOnCanvas切换模式时,都会给canvas重复绑定相同的鼠标事件监听器,切换几次模式就绑定了几个监听器,一次鼠标操作会触发多次绘制逻辑,自然就出现重复图形。

修复:

  1. 组件初始化时只绑定一次鼠标事件监听器,drawOnCanvas仅负责修改绘制模式变量,不要重复绑定事件。
  2. 启用注释的快照逻辑:鼠标按下时保存当前画布快照,每次鼠标移动时先恢复快照再绘制当前预览图形,避免拖动过程中生成多个重复的预览图形。
  3. 修复drawPencil里不存在的e.X属性,换成统一的偏移坐标计算逻辑,同时调整矩形宽高计算,避免反向拖动时绘制异常。

修复后核心代码示例

// 组件初始化时执行,仅执行一次
ngOnInit() {
  this.canvasEle = <HTMLCanvasElement> document.getElementById('canvasImage');
  this.context = this.canvasEle.getContext("2d");
  this.canvasEle.width = window.innerWidth-500;
  this.canvasEle.height = window.innerHeight -200;
  this.draw_color = "black";
  // 只加载一次底图
  this.loadBaseImage();
  // 只绑定一次事件
  this.bindCanvasEvents();
}

// 底图加载逻辑单独抽离
loadBaseImage() {
  this.img = new Image();
  this.img.width = 800;
  this.img.height = 600;
  this.img.onload = () => {
    this.context.drawImage(this.img,10,10,this.img.width, this.img.height,
      20,20, this.canvasEle.width,this.canvasEle.height);  
    // 加载完底图保存初始快照
    this.takesnapshot();
  };
  this.img.src = this.imageList['0'].LINK;
}

// 事件绑定仅执行一次
bindCanvasEvents() {
  let startPosition = {x: 0, y: 0};
  let lineCoordinates = {x: 0, y: 0};
  let isDrawStart = false;
  let offsetX,offsetY;

  const getClientOffset = (event) => {
    const {pageX, pageY} = event.touches ? event.touches[0] : event;
    const BB= this.canvasEle.getBoundingClientRect();
    return {
      x: pageX - BB.left,
      y: pageY - BB.top
    } 
  }
  const reOffset = () => {
    const BB= this.canvasEle.getBoundingClientRect();
    offsetX=BB.left;
    offsetY=BB.top;        
  }
  const drawRectangle = (e) => {
    this.context.beginPath();
    this.context.lineWidth=2;
    this.context.rect(startPosition.x, startPosition.y,lineCoordinates.x - startPosition.x, lineCoordinates.y - startPosition.y);
    this.context.strokeStyle = this.draw_color;
    this.context.stroke();
    e.preventDefault();
  }
  const drawPencil = (e) => {
    const coord = getClientOffset(e);
    this.context.lineWidth = 2;
    this.context.moveTo(lineCoordinates.x, lineCoordinates.y);
    this.context.lineTo(coord.x, coord.y);
    this.context.strokeStyle = this.draw_color;
    this.context.lineCap = "round";
    this.context.lineJoin = "round";
    this.context.stroke();
    lineCoordinates = coord;
    e.preventDefault();
  }
  const drawLine = (e) => {
    this.context.beginPath();
    this.context.lineWidth=2;
    this.context.moveTo(startPosition.x, startPosition.y);
    this.context.lineTo(lineCoordinates.x, lineCoordinates.y);
    this.context.strokeStyle=this.draw_color;
    this.context.stroke();
    this.context.closePath();
    e.preventDefault();
  }

  const mouseDownListener = (event) => {
    startPosition = getClientOffset(event);
    lineCoordinates = startPosition;
    isDrawStart = true;
    this.takesnapshot();
    event.preventDefault();
  }

  const mouseMoveListener = (event) => {
    if(!isDrawStart) return;
    this.restoresnapshot();
    if(this.mode == 'Line'){
      reOffset();
      lineCoordinates = getClientOffset(event);
      drawLine(event);
    }else if(this.mode == 'Rectangle'){
      lineCoordinates = getClientOffset(event);
      drawRectangle(event);
    }else{
      drawPencil(event);
    }
  }

  const mouseupListener = (event) => {
    isDrawStart = false;
    this.context.beginPath();
    this.takesnapshot();
    event.preventDefault();
  }

  this.canvasEle.addEventListener('mousedown', mouseDownListener);
  this.canvasEle.addEventListener('mousemove', mouseMoveListener);
  this.canvasEle.addEventListener('mouseup', mouseupListener);
}

// 模式切换仅修改mode变量
createPencil(){
  this.mode = "Pencil";
}
createLines(){
  this.mode = "Line";
}
createRectangle(){
  this.mode = "Rectangle";
}

takesnapshot = () => {
  this.snapshot = this.context.getImageData(0, 0, this.canvasEle.width, this.canvasEle.height);
}

restoresnapshot = () => {
  this.context.putImageData(this.snapshot,0, 0);
}

change_color_red(){
  this.draw_color = "red";
}
change_color_blue(){
  this.draw_color = "blue";
}
change_color_green(){
  this.draw_color = "green";
}

内容的提问来源于stack exchange,提问作者Mritunjay Sharma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 12:27:01