简介:在开发中,有时我们需要在图片上进行一些交互式操作,比如绘制区域、标记等。这种场景下,我们可以使用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"); },},
imgCanvas
用于绘制原始图片drawCanvas
用于临时绘制区域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
上监听click
、dblclick
和mousemove
事件,分别对应点击、双击和鼠标移动三种绘制交互。
四. 点击事件,用于开始一个新的区域绘制
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
数组中,用于保存所有已绘制的区域数据。然后,重置drawingPoints
和isDrawing
的状态,并移除所有绘制事件的监听器。
至此,一个区域的绘制就完成了。如果需要继续绘制新的区域,只需再次调用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上绘制图片区域的全部逻辑。可以根据具体需求进行相应的调整和扩展。
十. 执行过程
具体全部的执行顺序如下:
- 初始化Canvas
- 调用
initCanvas()
方法初始化三个Canvas画布 - 调用
initImgCanvas()
方法计算并设置画布宽高比例,加载并绘制图片
- 调用
- 开始绘制
- 调用
startDraw()
方法 - 监听
drawCanvas
的click
、dblclick
、mousemove
事件 - 点击时,在
drawImageClickFn
中记录点坐标 - 移动时,在
drawImageMoveFn
中实时绘制区域 - 双击时,在
drawImageDblClickFn
中完成当前区域绘制,保存至saveCanvas
- 调用
- 保存和渲染数据
- 调用
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. 开始绘制
具体流程:
renderDatas
函数的作用是将已有的绘制数据(submitData
)转换成canvas坐标,- 并调用
drawSaveArea
方法将其渲染到saveCanvas
上, - 该函数遍历
submitData
中的每个polygon
对象, - 根据
ratio
将其坐标值转换成canvas的坐标值, - 然后调用
drawSaveArea
方法绘制该区域, - 最终返回一个包含所有区域点坐标的数组
drawedPoints,
- 最后,需要按顺序调用
initCanvas()
->getImage()
->startDraw()
等方法,分别完成初始化画布、加载图片和开始绘制的功能。
十二. 全部代码
全部的vuejs代码和原生js代码直接点我头像,私我,1¥,获取全部代码。