实现Web端自定义截屏(上)

简介: 实现Web端自定义截屏(上)

前言


当客户在使用我们的产品过程中,遇到问题需要向我们反馈时,如果用纯文字的形式描述,我们很难懂客户的意思,要是能配上问题截图,这样我们就能很清楚的知道客户的问题了。


那么,我们就需要为我们的产品实现一个自定义截屏的功能,用户点完"截图"按钮后,框选任意区域,随后在框选的区域内进行圈选、画箭头、马赛克、直线、打字等操作,做完操作后用户可以选择保存框选区域的内容到本地或者直接发送给我们。

聪明的开发者可能已经猜到了,这是QQ/微信的截图功能,我的开源项目正好做到了截图功能,在做之前我找了很多资料,没有发现web端有这种东西存在,于是我就决定参照QQ的截图自己实现一个并做成插件供大家使用。

本文就跟大家分享下我在做这个"自定义截屏功能"时的实现思路以及过程,欢迎各位感兴趣的开发者阅读本文。


写在前面


本文插件的写法采用的是Vue3的compositionAPI,如果对其不了解的开发者请移步我的另一篇文章:使用Vue3的CompositionAPI来优化代码量


实现思路


我们先来看下QQ的截屏流程,进而分析它是怎么实现的。


截屏流程分析


我们先来分析下,截屏时的具体流程。


  • 点击截屏按钮后,我们会发现页面上所有动态效果都静止不动了,如下所示。


640.jpg

  • 随后,我们按住鼠标左键进行拖动,屏幕上会出现黑色蒙板,鼠标的拖动区域会出现镂空效果,如下所示(此处图片过大,无法展示请移步原文查看)


  • 完成拖拽后,框选区域的下方会出现工具栏,里面有框选、圈选、箭头、直线、画笔等工具,如下图所示。


640.png

                                 image-20210201142541572


  • 点击工具栏中任意一个图标,会出现画笔选择区域,在这里可以选择画笔大小、颜色如下所示。

640.png

  • 随后,我们在框选的区域内进行拖拽就会绘制出对应的图形,如下所示。


640.png


                       image-20210201144004992  


  • 最后,点击截图工具栏的下载图标即可将图片保存至本地,或者点击对号图片会自动粘贴到聊天输入框,如下所示。


640.png

截屏实现思路


通过上述截屏流程,我们便得到了下述实现思路:


  • 获取当前可视区域的内容,将其存储起来
  • 为整个cnavas画布绘制蒙层
  • 在获取到的内容中进行拖拽,绘制镂空选区
  • 选择截图工具栏的工具,选择画笔大小等信息
  • 在选区内拖拽绘制对应的图形
  • 将选区内的内容转换为图片


实现过程


我们分析出了实现思路,接下来我们将上述思路逐一进行实现。


获取当前可视区域内容


当点击截图按钮后,我们需要获取整个可视区域的内容,后续所有的操作都是在获取的内容上进行的,在web端我们可以使用canvas来实现这些操作。


那么,我们就需要先将body区域的内容转换为canvas,如果要从零开始实现这个转换,有点复杂而且工作量很大。


还好在前端社区中有个开源库叫html2canvas可以实现将指定dom转换为canvas,我们就采用这个库来实现我们的转换。


接下来,我们来看下具体实现过程:


新建一个名为screen-short.vue的文件,用于承载我们的整个截图组件。


  • 首先我们需要一个canvas容器来显示转换后的可视区域内容


<template>
  <teleport to="body">
    <!--截图区域-->
    <canvas
      id="screenShotContainer"
      :width="screenShortWidth"
      :height="screenShortHeight"
      ref="screenShortController"
    ></canvas>
  </teleport>
</template>


此处只展示了部分代码,完整代码请移步:screen-short.vue


  • 在组件挂载时,调用html2canvas提供的方法,将body中的内容转换为canvas,存储起来。


import html2canvas from "html2canvas";
import InitData from "@/module/main-entrance/InitData";
export default class EventMonitoring {
  // 当前实例的响应式data数据
  private readonly data: InitData;
  // 截图区域canvas容器
  private screenShortController: Ref<HTMLCanvasElement | null>;
  // 截图图片存放容器
  private screenShortImageController: HTMLCanvasElement | undefined;
  constructor(props: Record<string, any>, context: SetupContext<any>) {
    // 实例化响应式data
    this.data = new InitData();
    // 获取截图区域canvas容器
    this.screenShortController = this.data.getScreenShortController();
    onMounted(() => {
      // 设置截图区域canvas宽高
      this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
      html2canvas(document.body, {}).then(canvas => {
        // 装载截图的dom为null则退出
        if (this.screenShortController.value == null) return;
        // 存放html2canvas截取的内容
        this.screenShortImageController = canvas;
      })
    })
  }
}


