Vue 74 ,JavaScript 32 ,Canvas绘制图片和区域(前端使用Canvas绘制图片,并在图片上绘制区域)

目录

前言
一. 初始画布
二. 计算宽高
三. 开始绘制
四. 点击事件
五. 鼠标事件
六. 双击事件
七. 遍历坐标
八. 保存数据
九. 渲染数据
十. 执行过程
十一. 原生 JS
十二. 全部代码


前言

在前端开发中,有时我们需要在图片上进行一些交互式操作,比如绘制区域、标记等。这种场景下,我们可以使用HTML5的<canvas>标签元素来实现。Canvas 是 HTML5 提供的一种图形绘制接口,可以通过 JavaScript 在网页上绘制图形、动画和其他视觉效果。这里分享记录,如何在图片上绘制区域。

实现效果:

204cfb1789394df79e6694497d70322f.gif

那么具体如何使用Canvas在图片上绘制区域呢?

一. 初始画布

首先,我们需要初始化三个canvas画布(初始化Canvas)

initCanvas() {
  // 初始化canvas画布
  let canvasWrap = document.getElementsByClassName("canvas-wrap");
  this.wrapWidth = canvasWrap[0].clientWidth;
  this.wrapHeight = canvasWrap[0].clientHeight;

  this.imgCanvas = document.getElementById("imgCanvas");
  this.imgCtx = this.imgCanvas.getContext("2d");

  // 绘制canvas
  this.drawCanvas = document.getElementById("drawCanvas");
  this.drawCtx = this.drawCanvas.getContext("2d");

  // 保存绘制区域 saveCanvas
  this.saveCanvas = document.getElementById("saveCanvas");
  this.saveCtx = this.saveCanvas.getContext("2d");
},},
  1. imgCanvas用于绘制原始图片
  2. drawCanvas用于临时绘制区域
  3. saveCanvas用于保存最终绘制的区域


二. 计算宽高

计算并设置canvas的宽高比例,以适应图片尺寸,请看

initImgCanvas() {
  // 计算宽高比
  let ww = this.wrapWidth; // 画布宽度
  let wh = this.wrapHeight; // 画布高度 
  let iw = this.imgWidth; // 图片宽度
  let ih = this.imgHeight; // 图片高度

  if (iw / ih < ww / wh) {
    // 以高为主
    this.ratio = ih / wh;
    this.canvasHeight = wh;
    this.canvasWidth = (wh * iw) / ih;
  } else {
    // 以宽为主 
    this.ratio = iw / ww;
    this.canvasWidth = ww;
    this.canvasHeight = (ww * ih) / iw;
  }

  // 初始化画布大小
  this.imgCanvas.width = this.canvasWidth;
  this.imgCanvas.height = this.canvasHeight;
  this.drawCanvas.width = this.canvasWidth; 
  this.drawCanvas.height = this.canvasHeight;
  this.saveCanvas.width = this.canvasWidth;
  this.saveCanvas.height = this.canvasHeight;

  // 图片加载绘制
  let img = document.createElement("img");
  img.src = this.imgUrl;
  img.onload = () => {
    console.log("图片已加载");
    this.imgCtx.drawImage(img, 0, 0, this.canvasWidth, this.canvasHeight);
    this.renderDatas(); // 渲染原有数据
  };
},},

这里先计算画布和图片的宽高比,根据比例关系决定以宽为主还是以高为主进行等比缩放。然后设置三个canvas的宽高,并在图片加载完成后将其绘制到imgCanvas上。renderDatas函数用于渲染已有的绘制数据(如果有的话)。


三. 开始绘制

开始绘制,绘制的主要逻辑

startDraw() {
  // 绘制区域
  if (this.isDrawing) return;
  this.isDrawing = true;
  // 绘制逻辑
  this.drawCanvas.addEventListener("click", this.drawImageClickFn);
  this.drawCanvas.addEventListener("dblclick", this.drawImageDblClickFn);
  this.drawCanvas.addEventListener("mousemove", this.drawImageMoveFn);
},},

我们在drawCanvas上监听clickdblclickmousemove事件,分别对应点击、双击和鼠标移动三种绘制交互。


四. 点击事件

点击事件,用于开始一个新的区域绘制

drawImageClickFn(e) {
  let drawCtx = this.drawCtx;
  if (e.offsetX || e.layerX) {
    let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
    let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
    let lastPoint = this.drawingPoints[this.drawingPoints.length - 1] || [];
    if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
      this.drawingPoints.push([pointX, pointY]);
    }
  }
},},

这里获取鼠标点击的坐标,并将其推入drawingPoints数组中,用于临时保存当前绘制区域的点坐标。


