├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── feature-request.yml │ └── general.yml └── workflows │ ├── build-release.yml │ └── issue-reply.yml ├── .gitignore ├── .prettierrc.json ├── README.md ├── babel.config.js ├── bun.lockb ├── commitlint.config.js ├── jest.config.js ├── license ├── package.json ├── public ├── demo-img.jpeg ├── img │ ├── pentagram-h.png │ └── pentagram.png └── index.html ├── rollup-utils.js ├── rollup.config.js ├── src ├── assets │ ├── img │ │ ├── PopoverPullDownArrow.png │ │ ├── brush-click.png │ │ ├── brush-hover.png │ │ ├── brush.png │ │ ├── close-hover.png │ │ ├── close.png │ │ ├── confirm-hover.png │ │ ├── confirm.png │ │ ├── mosaicPen-click.png │ │ ├── mosaicPen-hover.png │ │ ├── mosaicPen.png │ │ ├── right-top-click.png │ │ ├── right-top-hover.png │ │ ├── right-top.png │ │ ├── round-click.png │ │ ├── round-hover.png │ │ ├── round-normal-big.png │ │ ├── round-normal-medium.png │ │ ├── round-normal-small.png │ │ ├── round-selected-big.png │ │ ├── round-selected-medium.png │ │ ├── round-selected-small.png │ │ ├── round.png │ │ ├── save-click.png │ │ ├── save-hover.png │ │ ├── save.png │ │ ├── seperateLine.png │ │ ├── square-click.png │ │ ├── square-hover.png │ │ ├── square.png │ │ ├── text-click.png │ │ ├── text-hover.png │ │ ├── text.png │ │ ├── undo-disabled.png │ │ ├── undo-hover.png │ │ └── undo.png │ └── scss │ │ └── screen-shot.scss ├── lib │ ├── common-methods │ │ ├── CanvasPatch.ts │ │ ├── DeviceTypeVerif.ts │ │ ├── FixedData.ts │ │ ├── GetBrushSelectedName.ts │ │ ├── GetCanvasImgData.ts │ │ ├── GetColor.ts │ │ ├── GetSelectedCalssName.ts │ │ ├── GetToolRelativePosition.ts │ │ ├── ImgScaling.ts │ │ ├── SaveBorderArrInfo.ts │ │ ├── SaveCanvasToBase64.ts │ │ ├── SaveCanvasToImage.ts │ │ ├── SelectColor.ts │ │ ├── SelectTextSize.ts │ │ ├── SetBrushSize.ts │ │ ├── SetSelectedClassName.ts │ │ ├── TakeOutHistory.ts │ │ ├── UpdateContainerMouseStyle.ts │ │ └── ZoomCutOutBoxPosition.ts │ ├── config │ │ └── Toolbar.ts │ ├── main-entrance │ │ ├── CreateDom.ts │ │ ├── InitData.ts │ │ └── PlugInParameters.ts │ ├── split-methods │ │ ├── AddHistoryData.ts │ │ ├── BoundaryJudgment.ts │ │ ├── CalculateOptionIcoPosition.ts │ │ ├── CalculateToolLocation.ts │ │ ├── DrawArrow.ts │ │ ├── DrawCircle.ts │ │ ├── DrawCutOutBox.ts │ │ ├── DrawImgToCanvas.ts │ │ ├── DrawLineArrow.ts │ │ ├── DrawMasking.ts │ │ ├── DrawMosaic.ts │ │ ├── DrawPencil.ts │ │ ├── DrawRectangle.ts │ │ ├── DrawText.ts │ │ ├── KeyboardEventHandle.ts │ │ ├── SetPlugInParameters.ts │ │ ├── ToolClickEvent.ts │ │ └── drawCrossImg.ts │ └── type │ │ └── ComponentType.ts └── main.ts ├── tests └── lib │ └── common-methods │ └── FixedData.test.ts ├── tsconfig.json ├── webstorm.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # 对所有文件有效 4 | [*] 5 | charset = utf-8 6 | tab_width = 2 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ['bug'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | 感谢反馈问题,请填写下方的表单完成本次问题的反馈 10 | - type: input 11 | id: js-screen-shot-version 12 | attributes: 13 | label: Version of js-screen-shot 14 | description: | 15 | The exact version of js-screen-shot you are using. 16 | 你所使用的 js-screen-shot 的准确版本。 17 | placeholder: eg. 1.0.0 18 | validations: 19 | required: true 20 | - type: dropdown 21 | id: operating-system 22 | attributes: 23 | label: Operating system and its version 24 | description: | 25 | What operating system are you seeing the problem on? 26 | 你是在哪个操作系统平台上发现的这个问题? 27 | multiple: true 28 | options: 29 | - MacOS 30 | - Windows 31 | - Linux 32 | validations: 33 | required: false 34 | - type: input 35 | id: browser 36 | attributes: 37 | label: Browser and its version 38 | description: | 39 | What browser are you seeing the problem on? Also, we'd like to know its exact version. 40 | 你是在哪个浏览器中发现的这个问题?最好可以提供浏览器准确的版本号。 41 | placeholder: eg. Google Chrome 111.0.5563.110 42 | validations: 43 | required: false 44 | - type: input 45 | id: reproduce 46 | attributes: 47 | label: Sandbox to reproduce 48 | description: | 49 | If possible, please try to attach a sandbox link to reproduce. 50 | **尽可能提供一个复现 demo**。 51 | validations: 52 | required: false 53 | - type: textarea 54 | id: what-happened 55 | attributes: 56 | label: What happened? 57 | description: | 58 | Also tell us ? 59 | 出现了什么问题? 60 | validations: 61 | required: false 62 | - type: textarea 63 | id: logs 64 | attributes: 65 | label: 报错信息 66 | description: | 67 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 68 | 如果有的话,请粘贴你遇到的报错信息或日志。下面输入框中的内容在 issue 提交后会被自动格式化成代码块。 69 | render: shell 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a Question 4 | url: https://github.com/likaia/js-screen-shot/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest us to add new feature 3 | labels: ['enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this form! 9 | 感谢你抽时间提出优化建议,请填写下方的表单完成本次问题的反馈 10 | - type: input 11 | id: js-screen-shot-version 12 | attributes: 13 | label: Version of js-screen-shot 14 | description: | 15 | The exact version of js-screen-shot you are using. 16 | 你所使用的 js-screen-shot 的准确版本。 17 | placeholder: eg. 1.0.0 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: feature-description 22 | attributes: 23 | label: 你的想法是什么? 24 | placeholder: 请尽可能详细地告诉我你期望实现一个什么样的功能或者哪里需要进行优化 25 | validations: 26 | required: false 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general.yml: -------------------------------------------------------------------------------- 1 | name: General Issue 2 | description: Open a blank issue 3 | body: 4 | - type: input 5 | id: js-screen-shot-version 6 | attributes: 7 | label: Version of js-screen-shot 8 | description: | 9 | The exact version of antd-mobile you are using. 10 | 你所使用的 js-screen-shot 的准确版本。 11 | placeholder: eg. 1.0.0 12 | validations: 13 | required: false 14 | - type: textarea 15 | id: description 16 | attributes: 17 | label: 问题描述 18 | placeholder: 请尽可能详细地告诉我你所遇到的问题 19 | validations: 20 | required: false 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | # 定义本Action需要对仓库中的文件进行写操作的权限。 4 | permissions: 5 | contents: write 6 | 7 | # 推送的tag中以v开头则执行此action 8 | on: 9 | push: 10 | tags: 11 | - "v*" 12 | 13 | jobs: 14 | build-release: 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - name: "Checkout code" 18 | uses: actions/checkout@v3 19 | 20 | # 设置node版本 21 | - name: "Set up Node.js" 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '14.18.0' 25 | # 安装依赖 26 | - name: "Install dependencies" 27 | run: npm install 28 | # 执行构建命令 29 | - name: "Install dependencies" 30 | run: npm run build-rollup:prod 31 | # 将dist目录打成zip包 32 | - name: Zip Dist 33 | run: zip -r dist.zip dist 34 | # 创建Release 35 | - name: Create Release 36 | id: create_release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }} 40 | with: 41 | tag_name: ${{ github.ref }} 42 | release_name: ${{ github.ref }} 43 | draft: false 44 | prerelease: false 45 | # 上传zip包 46 | - name: Upload Release Asset 47 | uses: actions/upload-release-asset@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }} 50 | with: 51 | upload_url: ${{ steps.create_release.outputs.upload_url }} 52 | asset_path: ./dist.zip 53 | asset_name: js-screen-shot-dist.zip 54 | asset_content_type: application/zip 55 | 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/issue-reply.yml: -------------------------------------------------------------------------------- 1 | name: Issue Reply 2 | 3 | on: 4 | issues: 5 | types: [labeled] 6 | 7 | jobs: 8 | reply-helper: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: feature request 12 | if: github.event.label.name == 'enhancement' 13 | uses: actions-cool/issues-helper@v2.5.0 14 | with: 15 | actions: 'create-comment' 16 | token: ${{ secrets.GITHUBTOKEN }} 17 | issue-number: ${{ github.event.issue.number }} 18 | body: | 19 | Hello @${{ github.event.issue.user.login }}. Your suggestion has been received, and you will be notified in the issue area after the evaluation is completed. 20 | 你好 @${{ github.event.issue.user.login }},已收到你的建议,评估完成后将在issue区域通知你。 21 | 22 | - name: need reproduction 23 | if: github.event.label.name == 'bug' 24 | uses: actions-cool/issues-helper@v2.5.0 25 | with: 26 | actions: 'create-comment' 27 | token: ${{ secrets.GITHUBTOKEN }} 28 | issue-number: ${{ github.event.issue.number }} 29 | body: | 30 | Hello @${{ github.event.issue.user.login }}. Your feedback has been received, and you will be notified in the issue area when the problem is resolved. 31 | 你好 @${{ github.event.issue.user.login }},已收到你反馈的问题,问题解决后将在issue区域通知你。 32 | -------------------------------------------------------------------------------- /.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 | coverage 25 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-web-screen-shot · [![npm](https://img.shields.io/badge/npm-v1.9.9_rc.27-2081C1)](https://www.npmjs.com/package/js-web-screen-shot) [![yarn](https://img.shields.io/badge/yarn-v1.9.9_rc.27-F37E42)](https://yarnpkg.com/package/js-web-screen-shot) [![github](https://img.shields.io/badge/GitHub-depositary-9A9A9A)](https://github.com/likaia/js-screen-shot) [![](https://img.shields.io/github/issues/likaia/js-screen-shot)](https://github.com/likaia/js-screen-shot/issues) [![]( https://img.shields.io/github/forks/likaia/js-screen-shot)](https://github.com/likaia/js-screen-shot/network/members) [![]( https://img.shields.io/github/stars/likaia/js-screen-shot)](https://github.com/likaia/js-screen-shot/stargazers) 2 | web端自定义截屏插件(原生JS版),运行视频:[实现web端自定义截屏功能](https://www.bilibili.com/video/BV1Ey4y127cV) ,效果图如下:![截屏效果图](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/486d810877a24582aa8cf110e643c138~tplv-k3u1fbpfcp-watermark.image) 3 | 4 | ## 写在前面 5 | 关于此插件的更多介绍以及实现原理请移步: 6 | - [实现Web端自定义截屏](https://juejin.cn/post/6924368956950052877) 7 | - [实现Web端自定义截屏(JS版)](https://juejin.cn/post/6931901091445473293) 8 | 9 | > 注意⚠️:本文档并非最新的,最新文档请移步[官网](https://www.kaisir.cn/js-screen-shot/) 10 | 11 | ## 插件安装 12 | ```bash 13 | yarn add js-web-screen-shot 14 | 15 | # or 16 | 17 | npm install js-web-screen-shot --save 18 | ``` 19 | 20 | ## 插件使用 21 | 由于插件采用原生js编写且不依赖任何第三方库,因此它可以在任意一台支持js的设备上运行。 22 | > 注意⚠️: 如果需要使用插件的webrtc模式或者截图写入剪切板功能,需要你的网站运行在`https`环境或者`localhost`环境。当然,也可以通过修改浏览器设置的方式实现在所有环境下都能运行。步骤如下: 23 | > 1.打开谷歌浏览器,在地址栏输入`chrome://flags/#unsafely-treat-insecure-origin-as-secure` 24 | > 2.在打开的界面中:下拉框选择enabled,地址填写你的项目访问路径。 25 | > ![img.png](https://www.kaisir.cn/uploads/MarkDownImg/20230531/5e49de8f32f54f8bb972b4f472d4272e.png) 26 | 27 | ### import形式使用插件 28 | * 在需要使用截屏插件的业务代码中导入插件 29 | ```javascript 30 | import ScreenShot from "js-web-screen-shot"; 31 | ``` 32 | * 在业务代码中使用时实例化插件即可 33 | ```javascript 34 | new ScreenShot(); 35 | ``` 36 | > ⚠️注意:实例化插件时一定要等dom加载完成,否则插件无法正常工作。 37 | ### cdn形式使用插件 38 | * 将插件的`dist`文件夹复制到你的项目中 39 | * 使用`script`标签引入dist目录下的`screenShotPlugin.umd.js`文件 40 | ```javascript 41 | 42 | ``` 43 | * 在业务代码中使用时实例化插件即可 44 | ```javascript 45 | // 截图确认按钮回调函数 46 | const callback = ({base64, cutInfo})=>{ 47 | console.log(base64, cutInfo); 48 | } 49 | // 截图取消时的回调函数 50 | const closeFn = ()=>{ 51 | console.log("截图窗口关闭"); 52 | } 53 | new screenShotPlugin({enableWebRtc: true, completeCallback: callback,closeCallback: closeFn}); 54 | ``` 55 | > ⚠️注意:实例化插件时一定要等dom加载完成,否则插件无法正常工作。 56 | 57 | ### electron环境下使用插件 58 | 由于electron环境下无法直接调用webrtc来获取屏幕流,因此需要调用者自己稍作处理,具体做法如下所示: 59 | * 直接获取设备的窗口,主线程发送一个IPC消息handle 60 | ```javascript 61 | // electron主线程 62 | import { desktopCapturer, webContents } from "electron"; 63 | 64 | // 修复electron18.0.0-beta.5 之后版本的BUG: 无法获取当前程序页面视频流 65 | const selfWindws = async () => 66 | await Promise.all( 67 | webContents 68 | .getAllWebContents() 69 | .filter(item => { 70 | const win = BrowserWindow.fromWebContents(item); 71 | return win && win.isVisible(); 72 | }) 73 | .map(async item => { 74 | const win = BrowserWindow.fromWebContents(item); 75 | const thumbnail = await win?.capturePage(); 76 | // 当程序窗口打开DevTool的时候 也会计入 77 | return { 78 | name: 79 | win?.getTitle() + (item.devToolsWebContents === null ? "" : "-dev"), // 给dev窗口加上后缀 80 | id: win?.getMediaSourceId(), 81 | thumbnail, 82 | display_id: "", 83 | appIcon: null 84 | }; 85 | }) 86 | ); 87 | 88 | // 获取设备窗口信息 89 | ipcMain.handle("IPC消息名称", async (_event, _args) => { 90 | return [ 91 | ...(await desktopCapturer.getSources({ types: ["window", "screen"] })), 92 | ...(await selfWindws()) 93 | ]; 94 | }); 95 | ``` 96 | 97 | * 渲染线程(前端)发送消息封装处理(相应写法自己调整) 98 | ```typescript 99 | // xxx.ts 100 | export const getDesktopCapturerSource = async () => { 101 | return await window.electron.ipcRenderer.invoke("IPC消息名称", []); 102 | } 103 | ``` 104 | 105 | * 获取指定窗口的媒体流 106 | ```typescript 107 | // yyy.ts 108 | export function getInitStream(source: any): Promise { 109 | return new Promise((resolve, _reject) => { 110 | // 获取指定窗口的媒体流 111 | // 此处遵循的是webRTC的接口类型 暂时TS类型没有支持 只能断言成any 112 | (navigator.mediaDevices as any).getUserMedia({ 113 | audio: false, 114 | video: { 115 | mandatory: { 116 | chromeMediaSource: 'desktop', 117 | chromeMediaSourceId: source.id 118 | }, 119 | } 120 | }).then((stream: MediaStream) => { 121 | resolve(stream); 122 | }).catch((error: any) => { 123 | console.log(error); 124 | resolve(null); 125 | }) 126 | }); 127 | } 128 | ``` 129 | 130 | * 前端调用设备窗口信息 131 | ```typescript 132 | import { getDesktopCapturerSource } from "xxx.ts"; 133 | import { getInitStream } from "yyy.ts"; 134 | import ScreenShot from "js-web-screen-shot"; 135 | 136 | export const doScreenShot = async ()=>{ 137 | // 下面这两块自己考虑 138 | const sources = await getDesktopCapturerSource(); // 这里返回的是设备上的所有窗口信息 139 | // 这里可以对`sources`数组下面id进行判断 找到当前的electron窗口 这里为了简单直接拿了第一个 140 | const stream = await getInitStream(sources[0]); 141 | 142 | new ScreenShot({ 143 | enableWebRtc: true, // 启用webrtc 144 | screenFlow: stream!, // 传入屏幕流数据 145 | level: 999, 146 | }); 147 | } 148 | ``` 149 | > 感谢 [@Vanisper](https://github.com/Vanisper) 提供的在electron环境下使用本插件的兼容思路。 150 | 151 | ### 使用electron编写Mac软件。 152 | 153 | 由于Mac上面有一个系统的标题栏,所以当我们的app在全屏的时候,工具栏会被Mac的标题栏给覆盖掉。如下图。 154 | 155 | 需要添加一个参数 156 | 157 | ```typescript 158 | screenShotIns = new ScreenShot({ 159 | menuBarHeight: 22, # Mac系统标题栏默认的高度 160 | }) 161 | ``` 162 | 163 | 因为Mac os没有一个API可以获取到系统标题栏的高度。所以这里给几个建议值。【**可以根据项目的实际情况进行微调**】 164 | 165 | | 场景 | 菜单栏高度(逻辑像素) | 说明 | 166 | | ------------------------------------ | ----------------------- | ------------------------------------------------------------ | 167 | | 普通分辨率非 Retina 显示器 | 22pt | 最常见的标准高度 | 168 | | Retina 显示器 | 22pt(实际像素是 44px) | Retina 显示器下缩放倍率为 2,视觉尺寸不变但像素是两倍 | 169 | | 开启「放大」/ 缩放显示(HiDPI 模式) | 24pt+ | 使用「放大文本」或非原生分辨率时,系统会调整菜单栏高度 | 170 | | 刘海屏 MacBook(如 M1/M2 Pro) | 24pt+ | 刘海下菜单栏实际显示区域变高以避开摄像头(比如 macOS Monterey 开始) | 171 | | Accessibility 启用大字号 | 24pt+ | 系统辅助功能或调整字体大小设置可能使菜单栏高度增加 | 172 | 173 | ![截屏2025-05-01 14.37.38](https://mrxutuchuang.oss-cn-beijing.aliyuncs.com/%E6%88%AA%E5%B1%8F2025-05-01%2014.37.38.png) 174 | 175 | ### electron示例代码 176 | 177 | 如果你看完上个章节的使用方法,依然不是很理解的话,这里准备了一份在electron环境下使用本插件的demo,请移步[electron-js-web-screen-shot-demo](https://github.com/Vanisper/electron-js-web-screen-shot-demo)。 178 | 179 | 180 | ### 兼容移动端 181 | 插件对触屏设备做了兼容处理,如果你是pc端的触屏设备可以支持webrtc模式,如果是移动端那么就只能使用html2canvas模式。 182 | ```javascript 183 | import ScreenShot from "js-web-screen-shot"; 184 | 185 | const config = { 186 | enableWebRtc: false 187 | }; 188 | const screenShotHandler = new ScreenShot(config); 189 | ``` 190 | 191 | ```html 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | /body> 200 | 201 | ``` 202 | 203 | > 注意:在移动端使用时,需要在head标签里禁止浏览器的缩放行为,否则就会出现在使用撤销功能时,多次双击造成界面放大问题。 204 | 205 | 206 | 207 | 208 | ### Vue项目下使用乱码问题 209 | 当你vue项目中使用h2c模式进行截图时,画布左上角可能会出现一些奇怪的字符,这是由于`noscript`标签导致的,将其删除即可。 210 | 211 | ### 参数说明 212 | 截图插件有一个可选参数,它接受一个对象,对象每个key的作用如下: 213 | * `enableWebRtc` 是否启用webrtc,值为`boolean`类型,值为`false`则使用`html2canvas`来截图 214 | * `screenFlow` 设备提供的屏幕流数据(用于electron环境下自己传入的视频流数据),需要将**enableWebRtc**属性设为`true` 215 | * `completeCallback` 截图完成回调函数,值为`Function`类型,最右侧的对号图标点击后会将图片的base64地址与裁剪信息回传给你定义的函数,如果不传的话则会将这些数据放到`sessionStorage`中,你可以通过下述方式拿到他: 216 | ```javascript 217 | sessionStorage.getItem("screenShotImg"); 218 | ``` 219 | * `closeCallback` 截图关闭回调函数,值为`Function`类型。 220 | * `triggerCallback` 截图响应回调函数,值为`Function`类型,使用html2canvas截屏时,页面图片过多时响应会较慢;使用webrtc截屏时用户点了分享,该函数为响应完成后触发的事件。回调函数返回一个对象,类型为: `{code: number,msg: string, displaySurface: string | null,displayLabel: string | null}`,code为0时代表截图加载完成,displaySurface返回的的是当前选择的窗口类型,displayLabel返回的是当前选择的标签页标识,浏览器不支持时此值为null。 221 | * `cancelCallback` 取消分享回到函数,值为`Function`类型,使用webrtc模式截屏时,用户点了取消或者浏览器不支持时所触发的事件。回调函数返回一个对象,类型为:`{code: number,msg: string, errorInfo: string}`,code为-1时代表用户未授权或者浏览器不支持webrtc。 222 | * `saveCallback` 保存截图回调函数,值为`Function`类型。回调函数中返回两个参数: 223 | * `code` 状态码,number类型,为0时代表保存成功 224 | * `msg` 消息码,string类型。 225 | * `level` 截图容器层级,值为number类型。 226 | * `cutBoxBdColor` 裁剪区域边框像素点颜色,值为string类型。 227 | * `maxUndoNum` 最大可撤销次数, 值为number类型 228 | * `canvasWidth` 画布宽度,值为number类型,必须与高度一起设置,单独设置无效。 229 | * `canvasHeight` 画布高度,值为number类型,必须与宽度一起设置,单独设置无效。 230 | * `position` 截图容器位置,值为`{left?: number, top?: number}`类型 231 | * `clickCutFullScreen` 单击截全屏启用状态,值为`boolean`类型, 默认为`false` 232 | * `hiddenToolIco` 需要隐藏的截图工具栏图标,值为`Object`类型,默认为`{}`。传你需要隐藏的图标名称,将值设为`true`即可,除关闭图标外,其他图标均可隐藏。可隐藏的key如下所示: 233 | * `square` 矩形绘制 234 | * `round` 圆形绘制 235 | * `rightTop` 箭头绘制 236 | * `brush` 涂鸦 237 | * `mosaicPen`马赛克工具 238 | * `text` 文本工具 239 | * `separateLine` 分割线 240 | * `save` 下载图片 241 | * `undo` 撤销工具 242 | * `confirm` 保存图片 243 | * `showScreenData` 截图组件加载完毕后,是否显示截图内容至canvas画布内,值为`boolean`类型,默认为`false`。 244 | * `customRightClickEvent` 自定义容器的右键点击事件,值为`Object`类型,接受2个参数: 245 | * `state` 是否拦截右键点击,值为boolean类型,默认为`false`。 246 | * `handleFn` 拦截后的事件处理函数,该属性为可选项,如果不传,默认行为是销毁组件。 247 | * `imgSrc` 截图内容,如果你已经通过其他方式获取到了屏幕内容(例如`electron`环境),那么可以将获取到的内容传入,此时插件将使用你传进来的图片,值为`string`类型(可以为图片`url`地址或者`base64`),默认为`null`。 248 | * `loadCrossImg` 是否加载跨域图片,值为`boolean`类型,默认为`false`。 249 | * `proxyUrl` 代理服务器地址,值为`string`类型,默认为"" 250 | * `screenShotDom` 需要进行截图的容器,值为`HTMLElement`类型,默认使用的是`body`。 251 | * `useRatioArrow` 是否使用等比例箭头, 默认为false(递增变粗的箭头)。 252 | * `imgAutoFit` 是否开启图片自适应, 默认为false。如果自定义了截图内容,浏览器的缩放比例不为100%时,可以设置此参数来修复图片与蒙板大小不一致的问题。 253 | * `cropBoxInfo` 初始裁剪框,值为`{ x: number; y: number; w: number; h: number }`类型,默认不加载。 254 | * `wrcReplyTime` webrtc模式捕捉屏幕时的响应时间,值为`number`类型,默认为500ms。 255 | * `wrcImgPosition` webrtc模式下是否需要对图像进行裁剪,值为`{ x: number; y: number; w: number; h: number }`类型,默认为不裁剪。 256 | * `noScroll` 截图容器是否可滚动,值为`boolean`类型,默认为`true`。 257 | * `maskColor` 蒙层颜色,值为`{ r: number; g: number; b: number; a: number }`类型,默认为:`{ r: 0; g: 0; b: 0; a: 0.6 }` 258 | * `toolPosition` 工具栏展示位置,值为`string`类型,默认为居中展示,提供三个选项: 259 | * `left` 左对齐于裁剪框 260 | * `center` 居中对齐于裁剪框 261 | * `right` 右对齐于裁剪框 262 | * `writeBase64` 是否将截图内容写入剪切板,值为`boolean`类型,默认为`true` 263 | * `wrcWindowMode` 是否启用窗口截图模式,值为`boolean`类型,默认为`false`,即当前标签页截图。如果标签页截图的内容有滚动条或者底部有空缺,可以考虑启用此模式。 264 | * `hiddenScrollBar` 是否隐藏滚动条,用webrtc模式截图时chrome 112版本的浏览器在部分系统下会挤压出现滚动条,如果出现你可以尝试通过此参数来进行修复。值为`Object`类型,有4个属性: 265 | * `state: boolean`; 启用状态, 默认为`false` 266 | * `fillState?: boolean`; 填充状态,默认为`false` 267 | * `color?: string`; 填充层颜色,滚动条隐藏后可能会出现空缺,需要进行填充,默认填充色为黑色。 268 | * `fillWidth?: number`; 填充层宽度,默认为截图容器的宽度 269 | * `fillHeight?: number`; 填充层高度,默认为空缺区域的高度 270 | 271 | > 使用当前标签页进行截图相对而言用户体验是最好的,但是因为`chrome 112`版本的bug会造成页面内容挤压导致截取到的内容不完整,因此只能采用其他方案来解决此问题了。`wrcWindowMode`和`hiddenScrollBar`都可以解决这个问题。 272 | > * `wrcWindowMode`方案会更完美些,但是用户授权时会出现其他的应用程序选项,用户体验会差一些 273 | > * `hiddenScrollBar`方案还是采用标签页截图,但是会造成内容挤压,底部出现空白。 274 | > 275 | > 两种方案的优点与缺点讲完了,最好的办法还是希望`chrome`能在之后的版本更新中修复此问题。 276 | 277 | 278 | > 上述类型中的`?:`为ts中的可选类型,意思为:这个key是可选的,如果需要就传,不需要就不传。 279 | 280 | > imgSrc是url时,如果图片资源跨域了,必须让图片服务器允许跨域才能正常加载。同样的loadCrossImg设置为true时,图片资源跨域了也需要让图片服务器允许跨域。 281 | 282 | ### 快捷键监听 283 | 插件容器监听了三个快捷键,如下所示: 284 | * `Esc`,按下键盘上的esc键时,等同于点了工具栏的关闭图标。 285 | * `Enter`,按下键盘上的enter键时,等同于点了截图工具栏的确认图标。 286 | * `Ctrl/Command + z`,按下这两个组合键时,等同于点了截图工具栏的撤销图标。 287 | 288 | 289 | ### 额外提供的API 290 | 插件暴露了一些内部变量出来,便于调用者根据自己的需求进行修改。 291 | 292 | #### getCanvasController 293 | 该函数用于获取截图容器的DOM,返回值为`HTMLCanvasElement`类型。 294 | 295 | 示例代码: 296 | 297 | ```javascript 298 | import ScreenShot from "js-web-screen-shot"; 299 | 300 | const screenShotHandler = new ScreenShot(); 301 | const canvasDom = screenShotHandler.getCanvasController(); 302 | ``` 303 | > 注意:如果截图容器尚未加载完毕,获取到的内容可能为null。 304 | 305 | #### destroyComponents 306 | 该函数用于销毁截图容器,无返回值。 307 | 308 | 示例代码: 309 | 310 | ```javascript 311 | import ScreenShot from "js-web-screen-shot"; 312 | 313 | const screenShotHandler = new ScreenShot(); 314 | screenShotHandler.destroyComponents() 315 | ``` 316 | #### completeScreenshot 317 | 该函数用于将框选区域的截图内容写入剪切版,无返回值。 318 | 319 | 该方法可以跟`cropBoxInfo`参数结合起来实现指定位置的自动截图,截图内容默认写入剪切版内,如果你想拿到截取到的base64内容可以通过`completeCallback`参数拿到,或者直接从sessionStorage中获取。 320 | 321 | 该回调函数中返回的参数格式如下所示: 322 | * base64 323 | * cutInfo 裁剪框位置参数 324 | * startX 325 | * startY 326 | * width 327 | * height 328 | 329 | 示例代码: 330 | ```javascript 331 | const plugin = new screenShotPlugin( 332 | { 333 | clickCutFullScreen:true, 334 | wrcWindowMode: true, 335 | cropBoxInfo:{x:350, y:20, w:300, h:300}, 336 | completeCallback: ({base64, cutInfo}) => { 337 | console.log(base64, cutInfo); 338 | }, 339 | triggerCallback:() => { 340 | // 截图组件加载完毕调用此方法来完成框选区域的截图 341 | plugin.completeScreenshot() 342 | } 343 | }); 344 | ``` 345 | > 注意:此方法在1.9.9版本之后不再返回字符串类型的数据,而是返回的对象格式。 346 | 347 | 348 | ### 工具栏图标定制 349 | 如果你需要修改截图工具栏的图标,可以通过覆盖元素css类名的方式实现,插件内所有图标的css类名如下所示: 350 | * square 矩形绘制图标 351 | * round 圆型绘制图标 352 | * right-top 箭头绘制图标 353 | * brush 画笔工具 354 | * mosaicPen 马赛克工具 355 | * text 文本工具 356 | * save 保存 357 | * close 关闭 358 | * undo 撤销 359 | * confirm 确认 360 | 361 | 以`square`为例,要修改它的图标,只需要将下述代码添加进你项目代码的样式中即可。 362 | ```scss 363 | .square { 364 | background-image: url("你的图标路径") !important; 365 | 366 | &:hover { 367 | background-image: url("你的图标路径") !important; 368 | } 369 | 370 | &:active { 371 | background-image: url("你的图标路径") !important; 372 | } 373 | } 374 | ``` 375 | 376 | 377 | ## 写在最后 378 | 至此,插件的所有使用方法就介绍完了,该插件的Vue3版本,请移步:[vue-web-screen-shot](https://www.npmjs.com/package/vue-web-screen-shot) 379 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: "defaults" }]], 3 | plugins: [] 4 | }; 5 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/bun.lockb -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-angular"] }; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 使用ts-jest来处理ts代码 3 | preset: "ts-jest", 4 | // 输出每个测试用例执行的结果 5 | verbose: true, 6 | // 是否显示覆盖率报告 7 | collectCoverage: true, 8 | // 告诉 jest 哪些文件需要经过单元测试 9 | collectCoverageFrom: ["tests/*.test.ts", "src/lib/**/*.ts", "src/*.ts"], 10 | testMatch: ["**/tests/**/*.test.ts", "**/tests/**/*.spec.ts"], 11 | moduleFileExtensions: ["js", "jsx", "ts", "tsx"], 12 | coverageThreshold: { 13 | global: { 14 | statements: 90, // 保证每个语句都执行了 15 | functions: 90, // 保证每个函数都调用了 16 | branches: 90 // 保证每个 if 等分支代码都执行了 17 | } 18 | }, 19 | // 需要忽略的目录 20 | testPathIgnorePatterns: ["/node_modules/", "/.rollup.cache/"], 21 | // 处理别名 22 | moduleNameMapper: { 23 | "^@/(.*)$": "/src/$1" 24 | }, 25 | // 运行时的环境 26 | testEnvironment: "jest-environment-jsdom" 27 | }; 28 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 js-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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-web-screen-shot", 3 | "version": "1.9.9-rc.27", 4 | "description": "web端自定义截屏插件(原生JS版)", 5 | "main": "dist/screenShotPlugin.common.js", 6 | "private": false, 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 && yarn run fix-dts-path", 15 | "fix-dts-path": "tsc-alias --outDir ./dist/", 16 | "test": "jest", 17 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 18 | "commit": "git-cz" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/likaia/js-screen-shot.git" 23 | }, 24 | "keywords": [ 25 | "web-best-screen-shot", 26 | "web-screen-shot", 27 | "screen-shot", 28 | "js-screen-shot", 29 | "截屏", 30 | "截图", 31 | "截图插件", 32 | "屏幕截图", 33 | "自定义截图", 34 | "web端自定义截屏", 35 | "electron", 36 | "Electron截图插件" 37 | ], 38 | "author": "likaia", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/likaia/js-screen-shot/issues" 42 | }, 43 | "homepage": "https://www.kaisir.cn/js-screen-shot", 44 | "devDependencies": { 45 | "@babel/core": "^7.21.0", 46 | "@babel/preset-env": "^7.20.2", 47 | "@commitlint/cli": "^11.0.0", 48 | "@commitlint/config-angular": "^11.0.0", 49 | "@rollup/plugin-alias": "^4.0.3", 50 | "@rollup/plugin-babel": "^6.0.3", 51 | "@rollup/plugin-commonjs": "^20.0.0", 52 | "@rollup/plugin-node-resolve": "^13.0.0", 53 | "@rollup/plugin-url": "^8.0.1", 54 | "@types/jest": "^29.5.0", 55 | "@typescript-eslint/eslint-plugin": "^2.33.0", 56 | "@typescript-eslint/parser": "^2.33.0", 57 | "@vue/eslint-config-prettier": "^6.0.0", 58 | "@vue/eslint-config-typescript": "^5.0.2", 59 | "autoprefixer": "^10.4.13", 60 | "commitizen": "^4.2.2", 61 | "core-js": "^3.6.5", 62 | "cssnano": "^5.1.15", 63 | "cz-conventional-changelog": "^3.3.0", 64 | "eslint": "^6.7.2", 65 | "eslint-plugin-prettier": "^3.1.3", 66 | "eslint-plugin-vue": "^7.0.0-0", 67 | "husky": "^4.3.0", 68 | "jest": "^29.5.0", 69 | "jest-environment-jsdom": "^29.5.0", 70 | "postcss": "^8.4.21", 71 | "postcss-import": "^15.1.0", 72 | "postcss-preset-env": "^8.0.1", 73 | "postcss-url": "^10.1.3", 74 | "prettier": "^1.19.1", 75 | "rollup": "^2.59.2", 76 | "rollup-plugin-copy": "^3.4.0", 77 | "rollup-plugin-delete": "^2.0.0", 78 | "rollup-plugin-livereload": "^2.0.5", 79 | "rollup-plugin-postcss": "^4.0.2", 80 | "rollup-plugin-progress": "^1.1.2", 81 | "rollup-plugin-serve": "^2.0.2", 82 | "rollup-plugin-terser": "^7.0.2", 83 | "rollup-plugin-typescript2": "^0.34.1", 84 | "rollup-plugin-visualizer": "^5.9.0", 85 | "sass": "^1.26.5", 86 | "sass-loader": "^8.0.2", 87 | "ts-jest": "^29.0.5", 88 | "tsc-alias": "^1.8.7", 89 | "tslib": "^2.6.2", 90 | "typescript": "~5.0.4", 91 | "yargs": "^17.7.1" 92 | }, 93 | "config": { 94 | "commitizen": { 95 | "path": "./node_modules/cz-conventional-changelog" 96 | } 97 | }, 98 | "husky": { 99 | "hooks": { 100 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 101 | } 102 | }, 103 | "dependencies": { 104 | "html2canvas": "1.4.1" 105 | }, 106 | "files": [ 107 | "/dist" 108 | ] 109 | } 110 | -------------------------------------------------------------------------------- /public/demo-img.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/public/demo-img.jpeg -------------------------------------------------------------------------------- /public/img/pentagram-h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/public/img/pentagram-h.png -------------------------------------------------------------------------------- /public/img/pentagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/public/img/pentagram.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | screen 9 | shot 10 | demo 11 | 13 | 26 | 42 | 149 | 150 | 151 |
153 |
154 | 截图插件文字展示 155 |
156 |
157 | 161 | 164 |

