├── .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 · [](https://www.npmjs.com/package/vue-web-screen-shot) [](https://yarnpkg.com/package/vue-web-screen-shot) [](https://github.com/likaia/screen-shot) [](https://github.com/likaia/screen-shot/issues) [](https://github.com/likaia/screen-shot/network/members) [](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 | 
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 |
34 |
35 |
39 |
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 |
2 |
3 |
4 |
5 |
11 |
12 |
49 |
50 |
60 |
100 |
101 |
108 |
109 |
110 |
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 |
--------------------------------------------------------------------------------