image-beautifier 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +25 -0
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/favicon.svg +20 -0
- package/index.html +13 -0
- package/jsconfig.json +12 -0
- package/package.json +53 -0
- package/postcss.config.mjs +6 -0
- package/preview.png +0 -0
- package/public/vite.svg +1 -0
- package/src/App.jsx +29 -0
- package/src/assets/blur.svg +8 -0
- package/src/assets/color.svg +1 -0
- package/src/assets/demo.png +0 -0
- package/src/assets/pencil.png +0 -0
- package/src/assets/rotate.png +0 -0
- package/src/components/ColorPicker.jsx +27 -0
- package/src/components/Icon.jsx +81 -0
- package/src/components/editor/Editor.jsx +19 -0
- package/src/components/editor/HotKeys.jsx +48 -0
- package/src/components/editor/View.jsx +167 -0
- package/src/components/editor/Zoom.jsx +54 -0
- package/src/components/editor/layers/FrameBox.jsx +51 -0
- package/src/components/editor/layers/Screenshot.jsx +89 -0
- package/src/components/editor/layers/ShapeLine.jsx +97 -0
- package/src/components/editor/layers/Watermark.jsx +41 -0
- package/src/components/header/EmojiSelect.jsx +35 -0
- package/src/components/header/Header.jsx +134 -0
- package/src/components/header/Logo.jsx +29 -0
- package/src/components/header/MediaLogo.jsx +10 -0
- package/src/components/header/WidthDropdown.jsx +74 -0
- package/src/components/init/Init.jsx +77 -0
- package/src/components/sideBar/BackgroundSelect.jsx +49 -0
- package/src/components/sideBar/CropperImage.jsx +73 -0
- package/src/components/sideBar/CustomSize.jsx +60 -0
- package/src/components/sideBar/DownloadBar.jsx +179 -0
- package/src/components/sideBar/DrawerBar.jsx +64 -0
- package/src/components/sideBar/Position.jsx +45 -0
- package/src/components/sideBar/SideBar.jsx +131 -0
- package/src/components/sideBar/SizeBar.jsx +114 -0
- package/src/components/sideBar/Watermark.jsx +59 -0
- package/src/hooks/useKeyboardShortcuts.js +28 -0
- package/src/hooks/usePaste.js +21 -0
- package/src/index.js +1 -0
- package/src/main.jsx +9 -0
- package/src/stores/editor.js +106 -0
- package/src/stores/index.js +7 -0
- package/src/stores/option.js +96 -0
- package/src/style/main.css +132 -0
- package/src/utils/UndoRedoManager.js +83 -0
- package/src/utils/backgroundConfig.js +387 -0
- package/src/utils/captureScreen.js +28 -0
- package/src/utils/sizeConfig.js +163 -0
- package/src/utils/utils.js +154 -0
- package/tailwind.config.mjs +15 -0
- package/vite.config.js +21 -0
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
root: true,
|
|
3
|
+
env: {
|
|
4
|
+
browser: true,
|
|
5
|
+
es2020: true
|
|
6
|
+
},
|
|
7
|
+
extends: [
|
|
8
|
+
'eslint:recommended',
|
|
9
|
+
'plugin:react/recommended',
|
|
10
|
+
'plugin:react/jsx-runtime',
|
|
11
|
+
'plugin:react-hooks/recommended',
|
|
12
|
+
],
|
|
13
|
+
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
14
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
15
|
+
settings: { react: { version: '18.2' } },
|
|
16
|
+
plugins: ['react-refresh'],
|
|
17
|
+
rules: {
|
|
18
|
+
'react/prop-types': 0,
|
|
19
|
+
'react/jsx-no-target-blank': 'off',
|
|
20
|
+
'react-refresh/only-export-components': [
|
|
21
|
+
'warn',
|
|
22
|
+
{ allowConstantExport: true },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Chenliwen
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 截图美化工具
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
预览地址:[https://screenshot.shoteasy.fun/](https://screenshot.shoteasy.fun/)
|
|
6
|
+
|
|
7
|
+
- 用于截图美化
|
|
8
|
+
- 图片批注
|
|
9
|
+
- 修改尺寸,预设各个社媒平台发布的尺寸模板
|
|
10
|
+
- 方框,圆圈,箭头,Emoji表情等各种尺寸的批注
|
|
11
|
+
- 添加水印
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TODO:
|
|
15
|
+
|
|
16
|
+
- Redo / Undo 步骤记录
|
|
17
|
+
- 设备套壳(macbook/iphone等)
|
|
18
|
+
- 接入Unsplash背景图
|
|
19
|
+
- 文字卡片
|
|
20
|
+
- 代码美化卡片
|
|
21
|
+
- GIF动画
|
|
22
|
+
|
|
23
|
+
将使用于谷歌截图插件: [ShotEasy](https://chromewebstore.google.com/detail/nmppkehciohcgcehlnifgeokgioidknh)
|
|
24
|
+
|
|
25
|
+
使用框架
|
|
26
|
+
|
|
27
|
+
[LeaferJs](https://github.com/leaferjs/ui)
|
|
28
|
+
|
package/favicon.svg
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 510 510" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g filter="url(#filter0_i_40_60)">
|
|
3
|
+
<path d="M60.1774 83.172C64.1232 61.4358 84.9426 47.0139 106.679 50.9598L271.777 80.9308C293.513 84.8767 307.935 105.696 303.989 127.432L247.462 438.817C243.516 460.553 222.696 474.975 200.96 471.029L35.8623 441.058C14.1261 437.112 -0.295702 416.293 3.65017 394.557L60.1774 83.172Z" fill="#812BED"/>
|
|
4
|
+
<path d="M179.201 136.846C173.053 115.627 185.269 93.4418 206.488 87.2932L367.654 40.5909C388.872 34.4423 411.058 46.6588 417.206 67.8773L505.29 371.846C511.438 393.065 499.222 415.25 478.003 421.399L316.837 468.101C295.619 474.25 273.433 462.033 267.285 440.815L179.201 136.846Z" fill="#0DB7FF"/>
|
|
5
|
+
<path d="M206.488 87.2932C185.269 93.4418 173.053 115.627 179.201 136.846L254.874 397.986L303.989 127.432C307.935 105.696 293.513 84.8767 271.777 80.9308L245.134 76.0943L206.488 87.2932Z" fill="#EC83FD"/>
|
|
6
|
+
<path d="M401 404C401 416.703 390.703 427 378 427C365.297 427 355 416.703 355 404C355 391.297 365.297 381 378 381C390.703 381 401 391.297 401 404Z" fill="#0D97D3"/>
|
|
7
|
+
<path d="M144 404C144 416.703 133.703 427 121 427C108.297 427 98 416.703 98 404C98 391.297 108.297 381 121 381C133.703 381 144 391.297 144 404Z" fill="#6616CB"/>
|
|
8
|
+
</g>
|
|
9
|
+
<defs>
|
|
10
|
+
<filter id="filter0_i_40_60" x="3" y="39" width="503.881" height="432.679" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
11
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
12
|
+
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
13
|
+
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
|
14
|
+
<feOffset dx="-2"/>
|
|
15
|
+
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
|
16
|
+
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0"/>
|
|
17
|
+
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_40_60"/>
|
|
18
|
+
</filter>
|
|
19
|
+
</defs>
|
|
20
|
+
</svg>
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Image Beautifier</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/jsconfig.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "image-beautifier",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Used to beautify image share display, add beautiful background, rounded corners, shadows, etc...",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
|
11
|
+
"preview": "vite preview"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@ctrl/tinycolor": "^4.1.0",
|
|
15
|
+
"@emoji-mart/data": "^1.2.1",
|
|
16
|
+
"@emoji-mart/react": "^1.1.1",
|
|
17
|
+
"@leafer-in/arrow": "^1.0.0",
|
|
18
|
+
"@leafer-in/editor": "^1.0.0",
|
|
19
|
+
"@leafer-in/scroll": "^1.0.0",
|
|
20
|
+
"@leafer-in/view": "^1.0.0",
|
|
21
|
+
"antd": "^5.19.1",
|
|
22
|
+
"clsx": "^2.1.1",
|
|
23
|
+
"cropperjs": "^1.6.2",
|
|
24
|
+
"emoji-mart": "^5.6.0",
|
|
25
|
+
"leafer-ui": "^1.0.0",
|
|
26
|
+
"lodash": "^4.17.21",
|
|
27
|
+
"lucide-react": "^0.407.0",
|
|
28
|
+
"mobx": "^6.13.0",
|
|
29
|
+
"mobx-react-lite": "^4.0.7",
|
|
30
|
+
"nanoid": "^5.0.7",
|
|
31
|
+
"react": "^18.3.1",
|
|
32
|
+
"react-cropper": "^2.3.3",
|
|
33
|
+
"react-dom": "^18.3.1",
|
|
34
|
+
"resize-detector": "^0.3.0",
|
|
35
|
+
"tailwind-merge": "^2.4.0",
|
|
36
|
+
"tinykeys": "^2.1.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^18.3.3",
|
|
40
|
+
"@types/react-dom": "^18.3.0",
|
|
41
|
+
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
42
|
+
"autoprefixer": "^10.4.19",
|
|
43
|
+
"eslint": "^8.57.0",
|
|
44
|
+
"eslint-plugin-react": "^7.34.2",
|
|
45
|
+
"eslint-plugin-react-hooks": "^4.6.2",
|
|
46
|
+
"eslint-plugin-react-refresh": "^0.4.7",
|
|
47
|
+
"postcss": "^8.4.39",
|
|
48
|
+
"tailwindcss": "^3.4.4",
|
|
49
|
+
"vite": "^5.3.1"
|
|
50
|
+
},
|
|
51
|
+
"author": "Chenliwen",
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|
package/preview.png
ADDED
|
Binary file
|
package/public/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
package/src/App.jsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { message } from 'antd';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import Header from '@components/header/Header';
|
|
5
|
+
import Editor from '@components/editor/Editor';
|
|
6
|
+
import SideBar from '@components/sideBar/SideBar';
|
|
7
|
+
import Init from '@components/init/Init';
|
|
8
|
+
import stores from '@stores';
|
|
9
|
+
import '@style/main.css';
|
|
10
|
+
|
|
11
|
+
export default observer(() => {
|
|
12
|
+
const workplace = stores.editor.img?.src ? <Editor /> : <Init />
|
|
13
|
+
const [messageApi, contextHolder] = message.useMessage();
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
stores.editor.setMessage(messageApi);
|
|
16
|
+
}, [messageApi]);
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
{contextHolder}
|
|
20
|
+
<div className="polka flex flex-col overflow-hidden antialiased w-full h-[100vh]">
|
|
21
|
+
<Header />
|
|
22
|
+
<div className="flex flex-col flex-1 h-0 md:flex-row md:items-stretch">
|
|
23
|
+
{workplace}
|
|
24
|
+
<SideBar />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</>
|
|
28
|
+
)
|
|
29
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg height="100" width="100" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<filter id="f1" x="0" y="0" xmlns="http://www.w3.org/2000/svg">
|
|
4
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="10" />
|
|
5
|
+
</filter>
|
|
6
|
+
</defs>
|
|
7
|
+
<image width="100" height="100" filter="url(#f1)" href="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png" />
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="22" height="22"><path d="M311.296 428.544V40.96C186.368 94.208 87.552 195.072 36.864 321.024l274.432 274.432V428.544z" fill="#37C15C"></path><path d="M428.544 311.296l274.432-274.432C642.048 12.288 577.536 0 512 0 440.832 0 372.736 14.848 311.296 40.96v387.584l117.248-117.248z" fill="#A9E247"></path><path d="M983.04 311.296h-387.584l117.248 117.248 274.432 274.432c24.576-60.416 36.864-125.44 36.864-190.976 0-71.168-14.848-139.264-40.96-200.704z" fill="#FF7D12"></path><path d="M712.704 595.456V983.04c124.928-53.248 223.744-154.112 274.432-280.064l-274.432-274.432v166.912z" fill="#F93B21"></path><path d="M595.456 712.704l-274.432 274.432c60.928 24.576 125.44 36.864 190.976 36.864 71.168 0 139.264-14.848 200.704-40.96v-387.584l-117.248 117.248z" fill="#846FEF"></path><path d="M428.544 712.704H40.96c53.248 124.928 154.112 223.744 280.064 274.432l274.432-274.432H428.544z" fill="#1E95FF"></path><path d="M311.296 595.456L36.864 321.024C12.288 381.952 0 446.464 0 512c0 71.168 14.848 139.264 40.96 200.704h387.584l-117.248-117.248z" fill="#14EFEF"></path><path d="M595.456 311.296H983.04c-53.248-124.928-154.112-223.744-280.064-274.432L428.544 311.296h166.912z" fill="#FFC931"></path></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ColorPicker, Button } from 'antd';
|
|
2
|
+
import { TinyColor } from '@ctrl/tinycolor';
|
|
3
|
+
import Icon from '@components/Icon';
|
|
4
|
+
|
|
5
|
+
export default (props) => {
|
|
6
|
+
const useDropper = () => {
|
|
7
|
+
if (!window.EyeDropper) return;
|
|
8
|
+
const eyeDropper = new EyeDropper();
|
|
9
|
+
eyeDropper.open().then((result) => {
|
|
10
|
+
const color = result.sRGBHex;
|
|
11
|
+
props?.onChange && props.onChange(new TinyColor(color));
|
|
12
|
+
}).catch((e) => {
|
|
13
|
+
console.log(e);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return <ColorPicker
|
|
17
|
+
{...props}
|
|
18
|
+
panelRender={(panel) => (
|
|
19
|
+
<>
|
|
20
|
+
{window.EyeDropper && <div className="mb-1">
|
|
21
|
+
<Button type="text" shape="circle" size="small" icon={<Icon.Pipette size={16} />} onClick={useDropper} />
|
|
22
|
+
</div>}
|
|
23
|
+
{panel}
|
|
24
|
+
</>
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Camera,
|
|
3
|
+
Check,
|
|
4
|
+
Square,
|
|
5
|
+
Circle,
|
|
6
|
+
Slash,
|
|
7
|
+
MoveDownLeft,
|
|
8
|
+
Pencil,
|
|
9
|
+
Smile,
|
|
10
|
+
Undo,
|
|
11
|
+
Redo,
|
|
12
|
+
ChevronRight,
|
|
13
|
+
ChevronDown,
|
|
14
|
+
ChevronUp,
|
|
15
|
+
RotateCcw,
|
|
16
|
+
ZoomIn,
|
|
17
|
+
ZoomOut,
|
|
18
|
+
Hand,
|
|
19
|
+
Crop,
|
|
20
|
+
FlipHorizontal2,
|
|
21
|
+
FlipVertical2,
|
|
22
|
+
Sunset,
|
|
23
|
+
Box,
|
|
24
|
+
LayoutGrid,
|
|
25
|
+
ImagePlus,
|
|
26
|
+
Type,
|
|
27
|
+
CodeXml,
|
|
28
|
+
ClipboardPaste,
|
|
29
|
+
ImagePlay,
|
|
30
|
+
Maximize,
|
|
31
|
+
ListCollapse,
|
|
32
|
+
ArrowUpRight,
|
|
33
|
+
ArrowDownRight,
|
|
34
|
+
ImageDown,
|
|
35
|
+
Copy,
|
|
36
|
+
Settings2,
|
|
37
|
+
ChevronLeft,
|
|
38
|
+
Pipette,
|
|
39
|
+
Trash2
|
|
40
|
+
} from 'lucide-react';
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
Camera,
|
|
44
|
+
Check,
|
|
45
|
+
Square,
|
|
46
|
+
Circle,
|
|
47
|
+
Slash,
|
|
48
|
+
MoveDownLeft,
|
|
49
|
+
Pencil,
|
|
50
|
+
Smile,
|
|
51
|
+
Undo,
|
|
52
|
+
Redo,
|
|
53
|
+
ChevronRight,
|
|
54
|
+
ChevronDown,
|
|
55
|
+
ChevronUp,
|
|
56
|
+
RotateCcw,
|
|
57
|
+
ZoomIn,
|
|
58
|
+
ZoomOut,
|
|
59
|
+
Hand,
|
|
60
|
+
Crop,
|
|
61
|
+
FlipHorizontal2,
|
|
62
|
+
FlipVertical2,
|
|
63
|
+
Sunset,
|
|
64
|
+
Box,
|
|
65
|
+
LayoutGrid,
|
|
66
|
+
ImagePlus,
|
|
67
|
+
Type,
|
|
68
|
+
CodeXml,
|
|
69
|
+
ClipboardPaste,
|
|
70
|
+
ImagePlay,
|
|
71
|
+
Maximize,
|
|
72
|
+
ListCollapse,
|
|
73
|
+
ArrowUpRight,
|
|
74
|
+
ArrowDownRight,
|
|
75
|
+
ImageDown,
|
|
76
|
+
Copy,
|
|
77
|
+
Settings2,
|
|
78
|
+
ChevronLeft,
|
|
79
|
+
Pipette,
|
|
80
|
+
Trash2
|
|
81
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import View from './View';
|
|
4
|
+
import Zoom from './Zoom';
|
|
5
|
+
|
|
6
|
+
export default observer(() => {
|
|
7
|
+
const [target, setTarget] = useState(null);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className='md:w-0 md:flex-1 overflow-hidden select-none relative'>
|
|
11
|
+
<div className="w-full h-full relative z-0" ref={
|
|
12
|
+
(node) => setTarget(node)
|
|
13
|
+
}>
|
|
14
|
+
{target && <View target={target} />}
|
|
15
|
+
</div>
|
|
16
|
+
<Zoom />
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect } from "react"
|
|
2
|
+
import { tinykeys } from "tinykeys"
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import stores from '@stores';
|
|
5
|
+
|
|
6
|
+
export default observer(() => {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const deleteItem = event => {
|
|
9
|
+
// event.preventDefault();
|
|
10
|
+
const { list } = stores.editor.app?.editor;
|
|
11
|
+
if (list?.length) {
|
|
12
|
+
for (let item of list) {
|
|
13
|
+
item.remove();
|
|
14
|
+
stores.editor.removeShape(item);
|
|
15
|
+
}
|
|
16
|
+
stores.editor.app.editor.cancel();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const handleZoom = type => {
|
|
20
|
+
if (type === 'fit') {
|
|
21
|
+
stores.editor.app?.tree.zoom(type, 100);
|
|
22
|
+
} else {
|
|
23
|
+
stores.editor.app?.tree.zoom(type);
|
|
24
|
+
}
|
|
25
|
+
stores.editor.setScale(stores.editor.app.tree.scale);
|
|
26
|
+
}
|
|
27
|
+
const unsubscribe = tinykeys(window, {
|
|
28
|
+
'Backspace': deleteItem,
|
|
29
|
+
'Delete': deleteItem,
|
|
30
|
+
'$mod+Minus': event => {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
handleZoom('out');
|
|
33
|
+
},
|
|
34
|
+
'$mod+Equal': event => {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
handleZoom('in');
|
|
37
|
+
},
|
|
38
|
+
'$mod+Digit0': event => {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
handleZoom('fit');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
unsubscribe();
|
|
45
|
+
}
|
|
46
|
+
}, [window]);
|
|
47
|
+
return null;
|
|
48
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { App, ResizeEvent, ZoomEvent, DragEvent, Cursor } from 'leafer-ui';
|
|
4
|
+
import debounce from 'lodash/debounce';
|
|
5
|
+
import { addListener, removeListener } from 'resize-detector';
|
|
6
|
+
import rotatePng from '@assets/rotate.png';
|
|
7
|
+
import pencilPng from '@assets/pencil.png';
|
|
8
|
+
import stores from '@stores';
|
|
9
|
+
import FrameBox from './layers/FrameBox';
|
|
10
|
+
import Screenshot from './layers/Screenshot';
|
|
11
|
+
import Watermark from './layers/Watermark';
|
|
12
|
+
import ShapeLine from './layers/ShapeLine';
|
|
13
|
+
import { ScrollBar } from '@leafer-in/scroll'
|
|
14
|
+
import { nanoid } from '@utils/utils';
|
|
15
|
+
import HotKeys from './HotKeys';
|
|
16
|
+
import '@leafer-in/editor';
|
|
17
|
+
import '@leafer-in/view';
|
|
18
|
+
|
|
19
|
+
Cursor.set('pencil', { url: pencilPng });
|
|
20
|
+
|
|
21
|
+
export default observer(({target}) => {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const app = new App({
|
|
24
|
+
view: target,
|
|
25
|
+
editor: {
|
|
26
|
+
lockRatio: 'corner',
|
|
27
|
+
stroke: '#3f99f7',
|
|
28
|
+
skewable: false,
|
|
29
|
+
hover: false,
|
|
30
|
+
middlePoint: { cornerRadius: 100, width: 20, height: 6 },
|
|
31
|
+
rotatePoint: {
|
|
32
|
+
width: 20,
|
|
33
|
+
height: 20,
|
|
34
|
+
fill: {
|
|
35
|
+
type: 'image',
|
|
36
|
+
url: rotatePng,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
tree: {
|
|
41
|
+
usePartRender: true,
|
|
42
|
+
},
|
|
43
|
+
sky: {
|
|
44
|
+
type: 'draw',
|
|
45
|
+
usePartRender: true,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
new ScrollBar(app);
|
|
49
|
+
|
|
50
|
+
stores.editor.setApp(app);
|
|
51
|
+
|
|
52
|
+
app.tree.on(ZoomEvent.ZOOM, () => {
|
|
53
|
+
stores.editor.setScale(app.tree.scale);
|
|
54
|
+
});
|
|
55
|
+
app.tree.on(ResizeEvent.RESIZE, () => {
|
|
56
|
+
stores.editor.setScale(app.tree.scale);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let shapeId = null;
|
|
60
|
+
app.tree.on(DragEvent.START, (arg) => {
|
|
61
|
+
if (!stores.editor.useTool) return;
|
|
62
|
+
const { target } = arg;
|
|
63
|
+
const shape = stores.editor.getShape(target.id);
|
|
64
|
+
if (shape) return;
|
|
65
|
+
shapeId = nanoid();
|
|
66
|
+
const size = arg.getPageBounds();
|
|
67
|
+
const type = stores.editor.useTool;
|
|
68
|
+
const newShape = {
|
|
69
|
+
id: shapeId,
|
|
70
|
+
type,
|
|
71
|
+
fill: stores.editor.annotateColor,
|
|
72
|
+
strokeWidth: stores.editor.strokeWidth,
|
|
73
|
+
zIndex: stores.editor.shapes.size + 1,
|
|
74
|
+
...size
|
|
75
|
+
};
|
|
76
|
+
if (['Slash', 'MoveDownLeft', 'Pencil'].includes(type)) {
|
|
77
|
+
newShape.points = [size.x, size.y];
|
|
78
|
+
}
|
|
79
|
+
stores.editor.addShape(newShape);
|
|
80
|
+
});
|
|
81
|
+
app.tree.on(DragEvent.DRAG, (arg) => {
|
|
82
|
+
if (!stores.editor.useTool) return;
|
|
83
|
+
if (!shapeId) return;
|
|
84
|
+
const shape = stores.editor.getShape(shapeId);
|
|
85
|
+
if (!shape) return;
|
|
86
|
+
const size = arg.getPageBounds();
|
|
87
|
+
const newShape = Object.assign({}, shape, size);
|
|
88
|
+
const { points, type } = newShape;
|
|
89
|
+
if (points && points.length) {
|
|
90
|
+
const { x, y } = arg.getInnerTotal();
|
|
91
|
+
const newX = x > 0 ? size.x + x : size.x;
|
|
92
|
+
const newY = y > 0 ? size.y + y : size.y;
|
|
93
|
+
if (type === 'Pencil') {
|
|
94
|
+
newShape.points = [...points, newX, newY];
|
|
95
|
+
} else {
|
|
96
|
+
newShape.points = [points[0], points[1], newX, newY];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
stores.editor.addShape(newShape);
|
|
100
|
+
});
|
|
101
|
+
app.tree.on(DragEvent.END, () => {
|
|
102
|
+
if (!stores.editor.useTool) return;
|
|
103
|
+
if (!shapeId) return;
|
|
104
|
+
const shape = stores.editor.getShape(shapeId);
|
|
105
|
+
if (shape) {
|
|
106
|
+
if ((shape.width === 0 || shape.height === 0) && !['Slash', 'MoveDownLeft', 'Pencil'].includes(shape.type)) {
|
|
107
|
+
stores.editor.removeShape(shape);
|
|
108
|
+
} else {
|
|
109
|
+
stores.editor.addShape(Object.assign({}, shape, {editable: true}));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
shapeId = null;
|
|
113
|
+
if (stores.editor.useTool !== 'Pencil') stores.editor.setUseTool(null);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 监听容器变化
|
|
117
|
+
const onResize = debounce(() => {
|
|
118
|
+
const { width, height } = target.getBoundingClientRect();
|
|
119
|
+
app.tree.zoom('fit', 100);
|
|
120
|
+
if (stores.option.frameConf.width < width && stores.option.frameConf.height < height) {
|
|
121
|
+
app.tree.zoom(1);
|
|
122
|
+
}
|
|
123
|
+
}, 10);
|
|
124
|
+
|
|
125
|
+
addListener(target, onResize);
|
|
126
|
+
|
|
127
|
+
// setTimeout(() => {
|
|
128
|
+
// const { width, height } = target.getBoundingClientRect();
|
|
129
|
+
// app.tree.zoom('fit', 100);
|
|
130
|
+
// if (stores.option.frameConf.width < width && stores.option.frameConf.height < height) {
|
|
131
|
+
// app.tree.zoom(1);
|
|
132
|
+
// }
|
|
133
|
+
// stores.editor.setScale(app.tree.scale);
|
|
134
|
+
// }, 10);
|
|
135
|
+
|
|
136
|
+
return (() => {
|
|
137
|
+
removeListener(target, onResize);
|
|
138
|
+
stores.editor.destroy();
|
|
139
|
+
});
|
|
140
|
+
}, [target]);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
const { width, height } = target.getBoundingClientRect();
|
|
145
|
+
stores.editor.app.tree.zoom('fit', 100);
|
|
146
|
+
if (stores.option.frameConf.width < width && stores.option.frameConf.height < height) {
|
|
147
|
+
stores.editor.app.tree.zoom(1);
|
|
148
|
+
}
|
|
149
|
+
stores.editor.setScale(stores.editor.app.tree.scale);
|
|
150
|
+
}, 20);
|
|
151
|
+
return (() => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
})
|
|
154
|
+
}, [stores.option.frameConf.width, stores.option.frameConf.height]);
|
|
155
|
+
|
|
156
|
+
if (!stores.editor.app?.tree) return null;
|
|
157
|
+
return (<>
|
|
158
|
+
<FrameBox parent={stores.editor.app.tree} cursor={stores.editor.cursor} {...stores.option.frameConf}>
|
|
159
|
+
{stores.editor.shapesList.map((item) => (
|
|
160
|
+
<ShapeLine key={item.id} {...item} />
|
|
161
|
+
))}
|
|
162
|
+
{stores.editor.img?.src && <Screenshot />}
|
|
163
|
+
{stores.option.waterImg && <Watermark />}
|
|
164
|
+
</FrameBox>
|
|
165
|
+
<HotKeys />
|
|
166
|
+
</>);
|
|
167
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import Icon from '@components/Icon';
|
|
4
|
+
import { Button, Dropdown } from 'antd';
|
|
5
|
+
import stores from '@stores';
|
|
6
|
+
|
|
7
|
+
const items = [
|
|
8
|
+
{
|
|
9
|
+
key: 0.5,
|
|
10
|
+
label: '50%'
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: 1,
|
|
14
|
+
label: '100%'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: 1.5,
|
|
18
|
+
label: '150%'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: 2,
|
|
22
|
+
label: '200%'
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export default observer(() => {
|
|
27
|
+
const handleZoom = (key) => {
|
|
28
|
+
stores.editor.app?.tree.zoom(key);
|
|
29
|
+
stores.editor.setScale(stores.editor.app.tree.scale);
|
|
30
|
+
}
|
|
31
|
+
const handleMenuClick = (item) => {
|
|
32
|
+
const num = Number(item.key);
|
|
33
|
+
if (num === 4) {
|
|
34
|
+
stores.editor.app?.tree.zoom('fit', 100);
|
|
35
|
+
} else {
|
|
36
|
+
stores.editor.app?.tree.zoom(num);
|
|
37
|
+
}
|
|
38
|
+
stores.editor.setScale(stores.editor.app.tree.scale);
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<div className="absolute z-10 bottom-4 gap-2 right-4 flex items-center ">
|
|
42
|
+
<div className="flex bg-white overflow-hidden rounded-full shadow-md">
|
|
43
|
+
<Button type="text" icon={<Icon.ZoomIn size={16} />} onClick={() => handleZoom('in')} />
|
|
44
|
+
<Dropdown menu={{ items, onClick: handleMenuClick }} placement="top">
|
|
45
|
+
<Button type="text">{stores.editor.scale}%</Button>
|
|
46
|
+
</Dropdown>
|
|
47
|
+
<Button type="text" icon={<Icon.ZoomOut size={16} />} onClick={() => handleZoom('out')} />
|
|
48
|
+
</div>
|
|
49
|
+
<div className="rounded-full bg-white shadow-md overflow-hidden">
|
|
50
|
+
<Button type="text" icon={<Icon.Maximize size={16} />} onClick={() => handleMenuClick({key: 4})} />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
});
|