├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── README.md ├── index.html ├── package.json ├── public └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── lib │ ├── defaultShapes.tsx │ ├── editor.tsx │ ├── index.tsx │ └── types.ts ├── main.tsx └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.test.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0, 33 | "react/jsx-uses-react": "off", 34 | "react/react-in-jsx-scope": "off" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | package-lock.json 27 | bun.lockb 28 | yarn.lock 29 | pnpm-lock.yaml 30 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.9.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | - 16 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "sash.hoverBorder": "#1f6fd0", 4 | "statusBar.background": "#1857a4", 5 | "statusBar.foreground": "#e7e7e7", 6 | "statusBarItem.hoverBackground": "#1f6fd0", 7 | "statusBarItem.remoteBackground": "#1857a4", 8 | "statusBarItem.remoteForeground": "#e7e7e7" 9 | }, 10 | "peacock.color": "#1857a4" 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fabricjs-react 2 | 3 | [![NPM](https://img.shields.io/npm/v/fabricjs-react.svg)](https://www.npmjs.com/package/fabricjs-react) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 4 | 5 | ## Install 6 | 7 | We'll need to install `fabric`, `react` and `react-dom` because are peer dependencies of this library if you haven't yet otherwise install only what you don't have: 8 | 9 | ```bash 10 | npm install --save fabricjs-react fabric react react-dom 11 | ``` 12 | 13 | ## Usage 14 | 15 | Take a look at sandbox: https://codesandbox.io/s/flamboyant-wind-ff3x8 16 | 17 | ```tsx 18 | import React from 'react' 19 | 20 | import { FabricJSCanvas, useFabricJSEditor } from 'fabricjs-react' 21 | 22 | const App = () => { 23 | const { editor, onReady } = useFabricJSEditor() 24 | const onAddCircle = () => { 25 | editor?.addCircle() 26 | } 27 | const onAddRectangle = () => { 28 | editor?.addRectangle() 29 | } 30 | 31 | return ( 32 |
33 | 34 | 35 | 36 |
37 | ) 38 | } 39 | 40 | export default App 41 | ``` 42 | 43 | ## Alternative use cases 44 | 45 | ### Add image ([#3](https://github.com/asotog/fabricjs-react/issues/3)) 46 | 47 | For this case, you have to reference the FabricJS dependency to first load the image: 48 | 49 | ```tsx 50 | import { FabricImage } from "fabric"; // this also installed on your project 51 | import { useFabricJSEditor } from 'fabricjs-react'; 52 | 53 | const { selectedObjects, editor, onReady } = useFabricJSEditor(); 54 | 55 | useEffect(() => { 56 | const loadImage = async () => { 57 | const image = await FabricImage.fromURL( 58 | "https://www.searchenginejournal.com/wp-content/uploads/2019/07/the-essential-guide-to-using-images-legally-online.png" 59 | ); 60 | editor?.canvas.add(image); 61 | } 62 | loadImage() 63 | }, [fabric, editor]) 64 | 65 | ... 66 | ``` 67 | 68 | ## Donations 69 | 70 | Buy Me A Coffee 71 | 72 | ## License 73 | 74 | MIT © [Alejandro Soto](https://github.com/Alejandro Soto) 75 | 76 | Feel free to collaborate. 77 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabricjs-react", 3 | "version": "2.1.0", 4 | "description": "support fabricjs from react", 5 | "author": "Alejandro Soto", 6 | "license": "MIT", 7 | "type": "module", 8 | "files": [ 9 | "dist" 10 | ], 11 | "main": "./dist/fabricjs-react.js", 12 | "types": "./dist/index.d.ts", 13 | "exports": { 14 | ".": { 15 | "import": "./dist/fabricjs-react.js", 16 | "types": "./dist/index.d.ts" 17 | } 18 | }, 19 | "scripts": { 20 | "dev": "vite", 21 | "build": "tsc && vite build", 22 | "preview": "vite preview" 23 | }, 24 | "peerDependencies": { 25 | "fabric": "^5 || ^6", 26 | "react": ">=18.2.0", 27 | "react-dom": ">=18.2.0" 28 | }, 29 | "devDependencies": { 30 | "@types/fabric": "^5.3.7", 31 | "@types/node": "^18.11.9", 32 | "@types/react": "^18.0.24", 33 | "@types/react-dom": "^18.0.8", 34 | "@vitejs/plugin-react": "^2.2.0", 35 | "path": "^0.12.7", 36 | "rollup-plugin-node-externals": "^7.1.3", 37 | "typescript": "^4.6.4", 38 | "vite": "^3.2.3", 39 | "vite-plugin-dts": "^1.7.1" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/asotog/fabricjs-react/issues" 43 | }, 44 | "engines": { 45 | "node": "20" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { FabricJSCanvas, useFabricJSEditor } from './lib' 3 | 4 | function App() { 5 | const { selectedObjects, editor, onReady } = useFabricJSEditor({ 6 | defaultStrokeColor: 'red' 7 | }) 8 | useEffect(() => { 9 | console.log({ editor }) 10 | }, [editor]) 11 | const [text, setText] = useState('') 12 | const [strokeColor, setStrokeColor] = useState('') 13 | const [fillColor, setFillColor] = useState('') 14 | 15 | const onAddCircle = () => { 16 | editor?.addCircle() 17 | } 18 | const onAddRectangle = () => { 19 | editor?.addRectangle() 20 | } 21 | const onAddLine = () => { 22 | editor?.addLine() 23 | } 24 | const onAddText = () => { 25 | if (selectedObjects?.length) { 26 | return editor?.updateText(text) 27 | } 28 | editor?.addText(text) 29 | } 30 | const onSetStrokeColor = () => { 31 | editor?.setStrokeColor(strokeColor) 32 | } 33 | const onSetFillColor = () => { 34 | editor?.setFillColor(fillColor) 35 | } 36 | const onDeleteAll = () => { 37 | editor?.deleteAll() 38 | } 39 | const onDeleteSelected = () => { 40 | editor?.deleteSelected() 41 | } 42 | const onZoomIn = () => { 43 | editor?.zoomIn() 44 | } 45 | const onZoomOut = () => { 46 | editor?.zoomOut() 47 | } 48 | return ( 49 | <> 50 | {editor ? ( 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | setText(e.target.value)} 63 | /> 64 | 65 | setStrokeColor(e.target.value)} 69 | /> 70 | 71 | setFillColor(e.target.value)} 75 | /> 76 | 77 | 78 |
79 |             fillColor: {editor.fillColor}, strokeColor: {editor.strokeColor}
80 |           
81 |
{JSON.stringify(selectedObjects)}
82 |
83 | ) : ( 84 | <>Loading... 85 | )} 86 | 87 | 88 | ) 89 | } 90 | 91 | export default App 92 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/defaultShapes.tsx: -------------------------------------------------------------------------------- 1 | import { DefaultLine } from './types' 2 | 3 | export const STROKE = '#000000' 4 | export const FILL = 'rgba(255, 255, 255, 0.0)' 5 | 6 | export const CIRCLE = { 7 | radius: 20, 8 | left: 100, 9 | top: 100, 10 | fill: FILL, 11 | stroke: STROKE 12 | } 13 | 14 | export const RECTANGLE = { 15 | left: 100, 16 | top: 100, 17 | fill: FILL, 18 | stroke: STROKE, 19 | width: 40, 20 | height: 40, 21 | angle: 0 22 | } 23 | 24 | export const LINE: DefaultLine = { 25 | points: [50, 100, 200, 200], 26 | options: { 27 | left: 170, 28 | top: 150, 29 | stroke: STROKE 30 | } 31 | } 32 | 33 | export const TEXT = { 34 | type: 'text', 35 | left: 100, 36 | top: 100, 37 | fontSize: 16, 38 | fontFamily: 'Arial', 39 | fill: STROKE 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/editor.tsx: -------------------------------------------------------------------------------- 1 | import * as fabric from 'fabric' 2 | import { CIRCLE, RECTANGLE, LINE, TEXT, FILL, STROKE } from './defaultShapes' 3 | import { useEffect, useMemo, useState } from 'react' 4 | 5 | /** 6 | * Creates editor 7 | */ 8 | const buildEditor = ( 9 | canvas: fabric.Canvas, 10 | fillColor: string, 11 | strokeColor: string, 12 | _setFillColor: (color: string) => void, 13 | _setStrokeColor: (color: string) => void, 14 | scaleStep: number 15 | ): FabricJSEditor => { 16 | return { 17 | canvas, 18 | addCircle: () => { 19 | const object = new fabric.Circle({ 20 | ...CIRCLE, 21 | fill: fillColor, 22 | stroke: strokeColor 23 | }) 24 | canvas.add(object) 25 | }, 26 | addRectangle: () => { 27 | const object = new fabric.Rect({ 28 | ...RECTANGLE, 29 | fill: fillColor, 30 | stroke: strokeColor 31 | }) 32 | canvas.add(object) 33 | }, 34 | addLine: () => { 35 | const object = new fabric.Line(LINE.points, { 36 | ...LINE.options, 37 | stroke: strokeColor 38 | }) 39 | canvas.add(object) 40 | }, 41 | addText: (text: string) => { 42 | // use stroke in text fill, fill default is most of the time transparent 43 | const object = new fabric.Textbox(text, { ...TEXT, fill: strokeColor }) 44 | object.set({ text: text }) 45 | canvas.add(object) 46 | }, 47 | updateText: (text: string) => { 48 | const objects: any[] = canvas.getActiveObjects() 49 | if (objects.length && objects[0].type === TEXT.type) { 50 | const textObject: fabric.Textbox = objects[0] 51 | textObject.set({ text }) 52 | canvas.renderAll() 53 | } 54 | }, 55 | deleteAll: () => { 56 | canvas.getObjects().forEach((object) => canvas.remove(object)) 57 | canvas.discardActiveObject() 58 | canvas.renderAll() 59 | }, 60 | deleteSelected: () => { 61 | canvas.getActiveObjects().forEach((object) => canvas.remove(object)) 62 | canvas.discardActiveObject() 63 | canvas.renderAll() 64 | }, 65 | fillColor, 66 | strokeColor, 67 | setFillColor: (fill: string) => { 68 | _setFillColor(fill) 69 | canvas.getActiveObjects().forEach((object) => object.set({ fill })) 70 | canvas.renderAll() 71 | }, 72 | setStrokeColor: (stroke: string) => { 73 | _setStrokeColor(stroke) 74 | canvas.getActiveObjects().forEach((object) => { 75 | if (object.type === TEXT.type) { 76 | // use stroke in text fill 77 | object.set({ fill: stroke }) 78 | return 79 | } 80 | object.set({ stroke }) 81 | }) 82 | canvas.renderAll() 83 | }, 84 | zoomIn: () => { 85 | const zoom = canvas.getZoom() 86 | canvas.setZoom(zoom / scaleStep) 87 | }, 88 | zoomOut: () => { 89 | const zoom = canvas.getZoom() 90 | canvas.setZoom(zoom * scaleStep) 91 | } 92 | } 93 | } 94 | 95 | const useFabricJSEditor = ( 96 | props: FabricJSEditorHookProps = {} 97 | ): FabricJSEditorHook => { 98 | const scaleStep = props.scaleStep || 0.5 99 | const { defaultFillColor, defaultStrokeColor } = props 100 | const [canvas, setCanvas] = useState(null) 101 | const [fillColor, setFillColor] = useState(defaultFillColor || FILL) 102 | const [strokeColor, setStrokeColor] = useState( 103 | defaultStrokeColor || STROKE 104 | ) 105 | const [selectedObjects, setSelectedObject] = useState([]) 106 | useEffect(() => { 107 | const bindEvents = (canvas: fabric.Canvas) => { 108 | canvas.on('selection:cleared', () => { 109 | setSelectedObject([]) 110 | }) 111 | canvas.on('selection:created', (e: any) => { 112 | setSelectedObject(e.selected) 113 | }) 114 | canvas.on('selection:updated', (e: any) => { 115 | setSelectedObject(e.selected) 116 | }) 117 | } 118 | if (canvas) { 119 | bindEvents(canvas) 120 | } 121 | }, [canvas]) 122 | 123 | const editor = useMemo( 124 | () => 125 | canvas 126 | ? buildEditor( 127 | canvas, 128 | fillColor, 129 | strokeColor, 130 | setFillColor, 131 | setStrokeColor, 132 | scaleStep 133 | ) 134 | : undefined, 135 | [canvas] 136 | ) 137 | return { 138 | selectedObjects, 139 | onReady: (canvasReady: fabric.Canvas): void => { 140 | console.log('Fabric canvas ready') 141 | setCanvas(canvasReady) 142 | }, 143 | editor 144 | } 145 | } 146 | 147 | export { buildEditor, useFabricJSEditor } 148 | export type { FabricJSEditorHook } 149 | 150 | export interface FabricJSEditor { 151 | canvas: fabric.Canvas 152 | addCircle: () => void 153 | addRectangle: () => void 154 | addLine: () => void 155 | addText: (text: string) => void 156 | updateText: (text: string) => void 157 | deleteAll: () => void 158 | deleteSelected: () => void 159 | fillColor: string 160 | strokeColor: string 161 | setFillColor: (color: string) => void 162 | setStrokeColor: (color: string) => void 163 | zoomIn: () => void 164 | zoomOut: () => void 165 | } 166 | 167 | interface FabricJSEditorState { 168 | editor?: FabricJSEditor 169 | } 170 | 171 | interface FabricJSEditorHook extends FabricJSEditorState { 172 | selectedObjects?: fabric.Object[] 173 | onReady: (canvas: fabric.Canvas) => void 174 | } 175 | 176 | interface FabricJSEditorHookProps { 177 | defaultFillColor?: string 178 | defaultStrokeColor?: string 179 | scaleStep?: number 180 | } 181 | -------------------------------------------------------------------------------- /src/lib/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import * as fabric from 'fabric' 3 | import { useFabricJSEditor, FabricJSEditor, FabricJSEditorHook } from './editor' 4 | 5 | export interface Props { 6 | className?: string 7 | onReady?: (canvas: fabric.Canvas) => void 8 | } 9 | 10 | /** 11 | * Fabric canvas as component 12 | */ 13 | const FabricJSCanvas = ({ className, onReady }: Props) => { 14 | const canvasEl = useRef(null) 15 | const canvasElParent = useRef(null) 16 | useEffect(() => { 17 | const canvas = new fabric.Canvas(canvasEl.current ?? undefined) 18 | const setCurrentDimensions = () => { 19 | canvas.setHeight(canvasElParent.current?.clientHeight || 0) 20 | canvas.setWidth(canvasElParent.current?.clientWidth || 0) 21 | canvas.renderAll() 22 | } 23 | const resizeCanvas = () => { 24 | setCurrentDimensions() 25 | } 26 | setCurrentDimensions() 27 | 28 | window.addEventListener('resize', resizeCanvas, false) 29 | 30 | if (onReady) { 31 | onReady(canvas) 32 | } 33 | 34 | return () => { 35 | canvas.dispose() 36 | window.removeEventListener('resize', resizeCanvas) 37 | } 38 | }, []) 39 | return ( 40 |
41 | 42 |
43 | ) 44 | } 45 | 46 | export { FabricJSCanvas, useFabricJSEditor } 47 | export type { FabricJSEditor, FabricJSEditorHook } 48 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { FabricObjectProps } from 'fabric/*' 2 | 3 | export type DefaultLine = { 4 | points: [number, number, number, number] 5 | options: Partial 6 | } 7 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | 5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 6 | 7 | 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import dts from 'vite-plugin-dts' 4 | import react from '@vitejs/plugin-react' 5 | import nodeExternals from 'rollup-plugin-node-externals' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | nodeExternals(), 11 | react(), 12 | dts({ 13 | insertTypesEntry: true, 14 | include: ['src/lib/**/*.ts', 'src/lib/**/*.tsx'], 15 | }) 16 | ], 17 | build: { 18 | lib: { 19 | entry: resolve(__dirname, 'src/lib/index.tsx'), 20 | formats: ['es'] 21 | }, 22 | copyPublicDir: false 23 | } 24 | }) 25 | --------------------------------------------------------------------------------