五. 鼠标事件

鼠标移动事件,用于实时绘制区域

drawImageMoveFn(e) {
  let drawCtx = this.drawCtx;
  if (e.offsetX || e.layerX) {
    let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
    let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
    // 绘制
    drawCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);

    // 绘制点
    drawCtx.fillStyle = "blue";
    this.drawingPoints.forEach((item, i) => {
      drawCtx.beginPath();
      drawCtx.arc(...item, 6, 0, 180);
      drawCtx.fill(); //填充
    });

    // 绘制动态区域
    drawCtx.save();
    drawCtx.beginPath();
    this.drawingPoints.forEach((item, i) => {
      drawCtx.lineTo(...item);
    });
    drawCtx.lineTo(pointX, pointY);
    drawCtx.lineWidth = "3";
    drawCtx.strokeStyle = "blue";
    drawCtx.fillStyle = "rgba(255, 0, 0, 0.3)";
    drawCtx.stroke();
    drawCtx.fill(); //填充
    drawCtx.restore();
  }
},},

这里先清空drawCanvas,然后遍历drawingPoints数组,绘制已经点击的点。接着再绘制一个动态区域,即从第一个点开始,连线到当前鼠标位置,形成一个闭合多边形区域。


六. 双击事件

双击事件,用于完成当前区域的绘制

drawImageDblClickFn(e) {
  let drawCtx = this.drawCtx;
  let saveCtx = this.saveCtx;
  if (e.offsetX || e.layerX) {
    let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
    let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
    let lastPoint = this.drawingPoints[this.drawingPoints.length - 1] || [];
    if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
      this.drawingPoints.push([pointX, pointY]);
    }
  }
  // 清空绘制图层
  drawCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
  // 绘制区域至保存图层
  this.drawSaveArea(this.drawingPoints);

  this.drawedPoints.push(this.drawingPoints);
  this.drawingPoints = [];
  this.isDrawing = false;

  // 绘制结束逻辑
  this.drawCanvas.removeEventListener("click", this.drawImageClickFn);
  this.drawCanvas.removeEventListener("dblclick", this.drawImageDblClickFn);
  this.drawCanvas.removeEventListener("mousemove", this.drawImageMoveFn);
},},

双击时,先获取双击的坐标点,并将其推入drawingPoints数组中。然后清空drawCanvas,并调用drawSaveArea方法,将当前绘制区域渲染到saveCanvas上。


七. 遍历坐标

遍历区域点坐标的方法

drawSaveArea(points) {
  if (points.length === 0) return;
  this.saveCtx.save();
  this.saveCtx.beginPath();
  points.forEach((item, i) => {
    this.saveCtx.lineTo(...item);
  });
  this.saveCtx.closePath();
  this.saveCtx.lineWidth = "2";
  this.saveCtx.fillStyle = "rgba(255,0, 255, 0.3)";
  this.saveCtx.strokeStyle = "red";
  this.saveCtx.stroke();
  this.saveCtx.fill(); 
  this.saveCtx.restore();
},},

drawSaveArea方法会遍历当前区域的所有点坐标,并在saveCanvas上绘制一个闭合的多边形区域,边框为红色,填充为半透明的紫色。接下来,将当前绘制区域的点坐标数组drawingPoints推入drawedPoints数组中,用于保存所有已绘制的区域数据。然后,重置drawingPointsisDrawing的状态,并移除所有绘制事件的监听器。

至此,一个区域的绘制就完成了。如果需要继续绘制新的区域,只需再次调用startDraw方法即可。


八. 保存数据

这里我们需要将绘制的区域数据保存下来,以及从已有数据中处理出需要的区域数据,请看

savePoints() {
  // 将画布坐标数据转换成提交数据
  let objectPoints = [];
  objectPoints = this.drawedPoints.map((area) => {
    let polygon = {};
    area.forEach((point, i) => {
      polygon[`x${i + 1}`] = Math.round(point[0] * this.ratio);
      polygon[`y${i + 1}`] = Math.round(point[1] * this.ratio);
    });
    return {
      polygon: polygon,
    };
  });
  this.submitData = objectPoints;
  console.log("最终提交数据", objectPoints);
},},

这里遍历所有已绘制的区域drawedPoints,将每个区域的点坐标根据ratio进行缩放(实际图片尺寸),并转换成一个polygon对象的形式,最终保存在submitData中。


九. 渲染数据