此处只展示了部分代码,完整代码请移步:EventMonitoring.ts


为canvas画布绘制蒙层


我们拿到了转换后的dom后,我们就需要绘制一个透明度为0.6的黑色蒙层,告知用户你现在处于截屏区域选区状态。


具体实现过程如下:


  • 创建DrawMasking.ts文件,蒙层的绘制逻辑在此文件中实现,代码如下。


/**
 * 绘制蒙层
 * @param context 需要进行绘制canvas
 */
export function drawMasking(context: CanvasRenderingContext2D) {
  // 清除画布
  context.clearRect(0, 0, window.innerWidth, window.innerHeight);
  // 绘制蒙层
  context.save();
  context.fillStyle = "rgba(0, 0, 0, .6)";
  context.fillRect(0, 0, window.innerWidth, window.innerHeight);
  // 绘制结束
  context.restore();
}


⚠️注释已经写的很详细了,对上述API不懂的开发者请移步:clearRect、save、fillStyle、fillRect、restore


  • html2canvas函数回调中调用绘制蒙层函数


html2canvas(document.body, {}).then(canvas => {
  // 获取截图区域画canvas容器画布
  const context = this.screenShortController.value?.getContext("2d");
  if (context == null) return;
  // 绘制蒙层
  drawMasking(context);
})


绘制镂空选区


我们在黑色蒙层中拖拽时,需要获取鼠标按下时的起始点坐标以及鼠标移动时的坐标,根据起始点坐标和移动时的坐标,我们就可以得到一个区域,此时我们将这块区域的蒙层凿开,将获取到的canvas图片内容绘制到蒙层下方,这样我们就实现了镂空选区效果。


整理下上述话语,思路如下:


  • 监听鼠标按下、移动、抬起事件
  • 获取鼠标按下、移动时的坐标
  • 根据获取到的坐标凿开蒙层
  • 将获取到的canvas图片内容绘制到蒙层下方
  • 实现镂空选区的拖拽与缩放


实现的效果如下:


640.gif


具体代码如下:


