import * as React from 'react' ; import { Canvas , CanvasRef } from '../Canvas' ; import { CanvasPath , ExportImageType , Point } from '../types' ; export type ReactSketchCanvasStates = { } ; export interface ReactSketchCanvasProps { id ? : string ; width ? : string ; height ? : string ; className ? : string ; strokeColor ? : string ; canvasColor ? : string ; backgroundImage ? : string ; exportWithBackgroundImage ? : boolean ; preserveBackgroundImageAspectRatio ? : string ; strokeWidth ? : number ; eraserWidth ? : number ; allowOnlyPointerType ? : string ; onChange ? : ( updatedPaths : CanvasPath [ ] ) => void ; onStroke ? : ( path : CanvasPath , isEraser : boolean ) => void ; style ? : React . CSSProperties ; withTimestamp ? : boolean ; } export interface ReactSketchCanvasRef { eraseMode : ( erase : boolean ) => void ; clearCanvas : ( ) => void ; undo : ( ) => void ; redo : ( ) => void ; exportImage : ( imageType : ExportImageType ) => Promise < string > ; exportSvg : ( ) => Promise < string > ; exportPaths : ( ) => Promise < CanvasPath [ ] > ; loadPaths : ( paths : CanvasPath [ ] ) => void ; getSketchingTime : ( ) => Promise < number > ; resetCanvas : ( ) => void ; } export const ReactSketchCanvas = React . forwardRef < ReactSketchCanvasRef , ReactSketchCanvasProps > ( ( props , ref ) => { const { id = 'react-sketch-canvas' , width = '100%' , height = '100%' , className = '' , canvasColor = 'white' , strokeColor = 'red' , backgroundImage = '' , exportWithBackgroundImage = false , preserveBackgroundImageAspectRatio = 'none' , strokeWidth = 4 , eraserWidth = 8 , allowOnlyPointerType = 'all' , style = { border : '0.0625rem solid #9c9c9c' , borderRadius : '0.25rem' , } , onChange = ( _paths ) : void => { } , onStroke = ( _path , _isEraser ) : void => { } , withTimestamp = false , } = props ; const svgCanvas = React . createRef < CanvasRef > ( ) ; const [ drawMode , setDrawMode ] = React . useState < boolean > ( true ) ; const [ isDrawing , setIsDrawing ] = React . useState < boolean > ( false ) ; const [ resetStack , setResetStack ] = React . useState < CanvasPath [ ] > ( [ ] ) ; const [ undoStack , setUndoStack ] = React . useState < CanvasPath [ ] > ( [ ] ) ; const [ currentPaths , setCurrentPaths ] = React . useState < CanvasPath [ ] > ( [ ] ) ; const liftStrokeUp = React . useCallback ( ( ) : void => { const lastStroke = currentPaths . slice ( - 1 ) ?. [ 0 ] ?? null ; if ( lastStroke === null ) { console . warn ( 'No stroke found!' ) ; return ; } onStroke ( lastStroke , ! lastStroke . drawMode ) ; } , [ isDrawing ] ) ; React . useEffect ( ( ) => { liftStrokeUp ( ) ; } , [ isDrawing ] ) ; React . useEffect ( ( ) => { onChange ( currentPaths ) ; } , [ currentPaths ] ) ; React . useImperativeHandle ( ref , ( ) => ( { eraseMode : ( erase ) : void => { setDrawMode ( ! erase ) ; } , clearCanvas : ( ) : void => { setResetStack ( [ ... currentPaths ] ) ; setCurrentPaths ( [ ] ) ; } , undo : ( ) : void => { if ( resetStack . length !== 0 ) { setCurrentPaths ( [ ... resetStack ] ) ; setResetStack ( [ ] ) ; return ; } setUndoStack ( ( undoStack ) => [ ... undoStack , ... currentPaths . slice ( - 1 ) ] ) ; setCurrentPaths ( ( currentPaths ) => currentPaths . slice ( 0 , - 1 ) ) ; } , redo : ( ) : void => { if ( undoStack . length === 0 ) return ; setCurrentPaths ( ( currentPaths ) => [ ... currentPaths , ... undoStack . slice ( - 1 ) , ] ) ; setUndoStack ( ( undoStack ) => undoStack . slice ( 0 , - 1 ) ) ; } , exportImage : ( imageType ) : Promise < string > => { const exportImage = svgCanvas . current ?. exportImage ; if ( ! exportImage ) { throw Error ( 'Export function called before canvas loaded' ) ; } else { return exportImage ( imageType ) ; } } , exportSvg : ( ) : Promise < string > => { return new Promise < string > ( ( resolve , reject ) => { const exportSvg = svgCanvas . current ?. exportSvg ; if ( ! exportSvg ) { reject ( Error ( 'Export function called before canvas loaded' ) ) ; } else { exportSvg ( ) . then ( ( data ) => { resolve ( data ) ; } ) . catch ( ( e ) => { reject ( e ) ; } ) ; } } ) ; } , exportPaths : ( ) : Promise < CanvasPath [ ] > => { return new Promise < CanvasPath [ ] > ( ( resolve , reject ) => { try { resolve ( currentPaths ) ; } catch ( e ) { reject ( e ) ; } } ) ; } , loadPaths : ( paths ) : void => { setCurrentPaths ( ( currentPaths ) => [ ... currentPaths , ... paths ] ) ; } , getSketchingTime : ( ) : Promise < number > => { return new Promise < number > ( ( resolve , reject ) => { if ( ! withTimestamp ) { reject ( new Error ( "Set 'withTimestamp' prop to get sketching time" ) ) ; } try { const sketchingTime = currentPaths . reduce ( ( totalSketchingTime , path ) => { const startTimestamp = path . startTimestamp ?? 0 ; const endTimestamp = path . endTimestamp ?? 0 ; return totalSketchingTime + ( endTimestamp - startTimestamp ) ; } , 0 ) ; resolve ( sketchingTime ) ; } catch ( e ) { reject ( e ) ; } } ) ; } , resetCanvas : ( ) : void => { setResetStack ( [ ] ) ; setUndoStack ( [ ] ) ; setCurrentPaths ( [ ] ) ; } , } ) ) ; const handlePointerDown = ( point ) : void => { setIsDrawing ( true ) ; setUndoStack ( [ ] ) ; let stroke = { drawMode : drawMode , strokeColor : drawMode ? strokeColor : '#000000' , strokeWidth : drawMode ? strokeWidth : eraserWidth , paths : [ point ] , } ; if ( withTimestamp ) { stroke = { ... stroke , startTimestamp : Date . now ( ) , endTimestamp : 0 , } ; } setCurrentPaths ( ( currentPaths ) => [ ... currentPaths , stroke ] ) ; } ; const handlePointerMove = ( point ) : void => { if ( ! isDrawing ) return ; const currentStroke = currentPaths . slice ( - 1 ) [ 0 ] ; const updatedStroke = { ... currentStroke , paths : [ ... currentStroke . paths , point ] , } ; setCurrentPaths ( ( currentPaths ) => [ ... currentPaths . slice ( 0 , - 1 ) , updatedStroke , ] ) ; } ; const handlePointerUp = ( ) : void => { if ( ! isDrawing ) { return ; } setIsDrawing ( false ) ; if ( ! withTimestamp ) { return ; } let currentStroke = currentPaths . slice ( - 1 ) ?. [ 0 ] ?? null ; if ( currentStroke === null ) { return ; } const updatedStroke = { ... currentStroke , endTimestamp : Date . now ( ) , } ; setCurrentPaths ( ( currentPaths ) => [ ... currentPaths . slice ( 0 , - 1 ) , updatedStroke , ] ) ; } ; return ( < Canvas ref = { svgCanvas } id = { id } width = { width } height = { height } className = { className } canvasColor = { canvasColor } backgroundImage = { backgroundImage } exportWithBackgroundImage = { exportWithBackgroundImage } preserveBackgroundImageAspectRatio = { preserveBackgroundImageAspectRatio } allowOnlyPointerType = { allowOnlyPointerType } style = { style } paths = { currentPaths } isDrawing = { isDrawing } onPointerDown = { handlePointerDown } onPointerMove = { handlePointerMove } onPointerUp = { handlePointerUp } / > ) ; } ) ;