- A+
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
思路分析
在开始动手之前,分析一下整个功能的实现过程:
-
根据图片大小创建
canvas1
画布,并将原图片直接定位在canvas1
上; -
在画布上添加一个蒙层,以区分当前
canvas
图像是被裁剪的原图像; -
在蒙层上方,对裁剪区域(鼠标移动形成的矩形范围)再次进行图像绘制;
-
获取裁剪区域的数据,并将该数据定位到另一个
canvas
画布上。
实现过程
准备工作
首先,编写所需的 HTML
结构并获取对应元素。
<body> <!-- 上传文件 --> <input type="file" id="imageFile" accept="image/*"> <!-- 保存被裁剪的原图像,初始样式需要设置 display: none --> <div class="canvasContainer1"> <canvas id="canvas1"></canvas> </div> <!-- 保存裁剪区域的图像,初始样式需要设置 display: none --> <div class="canvasContainer2"> <canvas id="canvas2"></canvas> </div> </body> <script> const imageFile = document.querySelector('#imageFile'); const canvasContainer1 = document.querySelector('.canvasContainer1'); const canvasContainer2 = document.querySelector('.canvasContainer2'); const canvas1 = document.querySelector('#canvas1'); const canvas2 = document.querySelector('#canvas2'); const ctx = canvas1.getContext('2d'); const ctx2 = canvas2.getContext('2d'); const imageBox = new Image(); // 创建一个存放图片的容器 </script>
绘制原图像
我们需要监听 input
元素的 change
事件,以获取上传图片的相关参数,这里主要是为了获取图片的宽度和高度。
我们创建一个 FileReader() 对象并监听其 load
事件。load
事件在读取操作成功后立刻执行,在这个方法中我们就可以获取图片的宽高。
function init() { imageFile.addEventListener('change', handleFileChange, false); // 监听图片上传事件。 } function handleFileChange(e) { const imgFile = e.target.files[0]; // 获取上传的图片对象。 const reader = new FileReader(); reader.onload = function(e) { const imgSrc = e.target.result; // 图片文件的 base64 编码格式。 imageBox.src = imgSrc; // 把图片放入 img 容器。 // 等图片加载完成后,获取图片的宽高。 imageBox.onload = function () { const imgWidth = this.width, imgHeight = this.height; console.log(imgWidth, imgHeight); } } if (imgFile) { reader.readAsDataURL(imgFile); // 读取图片文件,读取完成才能获取 result 属性。 } } init();
此时还没有图片,我们创建一个自适应图片大小的 canvas1
画布,并使用 drawImage()
方法将上传的图片直接定位到 canvas1
当中。
function handleFileChange(e) { const imgFile = e.target.files[0]; // 获取上传的图片对象。 const reader = new FileReader(); reader.onload = function (e) { const imgSrc = e.target.result; // 图片的 base64 编码。 imageBox.src = imgSrc; // 把上传的图像放入 img 容器。 // 图片加载完毕后执行 imageBox.onload = function () { // 获取图片的宽高。 const imgWidth = this.width, imgHeight = this.height; console.log(imgWidth, imgHeight); // 创建 canvas 画布并绘制图片。 generateCanvas(canvasContainer1, canvas1, imgWidth, imgHeight); ctx.drawImage(imageBox, 0, 0, imgWidth, imgHeight); } } if (imgFile) { reader.readAsDataURL(imgFile); // 将当前file读取成DataURL } } // 根据 width 和 height 创建 canvas 画布。 function generateCanvas(container, canvas, width, height) { container.width = width + 'px'; container.height = height + 'px'; canvas.width = width; canvas.height = height; container.style.display = 'block'; // 显示 canvas 区域。 }
可以看到原图像已经成功被绘制,接下来就可以开始动态绘制截图区域了。
绘制截图区域
在这个过程中,我们需要分别监听 imageBox
容器(原图像)上的 mousedown
、mousemove
和 mouseup
事件,这些事件的作用如下:
mousedown
事件:记录开始截图的位置,并开始监听mousemove
和mouseup
事件。mousemove
事件:监听鼠标的偏移量,以计算裁剪区域的宽度和高度。mouseup
事件:截图结束,注销监听mousedown
和mousemove
事件,并绘制裁剪区域。
let startPosition = []; // 记录鼠标点击(开始截图)的位置。 let screenshotData = []; // 保存截取部分的相关信息。 function init() { // 监听鼠标点击事件。 canvas1.addEventListener('mousedown', handleMouseDown, false); } // 记录鼠标点击(开始截图)的位置,并监听相关事件。 function handleMouseDown(e) { startPosition = [e.offsetX, e.offsetY]; canvas1.addEventListener('mousemove', handleMouseMove, false); canvas1.addEventListener('mouseup', handleMouseUp, false); } // 监听鼠标的偏移量,以计算裁剪区域的宽度和高度。 function handleMouseMove(e) { // 获取裁剪区域的宽度和高度。 const { offsetX, offsetY } = e; const [startX, startY] = startPosition; const [rectWidth, rectHeight] = [offsetX - startX, offsetY - startY]; console.log('rect', rectWidth, rectHeight); // 保存裁剪区域的相关信息。 screenshotData = [startX, startY, rectWidth, rectHeight]; } // 注销监听事件等后续操作。 function handleMouseUp() { canvas1.removeEventListener('mousemove', handleMouseMove, false); canvas1.removeEventListener('mouseup', handleMouseUp, false); }
在 handleMouseMove
函数中,我们已经获取了裁剪区域的宽高,也就是生成截图的宽高。
接下来,我们需要在原图像上展示出我们所裁剪的区域,也就是这个效果:
可以看到,原图像的上方、裁剪区域下方会覆盖一层半透明黑色蒙层,它的作用是区分原图层和裁剪部分图层。所以我们需要在绘制截图区域之前,添加一层蒙层。
注意,在已有内容的 canvas
画布上进行再次绘制之前,需要先清除整个画布的内容。 这里通过 clearRect()
方法清除 canvas1
画布上的所有内容,并添加蒙层。
我们继续来补充 handleMouseMove
和 handleMouseUp
函数中的逻辑:
const MASKER_OPACITY = 0.4; function handleMouseMove(e) { // 获取裁剪区域的宽度和高度。 const { offsetX, offsetY } = e; const [startX, startY] = startPosition; const [rectWidth, rectHeight] = [offsetX - startX, offsetY - startY]; console.log('rect', rectWidth, rectHeight); // 保存裁剪区域的相关信息。 screenshotData = [startX, startY, rectWidth, rectHeight]; // 再次绘制前,清理 canvas1 画布上的内容。 const { width, height } = canvas1; ctx.clearRect(0, 0, width, height); // 在 canvas1 画布上绘制蒙层。 drawImageMasker(0, 0, width, height, MASKER_OPACITY); // 绘制截图区域。 drawScreenShot(width, height, rectWidth, rectHeight); } // ... // 绘制图片蒙层,填充范围和颜色,以便区分原图层和裁剪部分图层。 function drawImageMasker(x, y, width, height, opacity) { ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`; ctx.fillRect(0, 0, width, height); } // 绘制裁剪的矩形区域。 function drawScreenShot(canWidth, canHeight, rectWidth, rectHeight) { // 在源图像外绘制新图像,只有源图像外的目标图像部分会被显示,源图像是透明的。 ctx.globalCompositeOperation = 'destination-out'; ctx.fillStyle = '#2c2c2c'; ctx.fillRect(...startPosition, rectWidth, rectHeight); // 在现有画布上绘制新的图形。 ctx.globalCompositeOperation = 'destination-over'; ctx.drawImage(imageBox, 0, 0, canWidth, canHeight, 0, 0, canWidth, canHeight); }
然后,当我们放开鼠标(结束截图动作)时,除了注销对 mousedown
和 mousemove
事件的监听,还需要将所得的裁剪区域的图像放入另一个 canvas
中。
在绘制新图像的过程中,我们需要使用以下方法:
- getImageData():读取
canvas
上的内容,返回一个ImageData
对象,包含了每个像素的信息。 - putImageData():将
ImagaData
对象的数据放入canvas
中,覆盖canvas
中的已有图像。
function handleMouseUp() { canvas1.removeEventListener('mousemove', handleMouseMove, false); canvas1.removeEventListener('mouseup', handleMouseUp, false); // 开始绘制截图区域图片。 drawScreenshotImage(screenshotData); // 如果裁剪得到新图像后,不希望保留原图像,可以设置以下属性。 // canvasContainer1.style.display = 'none'; } // 在新容器 canvas2 上绘制新图像。 function drawScreenshotImage(screenshotData) { // 获取 canvas1 的数据。 const data = ctx.getImageData(...screenshotData); // 创建 canvas2 画布。 generateCanvas(canvasContainer2, canvas2, screenshotData[2], screenshotData[3]); // 每次绘制前,都先进行清除操作。 ctx2.clearRect(...screenshotData); // 将 canvas1 的数据放入 canvas2 中。 ctx2.putImageData(data, 0, 0); }
经过以上步骤,就可以实现我们所需的效果