renderDatas() {
  // 将提交数据数据转换成画布坐标
  this.drawedPoints = this.submitData.map((item) => {
    let polygon = item.polygon;
    let points = [];
    for (let i = 1; i < Object.keys(polygon).length / 2 + 1; i++) {
      if (!isNaN(polygon[`x${i}`]) && !isNaN(polygon[`y${i}`])) {
        points.push([
          polygon[`x${i}`] / this.ratio,
          polygon[`y${i}`] / this.ratio,
        ]);
      }
    }
    this.drawSaveArea(points);
    return points;
  });
},},

渲染数据的逻辑是,遍历submitData中的每个polygon对象,根据ratio将其坐标值转换成canvas的坐标值,并调用drawSaveArea方法将其渲染到saveCanvas上。至此,我们就完成了在canvas上绘制图片区域的全部逻辑。可以根据具体需求进行相应的调整和扩展。


十. 执行过程

具体全部的执行顺序如下:

  1. 初始化Canvas
    • 调用initCanvas()方法初始化三个Canvas画布
    • 调用initImgCanvas()方法计算并设置画布宽高比例,加载并绘制图片
  2. 开始绘制
    • 调用startDraw()方法
    • 监听drawCanvasclickdblclickmousemove事件
    • 点击时,在drawImageClickFn中记录点坐标
    • 移动时,在drawImageMoveFn中实时绘制区域
    • 双击时,在drawImageDblClickFn中完成当前区域绘制,保存至saveCanvas
  3. 保存和渲染数据
    • 调用savePoints()方法,将绘制区域的点坐标数据转换并保存到submitData
    • 调用renderDatas()方法,将submitData中的数据转换并渲染到saveCanvas上。

简单来说,就是先初始化画布,然后开始绘制区域的交互,最后保存和渲染数据。


十一. 原生 JS

当然,如果想使用原生JS实现,可以改成像下面这样

let canvasWrap, wrapWidth, wrapHeight, imgCanvas, imgCtx, drawCanvas, 
drawCtx, saveCanvas, saveCtx;
let ratio, canvasWidth, canvasHeight, imgWidth, imgHeight, imgUrl;
let isDrawing = false;
let drawingPoints = [];
let drawedPoints = [];
let submitData = [];

// 1. 初始化Canvas画布
function initCanvas() {
  // 获取canvas容器元素并设置宽高
  canvasWrap = document.getElementsByClassName("canvas-wrap")[0];
  wrapWidth = canvasWrap.clientWidth;
  wrapHeight = canvasWrap.clientHeight;

  // 获取canvas元素并获取2D绘图上下文
  imgCanvas = document.getElementById("imgCanvas");
  imgCtx = imgCanvas.getContext("2d");
  drawCanvas = document.getElementById("drawCanvas");
  drawCtx = drawCanvas.getContext("2d");
  saveCanvas = document.getElementById("saveCanvas");
  saveCtx = saveCanvas.getContext("2d");
}

// 2. 初始化图片Canvas
function initImgCanvas() {
  // 计算画布和图片的宽高比
  let ww = wrapWidth;
  let wh = wrapHeight;
  let iw = imgWidth;
  let ih = imgHeight;

  if (iw / ih < ww / wh) {
    ratio = ih / wh;
    canvasHeight = wh;
    canvasWidth = (wh * iw) / ih;
  } else {
    ratio = iw / ww;
    canvasWidth = ww;
    canvasHeight = (ww * ih) / iw;
  }

  // 设置三个canvas的宽高
  imgCanvas.width = canvasWidth;
  imgCanvas.height = canvasHeight;
  drawCanvas.width = canvasWidth;
  drawCanvas.height = canvasHeight;
  saveCanvas.width = canvasWidth;
  saveCanvas.height = canvasHeight;

  // 加载图片并绘制到imgCanvas上
  let img = document.createElement("img");
  img.src = imgUrl;
  img.onload = () => {
    console.log("图片已加载");
    imgCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
    renderDatas(); // 渲染已有数据
  };
}

// 3. 开始绘制
function startDraw() {
  if (isDrawing) return;
  isDrawing = true;
  // 监听drawCanvas的click、dblclick和mousemove事件
  drawCanvas.addEventListener("click", drawImageClickFn);
  drawCanvas.addEventListener("dblclick", drawImageDblClickFn);
  drawCanvas.addEventListener("mousemove", drawImageMoveFn);
}

// 4. 清空所有绘制区域
function clearAll() {
  saveCtx.clearRect(0, 0, canvasWidth, canvasHeight);
  drawedPoints = [];
}

// 5. 获取并加载图片
function getImage() {
  imgUrl = "需要渲染的图片地址";
  imgWidth = 200;
  imgHeight = 300;
  imgUrl && initImgCanvas();
}

