react-native-draw-board v0.0.7-beta.0
Installation
yarn add react-native-draw-board
package is use some peer dependencies, that need to be installed manually (if not installed):
@react-native-community/slider
react-native-svg
Web additional steps
- manually install packages:
react-native
react-native-web
React Native additional steps
- follow installation steps for
@flyskywhy/react-native-gcanvas
- manually install packages (if not installed):
@react-native-async-storage/async-storage
react-native-gesture-handler
Usage
import React from "react";
import { DrawBoard } from "react-native-draw-board";
ReactDOM.render(<DrawBoard />, document.getElementById("root"));
Glossary
Coordinates:
x: number
y: number
Point:
x: number
y: number
t: number
- timestamp of event
Shape:
name: string
brushRadius: number
brushColor: string
points: Point[]
id?: string
- uniq id of shapeextra?: Record<string, any>
- extra data saved with shape, use for text shape
CachedHistory:
bookmarks: Bookmark[]
- technical array using for quicker navigation in historycache: Shape[]
currentIndex: number
- index with which history savedwidth: number
- sizes of original canvas in which drawing createdheight: number
GridState:
gridSize: number
color: string
secondColor: string
gridType:
GridMethodName
DrawAreaState:
scale: number
offset:
Coordinates
shapeName:
ToolName
brushRadius: number
brushColor: number
grid:
GridState
Props
All props are optional
Tools
import { TOOLS } from 'react-native-draw-board'
const TOOLS = {
FREE: '_free', //free pen drawing
LINE: '_line',
CIRCLE: '_circle',
RECTANGLE: '_rectangle',
TRIANGLE_RIGHT: '_triangleRight',
TRIANGLE_SYMMETRIC_VERTICAL: '_triangleSymmetricVertical',
TRIANGLE_SYMMETRIC_HORIZONTAL: '_triangleSymmetricHorizontal',
ERASE: '_erase',
TEXT: '_text',
CLEAR: '_clear',
}
Grid methods
For set up custom grid state use static methods of Grid class Note: if grid props is settled, grid tool hides in toolbar and user won't be able to change grid by himself
Grid.checkered
Grid.checkeredInverted
Grid.dotted
Grid.lined
Grid.blank
- use this method to hide grid
By default, Grid methods have their own grid props, it is possible to create grid method with custom props using Grid.setUp
method
const newGrid = Grid.setUp(Grid.checkeredInverted, {
gridSize: 15,
color: "blue", //color of grid
secondColor: "red", //color of background
})
If you'd like to use custom settings for Grid method with Grid.setUp
,
consider to wrap method in useCallback
hook for performance optimisations
import React, {useState, useCallback} from 'react'
import { DrawBoard, Grid } from 'react-native-draw-board'
...
const [gridSize, setGridSize] = useState(2)
const redLinedGrid = useCallback(Grid.setUp(Grid.lined, {
gridSize,
color: 'red'
}), [gridSize])
return (<DrawBoard grid={redLinedGrid} /> )
Shapes watching
If you need to know about changes happening on the board, consider to create watcher and pass it to watchers props.
For optimise draw board re-renders consider wrapping watcher in useMemo
hook
Possible watchers: ICacheWatcher
| onChange
| (ICacheWatcher | onChange)[]
ICacheWatcher:
onSet(current: Shape[], full: Shape[])
: fire each time when full history changes, e.g. change persist driver or callclear({full: true})
.current
: represent current state of history as it see user (with current index)full
: represent full history, even with element that hidden from user, because history index is less then length of historyonAdd(added: Shape, index: number)
: fire each time when something add to historyval
: a new element, corresponds to Shape interfaceindex
: index of this element in history (new history length - 1)onBack(full: Shape[], removed: Shape)
: fire when user go back in historyfull
: new historyremoved
: element that disappear from user vision, but it still in historyonForward(added: Shape, index: number)
: same as onAdd, but fire when user go forward in historyonChange(full: Shape[], index: number)
: fire each time when something happen with historyfull
current state of history (even with hidden elements)index
current index of history, separate visible elements from hidden (all elements after index is hidden from user)
import React from 'react'
import { DrawBoard, ShapeWatcher } from 'react-native-draw-board'
...
const [shapesCount, setShapesCount] = useState(0)
const watchers = useMemo(() => {
//It is possible to create shape watcher with different ways:
//Passing props to constructor:
const watcher = new ShapeWatcher({
onChange(history){
setShapesCount(history.length)
} ,
onSet(history){
console.log(history)
}
})
//Calling corresponding method:
const anotherOneWatcher = new ShapeWatcher()
.onChange((history) => {
//Fire on each change!
console.log(history)
})
.onSet((history) => {
//Fire on history set
console.log(history)
})
.onAdd(() => {})
return [watcher, anotherOneWatcher]
}, [])
return (<DrawBoard watchers={watchers} /> )
You also can use function that corresponds to onChange
method
import { DrawBoard } from 'react-native-draw-board'
...
const [shapesCount, setShapesCount] = useState(0)
const watcher = useCallback((history) => {
setShapesCount(history.length)
}, [])
return (<DrawBoard watchers={watcher} /> )
ShapeCounter
You can use ShapeCounter class represents ShapeWatcher interface, this class create object that counts different types of shape and fire two types of events
const watcher = new ShapeCounter(countTo?: Record<ToolName, number>)
Props:
countTo
(optional) represent an object where key is ToolName and value is number to which the counter will countIf not passed, will count all shapes, but onReach event won't fire
Methods:
watchChanged(listener: ({[key: ToolName]: number}) => void)
: subscribe to changes of shapes count
watchReached(listener: ({[key: ToolName]: boolean}) => void)
: subscribe to shapes count reached value, that set in constructor.
import { DrawBoard, ShapeCounter, TOOLS } from 'react-native-draw-board'
...
const [isShapesEnough, setIsShapesEnough] = useState(false)
const [totalShapes, setTotalShapes] = useState(null)
const watcher = useMemo(() => {
const counter = new ShapeCounter({
'total': 200,
[TOOLS.CIRCLE]: 20,
[TOOLS.RECTANGLE]: 33,
})
//fires only when state change, and pass only shapes that changed
counter.watchReached((elements) => {
//{
// total: true (it means total equal or more than 200
//}
if(total in elements) setIsShapesEnough(elements.total)
})
//fires on each shapes change, if total is set, it will always contain total prop
counter.watchChanged((elements) => {
//{
// circle: 23,
// total: 123
//}
setTotalShapes((state) => {
if(!state) return elements
return {...state, ...elements}
})
})
return counter
}, [])
return (<DrawBoard watchers={watcher} /> )
History Persist
If you want save or load history of draw board, you can create persistDriver and pass to persistDriver prop.
Possible persist driver: IPersistDriver
| IPersistDriver[]
IPersistDriver:
save(data: string): void
will call for saving data, in data prop it pass uncompressed history
load(): Promise<string>
will call for load data to draw area, it should return Promise that resolve string, resolved value should represent valid CachedHistory
saveState(data: DrawAreaState): void
will call for saving draw area state
loadState(): Promise<DrawAreaState>
will call for load draw area state
Inside draw board history loader work with useEffect hook, like
const driver = new PersistDriver()
useEffect(() => {
driver.load()
return () => driver.save(drawBoardHistory)
}, [persistDriver])
Data will save each time persistDriver change, so consider to wrap persistDriver to useMemo hook Also data will save each time user lost focus of screen e.g. close app. If you pass array of drivers, draw board will looking for first non-empty value for load state, but will save data with each driver:
import {
DrawBoard,
LocalStorageDriver,
PersistDriver
} from 'react-native-draw-board'
...
const drivers = useMemo(() => {
const localStorageDriver = new LocalStorageDriver({key: 'history'})
const oneMoreDriver = new PersistDriver(...some props)
return [localStorageDriver, oneMoreDriver]
}, [])
return (<DrawBoard persistDriver={drivers} /> )
PersistDriver
Props:
save
: (optional) (data: string) => void
load
: (optional) () => Promise\
withCompression
: (optional) boolean - compress data before passing to save/load method
Saving image of draw board:
import React, { useMemo, useRef } from 'react'
import { DrawBoard, PersistDriver } from 'react-native-draw-board'
...
const controller = useRef(null)
const drivers = useMemo(() => {
return new PersistDriver({
withCompression: true,
save() {
if (!controller.current) return
constroller.current.getImage().then((image) => {
try {
const blob = base64ToBlob(
image.replace('data:image/png;base64,', ''),
'image/png'
)
const imageFile = new File([blob], `file.png`, {
type: 'image/png',
})
const formData = new FormData()
formData.append('images', imageFile)
return fetch('send url', { body: formData, method: 'POST' })
} catch (e) {}
})
},
})
}, [])
return (
<DrawBoard
persistDriver={drivers}
controller={controller}
/>
)
LocalStorageDriver
Use LocalStorageDriver if you want to save history to local storage, in web it use localstorage, in ReactNative app it use AsyncStorage
Props:
key
(optional if stateKey settled): string - key where history records
stateKey
(optional if key settled): string - key where board state records
pack?
: (data: string) => object. Callback that will call before saving, usefull if you doesn't save data straight in key
unpack?
: (data: object) => string. Callback that will call after getting data from localstorge and before load to history
import React, { useMemo } from 'react'
import { DrawBoard, LocalStorageDriver } from 'react-native-draw-board'
const PREVIOUS_DRAWING = 'previousDrawing' //key in localstorage
...
const [currentExercise, setCurrentExercise] = useState({id: 'id'}) //any string
const driver = useMemo(() => {
const {
currentProblemId, // any string
currentSubProblemId, // any string
} = getCurrentProblemAndSubProblemId();
return new LocalStorageDriver({
key: PREVIOUS_DRAWING,
pack(drawing) {
return {
draw: {
[state.currentExercise._id]: {
[currentProblemId]: currentSubProblemId
? {
[currentSubProblemId]: drawing,
}
: drawing,
},
},
};
},
unpack(parsed) {
const previousDrawing = parsed["draw"];
const currentExerciseDrawing =
previousDrawing[currentExercise._id];
if (!currentExerciseDrawing) return "";
const currentProblemDrawing = currentExerciseDrawing[currentProblemId];
if (!currentProblemDrawing) return "";
if (!currentSubProblemId) return currentProblemDrawing;
const currentSubProblemDrawing =
currentProblemDrawing[currentSubProblemId];
if (!currentSubProblemDrawing) return "";
return currentSubProblemDrawing;
},
});
}, [getCurrentProblemAndSubProblemId, currentExercise._id]);
return (<DrawBoard persistDriver={driver} />)
StringStorageDriver
Simple driver that load CachedHistory as COMPRESSED string to draw board, and does not save anything
Props:
src
: string - compressed and valid CachedHistory
import React, { useMemo } from 'react'
import { DrawBoard, StringPersistDriver } from 'react-native-draw-board'
...
const [src, setSrc] = useState('')
const sourceBoard = useRef(null)
const targetDriver = useMemo(() => {
return new StringPersistDriver({ src });
}, [src]);
useEffect(() => {
const interval = setInterval(() => {
if(!sourceBoard.current) return
setSrc(sourceBoard.current.getHistory({ compression: true }))
}, 3000)
return () => clearInterval(interval)
}, [])
return (
<>
<DrawBoard controller={sourceBoard} />
<DrawBoard persistDriver={targetDriver} />
</>
)
Magma specific drivers
Here is some Magma specific drivers
ServerDriver
For saving data to server endpoint use Magma.AxiosPersistDriver
under the hood it use methods of /drawing-history
end-point
Props:
api
: axios instance with which you make requests to server
exerciseId
, problemId
, subProblemId (optional)
: discribe current problem
import React, { useMemo } from 'react'
import { DrawBoard, Magma } from 'react-native-draw-board'
...
const studentsApi = axios.create({
baseURL: "SOME_URL"
});
const [exerciseId, setExerciseId] = useState('')
const [currentProblemId, setCurrentProblemId] = useState('')
const [currentSubProblemId, setSubProblemId] = useState('')
const persistDriver = useMemo(() => {
const serverPersistDriver = new Magma.ServerPersistDriver({
api: studentsApi,
exerciseId,
problemId: currentProblemId,
subProblemId: currentSubProblemId,
});
}, [exerciseId, currentProblemId, currentSubProblemId])
return (<DrawBoard persistDriver={persistDriver} />)
Board controller
clear(props?: ClearProps)
clear all elements on board, clear will record to history Props:preventSave?: boolean
- prevent clear from saving to historyfull?: boolean
- clear also history of drawingsgetHistory(props?: HistoryProps)
save history as string in format of CachedHistory Props:compression?: boolean
- If passed, history will compress with pieroxy/lz-stringloadHistory(data: string, props?: HistoryProps)
loads a previously saved drawing using the getHistory string, if history compressed, you should pass compressed flag in HistoryPropsgetState()
save current history state, return DrawAreaStategetImage()
Return Promise that resolves string, representing image data of canvas.scale(multiplier: number)
scale drawing areamove(offset: Coordinates)
move drawing area