import React, { useRef, useEffect, useState, MouseEvent, CSSProperties, useImperativeHandle, forwardRef, useReducer, Reducer, MouseEventHandler, ReactNode, RefObject, useContext, useCallback, } from 'react'; import Tool, { ToolOption, Position, MAX_SCALE, MIN_SCALE } from './enums/Tool'; import { useIntl } from 'react-intl'; import { mapClientToCanvas, isMobileDevice, extract_scale_from_matrix } from './utils'; import { onStrokeMouseDown, onStrokeMouseMove, onStrokeMouseUp, drawStroke, Stroke, useStrokeDropdown, moveStoke, } from './StrokeTool'; import { onShapeMouseDown, onShapeMouseMove, onShapeMouseUp, Shape, drawRectangle, useShapeDropdown, } from './ShapeTool'; import { onImageComplete, Image, drawImage, drawBackgroundImage } from './ImageTool'; import { onTextMouseDown, onTextComplete, drawText, Text, useTextDropdown, font } from './TextTool'; import { onSelectMouseDown, onSelectMouseMove, onSelectMouseUp, onSelectMouseDoubleClick, SELECT_PADDING, } from './SelectTool'; import { defaultToolOption } from './enums/Tool'; import { debounce } from 'lodash'; import Icon from './icons/Icon'; import { v4 } from 'uuid'; import sketchStrokeCursor from './images/sketch_stroke_cursor'; import { useZoomGesture } from './gesture'; import EnableSketchPadContext from './contexts/EnableSketchPadContext'; import './SketchPad.less'; import ConfigContext from './ConfigContext'; import { usePinch, useWheel } from 'react-use-gesture'; export interface SketchPadProps { currentTool: Tool; setCurrentTool: (tool: Tool) => void; currentToolOption: ToolOption; userId: string; // controlled mode. operations?: Operation[]; onChange?: onChangeCallback; viewMatrix: ViewMatrix; onViewMatrixChange: (viewMatrix: ViewMatrix) => void; backgroundImage?: string; } export type onChangeCallback = (newOperaton: Operation, operationsAfter: Operation[]) => void; export type onSaveCallback = (image: { canvas: HTMLCanvasElement; dataUrl: string }) => void; export type SketchPadRef = { selectImage: (image: string) => void; undo: () => void; redo: () => void; clear: () => void; save: (handleSave?: onSaveCallback) => void; loadImage: (base64ImageString: string) => void; }; export type Remove = { operationId: string; }; export type Operation = (Stroke | Shape | Text | Image | Update | Remove) & { id: string; userId: string; timestamp: number; pos: Position; tool: Tool; }; export type Update = { operationId: string; data: Partial< (Stroke | Shape | Text | Image) & { pos: Position; } >; }; const DPR = window.devicePixelRatio || 1; const SELECT_BOX_PADDING = 3; const stopPropagation: MouseEventHandler = (e) => e.stopPropagation(); export type OperationListState = { queue: Operation[]; reduced: Operation[]; }; const reduceOperations = (operations: Operation[]): Operation[] => { const undoHistory: Operation[] = []; operations = operations .sort((a, b) => a.timestamp - b.timestamp) .reduce((r: Operation[], v) => { switch (v.tool) { case Tool.Undo: if (r.length) { undoHistory.push(r.pop() as Operation); } break; case Tool.Redo: if (undoHistory.length) { r.push(undoHistory.pop() as Operation); } break; default: undoHistory.splice(0); r.push(v); break; } return r; }, []); let clearIndex: number = -1; while ((clearIndex = operations.findIndex((v) => v.tool === Tool.Clear)) > 0) { operations = operations.slice(clearIndex); } operations.forEach((v, k) => { if (v.tool === Tool.Update) { const update = v as Update; const targetIndex = operations.findIndex((w) => w && w.id === update.operationId); if (~targetIndex) { const target = operations[targetIndex]; // @ts-ignore operations[targetIndex] = { ...operations[targetIndex], ...update.data }; // move other properties related to pos if (update.data.pos) { switch (target.tool) { case Tool.Eraser: case Tool.Stroke: operations[targetIndex] = { ...operations[targetIndex], ...{ points: moveStoke(target as Stroke, target.pos, update.data.pos) }, }; break; case Tool.Shape: { const newOperation: any = { ...operations[targetIndex] }; newOperation.start = { x: newOperation.pos.x, y: newOperation.pos.y, }; newOperation.end = { x: newOperation.pos.x + newOperation.pos.w, y: newOperation.pos.y + newOperation.pos.h, }; operations[targetIndex] = { ...newOperation }; break; } default: break; } } } } }); const removeIds = operations .filter((v) => v.tool === Tool.Remove) .map((v) => (v as Remove).operationId); operations = operations.filter((v) => v.tool !== Tool.Update && removeIds.indexOf(v.id) < 0); // keep Remove operation to keep undoable return operations; }; const operationListReducer: ( isControlled: boolean, onChange: onChangeCallback | undefined, ) => Reducer = (isControlled, onChange) => (state, action) => { switch (action.type) { case 'add': { let operation = action.payload.operation as Operation; const isLazy = action.payload.isLazy; const newQueue = state.queue.concat([operation]); if (!isControlled || isLazy) { return { queue: newQueue, reduced: reduceOperations(newQueue), }; } else { onChange && onChange(operation, newQueue); return state; } } case 'replaceLast': { let operation = action.payload.operation as Operation; const newQueue = state.queue.slice(0, -1).concat([operation]); return { queue: newQueue, reduced: reduceOperations(newQueue), }; } case 'replaceAll': { let newQueue = action.payload.queue as Operation[]; return { queue: newQueue, reduced: reduceOperations(newQueue), }; } case 'completeLazyUpdate': { let operation = state.queue[state.queue.length - 1]; if (isControlled && operation && operation.tool === Tool.Update) { onChange && onChange(operation, state.queue); } return state; } // Written by Huy ^_^ case 'replaceFirst': { let operation = action.payload.operation as Operation; const firstOperation: Operation | undefined = state.queue[0]; if (firstOperation) { operation = { ...operation, timestamp: firstOperation.timestamp - 1000 }; } const newQueue = [operation].concat(state.queue); return { queue: newQueue, reduced: reduceOperations(newQueue), }; } case 'clearPreviousBackground': { const newOperationQueue = state.queue.filter((operation: Operation) => { // if(operation.imgType) const img: Image = operation as Image; if (img?.imgType === 'background') return false; return true; }); // console.log(newOperationQueue); return { queue: newOperationQueue, reduced: reduceOperations(newOperationQueue), }; } default: return state; } }; enum ResizeDirection { TopLeft = 'TopLeft', TopCenter = 'TopCenter', MiddleRight = 'MiddleRight', MiddleLeft = 'MiddleLest', BottomRight = 'BottomRight', BottomCenter = 'BottomCenter', BottomLeft = 'BottomLeft', } let isResizing: null | ResizeDirection = null; let startResizePoint: [number, number] = [0, 0]; let startResizePos: Position | null = null; const useResizeHandler = ( selectedOperation: Operation | null, viewMatrix: number[], items: Operation[], operationListDispatch: React.Dispatch, setSelectedOperation: (operation: Operation) => void, handleCompleteOperation: ( tool?: Tool, data?: Stroke | Shape | Text | Image | Update | Remove, pos?: Position, ) => void, refCanvas: RefObject, prefixCls: string, ): { onMouseMove: (e: { clientX: number; clientY: number }) => void; onMouseUp: (e: { clientX: number; clientY: number }) => void; resizer: ReactNode; } => { if ( selectedOperation && (selectedOperation.tool === Tool.Shape || selectedOperation.tool === Tool.Image) ) { const [a, b, c, d, e, f] = viewMatrix; const pos = { x: selectedOperation.pos.x - SELECT_BOX_PADDING, y: selectedOperation.pos.y - SELECT_BOX_PADDING, w: selectedOperation.pos.w + 2 * SELECT_BOX_PADDING, h: selectedOperation.pos.h + 2 * SELECT_BOX_PADDING, }; const tl = [a * pos.x + c * pos.y + e, b * pos.x + d * pos.y + f]; const br = [ a * (pos.x + pos.w) + c * (pos.y + pos.h) + e, b * (pos.x + pos.w) + d * (pos.y + pos.h) + f, ]; const w = br[0] - tl[0], h = br[1] - tl[1]; const onMouseDown = (direction: ResizeDirection) => (e: MouseEvent) => { e.stopPropagation(); if (refCanvas.current) { isResizing = direction; startResizePoint = mapClientToCanvas(e, refCanvas.current, viewMatrix); startResizePos = { ...selectedOperation.pos }; } }; const onTouchStart = (direction: ResizeDirection) => (e: React.TouchEvent) => { e.stopPropagation(); if (refCanvas.current && e.touches[0]) { isResizing = direction; startResizePoint = mapClientToCanvas(e.touches[0], refCanvas.current, viewMatrix); startResizePos = { ...selectedOperation.pos }; } }; const onMouseMove = (e: { clientX: number; clientY: number }) => { if (selectedOperation && isResizing && refCanvas.current && startResizePos) { let pos = mapClientToCanvas(e, refCanvas.current, viewMatrix); const diff = { x: pos[0] - startResizePoint[0], y: pos[1] - startResizePoint[1], }; const updatePos = { ...startResizePos, }; if (isResizing === ResizeDirection.TopLeft) { diff.x = Math.min(diff.x, updatePos.w); diff.y = Math.min(diff.y, updatePos.h); updatePos.x += diff.x; updatePos.y += diff.y; updatePos.w -= diff.x; updatePos.h -= diff.y; } else if (isResizing === ResizeDirection.TopCenter) { diff.y = Math.min(diff.y, updatePos.h); updatePos.y += diff.y; updatePos.h -= diff.y; } else if (isResizing === ResizeDirection.MiddleRight) { diff.x = Math.max(diff.x, -updatePos.w); updatePos.w += diff.x; } else if (isResizing === ResizeDirection.BottomRight) { diff.x = Math.max(diff.x, -updatePos.w); diff.y = Math.max(diff.y, -updatePos.h); updatePos.w += diff.x; updatePos.h += diff.y; } else if (isResizing === ResizeDirection.BottomCenter) { diff.y = Math.max(diff.y, -updatePos.h); updatePos.h += diff.y; } else if (isResizing === ResizeDirection.BottomLeft) { diff.y = Math.max(diff.y, -updatePos.h); diff.x = Math.min(diff.x, updatePos.w); updatePos.x += diff.x; updatePos.w -= diff.x; updatePos.h += diff.y; } else if (isResizing === ResizeDirection.MiddleLeft) { diff.x = Math.min(diff.x, updatePos.w); updatePos.x += diff.x; updatePos.w -= diff.x; } const lastOperation = items[items.length - 1]; if ( lastOperation && lastOperation.tool === Tool.Update && (lastOperation as Update).operationId === selectedOperation.id && (lastOperation as Update).data.pos ) { const update = lastOperation as Update; if (update.data.pos) { update.data.pos = { ...updatePos, }; operationListDispatch({ type: 'replaceLast', payload: { operation: update, }, }); } } else { handleCompleteOperation(Tool.LazyUpdate, { operationId: selectedOperation.id, data: { pos: { ...updatePos }, }, }); } setSelectedOperation({ ...selectedOperation, pos: { ...updatePos } }); } }; const onMouseUp = () => { operationListDispatch({ type: 'completeLazyUpdate', }); isResizing = null; }; return { onMouseMove, onMouseUp, resizer: ( <>
), }; } else return { onMouseMove: () => {}, onMouseUp: () => {}, resizer: null, }; }; // a c e // b d f // 0 0 1 export type ViewMatrix = [number, number, number, number, number, number]; const SketchPad: React.ForwardRefRenderFunction = (props, ref) => { const { currentTool, setCurrentTool, userId, currentToolOption, operations, onChange, viewMatrix, onViewMatrixChange, backgroundImage, } = props; const refCanvas = useRef(null); const refContext = useRef(null); const refInput = useRef(null); const lastTapRef = useRef(0); const intl = useIntl(); const { prefixCls } = useContext(ConfigContext); const enableSketchPadContext = useContext(EnableSketchPadContext); const sketchpadPrefixCls = prefixCls + '-sketchpad'; const [hoverOperationId, setHoverOperationId] = useState(null); const [selectedOperation, setSelectedOperation] = useState(null); const isControlled = !!operations; const scale = extract_scale_from_matrix(viewMatrix); const reducer = useCallback(operationListReducer(isControlled, onChange), []); const [operationListState, operationListDispatch] = useReducer>( reducer, { queue: [], reduced: [], }, ); // console.log(operationListState); if (isControlled) { useEffect(() => { operationListDispatch({ type: 'replaceAll', payload: { queue: operations, }, }); }, [(operations as Operation[]).length]); } const refOperationListState = useRef(operationListState); refOperationListState.current = operationListState; const saveGlobalTransform = () => { if (!refContext.current) return; const context = refContext.current; context.save(); context.scale(DPR, DPR); const [a, b, c, d, e, f] = viewMatrix; context.transform(a, b, c, d, e, f); }; const restoreGlobalTransform = () => { if (!refContext.current) return; const context = refContext.current; context.restore(); }; const renderOperations = (operations: Operation[]) => { if (!refContext.current) return; const context = refContext.current; // clear canvas context.setTransform(1, 0, 0, 1, 0, 0); // Clear canvas context.clearRect(0, 0, context.canvas.width, context.canvas.height); saveGlobalTransform(); operations.forEach((operation) => { const hover = (!selectedOperation || selectedOperation.id !== operation.id) && operation.id === hoverOperationId; switch (operation.tool) { case Tool.Clear: restoreGlobalTransform(); context.clearRect(0, 0, context.canvas.width, context.canvas.height); saveGlobalTransform(); break; case Tool.Eraser: case Tool.Stroke: drawStroke(operation as Stroke, context, hover); break; case Tool.Shape: drawRectangle(operation as Shape, context, hover); break; case Tool.Text: drawText(operation as Text, context, operation.pos); break; case Tool.Image: drawImage(operation as Image, context, operation.pos, operation.id, () => { renderOperations(operations); setCurrentTool(Tool.Select); }); default: break; } }); // selected box if (selectedOperation) { context.beginPath(); context.lineWidth = 1; context.strokeStyle = '#d0d0d0'; context.rect( selectedOperation.pos.x - SELECT_BOX_PADDING, selectedOperation.pos.y - SELECT_BOX_PADDING, selectedOperation.pos.w + 2 * SELECT_BOX_PADDING, selectedOperation.pos.h + 2 * SELECT_BOX_PADDING, ); context.stroke(); context.closePath(); } restoreGlobalTransform(); }; useEffect(() => { const keydownHandler = (evt: KeyboardEvent) => { const { keyCode } = evt; // key 'delete' if (keyCode === 8) { if (selectedOperation) { setSelectedOperation(null); handleCompleteOperation(Tool.Remove, { operationId: selectedOperation.id }); } } else if (keyCode === 27) { // key 'esc' setSelectedOperation(null); } }; addEventListener('keydown', keydownHandler); return () => removeEventListener('keydown', keydownHandler); }, [selectedOperation && selectedOperation.id]); useEffect(() => { const resizeHandler = debounce(() => { const canvas = refCanvas.current; if (canvas && refOperationListState.current) { // high resolution canvas. const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * DPR; canvas.height = rect.height * DPR; renderOperations(refOperationListState.current.reduced); } }, 200); addEventListener('resize', resizeHandler); return () => removeEventListener('resize', resizeHandler); }, []); useEffect(() => { renderOperations(operationListState.reduced); }, [operationListState.reduced, viewMatrix, hoverOperationId, selectedOperation]); // disable default scrolling on mobile device. // refer: https://stackoverflow.com/questions/49500339/cant-prevent-touchmove-from-scrolling-window-on-ios useEffect(() => { const handler = (e: TouchEvent) => { // only disable scroll when interact with this board. if (lastTapRef.current) { e.preventDefault(); } onTouchMoveRef.current && onTouchMoveRef.current(e); }; document.addEventListener('touchmove', handler, { passive: false, }); return () => { document.removeEventListener('touchmove', handler); }; }, []); const handleCompleteOperation = ( tool?: Tool, data?: Stroke | Shape | Text | Image | Update | Remove, pos?: Position, type: string = 'add', ) => { if (!tool) { renderOperations(operationListState.reduced); return; } // coerce update. const isLazy = tool === Tool.LazyUpdate; tool = isLazy ? Tool.Update : tool; const message = Object.assign({}, data, { id: v4(), userId, timestamp: Date.now(), pos: pos as Position, tool: tool as Tool, }); operationListDispatch({ type: type ? type : 'add', payload: { operation: message, isLazy, }, }); }; const handleClearBackground = () => { operationListDispatch({ type: 'clearPreviousBackground', payload: {}, }); }; const { onMouseMove: onMouseResizeMove, onMouseUp: onMouseResizeUp, resizer, } = useResizeHandler( selectedOperation, viewMatrix, operationListState.queue, operationListDispatch, setSelectedOperation, handleCompleteOperation, refCanvas, sketchpadPrefixCls, ); const onMouseDown = (e: { clientX: number; clientY: number }) => { if (!refCanvas.current) return null; if (!enableSketchPadContext.enable) return null; const [x, y] = mapClientToCanvas(e, refCanvas.current, viewMatrix); switch (currentTool) { case Tool.Select: onSelectMouseDown(e, x, y, scale, operationListState, viewMatrix, setSelectedOperation); break; case Tool.Stroke: onStrokeMouseDown(x, y, currentToolOption); break; case Tool.Eraser: onStrokeMouseDown(x, y, { ...currentToolOption, strokeSize: (defaultToolOption.strokeSize * 2) / scale, strokeColor: 'rgba(255, 255, 255, 1)', }); break; case Tool.Shape: onShapeMouseDown(x, y, currentToolOption); break; case Tool.Text: onTextMouseDown(e, currentToolOption, scale, refInput, refCanvas, intl); break; default: break; } }; const onTouchStart = (e: React.TouchEvent) => { if (e.touches.length === 1) { if (e.timeStamp - lastTapRef.current < 300) { onDoubleClick(e.touches[0]); } else { onMouseDown(e.touches[0]); } } lastTapRef.current = e.timeStamp; }; const onDoubleClick = (e: { clientX: number; clientY: number }) => { if (!refCanvas.current) return null; const [x, y] = mapClientToCanvas(e, refCanvas.current, viewMatrix); switch (currentTool) { case Tool.Select: onSelectMouseDoubleClick( x, y, scale, operationListState, handleCompleteOperation, viewMatrix, refInput, refCanvas, intl, ); setSelectedOperation(null); break; default: setCurrentTool(Tool.Select); break; } }; const onMouseMove = (e: { clientX: number; clientY: number }) => { if (!refCanvas.current) return null; if (!enableSketchPadContext.enable) return null; onMouseResizeMove(e); const [x, y] = mapClientToCanvas(e, refCanvas.current, viewMatrix); switch (currentTool) { case Tool.Select: onSelectMouseMove( e, x, y, scale, operationListState, selectedOperation, onViewMatrixChange, setHoverOperationId, handleCompleteOperation, operationListDispatch, setSelectedOperation, ); break; case Tool.Eraser: case Tool.Stroke: { saveGlobalTransform(); refContext.current && onStrokeMouseMove(x, y, refContext.current); restoreGlobalTransform(); break; } case Tool.Shape: { renderOperations(operationListState.reduced); saveGlobalTransform(); refContext.current && onShapeMouseMove(x, y, refContext.current); restoreGlobalTransform(); break; } default: break; } }; const onTouchMove = (e: TouchEvent) => { if (e.touches.length === 1) { onMouseMove(e.touches[0]); } }; const onTouchMoveRef = useRef(onTouchMove); useEffect(() => { onTouchMoveRef.current = onTouchMove; }, [onTouchMove]); const onMouseUp = (e: { clientX: number; clientY: number }) => { if (!refCanvas.current) return null; if (!enableSketchPadContext.enable) return null; onMouseResizeUp(e); switch (currentTool) { case Tool.Select: onSelectMouseUp(operationListDispatch); break; case Tool.Eraser: { refContext.current && onStrokeMouseUp(setCurrentTool, handleCompleteOperation, Tool.Eraser); break; } case Tool.Stroke: { refContext.current && onStrokeMouseUp(setCurrentTool, handleCompleteOperation); break; } case Tool.Shape: { const [x, y] = mapClientToCanvas(e, refCanvas.current, viewMatrix); refContext.current && onShapeMouseUp(x, y, setCurrentTool, handleCompleteOperation); break; } default: break; } }; const onTouchEnd = (e: React.TouchEvent) => { if (e.changedTouches.length === 1) { onMouseUp(e.changedTouches[0]); } lastTapRef.current = 0; }; const onWheel = (evt: { stopPropagation?: React.WheelEvent['stopPropagation']; deltaY: number; ctrlKey: boolean; clientX: number; clientY: number; forceWheel?: boolean; }) => { // if (isMobileDevice && !evt.forceWheel) return; // evt.stopPropagation && evt.stopPropagation(); // const { deltaY, ctrlKey } = evt; // const [a, b, c, d, e, f] = viewMatrix; // let newScale = a + (ctrlKey ? -deltaY : deltaY) / 100; // newScale = Math.max(Math.min(newScale, MAX_SCALE), MIN_SCALE); // if (refCanvas.current) { // const pos = mapClientToCanvas(evt, refCanvas.current, viewMatrix); // const scaleChange = newScale - a; // onViewMatrixChange([ // newScale, // b, // c, // newScale, // e - pos[0] * scaleChange, // f - pos[1] * scaleChange, // ]); // } // setSelectedOperation(null); }; const onRemoveOperation = (evt: React.TouchEvent | React.MouseEvent) => { evt.preventDefault(); evt.stopPropagation(); if (selectedOperation) { setSelectedOperation(null); handleCompleteOperation(Tool.Remove, { operationId: selectedOperation.id }); } }; useEffect(() => { const canvas = refCanvas.current as HTMLCanvasElement; // high resolution canvas. const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * DPR; canvas.height = rect.height * DPR; refContext.current = canvas.getContext('2d'); canvas.oncontextmenu = (e) => { e.preventDefault(); }; }, []); const canvasStyle: CSSProperties = {}; if (currentTool === Tool.Stroke) { canvasStyle.cursor = `url(${sketchStrokeCursor}) 0 14, crosshair`; } else if (currentTool === Tool.Shape) { canvasStyle.cursor = `crosshair`; } else if (currentTool === Tool.Text) { canvasStyle.cursor = `text`; } // canvasStyle.backgroundImage = `url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAArMAAAFZCAYAAACL0KdiAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3Xf0n+P9P/BXIkOEUBpaFVvsY7b2iIYmRq3aGYTYRNByTtDDaUlbitRoEARJBDGOoEasIvaK0VIzRpSKoiWJJL9z3d8mv0SQyJD3dd2P+58mn7zH6/V4Xcd59v7c93U3mTx58uRwECBAgAABAgQIEMhQoIkwm+HUlEyAAAECBAgQIFAJCLMWAgECBAgQIECAQLYCwmy2o1M4AQIECBAgQICAMGsNECBAgAABAgQIZCsgzGY7OoUTIECAAAECBAgIs9YAAQIECBAgQIBAtgLCbLajUzgBAgQIECBAgIAwaw0QIECAAAECBAhkKyDMZjs6hRMgQIAAAQIECAiz1gABAgQIECBAgEC2AsJstqNTOAECBAgQIECAgDBrDRAgQIAAAQIECGQrIMxmOzqFEyBAgAABAgQICLPWAAECBAgQIECAQLYCwmy2o1M4AQIECBAgQICAMGsNECBAgAABAgQIZCsgzGY7OoUTIECAAAECBAgIs9YAAQIECBAgQIBAtgLCbLajUzgBAgQIECBAgIAwaw0QIECAAAECBAhkKyDMZjs6hRMgQIAAAQIECAiz1gABAgQIECBAgEC2AsJstqNTOAECBAgQIECAgDBrDRAgQIAAAQIECGQrIMxmOzqFEyBAgAABAgQICLPWAAECBAgQIECAQLYCwmy2o1M4AQIECBAgQICAMGsNECBAgAABAgQIZCsgzGY7OoUTIECAAAECBAgIs9YAAQIECBAgQIBAtgLCbLajUzgBAgQIECBAgIAwaw0QIECAAAECBAhkKyDMZjs6hRMgQIAAAQIECAiz1gABAgQIECBAgEC2AsJstqNTOAECBAgQIECAgDBrDRAgQIAAAQIECGQrIMxmOzqFEyBAgAABAgQICLPWAAECBAgQIECAQLYCwmy2o1M4AQIECBAgQICAMGsNECBAgAABAgQIZCsgzGY7OoUTIECAAAECBAgIs9YAAQIECBAgQIBAtgLCbLajUzgBAgQIECBAgIAwaw0QIECAAAECBAhkKyDMZjs6hRMgQIAAAQIECAiz1gABAgQIECBAgEC2AsJstqNTOAECBAgQIECAgDBrDRAgQIAAAQIECGQrIMxmOzqFEyBAgAABAgQICLPWAAECBAgQIECAQLYCwmy2o1M4AQIECBAgQICAMGsNECBAgAABAgQIZCsgzGY7OoUTIECAAAECBAgIs9YAAQIECBAgQIBAtgLCbLajUzgBAgQIECBAgIAwaw0QIECAAAECBAhkKyDMZjs6hRMgQIAAAQIECAiz1gABAgQIECBAgEC2AsJstqNTOAECBAgQIECAgDBrDRAgQIAAAQIECGQrIMxmOzqFEyBAgAABAgQICLPWAAECBAgQIECAQLYCwmy2o1M4AQIECBAgQICAMGsNECBAgAABAgQIZCsgzGY7OoUTIECAAAECBAgIs9YAAQIECBAgQIBAtgLCbLajUzgBAgQIECBAgIAwaw0QIECAAAECBAhkKyDMZjs6hRMgQIAAAQIECAiz1gABAgQIECBAgEC2AsJstqNTOAECBAgQIECAgDBrDRAgQIAAAQIECGQrIMxmOzqFEyBAgAABAgQICLPWAAECBAgQIECAQLYCwmy2o1M4AQIECBAgQICAMGsNECBAgAABAgQIZCsgzGY7OoXnKvD666/HiBEj4osvvsi1BXUTmC8CzZo1iw4dOsSKK644X77flxIg0JgCwmxjzkVVBQv07t07XnrppYI71BqBeSew8sorx7nnnjvvvsAnEyCQnYAwm93IFJy7QPfu3eODDz7IvQ31E5gvAosttlgMGjRovny3LyVAoDEFhNnGnIuqCha46qqrYsiQIQV3qDUC805gr732im7dus27L/DJBAhkJyDMZjcyBZcgMH78+GjSpEkJreiBwHcmMHny5GjRosV39n2+iACBPASE2TzmpEoCBAgQIECAAIGvEBBmLQsCBAgQIECAAIFsBYTZbEencAIECBAgQIAAAWHWGiBAgAABAgQIEMhWQJjNdnQKJ0CAAAECBAgQEGatAQIECBAgQIAAgWwFhNlsR6dwAgQIECBAgAABYdYaIECAAAECBAgQyFZAmM12dAonQIAAAQIECBAQZq0BAgQIECBAgACBbAWE2WxHp3ACBAgQIECAAAFh1hogQIAAAQIECBDIVkCYzXZ0CidAgAABAgQIEBBmrQECBAgQIECAAIFsBYTZbEencAIECBAgQIAAAWHWGiBAgAABAgQIEMhWQJjNdnQKJ0CAAAECBAgQEGatAQIECBAgQIAAgWwFhNlsR6dwAgQIECBAgAABYdYaIECAAAECBAgQyFZAmM12dAonQIAAAQIECBAQZq0BAgQIECBAgACBbAWE2WxHp3ACBAgQIECAAAFh1hogQIAAAQIECBDIVkCYzXZ0CidAgAABAgQIEBBmrQECBAgQIECAAIFsBYTZbEencAIECBAgQIAAAWHWGiBAgAABAgQIEMhWQJjNdnQKJ0CAAAECBAgQEGatAQIECBAgQIAAgWwFhNlsR6dwAgQIECBAgAABYdYaIECAAAECBAgQyFZAmM12dAonQIAAAQIECBAQZq0BAgQIECBAgACBbAWE2WxHp3ACBAgQIECAAAFh1hogQIAAAQIECBDIVkCYzXZ0CidAoNEFPv300/jiiy+iZcuW0apVq0YvV30ECBDIUkCYzXJsiiZAYH4LfP755zFq1Kh44YUX4uWXX45XXnklPvjgg5gwYUJMnDgxJk2aFC1atIgmTZpM/Xv6c/rZ4osvHiuttFK0b98+Vl111VhzzTVj0UUXnd8t+X4CBAhkKSDMZjk2RRMgMD8EPvroo3jppZfiT3/6U9x9992xyCKLxFZbbRXbbrttbLzxxvGjH/2oCq8zO8aMGRMPPvhg3HPPPdXnjB07NtZee+3Yb7/9Yuutt462bdvGQgstNLOP8e8ECBAgEBHCrGVAgACBmQj897//jd69e8fNN98cnTp1imOOOSaWWWaZ6mzqAgssMEd+6Qxuuhzh3//+d9xxxx3Rv3//ePfdd2PXXXeNX//617HEEkvM0ed7MwECBEoXEGZLn7D+CBCYbYE33ngjTj/99HjyySfjoIMOil122SWWWmqp2f68WXnj5MmTq0sW7rrrrhg2bFh1vW2XLl1i7733npW3ew0BAgRqJyDM1m7kGiZAYGYC48aNiyuvvDLOOeecKsT26tVrli4fmNnnzs6/pyB9yimnVGdrTz755OqShtatW8/OR3kPAQIEihQQZoscq6YIEJhdgfRr/3XWWSc22GCDuOiii6obthrhSDec7bjjjvHaa6/FjTfeWF1j6yBAgAAB18xaAwQIEJgqcP/990e3bt2iX79+sf3220ezZs0aTueRRx6prqX94Q9/WF1f2yhhu+GgFESAQG0EnJmtzag1SoDANwlcfvnlce6558YNN9wQyy+/fENjjR8/Ps4666wYPHhwXHbZZbHhhhs2dL2KI0CAwLwUEGbnpa7PJkCg4QXSDVd/+MMf4qabbop0ZnZOdyf4LhtO19GmyyHOP//86ua0WdkW7Lusz3cRIEDguxAQZr8LZd9BgEDDCpx22mnx7LPPxoABA7J8cMF7771X7XbQsWPHOOGEExrWWWEECBCYVwLC7LyS9bkECDS0QDojm27wuu2222LgwIFZBtkpwO+//3507949OnfuHEceeaQztA298hRHgMDcFhBm57aozyNAIAuB6667Lk499dRIN1SV8LSt9BSx9DSydFPYJptsksUMFEmAAIG5ISDMzg1Fn0GAQFYC6VGyPXr0iKeffjpatWqVVe3fVGy65GDVVVeNJ554IlZaaaVi+tIIAQIEvklAmLU+CBColcDrr78eu+22W1x99dXRvn374np/6KGHqjPO6Ya2BRdcsLj+NESAAIEvCwiz1gQBArURSA8e2GabbaJPnz6xww47FNt3Ouv84x//OA477LBie9QYAQIEpggIs9YCAQK1EEg3fO28887VU7QOPvjgonueOHFirL766vHiiy9mtdVY0UPRHAEC80xAmJ1ntD6YAIFGErjiiiti+PDhceWVV0bLli0bqbR5Ukvfvn1j3Lhx1dPCHAQIEChZQJgtebp6I0CgEnj++eerM7LPPfdctG7duhYqY8aMiU6dOlU3uTkIECBQsoAwW/J09UaAQKRfuafrY08//fRYf/31ayVy/PHHV4E2PVDBQYAAgVIFhNlSJ6svAgQqgeuvvz4GDRoUw4YNq51I2qKra9eu1Rnppk2b1q5/DRMgUA8BYbYec9YlgdoKpLOSF1xwQZHbcM1sqBMmTIh111037r333mjbtu3MXu7fCRAgkKWAMJvl2BRNgMCsCKSne/Xr1686M1vXIz3p7J133omjjz66rgT6JkCgcAFhtvABa49AnQU6d+4cvXv3ju222662DB999FGsttpqMXr06GjevHltHTROgEC5AsJsubPVGYHaC6y11lrx2GOPFfXI2m871EmTJsV6660XAwcOrC45cBAgQKA0AWG2tInqhwCBSiDd9HTppZfGH//4x9qLpD12P/zwwzjmmGNqbwGAAIHyBITZ8maqIwIEIqJbt27V41w32WQTHhGx5ZZbxv3338+CAAECxQkIs8WNVEMECIwfP766ez89LGGZZZYBEhEbb7xxDB06NJZbbjkeBAgQKEpAmC1qnJohQCAJ/POf/4zVV1893nvvvWjWrBmUiGo3g/TQiP33358HAQIEihIQZosap2YIEEgCL7/8cvTp0yeuueYaIP8TuPPOOyNt09W/f38mBAgQKEpAmC1qnJohQCAJ3HDDDTF27Njo0aMHkP8JjBs3rtqi7L777mNCgACBogSE2aLGqRkCBJLAiSeeGEcccUS0a9cOyDQC22yzTdx+++32m7UqCBAoSkCYLWqcmiFAIAlsv/321a/UF1poISDTCBx++OHRpUuX2HTTTbkQIECgGAFhtphRaoQAgSkC6eavp59+Olq2bAllGoHzzjsvPvvss/jlL3/JhQABAsUICLPFjFIjBAgkgXRt6PLLLx9vvvmmX6d/aUnccsstcfnll8e1115rsRAgQKAYAWG2mFFqhACBJPCvf/2renzrq6++aluuLy2J9FS0nj17xsiRIy0WAgQIFCMgzBYzSo0QIJAE0hnZDh06VNtzNW3aFMo0Ah999FFsttlm1cMkHAQIEChFQJgtZZL6IECgEnjppZdip512ir/97W/RpEkTKtMITJ48OdZcc8144YUXuBAgQKAYAWG2mFFqhACBJDBq1KjYc88948UXXwTyFQKbb755PPDAA2wIECBQjIAwW8woNUKAQBJIuxjst99+fpX+Ncthxx13jOHDh1ssBAgQKEZAmC1mlBohQECYnfka2GOPPexmMHMmryBAICMBYTajYSmVAIGZCzgz+81GBxxwQFx88cV2epj5UvIKAgQyERBmMxmUMgkQmDWBZ555Jvbdd1+XGXwNV3rM7xlnnBFt2rSZNVCvIkCAQIMLCLMNPiDlESDw7QTSnfq77bZbdQOY3QxmtBNmv9168moCBBpfQJht/BmpkACBbyGQ9pfdYYcd4u9//7sw+xVuhx56aJx55pmx8MILfwtVLyVAgEDjCgizjTsblREgMBsCb731Vmy55Zbxj3/8w0MTvsKvW7duccUVV8yGrLcQIECgMQWE2caci6oIEJhNAY+z/Wa4tAfvNddcM5u63kaAAIHGExBmG28mKiJAYA4Exo0bF8stt1yMHj06mjdvPgefVOZb0yUYt9xyS5nN6YoAgVoKCLO1HLumCZQtkMJseqxty5Yty250NrrbZJNNYuTIkbPxTm8hQIBAYwoIs405F1URIDAHAumRrXfeeWe0atVqDj6lzLeuueaati0rc7S6IlBbAWG2tqPXOIFyBfbaa6+49NJLo3Xr1uU2ORudffzxx7HRRhtV25Y5CBAgUIqAMFvKJPVBgMBUgT59+sRBBx0UK6ywApVpBNKlF126dIlHH32UCwECBIoREGaLGaVGCBCYNGlSfPDBBzF48OBYdtllq4cnOP6/wL333hunnnpqXHXVVdG2bdto0aIFHgIECGQvIMxmP0INECCQBFKI7dChQ7zyyisVyNFHHx19+/aFM43AoEGDYv/9969CbNOmTatQu/POOzMiQIBA1gLCbNbjUzwBAlMEfvKTn8Qpp5wSm222WbzzzjvRq1evuOuuuwBNI3DaaadVT/7q2bNnvP/++7H33nvHsGHDol27dpwIECCQrYAwm+3oFE6AwBSBzz//PDp16hR33313dcbxvffeizXWWKMKbOnvjv8TOOCAA+JXv/pVrL766tXfzzjjjJgwYUL1fwIcBAgQyFVAmM11cuomQGCqwPnnnx+LL7547LPPPtXPPv3001hyySXj7bffju9973uk/ieQHvN7//33T/VIj/7t3r17jBgxghEBAgSyFRBmsx2dwgkQmCKw4oorxh133BErr7xy9aPJkyfHEkssEY8//nikf3P8n8CXw2z62VZbbRX33XcfIgIECGQrIMxmOzqFEyAw5SxsmzZt4sMPP4zFFltsKsovfvGL6lfq6VpaR8QLL7xQXU5w3XXXTcdxyCGHRP/+/RERIEAgWwFhNtvRKZwAgSSQbvZafvnl47PPPosFFlhgKkp6aMKCCy4Y++67L6iIuOiiiyJdVpBuApv2OPvss6N3796MCBAgkK2AMJvt6BROgEASSFtxpZu/Xn755elA0lZdJ598clx44YWgIqJr166RzlZ/eSuutF3Xfvvtx4gAAQLZCgiz2Y5O4QQIJIH0VKu0xdSTTz45A8jmm28eDzzwAKiI2HjjjePqq6+uzmJPewwdOjTS438dBAgQyFVAmM11cuomQKASSGdk0xnHZ555ZgaR1VZbLR566KFqp4O6HxtuuGF1Q9yXjyFDhkzdBaLuRvonQCBPAWE2z7mpmgCB/wmMHj262lP2k08+mcFk2223jV//+teRztDW+Uj77p544olx2WWXzcCQrqU9+OCD68yjdwIEMhcQZjMfoPIJ1F1g7Nix1TZcKcy2bt16Oo7TTz89Fl100TjiiCNqzXTmmWfG0ksv/ZU3w51wwgnxu9/9rtY+midAIG8BYTbv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Bx54YBVk03HRRRfFKqusEh06dJgvdflSAgQINIqAMNsok1AHAQLzVeDqq6+urkX9/e9/P10dDzzwQHTv3j369u0be+yxx3da4+OPPx5HHnlktQ/unnvuOd13Dx06NJo1axa77777d1qTLyNAgECjCQizjTYR9RAgMF8EBg4cGG+88UaccsopM3x/2ot2p512qp6wlc7UtmzZcp7WmLbeuvvuu6vvOuecc6oHO3z5SI/hfeutt6Jnz57ztBYfToAAgUYXEGYbfULqI0DgOxFIN1WNGzeu+nX+1x0pOKbtsNL2XVN+3T+3i0tBdsCAAZF2LXj22Wejbdu2X/kVjz76aNxxxx1x0kknze0SfB4BAgSyEhBmsxqXYgkQmFcCaReDdINV165dv/Yrvvjii0iXI/zmN7+JwYMHx/rrrz9Xy/n000+r62LT5QPpRq82bdp87ec/88wzcfHFF1cPenAQIECgzgLCbJ2nr3cCBKYKpDOhm222WXUpwcyOp59+unrAQrqO9aijjopWrVrN7C0z/fd0ve5xxx1Xff8JJ5ww09enM8Tp9bfeeutMX+sFBAgQKFlAmC15unojQGCWBQ477LA44ogjYq211prl9+yzzz4xZsyYGDRoUCy99NKz/L5pXzhhwoTqbG+fPn0i3dS1ySabzNLnvPrqq9V1vM8///wsvd6LCBAgUKqAMFvqZPVFgMC3Eth1112rULrQQgvN8vsmTpxYPWghheC0RVba+/XbnKVNN5yl701P8+rXr18sscQSs/zd77zzTiy33HIxfvz4hn307iw344UECBCYAwFhdg7wvJUAgXIEfvrTn8aIESNmq6H0VK5001balzbt/dq5c+fYaKONqj9Pe6Sbu958880YOXJkDB8+vNqNID06d+edd/7WgTTtP5vCb3qgQuvWrWerbm8iQIBACQLCbAlT1AMBAnMkkELm5ptvHg8++OAcfU5685133hmXXnppPPfcc/H5559Xlx+ks7Xpz+mShLQLQjoTe8ghh8R2220329+Xzsims8jpDG26cc1BgACBugoIs3WdvL4JEJgqMHr06Cpczs2bqdLOB+l62HQpQgrLTZo0iQUWWKDaqaB58+ZzRT+d+b3llluiffv2c+XzfAgBAgRyFBBmc5yamgkQmKsCQ4YMibRva7rmNacjPVQh3QT2VQ9VyKkPtRIgQGBOBITZ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')`; if (backgroundImage) { canvasStyle.backgroundImage = `url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAArMAAAFZCAYAAACL0KdiAAAAAXNSR0IArs4c6QAAIABJREFUeF7t3XuwVlXdB/CVAamMQYUyKMFYDKkNVIx2IWuii2U0o3GtdEwQuxCBEY6SqNwUDAlRgwS80AiaplAO2aQDRhSJhkUUqSBEU+ggNo0hYDi+s57es18O4Os5i/Ocs/bzfNaMg8Cz9v6tz2/98WWf/ez9hldfffXVYBAgQIAAAQIECBAoocAbhNkSdk3JBAgQIECAAAECFQFh1kYgQIAAAQIECBAorYAwW9rWKZwAAQIECBAgQECYtQcIECBAgAABAgRKKyDMlrZ1CidAgAABAgQIEBBm7QECBAgQIECAAIHSCgizpW2dwgkQIECAAAECBIRZe4AAAQIECBAgQKC0AsJsaVuncAIECBAgQIAAAWHWHiBAgAABAgQIECitgDBb2tYpnAABAgQIECBAQJi1BwgQIECAAAECBEorIMyWtnUKJ0CAAAECBAgQEGbtAQIECBAgQIAAgdIKCLOlbZ3CCRAgQIAAAQIEhFl7gAABAgQIECBAoLQCwmxpW6dwAgQIECBAgAABYdYeIECAAAECBAgQKK2AMFva1imcAAECBAgQIEBAmLUHCBAgQIAAAQIESisgzJa2dQonQIAAAQIECBAQZu0BAgQIECBAgACB0goIs6VtncIJECBAgAABAgSEWXuAAAECBAgQIECgtALCbGlbp3ACBAgQIECAAAFh1h4gQIAAAQIECBAorYAwW9rWKZwAAQIECBAgQECYtQcIECBAgAABAgRKKyDMlrZ1CidAgAABAgQIEBBm7QECBAgQIECAAIHSCgizpW2dwgkQIECAAAECBIRZe4AAAQIECBAgQKC0AsJsaVuncAIECBAgQIAAAWHWHiBAgAABAgQIECitgDBb2tYpnAABAgQIECBAQJi1BwgQIECAAAECBEorIMyWtnUKJ0CAAAECBAgQEGbtAQIECBAgQIAAgdIKCLOlbZ3CCRAgQIAAAQIEhFl7gAABAgQIECBAoLQCwmxpW6dwAgQIECBAgAABYdYeIECAAAECBAgQKK2AMFva1imcAAECBAgQIEBAmLUHCBAgQIAAAQIESisgzJa2dQonQIAAAQIECBAQZu0BAgQIECBAgACB0goIs6VtncIJECBAgAABAgSEWXuAAAECBAgQIECgtALCbGlbp3ACBAgQIECAAAFh1h4gQIAAAQIECBAorYAwW9rWKZwAAQIECBAgQECYtQcIECBAgAABAgRKKyDMlrZ1CidAgAABAgQIEBBm7QECBAgQIECAAIHSCgizpW2dwgkQIECAAAECBIRZe4AAAQIECBAgQKC0AsJsaVuncAIECBAgQIAAAWHWHiBAgAABAgQIECitgDBb2tYpnAABAgQIECBAQJi1BwgQIECAAAECBEorIMyWtnUKJ0CAAAECBAgQEGbtAQIECBAgQIAAgdIKCLOlbZ3CCRAgQIAAAQIEhFl7gAABAgQIECBAoLQCwmxpW6dwAgQIECBAgAABYdYeIECAAAECBAgQKK2AMFva1imcAAECBAgQIEBAmLUHCBAgQIAAAQIESisgzJa2dQonQIAAAQIECBAQZu0BAgQIECBAgACB0goIs6VtncIJECBAgAABAgSEWXuAAAECBAgQIECgtALCbGlbp3ACBAgQIECAAAFh1h4gQIAAAQIECBAorYAwW9rWKZwAAQIECBAgQECYtQcIECBAgAABAgRKKyDMlrZ1CidAgAABAgQIEBBm7QECBAgQIECAAIHSCgizpW2dwgkQIECAAAECBIRZe4AAAQIECBAgQKC0AsJsaVuncAIECBAgQIAAAWHWHiBAgAABAgQIECitgDBb2tYpnAABAgQIECBAQJi1BwgQIECAAAECBEorIMyWtnUKJ0CAAAECBAgQEGbtAQIECBAgQIAAgdIKCLOlbZ3CCRAgQIAAAQIEhFl7gAABAgQIECBAoLQCwmxpW6dwAgQIECBAgAABYdYeIECAAAECBAgQKK2AMFva1imcAAECBAgQIEBAmLUHCBAgQIAAAQIESisgzJa2dQonQIAAAQIECBAQZu0BAgQIECBAgACB0goIs6VtncIJECBAgAABAgSEWXuAAAECBAgQIECgtALCbGlbp3ACBAgQIECAAAFh1h4gQIAAAQIECBAorYAwW9rWKZwAAQIECBAgQECYtQcIECBAgAABAgRKKyDMlrZ1CidAgAABAgRSBfbs2ROefvrpZk8/5phjQseOHcOxxx5b+bV9+/bNPoYJLSsgzLasp6MRIECAAAECGQv85S9/CYsWLQqbNm1qkSpPOumkcMYZZ4T3v//94T3veU+LHNNBmicgzDbPy6cJECBAgACBkgqsX78+TJ8+Pezbt68qK+jWrVv40Ic+FPr37x9OPfXUqpzDQQ8VaPMwu3379jBv3rzQpUuXMGHCBD0iQIAAAQIECFRFYOnSpWHJkiVVOfbBB41Xac8888wwYMCAEG9NMKon0KZhdseOHWHatGnhr3/9a2WF5513XvjSl75UvdU6MgECBAgQIFC3At/+9rdDvM0gjni/66RJk5pt8c9//jM8++yzlf9ijnnyySfDyy+//JrHiVdrP/7xj4dhw4aFdu3aNft8Jry+QJuG2Xipf+3atUWVn/jEJ8L48eNfv2qfIECAAAECBAg0U+D8888PMYzG0bNnz8pPhltixPtvY6iNmWbjxo2HPeS73vWuSqD94Ac/2BKndIwDBNoszK5bty5MmTKlUTP69OkTZs6cqUEECBAgQIAAgRYXGD16dPHT4M985jPhm9/8ZoufI96Xu3r16rBq1aqwf//+Rsfv0KFDJfv07du3xc9bzwdsszA7derU8Oijjzayb8l/JdVzU62dAAECBAgQOFTg97//fbj99tsrf/GFL3yh8mWtao2//e1vlUB73333NQq18ekH1157beW7QkbLCLRJmD3cVdm4nE6dOoV4c7ZBgAABAgQIEKgFgcceeyzceOON4YUXXiiWc+6554aLL764FpaXxRraJMwe7qpsg8aKFSuygFEEAQIECBAgQKAlBLZu3Vr5wvtzzz1XHG7u3LmhV69eLXH4uj9Gq4fZg6/KfvKTnwwPP/xw0YgFCxaEeAneIECAAAECBAjUikDMOnPmzCmWU617dmvFqznraPUwe/BV2cmTJ4f4X8O4+uqrK2/RMAgQIECAAAECtSRw+eWXhz/+8Y+uzrZwU1s1zB7uquw555zT6NuEF110URg0aFALL9PhCBAgQIAAAQJtKxBvpTzwcWDunW2ZfrRqmP3ud78bfvnLXxaVx8vt73jHOyovSti9e3flz112b5nGOgoBAgQIECCQl8C//vWv8JWvfCX8+9//rhQWnz37ve99L68iS1hNq4XZXbt2ha9+9athz549FabTTz+9eM7s1772tRAfYRGHZ82WcBcpmQABAgQIEGiSwIwZM8KaNWuKzy5evNhjupok99ofarUw+7Of/Sx8//vfLyr5+te/Hj73uc9Vfn/gm8C6du0abrvttiNclukECBAgQIAAgfwEVq5cGWbPnl0UNmHChDBgwID8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QCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNotOL/85AAADn0lEQVQ2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgCwFhNos2KIIAAQIECBAgQCBFQJhNUTOHAAECBAgQIEAgC4H/AbWeNpsiqXANAAAAAElFTkSuQmCC')`; canvasStyle.backgroundRepeat = 'no-repeat'; canvasStyle.backgroundSize = 'cover'; } else { canvasStyle.background = '#FFFFFF'; } useImperativeHandle( ref, () => { return { selectImage: (image: string) => { if (image && refCanvas.current) { onImageComplete(image, refCanvas.current, viewMatrix, handleCompleteOperation); } }, undo: () => { setSelectedOperation(null); if (operationListState.reduced.length) { handleCompleteOperation(Tool.Undo); } }, redo: () => { setSelectedOperation(null); let isRedoable = 0; const queue = operationListState.queue; for (let i = queue.length - 1; i >= 0; i--) { if (queue[i].tool === Tool.Undo) { isRedoable++; } else if (queue[i].tool === Tool.Redo) { isRedoable--; } else { break; } } if (isRedoable > 0) { handleCompleteOperation(Tool.Redo); } }, clear: () => { setSelectedOperation(null); handleCompleteOperation(Tool.Clear); }, save: (handleSave?: onSaveCallback) => { if (refCanvas.current && refContext.current) { const canvas = refCanvas.current; const w = canvas.width; const h = canvas.height; const context = refContext.current; context.globalCompositeOperation = 'destination-over'; context.fillStyle = '#fff'; context.fillRect(0, 0, w, h); const dataUrl = canvas.toDataURL('image/png'); if (handleSave) { handleSave({ canvas, dataUrl, }); } else { const a = document.createElement('a'); a.href = dataUrl; a.download = 'sketch.png'; a.click(); } } }, loadImage: (base64ImageString: string) => { if (refCanvas.current && refContext.current) { const image: HTMLImageElement = document.createElement('img'); // img. image.onload = function () { restoreGlobalTransform(); handleClearBackground(); handleCompleteOperation( Tool.Image, { imageData: base64ImageString, imgType: 'background' }, { x: 0, y: 0, w: refCanvas.current.width * 0.3, h: refCanvas.current.height * 0.3, // w: image.width, // h: image.height, }, // 'replaceFirst', ); // clearPreviousBackground saveGlobalTransform(); }; image.src = base64ImageString; } }, }; }, [{ ...operationListState }], ); useZoomGesture(refCanvas); const bindPinch = usePinch((state) => { const { ctrlKey, origin, delta } = state; if (origin) { onWheel({ deltaY: delta[0], ctrlKey, clientX: origin[0], clientY: origin[1], forceWheel: true, }); } }); const bindWheel = useWheel((state) => { const { ctrlKey, event, delta } = state; if (event && 'clientX' in event) { onWheel({ deltaY: delta[1], ctrlKey, clientX: event.clientX + 0, clientY: event.clientY + 0, forceWheel: true, }); } }); let settingMenu = null; let removeButton = null; if (selectedOperation) { let content = null; switch (selectedOperation.tool) { case Tool.Stroke: content = useStrokeDropdown( { strokeSize: (selectedOperation as Stroke).size, strokeColor: (selectedOperation as Stroke).color, } as ToolOption, (option: ToolOption) => { const data = { color: option.strokeColor, size: option.strokeSize, }; handleCompleteOperation(Tool.Update, { operationId: selectedOperation.id, data, }); setSelectedOperation({ ...selectedOperation, ...data }); }, () => {}, prefixCls, ); break; case Tool.Shape: content = useShapeDropdown( { shapeType: (selectedOperation as Shape).type, shapeBorderColor: (selectedOperation as Shape).color, shapeBorderSize: (selectedOperation as Shape).size, } as ToolOption, (option: ToolOption) => { const data = { type: option.shapeType, color: option.shapeBorderColor, size: option.shapeBorderSize, }; handleCompleteOperation(Tool.Update, { operationId: selectedOperation.id, data, }); setSelectedOperation({ ...selectedOperation, ...data }); }, () => {}, prefixCls, ); break; case Tool.Text: { const textOperation: Text = selectedOperation as Text; content = useTextDropdown( { textSize: textOperation.size, textColor: textOperation.color, } as ToolOption, (option: ToolOption) => { const data: Partial = { color: option.textColor, size: option.textSize, }; if (refContext.current && option.textSize !== textOperation.size) { const context = refContext.current; // font size has changed, need to update pos context.font = `${option.textSize}px ${font}`; context.textBaseline = 'alphabetic'; // measureText does not support multi-line const lines = textOperation.text.split('\n'); data.pos = { ...selectedOperation.pos, w: Math.max(...lines.map((line) => context.measureText(line).width)), h: lines.length * option.textSize, }; } handleCompleteOperation(Tool.Update, { operationId: selectedOperation.id, data, }); // @ts-ignore setSelectedOperation({ ...selectedOperation, ...data }); }, () => {}, intl, prefixCls, ); break; } default: break; } const resultRect = { xMin: selectedOperation.pos.x, xMax: selectedOperation.pos.x + selectedOperation.pos.w, yMin: selectedOperation.pos.y, yMax: selectedOperation.pos.y + selectedOperation.pos.h, }; const [a, b, c, d, e, f] = viewMatrix; const selectPadding = Math.max((SELECT_PADDING * 1) / scale || 0, SELECT_PADDING); const left = resultRect.xMin; const top = resultRect.yMax + selectPadding; const menuStyle: CSSProperties = { position: 'absolute', left: a * left + c * top + e, top: b * left + d * top + f, }; settingMenu = (
{content}
); const removeX = selectedOperation.tool === Tool.Text ? resultRect.xMax - 5 / scale : resultRect.xMax - 7 / scale; const removeY = selectedOperation.tool === Tool.Text ? resultRect.yMin - 11 / scale : resultRect.yMin - 9 / scale; const removeStyle: CSSProperties = { position: 'absolute', left: a * removeX + c * removeY + e, top: b * removeX + d * removeY + f, background: 'white', lineHeight: '16px', fontSize: '16px', borderRadius: '50%', cursor: 'pointer', color: '#f45b6c', }; removeButton = (
); } return (
{ onTextComplete( refInput, refCanvas, viewMatrix, scale, handleCompleteOperation, setCurrentTool, ); }} >
{settingMenu} {removeButton} {resizer}
); }; export default forwardRef(SketchPad);