// 6. 点击事件,记录点坐标
function drawImageClickFn(e) {
  if (e.offsetX || e.layerX) {
    let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
    let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
    let lastPoint = drawingPoints[drawingPoints.length - 1] || [];
    if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
      drawingPoints.push([pointX, pointY]);
    }
  }
}

// 7. 鼠标移动事件,实时绘制区域
function drawImageMoveFn(e) {
  if (e.offsetX || e.layerX) {
    let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
    let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
    drawCtx.clearRect(0, 0, canvasWidth, canvasHeight);

    drawCtx.fillStyle = "blue";
    drawingPoints.forEach((item, i) => {
      drawCtx.beginPath();
      drawCtx.arc(...item, 6, 0, 180);
      drawCtx.fill();
    });

    drawCtx.save();
    drawCtx.beginPath();
    drawingPoints.forEach((item, i) => {
      drawCtx.lineTo(...item);
    });
    drawCtx.lineTo(pointX, pointY);
    drawCtx.lineWidth = "3";
    drawCtx.strokeStyle = "blue";
    drawCtx.fillStyle = "rgba(255, 0, 0, 0.3)";
    drawCtx.stroke();
    drawCtx.fill();
    drawCtx.restore();
  }
}

// 8. 双击事件,完成当前区域绘制
function drawImageDblClickFn(e) {
  if (e.offsetX || e.layerX) {
    let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
    let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
    let lastPoint = drawingPoints[drawingPoints.length - 1] || [];
    if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
      drawingPoints.push([pointX, pointY]);
    }
  }
  drawCtx.clearRect(0, 0, canvasWidth, canvasHeight);
  drawSaveArea(drawingPoints);

  drawedPoints.push(drawingPoints);
  drawingPoints = [];
  isDrawing = false;

  drawCanvas.removeEventListener("click", drawImageClickFn);
  drawCanvas.removeEventListener("dblclick", drawImageDblClickFn);
  drawCanvas.removeEventListener("mousemove", drawImageMoveFn);
}

// 9. 绘制区域到saveCanvas
function drawSaveArea(points) {
  if (points.length === 0) return;
  saveCtx.save();
  saveCtx.beginPath();
  points.forEach((item, i) => {
    saveCtx.lineTo(...item);
  });
  saveCtx.closePath();
  saveCtx.lineWidth = "2";
  saveCtx.fillStyle = "rgba(255,0, 255, 0.3)";
  saveCtx.strokeStyle = "red";
  saveCtx.stroke();
  saveCtx.fill();
  saveCtx.restore();
}

// 10. 保存绘制数据
function savePoints() {
  let objectPoints = [];
  objectPoints = drawedPoints.map((area) => {
    let polygon = {};
    area.forEach((point, i) => {
      polygon[`x${i + 1}`] = Math.round(point[0] * ratio);
      polygon[`y${i + 1}`] = Math.round(point[1] * ratio);
    });
    return {
      polygon: polygon,
    };
  });
  submitData = objectPoints;
  console.log("最终提交数据", objectPoints);
}

// 11. 渲染已有数据
function renderDatas() {
  drawedPoints = submitData.map((item) => {
    let polygon = item.polygon;
    let points = [];
    for (let i = 1; i < Object.keys(polygon).length / 2 + 1; i++) {
      if (!isNaN(polygon[`x${i}`]) && !isNaN(polygon[`y${i}`])) {
        points.push([
          polygon[`x${i}`] / ratio, // 根据ratio换算canvas坐标
          polygon[`y${i}`] / ratio,
        ]);
      }
    }
    drawSaveArea(points); // 调用drawSaveArea将区域绘制到saveCanvas上
    return points;
  });
},},

// 使用方式
initCanvas(); // 1. 初始化Canvas画布
getImage(); // 5. 获取并加载图片 
startDraw(); // 3. 开始绘制

具体流程:

  1. renderDatas函数的作用是将已有的绘制数据(submitData)转换成canvas坐标,
  2. 并调用drawSaveArea方法将其渲染到saveCanvas上,
  3. 该函数遍历submitData中的每个polygon对象,
  4. 根据ratio将其坐标值转换成canvas的坐标值,
  5. 然后调用drawSaveArea方法绘制该区域,
  6. 最终返回一个包含所有区域点坐标的数组drawedPoints,
  7. 最后,需要按顺序调用initCanvas() -> getImage() -> startDraw()等方法,分别完成初始化画布、加载图片和开始绘制的功能。

十二. 全部代码

全部的VueJS代码和原生JavaScript代码,请打赏后,直接点我头像,私我,获取全部代码。

创作不易,感觉有用,就一键三连,感谢(●'◡'●)

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北城笑笑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值