├── .eslintignore ├── commitlint.config.js ├── public ├── demo-img.jpeg └── index.html ├── src ├── assets │ ├── img │ │ ├── brush.png │ │ ├── close.png │ │ ├── round.png │ │ ├── save.png │ │ ├── text.png │ │ ├── undo.png │ │ ├── confirm.png │ │ ├── square.png │ │ ├── mosaicPen.png │ │ ├── right-top.png │ │ ├── save-click.png │ │ ├── save-hover.png │ │ ├── text-click.png │ │ ├── text-hover.png │ │ ├── undo-hover.png │ │ ├── brush-click.png │ │ ├── brush-hover.png │ │ ├── close-hover.png │ │ ├── confirm-hover.png │ │ ├── round-click.png │ │ ├── round-hover.png │ │ ├── seperateLine.png │ │ ├── square-click.png │ │ ├── square-hover.png │ │ ├── undo-disabled.png │ │ ├── mosaicPen-click.png │ │ ├── mosaicPen-hover.png │ │ ├── right-top-click.png │ │ ├── right-top-hover.png │ │ ├── round-normal-big.png │ │ ├── round-normal-small.png │ │ ├── round-selected-big.png │ │ ├── PopoverPullDownArrow.png │ │ ├── round-normal-medium.png │ │ ├── round-selected-small.png │ │ └── round-selected-medium.png │ └── scss │ │ ├── global.scss │ │ └── screen-short.scss ├── shims-vue.d.ts ├── module │ ├── common-methords │ │ ├── SelectColor.ts │ │ ├── UpdateContainerMouseStyle.ts │ │ ├── GetBrushSelectedName.ts │ │ ├── CanvasPatch.ts │ │ ├── GetSelectedCalssName.ts │ │ ├── FixedData.ts │ │ ├── SetBrushSize.ts │ │ ├── SaveCanvasToImage.ts │ │ ├── GetColor.ts │ │ ├── SetSelectedClassName.ts │ │ ├── ImgScaling.ts │ │ ├── SaveCanvasToBase64.ts │ │ ├── ZoomCutOutBoxPosition.ts │ │ └── SaveBorderArrInfo.ts │ ├── split-methods │ │ ├── CalculateOptionIcoPosition.ts │ │ ├── DrawMasking.ts │ │ ├── BoundaryJudgment.ts │ │ ├── DrawRectangle.ts │ │ ├── DrawPencil.ts │ │ ├── CalculateToolLocation.ts │ │ ├── DrawCircle.ts │ │ ├── DrawText.ts │ │ ├── DrawLineArrow.ts │ │ ├── DrawMosaic.ts │ │ ├── DrawCutOutBox.ts │ │ └── DrawArrow.ts │ ├── config │ │ └── Toolbar.ts │ ├── type │ │ └── ComponentType.ts │ └── main-entrance │ │ ├── PlugInParameters.ts │ │ ├── InitData.ts │ │ └── EventMonitoring.ts ├── main.ts └── components │ └── screen-short.vue ├── .browserslistrc ├── .editorconfig ├── .prettierrc.json ├── babel.config.js ├── .gitignore ├── webstorm.config.js ├── .eslintrc.js ├── tsconfig.json ├── license ├── rollup-utils.js ├── package.json ├── README.md └── rollup.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-angular"] }; 2 | -------------------------------------------------------------------------------- /public/demo-img.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/public/demo-img.jpeg -------------------------------------------------------------------------------- /src/assets/img/brush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/brush.png -------------------------------------------------------------------------------- /src/assets/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/close.png -------------------------------------------------------------------------------- /src/assets/img/round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round.png -------------------------------------------------------------------------------- /src/assets/img/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/save.png -------------------------------------------------------------------------------- /src/assets/img/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/text.png -------------------------------------------------------------------------------- /src/assets/img/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/undo.png -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie < 8 5 | iOS 7 6 | last 3 iOS versions 7 | -------------------------------------------------------------------------------- /src/assets/img/confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/confirm.png -------------------------------------------------------------------------------- /src/assets/img/square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/square.png -------------------------------------------------------------------------------- /src/assets/img/mosaicPen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/mosaicPen.png -------------------------------------------------------------------------------- /src/assets/img/right-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/right-top.png -------------------------------------------------------------------------------- /src/assets/img/save-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/save-click.png -------------------------------------------------------------------------------- /src/assets/img/save-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/save-hover.png -------------------------------------------------------------------------------- /src/assets/img/text-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/text-click.png -------------------------------------------------------------------------------- /src/assets/img/text-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/text-hover.png -------------------------------------------------------------------------------- /src/assets/img/undo-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/undo-hover.png -------------------------------------------------------------------------------- /src/assets/img/brush-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/brush-click.png -------------------------------------------------------------------------------- /src/assets/img/brush-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/brush-hover.png -------------------------------------------------------------------------------- /src/assets/img/close-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/close-hover.png -------------------------------------------------------------------------------- /src/assets/img/confirm-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/confirm-hover.png -------------------------------------------------------------------------------- /src/assets/img/round-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round-click.png -------------------------------------------------------------------------------- /src/assets/img/round-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round-hover.png -------------------------------------------------------------------------------- /src/assets/img/seperateLine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/seperateLine.png -------------------------------------------------------------------------------- /src/assets/img/square-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/square-click.png -------------------------------------------------------------------------------- /src/assets/img/square-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/square-hover.png -------------------------------------------------------------------------------- /src/assets/img/undo-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/undo-disabled.png -------------------------------------------------------------------------------- /src/assets/img/mosaicPen-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/mosaicPen-click.png -------------------------------------------------------------------------------- /src/assets/img/mosaicPen-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/mosaicPen-hover.png -------------------------------------------------------------------------------- /src/assets/img/right-top-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/right-top-click.png -------------------------------------------------------------------------------- /src/assets/img/right-top-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/right-top-hover.png -------------------------------------------------------------------------------- /src/assets/img/round-normal-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round-normal-big.png -------------------------------------------------------------------------------- /src/assets/img/round-normal-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round-normal-small.png -------------------------------------------------------------------------------- /src/assets/img/round-selected-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round-selected-big.png -------------------------------------------------------------------------------- /src/assets/img/PopoverPullDownArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/PopoverPullDownArrow.png -------------------------------------------------------------------------------- /src/assets/img/round-normal-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round-normal-medium.png -------------------------------------------------------------------------------- /src/assets/img/round-selected-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round-selected-small.png -------------------------------------------------------------------------------- /src/assets/scss/global.scss: -------------------------------------------------------------------------------- 1 | .hidden-screen-shot-scroll { 2 | width: 100vw; 3 | height: 100vh; 4 | overflow: hidden; 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # 对所有文件有效 4 | [*] 5 | charset = utf-8 6 | tab_width = 2 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /src/assets/img/round-selected-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/screen-shot/HEAD/src/assets/img/round-selected-medium.png -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "endOfLine": "auto", 5 | "singleQuote": false, 6 | "semi": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | ie: "11" 8 | } 9 | } 10 | ] 11 | ], 12 | plugins: [] 13 | }; 14 | -------------------------------------------------------------------------------- /src/module/common-methords/SelectColor.ts: -------------------------------------------------------------------------------- 1 | import InitData from "@/module/main-entrance/InitData"; 2 | 3 | export function selectColor() { 4 | const data = new InitData(); 5 | // 显示颜色选择面板 6 | data.setColorPanelStatus(true); 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/module/common-methords/UpdateContainerMouseStyle.ts: -------------------------------------------------------------------------------- 1 | export function updateContainerMouseStyle( 2 | container: HTMLCanvasElement, 3 | toolName: string 4 | ) { 5 | switch (toolName) { 6 | case "text": 7 | container.style.cursor = "text"; 8 | break; 9 | default: 10 | container.style.cursor = "default"; 11 | break; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/module/common-methords/GetBrushSelectedName.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取画笔选项对应的选中时的class名 3 | * @param itemName 4 | */ 5 | export function getBrushSelectedName(itemName: number) { 6 | let className = ""; 7 | switch (itemName) { 8 | case 1: 9 | className = "brush-small-active"; 10 | break; 11 | case 2: 12 | className = "brush-medium-active"; 13 | break; 14 | case 3: 15 | className = "brush-big-active"; 16 | break; 17 | } 18 | return className; 19 | } 20 | -------------------------------------------------------------------------------- /src/module/split-methods/CalculateOptionIcoPosition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 计算截图工具栏画笔选项三角形角标位置 3 | * @param index 4 | */ 5 | export function calculateOptionIcoPosition(index: number) { 6 | switch (index) { 7 | case 1: 8 | return 24 - 8; 9 | case 2: 10 | return 24 * 2 + 8; 11 | case 3: 12 | return 24 * 4 - 6; 13 | case 4: 14 | return 24 * 5 + 8; 15 | case 5: 16 | return 24 * 7 + 6; 17 | case 6: 18 | return 24 * 9 - 6; 19 | default: 20 | return 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/module/config/Toolbar.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 1, 4 | title: "square" 5 | }, 6 | { 7 | id: 2, 8 | title: "round" 9 | }, 10 | { 11 | id: 3, 12 | title: "right-top" 13 | }, 14 | { 15 | id: 4, 16 | title: "brush" 17 | }, 18 | { 19 | id: 5, 20 | title: "mosaicPen" 21 | }, 22 | { 23 | id: 6, 24 | title: "text" 25 | }, 26 | { 27 | id: 7, 28 | title: "separateLine" 29 | }, 30 | { 31 | id: 8, 32 | title: "save" 33 | } 34 | ]; 35 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawMasking.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 绘制蒙层 3 | * @param context 需要进行绘制canvas 4 | * @param canvasWidth 5 | * @param canvasHeight 6 | */ 7 | export function drawMasking( 8 | context: CanvasRenderingContext2D, 9 | canvasWidth: number, 10 | canvasHeight: number 11 | ) { 12 | // 清除画布 13 | context.clearRect(0, 0, canvasWidth, canvasHeight); 14 | // 绘制蒙层 15 | context.save(); 16 | context.fillStyle = "rgba(0, 0, 0, .6)"; 17 | context.fillRect(0, 0, canvasWidth, canvasHeight); 18 | // 绘制结束 19 | context.restore(); 20 | } 21 | -------------------------------------------------------------------------------- /src/module/common-methords/CanvasPatch.ts: -------------------------------------------------------------------------------- 1 | // 获取canvas上下文对象,对高分屏进行修复 2 | export function getCanvas2dCtx( 3 | canvas: HTMLCanvasElement, 4 | width: number, 5 | height: number 6 | ) { 7 | // 获取设备像素比 8 | const dpr = window.devicePixelRatio || 1; 9 | canvas.width = Math.round(width * dpr); 10 | canvas.height = Math.round(height * dpr); 11 | canvas.style.width = width + "px"; 12 | canvas.style.height = height + "px"; 13 | const ctx = canvas.getContext("2d"); 14 | // 对画布进行缩放处理 15 | if (ctx) { 16 | ctx.scale(dpr, dpr); 17 | } 18 | return ctx; 19 | } 20 | -------------------------------------------------------------------------------- /webstorm.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const path = require("path"); 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, ".", dir); 7 | } 8 | 9 | module.exports = { 10 | context: path.resolve(__dirname, "./"), 11 | resolve: { 12 | extensions: [".js", ".vue", ".json"], 13 | alias: { 14 | "@": resolve("src"), 15 | "@views": resolve("src/views"), 16 | "@comp": resolve("src/components"), 17 | "@core": resolve("src/core"), 18 | "@utils": resolve("src/utils") 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/module/common-methords/GetSelectedCalssName.ts: -------------------------------------------------------------------------------- 1 | export function getSelectedClassName(index: number) { 2 | let className = ""; 3 | switch (index) { 4 | case 1: 5 | className = "square-active"; 6 | break; 7 | case 2: 8 | className = "round-active"; 9 | break; 10 | case 3: 11 | className = "right-top-active"; 12 | break; 13 | case 4: 14 | className = "brush-active"; 15 | break; 16 | case 5: 17 | className = "mosaicPen-active"; 18 | break; 19 | case 6: 20 | className = "text-active"; 21 | } 22 | return className; 23 | } 24 | -------------------------------------------------------------------------------- /src/module/split-methods/BoundaryJudgment.ts: -------------------------------------------------------------------------------- 1 | import { positionInfoType } from "@/module/type/ComponentType"; 2 | 3 | /** 4 | * 获取工具栏工具边界绘制状态 5 | * @param startX x轴绘制起点 6 | * @param startY y轴绘制起点 7 | * @param cutBoxPosition 裁剪框位置信息 8 | */ 9 | export function getDrawBoundaryStatus( 10 | startX: number, 11 | startY: number, 12 | cutBoxPosition: positionInfoType 13 | ): boolean { 14 | if ( 15 | startX < cutBoxPosition.startX || 16 | startY < cutBoxPosition.startY || 17 | startX > cutBoxPosition.startX + cutBoxPosition.width || 18 | startY > cutBoxPosition.startY + cutBoxPosition.height 19 | ) { 20 | // 无法绘制 21 | return false; 22 | } 23 | // 可以绘制 24 | return true; 25 | } 26 | -------------------------------------------------------------------------------- /src/module/common-methords/FixedData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 计算传进来的数据,不让其移出可视区域 3 | * @param data 需要计算的数据 4 | * @param trimDistance 裁剪框宽度 5 | * @param canvasDistance 画布宽度 6 | */ 7 | export function fixedData( 8 | data: number, 9 | trimDistance: number, 10 | canvasDistance: number 11 | ) { 12 | const nonNegativeData = function(data: number) { 13 | return data > 0 ? data : 0; 14 | }; 15 | 16 | if (nonNegativeData(data) + trimDistance > canvasDistance) { 17 | return nonNegativeData(canvasDistance - trimDistance); 18 | } else { 19 | return nonNegativeData(data); 20 | } 21 | } 22 | 23 | /** 24 | * 对参数进行处理,小于0则返回0 25 | */ 26 | export function nonNegativeData(data: number) { 27 | return data > 0 ? data : 0; 28 | } 29 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawRectangle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 绘制矩形 3 | * @param mouseX 4 | * @param mouseY 5 | * @param width 6 | * @param height 7 | * @param color 边框颜色 8 | * @param borderWidth 边框大小 9 | * @param context 需要进行绘制的canvas画布 10 | */ 11 | export function drawRectangle( 12 | mouseX: number, 13 | mouseY: number, 14 | width: number, 15 | height: number, 16 | color: string, 17 | borderWidth: number, 18 | context: CanvasRenderingContext2D 19 | ) { 20 | context.save(); 21 | // 设置边框颜色 22 | context.strokeStyle = color; 23 | // 设置边框大小 24 | context.lineWidth = borderWidth; 25 | context.beginPath(); 26 | // 绘制矩形 27 | context.rect(mouseX, mouseY, width, height); 28 | context.stroke(); 29 | // 绘制结束 30 | context.restore(); 31 | } 32 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "@vue/prettier", 11 | "@vue/prettier/@typescript-eslint" 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020 15 | }, 16 | "plugins": [ // 用到的插件 17 | "@typescript-eslint", 18 | "prettier" 19 | ], 20 | rules: { 21 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 22 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 23 | "prettier/prettier": "error", // prettier标记的地方抛出错误信息 24 | "spaced-comment": [2,"always"], // 注释后面必须写两个空格 25 | "@typescript-eslint/no-explicit-any": ["off"] // 关闭any校验 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/module/common-methords/SetBrushSize.ts: -------------------------------------------------------------------------------- 1 | import InitData from "@/module/main-entrance/InitData"; 2 | import { setSelectedClassName } from "@/module/common-methords/SetSelectedClassName"; 3 | 4 | /** 5 | * 设置画笔大小 6 | * @param size 7 | * @param index 8 | * @param mouseEvent 9 | */ 10 | export function setBrushSize( 11 | size: string, 12 | index: number, 13 | mouseEvent: MouseEvent 14 | ) { 15 | const data = new InitData(); 16 | // 为当前点击项添加选中时的class名 17 | setSelectedClassName(mouseEvent, index, true); 18 | let sizeNum = 2; 19 | switch (size) { 20 | case "small": 21 | sizeNum = 2; 22 | break; 23 | case "medium": 24 | sizeNum = 5; 25 | break; 26 | case "big": 27 | sizeNum = 10; 28 | break; 29 | } 30 | data.setPenSize(sizeNum); 31 | return sizeNum; 32 | } 33 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawPencil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 画笔绘制 3 | * @param context 4 | * @param mouseX 5 | * @param mouseY 6 | * @param size 7 | * @param color 8 | */ 9 | export function drawPencil( 10 | context: CanvasRenderingContext2D, 11 | mouseX: number, 12 | mouseY: number, 13 | size: number, 14 | color: string 15 | ) { 16 | // 开始绘制 17 | context.save(); 18 | // 设置边框大小 19 | context.lineWidth = size; 20 | // 设置边框颜色 21 | context.strokeStyle = color; 22 | context.lineTo(mouseX, mouseY); 23 | context.stroke(); 24 | // 绘制结束 25 | context.restore(); 26 | } 27 | 28 | /** 29 | * 画笔初始化 30 | */ 31 | export function initPencil( 32 | context: CanvasRenderingContext2D, 33 | mouseX: number, 34 | mouseY: number 35 | ) { 36 | // 开始||清空一条路径 37 | context.beginPath(); 38 | // 移动画笔位置 39 | context.moveTo(mouseX, mouseY); 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | ], 16 | "paths": { 17 | "@/*": [ 18 | "src/*" 19 | ] 20 | }, 21 | "declaration": true,// 是否生成声明文件 22 | "declarationDir": "dist/lib",// 声明文件打包的位置 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/module/common-methords/SaveCanvasToImage.ts: -------------------------------------------------------------------------------- 1 | import { getCanvas2dCtx } from "@/module/common-methords/CanvasPatch"; 2 | 3 | export function saveCanvasToImage( 4 | context: CanvasRenderingContext2D, 5 | startX: number, 6 | startY: number, 7 | width: number, 8 | height: number 9 | ) { 10 | // 获取设备像素比 11 | const dpr = window.devicePixelRatio || 1; 12 | // 获取裁剪框区域图片信息 13 | // 获取裁剪框区域图片信息 14 | const img = context.getImageData( 15 | startX * dpr, 16 | startY * dpr, 17 | width * dpr, 18 | height * dpr 19 | ); 20 | // 创建canvas标签,用于存放裁剪区域的图片 21 | const canvas = document.createElement("canvas"); 22 | // 获取裁剪框区域画布 23 | const imgContext = getCanvas2dCtx(canvas, width, height); 24 | if (imgContext) { 25 | // 将图片放进裁剪框内 26 | imgContext.putImageData(img, 0, 0); 27 | const a = document.createElement("a"); 28 | // 获取图片 29 | a.href = canvas.toDataURL("png"); 30 | // 下载图片 31 | a.download = `${new Date().getTime()}.png`; 32 | a.click(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/module/split-methods/CalculateToolLocation.ts: -------------------------------------------------------------------------------- 1 | import { positionInfoType } from "@/module/type/ComponentType"; 2 | 3 | /** 4 | * 计算截图工具栏位置 5 | * @param position 裁剪框位置信息 6 | * @param toolWidth 截图工具栏宽度 7 | * @param containerWidth 截图容器宽度 8 | */ 9 | export function calculateToolLocation( 10 | position: positionInfoType, 11 | toolWidth: number, 12 | containerWidth: number 13 | ) { 14 | // 工具栏X轴坐标 = (裁剪框的宽度 - 工具栏的宽度) / 2 + 裁剪框距离左侧的距离 15 | let mouseX = (position.width - toolWidth) / 2 + position.startX; 16 | // 工具栏超出画布左侧可视区域,进行位置修正 17 | if (mouseX < 0) mouseX = 0; 18 | 19 | // 计算工具栏在画布内的占用面积 20 | const toolSize = mouseX + toolWidth; 21 | // 工具栏超出画布右侧可视区域,进行位置修正 22 | if (toolSize > containerWidth) { 23 | mouseX = containerWidth - toolWidth; 24 | } 25 | // 工具栏Y轴坐标 26 | let mouseY = position.startY + position.height + 10; 27 | if (position.width < 0 && position.height < 0) { 28 | // 从右下角拖动时,工具条y轴的位置应该为position.startY + 10 29 | mouseY = position.startY + 10; 30 | } 31 | return { 32 | mouseX, 33 | mouseY 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/module/common-methords/GetColor.ts: -------------------------------------------------------------------------------- 1 | import InitData from "@/module/main-entrance/InitData"; 2 | 3 | export function getColor(index: number) { 4 | const data = new InitData(); 5 | let currentColor = "#F53440"; 6 | switch (index) { 7 | case 1: 8 | currentColor = "#F53440"; 9 | break; 10 | case 2: 11 | currentColor = "#F65E95"; 12 | break; 13 | case 3: 14 | currentColor = "#D254CF"; 15 | break; 16 | case 4: 17 | currentColor = "#12A9D7"; 18 | break; 19 | case 5: 20 | currentColor = "#30A345"; 21 | break; 22 | case 6: 23 | currentColor = "#FACF50"; 24 | break; 25 | case 7: 26 | currentColor = "#F66632"; 27 | break; 28 | case 8: 29 | currentColor = "#989998"; 30 | break; 31 | case 9: 32 | currentColor = "#000000"; 33 | break; 34 | case 10: 35 | currentColor = "#FEFFFF"; 36 | break; 37 | } 38 | data.setSelectedColor(currentColor); 39 | // 隐藏颜色选择面板 40 | data.setColorPanelStatus(false); 41 | return currentColor; 42 | } 43 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 screen-shot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/module/common-methords/SetSelectedClassName.ts: -------------------------------------------------------------------------------- 1 | import { getSelectedClassName } from "@/module/common-methords/GetSelectedCalssName"; 2 | import { getBrushSelectedName } from "@/module/common-methords/GetBrushSelectedName"; 3 | 4 | /** 5 | * 为当前点击项添加选中时的class,移除其兄弟元素选中时的class 6 | * @param mouseEvent 需要进行操作的元素 7 | * @param index 当前点击项 8 | * @param isOption 是否为画笔选项 9 | */ 10 | export function setSelectedClassName( 11 | mouseEvent: any, 12 | index: number, 13 | isOption: boolean 14 | ) { 15 | // 获取当前点击项选中时的class名 16 | let className = getSelectedClassName(index); 17 | if (isOption) { 18 | // 获取画笔选项选中时的对应的class 19 | className = getBrushSelectedName(index); 20 | } 21 | // 解决event 在火狐和Safari浏览上的兼容性问题 22 | const path = 23 | mouseEvent.path || (mouseEvent.composedPath && mouseEvent.composedPath()); 24 | // 获取div下的所有子元素 25 | const nodes = path[1].children; 26 | for (let i = 0; i < nodes.length; i++) { 27 | const item = nodes[i]; 28 | // 如果工具栏中已经有选中的class则将其移除 29 | if (item.className.includes("active")) { 30 | item.classList.remove(item.classList[2]); 31 | } 32 | } 33 | // 给当前点击项添加选中时的class 34 | mouseEvent.target.className += " " + className; 35 | } 36 | -------------------------------------------------------------------------------- /src/module/common-methords/ImgScaling.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 等比例缩放图片 3 | * @param imgWidth 4 | * @param imgHeight 5 | * @param containerWidth 6 | * @param containerHeight 7 | */ 8 | export function imgScaling( 9 | imgWidth: number, 10 | imgHeight: number, 11 | containerWidth: number, 12 | containerHeight: number 13 | ) { 14 | let [tempWidth, tempHeight] = [0, 0]; 15 | 16 | if (imgWidth > 0 && imgHeight > 0) { 17 | // 原图片宽高比例 大于 指定的宽高比例,这就说明了原图片的宽度必然 > 高度 18 | if (imgWidth / imgHeight >= containerWidth / containerHeight) { 19 | if (imgWidth > containerWidth) { 20 | tempWidth = containerWidth; 21 | // 按原图片的比例进行缩放 22 | tempHeight = (imgHeight * containerWidth) / imgWidth; 23 | } else { 24 | // 按照图片的大小进行缩放 25 | tempWidth = imgWidth; 26 | tempHeight = imgHeight; 27 | } 28 | } else { 29 | // 原图片的高度必然 > 宽度 30 | if (imgHeight > containerHeight) { 31 | tempHeight = containerHeight; 32 | // 按原图片的比例进行缩放 33 | tempWidth = (imgWidth * containerHeight) / imgHeight; 34 | } else { 35 | // 按原图片的大小进行缩放 36 | tempWidth = imgWidth; 37 | tempHeight = imgHeight; 38 | } 39 | } 40 | } 41 | 42 | return { tempWidth, tempHeight }; 43 | } 44 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawCircle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 绘制圆形 3 | * @param context 需要进行绘制的画布 4 | * @param mouseX 当前鼠标x轴坐标 5 | * @param mouseY 当前鼠标y轴坐标 6 | * @param mouseStartX 鼠标按下时的x轴坐标 7 | * @param mouseStartY 鼠标按下时的y轴坐标 8 | * @param borderWidth 边框宽度 9 | * @param color 边框颜色 10 | */ 11 | export function drawCircle( 12 | context: CanvasRenderingContext2D, 13 | mouseX: number, 14 | mouseY: number, 15 | mouseStartX: number, 16 | mouseStartY: number, 17 | borderWidth: number, 18 | color: string 19 | ) { 20 | // 坐标边界处理,解决反向绘制椭圆时的报错问题 21 | const startX = mouseX < mouseStartX ? mouseX : mouseStartX; 22 | const startY = mouseY < mouseStartY ? mouseY : mouseStartY; 23 | const endX = mouseX >= mouseStartX ? mouseX : mouseStartX; 24 | const endY = mouseY >= mouseStartY ? mouseY : mouseStartY; 25 | // 计算圆的半径 26 | const radiusX = (endX - startX) * 0.5; 27 | const radiusY = (endY - startY) * 0.5; 28 | // 计算圆心的x、y坐标 29 | const centerX = startX + radiusX; 30 | const centerY = startY + radiusY; 31 | // 开始绘制 32 | context.save(); 33 | context.beginPath(); 34 | context.lineWidth = borderWidth; 35 | context.strokeStyle = color; 36 | 37 | if (typeof context.ellipse === "function") { 38 | // 绘制圆,旋转角度与起始角度都为0,结束角度为2*PI 39 | context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI); 40 | } else { 41 | throw "你的浏览器不支持ellipse,无法绘制椭圆"; 42 | } 43 | context.stroke(); 44 | context.closePath(); 45 | // 结束绘制 46 | context.restore(); 47 | } 48 | -------------------------------------------------------------------------------- /src/module/common-methords/SaveCanvasToBase64.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将指定区域的canvas转换为base64格式的图片 3 | */ 4 | import { getCanvas2dCtx } from "@/module/common-methords/CanvasPatch"; 5 | 6 | export function saveCanvasToBase64( 7 | context: CanvasRenderingContext2D, 8 | startX: number, 9 | startY: number, 10 | width: number, 11 | height: number, 12 | quality = 0.75, 13 | writeBase64 = true 14 | ) { 15 | // 获取设备像素比 16 | const dpr = window.devicePixelRatio || 1; 17 | // 获取裁剪框区域图片信息 18 | const img = context.getImageData( 19 | startX * dpr, 20 | startY * dpr, 21 | width * dpr, 22 | height * dpr 23 | ); 24 | // 创建canvas标签,用于存放裁剪区域的图片 25 | const canvas = document.createElement("canvas"); 26 | // 获取裁剪框区域画布 27 | const imgContext = getCanvas2dCtx(canvas, width, height); 28 | if (imgContext) { 29 | // 将图片放进canvas中 30 | imgContext.putImageData(img, 0, 0); 31 | if (writeBase64) { 32 | // 将图片自动添加至剪贴板中 33 | canvas?.toBlob( 34 | blob => { 35 | if (blob == null) return; 36 | const Clipboard = window.ClipboardItem; 37 | // 浏览器不支持Clipboard 38 | if (Clipboard == null) return canvas.toDataURL("png"); 39 | const clipboardItem = new Clipboard({ 40 | [blob.type]: blob 41 | }); 42 | navigator.clipboard?.write([clipboardItem]).then(() => { 43 | return "写入成功"; 44 | }); 45 | }, 46 | "image/png", 47 | quality 48 | ); 49 | } 50 | return canvas.toDataURL("png"); 51 | } 52 | return ""; 53 | } 54 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue"; 2 | import screenShort from "@/components/screen-short.vue"; 3 | import PlugInParameters from "@/module/main-entrance/PlugInParameters"; 4 | import { screenShotType } from "@/module/type/ComponentType"; 5 | import "@/assets/scss/global.scss"; 6 | 7 | export default { 8 | install(app: App, options: screenShotType): void { 9 | const plugInParameters = new PlugInParameters(); 10 | if (options?.enableWebRtc != null) { 11 | plugInParameters.setWebRtcStatus(options.enableWebRtc); 12 | } 13 | 14 | if (options?.level != null) { 15 | plugInParameters.setLevel(options.level); 16 | } 17 | 18 | if (options?.clickCutFullScreen != null) { 19 | plugInParameters.setClickCutFullScreenStatus(options.clickCutFullScreen); 20 | } 21 | 22 | if (options?.hiddenToolIco) { 23 | plugInParameters.setHiddenToolIco(options.hiddenToolIco); 24 | } 25 | 26 | if (options?.enableCORS) { 27 | plugInParameters.setEnableCORSStatus(options.enableCORS); 28 | } 29 | 30 | if (options?.proxyAddress) { 31 | plugInParameters.setProxyAddress(options.proxyAddress); 32 | } 33 | if (options?.writeBase64 != null) { 34 | plugInParameters.setWriteImgState(options.writeBase64); 35 | } 36 | if (options?.hiddenScrollBar != null) { 37 | plugInParameters.setHiddenScrollBarInfo(options.hiddenScrollBar); 38 | } 39 | if (options?.wrcWindowMode != null) { 40 | plugInParameters.setWrcWindowMode(options.wrcWindowMode); 41 | } 42 | 43 | // 将截屏组件挂载到vue实例 44 | app.component(screenShort.name, screenShort); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/module/type/ComponentType.ts: -------------------------------------------------------------------------------- 1 | // 裁剪框节点事件定义 2 | export type cutOutBoxBorder = { 3 | x: number; 4 | y: number; 5 | width: number; 6 | height: number; 7 | index: number; // 样式 8 | option: number; // 操作 9 | }; 10 | 11 | // 鼠标起始位置坐标 12 | export type movePositionType = { 13 | moveStartX: number; 14 | moveStartY: number; 15 | }; 16 | 17 | // 裁剪框位置参数 18 | export type positionInfoType = { 19 | startX: number; 20 | startY: number; 21 | width: number; 22 | height: number; 23 | }; 24 | 25 | // 裁剪框缩放时所返回的数据类型 26 | export type zoomCutOutBoxReturnType = { 27 | tempStartX: number; 28 | tempStartY: number; 29 | tempWidth: number; 30 | tempHeight: number; 31 | }; 32 | 33 | // 绘制裁剪框所返回的数据类型 34 | export type drawCutOutBoxReturnType = { 35 | startX: number; 36 | startY: number; 37 | width: number; 38 | height: number; 39 | }; 40 | 41 | export type toolIcoType = { save?: boolean; undo?: boolean; confirm?: boolean }; 42 | 43 | export type screenShotType = { 44 | enableWebRtc?: boolean; // 是否启用webrtc,默认是启用状态 45 | level?: number; // 截图容器层级 46 | clickCutFullScreen?: boolean; // 单击截全屏启用状态, 默认值为false 47 | hiddenToolIco?: toolIcoType; // 需要隐藏的工具栏图标 48 | enableCORS?: boolean; // html2canvas截图时跨域启用状态 49 | proxyAddress?: string; // html2canvas截图时的图片服务器代理地址 50 | writeBase64?: boolean; // 是否将截图内容写入剪切板 51 | hiddenScrollBar?: hideBarInfoType; // 是否隐藏滚动条 52 | wrcWindowMode?: boolean; // 是否启用窗口截图模式,默认为当前标签页截图 53 | }; 54 | 55 | export type textInfoType = { 56 | positionX: number; 57 | positionY: number; 58 | color: string; 59 | size: number; 60 | }; 61 | 62 | export type hideBarInfoType = { 63 | state: boolean; 64 | color?: string; 65 | fillWidth?: number; 66 | fillHeight?: number; 67 | fillState?: boolean; 68 | }; 69 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawText.ts: -------------------------------------------------------------------------------- 1 | /* 获取文本内容的的换行部分*/ 2 | // const findBreakPoint = ( 3 | // text: string, 4 | // width: number, 5 | // context: CanvasRenderingContext2D 6 | // ) => { 7 | // let min = 0; 8 | // let max = text.length - 1; 9 | // 10 | // while (min <= max) { 11 | // const middle = Math.floor((min + max) / 2); 12 | // const middleWidth = context.measureText(text.substr(0, middle)).width; 13 | // const oneCharWiderThanMiddleWidth = context.measureText( 14 | // text.substr(0, middle + 1) 15 | // ).width; 16 | // if (middleWidth <= width && oneCharWiderThanMiddleWidth > width) { 17 | // return middle; 18 | // } 19 | // if (middleWidth < width) { 20 | // min = middle + 1; 21 | // } else { 22 | // max = middle - 1; 23 | // } 24 | // } 25 | // 26 | // return -1; 27 | // }; 28 | // 29 | // const breakLinesForCanvas = ( 30 | // text: string, 31 | // width: number, 32 | // context: CanvasRenderingContext2D 33 | // ) => { 34 | // const result = []; 35 | // let breakPoint = 0; 36 | // 37 | // while ((breakPoint = findBreakPoint(text, width, context)) !== -1) { 38 | // result.push(text.substr(0, breakPoint)); 39 | // text = text.substr(breakPoint); 40 | // } 41 | // 42 | // if (text) { 43 | // result.push(text); 44 | // } 45 | // 46 | // return result; 47 | // }; 48 | /** 49 | * 绘制文本 50 | * @param text 需要进行绘制的文字 51 | * @param mouseX 绘制位置的X轴坐标 52 | * @param mouseY 绘制位置的Y轴坐标 53 | * @param color 字体颜色 54 | * @param fontSize 字体大小 55 | * @param context 需要你行绘制的画布 56 | */ 57 | export function drawText( 58 | text: string, 59 | mouseX: number, 60 | mouseY: number, 61 | color: string, 62 | fontSize: number, 63 | context: CanvasRenderingContext2D 64 | ) { 65 | // 开始绘制 66 | context.save(); 67 | context.lineWidth = 1; 68 | // 设置字体颜色 69 | context.fillStyle = color; 70 | context.textBaseline = "middle"; 71 | context.font = `bold ${fontSize}px none`; 72 | context.fillText(text, mouseX, mouseY); 73 | // 结束绘制 74 | context.restore(); 75 | } 76 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawLineArrow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 绘制箭头 3 | * @param context 需要进行绘制的画布 4 | * @param mouseStartX 鼠标按下时的x轴坐标 P1 5 | * @param mouseStartY 鼠标按下式的y轴坐标 P1 6 | * @param mouseX 当前鼠标x轴坐标 P2 7 | * @param mouseY 当前鼠标y轴坐标 P2 8 | * @param theta 箭头斜线与直线的夹角角度 (θ) P3 ---> (P1、P2) || P4 ---> P1(P1、P2) 9 | * @param slashLength 箭头斜线的长度 P3 ---> P2 || P4 ---> P2 10 | * @param borderWidth 边框宽度 11 | * @param color 边框颜色 12 | */ 13 | export function drawLineArrow( 14 | context: CanvasRenderingContext2D, 15 | mouseStartX: number, 16 | mouseStartY: number, 17 | mouseX: number, 18 | mouseY: number, 19 | theta: number, 20 | slashLength: number, 21 | borderWidth: number, 22 | color: string 23 | ) { 24 | /** 25 | * 已知: 26 | * 1. P1、P2的坐标 27 | * 2. 箭头斜线(P3 || P4) ---> P2直线的长度 28 | * 3. 箭头斜线(P3 || P4) ---> (P1、P2)直线的夹角角度(θ) 29 | * 求: 30 | * P3、P4的坐标 31 | */ 32 | const angle = 33 | (Math.atan2(mouseStartY - mouseY, mouseStartX - mouseX) * 180) / Math.PI, // 通过atan2来获取箭头的角度 34 | angle1 = ((angle + theta) * Math.PI) / 180, // P3点的角度 35 | angle2 = ((angle - theta) * Math.PI) / 180, // P4点的角度 36 | topX = slashLength * Math.cos(angle1), // P3点的x轴坐标 37 | topY = slashLength * Math.sin(angle1), // P3点的y轴坐标 38 | botX = slashLength * Math.cos(angle2), // P4点的X轴坐标 39 | botY = slashLength * Math.sin(angle2); // P4点的Y轴坐标 40 | 41 | // 开始绘制 42 | context.save(); 43 | context.beginPath(); 44 | 45 | // P3的坐标位置 46 | let arrowX = mouseStartX - topX, 47 | arrowY = mouseStartY - topY; 48 | 49 | // 移动笔触到P3坐标 50 | context.moveTo(arrowX, arrowY); 51 | // 移动笔触到P1 52 | context.moveTo(mouseStartX, mouseStartY); 53 | // 绘制P1到P2的直线 54 | context.lineTo(mouseX, mouseY); 55 | // 计算P3的位置 56 | arrowX = mouseX + topX; 57 | arrowY = mouseY + topY; 58 | // 移动笔触到P3坐标 59 | context.moveTo(arrowX, arrowY); 60 | // 绘制P2到P3的斜线 61 | context.lineTo(mouseX, mouseY); 62 | // 计算P4的位置 63 | arrowX = mouseX + botX; 64 | arrowY = mouseY + botY; 65 | // 绘制P2到P4的斜线 66 | context.lineTo(arrowX, arrowY); 67 | // 上色 68 | context.strokeStyle = color; 69 | context.lineWidth = borderWidth; 70 | // 填充 71 | context.stroke(); 72 | // 结束绘制 73 | context.restore(); 74 | } 75 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | screen shot demo 6 | 7 | 8 | 20 | 34 | 69 | 70 | 71 |
72 |
73 | 截图插件文字展示 74 |
75 |
76 | 77 |

图片展示

78 | 79 | 85 | 86 | 87 |
88 | 89 | 90 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawMosaic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取图像指定坐标位置的颜色 3 | * @param imgData 需要进行操作的图片 4 | * @param x x点坐标 5 | * @param y y点坐标 6 | */ 7 | const getAxisColor = (imgData: ImageData, x: number, y: number) => { 8 | const w = imgData.width; 9 | const d = imgData.data; 10 | const color = []; 11 | color[0] = d[4 * (y * w + x)]; 12 | color[1] = d[4 * (y * w + x) + 1]; 13 | color[2] = d[4 * (y * w + x) + 2]; 14 | color[3] = d[4 * (y * w + x) + 3]; 15 | return color; 16 | }; 17 | 18 | /** 19 | * 设置图像指定坐标位置的颜色 20 | * @param imgData 需要进行操作的图片 21 | * @param x x点坐标 22 | * @param y y点坐标 23 | * @param color 颜色数组 24 | */ 25 | const setAxisColor = ( 26 | imgData: ImageData, 27 | x: number, 28 | y: number, 29 | color: Array 30 | ) => { 31 | const w = imgData.width; 32 | const d = imgData.data; 33 | d[4 * (y * w + x)] = color[0]; 34 | d[4 * (y * w + x) + 1] = color[1]; 35 | d[4 * (y * w + x) + 2] = color[2]; 36 | d[4 * (y * w + x) + 3] = color[3]; 37 | }; 38 | 39 | /** 40 | * 绘制马赛克 41 | * 实现思路: 42 | * 1. 获取鼠标划过路径区域的图像信息 43 | * 2. 将区域内的像素点绘制成周围相近的颜色 44 | * @param mouseX 当前鼠标X轴坐标 45 | * @param mouseY 当前鼠标Y轴坐标 46 | * @param size 马赛克画笔大小 47 | * @param degreeOfBlur 马赛克模糊度 48 | * @param context 需要进行绘制的画布 49 | */ 50 | export function drawMosaic( 51 | mouseX: number, 52 | mouseY: number, 53 | size: number, 54 | degreeOfBlur: number, 55 | context: CanvasRenderingContext2D 56 | ) { 57 | // 获取设备像素比 58 | const dpr = window.devicePixelRatio || 1; 59 | // 获取鼠标经过区域的图片像素信息 60 | const imgData = context.getImageData( 61 | mouseX * dpr, 62 | mouseY * dpr, 63 | size * dpr, 64 | size * dpr 65 | ); 66 | // 获取图像宽高 67 | const w = imgData.width; 68 | const h = imgData.height; 69 | // 等分图像宽高 70 | const stepW = w / degreeOfBlur; 71 | const stepH = h / degreeOfBlur; 72 | // 循环画布像素点 73 | for (let i = 0; i < stepH; i++) { 74 | for (let j = 0; j < stepW; j++) { 75 | // 随机获取一个小方格的随机颜色 76 | const color = getAxisColor( 77 | imgData, 78 | j * degreeOfBlur + Math.floor(Math.random() * degreeOfBlur), 79 | i * degreeOfBlur + Math.floor(Math.random() * degreeOfBlur) 80 | ); 81 | // 循环小方格的像素点 82 | for (let k = 0; k < degreeOfBlur; k++) { 83 | for (let l = 0; l < degreeOfBlur; l++) { 84 | // 设置小方格的颜色 85 | setAxisColor( 86 | imgData, 87 | j * degreeOfBlur + l, 88 | i * degreeOfBlur + k, 89 | color 90 | ); 91 | } 92 | } 93 | } 94 | } 95 | // 渲染打上马赛克后的图像信息 96 | context.putImageData(imgData, mouseX * dpr, mouseY * dpr); 97 | } 98 | -------------------------------------------------------------------------------- /src/module/main-entrance/PlugInParameters.ts: -------------------------------------------------------------------------------- 1 | import { hideBarInfoType, toolIcoType } from "@/module/type/ComponentType"; 2 | 3 | let enableWebRtc = true; 4 | let writeBase64 = true; 5 | let hiddenScrollBar = { 6 | color: "#000000", 7 | state: false, 8 | fillWidth: 0, 9 | fillHeight: 0, 10 | fillState: false 11 | }; 12 | 13 | // 数据初始化标识 14 | let initStatus = false; 15 | let level = 0; 16 | // 单击截取屏启用状态 17 | let clickCutFullScreen = false; 18 | // 需要隐藏的工具栏图标 19 | let hiddenToolIco: toolIcoType = {}; 20 | // html2canvas模式是否启用跨域图片加载模式 21 | let enableCORS = false; 22 | let proxyAddress: string | undefined = undefined; 23 | let wrcWindowMode = false; 24 | export default class PlugInParameters { 25 | constructor() { 26 | // 标识为true时则初始化数据 27 | if (initStatus) { 28 | enableWebRtc = true; 29 | writeBase64 = true; 30 | wrcWindowMode = false; 31 | // 初始化完成设置其值为false 32 | initStatus = false; 33 | level = 0; 34 | } 35 | } 36 | 37 | // 设置数据初始化标识 38 | public setInitStatus(status: boolean) { 39 | initStatus = status; 40 | } 41 | 42 | // 获取数据初始化标识 43 | public getInitStatus() { 44 | return initStatus; 45 | } 46 | 47 | // 获取webrtc启用状态 48 | public getWebRtcStatus() { 49 | return enableWebRtc; 50 | } 51 | 52 | // 设置webrtc启用状态 53 | public setWebRtcStatus(status: boolean) { 54 | enableWebRtc = status; 55 | } 56 | 57 | public getLevel() { 58 | return level; 59 | } 60 | 61 | public setLevel(val: number) { 62 | level = val; 63 | } 64 | 65 | public getClickCutFullScreenStatus() { 66 | return clickCutFullScreen; 67 | } 68 | 69 | public setClickCutFullScreenStatus(value: boolean) { 70 | clickCutFullScreen = value; 71 | } 72 | 73 | public getHiddenToolIco() { 74 | return hiddenToolIco; 75 | } 76 | 77 | public setHiddenToolIco(obj: toolIcoType) { 78 | hiddenToolIco = obj; 79 | } 80 | 81 | public getEnableCORSStatus(): boolean { 82 | return enableCORS; 83 | } 84 | 85 | public setEnableCORSStatus(status: boolean) { 86 | enableCORS = status; 87 | } 88 | 89 | public getProxyAddress(): string | undefined { 90 | return proxyAddress; 91 | } 92 | 93 | public setProxyAddress(address: string) { 94 | proxyAddress = address; 95 | } 96 | 97 | // 设置截图数据的写入状态 98 | public setWriteImgState(state: boolean) { 99 | writeBase64 = state; 100 | } 101 | public getWriteImgState() { 102 | return writeBase64; 103 | } 104 | 105 | public setHiddenScrollBarInfo(info: hideBarInfoType) { 106 | const { state, color, fillWidth, fillHeight, fillState } = info; 107 | hiddenScrollBar = { 108 | state, 109 | color: color ? color : "#000000", 110 | fillWidth: fillWidth ? fillWidth : 0, 111 | fillHeight: fillHeight ? fillHeight : 0, 112 | fillState: fillState ? fillState : false 113 | }; 114 | } 115 | 116 | public getHiddenScrollBarInfo() { 117 | return hiddenScrollBar; 118 | } 119 | 120 | public setWrcWindowMode(state: boolean) { 121 | wrcWindowMode = state; 122 | } 123 | 124 | public getWrcWindowMode() { 125 | return wrcWindowMode; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/module/common-methords/ZoomCutOutBoxPosition.ts: -------------------------------------------------------------------------------- 1 | import { nonNegativeData } from "@/module/common-methords/FixedData"; 2 | /** 3 | * 缩放裁剪框 4 | * @param currentX 当前鼠标X轴坐标 5 | * @param currentY 当前鼠标Y轴坐标 6 | * @param startX 裁剪框当前X轴坐标 7 | * @param startY 裁剪框当前Y轴坐标 8 | * @param width 裁剪框宽度 9 | * @param height 裁剪框高度 10 | * @param option 当前操作的节点 11 | * @private 12 | */ 13 | export function zoomCutOutBoxPosition( 14 | currentX: number, 15 | currentY: number, 16 | startX: number, 17 | startY: number, 18 | width: number, 19 | height: number, 20 | option: number 21 | ) { 22 | // 临时坐标 23 | let tempStartX, 24 | tempStartY, 25 | tempWidth, 26 | tempHeight = 0; 27 | // 判断操作方向 28 | switch (option) { 29 | case 2: // n 30 | tempStartY = 31 | currentY - (startY + height) > 0 ? startY + height : currentY; 32 | tempHeight = nonNegativeData(height - (currentY - startY)); 33 | return { 34 | tempStartX: startX, 35 | tempStartY, 36 | tempWidth: width, 37 | tempHeight 38 | }; 39 | case 3: // s 40 | tempHeight = nonNegativeData(currentY - startY); 41 | return { 42 | tempStartX: startX, 43 | tempStartY: startY, 44 | tempWidth: width, 45 | tempHeight 46 | }; 47 | case 4: // w 48 | tempStartX = currentX - (startX + width) > 0 ? startX + width : currentX; 49 | tempWidth = nonNegativeData(width - (currentX - startX)); 50 | return { 51 | tempStartX, 52 | tempStartY: startY, 53 | tempWidth, 54 | tempHeight: height 55 | }; 56 | case 5: // e 57 | tempWidth = nonNegativeData(currentX - startX); 58 | return { 59 | tempStartX: startX, 60 | tempStartY: startY, 61 | tempWidth, 62 | tempHeight: height 63 | }; 64 | case 6: // nw 65 | tempStartX = currentX - (startX + width) > 0 ? startX + width : currentX; 66 | tempStartY = 67 | currentY - (startY + height) > 0 ? startY + height : currentY; 68 | tempWidth = nonNegativeData(width - (currentX - startX)); 69 | tempHeight = nonNegativeData(height - (currentY - startY)); 70 | return { 71 | tempStartX, 72 | tempStartY, 73 | tempWidth, 74 | tempHeight 75 | }; 76 | case 7: // se 77 | tempWidth = nonNegativeData(currentX - startX); 78 | tempHeight = nonNegativeData(currentY - startY); 79 | return { 80 | tempStartX: startX, 81 | tempStartY: startY, 82 | tempWidth, 83 | tempHeight 84 | }; 85 | case 8: // ne 86 | tempStartY = 87 | currentY - (startY + height) > 0 ? startY + height : currentY; 88 | tempWidth = nonNegativeData(currentX - startX); 89 | tempHeight = nonNegativeData(height - (currentY - startY)); 90 | return { 91 | tempStartX: startX, 92 | tempStartY, 93 | tempWidth, 94 | tempHeight 95 | }; 96 | case 9: // sw 97 | tempStartX = currentX - (startX + width) > 0 ? startX + width : currentX; 98 | tempWidth = nonNegativeData(width - (currentX - startX)); 99 | tempHeight = nonNegativeData(currentY - startY); 100 | return { 101 | tempStartX, 102 | tempStartY: startY, 103 | tempWidth, 104 | tempHeight 105 | }; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/module/common-methords/SaveBorderArrInfo.ts: -------------------------------------------------------------------------------- 1 | import { cutOutBoxBorder, positionInfoType } from "@/module/type/ComponentType"; 2 | 3 | /** 4 | * 保存边框节点的相关信息 5 | * @param borderSize 边框节点直径大小 6 | * @param positionInfo 裁剪框位置信息 7 | * @private 8 | */ 9 | export function saveBorderArrInfo( 10 | borderSize: number, 11 | positionInfo: positionInfoType 12 | ) { 13 | // 获取裁剪框位置信息 14 | const { startX, startY, width, height } = positionInfo; 15 | const halfBorderSize = borderSize / 2; 16 | const borderArr: Array = []; 17 | // 移动, n北s南e东w西 18 | borderArr[0] = { 19 | x: startX + halfBorderSize, 20 | y: startY + halfBorderSize, 21 | width: width - borderSize, 22 | height: height - borderSize, 23 | index: 1, 24 | option: 1 25 | }; 26 | // n 27 | borderArr[1] = { 28 | x: startX + halfBorderSize, 29 | y: startY, 30 | width: width - borderSize, 31 | height: halfBorderSize, 32 | index: 2, 33 | option: 2 34 | }; 35 | borderArr[2] = { 36 | x: startX - halfBorderSize + width / 2, 37 | y: startY - halfBorderSize, 38 | width: borderSize, 39 | height: halfBorderSize, 40 | index: 2, 41 | option: 2 42 | }; 43 | // s 44 | borderArr[3] = { 45 | x: startX + halfBorderSize, 46 | y: startY - halfBorderSize + height, 47 | width: width - borderSize, 48 | height: halfBorderSize, 49 | index: 2, 50 | option: 3 51 | }; 52 | borderArr[4] = { 53 | x: startX - halfBorderSize + width / 2, 54 | y: startY + height, 55 | width: borderSize, 56 | height: halfBorderSize, 57 | index: 2, 58 | option: 3 59 | }; 60 | // w 61 | borderArr[5] = { 62 | x: startX, 63 | y: startY + halfBorderSize, 64 | width: halfBorderSize, 65 | height: height - borderSize, 66 | index: 3, 67 | option: 4 68 | }; 69 | borderArr[6] = { 70 | x: startX - halfBorderSize, 71 | y: startY - halfBorderSize + height / 2, 72 | width: halfBorderSize, 73 | height: borderSize, 74 | index: 3, 75 | option: 4 76 | }; 77 | // e 78 | borderArr[7] = { 79 | x: startX - halfBorderSize + width, 80 | y: startY + halfBorderSize, 81 | width: halfBorderSize, 82 | height: height - borderSize, 83 | index: 3, 84 | option: 5 85 | }; 86 | borderArr[8] = { 87 | x: startX + width, 88 | y: startY - halfBorderSize + height / 2, 89 | width: halfBorderSize, 90 | height: borderSize, 91 | index: 3, 92 | option: 5 93 | }; 94 | // nw 95 | borderArr[9] = { 96 | x: startX - halfBorderSize, 97 | y: startY - halfBorderSize, 98 | width: borderSize, 99 | height: borderSize, 100 | index: 4, 101 | option: 6 102 | }; 103 | // se 104 | borderArr[10] = { 105 | x: startX - halfBorderSize + width, 106 | y: startY - halfBorderSize + height, 107 | width: borderSize, 108 | height: borderSize, 109 | index: 4, 110 | option: 7 111 | }; 112 | // ne 113 | borderArr[11] = { 114 | x: startX - halfBorderSize + width, 115 | y: startY - halfBorderSize, 116 | width: borderSize, 117 | height: borderSize, 118 | index: 5, 119 | option: 8 120 | }; 121 | // sw 122 | borderArr[12] = { 123 | x: startX - halfBorderSize, 124 | y: startY - halfBorderSize + height, 125 | width: borderSize, 126 | height: borderSize, 127 | index: 5, 128 | option: 9 129 | }; 130 | return borderArr; 131 | } 132 | -------------------------------------------------------------------------------- /rollup-utils.js: -------------------------------------------------------------------------------- 1 | // 生成打包配置 2 | import { terser } from "rollup-plugin-terser"; 3 | import visualizer from "rollup-plugin-visualizer"; 4 | import serve from "rollup-plugin-serve"; 5 | import livereload from "rollup-plugin-livereload"; 6 | import delFile from "rollup-plugin-delete"; 7 | 8 | // 处理output对象中的format字段(传入的参数会与rollup所定义的参数不符,因此需要在这里进行转换) 9 | const buildFormat = formatVal => { 10 | let finalFormatVal = formatVal; 11 | switch (formatVal) { 12 | case "esm": 13 | finalFormatVal = "es"; 14 | break; 15 | case "common": 16 | finalFormatVal = "cjs"; 17 | break; 18 | default: 19 | break; 20 | } 21 | return finalFormatVal; 22 | }; 23 | 24 | /** 25 | * 根据外部条件判断是否需要给对象添加属性 26 | * @param obj 对象名 27 | * @param condition 条件 28 | * @param propName 属性名 29 | * @param propValue 属性值 30 | */ 31 | const addProperty = (obj, condition, propName, propValue) => { 32 | // 条件成立则添加 33 | if (condition) { 34 | obj[propName] = propValue; 35 | } 36 | }; 37 | 38 | const buildConfig = (packagingFormat = [], compressedState = "false") => { 39 | const outputConfig = []; 40 | for (let i = 0; i < packagingFormat.length; i++) { 41 | const pkgFormat = packagingFormat[i]; 42 | // 根据packagingFormat字段来构建对应格式的包 43 | const config = { 44 | file: `dist/screenShotPlugin.${pkgFormat}.js`, 45 | format: buildFormat(pkgFormat), 46 | name: "screenShotPlugin", 47 | globals: { 48 | vue: "Vue" 49 | } 50 | }; 51 | // 是否需要对代码进行压缩 52 | addProperty(config, compressedState === "true", "plugins", [ 53 | terser({ 54 | output: { 55 | comments: false // 删除注释 56 | } 57 | }) 58 | ]); 59 | addProperty(config, pkgFormat === "common", "exports", "named"); 60 | outputConfig.push(config); 61 | } 62 | return outputConfig; 63 | }; 64 | 65 | const buildCopyTargetsConfig = (useDevServer = "false") => { 66 | const result = [ 67 | { 68 | src: "src/assets/fonts/**", 69 | dest: "dist/assets/fonts" 70 | } 71 | ]; 72 | if (useDevServer === "true") { 73 | result.push({ 74 | src: "public/**", 75 | dest: "dist" 76 | }); 77 | } 78 | return result; 79 | }; 80 | 81 | // 生成打包后的模块占用信息 82 | const enablePKGStats = (status = "false") => { 83 | if (status === "true") { 84 | return visualizer({ 85 | filename: "dist/bundle-stats.html" 86 | }); 87 | } 88 | return null; 89 | }; 90 | 91 | const enableDevServer = status => { 92 | // 默认清空dist目录下的文件 93 | let serverConfig = [delFile({ targets: "dist/*" })]; 94 | if (status === "true") { 95 | // dev模式下不需要对dist目录进行清空 96 | serverConfig = [ 97 | serve({ 98 | // 服务器启动的文件夹,访问此路径下的index.html文件 99 | contentBase: "dist", 100 | port: 8123 101 | }), 102 | // watch dist目录,当目录中的文件发生变化时,刷新页面 103 | livereload("dist") 104 | ]; 105 | } 106 | return serverConfig; 107 | }; 108 | 109 | const buildTSConfig = (useDevServer = "false") => { 110 | return { 111 | tsconfig: "tsconfig.json", 112 | tsconfigOverride: { 113 | compilerOptions: { 114 | // dev模式下不生成.d.ts文件 115 | declaration: useDevServer !== "true", 116 | // 指定目标环境为es5 117 | target: "es5" 118 | } 119 | }, 120 | clean: true 121 | }; 122 | }; 123 | 124 | export { 125 | buildConfig, 126 | buildCopyTargetsConfig, 127 | enablePKGStats, 128 | enableDevServer, 129 | buildTSConfig 130 | }; 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-web-screen-shot", 3 | "version": "1.5.3", 4 | "description": "web端自定义截屏插件(Vue3版)", 5 | "private": false, 6 | "main": "dist/screenShotPlugin.common.js", 7 | "types": "dist/main.d.ts", 8 | "module": "dist/screenShotPlugin.esm.js", 9 | "umd:main": "dist/screenShotPlugin.umd.js", 10 | "publisher": "magicalprogrammer@qq.com", 11 | "scripts": { 12 | "build-rollup": "rollup -c --splitCss false --compState false --showPKGInfo true", 13 | "build-rollup:dev": "rollup -wc --splitCss false --compState false --showPKGInfo true --useDServer true --pkgFormat umd", 14 | "build-rollup:prod": "rollup -c --splitCss false --compState true", 15 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 16 | "commit": "git-cz" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/likaia/screen-shot.git" 21 | }, 22 | "keywords": [ 23 | "vue-screen-shot", 24 | "web-screen-shot", 25 | "截屏", 26 | "截图", 27 | "屏幕截图", 28 | "自定义截图", 29 | "web端自定义截屏" 30 | ], 31 | "author": "likaia", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/likaia/screen-shot/issues" 35 | }, 36 | "homepage": "https://github.com/likaia/screen-shot#readme", 37 | "dependencies": { 38 | "html2canvas": "^1.4.1", 39 | "webrtc-adapter": "^7.7.0" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.21.0", 43 | "@babel/preset-env": "^7.20.2", 44 | "@commitlint/cli": "^11.0.0", 45 | "@commitlint/config-angular": "^11.0.0", 46 | "@rollup/plugin-alias": "^4.0.3", 47 | "@rollup/plugin-babel": "^6.0.3", 48 | "@rollup/plugin-commonjs": "^20.0.0", 49 | "@rollup/plugin-node-resolve": "^13.0.0", 50 | "@rollup/plugin-url": "^8.0.1", 51 | "@typescript-eslint/eslint-plugin": "^2.33.0", 52 | "@typescript-eslint/parser": "^2.33.0", 53 | "@vue/eslint-config-prettier": "^6.0.0", 54 | "@vue/eslint-config-typescript": "^5.0.2", 55 | "autoprefixer": "^10.4.13", 56 | "commitizen": "^4.2.2", 57 | "core-js": "^3.6.5", 58 | "cssnano": "^5.1.15", 59 | "cz-conventional-changelog": "^3.3.0", 60 | "eslint": "^6.7.2", 61 | "eslint-plugin-prettier": "^3.1.3", 62 | "eslint-plugin-vue": "^7.0.0-0", 63 | "husky": "^4.3.0", 64 | "postcss": "^8.4.21", 65 | "postcss-import": "^15.1.0", 66 | "postcss-preset-env": "^8.0.1", 67 | "postcss-url": "^10.1.3", 68 | "prettier": "^1.19.1", 69 | "rollup": "^2.59.2", 70 | "rollup-plugin-copy": "^3.4.0", 71 | "rollup-plugin-delete": "^2.0.0", 72 | "rollup-plugin-livereload": "^2.0.5", 73 | "rollup-plugin-postcss": "^4.0.2", 74 | "rollup-plugin-progress": "^1.1.2", 75 | "rollup-plugin-serve": "^2.0.2", 76 | "rollup-plugin-terser": "^7.0.2", 77 | "rollup-plugin-typescript2": "^0.34.1", 78 | "rollup-plugin-visualizer": "^5.9.0", 79 | "rollup-plugin-vue": "^6.0.0", 80 | "sass": "^1.26.5", 81 | "sass-loader": "^8.0.2", 82 | "typescript": "~4.5.2", 83 | "vue": "^3.2.47", 84 | "yargs": "^17.7.1" 85 | }, 86 | "peerDependencies": { 87 | "core-js": "^3.6.5", 88 | "vue": "^3.2.47" 89 | }, 90 | "config": { 91 | "commitizen": { 92 | "path": "./node_modules/cz-conventional-changelog" 93 | } 94 | }, 95 | "husky": { 96 | "hooks": { 97 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 98 | } 99 | }, 100 | "files": [ 101 | "/dist" 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawCutOutBox.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 绘制裁剪框 3 | * @param mouseX 鼠标x轴坐标 4 | * @param mouseY 鼠标y轴坐标 5 | * @param width 裁剪框宽度 6 | * @param height 裁剪框高度 7 | * @param context 需要进行绘制的canvas画布 8 | * @param borderSize 边框节点直径 9 | * @param controller 需要进行操作的canvas容器 10 | * @param imageController 图片canvas容器 11 | * @param drawBorders 12 | * @private 13 | */ 14 | 15 | export function drawCutOutBox( 16 | mouseX: number, 17 | mouseY: number, 18 | width: number, 19 | height: number, 20 | context: CanvasRenderingContext2D, 21 | borderSize: number, 22 | controller: HTMLCanvasElement, 23 | imageController: HTMLCanvasElement, 24 | drawBorders = true 25 | ) { 26 | // 获取画布宽高 27 | const canvasWidth = controller?.width; 28 | const canvasHeight = controller?.height; 29 | 30 | // 画布、图片不存在则return 31 | if (!canvasWidth || !canvasHeight || !imageController || !controller) return; 32 | 33 | // 清除画布 34 | context.clearRect(0, 0, canvasWidth, canvasHeight); 35 | 36 | // 绘制蒙层 37 | context.save(); 38 | context.fillStyle = "rgba(0, 0, 0, .6)"; 39 | context.fillRect(0, 0, canvasWidth, canvasHeight); 40 | // 将蒙层凿开 41 | context.globalCompositeOperation = "source-atop"; 42 | // 裁剪选择框 43 | context.clearRect(mouseX, mouseY, width, height); 44 | // 绘制8个边框像素点并保存坐标信息以及事件参数 45 | context.globalCompositeOperation = "source-over"; 46 | context.fillStyle = "#2CABFF"; 47 | // 是否绘制裁剪框的8个像素点 48 | if (drawBorders) { 49 | // 像素点大小 50 | const size = borderSize; 51 | // 绘制像素点 52 | context.fillRect(mouseX - size / 2, mouseY - size / 2, size, size); 53 | context.fillRect( 54 | mouseX - size / 2 + width / 2, 55 | mouseY - size / 2, 56 | size, 57 | size 58 | ); 59 | context.fillRect(mouseX - size / 2 + width, mouseY - size / 2, size, size); 60 | context.fillRect( 61 | mouseX - size / 2, 62 | mouseY - size / 2 + height / 2, 63 | size, 64 | size 65 | ); 66 | context.fillRect( 67 | mouseX - size / 2 + width, 68 | mouseY - size / 2 + height / 2, 69 | size, 70 | size 71 | ); 72 | context.fillRect(mouseX - size / 2, mouseY - size / 2 + height, size, size); 73 | context.fillRect( 74 | mouseX - size / 2 + width / 2, 75 | mouseY - size / 2 + height, 76 | size, 77 | size 78 | ); 79 | context.fillRect( 80 | mouseX - size / 2 + width, 81 | mouseY - size / 2 + height, 82 | size, 83 | size 84 | ); 85 | } 86 | // 绘制结束 87 | context.restore(); 88 | // 使用drawImage将图片绘制到蒙层下方 89 | context.save(); 90 | 91 | context.globalCompositeOperation = "destination-over"; 92 | // 图片尺寸使用canvas容器的css中的尺寸 93 | const { imgWidth, imgHeight } = { 94 | imgWidth: parseInt(controller?.style.width), 95 | imgHeight: parseInt(controller?.style.height) 96 | }; 97 | 98 | context.drawImage(imageController, 0, 0, imgWidth, imgHeight); 99 | context.restore(); 100 | // 返回裁剪框临时位置信息 101 | if (width > 0 && height > 0) { 102 | // 考虑左上往右下拉区域的情况 103 | return { 104 | startX: mouseX, 105 | startY: mouseY, 106 | width: width, 107 | height: height 108 | }; 109 | } else if (width < 0 && height < 0) { 110 | // 考虑右下往左上拉区域的情况 111 | return { 112 | startX: mouseX + width, 113 | startY: mouseY + height, 114 | width: Math.abs(width), 115 | height: Math.abs(height) 116 | }; 117 | } else if (width > 0 && height < 0) { 118 | // 考虑左下往右上拉区域的情况 119 | return { 120 | startX: mouseX, 121 | startY: mouseY + height, 122 | width: width, 123 | height: Math.abs(height) 124 | }; 125 | } else if (width < 0 && height > 0) { 126 | // 考虑右上往左下拉区域的情况 127 | return { 128 | startX: mouseX + width, 129 | startY: mouseY, 130 | width: Math.abs(width), 131 | height: height 132 | }; 133 | } 134 | return { 135 | startX: mouseX, 136 | startY: mouseY, 137 | width: width, 138 | height: height 139 | }; 140 | } 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-web-screen-shot · [![npm](https://img.shields.io/badge/npm-v1.5.3-2081C1)](https://www.npmjs.com/package/vue-web-screen-shot) [![yarn](https://img.shields.io/badge/yarn-v1.5.3-F37E42)](https://yarnpkg.com/package/vue-web-screen-shot) [![github](https://img.shields.io/badge/GitHub-depositary-9A9A9A)](https://github.com/likaia/screen-shot) [![](https://img.shields.io/github/issues/likaia/screen-shot)](https://github.com/likaia/screen-shot/issues) [![]( https://img.shields.io/github/forks/likaia/screen-shot)](https://github.com/likaia/screen-shot/network/members) [![]( https://img.shields.io/github/stars/likaia/screen-shot)](https://github.com/likaia/screen-shot/stargazers) 2 | web端自定义截屏插件(Vue3版),运行视频:[实现web端自定义截屏功能](https://www.bilibili.com/video/BV1Ey4y127cV) , 本插件仅支持Vue3,如需在其他平台使用请移步 :[js-web-screen-shot](https://www.npmjs.com/package/js-web-screen-shot) 效果图如下: 3 | ![截屏效果图](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/486d810877a24582aa8cf110e643c138~tplv-k3u1fbpfcp-watermark.image) 4 | 5 | ## 写在前面 6 | 关于此插件的更多介绍以及实现原理请移步: 7 | - [实现Web端自定义截屏](https://juejin.cn/post/6924368956950052877) 8 | - [实现Web端自定义截屏(JS版)](https://juejin.cn/post/6931901091445473293) 9 | 10 | ## 插件安装 11 | ```bash 12 | yarn add vue-web-screen-shot 13 | 14 | # or 15 | 16 | npm install vue-web-screen-shot --save 17 | ``` 18 | 19 | ## 插件使用 20 | 由于插件使用Vue3编写,因此它只能在Vue3项目中运行,如果你需要在vue2项目或者其他js项目中运行请移步:[js-web-screen-shot](https://www.npmjs.com/package/js-web-screen-shot),它采用原生js编写,功能与本插件功能一致。 21 | > 注意⚠️: 如果需要使用插件的webrtc模式或者截图写入剪切板功能,需要你的网站运行在`https`环境或者`localhost`环境。 22 | 23 | * 在项目的入口文件`main.ts/main.js`中加入下述代码 24 | ```javascript 25 | // 导入截屏插件 26 | import screenShort from "vue-web-screen-shot"; 27 | const app = createApp(App); 28 | // 使用截屏插件 29 | app.use(screenShort, { enableWebRtc: false }) 30 | ``` 31 | * 在你的需要使用的业务代码中,添加下述代码 32 | ```vue 33 | 40 | 41 | 63 | ``` 64 | ### 参数说明 65 | 如示例代码所示,在template中直接使用`screen-short`插件,绑定组件需要的事件处理函数即可。 66 | 67 | 接下来就跟大家讲下组件中每个属性的意义: 68 | * screenshotStatus 用于控制组件是否出现在dom中 69 | * @destroy-component 用于接收截图组件传递的销毁消息,我们需要在对应的函数中销毁截图组件 70 | * @get-image-data 用于接收截图组件传递的框选区域的base64图片信息,我们需要为他提供一个函数来接收截图组件传递的消息 71 | * @webrtc-error 使用webrtc模式截图时,当用户的浏览器不支持或者未授权时会触发此回调 72 | 73 | #### 可选参数 74 | 截图插件有一个可选参数,它接受一个对象,对象每个key的作用如下: 75 | * `enableWebRtc` 是否启用webrtc,值为boolean类型,值为false则使用html2canvas来截图 76 | * `level` 截图容器层级,值为number类型。 77 | * `clickCutFullScreen` 单击截全屏启用状态,值为`boolean`类型, 默认为`false` 78 | * `hiddenToolIco` 需要隐藏的截图工具栏图标,值为`{ save?: boolean; undo?: boolean; confirm?: boolean }`类型,默认为`{}`。传你需要隐藏的图标名称,将值设为`true`即可。 79 | * `enableCORS` html2canvas截图模式下跨域的启用状态,值为`boolean`类型,默认为`false` 80 | * `proxyAddress` html2canvas截图模式下的图片服务器代理地址,值为`string`类型,默认为`undefined` 81 | * `writeBase64` 是否将截图内容写入剪切板,值为`boolean`类型,默认为`true` 82 | * `wrcWindowMode` 是否启用窗口截图模式,值为`boolean`类型,默认为`false`,即当前标签页截图。如果标签页截图的内容有滚动条或者底部有空缺,可以考虑启用此模式。 83 | * `hiddenScrollBar` 是否隐藏滚动条,用webrtc模式截图时chrome 112版本的浏览器在部分系统下会挤压出现滚动条,如果出现你可以尝试通过此参数来进行修复。值为`Object`类型,有4个属性: 84 | * `state: boolean`; 启用状态, 默认为`false` 85 | * `fillState?: boolean`; 填充状态,默认为`false` 86 | * `color?: string`; 填充层颜色,滚动条隐藏后可能会出现空缺,需要进行填充,默认填充色为黑色。 87 | * `fillWidth?: number`; 填充层宽度,默认为截图容器的宽度 88 | * `fillHeight?: number`; 填充层高度,默认为空缺区域的高度 89 | 90 | > 使用当前标签页进行截图相对而言用户体验是最好的,但是因为`chrome 112`版本的bug会造成页面内容挤压导致截取到的内容不完整,因此只能采用其他方案来解决此问题了。`wrcWindowMode`和`hiddenScrollBar`都可以解决这个问题。 91 | > * `wrcWindowMode`方案会更完美些,但是用户授权时会出现其他的应用程序选项,用户体验会差一些 92 | > * `hiddenScrollBar`方案还是采用标签页截图,但是会造成内容挤压,底部出现空白。 93 | > 94 | > 两种方案的优点与缺点都讲完了,最好的办法还是希望`chrome`能在之后的版本更新中修复此问题。 95 | 96 | 97 | ## 写在最后 98 | 至此,插件的所有使用方法就介绍完了。 99 | -------------------------------------------------------------------------------- /src/module/split-methods/DrawArrow.ts: -------------------------------------------------------------------------------- 1 | export class DrawArrow { 2 | // 起始点与结束点 3 | private beginPoint = { x: 0, y: 0 }; 4 | private stopPoint = { x: 0, y: 0 }; 5 | // 多边形的尺寸信息 6 | private polygonVertex: Array = []; 7 | // 起点与X轴之间的夹角角度值 8 | private angle = 0; 9 | // 箭头信息 10 | private arrowInfo = { 11 | edgeLen: 50, // 箭头的头部长度 12 | angle: 30 // 箭头的头部角度 13 | }; 14 | 15 | /** 16 | * 绘制箭头 17 | * @param ctx 需要进行绘制的画布 18 | * @param originX 鼠标按下时的x轴坐标 19 | * @param originY 鼠标按下式的y轴坐标 20 | * @param x 当前鼠标x轴坐标 21 | * @param y 当前鼠标y轴坐标 22 | * @param color 箭头颜色 23 | */ 24 | public draw( 25 | ctx: CanvasRenderingContext2D, 26 | originX: number, 27 | originY: number, 28 | x: number, 29 | y: number, 30 | color: string 31 | ) { 32 | this.beginPoint.x = originX; 33 | this.beginPoint.y = originY; 34 | this.stopPoint.x = x; 35 | this.stopPoint.y = y; 36 | this.arrowCord(this.beginPoint, this.stopPoint); 37 | this.sideCord(); 38 | this.drawArrow(ctx, color); 39 | } 40 | 41 | // 在画布上画出递增变粗的箭头 42 | private drawArrow(ctx: CanvasRenderingContext2D, color: string) { 43 | ctx.fillStyle = color; 44 | ctx.beginPath(); 45 | ctx.moveTo(this.polygonVertex[0], this.polygonVertex[1]); 46 | ctx.lineTo(this.polygonVertex[2], this.polygonVertex[3]); 47 | ctx.lineTo(this.polygonVertex[4], this.polygonVertex[5]); 48 | ctx.lineTo(this.polygonVertex[6], this.polygonVertex[7]); 49 | ctx.lineTo(this.polygonVertex[8], this.polygonVertex[9]); 50 | ctx.lineTo(this.polygonVertex[10], this.polygonVertex[11]); 51 | ctx.closePath(); 52 | ctx.fill(); 53 | } 54 | 55 | /** 56 | * 设置箭头的相关绘制信息 57 | * @param edgeLen 长度 58 | * @param angle 角度 59 | * @private 60 | */ 61 | private setArrowInfo(edgeLen: number, angle: number) { 62 | this.arrowInfo.edgeLen = edgeLen; 63 | this.arrowInfo.angle = angle; 64 | } 65 | 66 | // 计算箭头尺寸 67 | private dynArrowSize() { 68 | const x = this.stopPoint.x - this.beginPoint.x; 69 | const y = this.stopPoint.y - this.beginPoint.y; 70 | const length = Math.sqrt(x ** 2 + y ** 2); 71 | 72 | if (length < 50) { 73 | this.arrowInfo.edgeLen = length / 2; 74 | } else if (length < 250) { 75 | this.arrowInfo.edgeLen /= 2; 76 | } else if (length < 500) { 77 | this.arrowInfo.edgeLen = (this.arrowInfo.edgeLen * length) / 500; 78 | } 79 | } 80 | 81 | // 计算起点与X轴之间的夹角角度值 82 | private getRadian( 83 | beginPoint: { x: number; y: number }, 84 | stopPoint: { x: number; y: number } 85 | ) { 86 | this.angle = 87 | (Math.atan2(stopPoint.y - beginPoint.y, stopPoint.x - beginPoint.x) / 88 | Math.PI) * 89 | 180; 90 | 91 | this.setArrowInfo(50, 30); 92 | this.dynArrowSize(); 93 | } 94 | 95 | // 计算箭头底边两个点位置信息 96 | private arrowCord( 97 | beginPoint: { x: number; y: number }, 98 | stopPoint: { x: number; y: number } 99 | ) { 100 | this.polygonVertex[0] = beginPoint.x; 101 | this.polygonVertex[1] = beginPoint.y; 102 | this.polygonVertex[6] = stopPoint.x; 103 | this.polygonVertex[7] = stopPoint.y; 104 | this.getRadian(beginPoint, stopPoint); 105 | this.polygonVertex[8] = 106 | stopPoint.x - 107 | this.arrowInfo.edgeLen * 108 | Math.cos((Math.PI / 180) * (this.angle + this.arrowInfo.angle)); 109 | this.polygonVertex[9] = 110 | stopPoint.y - 111 | this.arrowInfo.edgeLen * 112 | Math.sin((Math.PI / 180) * (this.angle + this.arrowInfo.angle)); 113 | this.polygonVertex[4] = 114 | stopPoint.x - 115 | this.arrowInfo.edgeLen * 116 | Math.cos((Math.PI / 180) * (this.angle - this.arrowInfo.angle)); 117 | this.polygonVertex[5] = 118 | stopPoint.y - 119 | this.arrowInfo.edgeLen * 120 | Math.sin((Math.PI / 180) * (this.angle - this.arrowInfo.angle)); 121 | } 122 | 123 | // 计算另两个底边侧面点 124 | private sideCord() { 125 | const midpoint: { x: number; y: number } = { x: 0, y: 0 }; 126 | midpoint.x = (this.polygonVertex[4] + this.polygonVertex[8]) / 2; 127 | midpoint.y = (this.polygonVertex[5] + this.polygonVertex[9]) / 2; 128 | this.polygonVertex[2] = (this.polygonVertex[4] + midpoint.x) / 2; 129 | this.polygonVertex[3] = (this.polygonVertex[5] + midpoint.y) / 2; 130 | this.polygonVertex[10] = (this.polygonVertex[8] + midpoint.x) / 2; 131 | this.polygonVertex[11] = (this.polygonVertex[9] + midpoint.y) / 2; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import babel from "@rollup/plugin-babel"; 5 | import postcss from "rollup-plugin-postcss"; 6 | import vue from "rollup-plugin-vue"; 7 | import autoprefixer from "autoprefixer"; 8 | import copy from "rollup-plugin-copy"; 9 | import path from "path"; 10 | import alias from "@rollup/plugin-alias"; 11 | import postcssImport from "postcss-import"; 12 | import postcssUrl from "postcss-url"; 13 | import url from "@rollup/plugin-url"; 14 | import cssnano from "cssnano"; 15 | import yargs from "yargs"; 16 | import { 17 | buildConfig, 18 | buildCopyTargetsConfig, 19 | enableDevServer, 20 | enablePKGStats, 21 | buildTSConfig 22 | } from "./rollup-utils"; 23 | import progress from "rollup-plugin-progress"; 24 | 25 | // 使用yargs解析命令行执行时的添加参数 26 | const commandLineParameters = yargs(process.argv.slice(1)).options({ 27 | // css文件独立状态,默认为内嵌 28 | splitCss: { type: "string", alias: "spCss", default: "false" }, 29 | // 打包格式, 默认为 umd,esm,common 三种格式 30 | packagingFormat: { 31 | type: "string", 32 | alias: "pkgFormat", 33 | default: "umd,esm,common" 34 | }, 35 | // 打包后的js压缩状态 36 | compressedState: { type: "string", alias: "compState", default: "false" }, 37 | // 显示每个包的占用体积, 默认不显示 38 | showModulePKGInfo: { type: "string", alias: "showPKGInfo", default: "false" }, 39 | // 是否开启devServer, 默认不开启 40 | useDevServer: { type: "string", alias: "useDServer", default: "false" } 41 | }).argv; 42 | // 需要让rollup忽略的自定义参数 43 | const ignoredWarningsKey = [...Object.keys(commandLineParameters)]; 44 | const splitCss = commandLineParameters.splitCss; 45 | const packagingFormat = commandLineParameters.packagingFormat.split(","); 46 | const compressedState = commandLineParameters.compressedState; 47 | const showModulePKGInfo = commandLineParameters.showModulePKGInfo; 48 | const useDevServer = commandLineParameters.useDevServer; 49 | 50 | export default { 51 | input: "src/main.ts", 52 | output: buildConfig(packagingFormat, compressedState), 53 | external: ["vue"], 54 | // 警告处理钩子 55 | onwarn: function(warning, rollupWarn) { 56 | const message = warning.message; 57 | let matchingResult = false; 58 | for (let i = 0; i < ignoredWarningsKey.length; i++) { 59 | if (message.indexOf(ignoredWarningsKey[i]) !== -1) { 60 | matchingResult = true; 61 | break; 62 | } 63 | } 64 | // 错误警告中包含要忽略的key则退出函数 65 | if (warning.code === "UNKNOWN_OPTION" && matchingResult) { 66 | return; 67 | } 68 | rollupWarn(warning); 69 | }, 70 | plugins: [ 71 | nodeResolve({ 72 | // 读取.browserslist文件 73 | browser: true, 74 | preferBuiltins: false 75 | }), 76 | vue({ 77 | target: "browser", 78 | css: true, 79 | // 把组件转换成 render 函数 80 | compileTemplate: true, 81 | preprocessStyles: true, 82 | compilerOptions: { 83 | comments: false // 删除注释 84 | }, 85 | preprocessOptions: { 86 | scss: { 87 | includePaths: ["src/assets/scss"] 88 | } 89 | } 90 | }), 91 | commonjs(), 92 | alias({ 93 | entries: [{ find: "@", replacement: path.resolve(__dirname, "src") }] 94 | }), 95 | typescript(buildTSConfig(useDevServer)), 96 | // 此处用来处理外置css, 需要在入口文件中使用import来导入css文件 97 | postcss({ 98 | // 内联css 99 | extract: splitCss === "true" ? "style/css/screen-shot.css" : false, 100 | minimize: true, 101 | sourceMap: false, 102 | extensions: [".css", ".scss"], 103 | // 当前正在处理的CSS文件的路径, postcssUrl在拷贝资源时需要根据它来定位目标文件 104 | to: path.resolve(__dirname, "dist/assets/*"), 105 | use: ["sass"], 106 | // autoprefixer: 给css3的一些属性加前缀 107 | // postcssImport: 处理css文件中的@import语句 108 | // cssnano: 它可以通过移除注释、空格和其他不必要的字符来压缩CSS代码 109 | plugins: [ 110 | autoprefixer(), 111 | postcssImport(), 112 | // 对scss中的别名进行统一替换处理(vue组件内置或者入口导入的scss文件都会走这里的规则) 113 | postcssUrl([ 114 | { 115 | filter: "**/*.*", 116 | url(asset) { 117 | return asset.url.replace(/~@/g, "."); 118 | } 119 | } 120 | ]), 121 | // 再次调用将css中引入的图片按照规则进行处理 122 | postcssUrl([ 123 | { 124 | basePath: path.resolve(__dirname, "src"), 125 | url: "inline", 126 | maxSize: 8, // 最大文件大小(单位为KB),超过该大小的文件将不会被编码为base64 127 | fallback: "copy", // 如果文件大小超过最大大小,则使用copy选项复制文件 128 | useHash: true, // 进行hash命名 129 | encodeType: "base64" // 指定编码类型为base64 130 | } 131 | ]), 132 | cssnano({ 133 | preset: "default" // 使用默认配置 134 | }) 135 | ] 136 | }), 137 | // 处理通过img标签引入的图片 138 | url({ 139 | include: ["**/*.jpg", "**/*.png", "**/*.svg"], 140 | // 输出路径 141 | dest: "dist/assets", 142 | // 超过10kb则拷贝否则转base64 143 | limit: 10 * 1024 // 10KB 144 | }), 145 | babel(), 146 | enablePKGStats(showModulePKGInfo), 147 | ...enableDevServer(useDevServer), 148 | progress({ 149 | format: "[:bar] :percent (:current/:total)", 150 | clearLine: false 151 | }), 152 | copy({ 153 | targets: buildCopyTargetsConfig(useDevServer) 154 | }) 155 | ] 156 | }; 157 | -------------------------------------------------------------------------------- /src/components/screen-short.vue: -------------------------------------------------------------------------------- 1 | 111 | 112 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /src/module/main-entrance/InitData.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInternalInstance, ref } from "vue"; 2 | import { positionInfoType, toolIcoType } from "@/module/type/ComponentType"; 3 | 4 | // 截图容器宽高 5 | const screenShortWidth = ref(0); 6 | const screenShortHeight = ref(0); 7 | // 裁剪框拖拽状态 8 | let dragging = false; 9 | 10 | // 截图工具栏展示状态与位置 11 | const toolStatus = ref(false); 12 | const toolLeft = ref(0); 13 | const toolTop = ref(0); 14 | 15 | // 截图工具栏点击状态 16 | const toolClickStatus = ref(false); 17 | // 截图工具栏画笔选择显示状态 18 | const optionStatus = ref(false); 19 | // 颜色面板展示状态 20 | const colorPanelStatus = ref(false); 21 | // 当前选择的颜色 22 | const selectedColor = ref("#F53340"); 23 | // 当前点击的工具栏名称 24 | const toolName = ref(""); 25 | // 当前选择的画笔大小 26 | const penSize = ref(2); 27 | // 文本输入工具栏点击状态 28 | const textClickStatus = ref(false); 29 | // 撤销状态 30 | const undoStatus = ref(false); 31 | // 裁剪框位置参数 32 | let cutOutBoxPosition: positionInfoType = { 33 | startX: 0, 34 | startY: 0, 35 | width: 0, 36 | height: 0 37 | }; 38 | // 截图工具栏画笔选择三角形角标位置 39 | const optionIcoPosition = ref(0); 40 | // 需要隐藏的工具栏图标 41 | const hiddenToolIco = ref({}); 42 | 43 | // 获取截图容器dom 44 | let screenShortController = ref(null); 45 | // 获取截图工具栏容器dom 46 | let toolController = ref(null); 47 | // 屏幕截图容器 48 | let screenShortImageController: HTMLCanvasElement | null = null; 49 | // 获取文本输入区域dom 50 | let textInputController = ref(null); 51 | // 截图工具栏画笔选择dom 52 | let optionIcoController = ref(null); 53 | let optionController = ref(null); 54 | // 事件处理 55 | let emit: ((event: string, ...args: any[]) => void) | undefined; 56 | 57 | let currentInstance: ComponentInternalInstance | null | undefined; 58 | 59 | // 数据初始化标识 60 | let initStatus = false; 61 | 62 | export default class InitData { 63 | constructor() { 64 | // 标识为true时则初始化数据 65 | if (initStatus) { 66 | // 初始化完成设置其值为false 67 | initStatus = false; 68 | screenShortWidth.value = 0; 69 | screenShortHeight.value = 0; 70 | screenShortController = ref(null); 71 | toolController = ref(null); 72 | textInputController = ref(null); 73 | optionController = ref(null); 74 | optionIcoController = ref(null); 75 | cutOutBoxPosition = { 76 | startX: 0, 77 | startY: 0, 78 | width: 0, 79 | height: 0 80 | }; 81 | toolStatus.value = false; 82 | optionStatus.value = false; 83 | colorPanelStatus.value = false; 84 | emit = undefined; 85 | currentInstance = undefined; 86 | toolClickStatus.value = false; 87 | optionIcoPosition.value = 0; 88 | selectedColor.value = "#F53340"; 89 | toolName.value = ""; 90 | penSize.value = 2; 91 | hiddenToolIco.value = {}; 92 | } 93 | } 94 | 95 | // 设置数据初始化标识 96 | public setInitStatus(status: boolean) { 97 | initStatus = status; 98 | } 99 | 100 | // 获取数据初始化标识 101 | public getInitStatus() { 102 | return initStatus; 103 | } 104 | 105 | // 获取截图容器宽高 106 | public getScreenShortWidth() { 107 | return screenShortWidth; 108 | } 109 | public getScreenShortHeight() { 110 | return screenShortHeight; 111 | } 112 | 113 | // 设置截图容器宽高 114 | public setScreenShortInfo(width: number, height: number) { 115 | screenShortWidth.value = width; 116 | screenShortHeight.value = height; 117 | } 118 | 119 | // 获取截图容器dom 120 | public getScreenShortController() { 121 | return screenShortController; 122 | } 123 | 124 | // 获取截图工具栏dom 125 | public getToolController() { 126 | return toolController; 127 | } 128 | 129 | // 获取文本输入区域dom 130 | public getTextInputController() { 131 | return textInputController; 132 | } 133 | 134 | // 获取截图工具栏展示状态 135 | public getToolStatus() { 136 | return toolStatus; 137 | } 138 | 139 | // 获取文本输入工具栏展示状态 140 | public getTextStatus() { 141 | return textClickStatus; 142 | } 143 | 144 | // 获取屏幕截图容器 145 | public getScreenShortImageController() { 146 | return screenShortImageController; 147 | } 148 | 149 | // 设置屏幕截图 150 | public setScreenShortImageController(imageController: HTMLCanvasElement) { 151 | screenShortImageController = imageController; 152 | } 153 | 154 | // 设置截图工具栏展示状态 155 | public setToolStatus(status: boolean) { 156 | toolStatus.value = status; 157 | } 158 | 159 | // 设置文本输入工具栏展示状态 160 | public setTextStatus(status: boolean) { 161 | textClickStatus.value = status; 162 | } 163 | 164 | // 获取截图工具位置信息 165 | public getToolLeft() { 166 | return toolLeft; 167 | } 168 | public getToolTop() { 169 | return toolTop; 170 | } 171 | 172 | // 设置截图工具位置信息 173 | public setToolInfo(left: number, top: number) { 174 | toolLeft.value = left; 175 | toolTop.value = top; 176 | } 177 | 178 | // 获取截图工具栏点击状态 179 | public getToolClickStatus() { 180 | return toolClickStatus; 181 | } 182 | 183 | // 设置截图工具栏点击状态 184 | public setToolClickStatus(status: boolean) { 185 | toolClickStatus.value = status; 186 | } 187 | 188 | // 获取裁剪框位置信息 189 | public getCutOutBoxPosition() { 190 | return cutOutBoxPosition; 191 | } 192 | 193 | public getDragging() { 194 | return dragging; 195 | } 196 | 197 | public setDragging(status: boolean) { 198 | dragging = status; 199 | } 200 | 201 | // 设置裁剪框位置信息 202 | public setCutOutBoxPosition( 203 | mouseX: number, 204 | mouseY: number, 205 | width: number, 206 | height: number 207 | ) { 208 | cutOutBoxPosition.startX = mouseX; 209 | cutOutBoxPosition.startY = mouseY; 210 | cutOutBoxPosition.width = width; 211 | cutOutBoxPosition.height = height; 212 | } 213 | 214 | // 获取撤销状态 215 | public getUndoStatus() { 216 | return undoStatus; 217 | } 218 | // 设置撤销状态 219 | public setUndoStatus(status: boolean) { 220 | undoStatus.value = status; 221 | } 222 | 223 | // 获取/设置截图工具栏画笔选择工具展示状态 224 | public getOptionStatus() { 225 | return optionStatus; 226 | } 227 | public setOptionStatus(status: boolean) { 228 | optionStatus.value = status; 229 | } 230 | 231 | // 获取截图工具栏画笔选择工具dom 232 | public getOptionIcoController() { 233 | return optionIcoController; 234 | } 235 | public getOptionController() { 236 | return optionController; 237 | } 238 | 239 | // 获取/设置三角形角标位置 240 | public getOptionIcoPosition() { 241 | return optionIcoPosition; 242 | } 243 | public setOptionIcoPosition(position: number) { 244 | optionIcoPosition.value = position; 245 | } 246 | 247 | // 获取/设置颜色选择面板显示状态 248 | public getColorPanelStatus() { 249 | return colorPanelStatus; 250 | } 251 | public setColorPanelStatus(status: boolean) { 252 | colorPanelStatus.value = status; 253 | } 254 | 255 | // 获取/设置当前选择的颜色 256 | public getSelectedColor() { 257 | return selectedColor; 258 | } 259 | public setSelectedColor(color: string) { 260 | selectedColor.value = color; 261 | } 262 | 263 | // 获取/设置当前点击的工具栏条目名称 264 | public getToolName() { 265 | return toolName; 266 | } 267 | public setToolName(itemName: string) { 268 | toolName.value = itemName; 269 | } 270 | 271 | // 获取/设置当前画笔大小 272 | public getPenSize() { 273 | return penSize; 274 | } 275 | public setPenSize(size: number) { 276 | penSize.value = size; 277 | } 278 | 279 | /** 280 | * 设置父组件传递的数据 281 | * @param emitParam 282 | */ 283 | public setPropsData(emitParam: (event: string, ...args: any[]) => void) { 284 | emit = emitParam; 285 | } 286 | 287 | /** 288 | * 设置实例属性 289 | * @param instanceParam 290 | */ 291 | public setProperty(instanceParam: ComponentInternalInstance | null) { 292 | currentInstance = instanceParam; 293 | } 294 | 295 | // 获取当前实例 296 | public getCurrentInstance() { 297 | return currentInstance; 298 | } 299 | 300 | // 获取当前emit 301 | public getEmit() { 302 | return emit; 303 | } 304 | 305 | public getHiddenToolIco() { 306 | return hiddenToolIco; 307 | } 308 | 309 | public setHiddenToolIco(obj: toolIcoType) { 310 | hiddenToolIco.value = obj; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/assets/scss/screen-short.scss: -------------------------------------------------------------------------------- 1 | #screenShotContainer { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | } 6 | 7 | #screenShotPanel { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | #toolPanel { 16 | height: 24px; 17 | background: #ffffff; 18 | z-index: 9999; 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | min-width: 275px; 23 | padding: 10px; 24 | 25 | .item-panel { 26 | width: 24px; 27 | height: 24px; 28 | margin-right: 15px; 29 | float: left; 30 | 31 | &:last-child { 32 | margin-right: 0; 33 | } 34 | } 35 | 36 | .square { 37 | background-image: url("~@/assets/img/square.png"); 38 | background-size: cover; 39 | 40 | &:hover { 41 | background-image: url("~@/assets/img/square-hover.png"); 42 | } 43 | 44 | &:active { 45 | background-image: url("~@/assets/img/square-click.png"); 46 | } 47 | } 48 | .square-active { 49 | background-image: url("~@/assets/img/square-click.png"); 50 | } 51 | 52 | .round { 53 | background-image: url("~@/assets/img/round.png"); 54 | background-size: cover; 55 | 56 | &:hover { 57 | background-image: url("~@/assets/img/round-hover.png"); 58 | } 59 | 60 | &:active { 61 | background-image: url("~@/assets/img/round-click.png"); 62 | } 63 | } 64 | .round-active { 65 | background-image: url("~@/assets/img/round-click.png"); 66 | } 67 | 68 | .right-top { 69 | background-image: url("~@/assets/img/right-top.png"); 70 | background-size: cover; 71 | 72 | &:hover { 73 | background-image: url("~@/assets/img/right-top-hover.png"); 74 | } 75 | 76 | &:active { 77 | background-image: url("~@/assets/img/right-top-click.png"); 78 | } 79 | } 80 | .right-top-active { 81 | background-image: url("~@/assets/img/right-top-click.png"); 82 | } 83 | 84 | .brush { 85 | background-image: url("~@/assets/img/brush.png"); 86 | background-size: cover; 87 | 88 | &:hover { 89 | background-image: url("~@/assets/img/brush-hover.png"); 90 | } 91 | 92 | &:active { 93 | background-image: url("~@/assets/img/brush-click.png"); 94 | } 95 | } 96 | .brush-active{ 97 | background-image: url("~@/assets/img/brush-click.png"); 98 | } 99 | 100 | .mosaicPen { 101 | background-image: url("~@/assets/img/mosaicPen.png"); 102 | background-size: cover; 103 | 104 | &:hover { 105 | background-image: url("~@/assets/img/mosaicPen-hover.png"); 106 | } 107 | 108 | &:active { 109 | background-image: url("~@/assets/img/mosaicPen-click.png"); 110 | } 111 | } 112 | .mosaicPen-active { 113 | background-image: url("~@/assets/img/mosaicPen-click.png"); 114 | } 115 | 116 | .separateLine { 117 | width: 1px; 118 | background-image: url("~@/assets/img/seperateLine.png"); 119 | background-size: cover; 120 | } 121 | 122 | .text { 123 | background-image: url("~@/assets/img/text.png"); 124 | background-size: cover; 125 | 126 | &:hover { 127 | background-image: url("~@/assets/img/text-hover.png"); 128 | } 129 | 130 | &:active { 131 | background-image: url("~@/assets/img/text-click.png"); 132 | } 133 | } 134 | .text-active { 135 | background-image: url("~@/assets/img/text-click.png"); 136 | } 137 | 138 | .save { 139 | background-image: url("~@/assets/img/save.png"); 140 | background-size: cover; 141 | 142 | &:hover { 143 | background-image: url("~@/assets/img/save-hover.png"); 144 | } 145 | 146 | &:active { 147 | background-image: url("~@/assets/img/save-click.png"); 148 | } 149 | } 150 | 151 | .close { 152 | background-image: url("~@/assets/img/close.png"); 153 | background-size: cover; 154 | 155 | &:hover { 156 | background-image: url("~@/assets/img/close-hover.png"); 157 | } 158 | } 159 | 160 | .undo-disabled { 161 | background-size: cover; 162 | background-image: url("~@/assets/img/undo-disabled.png"); 163 | } 164 | 165 | .undo { 166 | background-size: cover; 167 | background-image: url("~@/assets/img/undo.png"); 168 | 169 | &:hover{ 170 | background-image: url("~@/assets/img/undo-hover.png"); 171 | } 172 | } 173 | 174 | .confirm { 175 | background-image: url("~@/assets/img/confirm.png"); 176 | background-size: cover; 177 | 178 | &:hover { 179 | background-image: url("~@/assets/img/confirm-hover.png"); 180 | } 181 | } 182 | } 183 | 184 | // 三角形角标 185 | .ico-panel { 186 | width: 0; 187 | height: 0; 188 | border-right: 6px solid transparent; 189 | border-left: 6px solid transparent; 190 | border-top: 6px solid #FFFFFF; 191 | position: absolute; 192 | top: 0; 193 | left: 23px; 194 | transform: rotate(180deg); 195 | 196 | img { 197 | width: 100%; 198 | height: 100%; 199 | } 200 | } 201 | 202 | #optionPanel { 203 | height: 20px; 204 | top: 6px; 205 | left: 0; 206 | border-radius: 5px; 207 | background: #ffffff; 208 | z-index: 9999; 209 | position: absolute; 210 | padding: 10px; 211 | 212 | .brush-select-panel{ 213 | height: 20px; 214 | float: left; 215 | 216 | .item-panel { 217 | width: 20px; 218 | height: 20px; 219 | margin-right: 18px; 220 | float: left; 221 | 222 | &:first-child { 223 | margin-left: 2px; 224 | } 225 | &:last-child { 226 | margin-right: 0; 227 | } 228 | } 229 | .brush-small { 230 | background-size: cover; 231 | background-image: url("~@/assets/img/round-normal-small.png"); 232 | 233 | &:hover { 234 | background-image: url("~@/assets/img/round-selected-small.png"); 235 | } 236 | &:active { 237 | background-image: url("~@/assets/img/round-selected-small.png"); 238 | } 239 | } 240 | .brush-small-active { 241 | background-image: url("~@/assets/img/round-selected-small.png"); 242 | } 243 | 244 | .brush-medium { 245 | background-size: cover; 246 | background-image: url("~@/assets/img/round-normal-medium.png"); 247 | 248 | &:hover { 249 | background-image: url("~@/assets/img/round-selected-medium.png"); 250 | } 251 | &:active { 252 | background-image: url("~@/assets/img/round-selected-medium.png"); 253 | } 254 | } 255 | .brush-medium-active{ 256 | background-image: url("~@/assets/img/round-selected-medium.png"); 257 | } 258 | 259 | .brush-big { 260 | background-size: cover; 261 | background-image: url("~@/assets/img/round-normal-big.png"); 262 | 263 | &:hover { 264 | background-image: url("~@/assets/img/round-selected-big.png"); 265 | } 266 | &:active { 267 | background-image: url("~@/assets/img/round-selected-big.png"); 268 | } 269 | } 270 | 271 | .brush-big-active { 272 | background-image: url("~@/assets/img/round-selected-big.png"); 273 | } 274 | } 275 | 276 | .right-panel { 277 | float: left; 278 | display: flex; 279 | align-items: center; 280 | margin-left: 39px; 281 | 282 | // 颜色容器 283 | .color-panel{ 284 | width: 72px; 285 | display: flex; 286 | justify-content: center; 287 | flex-wrap: wrap; 288 | background: #FFFFFF; 289 | border: solid 1px #E5E6E5; 290 | border-radius: 5px; 291 | position: absolute; 292 | top: -225px; 293 | right: 28px; 294 | 295 | .color-item { 296 | width: 62px; 297 | height: 20px; 298 | margin-bottom: 5px; 299 | 300 | &:nth-child(1) { 301 | margin-top: 5px; 302 | background: #F53440; 303 | } 304 | &:nth-child(2) { 305 | background: #F65E95; 306 | } 307 | &:nth-child(3) { 308 | background: #D254CF; 309 | } 310 | &:nth-child(4) { 311 | background: #12A9D7; 312 | } 313 | &:nth-child(5) { 314 | background: #30A345; 315 | } 316 | &:nth-child(6) { 317 | background: #FACF50; 318 | } 319 | &:nth-child(7) { 320 | background: #F66632; 321 | } 322 | &:nth-child(8) { 323 | background: #989998; 324 | } 325 | &:nth-child(9) { 326 | background: #000000; 327 | } 328 | &:nth-child(10) { 329 | border: solid 1px #E5E6E5; 330 | background: #FEFFFF; 331 | } 332 | } 333 | } 334 | // 已选择容器 335 | .color-select-panel { 336 | width: 62px; 337 | height: 20px; 338 | background: #F53340; 339 | border: solid 1px #E5E6E5; 340 | } 341 | 342 | .pull-down-arrow { 343 | width: 15px; 344 | height: 8px; 345 | margin-left: 10px; 346 | background-size: cover; 347 | background-image: url("~@/assets/img/PopoverPullDownArrow.png"); 348 | } 349 | } 350 | 351 | } 352 | 353 | #textInputPanel { 354 | min-width: 20px; 355 | min-height: 20px; 356 | padding: 0; 357 | margin: 0; 358 | box-sizing: border-box; 359 | position: absolute; 360 | outline: none; 361 | z-index: 9999; 362 | font-weight: bold; 363 | top: 0; 364 | left: 0; 365 | border: none; 366 | } 367 | -------------------------------------------------------------------------------- /src/module/main-entrance/EventMonitoring.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted, Ref, nextTick } from "vue"; 2 | import { SetupContext } from "@vue/runtime-core"; 3 | import { 4 | cutOutBoxBorder, 5 | movePositionType, 6 | zoomCutOutBoxReturnType, 7 | drawCutOutBoxReturnType, 8 | positionInfoType, 9 | textInfoType 10 | } from "@/module/type/ComponentType"; 11 | import { drawMasking } from "@/module/split-methods/DrawMasking"; 12 | import html2canvas from "html2canvas"; 13 | import PlugInParameters from "@/module/main-entrance/PlugInParameters"; 14 | import { fixedData, nonNegativeData } from "@/module/common-methords/FixedData"; 15 | import { zoomCutOutBoxPosition } from "@/module/common-methords/ZoomCutOutBoxPosition"; 16 | import { saveBorderArrInfo } from "@/module/common-methords/SaveBorderArrInfo"; 17 | import { drawCutOutBox } from "@/module/split-methods/DrawCutOutBox"; 18 | import InitData from "@/module/main-entrance/InitData"; 19 | import { calculateToolLocation } from "@/module/split-methods/CalculateToolLocation"; 20 | import { drawRectangle } from "@/module/split-methods/DrawRectangle"; 21 | import { drawCircle } from "@/module/split-methods/DrawCircle"; 22 | import { drawPencil, initPencil } from "@/module/split-methods/DrawPencil"; 23 | import { drawText } from "@/module/split-methods/DrawText"; 24 | import { saveCanvasToImage } from "@/module/common-methords/SaveCanvasToImage"; 25 | import { saveCanvasToBase64 } from "@/module/common-methords/SaveCanvasToBase64"; 26 | import { drawMosaic } from "@/module/split-methods/DrawMosaic"; 27 | import { calculateOptionIcoPosition } from "@/module/split-methods/CalculateOptionIcoPosition"; 28 | import { setSelectedClassName } from "@/module/common-methords/SetSelectedClassName"; 29 | import adapter from "webrtc-adapter"; 30 | import { getDrawBoundaryStatus } from "@/module/split-methods/BoundaryJudgment"; 31 | import { getCanvas2dCtx } from "@/module/common-methords/CanvasPatch"; 32 | import { updateContainerMouseStyle } from "@/module/common-methords/UpdateContainerMouseStyle"; 33 | import { DrawArrow } from "@/module/split-methods/DrawArrow"; 34 | 35 | export default class EventMonitoring { 36 | // 当前实例的响应式data数据 37 | private readonly data: InitData; 38 | private emit: ((event: string, ...args: any[]) => void) | undefined; 39 | 40 | // 截图区域canvas容器 41 | private screenShortController: Ref; 42 | // 截图工具栏dom 43 | private toolController: Ref; 44 | // 截图图片存放容器 45 | private screenShortImageController: HTMLCanvasElement | null; 46 | // 截图区域画布 47 | private screenShortCanvas: CanvasRenderingContext2D | undefined; 48 | // 文本区域dom 49 | private textInputController: Ref | undefined; 50 | // 截图工具栏画笔选项dom 51 | private optionController: Ref | undefined; 52 | private optionIcoController: Ref | undefined; 53 | // video容器用于存放屏幕MediaStream流 54 | private readonly videoController: HTMLVideoElement; 55 | private wrcWindowMode = false; 56 | // 图形位置参数 57 | private drawGraphPosition: positionInfoType = { 58 | startX: 0, 59 | startY: 0, 60 | width: 0, 61 | height: 0 62 | }; 63 | // 临时图形位置参数 64 | private tempGraphPosition: positionInfoType = { 65 | startX: 0, 66 | startY: 0, 67 | width: 0, 68 | height: 0 69 | }; 70 | // 裁剪框边框节点坐标事件 71 | private cutOutBoxBorderArr: Array = []; 72 | // 裁剪框顶点边框直径大小 73 | private borderSize = 10; 74 | // 当前操作的边框节点 75 | private borderOption: number | null = null; 76 | // 递增变粗箭头的实现 77 | private drawArrow = new DrawArrow(); 78 | 79 | // 点击裁剪框时的鼠标坐标 80 | private movePosition: movePositionType = { 81 | moveStartX: 0, 82 | moveStartY: 0 83 | }; 84 | 85 | // 裁剪框修剪状态 86 | private draggingTrim = false; 87 | // 裁剪框拖拽状态 88 | private dragging = false; 89 | // 鼠标是否在裁剪框内 90 | private mouseInsideCropBox = false; 91 | 92 | // 鼠标点击状态 93 | private clickFlag = false; 94 | // 鼠标拖动状态 95 | private dragFlag = false; 96 | // 单击截取屏启用状态 97 | private clickCutFullScreen = false; 98 | // 全屏截取状态 99 | private getFullScreenStatus = false; 100 | // 上一个裁剪框坐标信息 101 | private drawGraphPrevX = 0; 102 | private drawGraphPrevY = 0; 103 | 104 | // 当前点击的工具栏条目 105 | private toolName = ""; 106 | private fontSize = 17; 107 | // 撤销点击次数 108 | private undoClickNum = 0; 109 | // 最大可撤销次数 110 | private maxUndoNum = 15; 111 | // 马赛克涂抹区域大小 112 | private degreeOfBlur = 5; 113 | private dpr = window.devicePixelRatio || 1; 114 | // 截全屏时工具栏展示的位置要减去的高度 115 | private fullScreenDiffHeight = 60; 116 | private history: Array> = []; 117 | // 文本输入框位置 118 | private textInputPosition: { mouseX: number; mouseY: number } = { 119 | mouseX: 0, 120 | mouseY: 0 121 | }; 122 | // 是否隐藏页面滚动条 123 | private hiddenScrollBar = { 124 | color: "#000000", 125 | fillState: false, 126 | state: false, 127 | fillWidth: 0, 128 | fillHeight: 0 129 | }; 130 | private textInfo: textInfoType = { 131 | positionX: 0, 132 | positionY: 0, 133 | color: "", 134 | size: 0 135 | }; 136 | 137 | constructor(props: Record, context: SetupContext) { 138 | // 实例化响应式data 139 | this.data = new InitData(); 140 | // 获取截图区域canvas容器 141 | this.screenShortController = this.data.getScreenShortController(); 142 | this.toolController = this.data.getToolController(); 143 | this.textInputController = this.data.getTextInputController(); 144 | this.optionController = this.data.getOptionController(); 145 | this.optionIcoController = this.data.getOptionIcoController(); 146 | this.videoController = document.createElement("video"); 147 | this.videoController.autoplay = true; 148 | this.screenShortImageController = document.createElement("canvas"); 149 | // 设置实例与属性 150 | this.data.setPropsData(context.emit); 151 | 152 | onMounted(() => { 153 | this.emit = this.data.getEmit(); 154 | const plugInParameters = new PlugInParameters(); 155 | this.hiddenScrollBar = plugInParameters.getHiddenScrollBarInfo(); 156 | if (this.hiddenScrollBar.state) { 157 | // 设置页面宽高并隐藏滚动条 158 | this.updateScrollbarState(); 159 | } 160 | // 单击截屏启用状态 161 | this.clickCutFullScreen = plugInParameters.getClickCutFullScreenStatus(); 162 | // 设置需要隐藏的工具栏图标 163 | this.data.setHiddenToolIco(plugInParameters.getHiddenToolIco()); 164 | if (!plugInParameters.getWebRtcStatus()) { 165 | this.h2cMode(plugInParameters); 166 | return; 167 | } 168 | // 是否启用窗口截图模式 169 | this.wrcWindowMode = plugInParameters.getWrcWindowMode(); 170 | this.wrcMode(plugInParameters); 171 | }); 172 | 173 | onUnmounted(() => { 174 | // 初始化initData中的数据 175 | this.data.setInitStatus(true); 176 | }); 177 | } 178 | 179 | private wrcMode(plugInParameters: PlugInParameters) { 180 | if (this.screenShortImageController == null) return; 181 | // 设置截图图片存放容器宽高 182 | this.screenShortImageController.width = parseFloat( 183 | window.getComputedStyle(document.body).width 184 | ); 185 | this.screenShortImageController.height = parseFloat( 186 | window.getComputedStyle(document.body).height 187 | ); 188 | this.startCapture().then(() => { 189 | setTimeout(() => { 190 | if ( 191 | this.screenShortController.value == null || 192 | this.screenShortImageController == null 193 | ) { 194 | return; 195 | } 196 | const containerWidth = this.screenShortImageController?.width; 197 | const containerHeight = this.screenShortImageController?.height; 198 | let imgContainerWidth = containerWidth; 199 | let imgContainerHeight = containerHeight; 200 | if (this.wrcWindowMode) { 201 | imgContainerWidth = containerWidth * this.dpr; 202 | imgContainerHeight = containerHeight * this.dpr; 203 | } 204 | // 修正截图容器尺寸 205 | const context = getCanvas2dCtx( 206 | this.screenShortController.value, 207 | containerWidth, 208 | containerHeight 209 | ); 210 | // 修正图像容器画布尺寸并返回 211 | const imgContext = getCanvas2dCtx( 212 | this.screenShortImageController, 213 | imgContainerWidth, 214 | imgContainerHeight 215 | ); 216 | if (context == null || imgContext == null) return; 217 | 218 | const { videoWidth, videoHeight } = this.videoController; 219 | if (this.wrcWindowMode) { 220 | // 从窗口视频流中获取body内容 221 | const bodyImgData = this.getWindowContentData( 222 | videoWidth, 223 | videoHeight, 224 | containerWidth * this.dpr, 225 | containerHeight * this.dpr 226 | ); 227 | if (bodyImgData == null) return; 228 | // 将body内容绘制到图片容器里 229 | imgContext.putImageData(bodyImgData, 0, 0); 230 | } else { 231 | // 对webrtc源提供的图像宽高进行修复 232 | let fixWidth = containerWidth; 233 | let fixHeight = (videoHeight * containerWidth) / videoWidth; 234 | if (fixHeight > containerHeight) { 235 | fixWidth = (containerWidth * containerHeight) / fixHeight; 236 | fixHeight = containerHeight; 237 | } 238 | // 将获取到的屏幕流绘制到图片容器里 239 | imgContext?.drawImage( 240 | this.videoController, 241 | 0, 242 | 0, 243 | fixWidth, 244 | fixHeight 245 | ); 246 | // 隐藏滚动条会出现部分内容未截取到,需要进行修复 247 | const diffHeight = containerHeight - fixHeight; 248 | if ( 249 | this.hiddenScrollBar.state && 250 | diffHeight > 0 && 251 | this.hiddenScrollBar.fillState 252 | ) { 253 | // 填充容器的剩余部分 254 | imgContext.beginPath(); 255 | let fillWidth = containerWidth; 256 | let fillHeight = diffHeight; 257 | if (this.hiddenScrollBar.fillWidth > 0) { 258 | fillWidth = this.hiddenScrollBar.fillWidth; 259 | } 260 | if (this.hiddenScrollBar.fillHeight > 0) { 261 | fillHeight = this.hiddenScrollBar.fillHeight; 262 | } 263 | imgContext.rect(0, fixHeight, fillWidth, fillHeight); 264 | imgContext.fillStyle = this.hiddenScrollBar.color; 265 | imgContext.fill(); 266 | } 267 | } 268 | 269 | // 存储屏幕截图 270 | this.data.setScreenShortImageController( 271 | this.screenShortImageController 272 | ); 273 | 274 | // 将屏幕截图绘制到截图容器中 275 | this.drawContent(context, this.screenShortController.value); 276 | 277 | // 调整截屏容器层级 278 | this.screenShortController.value.style.zIndex = 279 | plugInParameters.getLevel() + ""; 280 | // 调整截图工具栏层级 281 | if (this.toolController.value == null) return; 282 | this.toolController.value.style.zIndex = `${plugInParameters.getLevel() + 283 | 1}`; 284 | // 停止捕捉屏幕 285 | this.stopCapture(); 286 | }, 500); 287 | }); 288 | } 289 | 290 | private h2cMode(plugInParameters: PlugInParameters) { 291 | if (this.screenShortImageController == null) return; 292 | const viewSize = { 293 | width: parseFloat(window.getComputedStyle(document.body).width), 294 | height: parseFloat(window.getComputedStyle(document.body).height) 295 | }; 296 | // 设置截图图片存放容器宽高 297 | this.screenShortImageController.width = viewSize.width; 298 | this.screenShortImageController.height = viewSize.height; 299 | // 获取截图区域画canvas容器画布 300 | if (this.screenShortController.value == null) return; 301 | const canvasContext = getCanvas2dCtx( 302 | this.screenShortController.value, 303 | this.screenShortImageController.width, 304 | this.screenShortImageController.height 305 | ); 306 | if (canvasContext == null) return; 307 | html2canvas(document.body, { 308 | useCORS: plugInParameters.getEnableCORSStatus(), 309 | proxy: plugInParameters.getProxyAddress() 310 | }).then(canvas => { 311 | // 装载截图的dom为null则退出 312 | if (this.screenShortController.value == null) return; 313 | 314 | // 调整截屏容器层级 315 | this.screenShortController.value.style.zIndex = 316 | plugInParameters.getLevel() + ""; 317 | // 调整截图工具栏层级 318 | if (this.toolController.value == null) return; 319 | this.toolController.value.style.zIndex = `${plugInParameters.getLevel() + 320 | 1}`; 321 | 322 | // 存放html2canvas截取的内容 323 | this.screenShortImageController = canvas; 324 | // 存储屏幕截图 325 | this.data.setScreenShortImageController(canvas); 326 | // 将屏幕截图绘制到截图容器中 327 | this.drawContent(canvasContext, this.screenShortController.value); 328 | }); 329 | } 330 | 331 | // 开始捕捉屏幕 332 | private startCapture = async () => { 333 | if (this.screenShortImageController == null) return; 334 | let captureStream = null; 335 | let mediaWidth = this.screenShortImageController.width * this.dpr; 336 | let mediaHeight = this.screenShortImageController.height * this.dpr; 337 | let curTabState = true; 338 | let displayConfig = {}; 339 | // 窗口模式启用时则 340 | if (this.wrcWindowMode) { 341 | mediaWidth = window.screen.width * this.dpr; 342 | mediaHeight = window.screen.height * this.dpr; 343 | curTabState = false; 344 | displayConfig = { 345 | displaySurface: "window" 346 | }; 347 | } 348 | 349 | try { 350 | // 捕获屏幕 351 | captureStream = await navigator.mediaDevices.getDisplayMedia({ 352 | audio: false, 353 | video: { 354 | width: mediaWidth, 355 | height: mediaHeight, 356 | ...displayConfig 357 | }, 358 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 359 | // @ts-ignore 360 | preferCurrentTab: curTabState 361 | }); 362 | // 将MediaStream输出至video标签 363 | this.videoController.srcObject = captureStream; 364 | } catch (err) { 365 | // 销毁组件 366 | this.resetComponent(); 367 | if (this.emit) { 368 | this.emit("webrtc-error", err); 369 | } 370 | throw "浏览器不支持webrtc或者用户未授权, 浏览器名称" + 371 | adapter.browserDetails.browser + 372 | ",浏览器版本号" + 373 | adapter.browserDetails.version + 374 | err; 375 | } 376 | return captureStream; 377 | }; 378 | 379 | // 停止捕捉屏幕 380 | private stopCapture = () => { 381 | const srcObject = this.videoController.srcObject; 382 | if (srcObject && "getTracks" in srcObject) { 383 | const tracks = srcObject.getTracks(); 384 | tracks.forEach(track => track.stop()); 385 | this.videoController.srcObject = null; 386 | } 387 | }; 388 | 389 | private drawContent( 390 | canvasContext: CanvasRenderingContext2D, 391 | screenShortController: HTMLCanvasElement 392 | ) { 393 | // 赋值截图区域canvas画布 394 | this.screenShortCanvas = canvasContext; 395 | // 绘制蒙层 396 | drawMasking( 397 | canvasContext, 398 | screenShortController.width, 399 | screenShortController.height 400 | ); 401 | 402 | // 添加监听 403 | screenShortController.addEventListener("mousedown", this.mouseDownEvent); 404 | screenShortController.addEventListener("mousemove", this.mouseMoveEvent); 405 | screenShortController.addEventListener("mouseup", this.mouseUpEvent); 406 | } 407 | 408 | // 鼠标按下事件 409 | private mouseDownEvent = (event: MouseEvent) => { 410 | // 非鼠标左键按下则终止 411 | if (event.button != 0) return; 412 | // 当前操作的是撤销 413 | if (this.toolName == "undo") return; 414 | this.dragging = true; 415 | this.clickFlag = true; 416 | const mouseX = nonNegativeData(event.offsetX); 417 | const mouseY = nonNegativeData(event.offsetY); 418 | 419 | // 如果当前操作的是截图工具栏 420 | if (this.data.getToolClickStatus().value) { 421 | // 记录当前鼠标开始坐标 422 | this.drawGraphPosition.startX = mouseX; 423 | this.drawGraphPosition.startY = mouseY; 424 | } 425 | // 当前操作的是画笔 426 | if (this.toolName == "brush" && this.screenShortCanvas) { 427 | // 初始化画笔 428 | initPencil(this.screenShortCanvas, mouseX, mouseY); 429 | } 430 | // 当前操作的文本 431 | if ( 432 | this.toolName == "text" && 433 | this.textInputController?.value && 434 | this.screenShortController?.value && 435 | this.screenShortCanvas 436 | ) { 437 | if (!this.mouseInsideCropBox) { 438 | return; 439 | } 440 | // 显示文本输入区域 441 | this.data.setTextStatus(true); 442 | // 判断输入框位置是否变化 443 | if ( 444 | this.textInputPosition.mouseX != 0 && 445 | this.textInputPosition.mouseY != 0 && 446 | this.textInputPosition.mouseX != mouseX && 447 | this.textInputPosition.mouseY != mouseY 448 | ) { 449 | drawText( 450 | this.textInputController.value?.innerText, 451 | this.textInputPosition.mouseX, 452 | this.textInputPosition.mouseY, 453 | this.data.getSelectedColor().value, 454 | this.fontSize, 455 | this.screenShortCanvas 456 | ); 457 | // 清空文本输入区域的内容 458 | this.textInputController.value.innerHTML = ""; 459 | // 保存绘制记录 460 | this.addHistory(); 461 | } 462 | // 修改文本区域位置 463 | this.textInputController.value.style.left = mouseX + "px"; 464 | this.textInputController.value.style.fontSize = this.fontSize + "px"; 465 | this.textInputController.value.style.fontFamily = "none"; 466 | this.textInputController.value.style.color = this.data.getSelectedColor().value; 467 | setTimeout(() => { 468 | // 获取焦点 469 | if (this.textInputController?.value) { 470 | // 获取输入框容器的高度 471 | const containerHeight = this.textInputController.value.offsetHeight; 472 | // 输入框容器y轴的位置需要在坐标的基础上再加上容器高度的一半,容器的位置就正好居中于光标 473 | // canvas渲染的时候就不会出现位置不一致的问题了 474 | const textMouseY = mouseY - Math.floor(containerHeight / 2); 475 | this.textInputController.value.style.top = textMouseY + "px"; 476 | // 获取焦点 477 | this.textInputController.value.focus(); 478 | // 记录当前输入框位置 479 | this.textInputPosition = { mouseX: mouseX, mouseY: mouseY }; 480 | this.textInfo = { 481 | positionX: mouseX, 482 | positionY: mouseY, 483 | color: this.data.getSelectedColor().value, 484 | size: this.fontSize 485 | }; 486 | } 487 | }); 488 | } 489 | 490 | // 如果操作的是裁剪框 491 | if (this.borderOption) { 492 | // 设置为拖动状态 493 | this.draggingTrim = true; 494 | // 记录移动时的起始点坐标 495 | this.movePosition.moveStartX = mouseX; 496 | this.movePosition.moveStartY = mouseY; 497 | } else { 498 | // 保存当前裁剪框的坐标 499 | this.drawGraphPrevX = this.drawGraphPosition.startX; 500 | this.drawGraphPrevY = this.drawGraphPosition.startY; 501 | // 绘制裁剪框,记录当前鼠标开始坐标 502 | this.drawGraphPosition.startX = mouseX; 503 | this.drawGraphPosition.startY = mouseY; 504 | } 505 | }; 506 | 507 | // 鼠标移动事件 508 | private mouseMoveEvent = (event: MouseEvent) => { 509 | if ( 510 | this.screenShortCanvas == null || 511 | this.screenShortController.value == null || 512 | this.toolName == "undo" 513 | ) { 514 | return; 515 | } 516 | 517 | // 工具栏未选择且鼠标处于按下状态时 518 | if (!this.data.getToolClickStatus().value && this.dragging) { 519 | // 修改拖动状态为true; 520 | this.dragFlag = true; 521 | // 隐藏截图工具栏 522 | this.data.setToolStatus(false); 523 | } 524 | this.clickFlag = false; 525 | // 获取当前绘制中的工具位置信息 526 | const { startX, startY, width, height } = this.drawGraphPosition; 527 | // 获取当前鼠标坐标 528 | const currentX = nonNegativeData(event.offsetX); 529 | const currentY = nonNegativeData(event.offsetY); 530 | // 绘制中工具的临时宽高 531 | const tempWidth = currentX - startX; 532 | const tempHeight = currentY - startY; 533 | // 工具栏绘制 534 | if (this.data.getToolClickStatus().value && this.dragging) { 535 | // 获取裁剪框位置信息 536 | const cutBoxPosition = this.data.getCutOutBoxPosition(); 537 | // 绘制中工具的起始x、y坐标不能小于裁剪框的起始坐标 538 | // 绘制中工具的起始x、y坐标不能大于裁剪框的结束标作 539 | // 当前鼠标的x坐标不能小于裁剪框起始x坐标,不能大于裁剪框的结束坐标 540 | // 当前鼠标的y坐标不能小于裁剪框起始y坐标,不能大于裁剪框的结束坐标 541 | if ( 542 | !getDrawBoundaryStatus(startX, startY, cutBoxPosition) || 543 | !getDrawBoundaryStatus(currentX, currentY, cutBoxPosition) 544 | ) 545 | return; 546 | // 当前操作的不是马赛克则显示最后一次画布绘制时的状态 547 | if (this.toolName != "mosaicPen") { 548 | this.showLastHistory(); 549 | } 550 | switch (this.toolName) { 551 | case "square": 552 | drawRectangle( 553 | startX, 554 | startY, 555 | tempWidth, 556 | tempHeight, 557 | this.data.getSelectedColor().value, 558 | this.data.getPenSize().value, 559 | this.screenShortCanvas 560 | ); 561 | break; 562 | case "round": 563 | drawCircle( 564 | this.screenShortCanvas, 565 | currentX, 566 | currentY, 567 | startX, 568 | startY, 569 | this.data.getPenSize().value, 570 | this.data.getSelectedColor().value 571 | ); 572 | break; 573 | case "right-top": 574 | this.drawArrow.draw( 575 | this.screenShortCanvas, 576 | startX, 577 | startY, 578 | currentX, 579 | currentY, 580 | this.data.getSelectedColor().value 581 | ); 582 | break; 583 | case "brush": 584 | // 画笔绘制 585 | drawPencil( 586 | this.screenShortCanvas, 587 | currentX, 588 | currentY, 589 | this.data.getPenSize().value, 590 | this.data.getSelectedColor().value 591 | ); 592 | break; 593 | case "mosaicPen": 594 | // 绘制马赛克,为了确保鼠标位置在绘制区域中间,所以对x、y坐标进行-10处理 595 | drawMosaic( 596 | currentX - 10, 597 | currentY - 10, 598 | this.data.getPenSize().value, 599 | this.degreeOfBlur, 600 | this.screenShortCanvas 601 | ); 602 | break; 603 | default: 604 | break; 605 | } 606 | return; 607 | } 608 | // 执行裁剪框操作函数 609 | this.operatingCutOutBox( 610 | currentX, 611 | currentY, 612 | startX, 613 | startY, 614 | width, 615 | height, 616 | this.screenShortCanvas 617 | ); 618 | // 如果鼠标未点击或者当前操作的是裁剪框都return 619 | if (!this.dragging || this.draggingTrim) return; 620 | // 绘制裁剪框 621 | this.tempGraphPosition = drawCutOutBox( 622 | startX, 623 | startY, 624 | tempWidth, 625 | tempHeight, 626 | this.screenShortCanvas, 627 | this.borderSize, 628 | this.screenShortController.value as HTMLCanvasElement, 629 | this.screenShortImageController as HTMLCanvasElement 630 | ) as drawCutOutBoxReturnType; 631 | }; 632 | 633 | /** 634 | * 从窗口数据流中截取页面body内容 635 | * @param videoWidth 窗口宽度 636 | * @param videoHeight 窗口高度 637 | * @param containerWidth body内容宽度 638 | * @param containerHeight body内容高度 639 | * @private 640 | */ 641 | private getWindowContentData( 642 | videoWidth: number, 643 | videoHeight: number, 644 | containerWidth: number, 645 | containerHeight: number 646 | ) { 647 | const videoCanvas = document.createElement("canvas"); 648 | videoCanvas.width = videoWidth; 649 | videoCanvas.height = videoHeight; 650 | const videoContext = getCanvas2dCtx(videoCanvas, videoWidth, videoHeight); 651 | if (videoContext) { 652 | videoContext.drawImage(this.videoController, 0, 0); 653 | const startX = 0; 654 | const startY = videoHeight - containerHeight; 655 | const width = containerWidth; 656 | const height = videoHeight - startY; 657 | // 获取裁剪框区域图片信息; 658 | return videoContext.getImageData( 659 | startX * this.dpr, 660 | startY * this.dpr, 661 | width * this.dpr, 662 | height * this.dpr 663 | ); 664 | } 665 | return null; 666 | } 667 | 668 | // 鼠标抬起事件 669 | private mouseUpEvent = () => { 670 | // 当前操作的是撤销 671 | if (this.toolName == "undo") return; 672 | // 绘制结束 673 | this.dragging = false; 674 | this.draggingTrim = false; 675 | if ( 676 | this.screenShortController.value == null || 677 | this.screenShortCanvas == null || 678 | this.screenShortImageController == null 679 | ) { 680 | return; 681 | } 682 | 683 | // 工具栏未点击且鼠标未拖动且单击截屏状态为false则复原裁剪框位置 684 | if ( 685 | !this.data.getToolClickStatus().value && 686 | !this.dragFlag && 687 | !this.clickCutFullScreen 688 | ) { 689 | // 复原裁剪框的坐标 690 | this.drawGraphPosition.startX = this.drawGraphPrevX; 691 | this.drawGraphPosition.startY = this.drawGraphPrevY; 692 | return; 693 | } 694 | // 调用者尚未拖拽生成选区 695 | // 鼠标尚未拖动 696 | // 单击截取屏幕状态为true 697 | // 则截取整个屏幕 698 | const cutBoxPosition = this.data.getCutOutBoxPosition(); 699 | if ( 700 | cutBoxPosition.width === 0 && 701 | cutBoxPosition.height === 0 && 702 | cutBoxPosition.startX === 0 && 703 | cutBoxPosition.startY === 0 && 704 | !this.dragFlag && 705 | this.clickCutFullScreen 706 | ) { 707 | this.getFullScreenStatus = true; 708 | // 设置裁剪框位置为全屏 709 | this.tempGraphPosition = drawCutOutBox( 710 | 0, 711 | 0, 712 | this.screenShortController.value.width - this.borderSize / 2, 713 | this.screenShortController.value.height - this.borderSize / 2, 714 | this.screenShortCanvas, 715 | this.borderSize, 716 | this.screenShortController.value, 717 | this.screenShortImageController 718 | ) as drawCutOutBoxReturnType; 719 | } 720 | if (this.data.getToolClickStatus().value) { 721 | // 保存绘制记录 722 | this.addHistory(); 723 | return; 724 | } 725 | // 保存绘制后的图形位置信息 726 | this.drawGraphPosition = this.tempGraphPosition; 727 | // 如果工具栏未点击则保存裁剪框位置 728 | if (!this.data.getToolClickStatus().value) { 729 | const { startX, startY, width, height } = this.drawGraphPosition; 730 | this.data.setCutOutBoxPosition(startX, startY, width, height); 731 | } 732 | // 保存边框节点信息 733 | this.cutOutBoxBorderArr = saveBorderArrInfo( 734 | this.borderSize, 735 | this.drawGraphPosition 736 | ); 737 | if (this.screenShortController.value != null) { 738 | // 修改鼠标状态为拖动 739 | this.screenShortController.value.style.cursor = "move"; 740 | // 复原拖动状态 741 | this.dragFlag = false; 742 | // 显示截图工具栏 743 | this.data.setToolStatus(true); 744 | nextTick().then(() => { 745 | if ( 746 | this.toolController.value != null && 747 | this.screenShortController.value 748 | ) { 749 | // 计算截图工具栏位置 750 | const toolLocation = calculateToolLocation( 751 | this.drawGraphPosition, 752 | this.toolController.value?.offsetWidth, 753 | this.screenShortController.value.width / this.dpr 754 | ); 755 | // 当前截取的是全屏,则修改工具栏的位置到截图容器最底部,防止超出 756 | if (this.getFullScreenStatus) { 757 | const containerHeight = parseInt( 758 | this.screenShortController.value.style.height 759 | ); 760 | // 重新计算工具栏的x轴位置 761 | const toolPositionX = 762 | (this.drawGraphPosition.width / this.dpr - 763 | this.toolController.value.offsetWidth) / 764 | 2; 765 | toolLocation.mouseY = containerHeight - this.fullScreenDiffHeight; 766 | toolLocation.mouseX = toolPositionX; 767 | } 768 | 769 | if (this.screenShortController.value) { 770 | const containerHeight = parseInt( 771 | this.screenShortController.value.style.height 772 | ); 773 | 774 | // 工具栏的位置超出截图容器时,调整工具栏位置防止超出 775 | if (toolLocation.mouseY > containerHeight - 64) { 776 | toolLocation.mouseY -= this.drawGraphPosition.height + 64; 777 | 778 | // 超出屏幕顶部时 779 | if (toolLocation.mouseY < 0) { 780 | const containerHeight = parseInt( 781 | this.screenShortController.value.style.height 782 | ); 783 | toolLocation.mouseY = 784 | containerHeight - this.fullScreenDiffHeight; 785 | } 786 | } 787 | } 788 | 789 | // 设置截图工具栏位置 790 | this.data.setToolInfo(toolLocation.mouseX, toolLocation.mouseY); 791 | // 状态重置 792 | this.getFullScreenStatus = false; 793 | } 794 | }); 795 | } 796 | }; 797 | 798 | /** 799 | * 操作裁剪框 800 | * @param currentX 裁剪框当前x轴坐标 801 | * @param currentY 裁剪框当前y轴坐标 802 | * @param startX 鼠标x轴坐标 803 | * @param startY 鼠标y轴坐标 804 | * @param width 裁剪框宽度 805 | * @param height 裁剪框高度 806 | * @param context 需要进行绘制的canvas画布 807 | * @private 808 | */ 809 | private operatingCutOutBox( 810 | currentX: number, 811 | currentY: number, 812 | startX: number, 813 | startY: number, 814 | width: number, 815 | height: number, 816 | context: CanvasRenderingContext2D 817 | ) { 818 | // canvas元素不存在 819 | if (this.screenShortController.value == null) { 820 | return; 821 | } 822 | // 获取鼠标按下时的坐标 823 | const { moveStartX, moveStartY } = this.movePosition; 824 | 825 | // 裁剪框边框节点事件存在且裁剪框未进行操作,则对鼠标样式进行修改 826 | if (this.cutOutBoxBorderArr.length > 0 && !this.draggingTrim) { 827 | // 标识鼠标是否在裁剪框内 828 | let flag = false; 829 | // 判断鼠标位置 830 | context.beginPath(); 831 | for (let i = 0; i < this.cutOutBoxBorderArr.length; i++) { 832 | context.rect( 833 | this.cutOutBoxBorderArr[i].x, 834 | this.cutOutBoxBorderArr[i].y, 835 | this.cutOutBoxBorderArr[i].width, 836 | this.cutOutBoxBorderArr[i].height 837 | ); 838 | // 当前坐标点处于8个可操作点上,修改鼠标指针样式 839 | if (context.isPointInPath(currentX * this.dpr, currentY * this.dpr)) { 840 | switch (this.cutOutBoxBorderArr[i].index) { 841 | case 1: 842 | if (this.data.getToolClickStatus().value) { 843 | // 修改截图容器内的鼠标样式 844 | updateContainerMouseStyle( 845 | this.screenShortController.value, 846 | this.toolName 847 | ); 848 | } else { 849 | this.screenShortController.value.style.cursor = "move"; 850 | } 851 | break; 852 | case 2: 853 | // 工具栏被点击则不改变指针样式 854 | if (this.data.getToolClickStatus().value) break; 855 | this.screenShortController.value.style.cursor = "ns-resize"; 856 | break; 857 | case 3: 858 | // 工具栏被点击则不改变指针样式 859 | if (this.data.getToolClickStatus().value) break; 860 | this.screenShortController.value.style.cursor = "ew-resize"; 861 | break; 862 | case 4: 863 | // 工具栏被点击则不改变指针样式 864 | if (this.data.getToolClickStatus().value) break; 865 | this.screenShortController.value.style.cursor = "nwse-resize"; 866 | break; 867 | case 5: 868 | // 工具栏被点击则不改变指针样式 869 | if (this.data.getToolClickStatus().value) break; 870 | this.screenShortController.value.style.cursor = "nesw-resize"; 871 | break; 872 | default: 873 | break; 874 | } 875 | this.borderOption = this.cutOutBoxBorderArr[i].option; 876 | flag = true; 877 | break; 878 | } 879 | } 880 | context.closePath(); 881 | this.mouseInsideCropBox = flag; 882 | if (!flag) { 883 | // 鼠标移出裁剪框重置鼠标样式 884 | this.screenShortController.value.style.cursor = "default"; 885 | // 重置当前操作的边框节点为null 886 | this.borderOption = null; 887 | } 888 | } 889 | 890 | // 裁剪框正在被操作 891 | if (this.draggingTrim) { 892 | // 当前操作节点为1时则为移动裁剪框 893 | if (this.borderOption === 1) { 894 | // 计算要移动的x轴坐标 895 | let x = fixedData( 896 | currentX - (moveStartX - startX), 897 | width, 898 | this.screenShortController.value.width 899 | ); 900 | // 计算要移动的y轴坐标 901 | let y = fixedData( 902 | currentY - (moveStartY - startY), 903 | height, 904 | this.screenShortController.value.height 905 | ); 906 | // 计算画布面积 907 | const containerWidth = 908 | this.screenShortController.value.width / this.dpr; 909 | const containerHeight = 910 | this.screenShortController.value.height / this.dpr; 911 | // 计算裁剪框在画布上所占的面积 912 | const cutOutBoxSizeX = x + width; 913 | const cutOutBoxSizeY = y + height; 914 | // 超出画布的可视区域,进行位置修正 915 | if (cutOutBoxSizeX > containerWidth) { 916 | x = containerWidth - width; 917 | } 918 | if (cutOutBoxSizeY > containerHeight) { 919 | y = containerHeight - height; 920 | } 921 | 922 | // 重新绘制裁剪框 923 | this.tempGraphPosition = drawCutOutBox( 924 | x, 925 | y, 926 | width, 927 | height, 928 | context, 929 | this.borderSize, 930 | this.screenShortController.value as HTMLCanvasElement, 931 | this.screenShortImageController as HTMLCanvasElement 932 | ) as drawCutOutBoxReturnType; 933 | } else { 934 | // 裁剪框其他8个点的拖拽事件 935 | const { 936 | tempStartX, 937 | tempStartY, 938 | tempWidth, 939 | tempHeight 940 | } = zoomCutOutBoxPosition( 941 | currentX, 942 | currentY, 943 | startX, 944 | startY, 945 | width, 946 | height, 947 | this.borderOption as number 948 | ) as zoomCutOutBoxReturnType; 949 | // 绘制裁剪框 950 | this.tempGraphPosition = drawCutOutBox( 951 | tempStartX, 952 | tempStartY, 953 | tempWidth, 954 | tempHeight, 955 | context, 956 | this.borderSize, 957 | this.screenShortController.value as HTMLCanvasElement, 958 | this.screenShortImageController as HTMLCanvasElement 959 | ) as drawCutOutBoxReturnType; 960 | } 961 | } 962 | } 963 | 964 | /** 965 | * 裁剪框工具栏点击事件 966 | * @param toolName 967 | * @param index 968 | * @param mouseEvent 969 | */ 970 | public toolClickEvent = ( 971 | toolName: string, 972 | index: number, 973 | mouseEvent: MouseEvent 974 | ) => { 975 | // 更新当前点击的工具栏条目 976 | this.toolName = toolName; 977 | const screenShortController = this.data.getScreenShortController(); 978 | const ScreenShortImageController = this.data.getScreenShortImageController(); 979 | if ( 980 | screenShortController.value == null || 981 | ScreenShortImageController == null 982 | ) 983 | return; 984 | // 获取canvas容器 985 | const screenShortCanvas = screenShortController.value.getContext( 986 | "2d" 987 | ) as CanvasRenderingContext2D; 988 | // 工具栏尚未点击,当前属于首次点击,重新绘制一个无像素点的裁剪框 989 | if (!this.data.getToolClickStatus().value) { 990 | // 获取裁剪框位置信息 991 | const cutBoxPosition = this.data.getCutOutBoxPosition(); 992 | // 开始绘制无像素点裁剪框 993 | drawCutOutBox( 994 | cutBoxPosition.startX, 995 | cutBoxPosition.startY, 996 | cutBoxPosition.width, 997 | cutBoxPosition.height, 998 | screenShortCanvas, 999 | this.borderSize, 1000 | screenShortController.value, 1001 | ScreenShortImageController, 1002 | false 1003 | ); 1004 | } 1005 | this.data.setToolName(toolName); 1006 | // 为当前点击项添加选中时的class名 1007 | setSelectedClassName(mouseEvent, index, false); 1008 | if (toolName != "text") { 1009 | // 显示画笔选择工具栏 1010 | this.data.setOptionStatus(true); 1011 | // 设置画笔选择工具栏三角形角标位置 1012 | this.data.setOptionIcoPosition(calculateOptionIcoPosition(index)); 1013 | } else { 1014 | // 隐藏画笔工具栏 1015 | this.data.setOptionStatus(false); 1016 | } 1017 | // 清空文本输入区域的内容并隐藏文本输入框 1018 | if ( 1019 | this.textInputController?.value != null && 1020 | this.data.getTextStatus() && 1021 | this.screenShortCanvas 1022 | ) { 1023 | const text = this.textInputController.value.innerText; 1024 | if (text && text !== "") { 1025 | const { positionX, positionY, color, size } = this.textInfo; 1026 | drawText( 1027 | text, 1028 | positionX, 1029 | positionY, 1030 | color, 1031 | size, 1032 | this.screenShortCanvas 1033 | ); 1034 | // 添加历史记录 1035 | this.addHistory(); 1036 | } 1037 | this.textInputController.value.innerHTML = ""; 1038 | this.data.setTextStatus(false); 1039 | } 1040 | // 初始化点击状态 1041 | this.dragging = false; 1042 | this.draggingTrim = false; 1043 | 1044 | // 保存图片 1045 | if (toolName == "save") { 1046 | this.getCanvasImgData(true); 1047 | } 1048 | // 销毁组件 1049 | if (toolName == "close") { 1050 | this.resetComponent(); 1051 | } 1052 | // 确认截图 1053 | if (toolName == "confirm" && this.screenShortCanvas && this.emit) { 1054 | const base64 = this.getCanvasImgData(false); 1055 | this.emit("get-image-data", base64); 1056 | } 1057 | // 撤销 1058 | if (toolName == "undo") { 1059 | // 隐藏画笔选项工具栏 1060 | this.data.setOptionStatus(false); 1061 | this.takeOutHistory(); 1062 | } 1063 | 1064 | // 设置裁剪框工具栏为点击状态 1065 | this.data.setToolClickStatus(true); 1066 | }; 1067 | 1068 | /** 1069 | * 保存当前画布状态 1070 | * @private 1071 | */ 1072 | private addHistory() { 1073 | if ( 1074 | this.screenShortCanvas != null && 1075 | this.screenShortController.value != null 1076 | ) { 1077 | // 获取canvas画布与容器 1078 | const context = this.screenShortCanvas; 1079 | const controller = this.screenShortController.value; 1080 | if (this.history.length > this.maxUndoNum) { 1081 | // 删除最早的一条画布记录 1082 | this.history.shift(); 1083 | } 1084 | // 保存当前画布状态 1085 | this.history.push({ 1086 | data: context.getImageData(0, 0, controller.width, controller.height) 1087 | }); 1088 | // 启用撤销按钮 1089 | this.data.setUndoStatus(true); 1090 | } 1091 | } 1092 | 1093 | /** 1094 | * 显示最新的画布状态 1095 | * @private 1096 | */ 1097 | private showLastHistory() { 1098 | if (this.screenShortCanvas != null) { 1099 | const context = this.screenShortCanvas; 1100 | if (this.history.length <= 0) { 1101 | this.addHistory(); 1102 | } 1103 | context.putImageData(this.history[this.history.length - 1]["data"], 0, 0); 1104 | } 1105 | } 1106 | 1107 | /** 1108 | * 取出一条历史记录 1109 | */ 1110 | private takeOutHistory() { 1111 | this.history.pop(); 1112 | if (this.screenShortCanvas != null && this.history.length > 0) { 1113 | const context = this.screenShortCanvas; 1114 | context.putImageData(this.history[this.history.length - 1]["data"], 0, 0); 1115 | } 1116 | 1117 | this.undoClickNum++; 1118 | // 历史记录已取完,禁用撤回按钮点击 1119 | if (this.history.length - 1 <= 0) { 1120 | this.undoClickNum = 0; 1121 | this.data.setUndoStatus(false); 1122 | } 1123 | } 1124 | 1125 | /** 1126 | * 重置组件 1127 | */ 1128 | private resetComponent = () => { 1129 | if (this.emit) { 1130 | // 隐藏截图工具栏 1131 | this.data.setToolStatus(false); 1132 | // 初始化响应式变量 1133 | this.data.setInitStatus(true); 1134 | // 销毁组件 1135 | this.destroyDOM(); 1136 | // 还原滚动条状态 1137 | if (this.hiddenScrollBar.state) { 1138 | this.updateScrollbarState(false); 1139 | } 1140 | this.emit("destroy-component", false); 1141 | return; 1142 | } 1143 | throw "组件重置失败"; 1144 | }; 1145 | 1146 | // 销毁截图容器 1147 | private destroyDOM() { 1148 | const screenShotPanel = document.getElementById("screenShotPanel"); 1149 | if (screenShotPanel && screenShotPanel.parentNode === document.body) { 1150 | document.body.removeChild(screenShotPanel); 1151 | } 1152 | } 1153 | 1154 | private updateScrollbarState(state = true) { 1155 | // 隐藏滚动条 1156 | if (state) { 1157 | document.documentElement.classList.add("hidden-screen-shot-scroll"); 1158 | document.body.classList.add("hidden-screen-shot-scroll"); 1159 | return; 1160 | } 1161 | // 还原滚动条状态 1162 | document.documentElement.classList.remove("hidden-screen-shot-scroll"); 1163 | document.body.classList.remove("hidden-screen-shot-scroll"); 1164 | } 1165 | 1166 | /** 1167 | * 将指定区域的canvas转为图片 1168 | * @private 1169 | */ 1170 | private getCanvasImgData = (isSave: boolean) => { 1171 | const plugInParameters = new PlugInParameters(); 1172 | // 获取裁剪区域位置信息 1173 | const { startX, startY, width, height } = this.data.getCutOutBoxPosition(); 1174 | let base64 = ""; 1175 | // 保存图片,需要减去八个点的大小 1176 | if (this.screenShortCanvas) { 1177 | if (isSave) { 1178 | // 将canvas转为图片 1179 | saveCanvasToImage( 1180 | this.screenShortCanvas, 1181 | startX, 1182 | startY, 1183 | width, 1184 | height 1185 | ); 1186 | } else { 1187 | // 将canvas转为base64 1188 | base64 = saveCanvasToBase64( 1189 | this.screenShortCanvas, 1190 | startX, 1191 | startY, 1192 | width, 1193 | height, 1194 | 0.75, 1195 | plugInParameters.getWriteImgState() 1196 | ); 1197 | } 1198 | } 1199 | // 重置组件 1200 | this.resetComponent(); 1201 | return base64; 1202 | }; 1203 | } 1204 | --------------------------------------------------------------------------------