前言
动手前,分析一下截图功能的实现过程:
-
根据图片大小创建一个
canvas
画布,并将原始图片绘制到canvas
上; -
在画布上添加蒙层,以方便后续区分原始图片和裁剪图片;
-
在蒙层上方,对裁剪区域(鼠标移动形成的矩形范围)进行图片绘制;
-
获取裁剪区域的数据,并将数据绘制到另一个
canvas
上。
实现过程
编写 HTML 结构
首先,编写所需的 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
事件,该事件在读取操作成功后会立刻执行,在此处就可以获取图片的宽高。
// 初始化逻辑
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; // 将图片放入容器元素
// 等图片加载完成后,即可获取图片的宽高
imageBox.onload = function () {
const imgWidth = this.width, imgHeight = this.height;
console.log(imgWidth, imgHeight);
}
}
if (imgFile) {
// 以 DataURL 的形式读取文件,读取完成后才会获取 result 属性
reader.readAsDataURL(imgFile);
}
}
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; // 将图片放入容器元素
imageBox.onload = function () {
// 等图片加载完成后,即可获取图片的宽高
const imgWidth = this.width, imgHeight = this.height;
console.log(imgWidth, imgHeight);
// 创建 canvas1 画布并绘制图片
generateCanvas(canvasContainer1, canvas1, imgWidth, imgHeight);
ctx.drawImage(imageBox, 0, 0, imgWidth, imgHeight);
}
}
if (imgFile) {
// 以 DataURL 的形式读取文件,读取完成后才能获取 result 属性
reader.readAsDataURL(imgFile);
}
}
// 根据 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
事件。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];
// 绘制前,进行清除操作
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);
}
关于 drawImage 方法的参数,可以参考官网文档。
然后,当我们松开鼠标(结束截图)时,除了注销对 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) {
// 获取裁剪区域的数据
const data = ctx.getImageData(...screenshotData);
// 创建 canvas2 画布
generateCanvas(canvasContainer2, canvas2, screenshotData[2], screenshotData[3]);
// 绘制前,进行清除操作
ctx2.clearRect(...screenshotData);
// 将裁剪区域的数据绘制到 canvas2 画布上
ctx2.putImageData(data, 0, 0);
}
经过上述步骤,就可以实现我们所需的效果:
总结
前端新手记录一下自己的学习过程,欢迎大家指出问题或提出建议。