export default class EventMonitoring {
   // 当前实例的响应式data数据
  private readonly data: InitData;
  // 截图区域canvas容器
  private screenShortController: Ref<HTMLCanvasElement | null>;
  // 截图图片存放容器
  private screenShortImageController: HTMLCanvasElement | undefined;
  // 截图区域画布
  private screenShortCanvas: CanvasRenderingContext2D | undefined;
  // 图形位置参数
  private drawGraphPosition: positionInfoType = {
    startX: 0,
    startY: 0,
    width: 0,
    height: 0
  };
  // 临时图形位置参数
  private tempGraphPosition: positionInfoType = {
    startX: 0,
    startY: 0,
    width: 0,
    height: 0
  };
  // 裁剪框边框节点坐标事件
  private cutOutBoxBorderArr: Array<cutOutBoxBorder> = [];
  // 裁剪框顶点边框直径大小
  private borderSize = 10;
  // 当前操作的边框节点
  private borderOption: number | null = null;
  // 点击裁剪框时的鼠标坐标
  private movePosition: movePositionType = {
    moveStartX: 0,
    moveStartY: 0
  };
  // 裁剪框修剪状态
  private draggingTrim = false;
  // 裁剪框拖拽状态
  private dragging = false;
  // 鼠标点击状态
  private clickFlag = false;
  constructor(props: Record<string, any>, context: SetupContext<any>) {
     // 实例化响应式data
    this.data = new InitData();
    // 获取截图区域canvas容器
    this.screenShortController = this.data.getScreenShortController();
    onMounted(() => {
      // 设置截图区域canvas宽高
      this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
      html2canvas(document.body, {}).then(canvas => {
        // 装载截图的dom为null则退出
        if (this.screenShortController.value == null) return;
        // 存放html2canvas截取的内容
        this.screenShortImageController = canvas;
        // 获取截图区域画canvas容器画布
        const context = this.screenShortController.value?.getContext("2d");
        if (context == null) return;
        // 赋值截图区域canvas画布
        this.screenShortCanvas = context;
        // 绘制蒙层
        drawMasking(context);
        // 添加监听
        this.screenShortController.value?.addEventListener(
          "mousedown",
          this.mouseDownEvent
        );
        this.screenShortController.value?.addEventListener(
          "mousemove",
          this.mouseMoveEvent
        );
        this.screenShortController.value?.addEventListener(
          "mouseup",
          this.mouseUpEvent
        );
      })
    })
  }
  // 鼠标按下事件
  private mouseDownEvent = (event: MouseEvent) => {
    this.dragging = true;
    this.clickFlag = true;
    const mouseX = nonNegativeData(event.offsetX);
    const mouseY = nonNegativeData(event.offsetY);
    // 如果操作的是裁剪框
    if (this.borderOption) {
      // 设置为拖动状态
      this.draggingTrim = true;
      // 记录移动时的起始点坐标
      this.movePosition.moveStartX = mouseX;
      this.movePosition.moveStartY = mouseY;
    } else {
      // 绘制裁剪框,记录当前鼠标开始坐标
      this.drawGraphPosition.startX = mouseX;
      this.drawGraphPosition.startY = mouseY;
    }
  }
  // 鼠标移动事件
  private mouseMoveEvent = (event: MouseEvent) => {
    this.clickFlag = false;
    // 获取裁剪框位置信息
    const { startX, startY, width, height } = this.drawGraphPosition;
    // 获取当前鼠标坐标
    const currentX = nonNegativeData(event.offsetX);
    const currentY = nonNegativeData(event.offsetY);
    // 裁剪框临时宽高
    const tempWidth = currentX - startX;
    const tempHeight = currentY - startY;
    // 执行裁剪框操作函数
    this.operatingCutOutBox(
      currentX,
      currentY,
      startX,
      startY,
      width,
      height,
      this.screenShortCanvas
    );
    // 如果鼠标未点击或者当前操作的是裁剪框都return
    if (!this.dragging || this.draggingTrim) return;
    // 绘制裁剪框
    this.tempGraphPosition = drawCutOutBox(
      startX,
      startY,
      tempWidth,
      tempHeight,
      this.screenShortCanvas,
      this.borderSize,
      this.screenShortController.value as HTMLCanvasElement,
      this.screenShortImageController as HTMLCanvasElement
    ) as drawCutOutBoxReturnType;
  }
    // 鼠标抬起事件
  private mouseUpEvent = () => {
    // 绘制结束
    this.dragging = false;
    this.draggingTrim = false;
    // 保存绘制后的图形位置信息
    this.drawGraphPosition = this.tempGraphPosition;
    // 如果工具栏未点击则保存裁剪框位置
    if (!this.data.getToolClickStatus().value) {
      const { startX, startY, width, height } = this.drawGraphPosition;
      this.data.setCutOutBoxPosition(startX, startY, width, height);
    }
    // 保存边框节点信息
    this.cutOutBoxBorderArr = saveBorderArrInfo(
      this.borderSize,
      this.drawGraphPosition
    );
  }
}


⚠️绘制镂空选区的代码较多,此处仅仅展示了鼠标的三个事件监听的相关代码,完整代码请移步:EventMonitoring.ts


  • 绘制裁剪框的代码如下


/**
 * 绘制裁剪框
 * @param mouseX 鼠标x轴坐标
 * @param mouseY 鼠标y轴坐标
 * @param width 裁剪框宽度
 * @param height 裁剪框高度
 * @param context 需要进行绘制的canvas画布
 * @param borderSize 边框节点直径
 * @param controller 需要进行操作的canvas容器
 * @param imageController 图片canvas容器
 * @private
 */
export function drawCutOutBox(
  mouseX: number,
  mouseY: number,
  width: number,
  height: number,
  context: CanvasRenderingContext2D,
  borderSize: number,
  controller: HTMLCanvasElement,
  imageController: HTMLCanvasElement
) {
  // 获取画布宽高
  const canvasWidth = controller?.width;
  const canvasHeight = controller?.height;
  // 画布、图片不存在则return
  if (!canvasWidth || !canvasHeight || !imageController || !controller) return;
  // 清除画布
  context.clearRect(0, 0, canvasWidth, canvasHeight);
  // 绘制蒙层
  context.save();
  context.fillStyle = "rgba(0, 0, 0, .6)";
  context.fillRect(0, 0, canvasWidth, canvasHeight);
  // 将蒙层凿开
  context.globalCompositeOperation = "source-atop";
  // 裁剪选择框
  context.clearRect(mouseX, mouseY, width, height);
  // 绘制8个边框像素点并保存坐标信息以及事件参数
  context.globalCompositeOperation = "source-over";
  context.fillStyle = "#2CABFF";
  // 像素点大小
  const size = borderSize;
  // 绘制像素点
  context.fillRect(mouseX - size / 2, mouseY - size / 2, size, size);
  context.fillRect(
    mouseX - size / 2 + width / 2,
    mouseY - size / 2,
    size,
    size
  );
  context.fillRect(mouseX - size / 2 + width, mouseY - size / 2, size, size);
  context.fillRect(
    mouseX - size / 2,
    mouseY - size / 2 + height / 2,
    size,
    size
  );
  context.fillRect(
    mouseX - size / 2 + width,
    mouseY - size / 2 + height / 2,
    size,
    size
  );
  context.fillRect(mouseX - size / 2, mouseY - size / 2 + height, size, size);
  context.fillRect(
    mouseX - size / 2 + width / 2,
    mouseY - size / 2 + height,
    size,
    size
  );
  context.fillRect(
    mouseX - size / 2 + width,
    mouseY - size / 2 + height,
    size,
    size
  );
  // 绘制结束
  context.restore();
  // 使用drawImage将图片绘制到蒙层下方
  context.save();
  context.globalCompositeOperation = "destination-over";
  context.drawImage(
    imageController,
    0,
    0,
    controller?.width,
    controller?.height
  );
  context.restore();
  // 返回裁剪框临时位置信息
  return {
    startX: mouseX,
    startY: mouseY,
    width: width,
    height: height
  };
}


