提交 88034db0 编写于 作者: Q qq_41923622

Fri Jan 24 14:45:01 CST 2025 inscode

上级 669eac6d
import { defineConfig } from 'dumi';
export default defineConfig({
outputPath: 'docs-dist',
base: '/tiny-image-editor/',
publicPath: '/tiny-image-editor/',
cssMinifier: 'none',
jsMinifier: 'none',
themeConfig: {
name: 'tiny-image-editor',
},
});
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
node_modules/
docs-dist/
module.exports = {
extends: require.resolve('@umijs/lint/dist/config/eslint'),
};
import { defineConfig } from 'father';
export default defineConfig({
// more father config: https://github.com/umijs/father/blob/master/docs/config.md
esm: { output: 'dist' },
prebundle: {
deps: {
"tiny-image-editor":{
minify: false,
}
},
},
umd: {
name: 'fatherDemo',
output: 'dist2'
}
});
.DS_Store
node_modules node_modules
/dist /dist
.dumi/tmp
.dumi/tmp-test
# local env files .dumi/tmp-production
.env.local .DS_Store
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
package-lock.json
# Editor directories and files
.idea
.vscode/*
!.vscode/preview.yml
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit "${1}"
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
run = "npm i && npm run dev" run = "npm i && yarn run dev"
language = "node" language = "node"
[env] [env]
......
registry=http://registry.npmmirror.com
module.exports = {
pluginSearchDirs: false,
plugins: [
require.resolve('prettier-plugin-organize-imports'),
require.resolve('prettier-plugin-packagejson'),
],
printWidth: 80,
proseWrap: 'never',
singleQuote: true,
trailingComma: 'all',
overrides: [
{
files: '*.md',
options: {
proseWrap: 'preserve',
},
},
],
};
{
"extends": "@umijs/lint/dist/config/stylelint"
}
MIT License
Copyright (c) 1460878723@qq.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# tiny-image-editor
[![NPM version](https://img.shields.io/npm/v/tiny-image-editor.svg?style=flat)](https://npmjs.org/package/tiny-image-editor)
[![NPM downloads](http://img.shields.io/npm/dm/tiny-image-editor.svg?style=flat)](https://npmjs.org/package/tiny-image-editor)
A react image editor of fabricjs
## Usage
`yarn add tiny-image-editor`
or
`npm i tiny-image-editor`
usage
```
import { Editor } from 'tiny-image-editor';
<Editor url="https://github.com/hututuhu/tiny-image-editor/assets/37233828/95077dc7-065d-4fd3-95f8-26399d5993ab" />
```
![ImageEditor](https://github.com/hututuhu/tiny-image-editor/assets/37233828/95077dc7-065d-4fd3-95f8-26399d5993ab)
## Options
TODO
## Development
```bash
# install dependencies
$ yarn install
# develop library by docs demo
$ yarn start
# build library source code
$ yarn run build
# build library source code in watch mode
$ yarn run build:watch
# build docs
$ yarn run docs:build
# check your project for potential problems
$ yarn run doctor
```
## LICENSE
MIT
import React from 'react';
import { Editor } from 'tiny-image-editor';
import image from './basic.jpg';
const Demo = () => {
return (
<div>
<Editor url={image} />
</div>
);
};
export default Demo;
此差异已折叠。
此差异已折叠。
<code src="../demos/Basic.tsx"></code>
<code src="../demos/Basic.tsx"></code>
---
hero:
title: tiny-image-editor
---
<code src="../demos/Basic.tsx"></code>
console.log("欢迎来到 InsCode");
\ No newline at end of file
此差异已折叠。
{ {
"name": "nodejs", "name": "tiny-image-editor",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "A react image editor of fabricjs",
"main": "index.js", "author": "hushanqing",
"scripts": { "license": "MIT",
"dev": "node index.js", "homepage": "https://github.com/hututuhu/tiny-image-editor",
"test": "echo \"Error: no test specified\" && exit 1" "repository": {
}, "type": "git",
"keywords": [], "url": "https://github.com/hututuhu/tiny-image-editor"
"author": "", },
"license": "ISC", "module": "dist/index.js",
"dependencies": { "types": "dist/index.d.ts",
"@types/node": "^18.0.6", "files": [
"node-fetch": "^3.2.6" "dist"
} ],
} "scripts": {
"build": "father build",
\ No newline at end of file "build:watch": "father dev",
"dev": "dumi dev",
"docs:build": "dumi build",
"doctor": "father doctor",
"lint": "npm run lint:es && npm run lint:css",
"lint:css": "stylelint \"{src,test}/**/*.{css,less}\"",
"lint:es": "eslint \"{src,test}/**/*.{js,jsx,ts,tsx}\"",
"prepare": "husky install && dumi setup",
"prepublishOnly": "father doctor && npm run build",
"start": "npm run dev"
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"lint-staged": {
"*.{md,json}": [
"prettier --write --no-error-on-unmatched-pattern"
],
"*.{css,less}": [
"stylelint --fix",
"prettier --write"
],
"*.{js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{ts,tsx}": [
"eslint --fix",
"prettier --parser=typescript --write"
]
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"fabric": "^5.3.0",
"rc-slider": "^10.5.0",
"react-popper": "^2.3.0"
},
"devDependencies": {
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@types/fabric": "^5.3.6",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@umijs/lint": "^4.0.0",
"dumi": "^2.2.13",
"eslint": "^8.23.0",
"father": "^4.1.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.0.0",
"prettier-plugin-packagejson": "^2.2.18",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"stylelint": "^14.9.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"publishConfig": {
"access": "public"
},
"authors": [
"1460878723@qq.com"
]
}
export * from './tiny-image-editor';
## API
| 参数(prop) | 类型(type) | 默认值(defaultValue) | 说明(description) |
| ---------- | ------------------------------ | -------------------- | ----------------- |
| url | string | - | 图片 URL |
| style | React.CSSProperties | - | 内联样式 |
| menus | MENU_TYPE_ENUM[] | - | 菜单 |
| lang | zh or en | en | 语言-language |
| onDownLoad | (param: IDownloadBody) => void | - | 下载回调 |
## MENU_TYPE_ENUM
```
export enum MENU_TYPE_ENUM {
crop = 'crop',
history = 'history',
download = 'download',
draw = 'draw',
flip = 'flip',
reset = 'reset',
rotate = 'rotate',
rect = 'rect',
circle = 'circle',
text = 'text',
upload = 'upload',
drag = 'drag',
scale = 'scale',
arrow = 'arrow',
mosaic = 'mosaic',
undo = 'undo',
redo = 'redo',
default = '',
}
```
## IDownloadBody
```
export interface IDownloadBody {
downLoadUrl?: string;
}
```
{
"name": "tiny-image-editor",
"version": "1.0.0",
"license": "MIT",
"main": "index.js",
"peerDependencies": {
"@popperjs/core": "^2.11.8",
"fabric": "^5.3.0",
"rc-slider": "^10.5.0",
"react-popper": "^2.3.0"
}
}
import React, { forwardRef, useImperativeHandle } from 'react';
/** 工具 */
import {
EMPTY_ARR,
EMPTY_STR,
IEditorProps,
MENU_TYPE_ENUM,
LANG
} from './constants';
import { EditorContext } from './util';
/** 小组件 */
import { Arrow } from './components/Arrow';
import { Circle } from './components/Circle';
import { Control } from './components/Control';
import { Crop } from './components/Crop';
import { Download } from './components/Download';
import { Drag } from './components/Drag';
import { Draw } from './components/Draw';
import { Flip } from './components/Flip';
import { History } from './components/History';
import Mosaic from './components/Mosaic';
import { Rect } from './components/Rect';
import { Reset } from './components/Reset';
import { Rotate } from './components/Rotate';
import { Scale } from './components/Scale';
import { Text } from './components/Text';
import { Upload } from './components/Upload';
import { useInit } from './components/init';
/** 图像编辑器组件 */
export const Editor = forwardRef(
(
{ url = EMPTY_STR, style, menus = EMPTY_ARR, onDownLoad,lang = LANG.zh }: IEditorProps,
ref,
) => {
const {
canvasEl,
canvasInstanceRef,
wrapperInstanceRef,
currentMenu,
setCurrentMenu,
currentMenuRef,
canvasIsRender,
initCanvasJson,
unSaveHistory,
historyRef,
initBackGroundImage,
downloadRef,
zoom,
setZoom,
} = useInit({ url });
console.log(menus)
/** 将下载方法透出去 */
useImperativeHandle(
ref,
() => ({
downloadRef: downloadRef,
}),
[],
);
return (
<EditorContext.Provider
value={{
canvasInstanceRef,
wrapperInstanceRef,
currentMenuRef,
currentMenu,
setCurrentMenu,
canvasIsRender,
canvasEl,
url,
onDownLoad,
initCanvasJson,
unSaveHistory,
historyRef,
initBackGroundImage,
zoom,
setZoom,
lang
}}
>
<div className="tie-image-editor" style={style}>
<Control />
<div className="tie-image-editor_tool clearfix">
{(!menus.length || menus.includes(MENU_TYPE_ENUM.rect)) && <Rect />}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.circle)) && (
<Circle />
)}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.arrow)) && (
<Arrow />
)}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.draw)) && <Draw />}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.text)) && <Text />}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.mosaic)) && (
<Mosaic />
)}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.crop)) && <Crop />}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.download)) && (
<Download ref={downloadRef} />
)}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.upload)) && (
<Upload />
)}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.drag)) && <Drag />}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.reset)) && (
<Reset />
)}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.rotate)) && (
<Rotate />
)}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.flip)) && <Flip />}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.scale)) && (
<Scale />
)}
{(!menus.length || menus.includes(MENU_TYPE_ENUM.history)) && (
<History ref={historyRef} />
)}
</div>
<div
ref={wrapperInstanceRef}
className="tie-image-editor_modal__wrapper"
>
<canvas ref={canvasEl} />
</div>
</div>
</EditorContext.Provider>
);
},
);
Editor.displayName = 'Editor';
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1703559162011" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1064" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 96c229.76 0 416 186.24 416 416s-186.24 416-416 416S96 741.76 96 512 282.24 96 512 96zM526.933333 298.666667h-29.866666a17.066667 17.066667 0 0 0-17.066667 17.066666v164.266667H315.733333a17.066667 17.066667 0 0 0-17.066666 17.066667v29.866666c0 9.386667 7.68 17.066667 17.066666 17.066667h164.266667v164.266667c0 9.386667 7.68 17.066667 17.066667 17.066666h29.866666a17.066667 17.066667 0 0 0 17.066667-17.066666v-164.266667h164.266667a17.066667 17.066667 0 0 0 17.066666-17.066667v-29.866666a17.066667 17.066667 0 0 0-17.066666-17.066667h-164.266667V315.733333a17.066667 17.066667 0 0 0-17.066667-17.066666z" fill="#515151" fill-opacity=".87" p-id="1065"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1695113090426" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12842" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M822.7 767.6L626.4 571.3l163.1-90.6c35.1-19.5 32-71-5.3-86.1l-551.6-223c-39.1-15.8-78 23.1-62.2 62.2l223 551.6c15.1 37.3 66.6 40.4 86.1 5.3l90.6-163.1 196.3 196.3c7.8 7.8 18 11.7 28.2 11.7s20.4-3.9 28.2-11.7c15.5-15.5 15.5-40.7-0.1-56.3z" fill="#515151" p-id="12843"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#515151" d="M512 960c-247.039484 0-448-200.960516-448-448S264.960516 64 512 64 960 264.960516 960 512 759.039484 960 512 960zM512 128c-211.744443 0-384 172.255557-384 384s172.255557 384 384 384 384-172.255557 384-384S723.744443 128 512 128z" /></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1703837617436" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1516" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M191.996412 319.999467m59.736381-59.736381l8.527708-8.527707q59.736381-59.736381 119.472761 0l392.529117 392.529116q59.736381 59.736381 0 119.472762l-8.527708 8.527708q-59.736381 59.736381-119.472762 0l-392.529116-392.529117q-59.736381-59.736381 0-119.472762Z" fill="#515151" p-id="1517"></path><path d="M191.996412 704.000876m59.736381-59.736381l392.529116-392.529116q59.736381-59.736381 119.472762 0l8.527708 8.527707q59.736381 59.736381 0 119.472762l-392.529117 392.529117q-59.736381 59.736381-119.472761 0l-8.527708-8.527708q-59.736381-59.736381 0-119.472762Z" fill="#515151" p-id="1518"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#515151" d="M884.731 698.366H772.909V288.365c0-20.584-16.685-37.275-37.269-37.275H325.639V139.274c0-20.582-16.693-37.273-37.275-37.273-20.582 0-37.275 16.691-37.275 37.273V251.09H139.275c-20.584 0-37.275 16.691-37.275 37.275 0 20.582 16.691 37.273 37.275 37.273H251.09v410.003c0 20.582 16.693 37.269 37.275 37.269h410.003v111.82c0 20.583 16.685 37.269 37.267 37.269 20.589 0 37.275-16.685 37.275-37.269V772.91h111.822c20.582 0 37.269-16.687 37.269-37.269-0.001-20.588-16.688-37.275-37.27-37.275z m-559.092 0V325.638h372.728v372.728H325.639z m0 0" /></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1703837610361" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1310" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M341.09 469.09l72.78 72.78a48.27 48.27 0 0 0 68.26 0L743.4 280.6a48.26 48.26 0 0 1 55.71-9l37 18.5a48.26 48.26 0 0 1 12.54 77.29L478.54 737.46a48.27 48.27 0 0 1-64.27 3.54l-217-173.6c-29.86-23.9-21.4-71.38 14.89-83.47l79.54-26.52a48.26 48.26 0 0 1 49.39 11.68z" fill="#515151" p-id="1311"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694850868080" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6322" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M170.666667 768m42.666666 0l597.333334 0q42.666667 0 42.666666 42.666667l0 0q0 42.666667-42.666666 42.666666l-597.333334 0q-42.666667 0-42.666666-42.666666l0 0q0-42.666667 42.666666-42.666667Z" fill="#515151" p-id="6323"></path><path d="M170.666667 853.333333m0-42.666666l0-85.333334q0-42.666667 42.666666-42.666666l0 0q42.666667 0 42.666667 42.666666l0 85.333334q0 42.666667-42.666667 42.666666l0 0q-42.666667 0-42.666666-42.666666Z" fill="#515151" p-id="6324"></path><path d="M768 853.333333m0-42.666666l0-85.333334q0-42.666667 42.666667-42.666666l0 0q42.666667 0 42.666666 42.666666l0 85.333334q0 42.666667-42.666666 42.666666l0 0q-42.666667 0-42.666667-42.666666Z" fill="#515151" p-id="6325"></path><path d="M512 640a42.666667 42.666667 0 0 1-24.746667-7.68l-170.666666-120.32a42.666667 42.666667 0 0 1-10.24-59.306667 42.666667 42.666667 0 0 1 59.733333-10.24L512 544.426667l145.066667-109.226667a42.666667 42.666667 0 0 1 51.2 68.266667l-170.666667 128a42.666667 42.666667 0 0 1-25.6 8.533333z" fill="#515151" p-id="6326"></path><path d="M512 554.666667a42.666667 42.666667 0 0 1-42.666667-42.666667V170.666667a42.666667 42.666667 0 0 1 85.333334 0v341.333333a42.666667 42.666667 0 0 1-42.666667 42.666667z" fill="#515151" p-id="6327"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694850318596" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16905" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M819.2 102.4h-56.53504c-18.76992 0-45.0048 10.87488-58.2656 24.13568l-51.2 51.2L629.07392 153.6c-26.5728-26.53184-70.00064-26.53184-96.57344 0L266.99776 419.10272a34.16064 34.16064 0 0 0 0 48.27136l12.07296 12.07296 289.62816-289.6384 36.22912 36.23936-362.02496 362.02496-0.03072-0.03072-92.17024 92.23168c-26.5728 26.5216-48.30208 78.97088-48.27136 116.50048L102.4 921.6h124.8256c37.49888 0 89.96864-21.72928 116.50048-48.26112l92.24192-92.20096 461.49632-461.49632C910.72512 306.37056 921.6 280.13568 921.6 261.36576V204.8L819.2 102.4zM295.46496 825.06752c-13.74208 13.73184-48.80384 28.30336-68.23936 28.30336l-56.53504-0.03072V796.7744c-0.03072-19.42528 14.49984-54.46656 28.27264-68.22912l43.9296-43.93984 96.53248 96.54272-43.96032 43.91936z m92.23168-92.20096l-96.53248-96.53248 362.02496-362.0352 96.57344 96.57344-362.06592 361.99424z m410.33728-410.2656l-96.5632-96.60416 72.3968-72.3968 96.60416 96.60416-72.43776 72.3968z" p-id="16906" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702545991424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4272" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M640 896h85.333333v-85.333333h-85.333333v85.333333z m170.666667-512h85.333333v-85.333333h-85.333333v85.333333zM128 213.333333v597.333334c0 47.146667 38.186667 85.333333 85.333333 85.333333h170.666667v-85.333333h-170.666667V213.333333h170.666667V128h-170.666667c-47.146667 0-85.333333 38.186667-85.333333 85.333333z m682.666667-85.333333v85.333333h85.333333c0-47.146667-38.186667-85.333333-85.333333-85.333333zM469.333333 981.333333h85.333334V42.666667h-85.333334v938.666666z m341.333334-256h85.333333v-85.333333h-85.333333v85.333333z m-170.666667-512h85.333333V128h-85.333333v85.333333z m170.666667 341.333334h85.333333v-85.333334h-85.333333v85.333334z m0 341.333333c47.146667 0 85.333333-38.186667 85.333333-85.333333h-85.333333v85.333333z" p-id="4273" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694849855144" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9603" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M210.773333 522.24a42.666667 42.666667 0 0 0-51.626666 32.426667l-20.906667 82.773333A42.666667 42.666667 0 0 0 170.666667 687.786667a42.666667 42.666667 0 0 0 51.626666-31.146667l20.906667-82.773333a42.666667 42.666667 0 0 0-32.426667-51.626667z m725.333334 320.853333l-170.666667-682.666666A42.666667 42.666667 0 0 0 725.333333 128h-128a42.666667 42.666667 0 0 0-42.666666 42.666667v682.666666a42.666667 42.666667 0 0 0 42.666666 42.666667h298.666667a42.666667 42.666667 0 0 0 33.706667-16.213333 42.666667 42.666667 0 0 0 8.96-36.693334zM640 810.666667V213.333333h52.053333l149.333334 597.333334zM273.066667 273.92a42.666667 42.666667 0 0 0-52.053334 31.146667L200.533333 387.84a42.666667 42.666667 0 0 0 31.146667 51.626667h10.24a42.666667 42.666667 0 0 0 42.666667-32.426667l20.48-82.773333a42.666667 42.666667 0 0 0-32-50.346667zM320.426667 213.333333h85.333333a42.666667 42.666667 0 0 0 0-85.333333h-85.333333a42.666667 42.666667 0 0 0 0 85.333333zM180.906667 810.666667a42.666667 42.666667 0 0 0-85.333334-10.24l-10.24 42.666666a42.666667 42.666667 0 0 0 7.68 36.693334A42.666667 42.666667 0 0 0 128 896h42.666667a42.666667 42.666667 0 0 0 10.24-85.333333zM426.666667 277.76a42.666667 42.666667 0 0 0-42.666667 42.666667v85.333333a42.666667 42.666667 0 0 0 85.333333 0v-85.333333a42.666667 42.666667 0 0 0-42.666666-42.666667z m0 256a42.666667 42.666667 0 0 0-42.666667 42.666667v85.333333a42.666667 42.666667 0 0 0 85.333333 0v-85.333333a42.666667 42.666667 0 0 0-42.666666-42.666667z m0 256a42.666667 42.666667 0 0 0-36.693334 20.906667H341.333333a42.666667 42.666667 0 0 0 0 85.333333h85.333334a42.666667 42.666667 0 0 0 42.666666-42.666667v-20.906666a42.666667 42.666667 0 0 0-42.666666-42.666667z" p-id="9604" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694849870986" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9778" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M563.626667 243.2a42.666667 42.666667 0 0 0 10.24 0l82.773333-20.906667A42.666667 42.666667 0 0 0 687.786667 170.666667a42.666667 42.666667 0 0 0-51.626667-31.146667L554.666667 159.146667a42.666667 42.666667 0 0 0 10.24 85.333333zM405.76 384h-85.333333a42.666667 42.666667 0 1 0 0 85.333333h85.333333a42.666667 42.666667 0 0 0 0-85.333333z m170.666667 0a42.666667 42.666667 0 1 0 0 85.333333h85.333333a42.666667 42.666667 0 0 0 0-85.333333zM315.306667 305.066667h10.24l82.773333-20.48a42.666667 42.666667 0 0 0-20.48-84.053334l-82.773333 20.48a42.666667 42.666667 0 0 0 10.24 85.333334zM170.666667 448.426667a42.666667 42.666667 0 0 0 42.666666-42.666667v-85.333333a42.666667 42.666667 0 0 0-85.333333 0v85.333333a42.666667 42.666667 0 0 0 42.666667 42.666667z m709.12-354.133334a42.666667 42.666667 0 0 0-36.693334-8.96l-42.666666 10.24a42.666667 42.666667 0 0 0-31.146667 51.626667 42.666667 42.666667 0 0 0 42.666667 32.426667A42.666667 42.666667 0 0 0 896 170.666667V128a42.666667 42.666667 0 0 0-16.213333-33.706667zM853.333333 298.666667a42.666667 42.666667 0 0 0-42.666666 42.666666v48.64a42.666667 42.666667 0 0 0 21.76 79.36H853.333333a42.666667 42.666667 0 0 0 42.666667-42.666666V341.333333a42.666667 42.666667 0 0 0-42.666667-42.666666z m0 256H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666666v128a42.666667 42.666667 0 0 0 32.426667 42.666667l682.666666 170.666667a42.666667 42.666667 0 0 0 10.24 0 42.666667 42.666667 0 0 0 42.666667-42.666667v-298.666667a42.666667 42.666667 0 0 0-42.666667-42.666666z m-42.666666 286.72l-597.333334-149.333334V640h597.333334z" p-id="9779" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694854421840" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28174" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M870.4 204.8c-18.6368 0-36.1472 5.0176-51.2 13.7728l0-64.9728c0-56.4736-45.9264-102.4-102.4-102.4-21.0944 0-40.6528 6.4-56.9856 17.3568-14.0288-39.8848-52.0192-68.5568-96.6144-68.5568s-82.6368 28.672-96.6144 68.5568c-16.2816-10.9568-35.8912-17.3568-56.9856-17.3568-56.4736 0-102.4 45.9264-102.4 102.4l0 377.4976-68.9152-119.4496c-13.3632-24.32-35.1744-41.6256-61.3888-48.7936-25.5488-6.9632-52.1216-3.2768-74.8544 10.3424-46.4384 27.8528-64.1536 90.8288-39.424 140.3904 1.536 3.1232 34.2016 70.0416 136.192 273.92 48.0256 96 100.7104 164.6592 156.6208 203.9808 43.8784 30.8736 74.1888 32.4608 79.8208 32.4608l256 0c43.5712 0 84.0704-14.1824 120.4224-42.0864 34.1504-26.2656 63.7952-64.256 88.064-112.8448 47.8208-95.6416 73.1136-227.9424 73.1136-382.6688l0-179.2c0-56.4736-45.9264-102.4-102.4-102.4zM921.6 486.4c0 146.7904-23.3984 271.1552-67.6864 359.7312-28.8768 57.7536-80.5888 126.6688-162.7136 126.6688l-255.488 0c-1.9968-0.1536-23.552-2.56-56.064-26.88-32.4096-24.2688-82.176-75.3664-135.0656-181.248-103.7824-207.5648-135.68-272.9472-135.9872-273.5616-0.0512-0.1024-0.0512-0.1536-0.1024-0.2048-12.8512-25.7536-3.7376-59.4944 19.9168-73.6768 10.6496-6.4 23.0912-8.0896 35.072-4.864 12.7488 3.4816 23.4496 12.0832 30.0544 24.1664 0.1024 0.1536 0.2048 0.3584 0.3072 0.512l79.9232 138.496c16.3328 29.8496 34.7136 42.3936 54.6304 37.3248 19.968-5.0688 30.0544-25.0368 30.0544-59.2384l0-400.0256c0-28.2112 22.9888-51.2 51.2-51.2s51.2 22.9888 51.2 51.2l0 332.8c0 14.1312 11.4688 25.6 25.6 25.6s25.6-11.4688 25.6-25.6l0-384c0-28.2112 22.9888-51.2 51.2-51.2s51.2 22.9888 51.2 51.2l0 384c0 14.1312 11.4688 25.6 25.6 25.6s25.6-11.4688 25.6-25.6l0-332.8c0-28.2112 22.9888-51.2 51.2-51.2s51.2 22.9888 51.2 51.2l0 384c0 14.1312 11.4688 25.6 25.6 25.6s25.6-11.4688 25.6-25.6l0-230.4c0-28.2112 22.9888-51.2 51.2-51.2s51.2 22.9888 51.2 51.2l0 179.2z" fill="#515151" p-id="28175"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702973104380" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4243" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M533.333333 85.333333C384 85.333333 251.733333 166.4 183.466667 290.133333L85.333333 192 85.333333 469.333333l277.333333 0L243.2 349.866667C298.666667 243.2 405.333333 170.666667 533.333333 170.666667c174.933333 0 320 145.066667 320 320 0 174.933333-145.066667 320-320 320-140.8 0-256-89.6-302.933333-213.333333L140.8 597.333333c46.933333 170.666667 204.8 298.666667 392.533333 298.666667 226.133333 0 405.333333-183.466667 405.333333-405.333333S755.2 85.333333 533.333333 85.333333zM469.333333 298.666667l0 217.6 200.533333 119.466667 34.133333-55.466667-170.666667-102.4L533.333333 298.666667 469.333333 298.666667z" opacity="0.9" p-id="4244" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#515151" d="M512 212h300v300H512zM212 512h300v300H212z" /><path fill="#515151" d="M812 992H212a180 180 0 0 1-180-180V212a180 180 0 0 1 180-180h600a180 180 0 0 1 180 180v600a180 180 0 0 1-180 180zM212 152a60 60 0 0 0-60 60v600a60 60 0 0 0 60 60h600a60 60 0 0 0 60-60V212a60 60 0 0 0-60-60z" /></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702438231396" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2364" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M85.344 106.656H256v170.656H85.344V106.656zM85.344 448H256v170.656H85.344V448zM256 277.344h170.656V448H256V277.344zM426.656 106.656h170.656v170.656h-170.656V106.656zM426.656 448h170.656v170.656h-170.656V448zM597.344 277.344H768V448h-170.656V277.344zM597.344 768h-170.656v-149.344H256.032V768H85.376v170.656h170.656v-149.344h170.656v149.344h170.656v-149.344H768v149.344h170.656V768H768v-149.344h-170.656zM768 106.656h170.656v170.656H768V106.656zM768 448h170.656v170.656H768V448z" p-id="2365" fill="#515151" data-spm-anchor-id="a313x.search_index.0.i1.70153a81pH8VDw" class=""></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1701141069715" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5133" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M841.34 959.36H182.66c-65.06 0-117.99-52.94-117.99-118.02V182.69c0-65.08 52.94-118.04 117.99-118.04h658.68c65.06 0 117.99 52.96 117.99 118.04v658.65c0 65.08-52.93 118.02-117.99 118.02zM182.66 142.17c-22.31 0-40.51 18.18-40.51 40.51v658.65c0 22.34 18.2 40.49 40.51 40.49h658.68c22.31 0 40.51-18.15 40.51-40.49V182.69c0-22.34-18.2-40.51-40.51-40.51H182.66z" fill="#515151" p-id="5134"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694850987960" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2591" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M765.013333 509.013333C487.253333 235.392 67.968 391.466667 65.792 697.941333l7.765333 2.645334c176.853333-218.112 417.024-291.84 605.866667-106.453334 1.706667 1.706667 1.792 4.608 0.085333 6.314667L561.92 718.08a4.266667 4.266667 0 0 0 2.986667 7.296h326.826666a4.266667 4.266667 0 0 0 4.266667-4.266667V394.24a4.224 4.224 0 0 0-7.253333-2.986667l-117.76 117.76a4.096 4.096 0 0 1-5.888 0z" p-id="2592" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702708510489" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5331" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M831.167365 607.851819c-15.128556-6.096582-32.063506 1.354796-38.160088 16.483352-46.288864 115.609261-156.478942 190.348842-281.120177 190.348842-104.770893 0-199.606615-52.837045-254.475854-138.640794l193.735832 39.514885c15.805954 3.161191 31.386108-6.999779 34.547299-22.805734 3.161191-15.805954-6.999779-31.386108-22.805733-34.547298l-252.895259-51.933848h-0.903198c-0.677398-0.225799-1.580595-0.225799-2.257993-0.2258H202.993605c-0.677398 0-1.354796 0-2.032194 0.2258-0.451599 0-0.677398 0-1.128996 0.225799-0.677398 0-1.128997 0.225799-1.806395 0.225799-0.451599 0-0.903197 0.225799-1.128997 0.2258-0.451599 0.225799-1.128997 0.225799-1.580595 0.451599-0.451599 0.225799-0.903197 0.225799-1.354796 0.451598-0.225799 0.225799-0.677398 0.225799-0.903198 0.2258-0.225799 0-0.225799 0.225799-0.451598 0.225799l-1.354796 0.677398-1.354796 0.677398c-0.451599 0.225799-0.677398 0.451599-1.128997 0.677398-0.451599 0.225799-1.128997 0.677398-1.580595 0.903197-0.225799 0-0.225799 0.225799-0.451599 0.2258-0.225799 0.225799-0.451599 0.225799-0.451599 0.451598-0.451599 0.451599-0.903197 0.677398-1.580595 1.128997-0.225799 0.225799-0.451599 0.451599-0.903197 0.677398l-1.354797 1.354796c-0.225799 0.225799-0.451599 0.451599-0.677398 0.903197-0.451599 0.451599-0.677398 0.903197-1.128996 1.354796-0.225799 0.225799-0.451599 0.677398-0.677398 0.903198-0.225799 0.451599-0.677398 0.903197-0.903198 1.354796-0.225799 0.451599-0.451599 0.677398-0.677398 1.128996l-0.677398 1.354797c-0.225799 0.451599-0.451599 0.677398-0.451598 1.128996-0.225799 0.451599-0.451599 0.903197-0.677398 1.580596-0.225799 0.451599-0.225799 0.677398-0.451599 1.128996-0.225799 0.677398-0.451599 1.128997-0.451599 1.806395 0 0.451599-0.225799 0.677398-0.225799 1.128997-0.225799 0.677398-0.225799 1.354796-0.451599 2.257993v1.128997L141.350386 887.842999c-2.032194 16.031753 9.257773 30.70871 25.289526 32.966703 1.354796 0.225799 2.483793 0.225799 3.838589 0.2258 14.451158 0 27.095921-10.838368 29.128114-25.515326l21.676737-168.672105c23.483131 31.837707 52.385447 59.836825 85.577949 82.868357 60.514223 41.772878 131.189416 63.675413 205.025799 63.675413s144.511577-22.128335 205.0258-63.675413c58.933627-40.643881 104.093495-97.319515 130.737817-163.704521 5.870783-15.128556-1.354796-32.289305-16.483352-38.160088zM857.134289 103.190298c-16.031753-2.032194-30.70871 9.257773-32.966704 25.289526l-21.676736 168.672105c-23.483131-31.837707-52.385447-59.836825-85.577949-82.642557-60.514223-41.772878-131.189416-63.675413-205.0258-63.675414-73.610584 0-144.511577 22.128335-205.025799 63.675414-58.933627 40.643881-104.093495 97.319515-130.737817 163.70452-6.096582 15.128556 1.354796 32.063506 16.483352 38.160088 15.128556 6.096582 32.063506-1.354796 38.160088-16.483352 46.288864-115.609261 156.478942-190.348842 281.120176-190.348842 104.770893 0 199.606615 52.837045 254.475855 138.640794L572.627122 308.441896c-15.805954-3.161191-31.386108 6.999779-34.547298 22.805734-3.161191 15.805954 6.999779 31.386108 22.805733 34.547298l253.572657 51.708049h0.451599c0.677398 0.225799 1.580595 0.225799 2.257993 0.451599h0.451599c0.903197 0 1.806395 0.225799 2.709592 0.225799h2.257993c0.451599 0 0.903197 0 1.128997-0.225799 0.451599 0 0.903197-0.225799 1.354796-0.2258 0.451599 0 0.677398-0.225799 1.128996-0.225799s0.903197-0.225799 1.580596-0.451599c0.225799 0 0.677398-0.225799 0.903197-0.225799 0.677398-0.225799 1.354796-0.451599 1.806395-0.677398 0.225799 0 0.225799 0 0.451599-0.2258 0.903197-0.225799 1.580595-0.677398 2.483792-1.128996 0.225799-0.225799 0.451599-0.225799 0.903198-0.451599s0.903197-0.451599 1.354796-0.903197c0.225799-0.225799 0.677398-0.451599 0.903197-0.677398 0.225799 0 0.225799-0.225799 0.451599-0.2258l0.451598-0.451598c0.451599-0.451599 1.128997-0.677398 1.580596-1.128997l0.677398-0.677398 1.354796-1.354796c0.225799-0.225799 0.451599-0.451599 0.677398-0.903197 0.451599-0.451599 0.677398-0.903197 1.128997-1.354796 0.225799-0.225799 0.451599-0.677398 0.677398-0.903198 0.225799-0.451599 0.677398-0.903197 0.903197-1.354796 0.225799-0.451599 0.451599-0.677398 0.677398-1.128996l0.677398-1.354797c0.225799-0.451599 0.451599-0.903197 0.677398-1.128996l0.677398-1.354796c0.225799-0.451599 0.225799-0.903197 0.451599-1.354796 0.225799-0.451599 0.225799-1.128997 0.451598-1.580596 0-0.451599 0.225799-0.677398 0.2258-1.128996 0.225799-0.677398 0.225799-1.354796 0.451598-2.032194 0-0.225799 0-0.451599 0.2258-0.677398v-0.2258l32.966703-256.282249c2.709592-16.257552-8.806174-30.934509-24.837927-32.966703z" fill="#515151" p-id="5332" data-spm-anchor-id="a313x.search_index.0.i5.79943a813nilqH" class="selected"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694850121784" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10964" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-0.4-12.6 6.1l-0.2 64c-118.6 0.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-0.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z" p-id="10965" fill="#515151"></path><path d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32z m-44 402H396V494h440v326z" p-id="10966" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1703559147194" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5088" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 96c229.76 0 416 186.24 416 416s-186.24 416-416 416S96 741.76 96 512 282.24 96 512 96z m196.266667 384H315.733333a17.066667 17.066667 0 0 0-17.066666 17.066667v29.866666c0 9.386667 7.68 17.066667 17.066666 17.066667h392.533334a17.066667 17.066667 0 0 0 17.066666-17.066667v-29.866666a17.066667 17.066667 0 0 0-17.066666-17.066667z" fill="#515151" fill-opacity=".87" p-id="5089"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694849832460" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8562" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.333333 170.666667H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666666v128a42.666667 42.666667 0 0 0 85.333333 0V256h256v554.666667H384a42.666667 42.666667 0 0 0 0 85.333333h256a42.666667 42.666667 0 0 0 0-85.333333h-85.333333V256h256v85.333333a42.666667 42.666667 0 0 0 85.333333 0V213.333333a42.666667 42.666667 0 0 0-42.666667-42.666666z" p-id="8563" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#515151" d="M799.5 179c24.6 0 44.5 20 44.5 44.5v575.9c0 24.6-20 44.5-44.5 44.5h-576c-24.6 0-44.5-20-44.5-44.5V223.5c0-24.6 20-44.5 44.5-44.5h576m0-120h-576C132.7 59 59 132.7 59 223.5v575.9C59 890.3 132.7 964 223.5 964h575.9c90.9 0 164.5-73.7 164.5-164.5v-576C964 132.7 890.3 59 799.5 59z" /></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694850976834" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7598" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M258.986667 509.013333c277.76-273.621333 697.045333-117.546667 699.306666 188.885334l-7.765333 2.645333c-176.853333-218.112-417.024-291.84-605.866667-106.453333a4.522667 4.522667 0 0 0-0.085333 6.314666l117.589333 117.589334a4.266667 4.266667 0 0 1-2.986666 7.253333H132.266667a4.266667 4.266667 0 0 1-4.266667-4.266667V394.24c0-3.84 4.608-5.717333 7.253333-2.986667l117.76 117.76c1.621333 1.706667 4.266667 1.621333 5.888 0z" p-id="7599" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694850896169" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6500" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M853.333333 256m-42.666666 0l-597.333334 0q-42.666667 0-42.666666-42.666667l0 0q0-42.666667 42.666666-42.666666l597.333334 0q42.666667 0 42.666666 42.666666l0 0q0 42.666667-42.666666 42.666667Z" fill="#515151" p-id="6501"></path><path d="M853.333333 170.666667m0 42.666666l0 85.333334q0 42.666667-42.666666 42.666666l0 0q-42.666667 0-42.666667-42.666666l0-85.333334q0-42.666667 42.666667-42.666666l0 0q42.666667 0 42.666666 42.666666Z" fill="#515151" p-id="6502"></path><path d="M256 170.666667m0 42.666666l0 85.333334q0 42.666667-42.666667 42.666666l0 0q-42.666667 0-42.666666-42.666666l0-85.333334q0-42.666667 42.666666-42.666666l0 0q42.666667 0 42.666667 42.666666Z" fill="#515151" p-id="6503"></path><path d="M341.333333 597.333333a42.666667 42.666667 0 0 1-34.133333-17.066666 42.666667 42.666667 0 0 1 8.533333-59.733334l170.666667-128a42.666667 42.666667 0 0 1 50.346667 0l170.666666 120.32a42.666667 42.666667 0 0 1 10.24 59.306667 42.666667 42.666667 0 0 1-59.733333 10.24L512 479.573333 366.933333 588.8a42.666667 42.666667 0 0 1-25.6 8.533333z" fill="#515151" p-id="6504"></path><path d="M512 896a42.666667 42.666667 0 0 1-42.666667-42.666667v-341.333333a42.666667 42.666667 0 0 1 85.333334 0v341.333333a42.666667 42.666667 0 0 1-42.666667 42.666667z" fill="#515151" p-id="6505"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1695106630007" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6574" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z" p-id="6575" fill="#515151"></path><path d="M921 867L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z" p-id="6576" fill="#515151"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1695106635608" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6749" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z" p-id="6750" fill="#515151"></path><path d="M921 867L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z" p-id="6751" fill="#515151"></path></svg>
\ No newline at end of file
/* eslint-disable @typescript-eslint/no-unused-expressions */
import classNames from 'classnames';
import React, { useContext, useEffect, useRef } from 'react';
import {
ACTION,
CURSOR,
IPaintTypes,
LANG,
MENU_TYPE_ENUM,
MENU_TYPE_TEXT,
paintConfig,
} from '../constants';
import { EditorContext } from '../util';
import Paint from './setting/Paint';
import Popover from './setting/Popover';
import ArrowClass from '../helpers/Arrow';
export const useArrow = () => {
const {
currentMenu,
setCurrentMenu,
canvasInstanceRef,
canvasIsRender,
currentMenuRef,
historyRef,
} = useContext(EditorContext);
const isDrawingLine = useRef(false);
const pointer = useRef({ x: 0, y: 0 });
const pointerPoints = useRef<number[]>([]);
const lineToDraw = useRef(null);
const paintConfigValue = useRef({
color: paintConfig.colors[0],
size: paintConfig.sizes[0],
});
const initArrow = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
canvas.on('mouse:down', (o: any) => {
if (
canvas.getActiveObject() ||
currentMenuRef.current !== MENU_TYPE_ENUM.arrow
) {
return;
}
canvas.discardActiveObject();
canvas.getObjects().forEach((obj: any) => {
obj.selectable = false;
obj.hasControls = false;
});
canvas.requestRenderAll();
isDrawingLine.current = true;
pointer.current = canvas.getPointer(o.e);
pointerPoints.current = [
pointer.current.x,
pointer.current.y,
pointer.current.x,
pointer.current.y,
];
const lineToDrawNew = new ArrowClass(pointerPoints.current, {
strokeWidth: paintConfigValue.current.size,
stroke: paintConfigValue.current.color,
id: new Date().valueOf() + '_' + Math.random() * 4,
});
// lineToDrawNew.selectable = false;
// lineToDrawNew.evented = false;
lineToDrawNew.strokeUniform = true;
lineToDraw.current = lineToDrawNew;
canvas.add(lineToDrawNew);
});
canvas.on('mouse:move', (o: any) => {
if (
!canvas.getActiveObject() &&
currentMenuRef.current === MENU_TYPE_ENUM.rect
) {
canvas.setCursor(CURSOR.crosshair);
}
if (
canvas.getActiveObject() ||
currentMenuRef.current !== MENU_TYPE_ENUM.arrow ||
!isDrawingLine.current ||
!lineToDraw.current
) {
canvas.renderAll();
return;
}
pointer.current = canvas.getPointer(o.e);
if (o.e.shiftKey) {
// calc angle
const startX = pointerPoints.current[0];
const startY = pointerPoints.current[1];
const x2 = pointer.current.x - startX;
const y2 = pointer.current.y - startY;
const r = Math.sqrt(x2 * x2 + y2 * y2);
let angle = (Math.atan2(y2, x2) / Math.PI) * 180;
angle = parseInt('' + ((angle + 7.5) % 360) / 15) * 15;
const cosx = r * Math.cos((angle * Math.PI) / 180);
const sinx = r * Math.sin((angle * Math.PI) / 180);
(lineToDraw.current as any).set({
x2: cosx + startX,
y2: sinx + startY,
});
} else {
if (!lineToDraw.current) {
return;
}
(lineToDraw.current as any).set({
x2: pointer.current.x,
y2: pointer.current.y,
});
}
canvas.renderAll();
});
canvas.on('mouse:up', () => {
if (
currentMenuRef.current !== MENU_TYPE_ENUM.arrow ||
!lineToDraw.current ||
!isDrawingLine.current
)
return;
(lineToDraw.current as any).setCoords();
isDrawingLine.current = false;
// canvas.discardActiveObject();
if (
pointer.current.x === pointerPoints.current[0] &&
pointer.current.y === pointerPoints.current[1]
) {
/** 无效矩形 */
canvas.remove(lineToDraw.current);
canvas.requestRenderAll();
historyRef.current.updateCanvasState(ACTION.add, true, false);
} else {
/** 有效矩形 */
canvas.setActiveObject(lineToDraw.current);
historyRef.current.updateCanvasState(ACTION.add, true, true);
}
});
};
useEffect(() => {
if (canvasInstanceRef.current && canvasIsRender) {
initArrow();
}
}, [canvasInstanceRef, canvasIsRender]);
const handleArrowTrigger = () => {
setCurrentMenu(
currentMenu === MENU_TYPE_ENUM.arrow
? MENU_TYPE_ENUM.default
: MENU_TYPE_ENUM.arrow,
);
};
const handlePaintChange = (type: IPaintTypes, value: number | string) => {
if (!canvasInstanceRef.current) {
return;
}
const canvas = canvasInstanceRef.current;
const activeObject = canvas.getActiveObjects()[0];
if (type === IPaintTypes.color) {
paintConfigValue.current.color = value as string;
activeObject && activeObject.set('stroke', value);
} else {
paintConfigValue.current.size = value as number;
activeObject && activeObject.set('strokeWidth', value);
}
canvas.renderAll();
};
return { handleArrowTrigger, handlePaintChange, currentMenu };
};
/** 箭头 */
export const Arrow = () => {
const { lang = LANG.en } = useContext(EditorContext);
const { handleArrowTrigger, handlePaintChange, currentMenu } = useArrow();
return (
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-arrow',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.arrow,
},
)}
>
<Paint
open={currentMenu === MENU_TYPE_ENUM.arrow}
onChange={handlePaintChange}
>
<Popover content={MENU_TYPE_TEXT.arrow[lang]} placement="top">
<i
className={classNames('tie-image-editor_icon')}
onClick={handleArrowTrigger}
/>
</Popover>
</Paint>
</div>
);
};
/* eslint-disable @typescript-eslint/no-unused-expressions */
import classNames from 'classnames';
import { fabric } from 'fabric';
import React, { useContext, useEffect, useRef } from 'react';
import {
ACTION,
CURSOR,
IPaintTypes,
LANG,
MENU_TYPE_ENUM,
MENU_TYPE_TEXT,
paintConfig,
} from '../constants';
import { EditorContext } from '../util';
import Paint from './setting/Paint';
import Popover from './setting/Popover';
export const useCircle = () => {
const {
canvasInstanceRef,
currentMenuRef,
setCurrentMenu,
canvasIsRender,
currentMenu,
historyRef,
} = useContext(EditorContext);
const startCircle = useRef(false);
const circlePoint = useRef({ x: 9, y: 0 });
const currentCircle = useRef<any>(null);
const paintConfigValue = useRef({
color: paintConfig.colors[0],
size: paintConfig.sizes[0],
});
const initCircle = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
canvas.on('mouse:down', function (o: any) {
if (
canvas.getActiveObject() ||
currentMenuRef.current !== MENU_TYPE_ENUM.circle
) {
return;
}
startCircle.current = true;
let pointer = canvas.getPointer(o.e);
circlePoint.current.x = pointer.x;
circlePoint.current.y = pointer.y;
currentCircle.current = new fabric.Circle({
left: circlePoint.current.x,
top: circlePoint.current.y,
originX: 'left',
originY: 'top',
radius: pointer.x - circlePoint.current.x,
angle: 0,
fill: '',
stroke: paintConfigValue.current.color,
strokeWidth: paintConfigValue.current.size,
});
canvas.add(currentCircle.current);
});
canvas.on('mouse:move', function (o: any) {
if (
!canvas.getActiveObject() &&
currentMenuRef.current === MENU_TYPE_ENUM.circle
) {
canvas.setCursor(CURSOR.crosshair);
}
if (
!startCircle.current ||
currentMenuRef.current !== MENU_TYPE_ENUM.circle
) {
canvas.renderAll();
return;
}
let pointer = canvas.getPointer(o.e);
let radius =
Math.max(
Math.abs(circlePoint.current.y - pointer.y),
Math.abs(circlePoint.current.x - pointer.x),
) / 2;
if (radius > currentCircle.current.strokeWidth) {
radius -= currentCircle.current.strokeWidth / 2;
}
currentCircle.current.set({ radius: radius });
if (circlePoint.current.x > pointer.x) {
currentCircle.current.set({ originX: 'right' });
} else {
currentCircle.current.set({ originX: 'left' });
}
if (circlePoint.current.y > pointer.y) {
currentCircle.current.set({ originY: 'bottom' });
} else {
currentCircle.current.set({ originY: 'top' });
}
canvas.renderAll();
});
canvas.on('mouse:up', function (o: any) {
if (
!currentCircle.current ||
currentMenuRef.current !== MENU_TYPE_ENUM.circle
) {
return;
}
let pointer = canvas.getPointer(o.e);
if (
pointer.x === currentCircle.current.left &&
pointer.y === currentCircle.current.top
) {
/** 无效 */
canvas.remove(currentCircle.current);
canvas.requestRenderAll();
historyRef.current.updateCanvasState(ACTION.add, true, false);
} else {
/** 有效 */
canvas.setActiveObject(currentCircle.current);
historyRef.current.updateCanvasState(ACTION.add, true, true);
}
startCircle.current = false;
currentCircle.current = null;
});
};
useEffect(() => {
const canvas = canvasInstanceRef.current;
if (canvas && canvasIsRender) {
initCircle();
}
}, [canvasInstanceRef, canvasIsRender]);
const handleDrawCircle = () => {
setCurrentMenu(
currentMenuRef.current === MENU_TYPE_ENUM.circle
? MENU_TYPE_ENUM.default
: MENU_TYPE_ENUM.circle,
);
};
const handlePaintChange = (type: IPaintTypes, value: number | string) => {
if (!canvasInstanceRef.current) {
return;
}
const canvas = canvasInstanceRef.current;
const activeObject = canvas.getActiveObjects()[0];
if (type === IPaintTypes.color) {
paintConfigValue.current.color = value as string;
activeObject && activeObject.set('stroke', value);
} else {
paintConfigValue.current.size = value as number;
activeObject && activeObject.set('strokeWidth', value);
}
canvas.renderAll();
};
return { handleDrawCircle, handlePaintChange, currentMenu };
};
/** 圆形 */
export const Circle = () => {
const { lang = LANG.en } = useContext(EditorContext);
const { handleDrawCircle, handlePaintChange, currentMenu } = useCircle();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-circle',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.circle,
},
)}
>
<Paint
open={currentMenu === MENU_TYPE_ENUM.circle}
onChange={handlePaintChange}
>
<Popover content={MENU_TYPE_TEXT.circle[lang]} placement="top">
<i
className={classNames('tie-image-editor_icon')}
onClick={handleDrawCircle}
/>
</Popover>
</Paint>
</div>
</>
);
};
import { fabric } from 'fabric';
import React, { useContext, useEffect } from 'react';
import { ACTION } from '../constants';
import { EditorContext } from '../util';
const deleteIcon =
"data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E";
export const useControl = () => {
const { canvasInstanceRef, canvasIsRender, historyRef } =
useContext(EditorContext);
/** 初始化控制器 */
const initControl = () => {
let img = document.createElement('img');
img.src = deleteIcon;
fabric.Object.prototype.transparentCorners = false;
fabric.Object.prototype.cornerColor = 'blue';
fabric.Object.prototype.cornerStyle = 'circle';
function deleteObject(eventData: any, transform: any) {
let target = transform.target;
let canvas = target.canvas;
canvas.remove(target);
canvas.requestRenderAll();
historyRef.current.updateCanvasState(ACTION.delete);
return true;
}
function renderIcon(
ctx: any,
left: any,
top: any,
styleOverride: any,
fabricObject: any,
) {
let size = 24;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(img, -size / 2, -size / 2, size, size);
ctx.restore();
}
fabric.Object.prototype.controls.deleteControl = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: 16,
cursorStyle: 'pointer',
mouseUpHandler: deleteObject,
render: renderIcon,
});
};
useEffect(() => {
if (canvasInstanceRef.current && canvasIsRender) {
initControl();
}
}, [canvasInstanceRef, canvasIsRender]);
};
/** 控制器 */
export const Control = () => {
useControl();
return <></>;
};
import classNames from 'classnames';
import { fabric } from 'fabric';
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import {
ACTION,
IMAGE_NAME,
LANG,
MENU_TYPE_ENUM,
MENU_TYPE_TEXT,
WORK_SPACE_ID,
keyCodes,
} from '../constants';
import Cropzone from '../helpers/CropZone';
import { EditorContext, clamp } from '../util';
import CropPop from './setting/CropPop';
import Popover from './setting/Popover';
const MOUSE_MOVE_THRESHOLD = 10;
export const useCrop = () => {
const {
canvasInstanceRef,
currentMenuRef,
setCurrentMenu,
canvasIsRender,
currentMenu,
wrapperInstanceRef,
historyRef,
unSaveHistory,
} = useContext(EditorContext);
/** 裁剪框实例 */
const cropzone = useRef<any>(null);
/** 裁剪开始X */
const startX = useRef(0);
/** 裁剪开始Y */
const startY = useRef(0);
/** 是否有ShiftKey */
const withShiftKey = useRef(false);
/** 计算裁剪框坐标 */
const calcRectDimensionFromPoint = useCallback(
(x: number, y: number, presetRatio = null) => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
const canvasWidth = canvas.getWidth();
const canvasHeight = canvas.getHeight();
const startXNew = startX.current;
const startYNew = startY.current;
let left = clamp(x, 0, startXNew);
let top = clamp(y, 0, startYNew);
let width = clamp(x, startXNew, canvasWidth) - left; // 开始坐标小于鼠标坐标小于画布坐标
let height = clamp(y, startYNew, canvasHeight) - top; //
if (withShiftKey.current && !presetRatio) {
if (width > height) {
height = width;
} else if (height > width) {
width = height;
}
if (startXNew >= x) {
left = startXNew - width;
}
if (startYNew >= y) {
top = startYNew - height;
}
} else if (presetRatio) {
height = width / presetRatio;
if (startXNew >= x) {
left = clamp(startXNew - width, 0, canvasWidth);
}
if (startYNew >= y) {
top = clamp(startYNew - height, 0, canvasHeight);
}
if (top + height > canvasHeight) {
height = canvasHeight - top;
width = height * presetRatio;
if (startXNew >= x) {
left = clamp(startXNew - width, 0, canvasWidth);
}
if (startYNew >= y) {
top = clamp(startYNew - height, 0, canvasHeight);
}
}
}
return {
left,
top,
width,
height,
};
},
[],
);
const keydown = useCallback((e: any) => {
if (currentMenuRef.current !== MENU_TYPE_ENUM.crop) {
return;
}
if (e.keyCode === keyCodes.SHIFT) {
withShiftKey.current = true;
}
}, []);
const keyup = useCallback((e: any) => {
if (currentMenuRef.current !== MENU_TYPE_ENUM.crop) {
return;
}
if (e.keyCode === keyCodes.SHIFT) {
withShiftKey.current = false;
}
}, []);
const mousemove = useCallback(
(fEvent: any) => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
if (currentMenuRef.current !== MENU_TYPE_ENUM.crop) {
return;
}
const pointer = canvas.getPointer(fEvent.e);
const { x, y } = pointer;
const cropzoneNew = cropzone.current;
if (!cropzoneNew) {
return;
}
if (
Math.abs(x - startX.current) + Math.abs(y - startY.current) >
MOUSE_MOVE_THRESHOLD
) {
canvas.remove(cropzoneNew);
cropzoneNew.set(
calcRectDimensionFromPoint(x, y, cropzoneNew.presetRatio),
);
canvas.add(cropzoneNew);
canvas.setActiveObject(cropzoneNew);
}
},
[canvasIsRender],
);
const mouseup = useCallback(() => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
if (currentMenuRef.current !== MENU_TYPE_ENUM.crop) {
return;
}
const cropzoneNew = cropzone.current;
canvas.setActiveObject(cropzoneNew);
canvas.off('mouse:move', mousemove);
canvas.off('mouse:up', mouseup);
}, [canvasIsRender]);
const mousedown = useCallback(
(fEvent: any) => {
if (fEvent.target) {
return;
}
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
if (currentMenuRef.current !== MENU_TYPE_ENUM.crop) {
return;
}
canvas.selection = false;
const coord = canvas.getPointer(fEvent.e);
startX.current = coord.x;
startY.current = coord.y;
canvas.on('mouse:move', mousemove);
canvas.on('mouse:up', mouseup);
},
[canvasIsRender],
);
const start = useCallback(() => {
const canvas = canvasInstanceRef.current;
if (!canvas || !canvasIsRender) {
return;
}
if (cropzone.current) {
return;
}
/** 不让触发ObjectAdd事件 */
unSaveHistory.current = true;
canvas.getObjects().forEach((obj: any) => {
obj.evented = false;
});
cropzone.current = new Cropzone(canvasInstanceRef.current, {
left: 0,
top: 0,
width: 0.5,
height: 0.5,
strokeWidth: 0,
cornerSize: 16,
cornerColor: 'blue',
fill: 'transparent',
hasRotatingPoint: false,
hasBorders: false,
lockScalingFlip: true,
lockRotation: true,
lockSkewingX: true,
lockSkewingY: true,
cornerStyle: 'circle',
cornerStrokeColor: 'blue',
transparentCorners: false,
lineWidth: 2,
borderColor: 'blue',
} as any);
canvas.discardActiveObject();
canvas.add(cropzone.current);
canvas.on('mouse:down', mousedown);
canvas.selection = false;
canvas.defaultCursor = 'crosshair';
fabric.util.addListener(document as any, 'keydown', keydown);
fabric.util.addListener(document as any, 'keyup', keyup);
}, [canvasIsRender]);
const end = useCallback(() => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
const cropzoneNew = cropzone.current;
if (!cropzoneNew) {
return;
}
canvas.remove(cropzoneNew);
canvas.selection = true;
canvas.defaultCursor = 'default';
canvas.off('mouse:down', mousedown);
canvas.off('mouse:move', mousemove);
canvas.off('mouse:up', mouseup);
canvas.forEachObject((obj: any) => {
obj.evented = true;
});
cropzone.current = null;
fabric.util.removeListener(document as any, 'keydown', keydown);
fabric.util.removeListener(document as any, 'keyup', keyup);
/** 解除,可以触发ObjectAdd事件 */
unSaveHistory.current = false;
}, [canvasIsRender]);
/** 切换裁剪 */
const handleCropTrigger = () => {
if (currentMenuRef.current !== MENU_TYPE_ENUM.crop) {
start();
} else {
end();
}
setCurrentMenu(
currentMenuRef.current !== MENU_TYPE_ENUM.crop
? MENU_TYPE_ENUM.crop
: MENU_TYPE_ENUM.default,
);
};
/** 获取裁剪框参数 */
const getCropzoneRect = () => {
const cropzoneNew = cropzone.current;
if (!cropzoneNew || !cropzoneNew.isValid()) {
return null;
}
return {
left: cropzoneNew.left,
top: cropzoneNew.top,
width: cropzoneNew.width,
height: cropzoneNew.height,
};
};
/** 获取裁剪部分图片数据 */
const getCroppedImageData = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
const containsCropzone = canvas.contains(cropzone.current);
const cropRect = getCropzoneRect();
if (!cropRect) {
return null;
}
if (containsCropzone) {
canvas.remove(cropzone.current);
}
const imageData = {
imageName: IMAGE_NAME,
url: canvas.toDataURL(cropRect),
};
if (containsCropzone) {
canvas.add(cropzone.current);
}
return imageData;
};
/** 确认或取消裁剪 */
const handleCropChange = (value: boolean) => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
if (!value) {
end();
start();
} else {
const data = getCroppedImageData();
if (!data) {
return;
}
if (!data.url.includes('base64')) {
// Toast.warn('请划分裁剪区域', 1500);
return;
}
canvas.clear();
canvas.setBackgroundImage(data.url, function () {
let oImg = canvas.backgroundImage;
const { height } = wrapperInstanceRef.current.getBoundingClientRect();
const center = canvas.getCenter();
const imgHeight = oImg.height;
/** 图片过大,使其大小正好跟容器一致 */
oImg.scale(height / imgHeight);
/** 使得图片在canvas中间 */
oImg.set({
left: center.left - (oImg.width * (height / imgHeight)) / 2,
top: center.top - (oImg.height * (height / imgHeight)) / 2,
});
/** 不让直接操作图片 */
oImg.selectable = false;
oImg.id = WORK_SPACE_ID;
canvas.renderAll();
/** 准备下一次裁剪 */
end();
historyRef.current.updateCanvasState(ACTION.add);
start();
});
}
};
useEffect(() => {
if (currentMenu !== MENU_TYPE_ENUM.crop && cropzone.current) {
/** 切换到其他菜单时清空 */
end();
}
}, [currentMenu]);
return { handleCropTrigger, handleCropChange, currentMenu };
};
/** 裁剪 */
export const Crop = () => {
const {
lang = LANG.en
} = useContext(EditorContext);
const { handleCropTrigger, handleCropChange, currentMenu } = useCrop();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-crop',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.crop,
},
)}
>
<CropPop
open={currentMenu === MENU_TYPE_ENUM.crop}
onChange={handleCropChange}
>
<Popover content={MENU_TYPE_TEXT.crop[lang]} placement="top">
<i className="tie-image-editor_icon" onClick={handleCropTrigger} />
</Popover>
</CropPop>
</div>
</>
);
};
import React, { forwardRef, useContext, useImperativeHandle } from 'react';
import { IMAGE_NAME, LANG, MENU_TYPE_TEXT } from '../constants';
import { EditorContext, base64ToBlob, isSupportFileApi } from '../util';
import Popover from './setting/Popover';
const useDownload = () => {
const { canvasInstanceRef, canvasEl, onDownLoad } = useContext(EditorContext);
const handleDownload = (auto: boolean = false) => {
console.log('heheh');
const canvas = canvasInstanceRef.current;
if (!canvas || !canvasEl.current) {
return Promise.reject();
}
const dataURL = canvas.toDataURL();
let imageName = Date.now() + IMAGE_NAME;
let blob, type;
if (onDownLoad) {
onDownLoad({ downLoadUrl: dataURL });
return Promise.resolve({ downLoadUrl: dataURL });
}
if (!auto) {
return Promise.resolve({ downLoadUrl: dataURL });
}
if (isSupportFileApi() && (window as any).saveAs) {
blob = base64ToBlob(dataURL);
type = blob.type.split('/')[1];
if (imageName.split('.').pop() !== type) {
imageName += `.${type}`;
}
(window as any).saveAs(blob, imageName);
} else {
if (imageName.split('.').pop() !== type) {
imageName += `.png`;
}
const anchorEl = document.createElement('a');
anchorEl.href = dataURL;
anchorEl.download = imageName;
document.body.appendChild(anchorEl);
anchorEl.click();
anchorEl.remove();
}
return Promise.resolve({ downLoadUrl: dataURL });
};
return { handleDownload };
};
/** 下载 */
export const Download = forwardRef((props, ref) => {
const {
lang = LANG.en
} = useContext(EditorContext);
const { handleDownload } = useDownload();
useImperativeHandle(
ref,
() => ({
download: handleDownload,
}),
[handleDownload],
);
return (
<div className="tie-image-editor_tool-item tie-image-editor_tool-download">
<Popover content={MENU_TYPE_TEXT.download[lang]} placement="top">
<i
className="tie-image-editor_icon"
onClick={() => handleDownload(true)}
/>
</Popover>
</div>
);
});
Download.displayName = 'Download';
import React, { useContext, useEffect, useRef } from 'react';
import classNames from 'classnames';
import { LANG, MENU_TYPE_ENUM, MENU_TYPE_TEXT, WORK_SPACE_ID } from '../constants';
import { EditorContext } from '../util';
import Popover from './setting/Popover';
export const useDrag = () => {
const {
canvasInstanceRef,
wrapperInstanceRef,
canvasIsRender,
currentMenu,
setCurrentMenu,
} = useContext(EditorContext);
const dragModeRef = useRef(false);
useEffect(() => {
dragModeRef.current = currentMenu === MENU_TYPE_ENUM.drag;
}, [setCurrentMenu, currentMenu]);
const _setDring = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
canvas.selection = false;
canvas.defaultCursor = 'grab';
canvas.getObjects().forEach((obj: any) => {
obj.selectable = false;
});
canvas.requestRenderAll();
};
const startDring = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
console.log('startDring');
setCurrentMenu(MENU_TYPE_ENUM.drag);
canvas.defaultCursor = 'grab';
canvas.renderAll();
};
const endDring = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
setCurrentMenu(MENU_TYPE_ENUM.default);
canvas.defaultCursor = 'default';
canvas.isDragging = false;
canvas.selection = false;
canvas.renderAll();
};
// 拖拽模式;
const initDring = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
canvas.on('mouse:down', function (this: any, opt: any) {
const evt = opt.e;
if (evt.altKey || dragModeRef.current) {
canvas.defaultCursor = 'grabbing';
canvas.discardActiveObject();
_setDring();
canvas.selection = false;
canvas.isDragging = true;
canvas.lastPosX = evt.clientX;
canvas.lastPosY = evt.clientY;
canvas.requestRenderAll();
}
});
canvas.on('mouse:move', function (this: any, opt: any) {
if (canvas.isDragging) {
canvas.discardActiveObject();
canvas.defaultCursor = 'grabbing';
const { e } = opt;
if (!canvas.viewportTransform) return;
const vpt = canvas.viewportTransform;
vpt[4] += e.clientX - canvas.lastPosX;
vpt[5] += e.clientY - canvas.lastPosY;
canvas.lastPosX = e.clientX;
canvas.lastPosY = e.clientY;
canvas.requestRenderAll();
}
});
canvas.on('mouse:up', function (this: any) {
if (!canvas.viewportTransform || !dragModeRef.current) return;
canvas.setViewportTransform(this.viewportTransform);
canvas.isDragging = false;
canvas.selection = true;
canvas.getObjects().forEach((obj: any) => {
if (obj.id !== WORK_SPACE_ID && obj.hasControls) {
obj.selectable = true;
}
});
canvas.defaultCursor = 'default';
canvas.requestRenderAll();
});
};
const handleWrapperKeyDown = (e: any) => {
/** 空格拖拽功能 */
const canvas = canvasInstanceRef.current;
if (e.keyCode === 32 && canvas && !dragModeRef.current) {
startDring();
}
e.preventDefault();
};
const handleWrapperKeyUp = (e: any) => {
/** 结束空格拖拽功能 */
const canvas = canvasInstanceRef.current;
if (e.keyCode === 32 && canvas) {
endDring();
}
};
useEffect(() => {
if (canvasInstanceRef.current && canvasIsRender) {
initDring();
wrapperInstanceRef.current.tabIndex = 1000;
wrapperInstanceRef.current.addEventListener(
'keydown',
handleWrapperKeyDown,
false,
);
wrapperInstanceRef.current.addEventListener(
'keyup',
handleWrapperKeyUp,
false,
);
}
return () => {
const canvas = canvasInstanceRef.current;
if (canvas && wrapperInstanceRef.current) {
wrapperInstanceRef.current?.removeEventListener(
'keydown',
handleWrapperKeyDown,
false,
);
wrapperInstanceRef.current?.removeEventListener(
'keyup',
handleWrapperKeyUp,
false,
);
}
};
}, [canvasInstanceRef, canvasIsRender]);
const handleMoving = () => {
setCurrentMenu(
currentMenu !== MENU_TYPE_ENUM.drag
? MENU_TYPE_ENUM.drag
: MENU_TYPE_ENUM.default,
);
};
return { handleMoving, currentMenu };
};
export const Drag = () => {
const {
lang = LANG.en
} = useContext(EditorContext);
const { handleMoving, currentMenu } = useDrag();
return (
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-hand',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.drag,
},
)}
>
<Popover content={MENU_TYPE_TEXT.drag[lang]} placement="top">
<i
className={classNames('tie-image-editor_icon')}
onClick={handleMoving}
/>
</Popover>
</div>
);
};
/* eslint-disable @typescript-eslint/no-unused-expressions */
import classNames from 'classnames';
import { fabric } from 'fabric';
import React, { useContext, useRef } from 'react';
import { IPaintTypes, LANG, MENU_TYPE_ENUM, MENU_TYPE_TEXT, paintConfig } from '../constants';
import { EditorContext } from '../util';
import Paint from './setting/Paint';
import Popover from './setting/Popover';
export const useDraw = () => {
const { canvasInstanceRef, currentMenuRef, setCurrentMenu, currentMenu } =
useContext(EditorContext);
const paintConfigValue = useRef({
color: paintConfig.colors[0],
size: paintConfig.sizes[0],
});
const initDraw = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
canvas.freeDrawingBrush = new fabric['PencilBrush'](canvas);
let brush = canvas.freeDrawingBrush;
brush.color = paintConfigValue.current.color;
if (brush.getPatternSrc) {
brush.source = brush.getPatternSrc.call(brush);
}
brush.width = paintConfigValue.current.size;
brush.shadow = new fabric.Shadow({
blur: paintConfigValue.current.size,
offsetX: 0,
offsetY: 0,
affectStroke: true,
color: '#333',
});
};
const handleDrawTrigger = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
if (currentMenuRef.current !== MENU_TYPE_ENUM.draw) {
canvas.isDrawingMode = true;
initDraw();
} else {
canvas.isDrawingMode = false;
}
setCurrentMenu(
currentMenuRef.current !== MENU_TYPE_ENUM.draw
? MENU_TYPE_ENUM.draw
: MENU_TYPE_ENUM.default,
);
};
const handlePaintChange = (type: IPaintTypes, value: number | string) => {
if (!canvasInstanceRef.current) {
return;
}
const canvas = canvasInstanceRef.current;
const activeObject = canvas.getActiveObjects()[0];
if (type === IPaintTypes.color) {
paintConfigValue.current.color = value as string;
activeObject && activeObject.set('fill', value);
} else {
paintConfigValue.current.size = value as number;
activeObject && activeObject.set('width', value);
}
canvas.renderAll();
initDraw();
};
return { handleDrawTrigger, handlePaintChange, currentMenu };
};
export const Draw = () => {
const {
lang = LANG.en
} = useContext(EditorContext);
const { handleDrawTrigger, handlePaintChange, currentMenu } = useDraw();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-draw',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.draw,
},
)}
>
<Paint
open={currentMenu === MENU_TYPE_ENUM.draw}
onChange={handlePaintChange}
>
<Popover content={MENU_TYPE_TEXT.draw[lang]} placement="top">
<i className="tie-image-editor_icon" onClick={handleDrawTrigger} />
</Popover>
</Paint>
</div>
</>
);
};
import classNames from 'classnames';
import React, { useContext, useEffect } from 'react';
import { ACTION, FLIP_TYPE, LANG, MENU_TYPE_ENUM, MENU_TYPE_TEXT } from '../constants';
import { EditorContext } from '../util';
import FlipPop from './setting/FlipPop';
import Popover from './setting/Popover';
export const useFlip = () => {
const {
canvasInstanceRef,
currentMenuRef,
setCurrentMenu,
canvasIsRender,
currentMenu,
historyRef,
} = useContext(EditorContext);
useEffect(() => {
const canvas = canvasInstanceRef.current;
if (!canvas || !canvasIsRender) {
return;
}
}, [canvasIsRender]);
const handleFlipTrigger = () => {
setCurrentMenu(
currentMenuRef.current !== MENU_TYPE_ENUM.flip
? MENU_TYPE_ENUM.flip
: MENU_TYPE_ENUM.default,
);
};
const handleFlipChange = (type: FLIP_TYPE, value: boolean) => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
/** 针对底图翻转的原理是将图标先旋转相反角度,再重新计算坐标,最后翻转 */
canvas.getObjects().forEach((obj: any) => {
obj.originalPoint = obj.originalPoint || { left: obj.left, top: obj.top };
let options = {
[type]: value,
};
if (type === FLIP_TYPE.flipX) {
if (value) {
options = {
...options,
left: canvas.width - obj.originalPoint.left - obj.width,
} as any;
} else {
options = { ...options, left: obj.originalPoint.left } as any;
}
} else {
if (value) {
options = {
...options,
top: canvas.height - obj.originalPoint.top - obj.height,
} as any;
} else {
options = { ...options, top: obj.originalPoint.top } as any;
}
}
obj
.set(options)
.rotate(parseFloat((obj.angle * -1).toString()))
.setCoords();
});
const canvasImage = canvas.backgroundImage;
let { angle } = canvasImage;
if (value) {
angle *= -1;
}
canvasImage.set({ [type]: value });
canvasImage.rotate(parseFloat(angle)).setCoords(); // parseFloat for -0 to 0
canvasImage.setCoords();
canvas.renderAll();
historyRef.current.updateCanvasState(ACTION.add);
};
return { handleFlipTrigger, handleFlipChange, currentMenu };
};
export const Flip = () => {
const {
lang = LANG.en
} = useContext(EditorContext);
const { handleFlipTrigger, handleFlipChange, currentMenu } = useFlip();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-flip',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.flip,
},
)}
>
<FlipPop
open={currentMenu === MENU_TYPE_ENUM.flip}
onChange={handleFlipChange}
>
<Popover content={MENU_TYPE_TEXT.flip[lang]} placement="top">
<i className="tie-image-editor_icon" onClick={handleFlipTrigger} />
</Popover>
</FlipPop>
</div>
</>
);
};
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
forwardRef,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useState,
} from 'react';
// import { Icon, Popover } from '@casstime/bricks';
import classNames from 'classnames';
import React from 'react';
import {
ACTION,
ACTION_TEXT,
LANG,
MAX_HISTORY_LEN,
MENU_TYPE_ENUM,
MENU_TYPE_TEXT,
WORK_SPACE_ID,
stayRemain,
} from '../constants';
import { EditorContext, IHistory } from '../util';
import Popover from './setting/Popover';
export const useHistory = () => {
const {
canvasInstanceRef,
canvasIsRender,
currentMenuRef,
initCanvasJson,
unSaveHistory,
} = useContext(EditorContext);
const [history, setHistory] = useState<IHistory[]>([] as IHistory[]);
const [currentId, setCurrentId] = useState('');
/**
* action:操作类型
* remove:是否移除最后一项
* append:是否追加最新项目
*/
const updateCanvasState = useCallback(
(action: ACTION, remove: boolean = false, append: boolean = false) => {
if (!canvasInstanceRef.current || unSaveHistory.current) {
return;
}
const canvas = canvasInstanceRef.current;
try {
/** 列表为空 */
const jsonData = canvas.toJSON(stayRemain);
const canvasAsJson = JSON.stringify(jsonData);
const id = Date.now() + '_' + Math.floor(Math.random() * 10000);
const currentIndex = history.findIndex(
({ id: hId = '' }) => hId === currentId,
);
/**
* (如果当前索引在中间,需要把索引后边的历史去掉)
* 1、根据remove决定是否移除最后一条
* 2、根据append决定是否追加一条
* 3、根据MAX_HISTORY_LEN,最多存 MAX_HISTORY_LEN 条历史
* */
const newHistory = history.slice(0, currentIndex + 1 - +remove).concat(
append || (!remove && !append)
? [
{
id,
data: canvasAsJson,
type:
action !== ACTION.delete
? currentMenuRef.current
: MENU_TYPE_ENUM.default,
action,
},
]
: [],
);
setHistory(
newHistory.slice(Math.max(0, history.length - MAX_HISTORY_LEN)),
);
setCurrentId(id);
} catch (error) {
console.error(error);
}
},
[history, currentId],
);
const objectModified = useCallback(
(e: any) => {
if (e.target.id === WORK_SPACE_ID || (e.action === undefined && e.e)) {
return;
}
updateCanvasState(ACTION.modified);
},
[updateCanvasState],
);
const objectAdd = useCallback(
(e: any) => {
if (e.action === undefined && e.e) {
return;
}
updateCanvasState(ACTION.add);
},
[updateCanvasState],
);
useEffect(() => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
if (canvas && canvasIsRender) {
canvas.on('object:modified', objectModified);
canvas.on('object:added', objectAdd);
}
return () => {
canvas.off('object:modified', objectModified);
canvas.off('object:added', objectAdd);
};
}, [canvasInstanceRef, canvasIsRender, objectModified, objectAdd]);
const renderNewCanvas = useCallback(
(id: string = '') => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
if (id === currentId) {
return;
}
const newCanvasStr =
history.find(({ id: cId }) => cId === id)?.data ||
initCanvasJson.current;
unSaveHistory.current = true;
canvas.loadFromJSON(newCanvasStr, () => {
canvas.renderAll();
unSaveHistory.current = false;
});
setCurrentId(id);
},
[currentId, history],
);
/** 前进 */
const handleRedo = useCallback(() => {
if (!canvasInstanceRef.current || !history.length) {
return;
}
let newId = '';
if (!currentId) {
newId = history[0]?.id;
} else {
history.forEach(({ id }, index) => {
if (id === currentId) {
newId = history[index + 1]?.id;
}
});
}
renderNewCanvas(newId);
}, [currentId, history]);
/** 后退 */
const handleUndo = useCallback(() => {
if (!canvasInstanceRef.current || !history.length) {
return;
}
let newId = '';
history.forEach(({ id }, index) => {
if (id === currentId) {
newId = history[index - 1]?.id;
}
});
renderNewCanvas(newId);
}, [currentId, history]);
return {
history,
currentId,
renderNewCanvas,
handleRedo,
handleUndo,
updateCanvasState,
};
};
/** 历史 */
export const History = forwardRef((props, ref) => {
const { lang = LANG.en } = useContext(EditorContext);
const {
history,
currentId,
renderNewCanvas,
handleRedo,
handleUndo,
updateCanvasState,
} = useHistory();
useImperativeHandle(ref, () => ({
updateCanvasState,
}));
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-history',
{
'tie-image-editor_tool-item--disabled': !history.length,
},
)}
>
<Popover
content={
<div className="tie-image-editor-history_pop">
{!!history.length && (
<>
<div className="tie-image-editor-history_pop-title">
{ACTION_TEXT.title[lang]}
<span className="tie-image-editor-history_pop-len">
{history.length}
</span>
</div>
<div
onClick={() => renderNewCanvas('')}
className={classNames('tie-image-editor-history_pop-item', {
'tie-image-editor-history_pop-item--checked': !currentId,
})}
>
<span className="tie-image-editor-history_pop-item-num">
1.
</span>
{ACTION_TEXT.init[lang]}
{!currentId && (
<i className="tie-image-editor-history_pop-item-icon" />
)}
</div>
</>
)}
{!history.length && <span>{ACTION_TEXT.placeholder[lang]}</span>}
{history.map(({ action = '', id, type }, index) => (
<div
key={id}
onClick={() => renderNewCanvas(id)}
className={classNames('tie-image-editor-history_pop-item', {
'tie-image-editor-history_pop-item--checked':
id === currentId,
})}
>
<span className="tie-image-editor-history_pop-item-num">
{index + 2}.
</span>
{!!ACTION_TEXT[action] && ACTION_TEXT[action][lang]}{' '}
{!!MENU_TYPE_TEXT[type] && MENU_TYPE_TEXT[type][lang]}
{id === currentId && (
<i className="tie-image-editor-history_pop-item-icon" />
)}
</div>
))}
</div>
}
placement="bottom"
className="tie-image-editor-history_target"
>
<i className={classNames('tie-image-editor_icon')} />
</Popover>
</div>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-redo',
{
'tie-image-editor_tool-item--disabled':
!history.length || currentId === history[history.length - 1]?.id,
},
)}
>
<Popover content={MENU_TYPE_TEXT.redo[lang]} placement="top">
<i
className={classNames('tie-image-editor_icon')}
onClick={
!history.length || currentId === history[history.length - 1]?.id
? undefined
: handleRedo
}
/>
</Popover>
</div>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-undo',
{
'tie-image-editor_tool-item--disabled':
!history.length || !currentId,
},
)}
>
<Popover content={MENU_TYPE_TEXT.undo[lang]} placement="top">
<i
className={classNames('tie-image-editor_icon')}
onClick={!history.length || !currentId ? undefined : handleUndo}
/>
</Popover>
</div>
</>
);
});
History.displayName = 'History';
import classNames from 'classnames';
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import { fabric } from 'fabric';
import { LANG, MENU_TYPE_ENUM, MENU_TYPE_TEXT, paintConfig } from '../constants';
import { EditorContext } from '../util';
import MosaicPop from './setting/MosaicPop';
import Popover from './setting/Popover';
let blockSize = paintConfig.mosaicSizes[0];
const mosaicify = (imageData: any) => {
const { data } = imageData;
const iLen = imageData.height;
const jLen = imageData.width;
let index;
let i;
let j;
let r;
let g;
let b;
let a;
let _i;
let _j;
let _iLen;
let _jLen;
for (i = 0; i < iLen; i += blockSize) {
for (j = 0; j < jLen; j += blockSize) {
index = i * 4 * jLen + j * 4;
r = data[index];
g = data[index + 1];
b = data[index + 2];
a = data[index + 3];
_iLen = Math.min(i + blockSize, iLen);
_jLen = Math.min(j + blockSize, jLen);
for (_i = i; _i < _iLen; _i++) {
for (_j = j; _j < _jLen; _j++) {
index = _i * 4 * jLen + _j * 4;
data[index] = r;
data[index + 1] = g;
data[index + 2] = b;
data[index + 3] = a;
/*
data[index] = 0;
data[index + 1] = 0;
data[index + 2] = 0;
*/
}
}
}
}
};
const useMosaic = () => {
const {
canvasInstanceRef,
canvasIsRender,
currentMenu,
currentMenuRef,
setCurrentMenu,
} = useContext(EditorContext);
const mosaicBrush = useRef<any>(null);
const initMosaic = useCallback(() => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
mosaicBrush.current = new fabric.PatternBrush(canvas);
mosaicBrush.current.getPatternSrc = function () {
// 创立一个暂存 canvas 来绘製要画的图案
const cropping = {
left: 0,
top: 0,
width: canvas.width,
height: canvas.height,
};
const imageCanvas = canvas.toCanvasElement(1, cropping);
const imageCtx = imageCanvas.getContext('2d');
const imageData = imageCtx.getImageData(
0,
0,
imageCanvas.width,
imageCanvas.height,
);
mosaicify(imageData);
imageCtx.putImageData(imageData, 0, 0);
const patternCanvas = (fabric as any).document.createElement('canvas'); // 这里的ceateElement一定要使用fabric内置的方法
const patternCtx = patternCanvas.getContext('2d');
patternCanvas.width = canvas.width || 0;
patternCanvas.height = canvas.height || 0;
patternCtx.drawImage(
imageCanvas,
0,
0,
imageCanvas.width,
imageCanvas.height,
cropping.left,
cropping.top,
cropping.width,
cropping.height,
);
return patternCanvas;
};
}, []);
useEffect(() => {
if (canvasIsRender) {
initMosaic();
}
}, [canvasIsRender]);
const handleMosaicTrigger = useCallback(() => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
if (currentMenuRef.current !== MENU_TYPE_ENUM.mosaic) {
/** 启动马赛克 */
canvas.isDrawingMode = true;
canvas.freeDrawingBrush = mosaicBrush.current;
let brush = canvas.freeDrawingBrush;
if (brush.getPatternSrc) {
brush.source = brush.getPatternSrc.call(brush);
}
brush.width = 30;
brush.shadow = new fabric.Shadow({
blur: 0,
offsetX: 0,
offsetY: 0,
affectStroke: true,
});
} else {
canvas.isDrawingMode = false;
}
setCurrentMenu(
currentMenuRef.current === MENU_TYPE_ENUM.mosaic
? MENU_TYPE_ENUM.default
: MENU_TYPE_ENUM.mosaic,
);
}, [initMosaic]);
const handleSizeChange = (size: number) => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
blockSize = size;
let brush = canvas.freeDrawingBrush;
if (brush.getPatternSrc) {
brush.source = brush.getPatternSrc.call(brush);
}
};
return { currentMenu, handleMosaicTrigger, handleSizeChange };
};
/**
* 马赛克
*/
const Mosaic = () => {
const {
lang = LANG.en
} = useContext(EditorContext);
const { currentMenu, handleMosaicTrigger, handleSizeChange } = useMosaic();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-mosaic',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.mosaic,
},
)}
>
<MosaicPop
open={currentMenu === MENU_TYPE_ENUM.mosaic}
onChange={handleSizeChange}
>
<Popover content={MENU_TYPE_TEXT.mosaic[lang]} placement="top">
<i
className="tie-image-editor_icon"
onClick={handleMosaicTrigger}
/>
</Popover>
</MosaicPop>
</div>
</>
);
};
export default Mosaic;
/* eslint-disable @typescript-eslint/no-unused-expressions */
import classNames from 'classnames';
import { fabric } from 'fabric';
import React, { useContext, useEffect, useRef } from 'react';
import {
ACTION,
CURSOR,
IPaintTypes,
LANG,
MENU_TYPE_ENUM,
MENU_TYPE_TEXT,
paintConfig,
} from '../constants';
import { EditorContext } from '../util';
import Paint from './setting/Paint';
import Popover from './setting/Popover';
export const useRect = () => {
const {
canvasInstanceRef,
currentMenuRef,
canvasIsRender,
currentMenu,
historyRef,
setCurrentMenu,
} = useContext(EditorContext);
const startRect = useRef(false);
const rectPoint = useRef({ x: 9, y: 0 });
const currentRect = useRef<any>(null);
const paintConfigValue = useRef({
color: paintConfig.colors[0],
size: paintConfig.sizes[0],
});
/** 初始化矩形 */
const initRect = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
canvas.on('mouse:down', function (o: any) {
if (
canvas.getActiveObject() ||
currentMenuRef.current !== MENU_TYPE_ENUM.rect
) {
return false;
}
startRect.current = true;
let pointer = canvas.getPointer(o.e);
rectPoint.current.x = pointer.x;
rectPoint.current.y = pointer.y;
currentRect.current = new fabric.Rect({
left: rectPoint.current.x,
top: rectPoint.current.y,
originX: 'left',
originY: 'top',
width: pointer.x - rectPoint.current.x,
height: pointer.y - rectPoint.current.y,
angle: 0,
fill: 'rgba(255,0,0,0)',
stroke: paintConfigValue.current.color,
strokeWidth: paintConfigValue.current.size,
transparentCorners: false,
});
canvas.add(currentRect.current);
});
canvas.on('mouse:move', function (o: any) {
if (
!canvas.getActiveObject() &&
currentMenuRef.current === MENU_TYPE_ENUM.rect
) {
canvas.setCursor(CURSOR.crosshair);
}
if (
!startRect.current ||
!currentRect.current ||
currentMenuRef.current !== MENU_TYPE_ENUM.rect
) {
canvas.renderAll();
return;
}
let pointer = canvas.getPointer(o.e);
if (rectPoint.current.x > pointer.x) {
currentRect.current.set({ left: Math.abs(pointer.x) });
}
if (rectPoint.current.y > pointer.y) {
currentRect.current.set({ top: Math.abs(pointer.y) });
}
currentRect.current.set({
width: Math.abs(rectPoint.current.x - pointer.x),
});
currentRect.current.set({
height: Math.abs(rectPoint.current.y - pointer.y),
});
canvas.setCursor(CURSOR.crosshair);
canvas.renderAll();
});
canvas.on('mouse:up', function (o: any) {
if (
!currentRect.current ||
currentMenuRef.current !== MENU_TYPE_ENUM.rect
) {
return;
}
let pointer = canvas.getPointer(o.e);
if (
pointer.x === currentRect.current.left &&
pointer.y === currentRect.current.top
) {
/** 无效矩形 */
canvas.remove(currentRect.current);
canvas.requestRenderAll();
historyRef.current.updateCanvasState(ACTION.add, true, false);
} else {
/** 有效矩形 */
canvas.setActiveObject(currentRect.current);
historyRef.current.updateCanvasState(ACTION.add, true, true);
}
startRect.current = false;
currentRect.current = null;
});
};
useEffect(() => {
if (canvasInstanceRef.current && canvasIsRender) {
initRect();
}
}, [canvasInstanceRef, canvasIsRender]);
const handleDrawRect = () => {
const newMenu =
currentMenuRef.current === MENU_TYPE_ENUM.rect
? MENU_TYPE_ENUM.default
: MENU_TYPE_ENUM.rect;
console.log('currentMenuRef.current', newMenu, currentMenuRef.current);
setCurrentMenu(newMenu);
};
const handlePaintChange = (type: IPaintTypes, value: number | string) => {
if (!canvasInstanceRef.current) {
return;
}
const canvas = canvasInstanceRef.current;
const activeObject = canvas.getActiveObjects()[0];
if (type === IPaintTypes.color) {
paintConfigValue.current.color = value as string;
activeObject && activeObject.set('stroke', value);
} else {
paintConfigValue.current.size = value as number;
activeObject && activeObject.set('strokeWidth', value);
}
canvas.renderAll();
};
return { handleDrawRect, handlePaintChange, currentMenu };
};
export const Rect = () => {
const { lang = LANG.en } = useContext(EditorContext);
const { handleDrawRect, handlePaintChange, currentMenu } = useRect();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-rect',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.rect,
},
)}
>
<Paint
open={currentMenu === MENU_TYPE_ENUM.rect}
onChange={handlePaintChange}
>
<Popover content={MENU_TYPE_TEXT.rect[lang]} placement="top">
<i
className={classNames('tie-image-editor_icon')}
onClick={handleDrawRect}
/>
</Popover>
</Paint>
</div>
</>
);
};
import { fabric } from 'fabric';
import React, { useContext } from 'react';
import { ACTION, LANG, MENU_TYPE_ENUM, MENU_TYPE_TEXT } from '../constants';
import { EditorContext } from '../util';
import Popover from './setting/Popover';
const useReset = () => {
const { setCurrentMenu, currentMenuRef, canvasInstanceRef, historyRef } =
useContext(EditorContext);
const handleReset = () => {
if (currentMenuRef.current !== MENU_TYPE_ENUM.reset) {
setCurrentMenu(MENU_TYPE_ENUM.reset);
}
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
/** 清空除了背景之外的图形 */
canvas.getObjects().forEach((o: any) => {
if (o !== canvas.backgroundImage) {
canvas.remove(o);
}
});
/** 将缩放也重置 */
const center = canvas.getCenter();
canvas.zoomToPoint(new fabric.Point(center.left, center.top), 1);
/** 将画布拖回初始值位置 */
const viewportTransform = canvas.viewportTransform;
viewportTransform[4] = 0;
viewportTransform[5] = 0;
canvas.setViewportTransform(viewportTransform);
canvas.renderAll();
/** 保存历史 */
historyRef.current.updateCanvasState(ACTION.add);
};
return { handleReset };
};
/** 重置 */
export const Reset = () => {
const { lang = LANG.en } = useContext(EditorContext);
const { handleReset } = useReset();
return (
<div className="tie-image-editor_tool-item tie-image-editor_tool-reset">
<Popover content={MENU_TYPE_TEXT.reset[lang]} placement="top">
<i className="tie-image-editor_icon" onClick={handleReset} />
</Popover>
</div>
);
};
import { useContext, useEffect } from 'react';
// import { Popover } from '@casstime/bricks';
import classNames from 'classnames';
import { fabric } from 'fabric';
import React from 'react';
import { ACTION, LANG, MENU_TYPE_ENUM, MENU_TYPE_TEXT } from '../constants';
import { EditorContext } from '../util';
import Popover from './setting/Popover';
import RotatePop from './setting/RotatePop';
export const useRotate = () => {
const {
canvasInstanceRef,
currentMenuRef,
setCurrentMenu,
canvasIsRender,
currentMenu,
historyRef,
} = useContext(EditorContext);
useEffect(() => {
const canvas = canvasInstanceRef.current;
if (!canvas || !canvasIsRender) {
return;
}
}, [canvasIsRender]);
const handleRotateTrigger = () => {
setCurrentMenu(
currentMenuRef.current !== MENU_TYPE_ENUM.rotate
? MENU_TYPE_ENUM.rotate
: MENU_TYPE_ENUM.default,
);
};
const handleRotateChange = (value: number | number[]) => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
canvas.getObjects().forEach((obj: any) => {
/** 记录第一次形状的位置,方便计算相对位置 */
obj.originalPoint =
obj.originalPoint || new fabric.Point(obj.left, obj.top);
let posNewCenter = fabric.util.rotatePoint(
obj.originalPoint,
canvas.getVpCenter(),
fabric.util.degreesToRadians(value as number),
);
obj.set({
left: posNewCenter.x,
top: posNewCenter.y,
angle: value,
});
});
/** 单独对背景图进行旋转 */
const canvasImage = canvas.backgroundImage;
canvasImage.rotate(value);
canvasImage.setCoords();
canvas.requestRenderAll();
historyRef.current.updateCanvasState(ACTION.add);
};
return { handleRotateTrigger, handleRotateChange, currentMenu };
};
export const Rotate = () => {
const {
lang = LANG.en
} = useContext(EditorContext);
const { handleRotateTrigger, handleRotateChange, currentMenu } = useRotate();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-rotate',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.rotate,
},
)}
>
<RotatePop
open={currentMenu === MENU_TYPE_ENUM.rotate}
onChange={handleRotateChange}
>
<Popover content={MENU_TYPE_TEXT.rotate[lang]} placement="top">
<i
className="tie-image-editor_icon"
onClick={handleRotateTrigger}
/>
</Popover>
</RotatePop>
</div>
</>
);
};
/* eslint-disable @typescript-eslint/no-unused-vars */
import { fabric } from 'fabric';
import React, { useCallback, useContext, useEffect } from 'react';
// import { Popover } from '@casstime/bricks';
import classNames from 'classnames';
import { LANG, MENU_TYPE_ENUM, MENU_TYPE_TEXT } from '../constants';
import { EditorContext } from '../util';
import Popover from './setting/Popover';
import ScalePop from './setting/ScalePop';
export const useScale = () => {
const {
canvasInstanceRef,
canvasIsRender,
currentMenuRef,
zoom,
setZoom,
setCurrentMenu,
} = useContext(EditorContext);
useEffect(() => {
const canvas = canvasInstanceRef.current;
if (canvas && canvasIsRender) {
canvas.on('mouse:wheel', function (opt: any) {
const delta = opt.e.deltaY;
let newZoom: number = canvas.getZoom();
newZoom *= 0.999 ** delta;
if (newZoom > 20) newZoom = 20;
if (newZoom < 0.01) newZoom = 0.01;
setZoom(newZoom);
// const center = canvas.getCenter();
// canvas.zoomToPoint(new fabric.Point(center.left, center.top), zoom);
canvas.requestRenderAll();
opt.e.preventDefault();
opt.e.stopPropagation();
});
}
}, [canvasInstanceRef, canvasIsRender]);
useEffect(() => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
const center = canvas.getCenter();
canvas.zoomToPoint(
new fabric.Point(center.left, center.top),
zoom < 0 ? 0.01 : zoom,
);
if (currentMenuRef.current !== MENU_TYPE_ENUM.scale) {
setCurrentMenu(MENU_TYPE_ENUM.scale);
}
}, [zoom]);
/** 放大 */
const handleScaleUp = () => {
if (!canvasInstanceRef.current) {
return;
}
const canvas = canvasInstanceRef.current;
let zoomRatio = canvas.getZoom();
zoomRatio += 0.05;
setZoom(zoomRatio);
// const center = canvas.getCenter();
// canvas.zoomToPoint(new fabric.Point(center.left, center.top), zoomRatio);
};
/** 缩小 */
const handleScaleDown = () => {
if (!canvasInstanceRef.current) {
return;
}
const canvas = canvasInstanceRef.current;
let zoomRatio = canvas.getZoom();
zoomRatio -= 0.05;
setZoom(zoomRatio);
// const center = canvas.getCenter();
// canvas.zoomToPoint(new fabric.Point(center.left, center.top), zoomRatio < 0 ? 0.01 : zoomRatio);
};
const handleScaleTrigger = () => {
setCurrentMenu(
currentMenuRef.current === MENU_TYPE_ENUM.scale
? MENU_TYPE_ENUM.default
: MENU_TYPE_ENUM.scale,
);
};
const handleReset = () => {
setZoom(1);
};
const handleOpenChange = useCallback((open: boolean) => {
setTimeout(() => {
/** 当前是缩放模式,点击其他区域才需要关闭缩放 */
if(MENU_TYPE_ENUM.scale === currentMenuRef.current){
setCurrentMenu(!open ? MENU_TYPE_ENUM.default : MENU_TYPE_ENUM.scale);
}
});
}, []);
return {
zoom,
handleScaleUp,
handleScaleDown,
handleScaleTrigger,
handleReset,
handleOpenChange,
};
};
export const Scale = () => {
const { lang = LANG.en, currentMenu } = useContext(EditorContext);
const {
zoom,
handleScaleDown,
handleScaleUp,
handleScaleTrigger,
handleReset,
handleOpenChange,
} = useScale();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-zoomIn',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.scale,
},
)}
>
<ScalePop
open={currentMenu === MENU_TYPE_ENUM.scale}
zoom={zoom}
onScaleDown={handleScaleDown}
onScaleUp={handleScaleUp}
onReset={handleReset}
onOpenChange={handleOpenChange}
resetText={MENU_TYPE_TEXT.reset[lang]}
key={MENU_TYPE_ENUM.scale}
>
<Popover content={MENU_TYPE_TEXT.scale[lang]} placement="top">
<i
className={classNames('tie-image-editor_icon')}
onClick={handleScaleTrigger}
/>
</Popover>
</ScalePop>
</div>
</>
);
};
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { fabric } from 'fabric';
import React, { useCallback, useContext, useEffect, useRef } from 'react';
// import { Popover } from '@casstime/bricks';
import classNames from 'classnames';
import {
IPaintTypes,
LANG,
MENU_TYPE_ENUM,
MENU_TYPE_TEXT,
paintConfig,
} from '../constants';
import { EditorContext } from '../util';
import Paint from './setting/Paint';
import Popover from './setting/Popover';
export const useText = () => {
const {
canvasInstanceRef,
currentMenuRef,
setCurrentMenu,
canvasIsRender,
currentMenu,
} = useContext(EditorContext);
const paintConfigValue = useRef({
color: paintConfig.colors[0],
size: paintConfig.fontSizes[0],
});
const isSelectingText = useRef(false);
const mouseup = useCallback((o: any) => {
const canvas = canvasInstanceRef.current;
if (!canvas || o.target) {
return;
}
if (isSelectingText.current) {
isSelectingText.current = false;
return;
}
if (
canvas.getActiveObject() ||
currentMenuRef.current !== MENU_TYPE_ENUM.text
) {
return;
}
const pointer = canvas.getPointer(o.e);
const text = new fabric.IText('输入内容', {
left: pointer.x,
top: pointer.y,
fontSize: paintConfigValue.current.size,
id: Math.random() * 4 + '_' + Date.now(),
fill: paintConfigValue.current.color,
} as any);
canvas.add(text);
canvas.setActiveObject(text);
isSelectingText.current = true;
canvas.renderAll();
}, []);
const initText = () => {
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
canvas.on('mouse:up', mouseup);
};
useEffect(() => {
const canvas = canvasInstanceRef.current;
if (!canvas || !canvasIsRender) {
return;
}
initText();
return () => {
canvas.off('mouse:up', mouseup);
};
}, [canvasIsRender]);
const handleTextTrigger = () => {
setCurrentMenu(
currentMenuRef.current !== MENU_TYPE_ENUM.text
? MENU_TYPE_ENUM.text
: MENU_TYPE_ENUM.default,
);
};
const handlePaintChange = (type: IPaintTypes, value: number | string) => {
if (!canvasInstanceRef.current) {
return;
}
const canvas = canvasInstanceRef.current;
const activeObject = canvas.getActiveObjects()[0];
if (type === IPaintTypes.color) {
paintConfigValue.current.color = value as string;
activeObject && activeObject.set('fill', value);
} else {
paintConfigValue.current.size = value as number;
activeObject && activeObject.set('fontSize', value);
}
canvas.renderAll();
};
return { handleTextTrigger, handlePaintChange, currentMenu };
};
export const Text = () => {
const { lang = LANG.en } = useContext(EditorContext);
const { handleTextTrigger, handlePaintChange, currentMenu } = useText();
return (
<>
<div
className={classNames(
'tie-image-editor_tool-item tie-image-editor_tool-text',
{
['tie-image-editor_tool-item--checked']:
currentMenu === MENU_TYPE_ENUM.text,
},
)}
>
<Paint
open={currentMenu === MENU_TYPE_ENUM.text}
onChange={handlePaintChange}
type={MENU_TYPE_ENUM.text}
>
<Popover content={MENU_TYPE_TEXT.text[lang]} placement="top">
<i className="tie-image-editor_icon" onClick={handleTextTrigger} />
</Popover>
</Paint>
</div>
</>
);
};
// import { Popover } from '@casstime/bricks';
import React, { useContext } from 'react';
import { ACTION, LANG, MENU_TYPE_ENUM, MENU_TYPE_TEXT } from '../constants';
import { EditorContext } from '../util';
import Popover from './setting/Popover';
const useUpload = () => {
const {
canvasInstanceRef,
canvasEl,
currentMenuRef,
historyRef,
setCurrentMenu,
initBackGroundImage,
} = useContext(EditorContext);
const handleUpload = (e: any) => {
const canvas = canvasInstanceRef.current;
if (
!canvas ||
!canvasEl.current ||
!e.target.files.length ||
!e.target.files[0]?.type?.includes('image')
) {
return;
}
if (currentMenuRef.current !== MENU_TYPE_ENUM.upload) {
setCurrentMenu(MENU_TYPE_ENUM.upload);
}
const reader = new FileReader();
reader.onload = function (event) {
if (!event || !event.target) {
return;
}
let imgObj = new Image();
imgObj.src = event.target.result as string;
initBackGroundImage(event.target.result as string, true, () => {
historyRef.current.updateCanvasState(ACTION.add);
});
};
reader.readAsDataURL(e.target.files[0]);
};
return { handleUpload };
};
/** 上传 */
export const Upload = () => {
const {
lang = LANG.en
} = useContext(EditorContext);
const { handleUpload } = useUpload();
return (
<div className="tie-image-editor_tool-item tie-image-editor_tool-upload">
<Popover content={MENU_TYPE_TEXT.upload[lang]} placement="top">
<i className="tie-image-editor_icon">
<input
onChange={handleUpload}
type="file"
className="tie-image-editor_tool-upload-input"
accept="image/*"
/>
</i>
</Popover>
</div>
);
};
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { fabric } from 'fabric';
import { useEffect, useRef, useState } from 'react';
import {
ACTION,
CURSOR,
IDownloadBody,
MENU_TYPE_ENUM,
WORK_SPACE_ID,
stayRemain,
} from '../constants';
interface IProps {
url: string;
}
/** 初始化图像编辑组件 */
export const useInit = ({ url }: IProps) => {
const canvasEl = useRef<HTMLCanvasElement>(null);
const imageInstanceRef = useRef<any>();
const canvasInstanceRef = useRef<any>();
const wrapperInstanceRef = useRef<HTMLDivElement>(null);
const [canvasIsRender, setCanvasIsRender] = useState(false);
/** 当前菜单 */
const [currentMenu, _setCurrentMenu] = useState(MENU_TYPE_ENUM.default);
const currentMenuRef = useRef(MENU_TYPE_ENUM.default);
/** canvas 初始化 */
const initCanvasJson = useRef<string>('{}');
/** 不应该保存历史 */
const unSaveHistory = useRef<boolean>(false);
/** 历史实例 */
const historyRef = useRef<{
updateCanvasState: (
action: ACTION,
remove: boolean,
append: boolean,
) => void;
}>({
updateCanvasState: () => {},
});
/** 下载 */
const downloadRef = useRef<{
downLoad: (auto?: boolean) => Promise<IDownloadBody>;
}>({
downLoad: () => Promise.resolve({}),
});
/** 缩放比例 */
const [zoom, setZoom] = useState(1);
/** 更新菜单 */
const setCurrentMenu = (newMenu: MENU_TYPE_ENUM) => {
_setCurrentMenu(newMenu);
currentMenuRef.current = newMenu;
const canvas = canvasInstanceRef.current;
if (!canvas) {
return;
}
/** 清空自由画状态 */
if (![MENU_TYPE_ENUM.draw, MENU_TYPE_ENUM.mosaic].includes(newMenu)) {
canvas.isDrawingMode = false;
}
/** 矩形 */
if (
[
MENU_TYPE_ENUM.rect,
MENU_TYPE_ENUM.circle,
MENU_TYPE_ENUM.arrow,
].includes(newMenu)
) {
canvas.setCursor(CURSOR.crosshair);
} else if (
[
MENU_TYPE_ENUM.rect,
MENU_TYPE_ENUM.circle,
MENU_TYPE_ENUM.arrow,
].includes(currentMenuRef.current)
) {
canvas.setCursor(CURSOR.default);
}
canvas.renderAll();
};
/** 初始化画布 */
const initCanvas = () => {
if (wrapperInstanceRef.current && canvasEl.current) {
try {
const { width, height } =
wrapperInstanceRef.current.getBoundingClientRect();
const canvas = new fabric.Canvas(canvasEl.current, {
containerClass: 'tie-image-editor_canvas',
// enableRetinaScaling: false,
// isDrawingMode: true,
// fireRightClick: true, // 启用右键,button的数字为3
// stopContextMenu: true, // 禁止默认右键菜单
// controlsAboveOverlay: true, // 超出clipPath后仍然展示控制条
width: width,
height: height,
});
canvasInstanceRef.current = canvas;
/** 通知其他组件,canvas已经实例化 */
setCanvasIsRender(true);
try {
/** 第一次初始化 */
initCanvasJson.current = JSON.stringify(canvas.toJSON(stayRemain));
} catch (error) {
console.error('initCanvasJson 初始化失败');
}
} catch (error) {
console.error(error);
}
}
};
/** 初始化背景图 */
const initBackGroundImage = (
imageUrl: string = '',
justBackground: boolean = false,
callback?: () => void,
) => {
const canvas = canvasInstanceRef.current;
if (imageUrl.trim() && canvas && wrapperInstanceRef.current) {
const { height } = wrapperInstanceRef.current.getBoundingClientRect();
imageInstanceRef.current = fabric.Image.fromURL(
imageUrl,
(oImg: any) => {
const center = canvas.getCenter();
const imgHeight = oImg.height;
/** 图片过大,使其大小正好跟容器一致 */
oImg.scale(height / imgHeight);
/** 使得图片在canvas中间 */
oImg.set({
left: center.left - (oImg.width * (height / imgHeight)) / 2,
top: center.top - (oImg.height * (height / imgHeight)) / 2,
});
/** 不让直接操作图片 */
oImg.selectable = false;
oImg.id = WORK_SPACE_ID;
canvas.setBackgroundImage(oImg, canvas.renderAll.bind(canvas));
/** 仅仅替换背景 */
if (justBackground) {
callback && callback();
return;
}
try {
/** 加载图片后再初始化一次 */
initCanvasJson.current = JSON.stringify(canvas.toJSON(stayRemain));
} catch (error) {
console.error('initCanvasJson 初始化失败');
}
callback && callback();
},
{
crossOrigin: 'anonymous',
},
);
}
};
useEffect(() => {
initCanvas();
}, [canvasEl, wrapperInstanceRef]);
useEffect(() => {
initBackGroundImage(url);
}, [url, canvasInstanceRef, wrapperInstanceRef]);
return {
canvasEl,
imageInstanceRef,
canvasInstanceRef,
wrapperInstanceRef,
currentMenu,
setCurrentMenu,
currentMenuRef,
canvasIsRender,
initCanvasJson,
unSaveHistory,
historyRef,
initBackGroundImage,
downloadRef,
zoom,
setZoom,
};
};
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import React from 'react';
import Popover from './Popover';
interface IProps {
children: React.ReactNode;
open?: boolean;
onChange?: (value: boolean) => void;
}
/** 旋转参数 */
const CropPop = ({ children, open = false, onChange }: IProps) => {
const handleChange = (value: boolean) => {
onChange && onChange(value);
};
return (
<Popover
content={
<div className="tie-image-editor_tool-crop-pop">
<div className="tie-image-editor_tool-item tie-image-editor_tool-done">
<i
className="tie-image-editor_icon"
onClick={() => handleChange(true)}
/>
</div>
<div className="tie-image-editor_tool-item tie-image-editor_tool-clear">
<i
className="tie-image-editor_icon"
onClick={() => handleChange(false)}
/>
</div>
</div>
}
placement="bottom"
open={open}
>
<div>{children}</div>
</Popover>
);
};
export default CropPop;
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import React, { useState } from 'react';
import { FLIP_TYPE } from '../../constants';
import Popover from './Popover';
interface IProps {
children: React.ReactNode;
open?: boolean;
onChange?: (type: FLIP_TYPE, value: boolean) => void;
}
/** 旋转参数 */
const FlipPop = ({ children, open = false, onChange }: IProps) => {
const [flipX, setFlipX] = useState(false);
const [flipY, setFlipY] = useState(false);
const handleAngelChange = (type: FLIP_TYPE, value: boolean) => {
if (type === FLIP_TYPE.flipX) {
setFlipX(value);
} else {
setFlipY(value);
}
onChange && onChange(type, value);
};
return (
<Popover
content={
<div className="tie-image-editor_tool-flip-pop">
<div
className="tie-image-editor_tool-item tie-image-editor_tool-flipX"
onClick={() => handleAngelChange(FLIP_TYPE.flipX, !flipX)}
>
<i className="tie-image-editor_icon" />
</div>
<div
className="tie-image-editor_tool-item tie-image-editor_tool-flipY"
onClick={() => handleAngelChange(FLIP_TYPE.flipY, !flipY)}
>
<i className="tie-image-editor_icon" />
</div>
</div>
}
placement="bottom"
open={open}
>
<div>{children}</div>
</Popover>
);
};
export default FlipPop;
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import React from 'react';
import { paintConfig } from '../../constants';
import Popover from './Popover';
interface IProps {
children: React.ReactNode;
open?: boolean;
onChange?: (value: number) => void;
}
/** 马赛克参数 */
const MosaicPop = ({ children, open = false, onChange }: IProps) => {
const handleSizeChange = (value: number) => {
onChange && onChange(value);
};
return (
<Popover
content={
<div className="tie-image-editor_tool-crop-pop">
<div className="tie-image-editor_tool-paint-size-wrapper">
{paintConfig.mosaicSizes.map((size, index) => (
<div
onClick={() => handleSizeChange(size)}
key={size}
className="tie-image-editor_tool-paint-size"
style={{ width: 10 + index * 5, height: 10 + index * 5 }}
></div>
))}
</div>
</div>
}
placement="bottom"
open={open}
>
<div>{children}</div>
</Popover>
);
};
export default MosaicPop;
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import React from 'react';
import { IPaintTypes, MENU_TYPE_ENUM, paintConfig } from '../../constants';
import Popover from './Popover';
interface IProps {
children: React.ReactNode;
open?: boolean;
onChange?: (type: IPaintTypes, value: number | string) => void;
type?: MENU_TYPE_ENUM;
}
const Paint = ({ children, open = false, type, onChange }: IProps) => {
const handleChange = (type: IPaintTypes, value: number | string) => {
onChange && onChange(type, value);
};
return (
<Popover
content={
<div className="tie-image-editor_tool-paint-content">
<div className="tie-image-editor_tool-paint-size-wrapper">
{(type === MENU_TYPE_ENUM.text
? paintConfig.fontSizes
: paintConfig.sizes
).map((size, index) => (
<div
onClick={() => handleChange(IPaintTypes.size, size)}
key={size}
className="tie-image-editor_tool-paint-size"
style={{ width: 10 + index * 5, height: 10 + index * 5 }}
></div>
))}
</div>
<div className="tie-image-editor_tool-paint-color-wrapper">
{paintConfig.colors.map((color) => (
<div
onClick={() => handleChange(IPaintTypes.color, color)}
key={color}
className="tie-image-editor_tool-paint-color"
style={{ background: color }}
></div>
))}
</div>
</div>
}
placement="bottom"
open={open}
>
<div>{children}</div>
</Popover>
);
};
export default Paint;
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import classNames from 'classnames';
import React, { SyntheticEvent, useEffect, useRef, useState } from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import { IDOMProps } from '../../constants';
import { removeNonHTMLProps } from '../../util';
import Portal from './Portal';
export type Placement =
| 'auto'
| 'auto-start'
| 'auto-end'
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'right'
| 'right-start'
| 'right-end'
| 'left'
| 'left-start'
| 'left-end';
interface IArrowProps {
arrowPlacement: string;
backgroundColor?: string;
children: React.ReactNode;
}
export interface IPopoverProps extends IDOMProps {
className?: string;
popoverClassName?: string;
style?: React.CSSProperties;
popoverStyle?: React.CSSProperties;
open?: boolean;
placement?: Placement;
positionFixed?: boolean;
children: React.ReactNode;
showArrow?: boolean;
usePortal?: boolean;
/** 自定义Portal容器节点 */
portalContainer?: Element;
content: React.ReactNode;
modifiers?: Array<Object>;
outlineColor?: string;
backgroundColor?: string;
trigger?: 'click' | 'focus' | 'hover';
visible?: boolean;
scheduleUpdate?: boolean;
getPopoverRef?: (ref: React.RefObject<HTMLDivElement>) => void;
onOpenChange?: (visible: boolean) => void;
/** 点击空白区域,自动关闭popover */
maskClosable?: boolean;
}
const ArrowWrapper = (props: IArrowProps) => {
return (
<div
className="popper__arrow"
style={{
['border' +
((props?.arrowPlacement[0] || '').toLocaleUpperCase() +
(props.arrowPlacement || '').substring(1)) +
'Color']: props.backgroundColor,
}}
>
{props.children}
</div>
);
};
const Popover = (props: IPopoverProps) => {
let popoverRef: React.RefObject<HTMLDivElement> = useRef(null);
let targetRef: React.RefObject<HTMLDivElement> = useRef(null);
const [open, setOpen] = useState<boolean | undefined>(props.open);
const {
className,
style,
children,
trigger,
visible,
popoverClassName,
popoverStyle,
showArrow,
content,
placement,
usePortal,
portalContainer,
positionFixed,
modifiers,
outlineColor,
backgroundColor,
scheduleUpdate,
onOpenChange,
maskClosable = true,
...restProps
} = props;
const domProps = removeNonHTMLProps(restProps, [
'popoverClassName',
'popoverStyle',
'showArrow',
'content',
'usePortal',
'positionFixed',
'outlineColor',
'backgroundColor',
'getPopoverRef',
'onOpenChange',
]);
const handleOpenChange = (openState: boolean) => {
if (onOpenChange) {
onOpenChange(openState);
}
if (props.open !== undefined) {
setOpen(props.open);
} else {
setOpen(openState);
}
};
const handleClick = (e: any) => {
if (!popoverRef || !targetRef) return;
if (
(popoverRef as any).current?.contains(e.target as HTMLDivElement) ||
(targetRef as any).current?.contains(e.target as HTMLDivElement)
) {
return;
}
if (maskClosable) {
handleOpenChange(false);
}
};
const mouseEnter = () => {
const { open, onOpenChange } = props;
if (open !== undefined && onOpenChange === undefined) return;
if (trigger === 'hover') {
handleOpenChange(true);
}
};
const mouseLeave = () => {
const { open, onOpenChange } = props;
if (open !== undefined && onOpenChange === undefined) return;
if (trigger === 'hover') {
handleOpenChange(false);
}
};
const handleVisiblePopover = (e: SyntheticEvent) => {
e.nativeEvent.stopImmediatePropagation();
if (trigger === 'click') {
handleOpenChange(!open);
return;
}
if (trigger === 'focus') {
handleOpenChange(true);
}
};
useEffect(() => {
if (props.getPopoverRef) {
props.getPopoverRef(popoverRef);
}
}, []);
useEffect(() => {
if (trigger === 'click' || trigger === 'focus') {
document.body.addEventListener('click', handleClick, false);
}
return () => {
document.body.removeEventListener('click', handleClick, false);
};
}, [maskClosable, props.open]);
useEffect(() => {
if (props.open !== undefined) {
handleOpenChange(props.open);
}
}, [props.open]);
/**
* 渲染 popover 弹层
*/
const renderPopper = () => {
const strategy = positionFixed ? 'fixed' : 'absolute';
const defaultModifiers = [
{
name: 'computeStyles',
options: {
gpuAcceleration: false, // true by default
},
},
{
name: 'offset',
options: {
offset: [0, 10],
},
},
];
// 获取(placement)方向并把首字母设置成大写
const direction = (placement?: string) => {
if (placement) {
return placement
.split('-')[0]
.toString()
.replace(/^\S/, (s) => s.toUpperCase());
}
};
// 设置popover的边框和(arrow)三角形的边框颜色
const getOutlineColor = (type: string) => {
if (type === 'content' && outlineColor) {
return {
borderColor: outlineColor,
};
}
if (type === 'arrow' && outlineColor) {
return {
[`border${direction(placement)}Color`]: outlineColor,
};
}
};
// 设置popover的背景
const getBackgroundColor = () => {
if (backgroundColor) {
return {
color: '#fff',
background: backgroundColor,
// border: 'none',
};
}
};
// 动态设置三角的背景色
const arrowPlacement = placement!.split('-')[0];
const popperContent = open && (
<Popper
placement={placement}
modifiers={[...defaultModifiers, ...(modifiers ? modifiers : [])]}
strategy={strategy}
>
{({ ref, style, placement, arrowProps, update }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (open) {
update();
}
}, [scheduleUpdate]);
return (
<div
className={classNames('tie-popover__popper', popoverClassName, {
'tie-popover__popper-background': backgroundColor,
})}
style={{
...style,
...popoverStyle,
...getOutlineColor('content'),
...getBackgroundColor(),
}}
ref={ref}
// eslint-disable-next-line react/no-unknown-property
x-placement={placement}
onMouseEnter={mouseEnter}
onMouseLeave={mouseLeave}
>
<div ref={popoverRef}>
{open && (
<div className="tie-popover__content">
<span>{content}</span>
</div>
)}
{showArrow && (
<ArrowWrapper
arrowPlacement={arrowPlacement}
backgroundColor={backgroundColor}
>
<div
data-popper-arrow
className="tie-popper__arrow popper__arrow"
// eslint-disable-next-line react/no-unknown-property
x-arrow="true"
style={{
...arrowProps.style,
...getOutlineColor('arrow'),
}}
ref={arrowProps.ref}
/>
</ArrowWrapper>
)}
</div>
</div>
);
}}
</Popper>
);
return usePortal ? (
<Portal container={portalContainer}>{popperContent}</Portal>
) : (
popperContent
);
};
// 解决children为Button元素且禁用的时候鼠标事件失效的问题
const getDisabledCompatibleChildren = (element: any) => {
if (element.type.__BRICKS_BUTTON && element.props.disabled) {
const displayStyle =
element.props.style && element.props.style.display
? element.props.style.display
: 'inline-block';
const child = React.cloneElement(element, {
style: {
...element.props.style,
pointerEvents: 'none',
},
});
return (
<span style={{ display: displayStyle, cursor: 'not-allowed' }}>
{child}
</span>
);
}
return element;
};
return (
<>
{visible ? (
<div
className={classNames('tie-popover', className)}
{...(domProps as IDOMProps)}
style={style}
>
<Manager>
<Reference>
{({ ref }) => (
<div ref={targetRef} className="tie-popover__target">
<div ref={ref}>
{React.Children.map(children, (child) => {
let ele = child as React.ReactElement;
ele = getDisabledCompatibleChildren(ele);
if (trigger === 'click') {
return React.cloneElement(ele, {
onClick: (e: SyntheticEvent) => {
handleVisiblePopover(e);
ele.props.onClick && ele.props.onClick();
},
});
}
if (trigger === 'focus') {
return React.cloneElement(ele, {
onFocus: (e: SyntheticEvent) => {
handleVisiblePopover(e);
ele.props.onFocus && ele.props.onFocus();
},
});
}
if (trigger === 'hover') {
return React.cloneElement(ele, {
onMouseEnter: () => {
mouseEnter();
ele.props.onMouseEnter && ele.props.onMouseEnter();
},
onMouseLeave: () => {
mouseLeave();
ele.props.onMouseLeave && ele.props.onMouseLeave();
},
});
}
return ele;
})}
</div>
</div>
)}
</Reference>
{renderPopper()}
</Manager>
</div>
) : (
children
)}
</>
);
};
Popover.defaultProps = {
visible: true,
trigger: 'hover',
placement: 'bottom',
showArrow: true,
usePortal: true,
modifiers: [],
};
export default Popover;
import React from 'react';
import ReactDOM from 'react-dom';
export interface IPortalProps {
container?: Element;
children?: React.ReactNode;
onRendered?: () => void;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export default class Portal extends React.Component<IPortalProps, {}> {
public container: null | Element = null;
constructor(props: IPortalProps) {
super(props);
this.container = null;
this.getContainer = this.getContainer.bind(this);
}
public componentDidMount() {
this.container = this.props.container || document.body;
this.forceUpdate(() => {
if (this.props.onRendered) {
this.props.onRendered();
}
});
}
public componentDidUpdate(prevProps: IPortalProps) {
if (prevProps.container !== this.props.container) {
this.container = this.props.container || null;
}
}
public getContainer() {
return this.container;
}
public render() {
const { children } = this.props;
return this.container && children
? ReactDOM.createPortal(children, this.container)
: null;
}
}
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
import React, { useState } from 'react';
import { paintConfig } from '../../constants';
import Popover from './Popover';
interface IProps {
children: React.ReactNode;
open?: boolean;
onChange?: (value: number | number[]) => void;
}
/** 旋转参数 */
const RotatePop = ({ children, open = false, onChange }: IProps) => {
const [angel, setAngel] = useState(paintConfig.rotates[0]);
const handleAngelChange = (value: number | number[]) => {
setAngel(+value);
onChange && onChange(+value);
};
return (
<Popover
content={
<div className="tie-image-editor_tool-rotate-pop-content">
<Slider
min={paintConfig.rotates[0]}
max={paintConfig.rotates[paintConfig.rotates.length - 1]}
onChange={handleAngelChange}
value={angel}
marks={paintConfig.rotates.reduce(
(pre, cur) => ({ ...pre, [cur]: cur + '°' }),
{},
)}
/>
</div>
}
placement="bottom-end"
open={open}
>
<div>{children}</div>
</Popover>
);
};
export default RotatePop;
/* eslint-disable react/button-has-type */
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from 'react';
import Popover from './Popover';
interface IProps {
children: React.ReactNode;
open?: boolean;
zoom?: number;
resetText?:string;
onScaleUp?: () => void;
onScaleDown?: () => void;
onReset?: () => void;
onOpenChange?: (open: boolean) => void;
}
/** 缩放参数 */
const ScalePop = ({
children,
open = false,
zoom = 1,
resetText="",
onScaleUp,
onScaleDown,
onReset,
onOpenChange,
}: IProps) => {
return (
<Popover
content={
<div className="tie-image-editor_tool-scale-pop-content">
<div className="tie-image-editor_tool-scale-pop-zoom">
{+(zoom * 100).toFixed(0)}%
</div>
<div className="tie-image-editor_tool-scale-pop-icons">
<div className="tie-image-editor_tool-item tie-image-editor_tool-subtract">
<i
className="tie-image-editor_tool-scale-pop-icon tie-image-editor_icon"
onClick={onScaleDown}
/>
</div>
<div className="tie-image-editor_tool-item tie-image-editor_tool-add">
<i
className="tie-image-editor_tool-scale-pop-icon tie-image-editor_icon"
onClick={onScaleUp}
/>
</div>
</div>
<button onClick={onReset}>{resetText}</button>
</div>
}
placement="bottom-end"
open={open}
showArrow={false}
trigger="click"
onOpenChange={onOpenChange}
>
<div>{children}</div>
</Popover>
);
};
export default ScalePop;
export const eventNames = {
OBJECT_ACTIVATED: 'objectActivated',
OBJECT_MOVED: 'objectMoved',
OBJECT_SCALED: 'objectScaled',
OBJECT_CREATED: 'objectCreated',
TEXT_EDITING: 'textEditing',
TEXT_CHANGED: 'textChanged',
ICON_CREATE_RESIZE: 'iconCreateResize',
ICON_CREATE_END: 'iconCreateEnd',
ADD_TEXT: 'addText',
ADD_OBJECT: 'addObject',
ADD_OBJECT_AFTER: 'addObjectAfter',
MOUSE_DOWN: 'mousedown',
MOUSE_UP: 'mouseup',
MOUSE_MOVE: 'mousemove',
// UNDO/REDO Events
REDO_STACK_CHANGED: 'redoStackChanged',
UNDO_STACK_CHANGED: 'undoStackChanged',
SELECTION_CLEARED: 'selectionCleared',
SELECTION_CREATED: 'selectionCreated',
};
/** 菜单 */
export enum MENU_TYPE_ENUM {
crop = 'crop',
history = 'history',
download = 'download',
draw = 'draw',
flip = 'flip',
reset = 'reset',
rotate = 'rotate',
rect = 'rect',
circle = 'circle',
text = 'text',
upload = 'upload',
drag = 'drag',
scale = 'scale',
arrow = 'arrow',
mosaic = 'mosaic',
undo = 'undo',
redo = 'redo',
default = '',
}
export enum LANG {
zh = 'zh',
en = 'en',
}
export const MENU_TYPE_TEXT: {
[key: string]: { [LANG.zh]: string; [LANG.en]: string };
} = {
[MENU_TYPE_ENUM.crop]: { [LANG.zh]: '裁剪', [LANG.en]: MENU_TYPE_ENUM.crop },
[MENU_TYPE_ENUM.history]: {
[LANG.zh]: '历史记录',
[LANG.en]: MENU_TYPE_ENUM.history,
},
[MENU_TYPE_ENUM.download]: {
[LANG.zh]: '下载',
[LANG.en]: MENU_TYPE_ENUM.download,
},
[MENU_TYPE_ENUM.draw]: { [LANG.zh]: '画笔', [LANG.en]: MENU_TYPE_ENUM.draw },
[MENU_TYPE_ENUM.flip]: { [LANG.zh]: '翻转', [LANG.en]: MENU_TYPE_ENUM.flip },
[MENU_TYPE_ENUM.reset]: {
[LANG.zh]: '重置',
[LANG.en]: MENU_TYPE_ENUM.reset,
},
[MENU_TYPE_ENUM.rotate]: {
[LANG.zh]: '旋转',
[LANG.en]: MENU_TYPE_ENUM.rotate,
},
[MENU_TYPE_ENUM.rect]: { [LANG.zh]: '矩形', [LANG.en]: MENU_TYPE_ENUM.rect },
[MENU_TYPE_ENUM.circle]: {
[LANG.zh]: '圆形',
[LANG.en]: MENU_TYPE_ENUM.circle,
},
[MENU_TYPE_ENUM.text]: { [LANG.zh]: '文本', [LANG.en]: MENU_TYPE_ENUM.text },
[MENU_TYPE_ENUM.upload]: {
[LANG.zh]: '上传',
[LANG.en]: MENU_TYPE_ENUM.upload,
},
[MENU_TYPE_ENUM.drag]: { [LANG.zh]: '拖拽', [LANG.en]: MENU_TYPE_ENUM.drag },
[MENU_TYPE_ENUM.scale]: {
[LANG.zh]: '缩放',
[LANG.en]: MENU_TYPE_ENUM.scale,
},
[MENU_TYPE_ENUM.arrow]: {
[LANG.zh]: '箭头',
[LANG.en]: MENU_TYPE_ENUM.arrow,
},
[MENU_TYPE_ENUM.mosaic]: {
[LANG.zh]: '马赛克',
[LANG.en]: MENU_TYPE_ENUM.mosaic,
},
[MENU_TYPE_ENUM.redo]: { [LANG.zh]: '前进', [LANG.en]: MENU_TYPE_ENUM.redo },
[MENU_TYPE_ENUM.undo]: { [LANG.zh]: '后退', [LANG.en]: MENU_TYPE_ENUM.undo },
};
/** 鼠标形状 */
export enum CURSOR {
default = 'default',
crosshair = 'crosshair',
pointer = 'pointer',
move = 'move',
grab = 'grab',
grabbing = 'grabbing',
zoomIn = 'zoom-in',
zoomOut = 'zoom-out',
}
export enum ACTION {
add = 'add',
modified = 'modified',
delete = 'delete',
}
export const ACTION_TEXT: {
[key: string]: { [LANG.zh]: string; [LANG.en]: string };
} = {
[ACTION.add]: {
[LANG.zh]: '添加',
[LANG.en]: ACTION.add,
},
[ACTION.modified]: {
[LANG.zh]: '修改',
[LANG.en]: ACTION.modified,
},
[ACTION.delete]: { [LANG.zh]: '删除', [LANG.en]: ACTION.delete },
placeholder: { [LANG.zh]: '暂无历史', [LANG.en]: 'no logging' },
init: { [LANG.zh]: '初始化', [LANG.en]: 'init' },
title: { [LANG.zh]: '历史', [LANG.en]: 'history' },
};
export enum FLIP_TYPE {
flipX = 'flipX',
flipY = 'flipY',
}
export const WORK_SPACE_ID = 'workspace';
export enum IPaintTypes {
size = 'size', //尺寸
color = 'color', //颜色
}
/** 一些绘画配置 */
export const paintConfig = {
sizes: [5, 10, 20], //通用画笔尺寸
colors: ['red', 'orange', 'yellow', 'green', 'blue', 'white'], //通用画笔颜色
fontSizes: [30, 60, 90], //通用文字颜色
rotates: [0, 45, 90, 135, 180, 225, 270, 315, 360], //通用旋转角度
mosaicSizes: [10, 20, 30], //马赛克大小
};
/** 下载函数参数 */
export interface IDownloadBody {
downLoadUrl?: string;
}
export interface IEditorProps {
url?: string;
style?: React.CSSProperties;
menus?: MENU_TYPE_ENUM[];
lang?: LANG;
onDownLoad?: (param: IDownloadBody) => void;
}
export const keyCodes = {
Z: 90,
Y: 89,
SHIFT: 16,
BACKSPACE: 8,
DEL: 46,
};
export const IMAGE_NAME = 'tie_image_editor';
export const DEFAULT_DIMENSION = 8;
/** 前进和回退时保留的属性 */
export const stayRemain = [
'id',
'gradientAngle',
'selectable',
'hasControls',
'source',
'editable',
];
/** 最大历史长度 */
export const MAX_HISTORY_LEN = 30;
/** 空值 */
export const EMPTY_STR = '';
export const EMPTY_ARR = [];
/** 原生dom事件的interface */
export type IDOMProps = React.DOMAttributes<HTMLElement>;
/** 通用的需要从 props 过滤掉的不属于 dom 的属性 */
export const INVALID_PROPS = [
'onChange',
'loadData',
'onClick',
'onSelect',
'options',
'name',
'value',
'defaultValue',
'values',
'defaultValues',
'placeholder',
'large',
'small',
'iconRender',
'icon',
'activeIndex',
'swipeAble',
'fixedArrows',
'error',
'onVisibleChange',
];
/* eslint-disable no-param-reassign */
import { fabric } from 'fabric';
/** 自定义箭头 */
const Arrow = fabric.util.createClass(fabric.Line, {
type: 'arrow',
superType: 'drawing',
initialize(points: any, options: any) {
if (!points) {
const { x1, x2, y1, y2 } = options;
points = [x1, y1, x2, y2];
}
options = options || {};
this.callSuper('initialize', points, options);
},
_render(ctx: any) {
this.callSuper('_render', ctx);
ctx.save();
const xDiff = this.x2 - this.x1;
const yDiff = this.y2 - this.y1;
const angle = Math.atan2(yDiff, xDiff);
ctx.translate((this.x2 - this.x1) / 2, (this.y2 - this.y1) / 2);
ctx.rotate(angle);
ctx.beginPath();
/** 箭头上的三角形大小 */
ctx.moveTo(this.strokeWidth * 2, 0);
ctx.lineTo(-this.strokeWidth * 2, this.strokeWidth * 2);
ctx.lineTo(-this.strokeWidth * 2, -this.strokeWidth * 2);
ctx.closePath();
ctx.fillStyle = this.stroke;
ctx.fill();
ctx.restore();
},
});
Arrow.fromObject = (options: any, callback: any) => {
const { x1, x2, y1, y2 } = options;
return callback(new (fabric as any).Arrow([x1, y1, x2, y2], options));
};
export default Arrow;
此差异已折叠。
export * from './Editor';
export * from './constants';
import '../styles/index.scss';
import React from 'react';
import {
ACTION,
IEditorProps,
INVALID_PROPS,
MENU_TYPE_ENUM,
} from './constants';
export interface IEditorContextProps extends IEditorProps {
canvasInstanceRef: React.MutableRefObject<any>;
wrapperInstanceRef: React.MutableRefObject<any>;
currentMenuRef: React.MutableRefObject<MENU_TYPE_ENUM>;
currentMenu: MENU_TYPE_ENUM;
setCurrentMenu: (value: MENU_TYPE_ENUM) => void;
canvasIsRender: boolean;
canvasEl: React.MutableRefObject<any>;
initCanvasJson: React.MutableRefObject<any>;
unSaveHistory: React.MutableRefObject<any>;
historyRef: React.MutableRefObject<any>;
zoom: number;
setZoom: (zoom: number) => void;
initBackGroundImage: (
imageUrl: string,
justBackground: boolean,
callback?: () => void,
) => void;
}
export const EditorContext = React.createContext<IEditorContextProps>({
canvasInstanceRef: { current: null },
wrapperInstanceRef: { current: null },
currentMenuRef: { current: MENU_TYPE_ENUM.default },
currentMenu: MENU_TYPE_ENUM.default,
setCurrentMenu: () => {},
canvasIsRender: false,
canvasEl: { current: null },
initCanvasJson: { current: null },
unSaveHistory: { current: null },
historyRef: { current: () => {} },
initBackGroundImage: () => {},
zoom: 1,
setZoom: () => {},
});
export interface IHistory {
id: string; //唯一标识
data: string;
type: MENU_TYPE_ENUM;
action: ACTION;
}
export const isSupportFileApi = () => {
return !!(window.File && window.FileList && window.FileReader);
};
export const base64ToBlob = (data: any) => {
const rImageType = /data:(image\/.+);base64,/;
let mimeString = '';
let raw, uInt8Array, i;
raw = data.replace(rImageType, (header: any, imageType: any) => {
mimeString = imageType;
return '';
});
raw = atob(raw);
const rawLength = raw.length;
uInt8Array = new Uint8Array(rawLength); // eslint-disable-line
for (i = 0; i < rawLength; i += 1) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: mimeString });
};
export const clamp = (value: number, minValue: number, maxValue: number) => {
if (minValue > maxValue) {
// eslint-disable-next-line no-param-reassign
[minValue, maxValue] = [maxValue, minValue];
}
return Math.max(minValue, Math.min(value, maxValue));
};
export const omit = <T extends { [key: string]: any }>(
object: T,
array: string[],
): T => {
// const mapObject = { ...object };
const mapObject = Object.assign({}, object);
array.forEach((key: string) => {
delete mapObject[key];
});
return mapObject;
};
/**
* 移除非 Dom 属性的 props 的值
* @param props 需要过滤 props 的属性
* @param {string[]} invalidProps 需要从 props 过滤掉的不属于 dom 的属性.
*/
export function removeNonHTMLProps(
props: { [key: string]: any },
invalidProps: string[] = [],
): { [key: string]: any } {
// eslint-disable-next-line no-param-reassign
invalidProps = invalidProps.concat(INVALID_PROPS);
return omit(props, invalidProps);
}
/**获取IE浏览器版本 */
export const getIEVersion = () => {
if (navigator) {
let userAgent = navigator.userAgent;
const ie11 =
userAgent.indexOf('Trident') > -1 && userAgent.indexOf('rv:11.0') > -1;
if (ie11) {
return 11;
}
const versionArr = navigator.appVersion.split(';')[1];
if (!versionArr) {
return;
}
const version = versionArr.replace(/[ ]/g, '');
const result = /MSIE(\d+)\./.exec(version);
if (result) {
return parseInt(result[1]);
}
}
};
此差异已折叠。
{
"compilerOptions": {
"strict": true,
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"jsx": "react",
"baseUrl": "./",
"paths": {
"@@/*": [".dumi/tmp/*"],
"tiny-image-editor": ["src"],
"tiny-image-editor/*": ["src/*", "*"]
}
},
"include": [".dumirc.ts", "src/**/*.ts", "src/**/*.tsx"]
}
{
"compilerOptions": {
"strict": true,
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"jsx": "react",
"baseUrl": "./",
"paths": {
"@@/*": [".dumi/tmp/*"],
"tiny-image-editor": ["src"],
"tiny-image-editor/*": ["src/*", "*"]
},
},
"include": [".dumirc.ts", "src/**/*.ts", "src/**/*.tsx"]
}
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册