├── .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 | [](https://www.npmjs.com/package/fabricjs-react) [](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 |
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 |
--------------------------------------------------------------------------------