import * as React from 'react'; import { Canvas, CanvasRef } from '../Canvas'; import { CanvasPath, ExportImageType, Point } from '../types'; export type ReactSketchCanvasStates = {}; /* Props validation */ 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; svgStyle?: React.CSSProperties; withTimestamp?: boolean; } export interface ReactSketchCanvasRef { eraseMode: (erase: boolean) => void; clearCanvas: () => void; undo: () => void; redo: () => void; exportImage: (imageType: ExportImageType) => Promise; exportSvg: () => Promise; exportPaths: () => Promise; loadPaths: (paths: CanvasPath[]) => void; getSketchingTime: () => Promise; 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', }, svgStyle = {}, onChange = (_paths: CanvasPath[]): void => {}, onStroke = (_path: CanvasPath, _isEraser: boolean): void => {}, withTimestamp = false, } = props; const svgCanvas = React.createRef(); const [drawMode, setDrawMode] = React.useState(true); const [isDrawing, setIsDrawing] = React.useState(false); const [resetStack, setResetStack] = React.useState([]); const [undoStack, setUndoStack] = React.useState([]); const [currentPaths, setCurrentPaths] = React.useState([]); 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: boolean): void => { setDrawMode(!erase); }, clearCanvas: (): void => { setResetStack([...currentPaths]); setCurrentPaths([]); }, undo: (): void => { // If there was a last reset then if (resetStack.length !== 0) { setCurrentPaths([...resetStack]); setResetStack([]); return; } setUndoStack((undoStack) => [...undoStack, ...currentPaths.slice(-1)]); setCurrentPaths((currentPaths) => currentPaths.slice(0, -1)); }, redo: (): void => { // Nothing to Redo if (undoStack.length === 0) return; setCurrentPaths((currentPaths) => [ ...currentPaths, ...undoStack.slice(-1), ]); setUndoStack((undoStack) => undoStack.slice(0, -1)); }, exportImage: (imageType: ExportImageType): Promise => { const exportImage = svgCanvas.current?.exportImage; if (!exportImage) { throw Error('Export function called before canvas loaded'); } else { return exportImage(imageType); } }, exportSvg: (): Promise => { return new Promise((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 => { return new Promise((resolve, reject) => { try { resolve(currentPaths); } catch (e) { reject(e); } }); }, loadPaths: (paths: CanvasPath[]): void => { setCurrentPaths((currentPaths) => [...currentPaths, ...paths]); }, getSketchingTime: (): Promise => { return new Promise((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: Point): void => { setIsDrawing(true); setUndoStack([]); let stroke: CanvasPath = { drawMode: drawMode, strokeColor: drawMode ? strokeColor : '#000000', // Eraser using mask strokeWidth: drawMode ? strokeWidth : eraserWidth, paths: [point], }; if (withTimestamp) { stroke = { ...stroke, startTimestamp: Date.now(), endTimestamp: 0, }; } setCurrentPaths((currentPaths) => [...currentPaths, stroke]); }; const handlePointerMove = (point: 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 ( ); });