当前位置:   article > 正文

Canvas绘制图片和区域(前端使用Canvas绘制图片,并在图片上绘制区域)_h5 canvas如何绘画区域

h5 canvas如何绘画区域

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


先看效果


如何使用Canvas在图片上绘制区域?

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

  1. initCanvas() {
  2. // 初始化canvas画布
  3. let canvasWrap = document.getElementsByClassName("canvas-wrap");
  4. this.wrapWidth = canvasWrap[0].clientWidth;
  5. this.wrapHeight = canvasWrap[0].clientHeight;
  6. this.imgCanvas = document.getElementById("imgCanvas");
  7. this.imgCtx = this.imgCanvas.getContext("2d");
  8. // 绘制canvas
  9. this.drawCanvas = document.getElementById("drawCanvas");
  10. this.drawCtx = this.drawCanvas.getContext("2d");
  11. // 保存绘制区域 saveCanvas
  12. this.saveCanvas = document.getElementById("saveCanvas");
  13. this.saveCtx = this.saveCanvas.getContext("2d");
  14. }
  1. imgCanvas用于绘制原始图片
  2. drawCanvas用于临时绘制区域
  3. saveCanvas用于保存最终绘制的区域


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

  1. initImgCanvas() {
  2. // 计算宽高比
  3. let ww = this.wrapWidth; // 画布宽度
  4. let wh = this.wrapHeight; // 画布高度
  5. let iw = this.imgWidth; // 图片宽度
  6. let ih = this.imgHeight; // 图片高度
  7. if (iw / ih < ww / wh) {
  8. // 以高为主
  9. this.ratio = ih / wh;
  10. this.canvasHeight = wh;
  11. this.canvasWidth = (wh * iw) / ih;
  12. } else {
  13. // 以宽为主
  14. this.ratio = iw / ww;
  15. this.canvasWidth = ww;
  16. this.canvasHeight = (ww * ih) / iw;
  17. }
  18. // 初始化画布大小
  19. this.imgCanvas.width = this.canvasWidth;
  20. this.imgCanvas.height = this.canvasHeight;
  21. this.drawCanvas.width = this.canvasWidth;
  22. this.drawCanvas.height = this.canvasHeight;
  23. this.saveCanvas.width = this.canvasWidth;
  24. this.saveCanvas.height = this.canvasHeight;
  25. // 图片加载绘制
  26. let img = document.createElement("img");
  27. img.src = this.imgUrl;
  28. img.onload = () => {
  29. console.log("图片已加载");
  30. this.imgCtx.drawImage(img, 0, 0, this.canvasWidth, this.canvasHeight);
  31. this.renderDatas(); // 渲染原有数据
  32. };
  33. }

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


开始绘制

