實現Web端自定義截屏(JS版)

前言

前幾天我發佈了一個web端自定義截圖的插件,在使用過程當中有開發者反饋這個插件沒法在vue2項目中使用,因而,我就開始找問題,發現個人插件是基於Vue3的開發的,因爲Vue3的插件和Vue2的插件徹底不兼容,所以插件也就只能在Vue3項目中使用。javascript

通過一番考慮後,我決定用原生js來重構這個插件,讓其不依賴任何庫,這樣它就能運行在任意一臺支持js的設備上,本文就跟你們分享下我重構這個插件的過程,歡迎各位感興趣的開發者閱讀本文。css

運行結果視頻:實現web端自定義截屏html

寫在前面

本文不講解插件的具體實現思路,對插件實現思路感興趣的開發者請移步:實現Web端自定義截屏vue

搭建開發環境

我想使用ts、scss、eslint、prettier來提高插件的可維護性,又嫌麻煩,不想手動配置webpack環境,因而我決定使用Vue CLI來搭建插件開發環境。java

本文不細講Vue CLI搭建插件開發環境的過程,對此感興趣的開發者請移步:使用CLI開發一個Vue3的npm庫webpack

移除vue相關依賴

咱們搭建好插件的開發環境後,CLI默認會在package.json中添加Vue的相關包,咱們的插件不會依賴於vue,所以咱們把它刪除便可。git

{
- "vue": "^3.0.0-0",
- "vue-class-component": "^8.0.0-0"
}

複製代碼

建立DOM

爲了方便開發者使用dom,這裏選擇使用js動態來建立dom,最後將其掛載到body中,在vue3版本的截圖插件中,咱們可使用vue組件來輔助咱們,這裏咱們就要基於組件來使用js來建立對應的dom,爲其綁定對應的事件。github

部分實現代碼以下,完整代碼請移步:CreateDom.tsweb

import toolbar from "@/lib/config/Toolbar";
import { toolbarType } from "@/lib/type/ComponentType";
import { toolClickEvent } from "@/lib/split-methods/ToolClickEvent";
import { setBrushSize } from "@/lib/common-methords/SetBrushSize";
import { selectColor } from "@/lib/common-methords/SelectColor";
import { getColor } from "@/lib/common-methords/GetColor";

export default class CreateDom {
  // 截圖區域canvas容器
  private readonly screenShortController: HTMLCanvasElement;
  // 截圖工具欄容器
  private readonly toolController: HTMLDivElement;
  // 繪製選項頂部ico容器
  private readonly optionIcoController: HTMLDivElement;
  // 畫筆繪製選項容器
  private readonly optionController: HTMLDivElement;
  // 文字工具輸入容器
  private readonly textInputController: HTMLDivElement;

  // 截圖工具欄圖標
  private readonly toolbar: Array<toolbarType>;
  
    constructor() {
    this.screenShortController = document.createElement("canvas");
    this.toolController = document.createElement("div");
    this.optionIcoController = document.createElement("div");
    this.optionController = document.createElement("div");
    this.textInputController = document.createElement("div");
    // 爲全部dom設置id
    this.setAllControllerId();
    // 爲畫筆繪製選項角標設置class
    this.setOptionIcoClassName();
    this.toolbar = toolbar;
    // 渲染工具欄
    this.setToolBarIco();
    // 渲染畫筆相關選項
    this.setBrushSelectPanel();
    // 渲染文本輸入
    this.setTextInputPanel();
    // 渲染頁面
    this.setDomToBody();
    // 隱藏全部dom
    this.hiddenAllDom();
  }
  
  /** 其餘代碼省略 **/
  
}
複製代碼

插件入口文件

在開發vue插件時咱們須要暴露一個install方法,因爲此處咱們不須要依賴vue,咱們就無需暴露install方法,個人預想效果是:用戶在使用我插件時,直接實例化插件就能正常運行。typescript

所以,咱們默認暴露出一個class,不管是使用script標籤引入插件,仍是在其餘js框架裏使用import來引入插件,都只須要在使用時new一下便可。

