基于 Canvas 实现截图功能

2,351 阅读5分钟

前言

动手前,分析一下截图功能的实现过程:

  1. 根据图片大小创建一个 canvas 画布,并将原始图片绘制到 canvas 上;

  2. 在画布上添加蒙层,以方便后续区分原始图片和裁剪图片;

  3. 在蒙层上方,对裁剪区域(鼠标移动形成的矩形范围)进行图片绘制;

  4. 获取裁剪区域的数据,并将数据绘制到另一个 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();

image.png

接下来,创建一个自适应图片大小的 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 画布
}

image.png

到此,已经成功绘制了原始图片,接下来开始绘制裁剪区域。

绘制裁剪区域

接下来,需要监听 imageBox 容器(原始图片)上的相关事件,这些事件及其作用如下:

  • mousedown 事件:开始截图,监听 mousemovemouseup 事件。
  • mousemove 事件:监听鼠标的偏移量,以计算裁剪区域的宽度和高度。
  • mouseup 事件:结束截图,注销监听 mousedownmousemove 事件,绘制裁剪区域。
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 方法中,已经获取了裁剪区域的宽高。接下来,我们需要在原始图片上展示刚刚裁剪出来的区域,也就是这个效果:

image.png

可以看到,原始图片的上方、裁剪区域的下方有一层半透明黑色蒙层,这样就可以区分原始图片和裁剪图片。所以我们需要在绘制裁剪图片之前,先添加蒙层。

注意,在已有内容的 canvas 画布上再次绘制前,需要先清除整个画布的内容。这里通过 clearRect 方法清除 canvas1 画布上的所有内容。

接下来,继续补充 handleMouseMovehandleMouseUp 函数中的逻辑:

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 方法的参数,可以参考官网文档

然后,当我们松开鼠标(结束截图)时,除了注销对 mousedownmousemove 事件的监听,还需要将裁剪区域的内容绘制到另一个 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);
}

经过上述步骤,就可以实现我们所需的效果:

image.png

总结

前端新手记录一下自己的学习过程,欢迎大家指出问题或提出建议。