三. 绘制的主要逻辑

  1. startDraw() {
  2. // 绘制区域
  3. if (this.isDrawing) return;
  4. this.isDrawing = true;
  5. // 绘制逻辑
  6. this.drawCanvas.addEventListener("click", this.drawImageClickFn);
  7. this.drawCanvas.addEventListener("dblclick", this.drawImageDblClickFn);
  8. this.drawCanvas.addEventListener("mousemove", this.drawImageMoveFn);
  9. }

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


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

  1. drawImageClickFn(e) {
  2. let drawCtx = this.drawCtx;
  3. if (e.offsetX || e.layerX) {
  4. let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
  5. let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
  6. let lastPoint = this.drawingPoints[this.drawingPoints.length - 1] || [];
  7. if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
  8. this.drawingPoints.push([pointX, pointY]);
  9. }
  10. }
  11. }

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


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

  1. drawImageMoveFn(e) {
  2. let drawCtx = this.drawCtx;
  3. if (e.offsetX || e.layerX) {
  4. let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
  5. let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
  6. // 绘制
  7. drawCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
  8. // 绘制点
  9. drawCtx.fillStyle = "blue";
  10. this.drawingPoints.forEach((item, i) => {
  11. drawCtx.beginPath();
  12. drawCtx.arc(...item, 6, 0, 180);
  13. drawCtx.fill(); //填充
  14. });
  15. // 绘制动态区域
  16. drawCtx.save();
  17. drawCtx.beginPath();
  18. this.drawingPoints.forEach((item, i) => {
  19. drawCtx.lineTo(...item);
  20. });
  21. drawCtx.lineTo(pointX, pointY);
  22. drawCtx.lineWidth = "3";
  23. drawCtx.strokeStyle = "blue";
  24. drawCtx.fillStyle = "rgba(255, 0, 0, 0.3)";
  25. drawCtx.stroke();
  26. drawCtx.fill(); //填充
  27. drawCtx.restore();
  28. }
  29. }

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


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

  1. drawImageDblClickFn(e) {
  2. let drawCtx = this.drawCtx;
  3. let saveCtx = this.saveCtx;
  4. if (e.offsetX || e.layerX) {
  5. let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
  6. let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
  7. let lastPoint = this.drawingPoints[this.drawingPoints.length - 1] || [];
  8. if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
  9. this.drawingPoints.push([pointX, pointY]);
  10. }
  11. }
  12. // 清空绘制图层
  13. drawCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
  14. // 绘制区域至保存图层
  15. this.drawSaveArea(this.drawingPoints);
  16. this.drawedPoints.push(this.drawingPoints);
  17. this.drawingPoints = [];
  18. this.isDrawing = false;
  19. // 绘制结束逻辑
  20. this.drawCanvas.removeEventListener("click", this.drawImageClickFn);
  21. this.drawCanvas.removeEventListener("dblclick", this.drawImageDblClickFn);
  22. this.drawCanvas.removeEventListener("mousemove", this.drawImageMoveFn);
  23. }

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


七. 遍历区域点坐标的方法

  1. drawSaveArea(points) {
  2. if (points.length === 0) return;
  3. this.saveCtx.save();
  4. this.saveCtx.beginPath();
  5. points.forEach((item, i) => {
  6. this.saveCtx.lineTo(...item);
  7. });
  8. this.saveCtx.closePath();
  9. this.saveCtx.lineWidth = "2";
  10. this.saveCtx.fillStyle = "rgba(255,0, 255, 0.3)";
  11. this.saveCtx.strokeStyle = "red";
  12. this.saveCtx.stroke();
  13. this.saveCtx.fill();
  14. this.saveCtx.restore();
  15. }

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

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


保存和渲染数据

八. 保存数据:我们需要将绘制的区域数据保存下来,以及从已有数据中渲染出区域

  1. savePoints() {
  2. // 将画布坐标数据转换成提交数据
  3. let objectPoints = [];
  4. objectPoints = this.drawedPoints.map((area) => {
  5. let polygon = {};
  6. area.forEach((point, i) => {
  7. polygon[`x${i + 1}`] = Math.round(point[0] * this.ratio);
  8. polygon[`y${i + 1}`] = Math.round(point[1] * this.ratio);
  9. });
  10. return {
  11. polygon: polygon,
  12. };
  13. });
  14. this.submitData = objectPoints;
  15. console.log("最终提交数据", objectPoints);
  16. }

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


九. 渲染数据

  1. renderDatas() {
  2. // 将提交数据数据转换成画布坐标
  3. this.drawedPoints = this.submitData.map((item) => {
  4. let polygon = item.polygon;
  5. let points = [];
  6. for (let i = 1; i < Object.keys(polygon).length / 2 + 1; i++) {
  7. if (!isNaN(polygon[`x${i}`]) && !isNaN(polygon[`y${i}`])) {
  8. points.push([
  9. polygon[`x${i}`] / this.ratio,
  10. polygon[`y${i}`] / this.ratio,
  11. ]);
  12. }
  13. }
  14. this.drawSaveArea(points);
  15. return points;
  16. });
  17. }