相关文章
|
4月前
|
安全 前端开发 API
【Azure 应用服务】Azure Web App 服务默认支持一些 Weak TLS Ciphers Suite,是否有办法自定义修改呢?
【Azure 应用服务】Azure Web App 服务默认支持一些 Weak TLS Ciphers Suite,是否有办法自定义修改呢?
|
4月前
|
JavaScript PHP 开发者
PHP中的异常处理与自定义错误处理器构建高效Web应用:Node.js与Express框架实战指南
【8月更文挑战第27天】在PHP编程世界中,异常处理和错误管理是代码健壮性的关键。本文将深入探讨PHP的异常处理机制,并指导你如何创建自定义错误处理器,以便优雅地管理运行时错误。我们将一起学习如何使用try-catch块捕获异常,以及如何通过set_error_handler函数定制错误响应。准备好让你的代码变得更加可靠,同时提供更友好的错误信息给最终用户。
|
7月前
|
JavaScript 前端开发 架构师
Web Components:自定义元素与Shadow DOM的实践
Web Components是用于创建可重用自定义HTML元素的技术集合,包括Custom Elements、Shadow DOM、HTML Templates和Slots。通过Custom Elements定义新元素,利用Shadow DOM封装私有样式,&lt;slot&gt;元素允许插入内容。自定义元素支持事件处理和属性观察,可复用且样式隔离。它们遵循Web标准,兼容各前端框架,注重性能优化,如懒加载和Shadow DOM优化。
76 0
|
Web App开发 存储 安全
大师学SwiftUI第17章Part1 - Web内容访问及自定义Safari视图控制器
App可以让用户访问网页,但实现的方式有不止一种。我们可以让用户通过链接在浏览器中打开文档、在应用界面中内嵌一个预定义的浏览器或是在后台下载并处理数据。
130 0
|
Web App开发 JavaScript 前端开发
Web组件规范和自定义元素
Web组件规范和自定义元素
157 0
|
搜索推荐 JavaScript 数据可视化
数据可视化大屏高德地图javascript webAPI开发的智慧治安物联网管理系统实战解析(web GIS、3D视图、个性化地图、标注、涟漪动画、自定义弹窗、3D控件)
数据可视化大屏高德地图javascript webAPI开发的智慧治安物联网管理系统实战解析(web GIS、3D视图、个性化地图、标注、涟漪动画、自定义弹窗、3D控件)
535 0
|
JSON 前端开发 API
layui框架实战案例(8):web图片裁切插件croppers.js组件实现上传图片的自定义截取(含php后端)
layui框架实战案例(8):web图片裁切插件croppers.js组件实现上传图片的自定义截取(含php后端)
583 0
|
JavaScript 前端开发 算法
web前端面试高频考点——Vue组件间的通信及高级特性(多种组件间的通信、自定义v-model、nextTick、插槽)
web前端面试高频考点——Vue组件间的通信及高级特性(多种组件间的通信、自定义v-model、nextTick、插槽)
153 0
|
缓存 NoSQL 前端开发
【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(三)路由、自定义校验器和 Redis
【Go】基于 Gin 从0到1搭建 Web 管理后台系统后端服务(三)路由、自定义校验器和 Redis
|
消息中间件 网络协议 前端开发
SpringBoot轻松整合WebSocket,实现Web在线聊天室
前面为大家讲述了 Spring Boot的整合Redis、RabbitMQ、Elasticsearch等各种框架组件;随着移动互联网的发展,服务端消息数据推送已经是一个非常重要、非常普遍的基础功能。今天就和大家聊聊在SpringBoot轻松整合WebSocket,实现Web在线聊天室,希望能对大家有所帮助。
SpringBoot轻松整合WebSocket,实现Web在线聊天室