165 | 图片展示

166 | 167 |
168 | 169 | 170 | -------------------------------------------------------------------------------- /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 | }; 48 | // 是否需要对代码进行压缩 49 | addProperty(config, compressedState === "true", "plugins", [ 50 | terser({ 51 | output: { 52 | comments: false // 删除注释 53 | } 54 | }) 55 | ]); 56 | addProperty(config, pkgFormat === "common", "exports", "named"); 57 | outputConfig.push(config); 58 | } 59 | return outputConfig; 60 | }; 61 | 62 | const buildCopyTargetsConfig = (useDevServer = "false") => { 63 | const result = [ 64 | { 65 | src: "src/assets/fonts/**", 66 | dest: "dist/assets/fonts" 67 | } 68 | ]; 69 | if (useDevServer === "true") { 70 | result.push({ 71 | src: "public/**", 72 | dest: "dist" 73 | }); 74 | } 75 | return result; 76 | }; 77 | 78 | // 生成打包后的模块占用信息 79 | const enablePKGStats = (status = "false") => { 80 | if (status === "true") { 81 | return visualizer({ 82 | filename: "dist/bundle-stats.html" 83 | }); 84 | } 85 | return null; 86 | }; 87 | 88 | const enableDevServer = status => { 89 | // 默认清空dist目录下的文件 90 | let serverConfig = [delFile({ targets: "dist/*" })]; 91 | if (status === "true") { 92 | // dev模式下不需要对dist目录进行清空 93 | serverConfig = [ 94 | serve({ 95 | // 服务器启动的文件夹,访问此路径下的index.html文件 96 | contentBase: "dist", 97 | port: 8123 98 | }), 99 | // watch dist目录,当目录中的文件发生变化时,刷新页面 100 | livereload("dist") 101 | ]; 102 | } 103 | return serverConfig; 104 | }; 105 | 106 | const buildTSConfig = (useDevServer = "false") => { 107 | return { 108 | tsconfig: "tsconfig.json", 109 | tsconfigOverride: { 110 | compilerOptions: { 111 | // dev模式下不生成.d.ts文件 112 | declaration: useDevServer !== "true", 113 | // 指定目标环境为es5 114 | target: "es5" 115 | }, 116 | // 打包时排除tests目录 117 | exclude: ["tests"] 118 | }, 119 | clean: true 120 | }; 121 | }; 122 | 123 | export { 124 | buildConfig, 125 | buildCopyTargetsConfig, 126 | enablePKGStats, 127 | enableDevServer, 128 | buildTSConfig 129 | }; 130 | -------------------------------------------------------------------------------- /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 autoprefixer from "autoprefixer"; 7 | import copy from "rollup-plugin-copy"; 8 | import path from "path"; 9 | import alias from "@rollup/plugin-alias"; 10 | import postcssImport from "postcss-import"; 11 | import postcssUrl from "postcss-url"; 12 | import url from "@rollup/plugin-url"; 13 | import cssnano from "cssnano"; 14 | import yargs from "yargs"; 15 | import { 16 | buildConfig, 17 | buildCopyTargetsConfig, 18 | enableDevServer, 19 | enablePKGStats, 20 | buildTSConfig 21 | } from "./rollup-utils"; 22 | import progress from "rollup-plugin-progress"; 23 | 24 | // 使用yargs解析命令行执行时的添加参数 25 | const commandLineParameters = yargs(process.argv.slice(1)).options({ 26 | // css文件独立状态,默认为内嵌 27 | splitCss: { type: "string", alias: "spCss", default: "false" }, 28 | // 打包格式, 默认为 umd,esm,common 三种格式 29 | packagingFormat: { 30 | type: "string", 31 | alias: "pkgFormat", 32 | default: "umd,esm,common" 33 | }, 34 | // 打包后的js压缩状态 35 | compressedState: { type: "string", alias: "compState", default: "false" }, 36 | // 显示每个包的占用体积, 默认不显示 37 | showModulePKGInfo: { type: "string", alias: "showPKGInfo", default: "false" }, 38 | // 是否开启devServer, 默认不开启 39 | useDevServer: { type: "string", alias: "useDServer", default: "false" } 40 | }).argv; 41 | // 需要让rollup忽略的自定义参数 42 | const ignoredWarningsKey = [...Object.keys(commandLineParameters)]; 43 | const splitCss = commandLineParameters.splitCss; 44 | const packagingFormat = commandLineParameters.packagingFormat.split(","); 45 | const compressedState = commandLineParameters.compressedState; 46 | const showModulePKGInfo = commandLineParameters.showModulePKGInfo; 47 | const useDevServer = commandLineParameters.useDevServer; 48 | 49 | export default { 50 | input: "src/main.ts", 51 | output: buildConfig(packagingFormat, compressedState), 52 | // 警告处理钩子 53 | onwarn: function(warning, rollupWarn) { 54 | const message = warning.message; 55 | let matchingResult = false; 56 | for (let i = 0; i < ignoredWarningsKey.length; i++) { 57 | if (message.indexOf(ignoredWarningsKey[i]) !== -1) { 58 | matchingResult = true; 59 | break; 60 | } 61 | } 62 | // 错误警告中包含要忽略的key则退出函数 63 | if (warning.code === "UNKNOWN_OPTION" && matchingResult) { 64 | return; 65 | } 66 | rollupWarn(warning); 67 | }, 68 | plugins: [ 69 | nodeResolve({ 70 | // 读取.browserslist文件 71 | browser: true, 72 | preferBuiltins: false 73 | }), 74 | commonjs(), 75 | alias({ 76 | entries: [{ find: "@", replacement: path.resolve(__dirname, "src") }] 77 | }), 78 | typescript(buildTSConfig(useDevServer)), 79 | // 此处用来处理外置css, 需要在入口文件中使用import来导入css文件 80 | postcss({ 81 | // 内联css 82 | extract: splitCss === "true" ? "style/css/screen-shot.css" : false, 83 | minimize: true, 84 | sourceMap: false, 85 | extensions: [".css", ".scss"], 86 | // 当前正在处理的CSS文件的路径, postcssUrl在拷贝资源时需要根据它来定位目标文件 87 | to: path.resolve(__dirname, "dist/assets/*"), 88 | use: ["sass"], 89 | // autoprefixer: 给css3的一些属性加前缀 90 | // postcssImport: 处理css文件中的@import语句 91 | // cssnano: 它可以通过移除注释、空格和其他不必要的字符来压缩CSS代码 92 | plugins: [ 93 | autoprefixer(), 94 | postcssImport(), 95 | // 对scss中的别名进行统一替换处理 96 | postcssUrl([ 97 | { 98 | filter: "**/*.*", 99 | url(asset) { 100 | return asset.url.replace(/~@/g, "."); 101 | } 102 | } 103 | ]), 104 | // 再次调用将css中引入的图片按照规则进行处理 105 | postcssUrl([ 106 | { 107 | basePath: path.resolve(__dirname, "src"), 108 | url: "inline", 109 | maxSize: 8, // 最大文件大小(单位为KB),超过该大小的文件将不会被编码为base64 110 | fallback: "copy", // 如果文件大小超过最大大小,则使用copy选项复制文件 111 | useHash: true, // 进行hash命名 112 | encodeType: "base64" // 指定编码类型为base64 113 | } 114 | ]), 115 | cssnano({ 116 | preset: "default" // 使用默认配置 117 | }) 118 | ] 119 | }), 120 | // 处理通过img标签引入的图片 121 | url({ 122 | include: ["**/*.jpg", "**/*.png", "**/*.svg"], 123 | // 输出路径 124 | dest: "dist/assets", 125 | // 超过10kb则拷贝否则转base64 126 | limit: 10 * 1024 // 10KB 127 | }), 128 | babel(), 129 | enablePKGStats(showModulePKGInfo), 130 | ...enableDevServer(useDevServer), 131 | progress({ 132 | format: "[:bar] :percent (:current/:total)", 133 | clearLine: false 134 | }), 135 | copy({ 136 | targets: buildCopyTargetsConfig(useDevServer) 137 | }) 138 | ] 139 | }; 140 | -------------------------------------------------------------------------------- /src/assets/img/PopoverPullDownArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/PopoverPullDownArrow.png -------------------------------------------------------------------------------- /src/assets/img/brush-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/brush-click.png -------------------------------------------------------------------------------- /src/assets/img/brush-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/brush-hover.png -------------------------------------------------------------------------------- /src/assets/img/brush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/brush.png -------------------------------------------------------------------------------- /src/assets/img/close-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/close-hover.png -------------------------------------------------------------------------------- /src/assets/img/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/close.png -------------------------------------------------------------------------------- /src/assets/img/confirm-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/confirm-hover.png -------------------------------------------------------------------------------- /src/assets/img/confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/confirm.png -------------------------------------------------------------------------------- /src/assets/img/mosaicPen-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/mosaicPen-click.png -------------------------------------------------------------------------------- /src/assets/img/mosaicPen-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/mosaicPen-hover.png -------------------------------------------------------------------------------- /src/assets/img/mosaicPen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/mosaicPen.png -------------------------------------------------------------------------------- /src/assets/img/right-top-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/right-top-click.png -------------------------------------------------------------------------------- /src/assets/img/right-top-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/right-top-hover.png -------------------------------------------------------------------------------- /src/assets/img/right-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/right-top.png -------------------------------------------------------------------------------- /src/assets/img/round-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round-click.png -------------------------------------------------------------------------------- /src/assets/img/round-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round-hover.png -------------------------------------------------------------------------------- /src/assets/img/round-normal-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round-normal-big.png -------------------------------------------------------------------------------- /src/assets/img/round-normal-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round-normal-medium.png -------------------------------------------------------------------------------- /src/assets/img/round-normal-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round-normal-small.png -------------------------------------------------------------------------------- /src/assets/img/round-selected-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round-selected-big.png -------------------------------------------------------------------------------- /src/assets/img/round-selected-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round-selected-medium.png -------------------------------------------------------------------------------- /src/assets/img/round-selected-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round-selected-small.png -------------------------------------------------------------------------------- /src/assets/img/round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/round.png -------------------------------------------------------------------------------- /src/assets/img/save-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/save-click.png -------------------------------------------------------------------------------- /src/assets/img/save-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/save-hover.png -------------------------------------------------------------------------------- /src/assets/img/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/save.png -------------------------------------------------------------------------------- /src/assets/img/seperateLine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/seperateLine.png -------------------------------------------------------------------------------- /src/assets/img/square-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/square-click.png -------------------------------------------------------------------------------- /src/assets/img/square-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/square-hover.png -------------------------------------------------------------------------------- /src/assets/img/square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/square.png -------------------------------------------------------------------------------- /src/assets/img/text-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/text-click.png -------------------------------------------------------------------------------- /src/assets/img/text-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/text-hover.png -------------------------------------------------------------------------------- /src/assets/img/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/text.png -------------------------------------------------------------------------------- /src/assets/img/undo-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/undo-disabled.png -------------------------------------------------------------------------------- /src/assets/img/undo-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/undo-hover.png -------------------------------------------------------------------------------- /src/assets/img/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/js-screen-shot/2b76afb1280d582e87738ee7f6637d9efd750e47/src/assets/img/undo.png -------------------------------------------------------------------------------- /src/assets/scss/screen-shot.scss: -------------------------------------------------------------------------------- 1 | #screenShotContainer { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | cursor: crosshair; 6 | } 7 | 8 | #toolPanel { 9 | min-width: 392px; 10 | height: 24px; 11 | background: #ffffff; 12 | z-index: 9999; 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | padding: 10px; 17 | box-sizing: content-box; 18 | 19 | .item-panel { 20 | width: 24px; 21 | height: 24px; 22 | margin-right: 15px; 23 | float: left; 24 | 25 | &:last-child { 26 | margin-right: 0; 27 | } 28 | } 29 | 30 | .square { 31 | background-image: url("~@/assets/img/square.png"); 32 | background-size: cover; 33 | 34 | &:hover { 35 | background-image: url("~@/assets/img/square-hover.png"); 36 | } 37 | 38 | &:active { 39 | background-image: url("~@/assets/img/square-click.png"); 40 | } 41 | } 42 | .square-active { 43 | background-image: url("~@/assets/img/square-click.png"); 44 | } 45 | 46 | .round { 47 | background-image: url("~@/assets/img/round.png"); 48 | background-size: cover; 49 | 50 | &:hover { 51 | background-image: url("~@/assets/img/round-hover.png"); 52 | } 53 | 54 | &:active { 55 | background-image: url("~@/assets/img/round-click.png"); 56 | } 57 | } 58 | .round-active { 59 | background-image: url("~@/assets/img/round-click.png"); 60 | } 61 | 62 | .right-top { 63 | background-image: url("~@/assets/img/right-top.png"); 64 | background-size: cover; 65 | 66 | &:hover { 67 | background-image: url("~@/assets/img/right-top-hover.png"); 68 | } 69 | 70 | &:active { 71 | background-image: url("~@/assets/img/right-top-click.png"); 72 | } 73 | } 74 | .right-top-active { 75 | background-image: url("~@/assets/img/right-top-click.png"); 76 | } 77 | 78 | .brush { 79 | background-image: url("~@/assets/img/brush.png"); 80 | background-size: cover; 81 | 82 | &:hover { 83 | background-image: url("~@/assets/img/brush-hover.png"); 84 | } 85 | 86 | &:active { 87 | background-image: url("~@/assets/img/brush-click.png"); 88 | } 89 | } 90 | .brush-active{ 91 | background-image: url("~@/assets/img/brush-click.png"); 92 | } 93 | 94 | .mosaicPen { 95 | background-image: url("~@/assets/img/mosaicPen.png"); 96 | background-size: cover; 97 | 98 | &:hover { 99 | background-image: url("~@/assets/img/mosaicPen-hover.png"); 100 | } 101 | 102 | &:active { 103 | background-image: url("~@/assets/img/mosaicPen-click.png"); 104 | } 105 | } 106 | .mosaicPen-active { 107 | background-image: url("~@/assets/img/mosaicPen-click.png"); 108 | } 109 | 110 | .separateLine { 111 | width: 1px; 112 | background-image: url("~@/assets/img/seperateLine.png"); 113 | background-size: cover; 114 | } 115 | 116 | .text { 117 | background-image: url("~@/assets/img/text.png"); 118 | background-size: cover; 119 | 120 | &:hover { 121 | background-image: url("~@/assets/img/text-hover.png"); 122 | } 123 | 124 | &:active { 125 | background-image: url("~@/assets/img/text-click.png"); 126 | } 127 | } 128 | .text-active { 129 | background-image: url("~@/assets/img/text-click.png"); 130 | } 131 | 132 | .save { 133 | background-image: url("~@/assets/img/save.png"); 134 | background-size: cover; 135 | 136 | &:hover { 137 | background-image: url("~@/assets/img/save-hover.png"); 138 | } 139 | 140 | &:active { 141 | background-image: url("~@/assets/img/save-click.png"); 142 | } 143 | } 144 | 145 | .close { 146 | background-image: url("~@/assets/img/close.png"); 147 | background-size: cover; 148 | 149 | &:hover { 150 | background-image: url("~@/assets/img/close-hover.png"); 151 | } 152 | } 153 | 154 | .undo-disabled { 155 | background-size: cover; 156 | background-image: url("~@/assets/img/undo-disabled.png"); 157 | } 158 | 159 | .undo { 160 | background-size: cover; 161 | background-image: url("~@/assets/img/undo.png"); 162 | 163 | &:hover{ 164 | background-image: url("~@/assets/img/undo-hover.png"); 165 | } 166 | } 167 | 168 | .confirm { 169 | background-image: url("~@/assets/img/confirm.png"); 170 | background-size: cover; 171 | 172 | &:hover { 173 | background-image: url("~@/assets/img/confirm-hover.png"); 174 | } 175 | } 176 | } 177 | 178 | .__screenshot-lock-scroll { 179 | margin: 0; 180 | height: 100% !important; 181 | overflow: hidden !important; 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 | z-index: 9999; 192 | position: absolute; 193 | top: 0; 194 | left: 23px; 195 | transform: rotate(180deg); 196 | 197 | img { 198 | width: 100%; 199 | height: 100%; 200 | } 201 | } 202 | 203 | #optionPanel { 204 | height: 20px; 205 | top: 6px; 206 | left: 0; 207 | border-radius: 5px; 208 | background: #ffffff; 209 | z-index: 9999; 210 | position: absolute; 211 | padding: 10px; 212 | box-sizing: content-box; 213 | 214 | .text-size-panel { 215 | width: 65px; 216 | height: 20px; 217 | font-size: 14px; 218 | float: left; 219 | border: 1px solid #bebfca; 220 | overflow: hidden; 221 | border-radius: 3px; 222 | cursor: pointer; 223 | display: flex; 224 | justify-content: center; 225 | align-items: center; 226 | } 227 | 228 | .text-select-panel { 229 | width: 65px; 230 | display: flex; 231 | flex-wrap: wrap; 232 | justify-content: center; 233 | background: #fff; 234 | border: 1px solid #d8dcea; 235 | border-radius: 3px; 236 | position: absolute; 237 | left: 10px; 238 | top: -321px; 239 | 240 | .text-item { 241 | width: 45px; 242 | height: 20px; 243 | font-size: 14px; 244 | text-align: center; 245 | margin-bottom: 5px; 246 | cursor: pointer; 247 | 248 | &:hover { 249 | background: #bebfca; 250 | } 251 | 252 | &:first-child { 253 | margin-top: 5px; 254 | } 255 | } 256 | } 257 | 258 | .brush-select-panel{ 259 | height: 20px; 260 | float: left; 261 | 262 | .item-panel { 263 | width: 20px; 264 | height: 20px; 265 | margin-right: 18px; 266 | float: left; 267 | 268 | &:first-child { 269 | margin-left: 2px; 270 | } 271 | &:last-child { 272 | margin-right: 0; 273 | } 274 | } 275 | .brush-small { 276 | background-size: cover; 277 | background-image: url("~@/assets/img/round-normal-small.png"); 278 | 279 | &:hover { 280 | background-image: url("~@/assets/img/round-selected-small.png"); 281 | } 282 | &:active { 283 | background-image: url("~@/assets/img/round-selected-small.png"); 284 | } 285 | } 286 | .brush-small-active { 287 | background-image: url("~@/assets/img/round-selected-small.png"); 288 | } 289 | 290 | .brush-medium { 291 | background-size: cover; 292 | background-image: url("~@/assets/img/round-normal-medium.png"); 293 | 294 | &:hover { 295 | background-image: url("~@/assets/img/round-selected-medium.png"); 296 | } 297 | &:active { 298 | background-image: url("~@/assets/img/round-selected-medium.png"); 299 | } 300 | } 301 | .brush-medium-active{ 302 | background-image: url("~@/assets/img/round-selected-medium.png"); 303 | } 304 | 305 | .brush-big { 306 | background-size: cover; 307 | background-image: url("~@/assets/img/round-normal-big.png"); 308 | 309 | &:hover { 310 | background-image: url("~@/assets/img/round-selected-big.png"); 311 | } 312 | &:active { 313 | background-image: url("~@/assets/img/round-selected-big.png"); 314 | } 315 | } 316 | 317 | .brush-big-active { 318 | background-image: url("~@/assets/img/round-selected-big.png"); 319 | } 320 | } 321 | 322 | .right-panel { 323 | float: left; 324 | display: flex; 325 | align-items: center; 326 | margin-left: 39px; 327 | 328 | // 颜色容器 329 | .color-panel{ 330 | width: 72px; 331 | display: flex; 332 | justify-content: center; 333 | flex-wrap: wrap; 334 | background: #FFFFFF; 335 | border: solid 1px #E5E6E5; 336 | border-radius: 5px; 337 | position: absolute; 338 | top: -225px; 339 | right: 28px; 340 | 341 | .color-item { 342 | width: 62px; 343 | height: 20px; 344 | margin-bottom: 5px; 345 | 346 | &:nth-child(1) { 347 | margin-top: 5px; 348 | background: #F53440; 349 | } 350 | &:nth-child(2) { 351 | background: #F65E95; 352 | } 353 | &:nth-child(3) { 354 | background: #D254CF; 355 | } 356 | &:nth-child(4) { 357 | background: #12A9D7; 358 | } 359 | &:nth-child(5) { 360 | background: #30A345; 361 | } 362 | &:nth-child(6) { 363 | background: #FACF50; 364 | } 365 | &:nth-child(7) { 366 | background: #F66632; 367 | } 368 | &:nth-child(8) { 369 | background: #989998; 370 | } 371 | &:nth-child(9) { 372 | background: #000000; 373 | } 374 | &:nth-child(10) { 375 | border: solid 1px #E5E6E5; 376 | background: #FEFFFF; 377 | } 378 | } 379 | } 380 | // 已选择容器 381 | .color-select-panel { 382 | width: 62px; 383 | height: 20px; 384 | background: #F53340; 385 | border: solid 1px #E5E6E5; 386 | 387 | &.text-select-status { 388 | margin-left: 31px; 389 | } 390 | } 391 | 392 | .pull-down-arrow { 393 | width: 15px; 394 | height: 8px; 395 | margin-left: 10px; 396 | background-size: cover; 397 | background-image: url("~@/assets/img/PopoverPullDownArrow.png"); 398 | } 399 | } 400 | 401 | } 402 | 403 | #cutBoxSizePanel { 404 | width: 85px; 405 | height: 25px; 406 | position: absolute; 407 | left: 0; 408 | top: 0; 409 | border-radius: 3px; 410 | z-index: 9999; 411 | background: rgba(0, 0, 0, .4); 412 | display: flex; 413 | align-items: center; 414 | justify-content: center; 415 | color: #FFFFFF; 416 | font-size: 14px; 417 | } 418 | 419 | #textInputPanel { 420 | min-width: 20px; 421 | min-height: 20px; 422 | font-weight: bold; 423 | padding: 0; 424 | margin: 0; 425 | box-sizing: border-box; 426 | position: fixed; 427 | outline: none; 428 | z-index: 9999; 429 | top: 0; 430 | left: 0; 431 | border: none; 432 | } 433 | .hidden-screen-shot-scroll { 434 | width: 100vw; 435 | height: 100vh; 436 | overflow: hidden; 437 | } 438 | .no-cursor * { 439 | cursor: none; 440 | } 441 | -------------------------------------------------------------------------------- /src/lib/common-methods/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 | -------------------------------------------------------------------------------- /src/lib/common-methods/DeviceTypeVerif.ts: -------------------------------------------------------------------------------- 1 | export function isPC(): boolean { 2 | const userAgentInfo = navigator.userAgent; 3 | const Agents = [ 4 | "Android", 5 | "iPhone", 6 | "SymbianOS", 7 | "Windows Phone", 8 | "iPad", 9 | "iPod" 10 | ]; 11 | let flag = true; 12 | for (let v = 0; v < Agents.length; v++) { 13 | if (userAgentInfo.indexOf(Agents[v]) > 0) { 14 | flag = false; 15 | break; 16 | } 17 | } 18 | return flag; 19 | } 20 | 21 | // 检测设备是否支持触摸 22 | export function isTouchDevice(): boolean { 23 | // 检查navigator.maxTouchPoints 24 | const maxTouchPoints = 25 | "maxTouchPoints" in navigator && navigator.maxTouchPoints > 0; 26 | // 检查旧版API navigator.msMaxTouchPoints 27 | const msMaxTouchPoints = 28 | "msMaxTouchPoints" in navigator && (navigator as any).msMaxTouchPoints > 0; 29 | // 检查触摸事件处理器 30 | const touchEvent = "ontouchstart" in window; 31 | // 使用CSS媒体查询检查指针类型 32 | const coarsePointer = 33 | window.matchMedia && window.matchMedia("(pointer: coarse)").matches; 34 | 35 | // 如果以上任何一种方法返回true,则设备支持触摸 36 | return maxTouchPoints || msMaxTouchPoints || touchEvent || coarsePointer; 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/common-methods/FixedData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 对参数进行处理,小于0则返回0 3 | */ 4 | export function nonNegativeData(data: number) { 5 | return data > 0 ? data : 0; 6 | } 7 | 8 | /** 9 | * 计算传进来的数据,不让其移出可视区域 10 | * @param data 需要计算的数据 11 | * @param trimDistance 裁剪框宽度 12 | * @param canvasDistance 画布宽度 13 | */ 14 | export function fixedData( 15 | data: number, 16 | trimDistance: number, 17 | canvasDistance: number 18 | ) { 19 | if (nonNegativeData(data) + trimDistance > canvasDistance) { 20 | return nonNegativeData(canvasDistance - trimDistance); 21 | } else { 22 | return nonNegativeData(data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/common-methods/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/lib/common-methods/GetCanvasImgData.ts: -------------------------------------------------------------------------------- 1 | import { saveCanvasToImage } from "@/lib/common-methods/SaveCanvasToImage"; 2 | import { saveCanvasToBase64 } from "@/lib/common-methods/SaveCanvasToBase64"; 3 | import InitData from "@/lib/main-entrance/InitData"; 4 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 5 | 6 | /** 7 | * 将指定区域的canvas转为图片 8 | */ 9 | export function getCanvasImgData(isSave: boolean) { 10 | const data = new InitData(); 11 | const plugInParameters = new PlugInParameters(); 12 | const screenShotCanvas = data.getScreenShotContainer()?.getContext("2d"); 13 | // 获取裁剪区域位置信息 14 | const { startX, startY, width, height } = data.getCutOutBoxPosition(); 15 | let base64 = ""; 16 | if (screenShotCanvas) { 17 | if (isSave) { 18 | // 将canvas转为图片 19 | saveCanvasToImage(screenShotCanvas, startX, startY, width, height); 20 | } else { 21 | // 将canvas转为base64 22 | base64 = saveCanvasToBase64( 23 | screenShotCanvas, 24 | startX, 25 | startY, 26 | width, 27 | height, 28 | 0.75, 29 | plugInParameters.getWriteImgState() 30 | ); 31 | } 32 | } 33 | return base64; 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/common-methods/GetColor.ts: -------------------------------------------------------------------------------- 1 | import InitData from "@/lib/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 | -------------------------------------------------------------------------------- /src/lib/common-methods/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/lib/common-methods/GetToolRelativePosition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取截图工具栏相对于视口的位置 3 | */ 4 | export function getToolRelativePosition( 5 | left?: number, 6 | top?: number, 7 | dom: HTMLElement = document.body 8 | ) { 9 | const rect = dom.getBoundingClientRect(); 10 | return { left: left || Math.abs(rect.left), top: top || Math.abs(rect.top) }; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/common-methods/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/lib/common-methods/SaveBorderArrInfo.ts: -------------------------------------------------------------------------------- 1 | import { cutOutBoxBorder, positionInfoType } from "@/lib/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 | -------------------------------------------------------------------------------- /src/lib/common-methods/SaveCanvasToBase64.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将指定区域的canvas转换为base64格式的图片 3 | */ 4 | import { getCanvas2dCtx } from "@/lib/common-methods/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/lib/common-methods/SaveCanvasToImage.ts: -------------------------------------------------------------------------------- 1 | import { getCanvas2dCtx } from "@/lib/common-methods/CanvasPatch"; 2 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 3 | 4 | export function saveCanvasToImage( 5 | context: CanvasRenderingContext2D, 6 | startX: number, 7 | startY: number, 8 | width: number, 9 | height: number 10 | ) { 11 | const plugInParameters = new PlugInParameters(); 12 | // 获取设备像素比 13 | const dpr = window.devicePixelRatio || 1; 14 | // 获取裁剪框区域图片信息 15 | // 获取裁剪框区域图片信息 16 | const img = context.getImageData( 17 | startX * dpr, 18 | startY * dpr, 19 | width * dpr, 20 | height * dpr 21 | ); 22 | // 创建canvas标签,用于存放裁剪区域的图片 23 | const canvas = document.createElement("canvas"); 24 | // 获取裁剪框区域画布 25 | const imgContext = getCanvas2dCtx(canvas, width, height); 26 | if (imgContext) { 27 | // 将图片放进裁剪框内 28 | imgContext.putImageData(img, 0, 0); 29 | const a = document.createElement("a"); 30 | // 获取图片 31 | a.href = canvas.toDataURL("png"); 32 | // 获取用户传入的文件名 33 | const imgName = plugInParameters?.getSaveImgTitle() || new Date().getTime(); 34 | // 下载图片 35 | a.download = `${imgName}.png`; 36 | a.click(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/common-methods/SelectColor.ts: -------------------------------------------------------------------------------- 1 | import InitData from "@/lib/main-entrance/InitData"; 2 | 3 | export function selectColor() { 4 | const data = new InitData(); 5 | // 显示颜色选择面板 6 | data.setColorPanelStatus(true); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/common-methods/SelectTextSize.ts: -------------------------------------------------------------------------------- 1 | import InitData from "@/lib/main-entrance/InitData"; 2 | 3 | export function selectTextSize() { 4 | const data = new InitData(); 5 | // 显示文字大小选择面板 6 | data.setTextSizeOptionStatus(true); 7 | } 8 | 9 | export function setTextSize(size: number) { 10 | const data = new InitData(); 11 | // 设置字体大小 12 | data.setFontSize(size); 13 | } 14 | 15 | export function getTextSize() { 16 | const data = new InitData(); 17 | // 获取字体大小 18 | return data.getFontSize(); 19 | } 20 | 21 | export function hiddenTextSizeOptionStatus() { 22 | const data = new InitData(); 23 | // 隐藏文字大小选择面板 24 | data.setTextSizeOptionStatus(false); 25 | } 26 | 27 | export function hiddenColorPanelStatus() { 28 | const data = new InitData(); 29 | // 隐藏颜色选择面板 30 | data.setColorPanelStatus(false); 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/common-methods/SetBrushSize.ts: -------------------------------------------------------------------------------- 1 | import InitData from "@/lib/main-entrance/InitData"; 2 | import { setSelectedClassName } from "@/lib/common-methods/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 | 34 | /** 35 | * 设置马赛克工具的笔触大小 36 | * @param size 37 | * @param index 38 | * @param mouseEvent 39 | */ 40 | export function setMosaicPenSize( 41 | size: string, 42 | index: number, 43 | mouseEvent: MouseEvent 44 | ) { 45 | const data = new InitData(); 46 | // 为当前点击项添加选中时的class名 47 | setSelectedClassName(mouseEvent, index, true); 48 | let sizeNum = 10; 49 | switch (size) { 50 | case "small": 51 | sizeNum = 10; 52 | break; 53 | case "medium": 54 | sizeNum = 20; 55 | break; 56 | case "big": 57 | sizeNum = 40; 58 | break; 59 | } 60 | data.setMosaicPenSize(sizeNum); 61 | return sizeNum; 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/common-methods/SetSelectedClassName.ts: -------------------------------------------------------------------------------- 1 | import { getSelectedClassName } from "@/lib/common-methods/GetSelectedCalssName"; 2 | import { getBrushSelectedName } from "@/lib/common-methods/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] as HTMLDivElement; 28 | const itemId: string | number = Number(item.getAttribute("data-id")); 29 | // 自定义的图标则重置其选中状态 30 | if (itemId > 100 && index !== Number.MAX_VALUE) { 31 | console.log("reset icon"); 32 | const icon = item.getAttribute("data-icon") as string; 33 | item.style.backgroundImage = `url(${icon})`; 34 | } 35 | // 如果工具栏中已经有选中的class则将其移除 36 | if (item.className.includes("active")) { 37 | item.classList.remove(item.classList[2]); 38 | } 39 | } 40 | if (className) { 41 | // 给当前点击项添加选中时的class 42 | mouseEvent.target.className += " " + className; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/common-methods/TakeOutHistory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 取出一条历史记录 3 | */ 4 | import InitData from "@/lib/main-entrance/InitData"; 5 | 6 | export function takeOutHistory() { 7 | const data = new InitData(); 8 | data.popHistory(); 9 | const screenShortCanvas = data.getScreenShotContainer()?.getContext("2d"); 10 | if (screenShortCanvas != null) { 11 | if (data.getHistory().length > 0) { 12 | screenShortCanvas.putImageData( 13 | data.getHistory()[data.getHistory().length - 1]["data"], 14 | 0, 15 | 0 16 | ); 17 | } 18 | } 19 | 20 | data.setUndoClickNum(data.getUndoClickNum() + 1); 21 | // 历史记录已取完,禁用撤回按钮点击 22 | if (data.getHistory().length - 1 <= 0) { 23 | data.setUndoClickNum(0); 24 | data.setUndoStatus(false); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/common-methods/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/lib/common-methods/ZoomCutOutBoxPosition.ts: -------------------------------------------------------------------------------- 1 | import { nonNegativeData } from "@/lib/common-methods/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/lib/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 | id: 9, 36 | title: "undo" 37 | }, 38 | { 39 | id: 10, 40 | title: "close" 41 | }, 42 | { 43 | id: 11, 44 | title: "confirm" 45 | } 46 | ]; 47 | -------------------------------------------------------------------------------- /src/lib/main-entrance/CreateDom.ts: -------------------------------------------------------------------------------- 1 | import toolbar from "@/lib/config/Toolbar"; 2 | import { 3 | positionInfoType, 4 | screenShotType, 5 | toolbarType, 6 | userToolbarFnType 7 | } from "@/lib/type/ComponentType"; 8 | import { 9 | toolClickEvent, 10 | toolClickEventForUserDefined 11 | } from "@/lib/split-methods/ToolClickEvent"; 12 | import { 13 | setBrushSize, 14 | setMosaicPenSize 15 | } from "@/lib/common-methods/SetBrushSize"; 16 | import { selectColor } from "@/lib/common-methods/SelectColor"; 17 | import { getColor } from "@/lib/common-methods/GetColor"; 18 | import { 19 | getTextSize, 20 | hiddenColorPanelStatus, 21 | hiddenTextSizeOptionStatus, 22 | selectTextSize, 23 | setTextSize 24 | } from "@/lib/common-methods/SelectTextSize"; 25 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 26 | import InitData from "@/lib/main-entrance/InitData"; 27 | 28 | export default class CreateDom { 29 | // 截图区域canvas容器 30 | private readonly screenShotController: HTMLCanvasElement; 31 | // 截图工具栏容器 32 | private readonly toolController: HTMLDivElement; 33 | // 绘制选项顶部ico容器 34 | private readonly optionIcoController: HTMLDivElement; 35 | // 画笔绘制选项容器 36 | private readonly optionController: HTMLDivElement; 37 | // 裁剪框大小显示容器 38 | private readonly cutBoxSizeContainer: HTMLDivElement; 39 | // 文字工具输入容器 40 | private readonly textInputController: HTMLDivElement; 41 | // 截图完成回调函数 42 | private readonly completeCallback: Function | undefined; 43 | // 截图关闭毁掉函数 44 | private readonly closeCallback: Function | undefined; 45 | // 需要隐藏的图标 46 | private readonly hiddenIcoArr: string[]; 47 | private data: InitData; 48 | 49 | // 截图工具栏图标 50 | private readonly toolbar: Array; 51 | 52 | private readonly textFontSizeList = [ 53 | 12, 54 | 13, 55 | 14, 56 | 15, 57 | 16, 58 | 17, 59 | 20, 60 | 24, 61 | 36, 62 | 48, 63 | 64, 64 | 72, 65 | 96 66 | ]; 67 | 68 | constructor(options: screenShotType) { 69 | const plugInParameters = new PlugInParameters(); 70 | this.screenShotController = document.createElement("canvas"); 71 | this.toolController = document.createElement("div"); 72 | this.optionIcoController = document.createElement("div"); 73 | this.optionController = document.createElement("div"); 74 | this.cutBoxSizeContainer = document.createElement("div"); 75 | this.textInputController = document.createElement("div"); 76 | this.completeCallback = options?.completeCallback; 77 | this.closeCallback = options?.closeCallback; 78 | this.hiddenIcoArr = []; 79 | this.toolbar = Object.assign([], toolbar); 80 | this.data = new InitData(); 81 | this.optionController.addEventListener("click", evt => { 82 | const target = evt.target as HTMLElement; 83 | if (target.id === "colorSelectPanel" || target.id === "textSizePanel") { 84 | return; 85 | } 86 | // 点击工具栏的其他位置则隐藏文字大小选择面板与颜色选择面板 87 | hiddenTextSizeOptionStatus(); 88 | hiddenColorPanelStatus(); 89 | }); 90 | // 成功回调函数不存在则设置一个默认的 91 | if ( 92 | !options || 93 | !Object.prototype.hasOwnProperty.call(options, "completeCallback") 94 | ) { 95 | this.completeCallback = (imgInfo: { 96 | base64: string; 97 | cutInfo: positionInfoType; 98 | }) => { 99 | sessionStorage.setItem("screenShotImg", JSON.stringify(imgInfo)); 100 | }; 101 | } 102 | 103 | // 筛选需要隐藏的图标 104 | if (options?.hiddenToolIco) { 105 | for (const iconKey in options.hiddenToolIco) { 106 | if (options.hiddenToolIco[iconKey]) { 107 | this.filterHideIcon(iconKey); 108 | } 109 | } 110 | } 111 | // 为所有dom设置id 112 | this.setAllControllerId(); 113 | // 为画笔绘制选项角标设置class 114 | this.setOptionIcoClassName(); 115 | // 将自定义的数据插入到默认数据的倒数第二个位置 116 | this.toolbar.splice( 117 | toolbar.length - 2, 118 | 0, 119 | ...plugInParameters.getUserToolbar() 120 | ); 121 | // 渲染工具栏 122 | this.setToolBarIco(); 123 | // 渲染文字大小选择容器 124 | this.setTextSizeSelectPanel(); 125 | // 渲染画笔相关选项 126 | this.setBrushSelectPanel(); 127 | // 渲染文本输入 128 | this.setTextInputPanel(); 129 | // 渲染页面 130 | this.setDomToBody(); 131 | // 隐藏所有dom 132 | this.hiddenAllDom(); 133 | } 134 | 135 | // 渲染截图工具栏图标 136 | private setToolBarIco() { 137 | for (let i = 0; i < this.toolbar.length; i++) { 138 | const item = this.toolbar[i]; 139 | // 判断是否有需要隐藏的图标 140 | let icoHiddenStatus = false; 141 | for (let j = 0; j < this.hiddenIcoArr.length; j++) { 142 | if (this.hiddenIcoArr[j] === item.title) { 143 | icoHiddenStatus = true; 144 | break; 145 | } 146 | } 147 | // 图标隐藏状态为true则直接跳过本次循环 148 | if (icoHiddenStatus) continue; 149 | const itemPanel = document.createElement("div"); 150 | // 给itemPanel绑定点击事件 151 | this.bindToolClickEvent(itemPanel, item); 152 | itemPanel.setAttribute("data-title", item.title); 153 | itemPanel.setAttribute("data-id", item.id + ""); 154 | if (item?.icon) { 155 | itemPanel.setAttribute("data-icon", item.icon); 156 | } 157 | this.toolController.appendChild(itemPanel); 158 | } 159 | // 有需要隐藏的截图工具栏时,则修改其最小宽度 160 | if (this.hiddenIcoArr.length > 0) { 161 | this.toolController.style.minWidth = "24px"; 162 | } 163 | } 164 | 165 | // 渲染文字大小选择容器 166 | private setTextSizeSelectPanel() { 167 | // 创建文字展示容器 168 | const textSizePanel = document.createElement("div"); 169 | textSizePanel.className = "text-size-panel"; 170 | textSizePanel.innerText = `${getTextSize()} px`; 171 | textSizePanel.id = "textSizePanel"; 172 | // 创建文字大小选择容器 173 | const textSelectPanel = document.createElement("div"); 174 | textSelectPanel.className = "text-select-panel"; 175 | textSelectPanel.id = "textSelectPanel"; 176 | // 创建文字选择下拉 177 | for (let i = 0; i < this.textFontSizeList.length; i++) { 178 | const itemPanel = document.createElement("div"); 179 | const size = this.textFontSizeList[i]; 180 | itemPanel.className = "text-item"; 181 | itemPanel.setAttribute("data-value", `${size}`); 182 | itemPanel.innerText = `${size} px`; 183 | // 添加点击监听 184 | itemPanel.addEventListener("click", () => { 185 | // 隐藏容器 186 | textSelectPanel.style.display = "none"; 187 | const currentTextSize = itemPanel.getAttribute("data-value"); 188 | // 容器赋值 189 | textSizePanel.innerText = `${currentTextSize} px`; 190 | if (currentTextSize) { 191 | setTextSize(+currentTextSize); 192 | } 193 | }); 194 | textSelectPanel.appendChild(itemPanel); 195 | } 196 | textSizePanel.style.display = "none"; 197 | textSelectPanel.style.display = "none"; 198 | // 容器点击时,展示文字大小选择容器 199 | textSizePanel.addEventListener("click", () => { 200 | selectTextSize(); 201 | }); 202 | this.optionController.appendChild(textSizePanel); 203 | this.optionController.appendChild(textSelectPanel); 204 | } 205 | 206 | // 渲染画笔大小选择图标与颜色选择容器 207 | private setBrushSelectPanel() { 208 | // 创建画笔选择容器 209 | const brushSelectPanel = document.createElement("div"); 210 | brushSelectPanel.id = "brushSelectPanel"; 211 | brushSelectPanel.className = "brush-select-panel"; 212 | for (let i = 0; i < 3; i++) { 213 | // 创建画笔图标容器 214 | const itemPanel = document.createElement("div"); 215 | itemPanel.className = "item-panel"; 216 | switch (i) { 217 | case 0: 218 | itemPanel.classList.add("brush-small"); 219 | itemPanel.classList.add("brush-small-active"); 220 | itemPanel.addEventListener("click", e => { 221 | setBrushSize("small", 1, e); 222 | setMosaicPenSize("small", 1, e); 223 | }); 224 | break; 225 | case 1: 226 | itemPanel.classList.add("brush-medium"); 227 | itemPanel.addEventListener("click", e => { 228 | setBrushSize("medium", 2, e); 229 | setMosaicPenSize("medium", 2, e); 230 | }); 231 | break; 232 | case 2: 233 | itemPanel.classList.add("brush-big"); 234 | itemPanel.addEventListener("click", e => { 235 | setBrushSize("big", 3, e); 236 | setMosaicPenSize("big", 3, e); 237 | }); 238 | break; 239 | } 240 | brushSelectPanel.appendChild(itemPanel); 241 | } 242 | // 右侧颜色选择容器 243 | const rightPanel = document.createElement("div"); 244 | rightPanel.className = "right-panel"; 245 | // 创建颜色选择容器 246 | const colorSelectPanel = document.createElement("div"); 247 | colorSelectPanel.className = "color-select-panel"; 248 | colorSelectPanel.id = "colorSelectPanel"; 249 | colorSelectPanel.addEventListener("click", () => { 250 | selectColor(); 251 | }); 252 | // 创建颜色显示容器 253 | const colorPanel = document.createElement("div"); 254 | colorPanel.id = "colorPanel"; 255 | colorPanel.className = "color-panel"; 256 | colorPanel.style.display = "none"; 257 | for (let i = 0; i < 10; i++) { 258 | const colorItem = document.createElement("div"); 259 | colorItem.className = "color-item"; 260 | colorItem.addEventListener("click", () => { 261 | getColor(i + 1); 262 | }); 263 | colorItem.setAttribute("data-index", i + ""); 264 | colorPanel.appendChild(colorItem); 265 | } 266 | rightPanel.appendChild(colorPanel); 267 | rightPanel.appendChild(colorSelectPanel); 268 | rightPanel.id = "rightPanel"; 269 | // 创建颜色下拉箭头选择容器 270 | const pullDownArrow = document.createElement("div"); 271 | pullDownArrow.className = "pull-down-arrow"; 272 | pullDownArrow.addEventListener("click", () => { 273 | selectColor(); 274 | }); 275 | rightPanel.appendChild(pullDownArrow); 276 | // 向画笔绘制选项容器追加画笔选择和颜色显示容器 277 | this.optionController.appendChild(brushSelectPanel); 278 | this.optionController.appendChild(rightPanel); 279 | } 280 | 281 | // 渲染文本输入区域容器 282 | private setTextInputPanel() { 283 | // 让div可编辑 284 | this.textInputController.contentEditable = "true"; 285 | // 关闭拼写检查 286 | this.textInputController.spellcheck = false; 287 | } 288 | 289 | // 为所有Dom设置id 290 | private setAllControllerId() { 291 | this.screenShotController.id = "screenShotContainer"; 292 | this.toolController.id = "toolPanel"; 293 | this.optionIcoController.id = "optionIcoController"; 294 | this.optionController.id = "optionPanel"; 295 | this.cutBoxSizeContainer.id = "cutBoxSizePanel"; 296 | this.textInputController.id = "textInputPanel"; 297 | } 298 | 299 | // 隐藏所有dom 300 | private hiddenAllDom() { 301 | this.screenShotController.style.display = "none"; 302 | this.toolController.style.display = "none"; 303 | this.optionIcoController.style.display = "none"; 304 | this.optionController.style.display = "none"; 305 | this.cutBoxSizeContainer.style.display = "none"; 306 | this.textInputController.style.display = "none"; 307 | } 308 | 309 | // 将截图相关dom渲染至body 310 | private setDomToBody() { 311 | this.clearBody(); 312 | document.body.appendChild(this.screenShotController); 313 | document.body.appendChild(this.toolController); 314 | document.body.appendChild(this.optionIcoController); 315 | document.body.appendChild(this.optionController); 316 | document.body.appendChild(this.cutBoxSizeContainer); 317 | document.body.appendChild(this.textInputController); 318 | } 319 | 320 | // 清除截图相关dom 321 | private clearBody() { 322 | document.getElementById("screenShotContainer")?.remove(); 323 | document.getElementById("toolPanel")?.remove(); 324 | document.getElementById("optionIcoController")?.remove(); 325 | document.getElementById("optionPanel")?.remove(); 326 | document.getElementById("optionPanel")?.remove(); 327 | document.getElementById("textInputPanel")?.remove(); 328 | } 329 | 330 | // 设置画笔绘制选项顶部ico样式 331 | private setOptionIcoClassName() { 332 | this.optionIcoController.className = "ico-panel"; 333 | } 334 | 335 | // 将需要隐藏的图标放入对应的数组中 336 | private filterHideIcon(icons: string) { 337 | switch (icons) { 338 | case "rightTop": 339 | this.hiddenIcoArr.push("right-top"); 340 | break; 341 | default: 342 | this.hiddenIcoArr.push(icons); 343 | break; 344 | } 345 | } 346 | 347 | // 为工具栏绑定点击事件 348 | public bindToolClickEvent(itemPanel: HTMLDivElement, item: toolbarType) { 349 | // 撤销按钮单独处理 350 | if (item.title == "undo") { 351 | itemPanel.className = `item-panel undo-disabled`; 352 | itemPanel.id = "undoPanel"; 353 | return; 354 | } 355 | itemPanel.className = `item-panel ${item.title}`; 356 | // 默认数据的处理 357 | if (item.id <= 100) { 358 | itemPanel.addEventListener("click", e => { 359 | toolClickEvent( 360 | item.title, 361 | item.id, 362 | e, 363 | this.completeCallback, 364 | this.closeCallback 365 | ); 366 | }); 367 | return; 368 | } 369 | // 用户自定义数据的处理 370 | itemPanel.addEventListener("click", e => { 371 | toolClickEventForUserDefined( 372 | item.id, 373 | item.title, 374 | item.activeIcon as string, 375 | item.clickFn as userToolbarFnType, 376 | e 377 | ); 378 | }); 379 | // 渲染图标 380 | itemPanel.style.backgroundImage = `url(${item.icon})`; 381 | itemPanel.style.backgroundSize = "cover"; 382 | // 鼠标移入时修改图标 383 | itemPanel.addEventListener("mouseenter", e => { 384 | this.switchBgIcon(e, item.activeIcon as string, this.data.getToolId()); 385 | }); 386 | // 鼠标移出时恢复图标 387 | itemPanel.addEventListener("mouseleave", e => { 388 | this.switchBgIcon(e, item.icon as string, this.data.getToolId()); 389 | }); 390 | } 391 | 392 | private switchBgIcon(e: MouseEvent, imgUrl: string, toolId: number | null) { 393 | const elItem = e.target as HTMLDivElement; 394 | const itemId = Number(elItem.getAttribute("data-id")); 395 | // 当前图标处于选中状态时停止图标的更换 396 | if (toolId === itemId) { 397 | return; 398 | } 399 | elItem.style.backgroundImage = `url(${imgUrl})`; 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/lib/main-entrance/InitData.ts: -------------------------------------------------------------------------------- 1 | import { positionInfoType, textInfoType } from "@/lib/type/ComponentType"; 2 | import { takeOutHistory } from "@/lib/common-methods/TakeOutHistory"; 3 | import { getToolRelativePosition } from "@/lib/common-methods/GetToolRelativePosition"; 4 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 5 | 6 | // 裁剪框修剪状态 7 | let draggingTrim = false; 8 | // 裁剪框拖拽状态 9 | let dragging = false; 10 | 11 | // 截图工具栏点击状态 12 | let toolClickStatus = false; 13 | // 当前选择的颜色 14 | let selectedColor = "#F53340"; 15 | // 当前点击的工具栏名称 16 | let toolName = ""; 17 | // 当前点击的工具栏id 18 | let toolId: number | null = null; 19 | // 当前选择的画笔大小 20 | let penSize = 2; 21 | // 马赛克工具的笔触大小 22 | let mosaicPenSize = 10; 23 | // 裁剪框顶点边框直径大小 24 | const borderSize = 10; 25 | // 撤销点击次数 26 | let undoClickNum = 0; 27 | // 画笔历史记录 28 | let history: Array> = []; 29 | // 文本输入工具栏点击状态 30 | const textClickStatus = false; 31 | // 工具栏超出截图容器状态 32 | let toolPositionStatus = false; 33 | // 裁剪框位置参数 34 | let cutOutBoxPosition: positionInfoType = { 35 | startX: 0, 36 | startY: 0, 37 | width: 0, 38 | height: 0 39 | }; 40 | 41 | // 获取截图容器dom 42 | let screenShotController: HTMLCanvasElement | null = null; 43 | // 获取截图工具栏容器dom 44 | let toolController: HTMLDivElement | null = null; 45 | let cutBoxSizeContainer: HTMLDivElement | null = null; 46 | // 获取文本输入区域dom 47 | let textInputController: HTMLDivElement | null = null; 48 | // 截图工具栏画笔选择dom 49 | let optionIcoController: HTMLDivElement | null = null; 50 | // 截图工具栏文字大小选择dom 51 | let optionTextSizeController: HTMLDivElement | null = null; 52 | let brushSelectionController: HTMLDivElement | null = null; 53 | let textSizeContainer: HTMLDivElement | null = null; 54 | let fontSize = 17; 55 | let optionController: HTMLDivElement | null = null; 56 | let colorSelectController: HTMLElement | null = null; 57 | let rightPanel: HTMLElement | null = null; 58 | let colorSelectPanel: HTMLElement | null = null; 59 | let undoController: HTMLElement | null = null; 60 | // 屏幕截图容器 61 | let screenShotImageController: HTMLCanvasElement | null = null; 62 | // 截图容器是否可滚动 63 | let noScrollStatus = false; 64 | // 数据初始化标识 65 | let initStatus = false; 66 | // 当前工具栏内选中的工具 67 | let activeTool = ""; 68 | let textInfo: textInfoType; 69 | // 是否需要还原页面的滚动条状态 70 | let resetScrollbarState = false; 71 | // 当前是否处于文本编辑状态 72 | let textEditState = false; 73 | 74 | export default class InitData { 75 | constructor() { 76 | // 标识为true时则初始化数据 77 | if (initStatus) { 78 | // 初始化完成设置其值为false 79 | initStatus = false; 80 | screenShotController = null; 81 | dragging = false; 82 | toolController = null; 83 | textInputController = null; 84 | optionController = null; 85 | optionIcoController = null; 86 | optionTextSizeController = null; 87 | brushSelectionController = null; 88 | textSizeContainer = null; 89 | cutBoxSizeContainer = null; 90 | cutOutBoxPosition = { 91 | startX: 0, 92 | startY: 0, 93 | width: 0, 94 | height: 0 95 | }; 96 | toolClickStatus = false; 97 | resetScrollbarState = false; 98 | textEditState = false; 99 | toolPositionStatus = false; 100 | selectedColor = "#F53340"; 101 | toolName = ""; 102 | toolId = null; 103 | penSize = 2; 104 | fontSize = 17; 105 | mosaicPenSize = 10; 106 | history = []; 107 | undoClickNum = 0; 108 | colorSelectController = null; 109 | rightPanel = null; 110 | colorSelectPanel = null; 111 | undoController = null; 112 | } 113 | } 114 | 115 | // 设置数据初始化标识 116 | public setInitStatus(status: boolean) { 117 | initStatus = status; 118 | } 119 | 120 | // 设置截图容器宽高 121 | public setScreenShotInfo(width: number, height: number) { 122 | this.getScreenShotContainer(); 123 | if (screenShotController == null) return; 124 | // 增加截图锁屏 125 | if (noScrollStatus) { 126 | document.body.classList.add("__screenshot-lock-scroll"); 127 | } 128 | screenShotController.width = width; 129 | screenShotController.height = height; 130 | } 131 | 132 | // 设置截图容器位置 133 | public setScreenShotPosition(left: number, top: number) { 134 | this.getScreenShotContainer(); 135 | if (screenShotController == null) return; 136 | const { left: rLeft, top: rTop } = getToolRelativePosition(left, top); 137 | screenShotController.style.left = rLeft + "px"; 138 | screenShotController.style.top = rTop + "px"; 139 | } 140 | 141 | // 显示截图区域容器 142 | public showScreenShotPanel() { 143 | this.getScreenShotContainer(); 144 | if (screenShotController == null) return; 145 | screenShotController.style.display = "block"; 146 | } 147 | 148 | // 获取截图容器dom 149 | public getScreenShotContainer() { 150 | screenShotController = document.getElementById( 151 | "screenShotContainer" 152 | ) as HTMLCanvasElement | null; 153 | return screenShotController; 154 | } 155 | 156 | // 获取截图工具栏dom 157 | public getToolController() { 158 | toolController = document.getElementById( 159 | "toolPanel" 160 | ) as HTMLDivElement | null; 161 | return toolController; 162 | } 163 | 164 | // 获取裁剪框尺寸显示容器 165 | public getCutBoxSizeContainer() { 166 | cutBoxSizeContainer = document.getElementById( 167 | "cutBoxSizePanel" 168 | ) as HTMLDivElement | null; 169 | return cutBoxSizeContainer; 170 | } 171 | 172 | // 获取文本输入区域dom 173 | public getTextInputController() { 174 | textInputController = document.getElementById( 175 | "textInputPanel" 176 | ) as HTMLDivElement | null; 177 | return textInputController; 178 | } 179 | 180 | // 获取文本输入工具栏展示状态 181 | public getTextStatus() { 182 | return textClickStatus; 183 | } 184 | 185 | // 获取屏幕截图容器 186 | public getScreenShotImageController() { 187 | return screenShotImageController; 188 | } 189 | 190 | // 设置屏幕截图 191 | public setScreenShotImageController(imageController: HTMLCanvasElement) { 192 | screenShotImageController = imageController; 193 | } 194 | 195 | // 设置截图工具栏展示状态 196 | public setToolStatus(status: boolean) { 197 | toolController = this.getToolController() as HTMLDivElement; 198 | if (status) { 199 | toolController.style.display = "block"; 200 | return; 201 | } 202 | toolController.style.display = "none"; 203 | } 204 | 205 | // 设置裁剪框尺寸显示容器展示状态 206 | public setCutBoxSizeStatus(status: boolean) { 207 | if (cutBoxSizeContainer == null) return; 208 | if (status) { 209 | cutBoxSizeContainer.style.display = "flex"; 210 | return; 211 | } 212 | cutBoxSizeContainer.style.display = "none"; 213 | } 214 | 215 | // 设置裁剪框尺寸显示容器位置 216 | public setCutBoxSizePosition(x: number, y: number) { 217 | if (cutBoxSizeContainer == null) return; 218 | const { left, top } = getToolRelativePosition(x, y); 219 | cutBoxSizeContainer.style.left = left + "px"; 220 | let sscTop = 0; 221 | if (screenShotController) { 222 | sscTop = parseInt(screenShotController.style.top); 223 | } 224 | cutBoxSizeContainer.style.top = top + sscTop + "px"; 225 | } 226 | 227 | public setTextEditState(state: boolean) { 228 | textEditState = state; 229 | } 230 | public getTextEditState() { 231 | return textEditState; 232 | } 233 | 234 | // 设置裁剪框尺寸 235 | public setCutBoxSize(width: number, height: number) { 236 | if (cutBoxSizeContainer == null) return; 237 | // width和height保留整数 238 | width = Math.floor(width); 239 | height = Math.floor(height); 240 | const childrenPanel = cutBoxSizeContainer.childNodes; 241 | // p标签已存在直接更改文本值即可 242 | if (childrenPanel.length > 0) { 243 | (childrenPanel[0] as HTMLParagraphElement).innerText = `${width} * ${height}`; 244 | return; 245 | } 246 | // 不存在则渲染 247 | const textPanel = document.createElement("p"); 248 | textPanel.innerText = `${width} * ${height}`; 249 | cutBoxSizeContainer.appendChild(textPanel); 250 | } 251 | 252 | // 设置文本输入工具栏展示状态 253 | public setTextStatus(status: boolean) { 254 | textInputController = this.getTextInputController(); 255 | if (textInputController == null) return; 256 | if (status) { 257 | // 显示文本输入工具 258 | textInputController.style.display = "block"; 259 | return; 260 | } 261 | textInputController.style.display = "none"; 262 | } 263 | 264 | // 设置截图工具位置信息 265 | public setToolInfo(left: number, top: number) { 266 | toolController = document.getElementById("toolPanel") as HTMLDivElement; 267 | const { left: rLeft, top: rTop } = getToolRelativePosition(left, top); 268 | toolController.style.left = rLeft + "px"; 269 | let sscTop = 0; 270 | if (screenShotController) { 271 | sscTop = parseInt(screenShotController.style.top); 272 | } 273 | toolController.style.top = rTop + sscTop + "px"; 274 | } 275 | 276 | // 获取截图工具栏点击状态 277 | public getToolClickStatus() { 278 | return toolClickStatus; 279 | } 280 | 281 | // 设置截图工具栏点击状态 282 | public setToolClickStatus(status: boolean) { 283 | toolClickStatus = status; 284 | } 285 | 286 | public setResetScrollbarState(state: boolean) { 287 | resetScrollbarState = state; 288 | } 289 | public getResetScrollbarState() { 290 | return resetScrollbarState; 291 | } 292 | 293 | // 获取裁剪框位置信息 294 | public getCutOutBoxPosition() { 295 | return cutOutBoxPosition; 296 | } 297 | 298 | public getDragging() { 299 | return dragging; 300 | } 301 | public setDragging(status: boolean) { 302 | dragging = status; 303 | } 304 | 305 | public getDraggingTrim() { 306 | return draggingTrim; 307 | } 308 | 309 | public getToolPositionStatus() { 310 | return toolPositionStatus; 311 | } 312 | 313 | public setToolPositionStatus(status: boolean) { 314 | toolPositionStatus = status; 315 | } 316 | 317 | public setDraggingTrim(status: boolean) { 318 | draggingTrim = status; 319 | } 320 | 321 | // 设置裁剪框位置信息 322 | public setCutOutBoxPosition( 323 | mouseX: number, 324 | mouseY: number, 325 | width: number, 326 | height: number 327 | ) { 328 | cutOutBoxPosition.startX = mouseX; 329 | cutOutBoxPosition.startY = mouseY; 330 | cutOutBoxPosition.width = width; 331 | cutOutBoxPosition.height = height; 332 | } 333 | 334 | public setFontSize(size: number) { 335 | fontSize = size; 336 | } 337 | 338 | // 设置截图工具栏画笔选择工具展示状态 339 | public setOptionStatus(status: boolean) { 340 | // 获取截图工具栏与三角形角标容器 341 | optionIcoController = this.getOptionIcoController(); 342 | optionController = this.getOptionController(); 343 | if (optionIcoController == null || optionController == null) return; 344 | if (status) { 345 | optionIcoController.style.display = "block"; 346 | optionController.style.display = "block"; 347 | return; 348 | } 349 | optionIcoController.style.display = "none"; 350 | optionController.style.display = "none"; 351 | } 352 | 353 | public getFontSize() { 354 | return fontSize; 355 | } 356 | 357 | // 设置截图工具栏文字大小下拉框选项选择工具展示状态 358 | public setTextSizeOptionStatus(status: boolean) { 359 | optionTextSizeController = this.getOptionTextSizeController(); 360 | if (optionTextSizeController == null) return; 361 | if (status) { 362 | optionTextSizeController.style.display = "flex"; 363 | return; 364 | } 365 | optionTextSizeController.style.display = "none"; 366 | } 367 | 368 | public setTextSizePanelStatus(status: boolean) { 369 | textSizeContainer = this.getTextSizeContainer(); 370 | if (textSizeContainer == null) return; 371 | if (status) { 372 | console.log("显示"); 373 | textSizeContainer.style.display = "flex"; 374 | return; 375 | } 376 | textSizeContainer.style.display = "none"; 377 | } 378 | 379 | public setBrushSelectionStatus(status: boolean) { 380 | brushSelectionController = this.getBrushSelectionController(); 381 | if (brushSelectionController == null) return; 382 | if (status) { 383 | brushSelectionController.style.display = "block"; 384 | return; 385 | } 386 | brushSelectionController.style.display = "none"; 387 | } 388 | 389 | // 隐藏画笔工具栏三角形角标 390 | public hiddenOptionIcoStatus() { 391 | optionIcoController = this.getOptionIcoController(); 392 | if (optionIcoController == null) return; 393 | optionIcoController.style.display = "none"; 394 | } 395 | 396 | // 获取截图工具栏画笔选择工具dom 397 | public getOptionIcoController() { 398 | optionIcoController = document.getElementById( 399 | "optionIcoController" 400 | ) as HTMLDivElement | null; 401 | return optionIcoController; 402 | } 403 | 404 | public getTextSizeContainer() { 405 | textSizeContainer = document.getElementById( 406 | "textSizePanel" 407 | ) as HTMLDivElement | null; 408 | return textSizeContainer; 409 | } 410 | 411 | public getOptionTextSizeController() { 412 | optionTextSizeController = document.getElementById( 413 | "textSelectPanel" 414 | ) as HTMLDivElement | null; 415 | return optionTextSizeController; 416 | } 417 | 418 | public getBrushSelectionController() { 419 | brushSelectionController = document.getElementById( 420 | "brushSelectPanel" 421 | ) as HTMLDivElement | null; 422 | return brushSelectionController; 423 | } 424 | 425 | public getOptionController() { 426 | optionController = document.getElementById( 427 | "optionPanel" 428 | ) as HTMLDivElement | null; 429 | return optionController; 430 | } 431 | 432 | // 设置画笔选择工具栏位置 433 | public setOptionPosition(position: number) { 434 | // 获取截图工具栏与三角形角标容器 435 | optionIcoController = this.getOptionIcoController(); 436 | optionController = this.getOptionController(); 437 | if (optionIcoController == null || optionController == null) return; 438 | // 修改位置 439 | const toolPosition = this.getToolPosition(); 440 | if (toolPosition == null) return; 441 | const icoLeft = toolPosition.left + position + "px"; 442 | const icoTop = toolPosition.top + 44 + "px"; 443 | const optionLeft = toolPosition.left + "px"; 444 | const optionTop = toolPosition.top + 44 + 6 + "px"; 445 | optionIcoController.style.left = icoLeft; 446 | optionIcoController.style.top = icoTop; 447 | optionController.style.left = optionLeft; 448 | optionController.style.top = optionTop; 449 | } 450 | 451 | // 获取工具栏位置 452 | public getToolPosition() { 453 | toolController = this.getToolController(); 454 | if (toolController == null) return; 455 | return { 456 | left: toolController.offsetLeft, 457 | top: toolController.offsetTop 458 | }; 459 | } 460 | 461 | // 获取/设置当前选择的颜色 462 | public getSelectedColor() { 463 | return selectedColor; 464 | } 465 | public setSelectedColor(color: string) { 466 | selectedColor = color; 467 | colorSelectPanel = this.getColorSelectPanel(); 468 | if (colorSelectPanel == null) return; 469 | colorSelectPanel.style.backgroundColor = selectedColor; 470 | } 471 | 472 | public getColorSelectPanel() { 473 | colorSelectPanel = document.getElementById("colorSelectPanel"); 474 | return colorSelectPanel; 475 | } 476 | 477 | // 获取/设置当前点击的工具栏条目名称 478 | public getToolName() { 479 | return toolName; 480 | } 481 | public setToolName(itemName: string) { 482 | toolName = itemName; 483 | } 484 | 485 | public getToolId() { 486 | return toolId; 487 | } 488 | public setToolId(id: number | null) { 489 | toolId = id; 490 | } 491 | 492 | // 获取/设置当前画笔大小 493 | public getPenSize() { 494 | return penSize; 495 | } 496 | public setPenSize(size: number) { 497 | penSize = size; 498 | } 499 | 500 | public getMosaicPenSize() { 501 | return mosaicPenSize; 502 | } 503 | 504 | public setMosaicPenSize(size: number) { 505 | mosaicPenSize = size; 506 | } 507 | 508 | public getBorderSize() { 509 | return borderSize; 510 | } 511 | 512 | public getHistory() { 513 | return history; 514 | } 515 | 516 | public shiftHistory() { 517 | return history.shift(); 518 | } 519 | 520 | public popHistory() { 521 | return history.pop(); 522 | } 523 | 524 | public pushHistory(item: Record) { 525 | history.push(item); 526 | } 527 | 528 | public getUndoClickNum() { 529 | return undoClickNum; 530 | } 531 | public setUndoClickNum(clickNumber: number) { 532 | undoClickNum = clickNumber; 533 | } 534 | 535 | public getColorPanel() { 536 | colorSelectController = document.getElementById("colorPanel"); 537 | return colorSelectController; 538 | } 539 | public setColorPanelStatus(status: boolean) { 540 | colorSelectController = this.getColorPanel(); 541 | if (colorSelectController == null) return; 542 | if (status) { 543 | colorSelectController.style.display = "flex"; 544 | return; 545 | } 546 | colorSelectController.style.display = "none"; 547 | } 548 | 549 | public getNoScrollStatus() { 550 | return noScrollStatus; 551 | } 552 | public setNoScrollStatus(status?: boolean) { 553 | if (status != null) { 554 | noScrollStatus = status; 555 | } 556 | } 557 | 558 | public setActiveToolName(toolName: string) { 559 | activeTool = toolName; 560 | } 561 | 562 | public getActiveToolName() { 563 | return activeTool; 564 | } 565 | 566 | public setTextInfo(info: textInfoType) { 567 | textInfo = info; 568 | } 569 | 570 | public getTextInfo() { 571 | return textInfo; 572 | } 573 | 574 | public getRightPanel() { 575 | rightPanel = document.getElementById("rightPanel"); 576 | return rightPanel; 577 | } 578 | public setRightPanel(status: boolean) { 579 | rightPanel = this.getRightPanel(); 580 | if (rightPanel == null) return; 581 | if (status) { 582 | rightPanel.style.display = "flex"; 583 | return; 584 | } 585 | rightPanel.style.display = "none"; 586 | } 587 | 588 | public setUndoStatus(status: boolean) { 589 | undoController = this.getUndoController(); 590 | if (undoController == null) return; 591 | if (status) { 592 | // 启用撤销按钮 593 | undoController.classList.add("undo"); 594 | undoController.classList.remove("undo-disabled"); 595 | undoController.addEventListener("click", this.cancelEvent); 596 | return; 597 | } 598 | // 禁用撤销按钮 599 | undoController.classList.add("undo-disabled"); 600 | undoController.classList.remove("undo"); 601 | undoController.removeEventListener("click", this.cancelEvent); 602 | } 603 | 604 | public cancelEvent() { 605 | takeOutHistory(); 606 | } 607 | 608 | public getUndoController() { 609 | undoController = document.getElementById("undoPanel"); 610 | return undoController; 611 | } 612 | 613 | // 销毁截图容器 614 | public destroyDOM() { 615 | if ( 616 | screenShotController == null || 617 | toolController == null || 618 | optionIcoController == null || 619 | optionController == null || 620 | textInputController == null || 621 | cutBoxSizeContainer == null 622 | ) 623 | return; 624 | const plugInParameters = new PlugInParameters(); 625 | // 销毁dom 626 | if (noScrollStatus) { 627 | document.body.classList.remove("__screenshot-lock-scroll"); 628 | } 629 | document.body.removeChild(screenShotController); 630 | document.body.removeChild(toolController); 631 | document.body.removeChild(optionIcoController); 632 | document.body.removeChild(optionController); 633 | document.body.removeChild(textInputController); 634 | document.body.removeChild(cutBoxSizeContainer); 635 | if (document.body.classList.contains("no-cursor")) { 636 | document.body.classList.remove("no-cursor"); 637 | } 638 | if (resetScrollbarState) { 639 | // 还原滚动条状态 640 | document.documentElement.classList.remove("hidden-screen-shot-scroll"); 641 | document.body.classList.remove("hidden-screen-shot-scroll"); 642 | } 643 | // 重置插件全局参数状态 644 | plugInParameters.setInitStatus(true); 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /src/lib/main-entrance/PlugInParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | customToolbarType, 3 | mouseEventType, 4 | screenShotType, 5 | userToolbarType 6 | } from "@/lib/type/ComponentType"; 7 | 8 | let enableWebRtc = true; 9 | // electron环境下使用webrtc需要自己传入屏幕流 10 | let screenFlow: MediaStream | null = null; 11 | 12 | // 数据初始化标识 13 | let initStatus = false; 14 | 15 | // 画布宽高 16 | let canvasWidth = 0; 17 | let canvasHeight = 0; 18 | 19 | // 展示截屏图片至容器 20 | let showScreenData = false; 21 | let screenShotDom: null | HTMLElement = null; 22 | let destroyContainer = true; 23 | 24 | // 蒙层颜色 25 | const maskColor = { r: 0, g: 0, b: 0, a: 0.6 }; 26 | let writeBase64 = true; 27 | let cutBoxBdColor = "#2CABFF"; 28 | // 最大可撤销次数 29 | let maxUndoNum = 15; 30 | // 是否使用等比例箭头 31 | let useRatioArrow = false; 32 | // 开启图片自适应 33 | let imgAutoFit = false; 34 | // 自定义传入图片尺寸 35 | let useCustomImgSize = false; 36 | let customImgSize = { w: 0, h: 0 }; 37 | // 调用者定义的工具栏数据 38 | let userToolbar: Array = []; 39 | let h2cCrossImgLoadErrFn: screenShotType["h2cImgLoadErrCallback"] | null = null; 40 | let saveCallback: ((code: number, msg: string) => void) | null = null; 41 | let saveImgTitle: string | null = null; 42 | let canvasEvents: mouseEventType | null = null; 43 | let menuBarHeight = 0; 44 | 45 | export default class PlugInParameters { 46 | constructor() { 47 | // 标识为true时则初始化数据 48 | if (initStatus) { 49 | enableWebRtc = true; 50 | canvasWidth = 0; 51 | canvasHeight = 0; 52 | cutBoxBdColor = "#2CABFF"; 53 | showScreenData = false; 54 | writeBase64 = true; 55 | screenFlow = null; 56 | // 初始化完成设置其值为false 57 | initStatus = false; 58 | screenShotDom = null; 59 | saveCallback = null; 60 | maxUndoNum = 15; 61 | useRatioArrow = false; 62 | imgAutoFit = false; 63 | saveImgTitle = null; 64 | destroyContainer = true; 65 | userToolbar = []; 66 | h2cCrossImgLoadErrFn = null; 67 | menuBarHeight = 0; 68 | } 69 | } 70 | 71 | // 设置数据初始化标识 72 | public setInitStatus(status: boolean) { 73 | initStatus = status; 74 | } 75 | 76 | // 获取数据初始化标识 77 | public getInitStatus() { 78 | return initStatus; 79 | } 80 | 81 | // 获取webrtc启用状态 82 | public getWebRtcStatus() { 83 | return enableWebRtc; 84 | } 85 | 86 | // 设置webrtc启用状态 87 | public setWebRtcStatus(status: boolean) { 88 | enableWebRtc = status; 89 | } 90 | 91 | public setScreenShotDom(dom: HTMLElement) { 92 | screenShotDom = dom; 93 | } 94 | 95 | public getCutBoxBdColor() { 96 | return cutBoxBdColor; 97 | } 98 | 99 | public setCutBoxBdColor(color: string) { 100 | cutBoxBdColor = color; 101 | } 102 | 103 | public getScreenShotDom() { 104 | return screenShotDom; 105 | } 106 | 107 | // 获取屏幕流 108 | public getScreenFlow() { 109 | return screenFlow; 110 | } 111 | 112 | // 设置屏幕流 113 | public setScreenFlow(stream: MediaStream) { 114 | screenFlow = stream; 115 | } 116 | 117 | // 获取画布宽高 118 | public getCanvasSize() { 119 | return { canvasWidth: canvasWidth, canvasHeight: canvasHeight }; 120 | } 121 | 122 | // 设置画布宽高 123 | public setCanvasSize(width: number, height: number) { 124 | canvasWidth = width; 125 | canvasHeight = height; 126 | } 127 | 128 | // 获取展示图片至容器的状态 129 | public getShowScreenDataStatus() { 130 | return showScreenData; 131 | } 132 | 133 | // 设置展示图片至容器的状态 134 | public setShowScreenDataStatus(status: boolean) { 135 | showScreenData = status; 136 | } 137 | 138 | // 设置蒙层颜色 139 | public setMaskColor(color: { r: number; g: number; b: number; a: number }) { 140 | maskColor.r = color.r; 141 | maskColor.g = color.g; 142 | maskColor.b = color.b; 143 | maskColor.a = color.a; 144 | } 145 | 146 | public getMaskColor() { 147 | return maskColor; 148 | } 149 | 150 | // 设置截图数据的写入状态 151 | public setWriteImgState(state: boolean) { 152 | writeBase64 = state; 153 | } 154 | 155 | public getWriteImgState() { 156 | return writeBase64; 157 | } 158 | 159 | public setSaveCallback(saveFn: (code: number, msg: string) => void) { 160 | saveCallback = saveFn; 161 | } 162 | 163 | public getSaveCallback() { 164 | return saveCallback; 165 | } 166 | 167 | public setMaxUndoNum(num: number) { 168 | maxUndoNum = num; 169 | } 170 | 171 | public getMaxUndoNum() { 172 | return maxUndoNum; 173 | } 174 | 175 | public setRatioArrow(state: boolean) { 176 | useRatioArrow = state; 177 | } 178 | 179 | public getRatioArrow() { 180 | return useRatioArrow; 181 | } 182 | 183 | public setImgAutoFit(state: boolean) { 184 | imgAutoFit = state; 185 | } 186 | 187 | public getImgAutoFit() { 188 | return imgAutoFit; 189 | } 190 | 191 | public setUseCustomImgSize( 192 | state: boolean, 193 | sizeInfo?: { w: number; h: number } 194 | ) { 195 | if (state && sizeInfo) { 196 | useCustomImgSize = true; 197 | customImgSize = sizeInfo; 198 | } 199 | } 200 | 201 | public getCustomImgSize() { 202 | return { 203 | useCustomImgSize, 204 | customImgSize 205 | }; 206 | } 207 | 208 | public setSaveImgTitle(title: string) { 209 | saveImgTitle = title; 210 | } 211 | 212 | public getSaveImgTitle() { 213 | return saveImgTitle; 214 | } 215 | 216 | public setDestroyContainerState(state: boolean) { 217 | destroyContainer = state; 218 | } 219 | 220 | public getDestroyContainerState() { 221 | return destroyContainer; 222 | } 223 | 224 | public setUserToolbar(toolbar: Array) { 225 | const toolbarData: Array = []; 226 | for (let i = 0; i < toolbar.length; i++) { 227 | const item = toolbar[i]; 228 | // 自定义工具栏id从100开始 229 | toolbarData.push({ ...item, id: 100 + (i + 1) }); 230 | } 231 | userToolbar = toolbarData; 232 | } 233 | 234 | public getUserToolbar() { 235 | return userToolbar; 236 | } 237 | 238 | public setH2cCrossImgLoadErrFn(fn: screenShotType["h2cImgLoadErrCallback"]) { 239 | h2cCrossImgLoadErrFn = fn; 240 | } 241 | 242 | public getH2cCrossImgLoadErrFn() { 243 | return h2cCrossImgLoadErrFn; 244 | } 245 | 246 | public setCanvasEvents(event: mouseEventType) { 247 | canvasEvents = event; 248 | } 249 | public getCanvasEvents() { 250 | return canvasEvents; 251 | } 252 | 253 | public getMenuBarHeight() { 254 | return menuBarHeight; 255 | } 256 | 257 | public setMenuBarHeight(val: number) { 258 | menuBarHeight = val; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/lib/split-methods/AddHistoryData.ts: -------------------------------------------------------------------------------- 1 | import InitData from "@/lib/main-entrance/InitData"; 2 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 3 | 4 | // 保存当前画布状态 5 | export function addHistory() { 6 | const data = new InitData(); 7 | const plugInParameters = new PlugInParameters(); 8 | const screenShotController = data.getScreenShotContainer(); 9 | if (screenShotController == null) return; 10 | // 获取canvas容器 11 | // 获取canvas画布与容器 12 | const context = screenShotController.getContext( 13 | "2d" 14 | ) as CanvasRenderingContext2D; 15 | const controller = screenShotController; 16 | if (data.getHistory().length > plugInParameters.getMaxUndoNum()) { 17 | // 删除最早的一条画布记录 18 | data.shiftHistory(); 19 | } 20 | // 保存当前画布状态 21 | data.pushHistory({ 22 | data: context.getImageData(0, 0, controller.width, controller.height) 23 | }); 24 | // 启用撤销按钮 25 | data.setUndoStatus(true); 26 | } 27 | 28 | export function showLastHistory(context: CanvasRenderingContext2D) { 29 | const data = new InitData(); 30 | context.putImageData( 31 | data.getHistory()[data.getHistory().length - 1]["data"], 32 | 0, 33 | 0 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/split-methods/BoundaryJudgment.ts: -------------------------------------------------------------------------------- 1 | import { positionInfoType } from "@/lib/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/lib/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/lib/split-methods/CalculateToolLocation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | positionInfoType, 3 | toolPositionValType 4 | } from "@/lib/type/ComponentType"; 5 | 6 | /** 7 | * 计算截图工具栏位置 8 | * @param position 裁剪框位置信息 9 | * @param toolWidth 截图工具栏宽度 10 | * @param containerWidth 截图容器宽度 11 | * @param placement 展示位置 12 | * @param containerLocation 截图容器位置信息 13 | */ 14 | export function calculateToolLocation( 15 | position: positionInfoType, 16 | toolWidth: number, 17 | containerWidth: number, 18 | placement: toolPositionValType, 19 | containerLocation: { top: number; left: number } 20 | ) { 21 | // 工具栏X轴坐标 = (裁剪框的宽度 - 工具栏的宽度) / 2 + (裁剪框距离左侧的距离 - 容器距离左侧的距离) 22 | let mouseX = 23 | (position.width - toolWidth) / 2 + 24 | (position.startX - containerLocation.left); 25 | 26 | // 左对齐 27 | if (placement === "left") { 28 | mouseX = position.startX; 29 | } 30 | 31 | // 右对齐 32 | if (placement === "right") { 33 | mouseX = position.startX + position.width - toolWidth; 34 | } 35 | 36 | // 工具栏超出画布左侧可视区域,进行位置修正 37 | if (mouseX < 0) mouseX = 0; 38 | 39 | // 计算工具栏在画布内的占用面积 40 | const toolSize = mouseX + toolWidth; 41 | // 工具栏超出画布右侧可视区域,进行位置修正 42 | if (toolSize > containerWidth) { 43 | mouseX = containerWidth - toolWidth; 44 | } 45 | 46 | // 工具栏Y轴坐标 47 | let mouseY = position.startY + position.height + 10; 48 | if ( 49 | (position.width < 0 && position.height < 0) || 50 | (position.width > 0 && position.height < 0) 51 | ) { 52 | // 从右下角或者左下角拖动时,工具条y轴的位置应该为position.startY + 10 53 | mouseY = position.startY + 10; 54 | } 55 | // 需要减去容器本身距离顶部的距离 56 | mouseY -= containerLocation.top; 57 | return { 58 | mouseX, 59 | mouseY 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/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 | private size = 1; 15 | 16 | /** 17 | * 绘制箭头 18 | * @param ctx 需要进行绘制的画布 19 | * @param originX 鼠标按下时的x轴坐标 20 | * @param originY 鼠标按下式的y轴坐标 21 | * @param x 当前鼠标x轴坐标 22 | * @param y 当前鼠标y轴坐标 23 | * @param color 箭头颜色 24 | * @param size 箭头尺寸 25 | */ 26 | public draw( 27 | ctx: CanvasRenderingContext2D, 28 | originX: number, 29 | originY: number, 30 | x: number, 31 | y: number, 32 | color: string, 33 | size: number 34 | ) { 35 | this.beginPoint.x = originX; 36 | this.beginPoint.y = originY; 37 | this.stopPoint.x = x; 38 | this.stopPoint.y = y; 39 | this.arrowCord(this.beginPoint, this.stopPoint); 40 | this.sideCord(); 41 | this.drawArrow(ctx, color); 42 | switch (size) { 43 | case 2: 44 | this.size = 1; 45 | break; 46 | case 5: 47 | this.size = 1.3; 48 | break; 49 | case 10: 50 | this.size = 1.7; 51 | break; 52 | default: 53 | this.size = 1; 54 | break; 55 | } 56 | } 57 | 58 | // 计算箭头底边两个点位置信息 59 | private arrowCord( 60 | beginPoint: { x: number; y: number }, 61 | stopPoint: { x: number; y: number } 62 | ) { 63 | this.polygonVertex[0] = beginPoint.x; 64 | // 多边形的第一个顶点设为起点 65 | this.polygonVertex[1] = beginPoint.y; 66 | this.polygonVertex[6] = stopPoint.x; 67 | // 第七个顶点设为终点 68 | this.polygonVertex[7] = stopPoint.y; 69 | // 计算夹角 70 | this.getRadian(beginPoint, stopPoint); 71 | // 使用三角函数计算出8、9顶点的坐标 72 | this.polygonVertex[8] = 73 | stopPoint.x - 74 | this.arrowInfo.edgeLen * 75 | Math.cos((Math.PI / 180) * (this.angle + this.arrowInfo.angle)); 76 | this.polygonVertex[9] = 77 | stopPoint.y - 78 | this.arrowInfo.edgeLen * 79 | Math.sin((Math.PI / 180) * (this.angle + this.arrowInfo.angle)); 80 | // 使用三角函数计算出4、5顶点的坐标 81 | this.polygonVertex[4] = 82 | stopPoint.x - 83 | this.arrowInfo.edgeLen * 84 | Math.cos((Math.PI / 180) * (this.angle - this.arrowInfo.angle)); 85 | this.polygonVertex[5] = 86 | stopPoint.y - 87 | this.arrowInfo.edgeLen * 88 | Math.sin((Math.PI / 180) * (this.angle - this.arrowInfo.angle)); 89 | } 90 | 91 | // 计算两个点之间的夹角 92 | private getRadian( 93 | beginPoint: { x: number; y: number }, 94 | stopPoint: { x: number; y: number } 95 | ) { 96 | // 使用atan2算出夹角(弧度),并将其转换为角度值(弧度 / 180) 97 | this.angle = 98 | (Math.atan2(stopPoint.y - beginPoint.y, stopPoint.x - beginPoint.x) / 99 | Math.PI) * 100 | 180; 101 | 102 | this.setArrowInfo(50 * this.size, 30 * this.size); 103 | this.dynArrowSize(); 104 | } 105 | 106 | // 计算另两个底边侧面点 107 | private sideCord() { 108 | const midpoint: { x: number; y: number } = { x: 0, y: 0 }; 109 | 110 | midpoint.x = (this.polygonVertex[4] + this.polygonVertex[8]) / 2; 111 | // 通过求出第5个顶点和第9个顶点的横纵坐标的平均值,得到多边形的中心点坐标, 112 | midpoint.y = (this.polygonVertex[5] + this.polygonVertex[9]) / 2; 113 | this.polygonVertex[2] = (this.polygonVertex[4] + midpoint.x) / 2; 114 | this.polygonVertex[3] = (this.polygonVertex[5] + midpoint.y) / 2; 115 | this.polygonVertex[10] = (this.polygonVertex[8] + midpoint.x) / 2; 116 | this.polygonVertex[11] = (this.polygonVertex[9] + midpoint.y) / 2; 117 | } 118 | 119 | /** 120 | * 设置箭头的相关绘制信息 121 | * @param edgeLen 长度 122 | * @param angle 角度 123 | * @private 124 | */ 125 | private setArrowInfo(edgeLen: number, angle: number) { 126 | this.arrowInfo.edgeLen = edgeLen; 127 | this.arrowInfo.angle = angle; 128 | } 129 | 130 | // 计算箭头尺寸 131 | private dynArrowSize() { 132 | const x = this.stopPoint.x - this.beginPoint.x; 133 | const y = this.stopPoint.y - this.beginPoint.y; 134 | // 计算两点之间的直线距离 135 | const length = Math.sqrt(x ** 2 + y ** 2); 136 | 137 | // 根据箭头始点和终点之间的距离自适应地调整箭头大小。 138 | if (length < 50) { 139 | this.arrowInfo.edgeLen = length / 2; 140 | } else if (length < 250) { 141 | this.arrowInfo.edgeLen /= 2; 142 | } else if (length < 500) { 143 | this.arrowInfo.edgeLen = (this.arrowInfo.edgeLen * length) / 500; 144 | } 145 | } 146 | 147 | // 在画布上画出递增变粗的箭头 148 | private drawArrow(ctx: CanvasRenderingContext2D, color: string) { 149 | ctx.fillStyle = color; 150 | ctx.beginPath(); 151 | ctx.moveTo(this.polygonVertex[0], this.polygonVertex[1]); 152 | ctx.lineTo(this.polygonVertex[2], this.polygonVertex[3]); 153 | ctx.lineTo(this.polygonVertex[4], this.polygonVertex[5]); 154 | ctx.lineTo(this.polygonVertex[6], this.polygonVertex[7]); 155 | ctx.lineTo(this.polygonVertex[8], this.polygonVertex[9]); 156 | ctx.lineTo(this.polygonVertex[10], this.polygonVertex[11]); 157 | ctx.closePath(); 158 | ctx.fill(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/lib/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/lib/split-methods/DrawCutOutBox.ts: -------------------------------------------------------------------------------- 1 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 2 | 3 | /** 4 | * 绘制裁剪框 5 | * @param mouseX 鼠标x轴坐标 6 | * @param mouseY 鼠标y轴坐标 7 | * @param width 裁剪框宽度 8 | * @param height 裁剪框高度 9 | * @param context 需要进行绘制的canvas画布 10 | * @param borderSize 边框节点直径 11 | * @param controller 需要进行操作的canvas容器 12 | * @param imageController 图片canvas容器 13 | * @param drawBorders 14 | * @private 15 | */ 16 | 17 | export function drawCutOutBox( 18 | mouseX: number, 19 | mouseY: number, 20 | width: number, 21 | height: number, 22 | context: CanvasRenderingContext2D, 23 | borderSize: number, 24 | controller: HTMLCanvasElement, 25 | imageController: HTMLCanvasElement, 26 | drawBorders = true 27 | ) { 28 | // 获取画布宽高 29 | const canvasWidth = controller?.width; 30 | const canvasHeight = controller?.height; 31 | const dpr = window.devicePixelRatio || 1; 32 | const data = new PlugInParameters(); 33 | 34 | // 画布、图片不存在则return 35 | if (!canvasWidth || !canvasHeight || !imageController || !controller) return; 36 | 37 | // 清除画布 38 | context.clearRect(0, 0, canvasWidth, canvasHeight); 39 | width = width != 0 ? width : 5; 40 | height = height != 0 ? height : 5; 41 | 42 | // 绘制蒙层 43 | context.save(); 44 | const maskColor = data.getMaskColor(); 45 | context.fillStyle = "rgba(0, 0, 0, .6)"; 46 | if (maskColor) { 47 | context.fillStyle = `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${maskColor.a})`; 48 | } 49 | context.fillRect(0, 0, canvasWidth, canvasHeight); 50 | // 将蒙层凿开 51 | context.globalCompositeOperation = "source-atop"; 52 | // 裁剪选择框 53 | context.clearRect(mouseX, mouseY, width, height); 54 | // 绘制8个边框像素点并保存坐标信息以及事件参数 55 | context.globalCompositeOperation = "source-over"; 56 | context.fillStyle = data.getCutBoxBdColor(); 57 | // 是否绘制裁剪框的8个像素点 58 | if (drawBorders) { 59 | // 像素点大小 60 | const size = borderSize; 61 | // 绘制像素点 62 | context.fillRect(mouseX - size / 2, mouseY - size / 2, size, size); 63 | context.fillRect( 64 | mouseX - size / 2 + width / 2, 65 | mouseY - size / 2, 66 | size, 67 | size 68 | ); 69 | context.fillRect(mouseX - size / 2 + width, mouseY - size / 2, size, size); 70 | context.fillRect( 71 | mouseX - size / 2, 72 | mouseY - size / 2 + height / 2, 73 | size, 74 | size 75 | ); 76 | context.fillRect( 77 | mouseX - size / 2 + width, 78 | mouseY - size / 2 + height / 2, 79 | size, 80 | size 81 | ); 82 | context.fillRect(mouseX - size / 2, mouseY - size / 2 + height, size, size); 83 | context.fillRect( 84 | mouseX - size / 2 + width / 2, 85 | mouseY - size / 2 + height, 86 | size, 87 | size 88 | ); 89 | context.fillRect( 90 | mouseX - size / 2 + width, 91 | mouseY - size / 2 + height, 92 | size, 93 | size 94 | ); 95 | } 96 | // 绘制结束 97 | context.restore(); 98 | // 使用drawImage将图片绘制到蒙层下方 99 | context.save(); 100 | 101 | context.globalCompositeOperation = "destination-over"; 102 | // 图片尺寸使用canvas容器的css中的尺寸 103 | let { imgWidth, imgHeight } = { 104 | imgWidth: parseInt(controller?.style.width), 105 | imgHeight: parseInt(controller?.style.height) 106 | }; 107 | 108 | // 用户有传入截图dom绘制时使用其dom的尺寸 109 | const screenShotDom = data.getScreenShotDom(); 110 | if (screenShotDom != null) { 111 | imgWidth = screenShotDom.clientWidth; 112 | imgHeight = screenShotDom.clientHeight; 113 | } 114 | 115 | // 用户有传入自定义尺寸则使用 116 | if (data.getCustomImgSize().useCustomImgSize) { 117 | const { w, h } = data.getCustomImgSize().customImgSize; 118 | imgWidth = w; 119 | imgHeight = h; 120 | } 121 | 122 | // 非webrtc模式、未开启图片自适应、未自定义图片尺寸、未传入截图dom时,图片的宽高不做处理 123 | if ( 124 | !data.getWebRtcStatus() && 125 | !data.getImgAutoFit() && 126 | !data.getCustomImgSize().useCustomImgSize && 127 | screenShotDom == null 128 | ) { 129 | imgWidth = imageController.width / dpr; 130 | imgHeight = imageController.height / dpr; 131 | } 132 | 133 | context.drawImage(imageController, 0, 0, imgWidth, imgHeight); 134 | context.restore(); 135 | // 返回裁剪框临时位置信息 136 | if (width > 0 && height > 0) { 137 | // 考虑左上往右下拉区域的情况 138 | return { 139 | startX: mouseX, 140 | startY: mouseY, 141 | width: width, 142 | height: height 143 | }; 144 | } else if (width < 0 && height < 0) { 145 | // 考虑右下往左上拉区域的情况 146 | return { 147 | startX: mouseX + width, 148 | startY: mouseY + height, 149 | width: Math.abs(width), 150 | height: Math.abs(height) 151 | }; 152 | } else if (width > 0 && height < 0) { 153 | // 考虑左下往右上拉区域的情况 154 | return { 155 | startX: mouseX, 156 | startY: mouseY + height, 157 | width: width, 158 | height: Math.abs(height) 159 | }; 160 | } else if (width < 0 && height > 0) { 161 | // 考虑右上往左下拉区域的情况 162 | return { 163 | startX: mouseX + width, 164 | startY: mouseY, 165 | width: Math.abs(width), 166 | height: height 167 | }; 168 | } 169 | return { 170 | startX: mouseX, 171 | startY: mouseY, 172 | width: width, 173 | height: height 174 | }; 175 | } 176 | -------------------------------------------------------------------------------- /src/lib/split-methods/DrawImgToCanvas.ts: -------------------------------------------------------------------------------- 1 | export function drawImgToCanvas( 2 | imgSrc: string, 3 | width: number, 4 | height: number, 5 | dpr: number 6 | ): Promise { 7 | return new Promise((resolve, reject) => { 8 | const canvasElement = document.createElement("canvas"); 9 | const ctx = canvasElement.getContext("2d"); 10 | canvasElement.width = width * dpr; 11 | canvasElement.height = height * dpr; 12 | // 设置canvas的显示大小 13 | canvasElement.style.width = `${width}px`; 14 | canvasElement.style.height = `${height}px`; 15 | const imgContainer = new Image(); 16 | imgContainer.src = imgSrc; 17 | imgContainer.width = width; 18 | imgContainer.height = height; 19 | imgContainer.crossOrigin = "Anonymous"; 20 | imgContainer.onload = () => { 21 | if (ctx == null) { 22 | reject("图像绘制失败"); 23 | return; 24 | } 25 | ctx.scale(dpr, dpr); 26 | ctx.drawImage(imgContainer, 0, 0, width, height); 27 | resolve(canvasElement); 28 | }; 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/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 | -------------------------------------------------------------------------------- /src/lib/split-methods/DrawMasking.ts: -------------------------------------------------------------------------------- 1 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 2 | 3 | /** 4 | * 绘制蒙层 5 | * @param context 需要进行绘制canvas 6 | * @param imgData 屏幕截图canvas容器 7 | */ 8 | export function drawMasking( 9 | context: CanvasRenderingContext2D, 10 | imgData?: HTMLCanvasElement 11 | ) { 12 | const data = new PlugInParameters(); 13 | const plugInParameters = new PlugInParameters(); 14 | const canvasSize = plugInParameters.getCanvasSize(); 15 | const viewSize = { 16 | width: parseFloat(window.getComputedStyle(document.body).width), 17 | height: parseFloat(window.getComputedStyle(document.body).height) 18 | }; 19 | const maxWidth = Math.max( 20 | viewSize.width || 0, 21 | Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), 22 | Math.max(document.body.offsetWidth, document.documentElement.offsetWidth), 23 | Math.max(document.body.clientWidth, document.documentElement.clientWidth) 24 | ); 25 | const maxHeight = Math.max( 26 | viewSize.height || 0, 27 | Math.max(document.body.scrollHeight, document.documentElement.scrollHeight), 28 | Math.max(document.body.offsetHeight, document.documentElement.offsetHeight), 29 | Math.max(document.body.clientHeight, document.documentElement.clientHeight) 30 | ); 31 | // 清除画布 32 | context.clearRect(0, 0, maxWidth, maxHeight); 33 | // 屏幕截图存在且展示截图数据的状态为true则进行绘制 34 | if (imgData != null && plugInParameters.getShowScreenDataStatus()) { 35 | // 调用者传了画布尺寸则使用,否则使用窗口宽高 36 | if (canvasSize.canvasWidth !== 0 && canvasSize.canvasHeight !== 0) { 37 | context.drawImage( 38 | imgData, 39 | 0, 40 | 0, 41 | canvasSize.canvasWidth, 42 | canvasSize.canvasHeight 43 | ); 44 | } else { 45 | context.drawImage(imgData, 0, 0, maxWidth, maxHeight); 46 | } 47 | } 48 | // 绘制蒙层 49 | context.save(); 50 | const maskColor = data.getMaskColor(); 51 | context.fillStyle = "rgba(0, 0, 0, .6)"; 52 | if (maskColor) { 53 | context.fillStyle = `rgba(${maskColor.r}, ${maskColor.g}, ${maskColor.b}, ${maskColor.a})`; 54 | } 55 | if (canvasSize.canvasWidth !== 0 && canvasSize.canvasHeight !== 0) { 56 | context.fillRect(0, 0, canvasSize.canvasWidth, canvasSize.canvasHeight); 57 | } else { 58 | context.fillRect(0, 0, maxWidth, maxHeight); 59 | } 60 | // 绘制结束 61 | context.restore(); 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/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/lib/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 | -------------------------------------------------------------------------------- /src/lib/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 | -------------------------------------------------------------------------------- /src/lib/split-methods/DrawText.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 绘制文本 3 | * @param text 需要进行绘制的文字 4 | * @param mouseX 绘制位置的X轴坐标 5 | * @param mouseY 绘制位置的Y轴坐标 6 | * @param color 字体颜色 7 | * @param fontSize 字体大小 8 | * @param context 需要你行绘制的画布 9 | */ 10 | export function drawText( 11 | text: string, 12 | mouseX: number, 13 | mouseY: number, 14 | color: string, 15 | fontSize: number, 16 | context: CanvasRenderingContext2D 17 | ) { 18 | context.save(); 19 | context.lineWidth = 1; 20 | context.fillStyle = color; 21 | context.textBaseline = "middle"; 22 | context.font = `bold ${fontSize}px none`; 23 | // 处理换行符并绘制多行文本 24 | const lines = text.split("\n"); // 根据换行符拆分文本为多行 25 | console.log(lines); 26 | const lineHeight = fontSize * 1.4; // 设定行高为字体大小的1.4倍 27 | lines.forEach((line, index) => { 28 | // 调整每行的垂直位置 29 | const lineY = mouseY + lineHeight * index; 30 | context.fillText(line, mouseX, lineY); 31 | }); 32 | context.restore(); 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/split-methods/KeyboardEventHandle.ts: -------------------------------------------------------------------------------- 1 | // 键盘按下事件处理类 2 | import InitData from "@/lib/main-entrance/InitData"; 3 | 4 | export default class KeyboardEventHandle { 5 | // 截图工具栏容器 6 | private readonly toolController: HTMLDivElement | null = null; 7 | 8 | constructor( 9 | screenShotController: HTMLCanvasElement, 10 | toolController: HTMLDivElement 11 | ) { 12 | const data = new InitData(); 13 | const textInputContainer = document.getElementById("textInputPanel"); 14 | this.toolController = toolController; 15 | // 调整截图容器显示权重 16 | screenShotController.tabIndex = 9999; 17 | // 监听全局键盘按下事件 18 | document.body.addEventListener("keydown", (event: KeyboardEvent) => { 19 | // 文本输入框存在时则终止 20 | if (data.getTextEditState()) { 21 | data.setTextEditState(false); 22 | return; 23 | } 24 | if (event.code === "Escape") { 25 | // ESC按下,触发取消截图事件 26 | this.triggerEvent("close"); 27 | } 28 | 29 | if ( 30 | event.code === "Enter" && 31 | textInputContainer && 32 | textInputContainer.style.display !== "block" 33 | ) { 34 | // Enter按下,触发确认截图事件 35 | this.triggerEvent("confirm"); 36 | } 37 | 38 | // 按下command+z或者ctrl+z快捷键选中撤销工具 39 | if ((event.metaKey || event.ctrlKey) && event.code === "KeyZ") { 40 | this.triggerEvent("undo"); 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * 触发工具栏指定模块的点击事件 47 | * @param eventName 事件名, 与截图工具栏中的data-title属性值保持一致 48 | * @private 49 | */ 50 | public triggerEvent(eventName: string): void { 51 | if (this.toolController == null) return; 52 | for (let i = 0; i < this.toolController.childNodes.length; i++) { 53 | const childNode = this.toolController.childNodes[i] as HTMLDivElement; 54 | const toolName = childNode.getAttribute("data-title"); 55 | if (toolName === eventName) { 56 | // 执行参数事件 57 | childNode.click(); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/split-methods/SetPlugInParameters.ts: -------------------------------------------------------------------------------- 1 | import { screenShotType } from "@/lib/type/ComponentType"; 2 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 3 | 4 | // 为插件的全局参数设置数据 5 | export function setPlugInParameters(options: screenShotType) { 6 | const plugInParameters = new PlugInParameters(); 7 | // webrtc启用状态, 默认为true,如果设置了false则修改默认值 8 | if (options?.enableWebRtc === false) { 9 | plugInParameters.setWebRtcStatus(false); 10 | plugInParameters.setInitStatus(false); 11 | } 12 | 13 | // 读取并设置参数中的视频流数据 14 | if (options?.screenFlow instanceof MediaStream) { 15 | plugInParameters.setScreenFlow(options.screenFlow); 16 | } 17 | 18 | // 读取参数中的画布宽高, 两者都存在时才设置 19 | if (options?.canvasWidth && options?.canvasHeight) { 20 | plugInParameters.setCanvasSize(options.canvasWidth, options.canvasHeight); 21 | } 22 | 23 | // 读取参数设置默认展示截屏数据的状态,默认为false,如果设置了true才修改 24 | if (options?.showScreenData === true) { 25 | plugInParameters.setShowScreenDataStatus(true); 26 | } 27 | if (options?.maskColor && typeof options.maskColor === "object") { 28 | plugInParameters.setMaskColor(options.maskColor); 29 | } 30 | 31 | // 调用者关闭了剪切板写入,则修改全局变量(默认为true) 32 | if (options?.writeBase64 === false) { 33 | plugInParameters.setWriteImgState(options.writeBase64); 34 | } 35 | 36 | // 调用者传入了截图dom 37 | if (options?.screenShotDom) { 38 | plugInParameters.setScreenShotDom(options.screenShotDom); 39 | } 40 | 41 | // 调用者传入了裁剪区域边框像素点颜色信息 42 | if (options?.cutBoxBdColor) { 43 | plugInParameters.setCutBoxBdColor(options.cutBoxBdColor); 44 | } 45 | 46 | // 调用者传入了保存截图回调 47 | if (options?.saveCallback) { 48 | plugInParameters.setSaveCallback(options.saveCallback); 49 | } 50 | 51 | // 设置最大撤销次数 52 | if (options?.maxUndoNum) { 53 | plugInParameters.setMaxUndoNum(options.maxUndoNum); 54 | } 55 | 56 | // 箭头绘制工具是否使用等比例绘制方式 57 | if (options?.useRatioArrow) { 58 | plugInParameters.setRatioArrow(options.useRatioArrow); 59 | } 60 | 61 | // 设置图片自适应开启状态 62 | if (options?.imgAutoFit) { 63 | plugInParameters.setImgAutoFit(options.imgAutoFit); 64 | } 65 | 66 | // 设置图片尺寸 67 | if (options?.useCustomImgSize && options?.customImgSize) { 68 | plugInParameters.setUseCustomImgSize( 69 | options.useCustomImgSize, 70 | options.customImgSize 71 | ); 72 | } 73 | 74 | // 设置图片保存时的文件名称 75 | if (options?.saveImgTitle) { 76 | plugInParameters.setSaveImgTitle(options.saveImgTitle); 77 | } 78 | 79 | // 确认截图时,是否需要销毁dom 80 | if (options?.destroyContainer === false) { 81 | console.log("状态设置", options.destroyContainer); 82 | plugInParameters.setDestroyContainerState(options.destroyContainer); 83 | } 84 | 85 | // 设置用户定义的toolbar数据 86 | if (options?.userToolbar) { 87 | plugInParameters.setUserToolbar(options.userToolbar); 88 | } 89 | 90 | // h2c模式下,跨域图片加载失败时的回调函数 91 | if (options?.h2cImgLoadErrCallback) { 92 | plugInParameters.setH2cCrossImgLoadErrFn(options.h2cImgLoadErrCallback); 93 | } 94 | 95 | // 处理用户定义的画布事件 96 | if (options?.canvasEvents) { 97 | plugInParameters.setCanvasEvents(options.canvasEvents); 98 | } 99 | 100 | // 设置标题栏的高度 101 | if (options?.menuBarHeight) { 102 | plugInParameters.setMenuBarHeight(options?.menuBarHeight || 0); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/lib/split-methods/ToolClickEvent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 裁剪框工具栏点击事件 3 | */ 4 | import { setSelectedClassName } from "@/lib/common-methods/SetSelectedClassName"; 5 | import { calculateOptionIcoPosition } from "@/lib/split-methods/CalculateOptionIcoPosition"; 6 | import InitData from "@/lib/main-entrance/InitData"; 7 | import { getCanvasImgData } from "@/lib/common-methods/GetCanvasImgData"; 8 | import { takeOutHistory } from "@/lib/common-methods/TakeOutHistory"; 9 | import { drawCutOutBox } from "@/lib/split-methods/DrawCutOutBox"; 10 | import { drawText } from "@/lib/split-methods/DrawText"; 11 | import { addHistory } from "@/lib/split-methods/AddHistoryData"; 12 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 13 | import { userToolbarFnType } from "@/lib/type/ComponentType"; 14 | 15 | function getToolbarContainer() { 16 | const data = new InitData(); 17 | const textInputController = data.getTextInputController(); 18 | const screenShotController = data.getScreenShotContainer(); 19 | const ScreenShotImageController = data.getScreenShotImageController(); 20 | if (screenShotController == null || ScreenShotImageController == null) 21 | return null; 22 | const screenShotCanvas = screenShotController.getContext( 23 | "2d" 24 | ) as CanvasRenderingContext2D; 25 | return { 26 | textInputController, 27 | screenShotController, 28 | ScreenShotImageController, 29 | screenShotCanvas 30 | }; 31 | } 32 | 33 | // 隐藏文本输入框 34 | function hideTextInput( 35 | toolName: string, 36 | screenShotCanvas: CanvasRenderingContext2D 37 | ) { 38 | const data = new InitData(); 39 | const textInputController = data.getTextInputController(); 40 | if ( 41 | (textInputController != null && data.getTextStatus()) || 42 | (textInputController != null && toolName !== "text") 43 | ) { 44 | const text = textInputController.innerText; 45 | if (text && text !== "") { 46 | const { positionX, positionY, color, size } = data.getTextInfo(); 47 | drawText(text, positionX, positionY, color, size, screenShotCanvas); 48 | // 添加历史记录 49 | addHistory(); 50 | } 51 | textInputController.innerHTML = ""; 52 | data.setTextStatus(false); 53 | } 54 | } 55 | 56 | // 绘制无像素点的裁剪框 57 | function drawCutOutBoxWithoutPixel( 58 | screenShotCanvas: CanvasRenderingContext2D, 59 | screenShotController: HTMLCanvasElement, 60 | ScreenShotImageController: HTMLCanvasElement 61 | ) { 62 | const data = new InitData(); 63 | const leftValue = data.getToolPosition()?.left || 0; 64 | const topValue = data.getToolPosition()?.top || 0; 65 | // 工具栏位置超出时,对其进行修正处理 66 | if (topValue && data.getToolPositionStatus()) { 67 | // 调整工具栏位置 68 | data.setToolInfo(leftValue, topValue - 46); 69 | } 70 | data.setToolStatus(true); 71 | // 获取裁剪框位置信息 72 | const cutBoxPosition = data.getCutOutBoxPosition(); 73 | // 开始绘制无像素点裁剪框 74 | drawCutOutBox( 75 | cutBoxPosition.startX, 76 | cutBoxPosition.startY, 77 | cutBoxPosition.width, 78 | cutBoxPosition.height, 79 | screenShotCanvas, 80 | data.getBorderSize(), 81 | screenShotController as HTMLCanvasElement, 82 | ScreenShotImageController, 83 | false 84 | ); 85 | } 86 | 87 | export function toolClickEvent( 88 | toolName: string, 89 | index: number, 90 | mouseEvent: any, 91 | completeCallback: Function | undefined, 92 | closeCallback: Function | undefined 93 | ) { 94 | const data = new InitData(); 95 | const plugInParameters = new PlugInParameters(); 96 | data.setActiveToolName(toolName); 97 | data.setToolId(index); 98 | const toolBarContainer = getToolbarContainer(); 99 | if (toolBarContainer == null) { 100 | return; 101 | } 102 | const { 103 | screenShotController, 104 | ScreenShotImageController, 105 | screenShotCanvas 106 | } = toolBarContainer; 107 | // 工具栏尚未点击,当前属于首次点击,重新绘制一个无像素点的裁剪框 108 | if (!data.getToolClickStatus()) { 109 | drawCutOutBoxWithoutPixel( 110 | screenShotCanvas, 111 | screenShotController, 112 | ScreenShotImageController 113 | ); 114 | } 115 | // 更新当前点击的工具栏条目 116 | data.setToolName(toolName); 117 | // 为当前点击项添加选中时的class名 118 | setSelectedClassName(mouseEvent, index, false); 119 | if (toolName === "text") { 120 | // 显示文字选择容器 121 | data.setTextSizePanelStatus(true); 122 | // 隐藏画笔尺寸选择容器 123 | data.setBrushSelectionStatus(false); 124 | // 颜色选择容器添加布局兼容样式 125 | data.getColorSelectPanel()?.classList.add("text-select-status"); 126 | } else { 127 | // 隐藏下拉选择框 128 | data.setTextSizePanelStatus(false); 129 | // 显示画笔尺寸选择容器 130 | data.setBrushSelectionStatus(true); 131 | } 132 | // 显示选项面板 133 | data.setOptionStatus(true); 134 | // 设置选项面板位置 135 | data.setOptionPosition(calculateOptionIcoPosition(index)); 136 | data.setRightPanel(true); 137 | if (toolName == "mosaicPen") { 138 | // 马赛克工具隐藏右侧颜色面板与角标 139 | data.setRightPanel(false); 140 | data.hiddenOptionIcoStatus(); 141 | } 142 | // 清空文本输入区域的内容并隐藏文本输入框 143 | hideTextInput(toolName, screenShotCanvas); 144 | // 初始化点击状态 145 | data.setDragging(false); 146 | data.setDraggingTrim(false); 147 | 148 | // 保存图片 149 | if (toolName == "save") { 150 | getCanvasImgData(true); 151 | const callback = plugInParameters.getSaveCallback(); 152 | if (callback) { 153 | callback(0, "保存成功"); 154 | } 155 | // 销毁组件 156 | data.destroyDOM(); 157 | data.setInitStatus(true); 158 | } 159 | // 销毁组件 160 | if (toolName == "close") { 161 | // 触发关闭回调函数 162 | if (closeCallback) { 163 | closeCallback(); 164 | } 165 | data.destroyDOM(); 166 | data.setInitStatus(true); 167 | } 168 | // 确认截图 169 | if (toolName == "confirm") { 170 | const base64 = getCanvasImgData(false); 171 | // 触发回调函数,截图数据回传给插件调用者 172 | if (completeCallback) { 173 | completeCallback({ base64, cutInfo: data.getCutOutBoxPosition() }); 174 | } 175 | if (!plugInParameters.getDestroyContainerState()) { 176 | // 隐藏工具栏 177 | data.setToolStatus(false); 178 | data.setOptionStatus(false); 179 | return; 180 | } 181 | // 销毁组件 182 | data.destroyDOM(); 183 | data.setInitStatus(true); 184 | } 185 | // 撤销 186 | if (toolName == "undo") { 187 | // 隐藏画笔选项工具栏 188 | data.setOptionStatus(false); 189 | takeOutHistory(); 190 | } 191 | 192 | // 设置裁剪框工具栏为点击状态 193 | data.setToolClickStatus(true); 194 | } 195 | 196 | // 处理用户自定义工具栏的点击事件 197 | export function toolClickEventForUserDefined( 198 | index: number, 199 | toolName: string, 200 | activeIcon: string, 201 | clickFn: userToolbarFnType, 202 | mouseEvent: MouseEvent 203 | ) { 204 | const data = new InitData(); 205 | data.setActiveToolName(toolName); 206 | data.setToolId(index); 207 | const target = mouseEvent.target as HTMLDivElement; 208 | target.style.backgroundImage = `url(${activeIcon})`; 209 | const toolBarContainer = getToolbarContainer(); 210 | if (toolBarContainer == null) { 211 | return; 212 | } 213 | const { 214 | screenShotController, 215 | ScreenShotImageController, 216 | screenShotCanvas 217 | } = toolBarContainer; 218 | // 工具栏尚未点击,当前属于首次点击,重新绘制一个无像素点的裁剪框 219 | if (!data.getToolClickStatus()) { 220 | drawCutOutBoxWithoutPixel( 221 | screenShotCanvas, 222 | screenShotController, 223 | ScreenShotImageController 224 | ); 225 | } 226 | clickFn({ 227 | screenShotCanvas, 228 | screenShotController, 229 | ScreenShotImageController, 230 | currentInfo: { 231 | toolName, 232 | toolId: index 233 | } 234 | }); 235 | data.setToolName(toolName); 236 | setSelectedClassName(mouseEvent, Number.MAX_VALUE, false); 237 | // 隐藏选项面板 238 | data.setOptionStatus(false); 239 | hideTextInput(toolName, screenShotCanvas); 240 | // 初始化点击状态 241 | data.setDragging(false); 242 | data.setDraggingTrim(false); 243 | // 设置裁剪框工具栏为点击状态 244 | data.setToolClickStatus(true); 245 | } 246 | -------------------------------------------------------------------------------- /src/lib/split-methods/drawCrossImg.ts: -------------------------------------------------------------------------------- 1 | import PlugInParameters from "@/lib/main-entrance/PlugInParameters"; 2 | 3 | export function drawCrossImg(html: Document) { 4 | const promises: Promise[] = []; 5 | const imageNodes = html.querySelectorAll("img"); 6 | const plugInParameters = new PlugInParameters(); 7 | imageNodes.forEach(element => { 8 | const href = element.getAttribute("src"); 9 | if (!href) return; 10 | if (href && href.startsWith("base64")) return; 11 | const promise = new Promise((resolve, reject) => { 12 | const img = new Image(); 13 | img.crossOrigin = "anonymous"; 14 | img.src = `${href}&time=${+new Date().getTime()}`; 15 | img.onload = function() { 16 | const width = element.width; 17 | const height = element.height; 18 | const canvas = document.createElement("canvas"); 19 | canvas.width = width; 20 | canvas.height = height; 21 | const ctx: any = canvas.getContext("2d"); 22 | ctx.drawImage(img, 0, 0, width, height); 23 | const base64 = canvas?.toDataURL(); 24 | element.setAttribute("src", base64); 25 | resolve("转换成功"); 26 | }; 27 | img.onerror = function(err) { 28 | const h2cCrossImgLoadErrFn = plugInParameters?.getH2cCrossImgLoadErrFn(); 29 | if (h2cCrossImgLoadErrFn && typeof err != "string") { 30 | h2cCrossImgLoadErrFn({ 31 | ...err, 32 | imgUrl: href 33 | }); 34 | } 35 | // 跨域图片加载失败时,此处不做处理 36 | resolve(true); 37 | }; 38 | if (href !== null) { 39 | img.src = href; 40 | } 41 | }); 42 | promises.push(promise as Promise); 43 | }); 44 | return Promise.all(promises); 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/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 | export type textInfoType = { 26 | positionX: number; 27 | positionY: number; 28 | color: string; 29 | size: number; 30 | }; 31 | 32 | // 裁剪框缩放时所返回的数据类型 33 | export type zoomCutOutBoxReturnType = { 34 | tempStartX: number; 35 | tempStartY: number; 36 | tempWidth: number; 37 | tempHeight: number; 38 | }; 39 | 40 | // 工具栏的展示位置 41 | export type toolPositionValType = "left" | "right" | "center"; 42 | 43 | export type hideBarInfoType = { 44 | state: boolean; 45 | color?: string; 46 | fillWidth?: number; 47 | fillHeight?: number; 48 | fillState?: boolean; 49 | }; 50 | 51 | // 绘制裁剪框所返回的数据类型 52 | export type drawCutOutBoxReturnType = { 53 | startX: number; 54 | startY: number; 55 | width: number; 56 | height: number; 57 | }; 58 | 59 | export type toolIcoType = { 60 | [key: string]: boolean | undefined; 61 | square?: boolean; 62 | round?: boolean; 63 | rightTop?: boolean; 64 | brush?: boolean; 65 | mosaicPen?: boolean; 66 | text?: boolean; 67 | separateLine?: boolean; 68 | save?: boolean; 69 | undo?: boolean; 70 | confirm?: boolean; 71 | }; 72 | 73 | export type mouseEventType = { 74 | mouseDownFn: ( 75 | event: MouseEvent | TouchEvent, 76 | mouseX: number, 77 | mouseY: number, 78 | addHistory: () => void 79 | ) => void; 80 | mouseMoveFn: ( 81 | event: MouseEvent | TouchEvent, 82 | mouseInfo: { 83 | startX: number; 84 | startY: number; 85 | currentX: number; 86 | currentY: number; 87 | }, 88 | showLastHistory: (context: CanvasRenderingContext2D) => void 89 | ) => void; 90 | mouseUpFn: ( 91 | showLastHistory: (context: CanvasRenderingContext2D) => void 92 | ) => void; 93 | }; 94 | 95 | // 截图工具栏图标数据类型 96 | export type toolbarType = { 97 | id: number; 98 | title: string; 99 | icon?: string; 100 | activeIcon?: string; 101 | clickFn?: () => void; 102 | }; 103 | export type crcEventType = { state: boolean; handleFn?: () => void }; 104 | 105 | // 用户自定义的工具栏图标数据类型 106 | export type userToolbarType = { 107 | title: string; 108 | icon: string; 109 | activeIcon: string; 110 | clickFn: () => void; 111 | }; 112 | export type customToolbarType = userToolbarType & { id: number }; 113 | 114 | export type userToolbarFnType = (canvasInfo: { 115 | screenShotCanvas: CanvasRenderingContext2D; 116 | screenShotController: HTMLCanvasElement; 117 | ScreenShotImageController: HTMLCanvasElement; 118 | currentInfo: { toolName: string; toolId: number }; 119 | }) => void; // 用户自定义工具栏点击事件 120 | 121 | export type screenShotType = { 122 | enableWebRtc?: boolean; // 是否启用webrtc,默认是启用状态 123 | screenFlow?: MediaStream; // 设备提供的屏幕流数据(用于electron环境下自己传入的视频流数据) 124 | level?: number; // 截图容器层级 125 | canvasWidth?: number; // 截图画布宽度 126 | canvasHeight?: number; // 截图画布高度 127 | completeCallback?: (imgInfo: { 128 | base64: string; 129 | cutInfo: positionInfoType; 130 | }) => void; // 工具栏截图确认回调 131 | closeCallback?: () => void; // 工具栏关闭回调 132 | h2cImgLoadErrCallback?: (err: Event & { imgUrl: string }) => void; // html2canvas跨域图片加载失败回调 133 | triggerCallback?: (res: { 134 | code: number; 135 | msg: string; 136 | displaySurface: string | null; 137 | displayLabel: string | null; 138 | }) => void; // html2canvas截图响应回调 139 | cancelCallback?: (res: { 140 | code: number; 141 | msg: string; 142 | errorInfo: string; 143 | }) => void; // webrtc截图未授权回调 144 | saveCallback?: (code: number, msg: string) => void; // 保存截图回调 145 | position?: { top?: number; left?: number }; // 截图容器位置 146 | clickCutFullScreen?: boolean; // 单击截全屏启用状态, 默认值为false 147 | hiddenToolIco?: toolIcoType; // 需要隐藏的工具栏图标 148 | showScreenData?: boolean; // 展示截屏图片至容器,默认值为false 149 | imgSrc?: string; // 截图内容,默认为false 150 | loadCrossImg?: boolean; // 加载跨域图片状态 151 | proxyUrl?: string; // 代理服务器地址 152 | useCORS?: boolean; // 是否启用跨域 153 | screenShotDom?: HTMLElement | HTMLDivElement | HTMLCanvasElement; // 需要进行截图的容器 154 | cropBoxInfo?: { x: number; y: number; w: number; h: number }; // 是否加载默认的裁剪框 155 | wrcReplyTime?: number; // webrtc捕捉屏幕响应时间,默认为500ms 156 | wrcImgPosition?: { x: number; y: number; w: number; h: number }; // webrtc模式下是否需要对图像进行裁剪 157 | noScroll?: boolean; // 截图容器是否可滚动,默认为true 158 | maskColor?: { r: number; g: number; b: number; a: number }; // 蒙层颜色 159 | toolPosition?: toolPositionValType; // 工具栏显示位置,默认为居中 160 | writeBase64?: boolean; // 是否将截图内容写入剪切板 161 | hiddenScrollBar?: hideBarInfoType; // 是否隐藏滚动条 162 | wrcWindowMode?: boolean; // 是否启用窗口截图模式,默认为当前标签页截图 163 | customRightClickEvent?: crcEventType; // 是否自定义容器的右键点击事件 164 | cutBoxBdColor?: string; // 裁剪区域边框像素点颜色 165 | maxUndoNum?: number; // 最大可撤销次数 166 | useRatioArrow?: boolean; // 是否使用等比例箭头, 默认为false(递增变粗的箭头) 167 | imgAutoFit?: boolean; // 是否开启图片自适应, 默认为false(用户自定义了截图内容的情况下使用) 168 | useCustomImgSize?: boolean; // 自定义图片尺寸, 默认为false(手动传入图片内容时,是否需要自定义其宽高) 169 | customImgSize?: { w: number; h: number }; // 自定义图片尺寸 170 | saveImgTitle?: string; // 保存图片时的文件名 171 | destroyContainer?: boolean; // 确认截图时是否销毁容器 172 | userToolbar?: Array; // 用户自定义的工具栏图标 173 | canvasEvents?: mouseEventType; // 截图画布的事件监听 174 | h2cIgnoreElementsCallback?: (element: Element) => boolean; // html2canvas模式需要忽略的元素回调 175 | menuBarHeight?: number; // 菜单栏高度(针对electron中,全屏模式下) 176 | }; 177 | -------------------------------------------------------------------------------- /tests/lib/common-methods/FixedData.test.ts: -------------------------------------------------------------------------------- 1 | import { fixedData } from "@/lib/common-methods/FixedData"; 2 | 3 | describe("测试计算可视区域边界值函数", () => { 4 | const canvasDistance = 100; 5 | const trimDistance = 10; 6 | test("当前绘制位置超出画布", () => { 7 | // 超出画布时应该做修复 8 | expect(fixedData(canvasDistance + 1, trimDistance, canvasDistance)).toBe( 9 | canvasDistance - trimDistance 10 | ); 11 | }); 12 | 13 | test("当前绘制位置未超出画布", () => { 14 | // 未超出画布不做修复 15 | expect(fixedData(50, trimDistance, canvasDistance)).toBe(50); 16 | }); 17 | 18 | test("当前绘制位置未超出画布且处于裁剪框内", () => { 19 | // 当前绘制位置未超出画布且在裁剪框内也不做修复 20 | expect(fixedData(5, trimDistance, canvasDistance)).toBe(5); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /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": false, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": [ 16 | "src/*" 17 | ] 18 | }, 19 | "declaration": true,// 是否生成声明文件 20 | "declarationDir": "dist/type",// 声明文件打包的位置 21 | "lib": [ 22 | "esnext", 23 | "dom", 24 | "dom.iterable", 25 | "scripthost" 26 | ] 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "src/**/*.tsx", 31 | "tests/**/*.ts", 32 | "tests/**/*.tsx" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------