部分代碼以下,完整代碼請移步:main.ts

import CreateDom from "@/lib/main-entrance/CreateDom";
// 導入截圖所需樣式
import "@/assets/scss/screen-short.scss";
import InitData from "@/lib/main-entrance/InitData";
import {
  cutOutBoxBorder,
  drawCutOutBoxReturnType,
  movePositionType,
  positionInfoType,
  zoomCutOutBoxReturnType
} from "@/lib/type/ComponentType";
import { drawMasking } from "@/lib/split-methods/DrawMasking";
import { fixedData, nonNegativeData } from "@/lib/common-methords/FixedData";
import { drawPencil, initPencil } from "@/lib/split-methods/DrawPencil";
import { drawText } from "@/lib/split-methods/DrawText";
import { drawRectangle } from "@/lib/split-methods/DrawRectangle";
import { drawCircle } from "@/lib/split-methods/DrawCircle";
import { drawLineArrow } from "@/lib/split-methods/DrawLineArrow";
import { drawMosaic } from "@/lib/split-methods/DrawMosaic";
import { drawCutOutBox } from "@/lib/split-methods/DrawCutOutBox";
import { zoomCutOutBoxPosition } from "@/lib/common-methords/ZoomCutOutBoxPosition";
import { saveBorderArrInfo } from "@/lib/common-methords/SaveBorderArrInfo";
import { calculateToolLocation } from "@/lib/split-methods/CalculateToolLocation";

export default class ScreenShort {
  // 當前實例的響應式data數據
  private readonly data: InitData;

  // video容器用於存放屏幕MediaStream流
  private readonly videoController: HTMLVideoElement;
  // 截圖區域canvas容器
  private readonly screenShortController: HTMLCanvasElement | null;
  // 截圖工具欄dom
  private readonly toolController: HTMLDivElement | null;
  // 截圖圖片存放容器
  private readonly screenShortImageController: HTMLCanvasElement;
  // 截圖區域畫布
  private screenShortCanvas: CanvasRenderingContext2D | undefined;
  // 文本區域dom
  private readonly textInputController: HTMLDivElement | null;
  // 截圖工具欄畫筆選項dom
  private optionController: HTMLDivElement | null;
  private optionIcoController: HTMLDivElement | null;
  // 圖形位置參數
  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 borderOption: number | null = null;

  // 點擊裁剪框時的鼠標座標
  private movePosition: movePositionType = {
    moveStartX: 0,
    moveStartY: 0
  };

  // 鼠標點擊狀態
  private clickFlag = false;
  private fontSize = 17;
  // 最大可撤銷次數
  private maxUndoNum = 15;
  // 馬賽克塗抹區域大小
  private degreeOfBlur = 5;

  // 文本輸入框位置
  private textInputPosition: { mouseX: number; mouseY: number } = {
    mouseX: 0,
    mouseY: 0
  };
  constructor() {
    // 建立dom
    new CreateDom();
    this.videoController = document.createElement("video");
    this.videoController.autoplay = true;
    this.screenShortImageController = document.createElement("canvas");
    // 實例化響應式data
    this.data = new InitData();
    // 獲取截圖區域canvas容器
    this.screenShortController = this.data.getScreenShortController() as HTMLCanvasElement | null;
    this.toolController = this.data.getToolController() as HTMLDivElement | null;
    this.textInputController = this.data.getTextInputController() as HTMLDivElement | null;
    this.optionController = this.data.getOptionController() as HTMLDivElement | null;
    this.optionIcoController = this.data.getOptionIcoController() as HTMLDivElement | null;
    this.load();
  }
  
  /** 其餘代碼省略 **/
}
複製代碼

對外暴露default屬性

作完上述配置後咱們的插件開發環境就搭建好了,我執行build命令打包插件後,在vue2項目中使用import形式正常運行,在使用script標籤時引入時卻報錯了,因而我將暴露出來的screenShotPlugin變量打印出來後發現他還有個default屬性,default屬性纔是咱們插件暴露出來的東西。