渲染数据的逻辑是,遍历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实现,可以改成像下面这样

  1. let canvasWrap, wrapWidth, wrapHeight, imgCanvas, imgCtx, drawCanvas, drawCtx, saveCanvas, saveCtx;
  2. let ratio, canvasWidth, canvasHeight, imgWidth, imgHeight, imgUrl;
  3. let isDrawing = false;
  4. let drawingPoints = [];
  5. let drawedPoints = [];
  6. let submitData = [];
  7. // 1. 初始化Canvas画布
  8. function initCanvas() {
  9. // 获取canvas容器元素并设置宽高
  10. canvasWrap = document.getElementsByClassName("canvas-wrap")[0];
  11. wrapWidth = canvasWrap.clientWidth;
  12. wrapHeight = canvasWrap.clientHeight;
  13. // 获取canvas元素并获取2D绘图上下文
  14. imgCanvas = document.getElementById("imgCanvas");
  15. imgCtx = imgCanvas.getContext("2d");
  16. drawCanvas = document.getElementById("drawCanvas");
  17. drawCtx = drawCanvas.getContext("2d");
  18. saveCanvas = document.getElementById("saveCanvas");
  19. saveCtx = saveCanvas.getContext("2d");
  20. }
  21. // 2. 初始化图片Canvas
  22. function initImgCanvas() {
  23. // 计算画布和图片的宽高比
  24. let ww = wrapWidth;
  25. let wh = wrapHeight;
  26. let iw = imgWidth;
  27. let ih = imgHeight;
  28. if (iw / ih < ww / wh) {
  29. ratio = ih / wh;
  30. canvasHeight = wh;
  31. canvasWidth = (wh * iw) / ih;
  32. } else {
  33. ratio = iw / ww;
  34. canvasWidth = ww;
  35. canvasHeight = (ww * ih) / iw;
  36. }
  37. // 设置三个canvas的宽高
  38. imgCanvas.width = canvasWidth;
  39. imgCanvas.height = canvasHeight;
  40. drawCanvas.width = canvasWidth;
  41. drawCanvas.height = canvasHeight;
  42. saveCanvas.width = canvasWidth;
  43. saveCanvas.height = canvasHeight;
  44. // 加载图片并绘制到imgCanvas上
  45. let img = document.createElement("img");
  46. img.src = imgUrl;
  47. img.onload = () => {
  48. console.log("图片已加载");
  49. imgCtx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
  50. renderDatas(); // 渲染已有数据
  51. };
  52. }
  53. // 3. 开始绘制
  54. function startDraw() {
  55. if (isDrawing) return;
  56. isDrawing = true;
  57. // 监听drawCanvas的click、dblclick和mousemove事件
  58. drawCanvas.addEventListener("click", drawImageClickFn);
  59. drawCanvas.addEventListener("dblclick", drawImageDblClickFn);
  60. drawCanvas.addEventListener("mousemove", drawImageMoveFn);
  61. }
  62. // 4. 清空所有绘制区域
  63. function clearAll() {
  64. saveCtx.clearRect(0, 0, canvasWidth, canvasHeight);
  65. drawedPoints = [];
  66. }
  67. // 5. 获取并加载图片
  68. function getImage() {
  69. imgUrl = "需要渲染的图片地址";
  70. imgWidth = 200;
  71. imgHeight = 300;
  72. imgUrl && initImgCanvas();
  73. }
  74. // 6. 点击事件,记录点坐标
  75. function drawImageClickFn(e) {
  76. if (e.offsetX || e.layerX) {
  77. let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
  78. let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
  79. let lastPoint = drawingPoints[drawingPoints.length - 1] || [];
  80. if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
  81. drawingPoints.push([pointX, pointY]);
  82. }
  83. }
  84. }
  85. // 7. 鼠标移动事件,实时绘制区域
  86. function drawImageMoveFn(e) {
  87. if (e.offsetX || e.layerX) {
  88. let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
  89. let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
  90. drawCtx.clearRect(0, 0, canvasWidth, canvasHeight);
  91. drawCtx.fillStyle = "blue";
  92. drawingPoints.forEach((item, i) => {
  93. drawCtx.beginPath();
  94. drawCtx.arc(...item, 6, 0, 180);
  95. drawCtx.fill();
  96. });
  97. drawCtx.save();
  98. drawCtx.beginPath();
  99. drawingPoints.forEach((item, i) => {
  100. drawCtx.lineTo(...item);
  101. });
  102. drawCtx.lineTo(pointX, pointY);
  103. drawCtx.lineWidth = "3";
  104. drawCtx.strokeStyle = "blue";
  105. drawCtx.fillStyle = "rgba(255, 0, 0, 0.3)";
  106. drawCtx.stroke();
  107. drawCtx.fill();
  108. drawCtx.restore();
  109. }
  110. }
  111. // 8. 双击事件,完成当前区域绘制
  112. function drawImageDblClickFn(e) {
  113. if (e.offsetX || e.layerX) {
  114. let pointX = e.offsetX == undefined ? e.layerX : e.offsetX;
  115. let pointY = e.offsetY == undefined ? e.layerY : e.offsetY;
  116. let lastPoint = drawingPoints[drawingPoints.length - 1] || [];
  117. if (lastPoint[0] !== pointX || lastPoint[1] !== pointY) {
  118. drawingPoints.push([pointX, pointY]);
  119. }
  120. }
  121. drawCtx.clearRect(0, 0, canvasWidth, canvasHeight);
  122. drawSaveArea(drawingPoints);
  123. drawedPoints.push(drawingPoints);
  124. drawingPoints = [];
  125. isDrawing = false;
  126. drawCanvas.removeEventListener("click", drawImageClickFn);
  127. drawCanvas.removeEventListener("dblclick", drawImageDblClickFn);
  128. drawCanvas.removeEventListener("mousemove", drawImageMoveFn);
  129. }
  130. // 9. 绘制区域到saveCanvas
  131. function drawSaveArea(points) {
  132. if (points.length === 0) return;
  133. saveCtx.save();
  134. saveCtx.beginPath();
  135. points.forEach((item, i) => {
  136. saveCtx.lineTo(...item);
  137. });
  138. saveCtx.closePath();
  139. saveCtx.lineWidth = "2";
  140. saveCtx.fillStyle = "rgba(255,0, 255, 0.3)";
  141. saveCtx.strokeStyle = "red";
  142. saveCtx.stroke();
  143. saveCtx.fill();
  144. saveCtx.restore();
  145. }
  146. // 10. 保存绘制数据
  147. function savePoints() {
  148. let objectPoints = [];
  149. objectPoints = drawedPoints.map((area) => {
  150. let polygon = {};
  151. area.forEach((point, i) => {
  152. polygon[`x${i + 1}`] = Math.round(point[0] * ratio);
  153. polygon[`y${i + 1}`] = Math.round(point[1] * ratio);
  154. });
  155. return {
  156. polygon: polygon,
  157. };
  158. });
  159. submitData = objectPoints;
  160. console.log("最终提交数据", objectPoints);
  161. }
  162. // 11. 渲染已有数据
  163. function renderDatas() {
  164. drawedPoints = submitData.map((item) => {
  165. let polygon = item.polygon;
  166. let points = [];
  167. for (let i = 1; i < Object.keys(polygon).length / 2 + 1; i++) {
  168. if (!isNaN(polygon[`x${i}`]) && !isNaN(polygon[`y${i}`])) {
  169. points.push([
  170. polygon[`x${i}`] / ratio, // 根据ratio换算canvas坐标
  171. polygon[`y${i}`] / ratio,
  172. ]);
  173. }
  174. }
  175. drawSaveArea(points); // 调用drawSaveArea将区域绘制到saveCanvas上
  176. return points;
  177. });
  178. }
  179. // 使用方式
  180. initCanvas(); // 1. 初始化Canvas画布
  181. getImage(); // 5. 获取并加载图片
  182. startDraw(); // 3. 开始绘制

renderDatas函数的作用是将已有的绘制数据(submitData)转换成canvas坐标,

并调用drawSaveArea方法将其渲染到saveCanvas上。

该函数遍历submitData中的每个polygon对象,

根据ratio将其坐标值转换成canvas的坐标值,

然后调用drawSaveArea方法绘制该区域。

最终返回一个包含所有区域点坐标的数组drawedPoints

最后,需要按顺序调用initCanvas() -> getImage() -> startDraw()等方法,分别完成初始化画布、加载图片和开始绘制的功能。

十二. 全部代码

全部的vuejs代码和原生js代码直接点我头像,私我,获取全部代码。
 

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/622283
推荐阅读
相关标签
  

闽ICP备14008679号