0.0.7-beta.0 • Published 2 years ago

react-native-draw-board v0.0.7-beta.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

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

React Native additional steps

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 shape
  • extra?: Record<string, any> - extra data saved with shape, use for text shape
CachedHistory:
  • bookmarks: Bookmark[] - technical array using for quicker navigation in history
  • cache: Shape[]
  • currentIndex: number - index with which history saved
  • width: number - sizes of original canvas in which drawing created
  • height: number
GridState:
  • gridSize: number
  • color: string
  • secondColor: string
  • gridType:GridMethodName

DrawAreaState:

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 call clear({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 history

  • onAdd(added: Shape, index: number): fire each time when something add to history val: a new element, corresponds to Shape interface index: index of this element in history (new history length - 1)

  • onBack(full: Shape[], removed: Shape): fire when user go back in history full: new history removed: element that disappear from user vision, but it still in history

  • onForward(added: Shape, index: number): same as onAdd, but fire when user go forward in history

  • onChange(full: Shape[], index: number): fire each time when something happen with history full 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 history full?: boolean - clear also history of drawings

  • getHistory(props?: HistoryProps) save history as string in format of CachedHistory Props: compression?: boolean - If passed, history will compress with pieroxy/lz-string

  • loadHistory(data: string, props?: HistoryProps) loads a previously saved drawing using the getHistory string, if history compressed, you should pass compressed flag in HistoryProps

  • getState() save current history state, return DrawAreaState
  • getImage() Return Promise that resolves string, representing image data of canvas.
  • scale(multiplier: number) scale drawing area
  • move(offset: Coordinates) move drawing area