import * as React from 'react'; import Paths, { SvgPath } from '../Paths'; import { CanvasPath, ExportImageType, Point } from '../types'; const loadImage = (url: string): Promise => new Promise((resolve, reject) => { const img = new Image(); img.addEventListener('load', () => { if (img.width > 0) { resolve(img); } reject('Image not found'); }); img.addEventListener('error', (err) => reject(err)); img.src = url; img.setAttribute('crossorigin', 'anonymous'); }); function getCanvasWithViewBox(canvas: HTMLDivElement) { const svgCanvas = canvas.firstChild?.cloneNode(true) as SVGElement; const width = canvas.offsetWidth; const height = canvas.offsetHeight; svgCanvas.setAttribute('viewBox', `0 0 ${width} ${height}`); svgCanvas.setAttribute('width', width.toString()); svgCanvas.setAttribute('height', height.toString()); return { svgCanvas, width, height }; } export interface CanvasProps { paths: CanvasPath[]; isDrawing: boolean; onPointerDown: (point: Point) => void; onPointerMove: (point: Point) => void; onPointerUp: () => void; className?: string; id?: string; width: string; height: string; canvasColor: string; backgroundImage: string; exportWithBackgroundImage: boolean; preserveBackgroundImageAspectRatio: string; allowOnlyPointerType: string; style: React.CSSProperties; svgStyle: React.CSSProperties; } export interface CanvasRef { exportImage: (imageType: ExportImageType) => Promise; exportSvg: () => Promise; } export const Canvas = React.forwardRef((props, ref) => { const { paths, isDrawing, onPointerDown, onPointerMove, onPointerUp, id = 'react-sketch-canvas', width = '100%', height = '100%', className = 'react-sketch-canvas', canvasColor = 'red', backgroundImage = '', exportWithBackgroundImage = false, preserveBackgroundImageAspectRatio = 'none', allowOnlyPointerType = 'all', style = { border: '0.0625rem solid #9c9c9c', borderRadius: '0.25rem', }, svgStyle = {}, } = props; const canvasRef = React.useRef(null); // Converts mouse coordinates to relative coordinate based on the absolute position of svg const getCoordinates = ( pointerEvent: React.PointerEvent ): Point => { const boundingArea = canvasRef.current?.getBoundingClientRect(); const scrollLeft = window.scrollX ?? 0; const scrollTop = window.scrollY ?? 0; if (!boundingArea) { return { x: 0, y: 0 }; } const point: Point = { x: pointerEvent.pageX - boundingArea.left - scrollLeft, y: pointerEvent.pageY - boundingArea.top - scrollTop, }; return point; }; /* Mouse Handlers - Mouse down, move and up */ const handlePointerDown = ( event: React.PointerEvent ): void => { // Allow only chosen pointer type if ( allowOnlyPointerType !== 'all' && event.pointerType !== allowOnlyPointerType ) { return; } if (event.pointerType === 'mouse' && event.button !== 0) return; const point = getCoordinates(event); onPointerDown(point); }; const handlePointerMove = ( event: React.PointerEvent ): void => { if (!isDrawing) return; // Allow only chosen pointer type if ( allowOnlyPointerType !== 'all' && event.pointerType !== allowOnlyPointerType ) { return; } const point = getCoordinates(event); onPointerMove(point); }; const handlePointerUp = ( event: React.PointerEvent | PointerEvent ): void => { if (event.pointerType === 'mouse' && event.button !== 0) return; // Allow only chosen pointer type if ( allowOnlyPointerType !== 'all' && event.pointerType !== allowOnlyPointerType ) { return; } onPointerUp(); }; /* Mouse Handlers ends */ React.useImperativeHandle(ref, () => ({ exportImage: (imageType: ExportImageType): Promise => { return new Promise(async (resolve, reject) => { try { const canvas = canvasRef.current; if (!canvas) { throw Error('Canvas not rendered yet'); } const { svgCanvas, width, height } = getCanvasWithViewBox(canvas); const canvasSketch = `data:image/svg+xml;base64,${btoa( svgCanvas.outerHTML )}`; const loadImagePromises = [await loadImage(canvasSketch)]; if (exportWithBackgroundImage) { try { const img = await loadImage(backgroundImage); loadImagePromises.push(img); } catch (error) { console.warn( 'exportWithBackgroundImage props is set without a valid background image URL. This option is ignored' ); } } Promise.all(loadImagePromises) .then((images) => { const renderCanvas = document.createElement('canvas'); renderCanvas.setAttribute('width', width.toString()); renderCanvas.setAttribute('height', height.toString()); const context = renderCanvas.getContext('2d'); if (!context) { throw Error('Canvas not rendered yet'); } images.reverse().forEach((image) => { context.drawImage(image, 0, 0); }); resolve(renderCanvas.toDataURL(`image/${imageType}`)); }) .catch((e) => { throw e; }); } catch (e) { reject(e); } }); }, exportSvg: (): Promise => { return new Promise((resolve, reject) => { try { const canvas = canvasRef.current ?? null; if (canvas !== null) { const { svgCanvas } = getCanvasWithViewBox(canvas); if (exportWithBackgroundImage) { resolve(svgCanvas.outerHTML); return; } svgCanvas.querySelector(`#${id}__background`)?.remove(); svgCanvas .querySelector(`#${id}__canvas-background`) ?.setAttribute('fill', canvasColor); resolve(svgCanvas.outerHTML); } reject(new Error('Canvas not loaded')); } catch (e) { reject(e); } }); }, })); /* Add event listener to Mouse up and Touch up to release drawing even when point goes out of canvas */ React.useEffect(() => { document.addEventListener('pointerup', handlePointerUp); return () => { document.removeEventListener('pointerup', handlePointerUp); }; }, [handlePointerUp]); const eraserPaths = paths.filter((path) => !path.drawMode); let currentGroup = 0; const pathGroups = paths.reduce( (arrayGroup, path) => { if (!path.drawMode) { currentGroup += 1; return arrayGroup; } if (arrayGroup[currentGroup] === undefined) { arrayGroup[currentGroup] = []; } arrayGroup[currentGroup].push(path); return arrayGroup; }, [[]] ); return (
{eraserPaths.map((eraserPath, i) => ( ))} {backgroundImage && ( )} {eraserPaths.map((_, i) => ( {Array.from( { length: eraserPaths.length - i }, (_, j) => j + i ).map((k) => ( ))} ))} {pathGroups.map((pathGroup, i) => ( ))}
); });