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

avatar
作者
筋斗云
阅读量:1

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


那么具体如何使用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实现,可以改成像下面这样

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代码和原生js代码直接点我头像,私我,1¥,获取全部代码。
 

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

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!