├── .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 | ![Editor Preview](https://i.ibb.co/2jZdhXj/preview3.png) 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 |
5 |
FooterMenu
6 |
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 |
7 |
8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | ) 18 | } 19 | 20 | export default Navbar 21 | -------------------------------------------------------------------------------- /src/components/Editor/Navbar/NavbarIcons.tsx: -------------------------------------------------------------------------------- 1 | export function GithubIcon() { 2 | return ( 3 |
4 | 5 | 6 | 7 |
8 | ) 9 | } 10 | 11 | export function DownloadIcon() { 12 | return ( 13 |
14 | 15 | 19 | 20 |
21 | ) 22 | } 23 | 24 | export function LogoIcon() { 25 | return ( 26 | 33 | 34 | 56 | 57 | 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 |
8 |
9 | 10 | 14 | 15 |
16 |
17 | 18 | 25 | 26 |
27 |
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 |
16 | 22 | 23 | 24 |
25 | ) 26 | } 27 | 28 | export default Panels 29 | -------------------------------------------------------------------------------- /src/components/Editor/Panels/PanelsList/PanelItemIcon.tsx: -------------------------------------------------------------------------------- 1 | function TemplateIcon() { 2 | return ( 3 | 4 | 9 | 10 | ) 11 | } 12 | 13 | function ObjectIcon() { 14 | return ( 15 | 16 | 20 | 21 | ) 22 | } 23 | 24 | function TextIcon() { 25 | return ( 26 | 27 | 32 | 33 | ) 34 | } 35 | 36 | function ImageIcon() { 37 | return ( 38 | 39 | 43 | 44 | ) 45 | } 46 | 47 | function VideoIcon() { 48 | return ( 49 | 50 | 56 | 57 | ) 58 | } 59 | 60 | function AudioIcon() { 61 | return ( 62 | 63 | 69 | 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 | color picker 55 | ) : ( 56 |
57 | )} 58 |
59 | 60 | {dropdown.displayColorPicker ? ( 61 |
62 |
63 | 64 |
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 | 78 | 79 | 80 | 84 | 88 | 92 | 96 | 97 | 98 | ) 99 | } 100 | 101 | function DeleteIcon() { 102 | return ( 103 | 104 | 108 | 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 | --------------------------------------------------------------------------------