求助了下我朋友@_Dreams找到了解決方案,須要配置下webpack中的output.libraryExport屬性,咱們的插件是使用Vue CLI開發的,有關webpack的配置須要在須要在vue.config.js中進行配置,代碼以下:

module.exports = {
    // 自定義webpack配置
  configureWebpack: {
    output: {
      // 對外暴露default屬性
      libraryExport: "default"
    }
  }
}
複製代碼

這一塊的配置在Vue CLI文檔中也有被提到,感興趣的開發者請移步:build-targets.html#vue-vs-js-ts-entry-files

使用webrtc截取整個屏幕

插件一開始使用的是html2canvas來將dom轉換爲canvas的,由於他要遍歷整個body中的dom,而後再轉換成canvas,並且圖片還不能跨域,若是頁面中圖片一多,它會變得很是慢。

上一篇文章的評論區中有位開發者 @名字什麼的都不重要 建議我使用webrtc來替代html2canvas,因而我就看了下webrtc的相關文檔,最終實現了截屏功能,它截取出來的東西更精確、性能更好,不存在卡頓問題也不存在css問題,並且它把選擇權交給了用戶,讓用戶決定來共享屏幕的那一部份內容。

實現思路

接下來就跟你們分享下個人實現思路:

  • 使用getDisplayMedia來捕獲屏幕,獲得MediaStream
  • 將獲得的MediaStream流輸出到video標籤中
  • 使用canvas將video標籤中的內容繪製到canvas容器中

有關getDisplayMedia的具體用法,請移步:使用屏幕捕獲API

實現代碼

接下來,咱們來看下具體的實現代碼,完整代碼請移步:main.ts

// 加載截圖組件
  private load() {
    // 設置截圖區域canvas寬高
    this.data.setScreenShortInfo(window.innerWidth, window.innerHeight);
    // 設置截圖圖片存放容器寬高
    this.screenShortImageController.width = window.innerWidth;
    this.screenShortImageController.height = window.innerHeight;
    // 顯示截圖區域容器
    this.data.showScreenShortPanel();
    // 截取整個屏幕
    this.screenShot();
  }

  // 開始捕捉屏幕
  private startCapture = async () => {
    let captureStream = null;

    try {
      // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
      // @ts-ignore
      // 捕獲屏幕
      captureStream = await navigator.mediaDevices.getDisplayMedia();
      // 將MediaStream輸出至video標籤
      this.videoController.srcObject = captureStream;
    } catch (err) {
      throw "瀏覽器不支持webrtc" + err;
    }
    return captureStream;
  };

  // 中止捕捉屏幕
  private stopCapture = () => {
    const srcObject = this.videoController.srcObject;
    if (srcObject && "getTracks" in srcObject) {
      const tracks = srcObject.getTracks();
      tracks.forEach(track => track.stop());
      this.videoController.srcObject = null;
    }
  };

  // 截屏
  private screenShot = () => {
    // 開始捕捉屏幕
    this.startCapture().then(() => {
      setTimeout(() => {
        // 獲取截圖區域canvas容器畫布
        const context = this.screenShortController?.getContext("2d");
        if (context == null || this.screenShortController == null) return;

        // 賦值截圖區域canvas畫布
        this.screenShortCanvas = context;
        // 繪製蒙層
        drawMasking(context);
        // 將獲取到的屏幕截圖繪製到圖片容器裏
        this.screenShortImageController
          .getContext("2d")
          ?.drawImage(
            this.videoController,
            0,
            0,
            this.screenShortImageController?.width,
            this.screenShortImageController?.height
          );
        // 添加監聽
        this.screenShortController?.addEventListener(
          "mousedown",
          this.mouseDownEvent
        );
        this.screenShortController?.addEventListener(
          "mousemove",
          this.mouseMoveEvent
        );
        this.screenShortController?.addEventListener(
          "mouseup",
          this.mouseUpEvent
        );
        // 中止捕捉屏幕
        this.stopCapture();
      }, 300);
    });
  };
複製代碼

插件地址

至此,插件的實現過程就分享完畢了。

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注😊
  • 本文首發於掘金,未經許可禁止轉載💌
相關文章
相關標籤/搜索