├── .env.sample
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── craco.config.js
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── assets
│ └── images
│ │ └── base-color-picker.png
├── components
│ ├── Canvas
│ │ ├── Canvas.tsx
│ │ ├── CanvasContext.tsx
│ │ ├── CanvasObjects.ts
│ │ ├── constants
│ │ │ └── contants.ts
│ │ ├── handlers
│ │ │ ├── index.ts
│ │ │ ├── useContainerHandler.ts
│ │ │ ├── useCoreHandler.ts
│ │ │ ├── useCustomizationHandler.ts
│ │ │ ├── useEventsHandler.ts
│ │ │ ├── useGuidelinesHandler.ts
│ │ │ └── useZoomHandler.ts
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ └── useCanvasContext.ts
│ │ ├── index.ts
│ │ └── utils
│ │ │ ├── drawer.ts
│ │ │ ├── index.ts
│ │ │ └── keyboard.ts
│ └── Editor
│ │ ├── CanvasArea
│ │ ├── CanvasArea.scss
│ │ └── CanvasArea.tsx
│ │ ├── Editor.scss
│ │ ├── Editor.tsx
│ │ ├── FooterMenu
│ │ ├── FooterMenu.scss
│ │ └── FooterMenu.tsx
│ │ ├── Navbar
│ │ ├── Navbar.scss
│ │ ├── Navbar.tsx
│ │ └── NavbarIcons.tsx
│ │ ├── Panels
│ │ ├── ClosePanel.tsx
│ │ ├── ImagesPanel
│ │ │ └── ImagesPanel.tsx
│ │ ├── MusicPanel
│ │ │ └── MusicPanel.tsx
│ │ ├── ObjectsPanel
│ │ │ └── ObjectsPanel.tsx
│ │ ├── PanelItem
│ │ │ ├── PanelItem.scss
│ │ │ └── PanelItem.tsx
│ │ ├── Panels.scss
│ │ ├── Panels.tsx
│ │ ├── PanelsList
│ │ │ ├── PanelItemIcon.tsx
│ │ │ ├── PanelsList.scss
│ │ │ ├── PanelsList.tsx
│ │ │ ├── PanelsListItem.tsx
│ │ │ └── tabItems.ts
│ │ ├── TemplatesPanel
│ │ │ └── TemplatesPanel.tsx
│ │ ├── TextPanel
│ │ │ └── TextPanel.tsx
│ │ └── VideosPanel
│ │ │ └── VideosPanel.tsx
│ │ ├── Toolbox
│ │ ├── DefaultToolbox
│ │ │ ├── DefaultToobox.scss
│ │ │ └── DefaultToolbox.tsx
│ │ ├── TextToolbox
│ │ │ ├── TextToolbox.scss
│ │ │ └── TextToolbox.tsx
│ │ ├── Toolbox.scss
│ │ └── Toolbox.tsx
│ │ └── index.ts
├── i18n
│ ├── i18nClient.ts
│ ├── index.ts
│ └── locales
│ │ ├── en-EN.json
│ │ └── tr-TR.json
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
├── services
│ └── iconscout.ts
├── setupTests.ts
└── theme
│ └── colors.scss
├── tsconfig.json
├── tsconfigExtra.json
└── yarn.lock
/.env.sample:
--------------------------------------------------------------------------------
1 | REACT_APP_ICONSCOUT_CLIENT_ID="client-id"
2 | REACT_APP_ICONSCOUT_SECRET="client-secret"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "tabWidth": 2,
5 | "printWidth": 110,
6 | "arrowParens": "avoid"
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 xorb
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
19 | OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Design Editor
2 |
3 | React design editor using FabricJS. Create images in React, draw diagrams and arrange compositions using the image editor and save the result to one of several export formats, provides functionality similar to canva.com.
4 |
5 | 
6 |
7 | ## Features
8 |
9 | - [x] Add, remove, resize, reorder, clone, copy/paste objects
10 | - [x] Group/ungroup objects
11 | - [x] Zoom/pan canvas
12 | - [ ] Import and export to JSON or image
13 | - [ ] Context menu
14 | - [ ] Animation support, with Fade / Bounce / Shake / Scaling / Rotation / Flash effects
15 | - [ ] Multiple interation modes: grasp, selection, ctrl + drag grab
16 | - [x] Undo/Redo support
17 | - [x] Guidelines support
18 | - [ ] Multiple canvas support
19 | - [x] Preview mode
20 |
21 | ## How to start
22 |
23 | NodeJS required. Start in development mode using the following commands.
24 |
25 | ```sh
26 | # install dependencies
27 | yarn install
28 | # start development server
29 | yarn start
30 | ```
31 |
32 | Web application service will start running at `localhost:3000`
33 |
34 | ## Integrations
35 |
36 | In order to provide rich content, the following integrations are implemented.
37 |
38 | ### Iconscout
39 |
40 | Illusatrions and icons provider. Add credentials to `.env` file.
41 |
42 | ```sh
43 | ICONSCOUT_CLIENT_ID="your-client-id"
44 | ICONSCOUT_SECRET="your-secret"
45 | ```
46 |
47 | Currently, this values are being included in the repository. In the furure, you will require to add your own credentials.
48 |
49 | ## Contribution
50 |
51 | Feel free to contribute by opening issues with any questions, bug reports or feature requests.
52 |
53 | ## Author
54 |
55 | Created and maintained by Dany Boza ([@xorbmoon](https://twitter.com/xorbmoon)).
56 |
57 | ## License
58 |
59 | [MIT](LICENSE)
60 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const resloveSrc = (...paths) => path.join(__dirname, 'src', ...paths)
4 |
5 | module.exports = {
6 | webpack: {
7 | alias: {
8 | '@': resloveSrc(),
9 | '@assets': resloveSrc('assets'),
10 | '@components': resloveSrc('components'),
11 | '@scenes': resloveSrc('scenes'),
12 | '@store': resloveSrc('store'),
13 | '@services': resloveSrc('services'),
14 | '@utils': resloveSrc('utils'),
15 | },
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-design-editor",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@chakra-ui/icons": "^1.0.12",
7 | "@chakra-ui/react": "^1.6.0",
8 | "@emotion/react": "^11",
9 | "@emotion/styled": "^11",
10 | "axios": "^0.21.1",
11 | "classnames": "^2.3.1",
12 | "craco": "^0.0.3",
13 | "fabric": "^4.4.0",
14 | "focus-visible": "^5.2.0",
15 | "framer-motion": "^4",
16 | "i18next": "^20.2.2",
17 | "lodash": "^4.17.21",
18 | "nanoid": "^3.1.22",
19 | "node-sass": "^5.0.0",
20 | "react": "^17.0.2",
21 | "react-color": "^2.19.3",
22 | "react-custom-scrollbars": "^4.2.1",
23 | "react-dom": "^17.0.2",
24 | "react-scripts": "4.0.3",
25 | "use-debounce": "^6.0.1",
26 | "web-vitals": "^1.0.1"
27 | },
28 | "scripts": {
29 | "start": "craco start",
30 | "build": "craco build",
31 | "test": "craco test",
32 | "eject": "craco eject",
33 | "format": "prettier --write src"
34 | },
35 | "eslintConfig": {
36 | "extends": [
37 | "react-app",
38 | "react-app/jest"
39 | ]
40 | },
41 | "browserslist": {
42 | "production": [
43 | ">0.2%",
44 | "not dead",
45 | "not op_mini all"
46 | ],
47 | "development": [
48 | "last 1 chrome version",
49 | "last 1 firefox version",
50 | "last 1 safari version"
51 | ]
52 | },
53 | "devDependencies": {
54 | "@testing-library/jest-dom": "^5.11.4",
55 | "@testing-library/react": "^11.1.0",
56 | "@testing-library/user-event": "^12.1.10",
57 | "@types/fabric": "^4.2.5",
58 | "@types/jest": "^26.0.15",
59 | "@types/node": "^12.0.0",
60 | "@types/react": "^17.0.0",
61 | "@types/react-color": "^3.0.4",
62 | "@types/react-dom": "^17.0.0",
63 | "prettier": "^2.2.1",
64 | "typescript": "^4.1.2"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bharathreddyza/react-design-editor/b55c3fa87f295bf060f5bf020cbb1df1e816d3e5/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
23 |
24 |
25 |
29 |
30 |
39 | React Design Editor
40 |
41 |
42 |
43 |
44 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bharathreddyza/react-design-editor/b55c3fa87f295bf060f5bf020cbb1df1e816d3e5/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bharathreddyza/react-design-editor/b55c3fa87f295bf060f5bf020cbb1df1e816d3e5/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/assets/images/base-color-picker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bharathreddyza/react-design-editor/b55c3fa87f295bf060f5bf020cbb1df1e816d3e5/src/assets/images/base-color-picker.png
--------------------------------------------------------------------------------
/src/components/Canvas/Canvas.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useCanvasContext } from '@components/Canvas/hooks'
3 | import { fabric } from 'fabric'
4 | import {
5 | useCustomizationHandler,
6 | useEventsHandler,
7 | useZoomHandler,
8 | useContainerHandler,
9 | useGuidelinesHandler,
10 | } from '@components/Canvas/handlers'
11 |
12 | function Canvas() {
13 | const containerRef = useContainerHandler()
14 | const { setCanvas } = useCanvasContext()
15 | useCustomizationHandler()
16 | useGuidelinesHandler()
17 | useEventsHandler()
18 | useZoomHandler()
19 | useEffect(() => {
20 | const initialHeigh = containerRef.current.clientHeight
21 | const initialWidth = containerRef.current.clientWidth
22 |
23 | const canvas = new fabric.Canvas('canvas', {
24 | backgroundColor: '#ecf0f1',
25 | height: initialHeigh,
26 | width: initialWidth,
27 | })
28 |
29 | setCanvas(canvas)
30 | const workarea = new fabric.Rect({
31 | //@ts-ignore
32 | id: 'workarea',
33 | width: 600,
34 | height: 400,
35 | absolutePositioned: true,
36 | fill: '#ffffff',
37 | selectable: false,
38 | hoverCursor: 'default',
39 | })
40 | canvas.add(workarea)
41 | workarea.center()
42 | }, [])
43 | return (
44 |
45 |
46 |
47 | )
48 | }
49 |
50 | export default Canvas
51 |
--------------------------------------------------------------------------------
/src/components/Canvas/CanvasContext.tsx:
--------------------------------------------------------------------------------
1 | import { FC, createContext, useState } from 'react'
2 | import { fabric } from 'fabric'
3 |
4 | interface CanvasContext {
5 | zoomRatio: number
6 | setZoomRatio: (value: number) => void
7 | canvas: fabric.Canvas | null
8 | setCanvas: (canvas: fabric.Canvas) => void
9 | activeObject: fabric.Object | null
10 | setActiveObject: (object: fabric.Object | null) => void
11 | }
12 |
13 | export const Context = createContext({
14 | zoomRatio: 1,
15 | setZoomRatio: () => {},
16 | canvas: null,
17 | setCanvas: () => {},
18 | activeObject: null,
19 | setActiveObject: () => {},
20 | })
21 |
22 | export const CanvasProvider: FC = ({ children }) => {
23 | const [canvas, setCanvas] = useState(null)
24 | const [activeObject, setActiveObject] = useState(null)
25 | const [zoomRatio, setZoomRatio] = useState(1)
26 | const context = { canvas, setCanvas, activeObject, setActiveObject, zoomRatio, setZoomRatio }
27 |
28 | return {children}
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Canvas/CanvasObjects.ts:
--------------------------------------------------------------------------------
1 | import { fabric } from 'fabric'
2 |
3 | export const CanvasObjects = {
4 | text: {
5 | render: options => {
6 | const { text, ...textOptions } = options
7 | return new fabric.Textbox(text, textOptions)
8 | },
9 | },
10 | image: {
11 | render: options => {
12 | return new fabric.Image()
13 | },
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Canvas/constants/contants.ts:
--------------------------------------------------------------------------------
1 | export const propertiesToInclude = ['id', 'selectable']
2 |
--------------------------------------------------------------------------------
/src/components/Canvas/handlers/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useCustomizationHandler } from './useCustomizationHandler'
2 | export { default as useEventsHandler } from './useEventsHandler'
3 | export { default as useZoomHandler } from './useZoomHandler'
4 | export { default as useCoreHandler } from './useCoreHandler'
5 | export { default as useContainerHandler } from './useContainerHandler'
6 | export { default as useGuidelinesHandler } from './useGuidelinesHandler'
7 |
--------------------------------------------------------------------------------
/src/components/Canvas/handlers/useContainerHandler.ts:
--------------------------------------------------------------------------------
1 | import { useCanvasContext } from '@components/Canvas/hooks'
2 | import { createRef, useCallback, useEffect } from 'react'
3 |
4 | function useContainerHandler() {
5 | const containerRef = createRef()
6 | const { canvas } = useCanvasContext()
7 | const updateCanvasSize = useCallback(
8 | (x, y) => {
9 | if (canvas) {
10 | canvas.setHeight(y).setWidth(x)
11 | canvas.renderAll()
12 | // @ts-ignore
13 | const workarea = canvas.getObjects().find(obj => obj.id === 'workarea')
14 | if (workarea) {
15 | workarea.center()
16 | }
17 | }
18 | },
19 | [canvas]
20 | )
21 | useEffect(() => {
22 | const containerWidth = containerRef.current.clientWidth
23 | const containerHeight = containerRef.current.clientHeight
24 | updateCanvasSize(containerWidth, containerHeight)
25 | // eslint-disable-next-line react-hooks/exhaustive-deps
26 | }, [canvas])
27 |
28 | return containerRef
29 | }
30 |
31 | export default useContainerHandler
32 |
--------------------------------------------------------------------------------
/src/components/Canvas/handlers/useCoreHandler.ts:
--------------------------------------------------------------------------------
1 | import { useCanvasContext } from '@components/Canvas/hooks'
2 | import { useCallback } from 'react'
3 | import { CanvasObjects } from '@components/Canvas'
4 | import { propertiesToInclude } from '../constants/contants'
5 |
6 | function useCoreHandler() {
7 | const { canvas, activeObject } = useCanvasContext()
8 |
9 | // Add objects to canvas
10 | const addObject = useCallback(
11 | options => {
12 | const { type, ...textOptions } = options
13 | const element = CanvasObjects[type].render(textOptions)
14 | //@ts-ignore
15 | const workarea = canvas.getObjects().find(obj => obj.id === 'workarea')
16 | canvas.add(element)
17 | element.center()
18 |
19 | element.clipPath = workarea
20 | canvas.renderAll()
21 | },
22 | [canvas]
23 | )
24 |
25 | // Update properties, optional set metadata if present
26 | const setProperty = useCallback(
27 | (property, value) => {
28 | if (activeObject) {
29 | activeObject.set(property, value)
30 | activeObject.setCoords()
31 | canvas.requestRenderAll()
32 | }
33 | },
34 | [activeObject, canvas]
35 | )
36 |
37 | const exportJSON = useCallback(() => {
38 | const json = canvas.toJSON(propertiesToInclude)
39 | return json
40 | }, [canvas])
41 |
42 | const loadJSON = useCallback(
43 | json => {
44 | if (canvas) {
45 | canvas.loadFromJSON(json, () => {
46 | canvas.requestRenderAll()
47 | })
48 | }
49 | },
50 | [canvas]
51 | )
52 |
53 | const setCanvasBackgroundColor = useCallback(
54 | color => {
55 | // @ts-ignore
56 | const workarea = canvas.getObjects().find(object => object.id === 'workarea')
57 | if (workarea) {
58 | workarea.set('fill', color)
59 | canvas.requestRenderAll()
60 | }
61 | },
62 | [canvas]
63 | )
64 |
65 | return { exportJSON, loadJSON, setCanvasBackgroundColor, addObject, setProperty }
66 | }
67 |
68 | export default useCoreHandler
69 |
--------------------------------------------------------------------------------
/src/components/Canvas/handlers/useCustomizationHandler.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { useEffect } from 'react'
3 | import { fabric } from 'fabric'
4 | import {
5 | drawCircleIcon,
6 | drawVerticalLineIcon,
7 | drawHorizontalLineIcon,
8 | drawRotateIcon,
9 | } from '@components/Canvas/utils'
10 | import { useCanvasContext } from '@components/Canvas/hooks'
11 |
12 | function useCustomizationHandler() {
13 | const { canvas } = useCanvasContext()
14 |
15 | /**
16 | * Customize fabric controls
17 | */
18 | useEffect(() => {
19 | fabric.Object.prototype.transparentCorners = false
20 | fabric.Object.prototype.cornerColor = '#20bf6b'
21 | fabric.Object.prototype.cornerStyle = 'circle'
22 | fabric.Object.prototype.borderColor = '#00D9E1'
23 | fabric.Object.prototype.cornerSize = 12
24 | fabric.Object.prototype.borderScaleFactor = 2.4
25 | fabric.Object.prototype.borderOpacityWhenMoving = 0
26 |
27 | fabric.Object.prototype.controls.tr = new fabric.Control({
28 | x: 0.5,
29 | y: -0.5,
30 | actionHandler: fabric.controlsUtils.scalingEqually,
31 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
32 | actionName: fabric.controlsUtils.scaleOrSkewActionName,
33 | render: drawCircleIcon,
34 | cornerSize: 28,
35 | withConnection: true,
36 | })
37 |
38 | fabric.Object.prototype.controls.tl = new fabric.Control({
39 | x: -0.5,
40 | y: -0.5,
41 | actionHandler: fabric.controlsUtils.scalingEqually,
42 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
43 | actionName: fabric.controlsUtils.scaleOrSkewActionName,
44 | render: drawCircleIcon,
45 | cornerSize: 28,
46 | withConnection: true,
47 | })
48 |
49 | fabric.Object.prototype.controls.bl = new fabric.Control({
50 | x: -0.5,
51 | y: 0.5,
52 | actionHandler: fabric.controlsUtils.scalingEqually,
53 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
54 | actionName: fabric.controlsUtils.scaleOrSkewActionName,
55 | render: drawCircleIcon,
56 | cornerSize: 28,
57 | withConnection: true,
58 | })
59 |
60 | fabric.Object.prototype.controls.br = new fabric.Control({
61 | x: 0.5,
62 | y: 0.5,
63 | actionHandler: fabric.controlsUtils.scalingEqually,
64 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
65 | actionName: fabric.controlsUtils.scaleOrSkewActionName,
66 | render: drawCircleIcon,
67 | cornerSize: 28,
68 | withConnection: true,
69 | })
70 |
71 | fabric.Object.prototype.controls.ml = new fabric.Control({
72 | x: -0.5,
73 | y: 0,
74 | actionHandler: fabric.controlsUtils.scalingXOrSkewingY,
75 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
76 | actionName: fabric.controlsUtils.scaleOrSkewActionName,
77 | render: drawVerticalLineIcon,
78 | cornerSize: 28,
79 | withConnection: true,
80 | })
81 |
82 | fabric.Object.prototype.controls.mt = new fabric.Control({
83 | x: 0,
84 | y: -0.5,
85 | actionHandler: fabric.controlsUtils.scalingYOrSkewingX,
86 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
87 | actionName: fabric.controlsUtils.scaleOrSkewActionName,
88 | render: drawHorizontalLineIcon,
89 | cornerSize: 28,
90 | withConnection: true,
91 | })
92 |
93 | fabric.Object.prototype.controls.mb = new fabric.Control({
94 | x: 0,
95 | y: 0.5,
96 | actionHandler: fabric.controlsUtils.scalingYOrSkewingX,
97 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
98 | actionName: fabric.controlsUtils.scaleOrSkewActionName,
99 | render: drawHorizontalLineIcon,
100 | cornerSize: 28,
101 | withConnection: true,
102 | })
103 |
104 | fabric.Object.prototype.controls.mr = new fabric.Control({
105 | x: 0.5,
106 | y: 0,
107 | actionHandler: fabric.controlsUtils.scalingXOrSkewingY,
108 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
109 | actionName: fabric.controlsUtils.scaleOrSkewActionName,
110 | render: drawVerticalLineIcon,
111 | cornerSize: 28,
112 | withConnection: true,
113 | })
114 |
115 | fabric.Object.prototype.controls.mtr = new fabric.Control({
116 | x: 0,
117 | y: -0.5,
118 | offsetY: -40,
119 | actionHandler: fabric.controlsUtils.rotationWithSnapping,
120 | cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
121 | actionName: 'rotate',
122 | render: drawRotateIcon,
123 | cornerSize: 28,
124 | withConnection: false,
125 | })
126 |
127 | // Texbox controls
128 | fabric.Textbox.prototype.controls.tr = fabric.Object.prototype.controls.tr
129 | fabric.Textbox.prototype.controls.tl = fabric.Object.prototype.controls.tl
130 | fabric.Textbox.prototype.controls.bl = fabric.Object.prototype.controls.bl
131 | fabric.Textbox.prototype.controls.br = fabric.Object.prototype.controls.br
132 |
133 | fabric.Textbox.prototype.controls.mt = new fabric.Control({
134 | render: () => false,
135 | })
136 |
137 | fabric.Textbox.prototype.controls.mb = fabric.Textbox.prototype.controls.mt
138 |
139 | fabric.Textbox.prototype.controls.mr = new fabric.Control({
140 | x: 0.5,
141 | y: 0,
142 | actionHandler: fabric.controlsUtils.changeWidth,
143 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
144 | actionName: 'resizing',
145 | render: drawVerticalLineIcon,
146 | cornerSize: 28,
147 | withConnection: true,
148 | })
149 |
150 | fabric.Textbox.prototype.controls.ml = new fabric.Control({
151 | x: -0.5,
152 | y: 0,
153 | actionHandler: fabric.controlsUtils.changeWidth,
154 | cursorStyleHandler: fabric.controlsUtils.scaleSkewCursorStyleHandler,
155 | actionName: 'resizing',
156 | render: drawVerticalLineIcon,
157 | cornerSize: 28,
158 | withConnection: true,
159 | })
160 |
161 | fabric.Textbox.prototype.controls.mtr = new fabric.Control({
162 | x: 0,
163 | y: -0.5,
164 | offsetY: -40,
165 | actionHandler: fabric.controlsUtils.rotationWithSnapping,
166 | cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,
167 | actionName: 'rotate',
168 | render: drawRotateIcon,
169 | cornerSize: 28,
170 | withConnection: false,
171 | })
172 | }, [])
173 |
174 | /**
175 | * Customize selected styles for groups
176 | */
177 | useEffect(() => {
178 | if (canvas) {
179 | canvas.on('selection:created', function (ev) {
180 | const objects = canvas.getActiveObjects()
181 | if (objects.length > 1) {
182 | ev.target.setControlsVisibility({
183 | mt: false,
184 | mb: false,
185 | mr: false,
186 | ml: false,
187 | })
188 | ev.target.borderDashArray = [7]
189 | }
190 | })
191 | }
192 | }, [canvas])
193 |
194 | /**
195 | * Customize seletion styles
196 | */
197 | useEffect(() => {
198 | if (canvas) {
199 | canvas.selectionColor = 'rgba(46, 204, 113, 0.15)'
200 | canvas.selectionBorderColor = 'rgb(39, 174, 96)'
201 | canvas.selectionLineWidth = 0.4
202 | }
203 | }, [canvas])
204 | }
205 |
206 | export default useCustomizationHandler
207 |
--------------------------------------------------------------------------------
/src/components/Canvas/handlers/useEventsHandler.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from 'react'
2 | import { useCanvasContext } from '@components/Canvas/hooks'
3 | import { isArrow, isCtrlShiftZ, isCtrlZ } from '../utils/keyboard'
4 |
5 | function useEventHandlers() {
6 | const { canvas, setActiveObject, activeObject, setZoomRatio } = useCanvasContext()
7 |
8 | /**
9 | * Canvas Mouse wheel handler
10 | */
11 |
12 | const onMouseWheel = useCallback(
13 | event => {
14 | if (canvas && event.e.ctrlKey) {
15 | const delta = event.e.deltaY
16 | let zoomRatio = canvas.getZoom()
17 | if (delta > 0) {
18 | zoomRatio -= 0.04
19 | } else {
20 | zoomRatio += 0.04
21 | }
22 | setZoomRatio(zoomRatio)
23 | }
24 | event.e.preventDefault()
25 | event.e.stopPropagation()
26 | },
27 | [canvas]
28 | )
29 |
30 | useEffect(() => {
31 | if (canvas) {
32 | canvas.on('mouse:wheel', onMouseWheel)
33 | }
34 | return () => {
35 | if (canvas) {
36 | canvas.off('mouse:wheel', onMouseWheel)
37 | }
38 | }
39 | }, [canvas])
40 |
41 | /**
42 | * Canvas selection handlers
43 | */
44 |
45 | const onSelect = useCallback(
46 | ({ target }) => {
47 | if (target) {
48 | if (canvas) {
49 | setActiveObject(canvas.getActiveObject())
50 | }
51 | } else {
52 | setActiveObject(null)
53 | }
54 | },
55 | [canvas]
56 | )
57 |
58 | useEffect(() => {
59 | if (canvas) {
60 | canvas.on('selection:created', onSelect)
61 | canvas.on('selection:cleared', onSelect)
62 | canvas.on('selection:updated', onSelect)
63 | }
64 | return () => {
65 | if (canvas) {
66 | canvas.off('selection:cleared', onSelect)
67 | canvas.off('selection:created', onSelect)
68 | canvas.off('selection:updated', onSelect)
69 | }
70 | }
71 | }, [canvas])
72 |
73 | /**
74 | * Keyboard Events Handler
75 | */
76 |
77 | const undo = useCallback(() => {
78 | // @ts-ignore
79 | canvas?.undo()
80 | }, [canvas])
81 |
82 | const redo = useCallback(() => {
83 | // @ts-ignore
84 | canvas?.redo()
85 | }, [canvas])
86 |
87 | const moveUp = useCallback(() => {
88 | if (activeObject && canvas) {
89 | activeObject.top = activeObject.top - 2
90 | activeObject.setCoords()
91 | canvas.requestRenderAll()
92 | }
93 | }, [activeObject, canvas])
94 |
95 | const moveDown = useCallback(() => {
96 | if (activeObject && canvas) {
97 | activeObject.top = activeObject.top + 2
98 | activeObject.setCoords()
99 | canvas.requestRenderAll()
100 | }
101 | }, [activeObject, canvas])
102 |
103 | const moveRight = useCallback(() => {
104 | if (activeObject && canvas) {
105 | activeObject.left = activeObject.left + 2
106 | activeObject.setCoords()
107 | canvas.requestRenderAll()
108 | }
109 | }, [activeObject, canvas])
110 |
111 | const moveLeft = useCallback(() => {
112 | if (activeObject && canvas) {
113 | activeObject.left = activeObject.left - 2
114 | activeObject.setCoords()
115 | canvas.requestRenderAll()
116 | }
117 | }, [activeObject, canvas])
118 |
119 | const onKeyDown = useCallback(
120 | e => {
121 | isCtrlZ(e) && undo()
122 | isCtrlShiftZ(e) && redo()
123 | if (isArrow(e)) {
124 | e.code === 'ArrowLeft' && moveLeft()
125 | e.code === 'ArrowRight' && moveRight()
126 | e.code === 'ArrowDown' && moveDown()
127 | e.code === 'ArrowUp' && moveUp()
128 | }
129 | },
130 | [canvas, activeObject]
131 | )
132 | useEffect(() => {
133 | document.addEventListener('keydown', onKeyDown)
134 | return () => {
135 | document.removeEventListener('keydown', onKeyDown)
136 | }
137 | }, [canvas, activeObject])
138 | }
139 |
140 | export default useEventHandlers
141 |
--------------------------------------------------------------------------------
/src/components/Canvas/handlers/useGuidelinesHandler.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { fabric } from 'fabric'
3 | import { ILineOptions } from 'fabric/fabric-impl'
4 | import { useCanvasContext } from '@components/Canvas/hooks'
5 |
6 | function useGuidelinesHandler() {
7 | const { canvas } = useCanvasContext()
8 | useEffect(() => {
9 | if (canvas) {
10 | let ctx = canvas.getSelectionContext(),
11 | aligningLineOffset = 5,
12 | aligningLineMargin = 4,
13 | aligningLineWidth = 1,
14 | aligningLineColor = 'rgba(255, 121, 121,1.0)',
15 | viewportTransform = canvas.viewportTransform,
16 | zoom = 1
17 |
18 | const drawVerticalLine = (coords: ILineOptions) => {
19 | drawLine(
20 | coords.x1! + 0.5,
21 | coords.y1! > coords.y2! ? coords.y2! : coords.y1!,
22 | coords.x1! + 0.5,
23 | coords.y2! > coords.y1! ? coords.y2! : coords.y1!
24 | )
25 | }
26 |
27 | const drawHorizontalLine = (coords: ILineOptions) => {
28 | drawLine(
29 | coords.x1! > coords.x2! ? coords.x2! : coords.x1!,
30 | coords.y1! + 0.5,
31 | coords.x2! > coords.x1! ? coords.x2! : coords.x1!,
32 | coords.y1! + 0.5
33 | )
34 | }
35 |
36 | const drawLine = (x1: number, y1: number, x2: number, y2: number) => {
37 | ctx.save()
38 | ctx.lineWidth = aligningLineWidth
39 | ctx.strokeStyle = aligningLineColor
40 | ctx.beginPath()
41 | if (viewportTransform) {
42 | ctx.moveTo((x1 + viewportTransform[4]) * zoom, (y1 + viewportTransform[5]) * zoom)
43 | ctx.lineTo((x2 + viewportTransform[4]) * zoom, (y2 + viewportTransform[5]) * zoom)
44 | }
45 | ctx.stroke()
46 | ctx.restore()
47 | }
48 |
49 | const isInRange = (value1: number, value2: number) => {
50 | value1 = Math.round(value1)
51 | value2 = Math.round(value2)
52 | for (let i = value1 - aligningLineMargin, len = value1 + aligningLineMargin; i <= len; i++) {
53 | if (i === value2) {
54 | return true
55 | }
56 | }
57 | return false
58 | }
59 |
60 | let verticalLines: ILineOptions[] = [],
61 | horizontalLines: ILineOptions[] = []
62 |
63 | canvas.on('mouse:down', function () {
64 | viewportTransform = canvas.viewportTransform
65 | zoom = canvas.getZoom()
66 | })
67 |
68 | canvas.on('object:moving', function (e) {
69 | let activeObject = e.target
70 | if (!activeObject || !viewportTransform) return
71 |
72 | let canvasObjects = canvas.getObjects()
73 | let activeObjectCenter = activeObject.getCenterPoint(),
74 | activeObjectLeft = activeObjectCenter.x,
75 | activeObjectTop = activeObjectCenter.y,
76 | activeObjectBoundingRect = activeObject.getBoundingRect(),
77 | activeObjectHeight = activeObjectBoundingRect.height / viewportTransform[3],
78 | activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0],
79 | horizontalInTheRange = false,
80 | verticalInTheRange = false,
81 | transform = canvas.viewportTransform
82 |
83 | if (!transform) return
84 |
85 | // It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions,
86 | // but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move
87 |
88 | for (let i = canvasObjects.length; i--; ) {
89 | if (canvasObjects[i] === activeObject) continue
90 |
91 | let objectCenter = canvasObjects[i].getCenterPoint(),
92 | objectLeft = objectCenter.x,
93 | objectTop = objectCenter.y,
94 | objectBoundingRect = canvasObjects[i].getBoundingRect(),
95 | objectHeight = objectBoundingRect.height / viewportTransform[3],
96 | objectWidth = objectBoundingRect.width / viewportTransform[0]
97 |
98 | // snap by the horizontal center line
99 | if (isInRange(objectLeft, activeObjectLeft)) {
100 | verticalInTheRange = true
101 | verticalLines.push({
102 | x1: objectLeft,
103 | y1:
104 | objectTop < activeObjectTop
105 | ? objectTop - objectHeight / 2 - aligningLineOffset
106 | : objectTop + objectHeight / 2 + aligningLineOffset,
107 | y2:
108 | activeObjectTop > objectTop
109 | ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
110 | : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
111 | })
112 | activeObject.setPositionByOrigin(
113 | new fabric.Point(objectLeft, activeObjectTop),
114 | 'center',
115 | 'center'
116 | )
117 | }
118 |
119 | // snap by the left edge
120 | if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) {
121 | verticalInTheRange = true
122 | verticalLines.push({
123 | x1: objectLeft - objectWidth / 2,
124 | y1:
125 | objectTop < activeObjectTop
126 | ? objectTop - objectHeight / 2 - aligningLineOffset
127 | : objectTop + objectHeight / 2 + aligningLineOffset,
128 | y2:
129 | activeObjectTop > objectTop
130 | ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
131 | : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
132 | })
133 | activeObject.setPositionByOrigin(
134 | new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop),
135 | 'center',
136 | 'center'
137 | )
138 | }
139 |
140 | // snap by the right edge
141 | if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) {
142 | verticalInTheRange = true
143 | verticalLines.push({
144 | x1: objectLeft + objectWidth / 2,
145 | y1:
146 | objectTop < activeObjectTop
147 | ? objectTop - objectHeight / 2 - aligningLineOffset
148 | : objectTop + objectHeight / 2 + aligningLineOffset,
149 | y2:
150 | activeObjectTop > objectTop
151 | ? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
152 | : activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
153 | })
154 | activeObject.setPositionByOrigin(
155 | new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop),
156 | 'center',
157 | 'center'
158 | )
159 | }
160 |
161 | // snap by the vertical center line
162 | if (isInRange(objectTop, activeObjectTop)) {
163 | horizontalInTheRange = true
164 | horizontalLines.push({
165 | y1: objectTop,
166 | x1:
167 | objectLeft < activeObjectLeft
168 | ? objectLeft - objectWidth / 2 - aligningLineOffset
169 | : objectLeft + objectWidth / 2 + aligningLineOffset,
170 | x2:
171 | activeObjectLeft > objectLeft
172 | ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
173 | : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
174 | })
175 | activeObject.setPositionByOrigin(
176 | new fabric.Point(activeObjectLeft, objectTop),
177 | 'center',
178 | 'center'
179 | )
180 | }
181 |
182 | // snap by the top edge
183 | if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) {
184 | horizontalInTheRange = true
185 | horizontalLines.push({
186 | y1: objectTop - objectHeight / 2,
187 | x1:
188 | objectLeft < activeObjectLeft
189 | ? objectLeft - objectWidth / 2 - aligningLineOffset
190 | : objectLeft + objectWidth / 2 + aligningLineOffset,
191 | x2:
192 | activeObjectLeft > objectLeft
193 | ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
194 | : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
195 | })
196 | activeObject.setPositionByOrigin(
197 | new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2),
198 | 'center',
199 | 'center'
200 | )
201 | }
202 |
203 | // snap by the bottom edge
204 | if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) {
205 | horizontalInTheRange = true
206 | horizontalLines.push({
207 | y1: objectTop + objectHeight / 2,
208 | x1:
209 | objectLeft < activeObjectLeft
210 | ? objectLeft - objectWidth / 2 - aligningLineOffset
211 | : objectLeft + objectWidth / 2 + aligningLineOffset,
212 | x2:
213 | activeObjectLeft > objectLeft
214 | ? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
215 | : activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
216 | })
217 | activeObject.setPositionByOrigin(
218 | new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2),
219 | 'center',
220 | 'center'
221 | )
222 | }
223 | }
224 |
225 | if (!horizontalInTheRange) {
226 | horizontalLines.length = 0
227 | }
228 |
229 | if (!verticalInTheRange) {
230 | verticalLines.length = 0
231 | }
232 | })
233 |
234 | canvas.on('before:render', function () {
235 | canvas.clearContext(ctx)
236 | })
237 |
238 | canvas.on('after:render', function () {
239 | for (let i = verticalLines.length; i--; ) {
240 | drawVerticalLine(verticalLines[i])
241 | }
242 | for (let i = horizontalLines.length; i--; ) {
243 | drawHorizontalLine(horizontalLines[i])
244 | }
245 |
246 | verticalLines.length = horizontalLines.length = 0
247 | })
248 |
249 | canvas.on('mouse:up', function () {
250 | verticalLines.length = horizontalLines.length = 0
251 | canvas.renderAll()
252 | })
253 | }
254 | }, [canvas])
255 | }
256 |
257 | export default useGuidelinesHandler
258 |
--------------------------------------------------------------------------------
/src/components/Canvas/handlers/useZoomHandler.ts:
--------------------------------------------------------------------------------
1 | import { fabric } from 'fabric'
2 | import { useCallback, useEffect } from 'react'
3 | import { useCanvasContext } from '@components/Canvas/hooks'
4 |
5 | function useZoomHandler() {
6 | const { canvas, zoomRatio } = useCanvasContext()
7 |
8 | const updateZoom = useCallback(
9 | (zoomRatio: number) => {
10 | if (canvas) {
11 | canvas.zoomToPoint(new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2), zoomRatio)
12 | }
13 | },
14 | [canvas]
15 | )
16 |
17 | useEffect(() => {
18 | updateZoom(zoomRatio)
19 | }, [zoomRatio])
20 | }
21 |
22 | export default useZoomHandler
23 |
--------------------------------------------------------------------------------
/src/components/Canvas/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useCanvasContext } from './useCanvasContext'
2 |
--------------------------------------------------------------------------------
/src/components/Canvas/hooks/useCanvasContext.ts:
--------------------------------------------------------------------------------
1 | import { Context } from '@components/Canvas'
2 | import { useContext } from 'react'
3 |
4 | function useCanvasContext() {
5 | const { zoomRatio, setZoomRatio, setCanvas, canvas, activeObject, setActiveObject } = useContext(Context)
6 | return {
7 | zoomRatio,
8 | setZoomRatio,
9 | setCanvas,
10 | canvas,
11 | activeObject,
12 | setActiveObject,
13 | }
14 | }
15 |
16 | export default useCanvasContext
17 |
--------------------------------------------------------------------------------
/src/components/Canvas/index.ts:
--------------------------------------------------------------------------------
1 | import Canvas from './Canvas'
2 | export * from './CanvasContext'
3 | export * from './CanvasObjects'
4 |
5 | export default Canvas
6 |
--------------------------------------------------------------------------------
/src/components/Canvas/utils/drawer.ts:
--------------------------------------------------------------------------------
1 | import { fabric } from 'fabric'
2 |
3 | export function drawCircleIcon(ctx, left, top, styleOverride, fabricObject) {
4 | ctx.save()
5 | ctx.translate(left, top)
6 | ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle))
7 | ctx.beginPath()
8 | ctx.lineCap = 'round'
9 | ctx.lineWidth = 3
10 | ctx.shadowBlur = 2
11 | ctx.shadowColor = 'black'
12 | ctx.arc(0, 0, 5.5, 0, 2 * Math.PI)
13 | ctx.fillStyle = '#ffffff'
14 | ctx.fill()
15 | ctx.restore()
16 | }
17 |
18 | export function drawVerticalLineIcon(ctx, left, top, styleOverride, fabricObject) {
19 | const size = this.cornerSize
20 | ctx.save()
21 | ctx.translate(left, top)
22 | ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle))
23 | ctx.beginPath()
24 | ctx.lineCap = 'round'
25 | ctx.lineWidth = 3
26 | ctx.shadowBlur = 2
27 | ctx.shadowColor = 'black'
28 | ctx.moveTo(-0.5, -size / 4)
29 | ctx.lineTo(-0.5, -size / 4 + size / 2)
30 | ctx.strokeStyle = '#ffffff'
31 | ctx.stroke()
32 | ctx.restore()
33 | }
34 |
35 | export function drawHorizontalLineIcon(ctx, left, top, styleOverride, fabricObject) {
36 | const size = this.cornerSize
37 | ctx.save()
38 | ctx.translate(left, top)
39 | ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle))
40 | ctx.beginPath()
41 | ctx.lineCap = 'round'
42 | ctx.lineWidth = 3
43 | ctx.shadowBlur = 2
44 | ctx.shadowColor = 'black'
45 | ctx.moveTo(-size / 4, -0.5)
46 | ctx.lineTo(-size / 4 + size / 2, -0.5)
47 | ctx.strokeStyle = '#ffffff'
48 | ctx.stroke()
49 | ctx.restore()
50 | }
51 |
52 | export function drawRotateIcon(ctx, left, top, styleOverride, fabricObject) {
53 | const radius = 6
54 | let lineWidth = radius / 3
55 | let arrowWidth = radius / 2
56 | const center = {
57 | x: left,
58 | y: top
59 | }
60 | let arrow1 = {
61 | startAngle: (1 / 2) * Math.PI + 0.6,
62 | endAngle: (3 / 2) * Math.PI
63 | }
64 |
65 | let arrow2 = {
66 | startAngle: (3 / 2) * Math.PI + 0.6,
67 | endAngle: (1 / 2) * Math.PI
68 | }
69 | function draw(startAngle, endAngle) {
70 | ctx.beginPath()
71 | ctx.shadowBlur = 0
72 |
73 | ctx.arc(center.x, center.y, radius, startAngle, endAngle)
74 | ctx.lineWidth = lineWidth
75 | ctx.strokeStyle = '#000000'
76 | ctx.stroke()
77 |
78 | ctx.beginPath()
79 | let arrowTop = getPointOnCircle(center, radius, endAngle + 0.4)
80 |
81 | ctx.moveTo(arrowTop.x, arrowTop.y)
82 |
83 | let arrowLeft = getPointOnCircle(center, radius - arrowWidth, endAngle)
84 | ctx.lineTo(arrowLeft.x, arrowLeft.y)
85 |
86 | let arrowRight = getPointOnCircle(center, radius + arrowWidth, endAngle)
87 | ctx.lineTo(arrowRight.x, arrowRight.y)
88 | ctx.fillStyle = '#000000'
89 |
90 | ctx.closePath()
91 | ctx.fill()
92 | }
93 |
94 | function getPointOnCircle(center, radius, angle) {
95 | let pX = center.x + Math.cos(angle) * radius
96 | let pY = center.y + Math.sin(angle) * radius
97 | return { x: pX, y: pY }
98 | }
99 |
100 | ctx.save()
101 | ctx.translate(0, 0)
102 |
103 | ctx.beginPath()
104 | ctx.arc(center.x, center.y, radius + 6, 0, Math.PI * 2)
105 | ctx.fillStyle = '#ffffff'
106 | ctx.shadowBlur = 2
107 | ctx.shadowColor = 'black'
108 | ctx.fill()
109 | ctx.closePath()
110 | draw(arrow1.startAngle, arrow1.endAngle)
111 | draw(arrow2.startAngle, arrow2.endAngle)
112 | ctx.restore()
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/Canvas/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './drawer'
2 |
--------------------------------------------------------------------------------
/src/components/Canvas/utils/keyboard.ts:
--------------------------------------------------------------------------------
1 | export const isCtrlZ = e => {
2 | return e.ctrlKey && !e.shiftKey && e.code === 'KeyZ'
3 | }
4 | export const isCtrlShiftZ = e => {
5 | return e.ctrlKey && e.shiftKey && e.code === 'KeyZ'
6 | }
7 |
8 | export const isArrow = e => {
9 | return ['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp'].includes(e.code)
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/Editor/CanvasArea/CanvasArea.scss:
--------------------------------------------------------------------------------
1 | .canvasarea {
2 | flex: 1;
3 | display: flex;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Editor/CanvasArea/CanvasArea.tsx:
--------------------------------------------------------------------------------
1 | import "./CanvasArea.scss"
2 | import Canvas from "@components/Canvas"
3 | function CanvasArea() {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default CanvasArea
12 |
--------------------------------------------------------------------------------
/src/components/Editor/Editor.scss:
--------------------------------------------------------------------------------
1 | .editor {
2 | height: 100vh;
3 | width: 100vw;
4 | display: flex;
5 | flex-direction: column;
6 | background: #ecf0f1;
7 | flex: 1;
8 | > .section-two {
9 | display: flex;
10 | flex: 1;
11 | > .section-three {
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | }
16 | }
17 | }
18 |
19 | .editor-canvas {
20 | flex: 1;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Editor/Editor.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from '@components/Editor/Navbar/Navbar'
2 | import Panels from '@components/Editor/Panels/Panels'
3 | import FooterMenu from '@components/Editor/FooterMenu/FooterMenu'
4 | import Toolbox from '@components/Editor/Toolbox/Toolbox'
5 | import CanvasArea from '@components/Editor/CanvasArea/CanvasArea'
6 | import './Editor.scss'
7 |
8 | function Editor() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default Editor
25 |
--------------------------------------------------------------------------------
/src/components/Editor/FooterMenu/FooterMenu.scss:
--------------------------------------------------------------------------------
1 | .footermenu {
2 | height: 50px;
3 | background: #ffffff;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Editor/FooterMenu/FooterMenu.tsx:
--------------------------------------------------------------------------------
1 | import './FooterMenu.scss'
2 | function FooterMenu() {
3 | return (
4 |
7 | )
8 | }
9 |
10 | export default FooterMenu
11 |
--------------------------------------------------------------------------------
/src/components/Editor/Navbar/Navbar.scss:
--------------------------------------------------------------------------------
1 | .navbar {
2 | background: linear-gradient(90deg, #00c4cc, #7d2ae8);
3 | height: 60px;
4 | display: flex;
5 | align-items: center;
6 | justify-content: space-between;
7 | padding: 0 1.36rem;
8 | }
9 |
10 | .navbar-left {
11 | color: #fff;
12 | display: flex;
13 | svg {
14 | width: 36px;
15 | height: 36px;
16 | }
17 | }
18 |
19 | .navbar-action-items {
20 | color: #fff;
21 | display: flex;
22 | }
23 |
24 | .navbar-icon {
25 | border-radius: 4px;
26 | padding: 0.5rem;
27 | background: rgba($color: #fff, $alpha: 0.15);
28 | margin-left: 1rem;
29 | cursor: pointer;
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/Editor/Navbar/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import './Navbar.scss'
2 | import { DownloadIcon, LogoIcon, GithubIcon } from './NavbarIcons'
3 |
4 | function Navbar() {
5 | return (
6 |
17 | )
18 | }
19 |
20 | export default Navbar
21 |
--------------------------------------------------------------------------------
/src/components/Editor/Navbar/NavbarIcons.tsx:
--------------------------------------------------------------------------------
1 | export function GithubIcon() {
2 | return (
3 |
8 | )
9 | }
10 |
11 | export function DownloadIcon() {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export function LogoIcon() {
25 | return (
26 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/ClosePanel.tsx:
--------------------------------------------------------------------------------
1 | interface ClosePanelProp {
2 | closePanel: () => void
3 | }
4 |
5 | function ClosePanel({ closePanel }: ClosePanelProp) {
6 | return (
7 |
28 | )
29 | }
30 |
31 | export default ClosePanel
32 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/ImagesPanel/ImagesPanel.tsx:
--------------------------------------------------------------------------------
1 | import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'
2 | import { SearchIcon } from '@chakra-ui/icons'
3 |
4 | function ImagesPanel() {
5 | return (
6 | <>
7 |
8 |
9 | } />
10 |
11 |
12 |
13 | >
14 | )
15 | }
16 | export default ImagesPanel
17 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/MusicPanel/MusicPanel.tsx:
--------------------------------------------------------------------------------
1 | import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'
2 | import { SearchIcon } from '@chakra-ui/icons'
3 |
4 | function MusicPanel() {
5 | return (
6 | <>
7 |
8 |
9 | } />
10 |
11 |
12 |
13 | >
14 | )
15 | }
16 | export default MusicPanel
17 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/ObjectsPanel/ObjectsPanel.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'
3 | import { SearchIcon } from '@chakra-ui/icons'
4 | import { getImage, getImages } from '@services/iconscout'
5 | import { useCanvasContext } from '@components/Canvas/hooks'
6 | import { useDebounce } from 'use-debounce'
7 |
8 | import { fabric } from 'fabric'
9 |
10 | function ObjectsPanel() {
11 | const [search, setSearch] = useState('')
12 | const [objects, setObjects] = useState([])
13 | const [value] = useDebounce(search, 1000)
14 | const { canvas } = useCanvasContext()
15 |
16 | useEffect(() => {
17 | getImages('love')
18 | .then((data: any) => setObjects(data))
19 | .catch(console.log)
20 | }, [])
21 |
22 | useEffect(() => {
23 | if (value) {
24 | getImages(value)
25 | .then((data: any) => setObjects(data))
26 | .catch(console.log)
27 | }
28 | }, [value])
29 | const renderItems = () => {
30 | return objects.map(obj => {
31 | return (
32 | downloadImage(obj.uuid)} key={obj.uuid}>
33 |

34 |
35 | )
36 | })
37 | }
38 | const downloadImage = uuid => {
39 | getImage(uuid)
40 | .then(url => {
41 | fabric.loadSVGFromURL(url, (objects, options) => {
42 | const object = fabric.util.groupSVGElements(objects, options)
43 | //@ts-ignore
44 | const workarea = canvas.getObjects().find(obj => obj.id === 'workarea')
45 | canvas.add(object)
46 | object.scaleToHeight(300)
47 | object.center()
48 | object.clipPath = workarea
49 | canvas.renderAll()
50 | })
51 | })
52 | .catch(console.log)
53 | }
54 |
55 | return (
56 | <>
57 |
58 |
59 | } />
60 | setSearch(e.target.value)}
62 | style={{ background: '#fff' }}
63 | type="tel"
64 | placeholder="Search objects"
65 | />
66 |
67 |
68 |
69 | {renderItems()}
70 |
71 | >
72 | )
73 | }
74 | export default ObjectsPanel
75 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/PanelItem/PanelItem.scss:
--------------------------------------------------------------------------------
1 | .panel-item-container {
2 | width: 0;
3 | transition: width 0.5s;
4 | position: relative;
5 | overflow: hidden;
6 | display: flex;
7 | &.open {
8 | width: 360px;
9 | }
10 | }
11 | .panel-item {
12 | flex: 1;
13 | }
14 |
15 | .objects-list {
16 | display: grid;
17 | grid-template-columns: repeat(3, 1fr);
18 | // grid-auto-rows: minmax(100px, auto);
19 | // gap: 1rem;
20 | padding: 1rem 0;
21 | }
22 |
23 | .object-item {
24 | // height: 6rem;
25 | // background: rebeccapurple;
26 | }
27 |
28 | .object-item-container {
29 | // background: red;
30 | transition: background 0.4s;
31 | cursor: pointer;
32 | padding: 1rem;
33 | &:hover {
34 | background: rgba($color: #fff, $alpha: 0.1);
35 | }
36 | }
37 |
38 | // Panel Text
39 | .panel-text {
40 | width: 360px;
41 | color: #fff;
42 | .add-text-items {
43 | display: grid;
44 | gap: 0.5rem;
45 | }
46 | .label {
47 | font-weight: 600;
48 | padding: 0.8rem 0;
49 | font-size: 0.84rem;
50 | }
51 | .add-text-item {
52 | background: rgba($color: #fff, $alpha: 0.1);
53 | height: 50px;
54 | display: flex;
55 | align-items: center;
56 | padding-left: 1rem;
57 | cursor: pointer;
58 | transition: background 0.4s;
59 | &:hover {
60 | background: rgba($color: #fff, $alpha: 0.15);
61 | }
62 | }
63 | .add-heading {
64 | font-weight: 700;
65 | font-size: 1.66rem;
66 | }
67 | .add-subheading {
68 | background: rgba($color: #fff, $alpha: 0.1);
69 | font-size: 1.12rem;
70 | font-weight: 500;
71 | }
72 | .add-body-text {
73 | background: rgba($color: #fff, $alpha: 0.1);
74 | font-size: 0.76rem;
75 | font-weight: 400;
76 | color: rgba($color: #fff, $alpha: 0.9);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/PanelItem/PanelItem.tsx:
--------------------------------------------------------------------------------
1 | import ImagesPanel from '../ImagesPanel/ImagesPanel'
2 | import MusicPanel from '../MusicPanel/MusicPanel'
3 | import ObjectsPanel from '../ObjectsPanel/ObjectsPanel'
4 | import TemplatesPanel from '../TemplatesPanel/TemplatesPanel'
5 | import TextPanel from '../TextPanel/TextPanel'
6 | import VideosPanel from '../VideosPanel/VideosPanel'
7 | import { Scrollbars } from 'react-custom-scrollbars'
8 | import classNames from 'classnames'
9 | import './PanelItem.scss'
10 |
11 | interface Props {
12 | panelOpen: boolean
13 | activeTab: string
14 | }
15 | function PanelItem({ panelOpen, activeTab }: Props) {
16 | const className = classNames({
17 | 'panel-item-container': true,
18 | open: panelOpen,
19 | })
20 |
21 | return (
22 |
23 |
24 |
}
26 | autoHide
27 | >
28 | {activeTab === 'text' && }
29 | {activeTab === 'images' && }
30 | {activeTab === 'musics' && }
31 | {activeTab === 'objects' && }
32 | {activeTab === 'templates' && }
33 | {activeTab === 'videos' && }
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export default PanelItem
41 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/Panels.scss:
--------------------------------------------------------------------------------
1 | @import '../../../theme/colors.scss';
2 |
3 | .panels {
4 | position: relative;
5 | background: $dark-2;
6 | box-shadow: 5px 0 5px -5px rgba(0, 0, 0, 0.5);
7 | display: flex;
8 | user-select: none;
9 | font-family: 'Lexend';
10 | }
11 |
12 | .panel-item-close {
13 | cursor: pointer;
14 | position: absolute;
15 | top: 50%;
16 | right: 0;
17 | transform: translate(100%, -50%);
18 | z-index: 2;
19 | }
20 |
21 | .c1 {
22 | margin-left: -6px;
23 | cursor: pointer;
24 | border: none;
25 | background: none;
26 | overflow: hidden;
27 | pointer-events: none;
28 | position: relative;
29 | color: #fff;
30 | right: -5px;
31 | }
32 |
33 | .c2 {
34 | margin-left: -6px;
35 | border: none;
36 | right: 8px;
37 | position: relative;
38 | justify-content: center;
39 | position: absolute;
40 | color: #fff;
41 | top: 50%;
42 | transform: translate(100%, -50%);
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/Panels.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import ClosePanel from './ClosePanel'
3 | import PanelItemsList from './PanelsList/PanelsList'
4 | import PanelItem from './PanelItem/PanelItem'
5 | import './Panels.scss'
6 |
7 | function Panels() {
8 | const [panelOpen, setPanelOpen] = useState(true)
9 | const [activeTab, setActiveTab] = useState('objects')
10 |
11 | const closePanel = () => {
12 | setPanelOpen(!panelOpen)
13 | }
14 | return (
15 |
25 | )
26 | }
27 |
28 | export default Panels
29 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/PanelsList/PanelItemIcon.tsx:
--------------------------------------------------------------------------------
1 | function TemplateIcon() {
2 | return (
3 |
10 | )
11 | }
12 |
13 | function ObjectIcon() {
14 | return (
15 |
21 | )
22 | }
23 |
24 | function TextIcon() {
25 | return (
26 |
33 | )
34 | }
35 |
36 | function ImageIcon() {
37 | return (
38 |
44 | )
45 | }
46 |
47 | function VideoIcon() {
48 | return (
49 |
57 | )
58 | }
59 |
60 | function AudioIcon() {
61 | return (
62 |
70 | )
71 | }
72 |
73 | const PanelIcons = {
74 | templates: {
75 | render: () => ,
76 | },
77 | objects: {
78 | render: () => ,
79 | },
80 | images: {
81 | render: () => ,
82 | },
83 | videos: {
84 | render: () => ,
85 | },
86 | texts: {
87 | render: () => ,
88 | },
89 | musics: {
90 | render: () => ,
91 | },
92 | }
93 |
94 | export default PanelIcons
95 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/PanelsList/PanelsList.scss:
--------------------------------------------------------------------------------
1 | @import '../../../../theme/colors.scss';
2 |
3 | .panel-items-list {
4 | width: 72px;
5 | background: $dark-1;
6 | color: #fff;
7 | }
8 |
9 | .panel-items-list-item {
10 | display: flex;
11 | align-items: center;
12 | flex-direction: column;
13 | justify-content: center;
14 | height: 72px;
15 | text-align: center;
16 | font-size: 0.8rem;
17 | position: relative;
18 | cursor: pointer;
19 | transition: color 0.2s;
20 | color: rgba($color: #ffffff, $alpha: 0.8);
21 | &:hover {
22 | color: rgba($color: #ffffff, $alpha: 1);
23 | }
24 | &.active {
25 | background: $dark-2;
26 | color: rgba($color: #ffffff, $alpha: 1);
27 | }
28 | span {
29 | padding-top: 0.2rem;
30 | }
31 | }
32 |
33 | .panel-items-list-item.active::before {
34 | background: radial-gradient(circle closest-side, transparent 0, transparent 50%, $dark-2 0) 200% 200%/400%
35 | 400%;
36 | top: -8px;
37 | content: '';
38 | position: absolute;
39 | right: 0;
40 | height: 8px;
41 | width: 8px;
42 | overflow: hidden;
43 | }
44 |
45 | .panel-items-list-item.active::after {
46 | border-right: none !important;
47 | height: 100%;
48 | top: 8px;
49 | right: -1px;
50 | transform: scaleY(-1);
51 | background: radial-gradient(circle closest-side, transparent 0, transparent 50%, $dark-2 0) 200% 200%/400%
52 | 400%;
53 | width: 8px;
54 | overflow: hidden;
55 | content: '';
56 | position: absolute;
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/PanelsList/PanelsList.tsx:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars'
2 | import { tabItems } from './tabItems'
3 | import PanelItemsListItem from './PanelsListItem'
4 | import i18n from 'i18next'
5 |
6 | import './PanelsList.scss'
7 |
8 | interface Props {
9 | setActiveTab: React.Dispatch>
10 | activeTab: string
11 | panelOpen: boolean
12 | setPanelOpen: React.Dispatch>
13 | }
14 |
15 | function PanelItems(props: Props) {
16 | const { setActiveTab, activeTab, setPanelOpen, panelOpen } = props
17 | return (
18 |
19 |
}
21 | autoHide
22 | >
23 | {tabItems.map(tabItem => (
24 |
34 | ))}
35 |
36 |
37 | )
38 | }
39 |
40 | export default PanelItems
41 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/PanelsList/PanelsListItem.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import PanelItemIcon from './PanelItemIcon'
3 | // import "./Item.less"
4 |
5 | interface Props {
6 | activeTab: string
7 | label: string
8 | icon: string
9 | name: string
10 | panelOpen: boolean
11 | setActiveTab: React.Dispatch>
12 | setPanelOpen: React.Dispatch>
13 | }
14 |
15 | function PanelItem(props: Props) {
16 | const { setActiveTab, label, icon, name, activeTab, setPanelOpen } = props
17 | const className = classNames({
18 | 'panel-items-list-item': true,
19 | active: activeTab === name,
20 | })
21 |
22 | return (
23 | {
26 | setPanelOpen(true)
27 | setActiveTab(name)
28 | }}
29 | >
30 | {PanelItemIcon[icon].render()}
31 | {label}
32 |
33 | )
34 | }
35 |
36 | export default PanelItem
37 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/PanelsList/tabItems.ts:
--------------------------------------------------------------------------------
1 | export const tabItems = [
2 | {
3 | icon: 'templates',
4 | label: 'Şablonlar',
5 | name: 'templates',
6 | },
7 | {
8 | icon: 'images',
9 | label: 'Fotoğraf',
10 | name: 'images',
11 | },
12 | {
13 | icon: 'texts',
14 | label: 'Metin',
15 | name: 'text',
16 | },
17 | {
18 | icon: 'objects',
19 | label: 'Nesneler',
20 | name: 'objects',
21 | },
22 | {
23 | icon: 'musics',
24 | label: 'Müzik',
25 | name: 'musics',
26 | },
27 | {
28 | icon: 'videos',
29 | label: 'Videolar',
30 | name: 'videos',
31 | },
32 | ]
33 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/TemplatesPanel/TemplatesPanel.tsx:
--------------------------------------------------------------------------------
1 | import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'
2 | import { SearchIcon } from '@chakra-ui/icons'
3 |
4 | function TemplatesPanel() {
5 | return (
6 | <>
7 |
8 |
9 | } />
10 |
11 |
12 |
13 | >
14 | )
15 | }
16 | export default TemplatesPanel
17 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/TextPanel/TextPanel.tsx:
--------------------------------------------------------------------------------
1 | import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'
2 | import { SearchIcon } from '@chakra-ui/icons'
3 | import { useCoreHandler } from '@/components/Canvas/handlers'
4 |
5 | function TextPanel() {
6 | const { addObject } = useCoreHandler()
7 | const addHeading = () => {
8 | const options = {
9 | type: 'text',
10 | text: 'Add a heading',
11 | fontSize: 32,
12 | width: 320,
13 | fontWeight: 700,
14 | fontFamily: 'Lexend',
15 | textAlign: 'center',
16 | }
17 | addObject(options)
18 | }
19 |
20 | const addSubheading = () => {
21 | const options = {
22 | type: 'text',
23 | text: 'Add a subheading',
24 | fontSize: 24,
25 | width: 320,
26 | fontWeight: 500,
27 | fontFamily: 'Lexend',
28 | textAlign: 'center',
29 | }
30 | addObject(options)
31 | }
32 |
33 | const addTextBody = () => {
34 | const options = {
35 | type: 'text',
36 | text: 'Add a little bit of body text',
37 | fontSize: 18,
38 | width: 320,
39 | fontWeight: 300,
40 | fontFamily: 'Lexend',
41 | textAlign: 'center',
42 | }
43 | addObject(options)
44 | }
45 | return (
46 | <>
47 |
48 |
49 | } />
50 |
51 |
52 |
Click text to add to page
53 |
54 |
55 | Add a heading
56 |
57 |
58 | Add a subheading
59 |
60 |
61 | Add a litle bit of body text
62 |
63 |
64 |
65 | >
66 | )
67 | }
68 | export default TextPanel
69 |
--------------------------------------------------------------------------------
/src/components/Editor/Panels/VideosPanel/VideosPanel.tsx:
--------------------------------------------------------------------------------
1 | import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'
2 | import { SearchIcon } from '@chakra-ui/icons'
3 |
4 | function VideosPanel() {
5 | return (
6 | <>
7 |
8 |
9 | } />
10 |
11 |
12 |
13 | >
14 | )
15 | }
16 | export default VideosPanel
17 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbox/DefaultToolbox/DefaultToobox.scss:
--------------------------------------------------------------------------------
1 | .editor-toolbox.default {
2 | align-items: center;
3 | display: flex;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbox/DefaultToolbox/DefaultToolbox.tsx:
--------------------------------------------------------------------------------
1 | import { useCoreHandler } from '@/components/Canvas/handlers'
2 | import { CSSProperties, useState } from 'react'
3 | import { TwitterPicker } from 'react-color'
4 | import emptyColorPlaceholder from '@/assets/images/base-color-picker.png'
5 | import './DefaultToobox.scss'
6 | // import './Toolbox.css'
7 |
8 | function VerticalSeparator() {
9 | return
10 | }
11 |
12 | function Toolbox() {
13 | const [dropdown, setDropdown] = useState({
14 | displayColorPicker: false,
15 | })
16 | const [options, setOptions] = useState({
17 | backgroundColor: '#ffffff',
18 | })
19 | const { setCanvasBackgroundColor } = useCoreHandler()
20 |
21 | const handleClick = () => {
22 | setDropdown({ ...dropdown, displayColorPicker: !dropdown.displayColorPicker })
23 | }
24 | const handleClose = () => {
25 | setDropdown({ ...dropdown, displayColorPicker: false })
26 | }
27 |
28 | const popover: CSSProperties = {
29 | position: 'absolute',
30 | zIndex: 2,
31 | }
32 | const cover: CSSProperties = {
33 | position: 'fixed',
34 | top: '0px',
35 | right: '0px',
36 | bottom: '0px',
37 | left: '0px',
38 | }
39 |
40 | const onColorChange = color => {
41 | setCanvasBackgroundColor(color.hex)
42 | setOptions({ ...options, backgroundColor: color.hex })
43 | }
44 | return (
45 |
46 |
47 |
48 |
49 | {options.backgroundColor === '#ffffff' ? (
50 |

55 | ) : (
56 |
57 | )}
58 |
59 |
60 | {dropdown.displayColorPicker ? (
61 |
65 | ) : null}
66 |
67 |
68 |
69 |
70 | )
71 | }
72 |
73 | export default Toolbox
74 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbox/TextToolbox/TextToolbox.scss:
--------------------------------------------------------------------------------
1 | .editor-toolbox.text {
2 | height: 54px;
3 | background: #ffffff;
4 | border-bottom: 1px solid rgba(57, 76, 96, 0.15);
5 | display: flex;
6 | align-items: center;
7 | position: relative;
8 | justify-content: space-between;
9 | .section-two {
10 | display: flex;
11 | }
12 | }
13 |
14 | .list-item {
15 | height: 46px;
16 | display: flex;
17 | align-items: center;
18 | cursor: pointer;
19 | &:hover {
20 | background: rgba($color: #000000, $alpha: 0.03);
21 | }
22 | }
23 |
24 | .font-family-selector {
25 | width: 180px;
26 | cursor: pointer;
27 | border: 1px solid rgba($color: #000000, $alpha: 0.2);
28 | height: 36px;
29 | padding-left: 0.5rem;
30 | padding-right: 0.5rem;
31 | display: flex;
32 | align-items: center;
33 | border-radius: 4px;
34 | justify-content: space-between;
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbox/TextToolbox/TextToolbox.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { ChevronDownIcon } from '@chakra-ui/icons'
3 | import { useCanvasContext } from '@/components/Canvas/hooks'
4 | import { useEffect, useState } from 'react'
5 | import { Popover, PopoverTrigger, PopoverContent, PopoverBody } from '@chakra-ui/react'
6 | import './TextToolbox.scss'
7 | import { useCoreHandler } from '@/components/Canvas/handlers'
8 |
9 | const fontsList = ['Open Sans', 'Lexend', 'Comic Neue', 'Patrick Hand']
10 |
11 | function TextTool() {
12 | const { activeObject } = useCanvasContext()
13 | const { setProperty } = useCoreHandler()
14 | const [options, setOptions] = useState({
15 | fontFamily: 'Lexend',
16 | fontSize: 1,
17 | fontWeight: 2,
18 | textAlign: 'center',
19 | textDecoration: 'none',
20 | })
21 |
22 | useEffect(() => {
23 | if (activeObject) {
24 | const updatedOptions = {
25 | fontFamily: activeObject.fontFamily,
26 | fontSize: activeObject.fontSize,
27 | fontWeight: activeObject.fontWeight,
28 | textAlign: activeObject.textAlign,
29 | }
30 | setOptions({ ...options, ...updatedOptions })
31 | }
32 | }, [activeObject])
33 |
34 | const onChangeFontFamily = fontFamily => {
35 | setProperty('fontFamily', fontFamily)
36 | setOptions({ ...options, fontFamily })
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
{options.fontFamily}
47 |
48 |
49 |
50 |
51 |
52 | {fontsList.map(fontItem => (
53 | onChangeFontFamily(fontItem)}
55 | style={{ fontFamily: fontItem }}
56 | className="list-item"
57 | key={fontItem}
58 | >
59 | {fontItem}
60 |
61 | ))}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | function OpacityIcon() {
76 | return (
77 |
98 | )
99 | }
100 |
101 | function DeleteIcon() {
102 | return (
103 |
109 | )
110 | }
111 | export default TextTool
112 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbox/Toolbox.scss:
--------------------------------------------------------------------------------
1 | .editor-color-holder {
2 | height: 30px;
3 | width: 30px;
4 | }
5 |
6 | .vertical-separator {
7 | background: rgba(57, 76, 96, 0.15);
8 | width: 1px;
9 | height: 30px;
10 | margin: 0 12px;
11 | }
12 |
13 | .editor-toolbox {
14 | height: 54px;
15 | background: #ffffff;
16 | border-bottom: 1px solid rgba(57, 76, 96, 0.15);
17 | padding: 0 0.8rem;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbox/Toolbox.tsx:
--------------------------------------------------------------------------------
1 | import { useCanvasContext } from '@/components/Canvas/hooks'
2 | import DefaultToolbox from './DefaultToolbox/DefaultToolbox'
3 | import TextToolbox from './TextToolbox/TextToolbox'
4 |
5 | import './Toolbox.scss'
6 |
7 | function Toolbox() {
8 | const { activeObject } = useCanvasContext()
9 | if (!activeObject) {
10 | return
11 | }
12 | const activeObjectType = activeObject.type
13 |
14 | return {activeObjectType === 'textbox' ? : }
15 | }
16 |
17 | export default Toolbox
18 |
--------------------------------------------------------------------------------
/src/components/Editor/index.ts:
--------------------------------------------------------------------------------
1 | import Editor from './Editor'
2 |
3 | export default Editor
4 |
--------------------------------------------------------------------------------
/src/i18n/i18nClient.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next'
2 | // import LanguageDetector from "i18next-browser-languagedetector"
3 | import enLocale from './locales/en-EN.json'
4 | import trLocale from './locales/tr-TR.json'
5 |
6 | const i18nClient = i18n.init({
7 | load: 'all',
8 | whitelist: ['en', 'en-US', 'tr', 'tr-TR'],
9 | lng: 'en',
10 | // nonExplicitWhitelist: false,
11 | resources: {
12 | en: {
13 | translation: enLocale,
14 | },
15 | tr: {
16 | translation: trLocale,
17 | },
18 | },
19 | })
20 |
21 | export default i18nClient
22 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | export { default as i18nClient } from "./i18nClient"
2 |
--------------------------------------------------------------------------------
/src/i18n/locales/en-EN.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "panel": {
4 | "items": {
5 | "formats": "Formats",
6 | "templates": "Templates",
7 | "images": "Images",
8 | "text": "Text",
9 | "documents": "Documents",
10 | "objects": "Objects",
11 | "musics": "Music",
12 | "videos": "Videos",
13 | "graphics": "Graphics",
14 | "layers": "layers",
15 | "folders": "Folders",
16 | "help": "Help"
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/i18n/locales/tr-TR.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor": {
3 | "panel": {
4 | "items": {
5 | "formats": "Formatlar",
6 | "templates": "Şablonlar",
7 | "images": "Fotoğraf",
8 | "text": "Metin",
9 | "documents": "Doküman",
10 | "objects": "Nesneler",
11 | "musics": "Müzik",
12 | "videos": "Videolar",
13 | "graphics": "Grafikler",
14 | "layers": "Katmanlar",
15 | "folders": "Dosyalarım",
16 | "help": "Yardım"
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
4 | 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom'
2 | import Editor from '@components/Editor'
3 | import reportWebVitals from './reportWebVitals'
4 | import { CanvasProvider } from '@components/Canvas'
5 | import { ChakraProvider } from '@chakra-ui/react'
6 |
7 | import 'focus-visible/dist/focus-visible'
8 | import './i18n/index'
9 | import './index.css'
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | )
19 |
20 | // If you want to start measuring performance in your app, pass a function
21 | // to log results (for example: reportWebVitals(console.log))
22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
23 | reportWebVitals()
24 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals'
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry)
7 | getFID(onPerfEntry)
8 | getFCP(onPerfEntry)
9 | getLCP(onPerfEntry)
10 | getTTFB(onPerfEntry)
11 | })
12 | }
13 | }
14 |
15 | export default reportWebVitals
16 |
--------------------------------------------------------------------------------
/src/services/iconscout.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const iconscoutClient = axios.create({
4 | baseURL: 'https://api.iconscout.com/v3/',
5 | headers: {
6 | 'Client-ID': process.env.REACT_APP_ICONSCOUT_CLIENT_ID,
7 | 'Client-Secret': process.env.REACT_APP_ICONSCOUT_SECRET,
8 | },
9 | })
10 |
11 | export function getImages(query) {
12 | return new Promise((resolve, reject) => {
13 | iconscoutClient
14 | .get('search', {
15 | params: {
16 | query: query,
17 | product_type: 'item',
18 | asset: 'illustration',
19 | price: 'free',
20 | per_page: 20,
21 | page: 1,
22 | formats: ['svg'],
23 | styles: ['colored-outline'],
24 | sort: 'popular',
25 | },
26 | })
27 | .then(response => {
28 | const items = response.data.response.items.data
29 | resolve(items)
30 | })
31 | .catch(err => {
32 | reject(err)
33 | })
34 | })
35 | }
36 |
37 | export function getImage(uuid): Promise {
38 | return new Promise((resolve, reject) => {
39 | iconscoutClient
40 | .post(`items/${uuid}/api-download`, {
41 | format: 'svg',
42 | })
43 | .then(response => {
44 | const imageURL = response.data.response.download.url
45 | resolve(imageURL)
46 | })
47 | .catch(err => {
48 | reject(err)
49 | })
50 | })
51 | }
52 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom'
6 |
--------------------------------------------------------------------------------
/src/theme/colors.scss:
--------------------------------------------------------------------------------
1 | $dark-1: #0e1419;
2 | $dark-2: #29303a;
3 | // $border-dark: rgba($base-color, 0.88);
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfigExtra.json",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": false,
15 | "forceConsistentCasingInFileNames": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "noEmit": true,
22 | "jsx": "react-jsx"
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfigExtra.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["src/*"],
6 | "@assets/*": ["src/assets/*"],
7 | "@components/*": ["src/components/*"],
8 | "@scenes/*": ["src/scenes/*"],
9 | "@store/*": ["src/store/*"],
10 | "@services/*": ["src/services/*"],
11 | "@utils/*": ["src/utils/*"]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------