diff --git a/src/components/image-editor/hooks/use-drawing.ts b/src/components/image-editor/hooks/use-drawing.ts index dd0875db..1eace8a9 100644 --- a/src/components/image-editor/hooks/use-drawing.ts +++ b/src/components/image-editor/hooks/use-drawing.ts @@ -33,6 +33,7 @@ export default function useDrawing(props: { const baseScale = useRef(1); const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); const maskStorkeRef = useRef([]); + const isLoadingMaskRef = useRef(false); const disabled = useMemo(() => { return isDisabled || invertMask || !!maskUpload?.length; @@ -411,6 +412,7 @@ export default function useDrawing(props: { autoScale, baseScale, maskStorkeRef, + isLoadingMaskRef, creatOffscreenCanvas, setMaskStrokes, fitView, diff --git a/src/components/image-editor/hooks/use-zoom.ts b/src/components/image-editor/hooks/use-zoom.ts index 3dde76af..d897b5cf 100644 --- a/src/components/image-editor/hooks/use-zoom.ts +++ b/src/components/image-editor/hooks/use-zoom.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { MutableRefObject, useState } from 'react'; +import React, { MutableRefObject, useState } from 'react'; export default function useZoom(props: { overlayCanvasRef: any; @@ -10,6 +10,7 @@ export default function useZoom(props: { autoScale: MutableRefObject; baseScale: MutableRefObject; translatePos: MutableRefObject<{ x: number; y: number }>; + isLoadingMaskRef: MutableRefObject; }) { const MIN_SCALE = 0.5; const MAX_SCALE = 8; @@ -22,7 +23,8 @@ export default function useZoom(props: { lineWidth, translatePos, autoScale, - baseScale + baseScale, + isLoadingMaskRef } = props; const [activeScale, setActiveScale] = useState(1); @@ -96,6 +98,9 @@ export default function useZoom(props: { }; const handleOnWheel = (event: any) => { + if (isLoadingMaskRef.current) { + return; + } // stop handleZoom(event); updateCursorSize(); diff --git a/src/components/image-editor/index.tsx b/src/components/image-editor/index.tsx index f937db68..d7ae73ed 100644 --- a/src/components/image-editor/index.tsx +++ b/src/components/image-editor/index.tsx @@ -50,7 +50,9 @@ type CanvasImageEditorProps = { clearUploadMask?: () => void; onSave: (imageData: { mask: string | null; img: string }) => void; onScaleImageSize?: (data: { width: number; height: number }) => void; - uploadButton: React.ReactNode; + handleUpdateImageList: (fileList: any[]) => void; + handleUpdateMaskList: (fileList: any[]) => void; + uploadButton?: React.ReactNode; imageStatus: { isOriginal: boolean; isResetNeeded: boolean; @@ -70,11 +72,15 @@ const CanvasImageEditor: React.FC = forwardRef( imageStatus, onSave, onScaleImageSize, + handleUpdateImageList, + handleUpdateMaskList, uploadButton, maskUpload }, ref ) => { + const invertWorkerRef = useRef(null); + const loadMaksWorkerRef = useRef(null); const containerRef = useRef(null); const [lineWidth, setLineWidth] = useState(60); const translatePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); @@ -98,6 +104,7 @@ const CanvasImageEditor: React.FC = forwardRef( autoScale, baseScale, maskStorkeRef, + isLoadingMaskRef, setMaskStrokes, draw, drawStroke, @@ -134,7 +141,8 @@ const CanvasImageEditor: React.FC = forwardRef( lineWidth, translatePos, autoScale, - baseScale + baseScale, + isLoadingMaskRef }); const disabled = useMemo(() => { @@ -201,54 +209,62 @@ const CanvasImageEditor: React.FC = forwardRef( console.log('Resetting strokes', currentStroke.current); }, []); - const loadMaskPixs = async (strokes: Stroke[], isInitial?: boolean) => { - if (!strokes.length) { - return; - } + const loadMaskPixs = (maskStrokes: Stroke[], mainStrokes?: Stroke[]) => { + try { + if (!maskStrokes.length && !mainStrokes?.length) { + return; + } - const overlayCanvas = overlayCanvasRef.current!; - const overlayCtx = overlayCanvas.getContext('2d')!; + isLoadingMaskRef.current = true; + + const offscreenCanvas = new OffscreenCanvas( + overlayCanvasRef.current!.width, + overlayCanvasRef.current!.height + ); + + if (!loadMaksWorkerRef.current) { + loadMaksWorkerRef.current = new Worker( + new URL('./offscreen-worker.ts', import.meta.url), + { + type: 'module' + } + ); + } + + loadMaksWorkerRef.current!.onmessage = (event: any) => { + if (event.data.type === 'done' && event.data.imageData) { + // draw the data to the overlay canvas + const ctx = overlayCanvasRef.current!.getContext('2d')!; + ctx.putImageData(event.data.imageData, 0, 0); + + isLoadingMaskRef.current = false; + } + }; - strokes.forEach((stroke: Point[], index) => { - drawFillRect(overlayCtx, stroke, { - color: COLOR, - isInitial: isInitial + // send offscreen canvas to worker + loadMaksWorkerRef.current?.postMessage( + { canvas: offscreenCanvas, type: 'init' }, + [offscreenCanvas] + ); + + // send draw data to worker + loadMaksWorkerRef.current?.postMessage({ + type: 'draw', + maskStrokes: maskStrokes, + strokes: mainStrokes || [] }); - }); - console.log('Loading mask pixels-------:'); + } catch (error) { + console.log('error---', error); + } }; - const redrawStrokes = (strokes: Stroke[]) => { - clearOverlayCanvas(); - setTransform(); - console.log( - 'Redrawing strokes:', - strokes.length, - maskStorkeRef.current.length - ); + const redrawStrokes = async (strokes: Stroke[]) => { if (!strokes.length && !maskStorkeRef.current.length) { + clearOverlayCanvas(); return; } - const overlayCanvas = overlayCanvasRef.current!; - - const overlayCtx = overlayCanvas!.getContext('2d')!; - - // clear offscreen canvas - - loadMaskPixs(maskStorkeRef.current); - - strokes?.forEach((stroke: Point[], index) => { - drawStroke(overlayCtx, stroke, { - color: COLOR, - compositeOperation: 'destination-out' - }); - - drawStroke(overlayCtx, stroke, { - color: COLOR, - compositeOperation: 'source-over' - }); - }); + loadMaskPixs(maskStorkeRef.current, strokes); }; const undo = useCallback(() => { @@ -362,23 +378,45 @@ const CanvasImageEditor: React.FC = forwardRef( clearOverlayCanvas(); - ctx.fillStyle = COLOR; - ctx.fillRect(0, 0, canvasWidth, canvasHeight); + const offscreenCanvas = new OffscreenCanvas( + overlayCanvasRef.current!.width, + overlayCanvasRef.current!.height + ); + + if (!invertWorkerRef.current) { + invertWorkerRef.current = new Worker( + new URL('./invert-worker.ts', import.meta.url), + { + type: 'module' + } + ); + } - setTransform(); - ctx.globalCompositeOperation = 'destination-out'; - - [...strokesRef.current, ...maskStorkeRef.current].forEach((stroke) => { - stroke.forEach((point: Point) => { - const { x, y } = getTransformedPoint(point.x, point.y); - const lineWidth = getTransformLineWidth(point.lineWidth); - ctx.fillStyle = 'rgba(0,0,0,1)'; - ctx.beginPath(); - ctx.arc(x, y, lineWidth / 2, 0, Math.PI * 2); - ctx.fill(); - }); + invertWorkerRef.current.onmessage = (event: any) => { + if (event.data.type === 'done' && event.data.imageData) { + // draw the data to the overlay canvas + const ctx = overlayCanvasRef.current!.getContext('2d')!; + ctx.putImageData(event.data.imageData, 0, 0); + + isLoadingMaskRef.current = false; + } + }; + + // send offscreen canvas to worker + invertWorkerRef.current?.postMessage( + { canvas: offscreenCanvas, type: 'init' }, + [offscreenCanvas] + ); + + // send draw data to worker + const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight); + invertWorkerRef.current?.postMessage({ + type: 'draw', + width: imageData.width, + height: imageData.height, + strokes: strokesRef.current, + maskStrokes: maskStorkeRef.current }); - ctx.globalCompositeOperation = 'source-out'; } else { redrawStrokes(strokesRef.current); } @@ -491,10 +529,31 @@ const CanvasImageEditor: React.FC = forwardRef( setStrokes([]); setTransform(); clearOverlayCanvas(); - loadMaskPixs(strokes, true); + loadMaskPixs(strokes); } })); + useEffect(() => { + invertWorkerRef.current = new Worker( + new URL('./invert-worker.ts', import.meta.url), + { + type: 'module' + } + ); + + loadMaksWorkerRef.current = new Worker( + new URL('./offscreen-worker.ts', import.meta.url), + { + type: 'module' + } + ); + + return () => { + invertWorkerRef.current?.terminate(); + loadMaksWorkerRef.current?.terminate(); + }; + }, []); + useEffect(() => { if (maskUpload?.length) { // clear the overlay canvas @@ -516,9 +575,11 @@ const CanvasImageEditor: React.FC = forwardRef( undo={undo} onClear={onReset} handleFitView={handleFitView} - uploadButton={uploadButton} + handleUpdateImageList={handleUpdateImageList} + handleUpdateMaskList={handleUpdateMaskList} disabled={disabled} lineWidth={lineWidth} + invertMask={invertMask} loading={loading} > @@ -556,20 +617,32 @@ const CanvasImageEditor: React.FC = forwardRef( })} style={{ position: 'absolute', zIndex: 10, cursor: 'none' }} onMouseDown={(event) => { + if (isLoadingMaskRef.current) { + return; + } mouseDownState.current = true; startDrawing(event); }} onMouseUp={(event) => { + if (isLoadingMaskRef.current) { + return; + } mouseDownState.current = false; endDrawing(event); }} onMouseEnter={handleMouseEnter} onWheel={handleOnWheel} onMouseMove={(e) => { + if (isLoadingMaskRef.current) { + return; + } handleMouseMove(e); draw(e); }} onMouseLeave={(e) => { + if (isLoadingMaskRef.current) { + return; + } endDrawing(e); handleMouseLeave(); }} diff --git a/src/components/image-editor/invert-worker.ts b/src/components/image-editor/invert-worker.ts new file mode 100644 index 00000000..6c95eeaf --- /dev/null +++ b/src/components/image-editor/invert-worker.ts @@ -0,0 +1,54 @@ +/// + +let offscreenCanvas: OffscreenCanvas; +let ctx: OffscreenCanvasRenderingContext2D; + +const COLOR = 'rgba(0, 0, 255, 0.3)'; + +type Point = { x: number; y: number; lineWidth: number }; +type Stroke = Point[]; + +self.onmessage = (event) => { + const { width, height, strokes, maskStrokes } = event.data; + + if (event.data.type === 'init') { + offscreenCanvas = event.data.canvas; + ctx = offscreenCanvas!.getContext('2d')!; + return; + } + + if (event.data.type === 'draw') { + ctx.fillStyle = COLOR; + ctx.fillRect(0, 0, width, height); + + ctx.globalCompositeOperation = 'destination-out'; + + [...strokes].forEach((stroke: Stroke) => { + stroke.forEach((point: { x: number; y: number; lineWidth: number }) => { + const lineWidth = point.lineWidth; + ctx.fillStyle = 'rgba(0,0,0,1)'; + ctx.beginPath(); + ctx.arc(point.x, point.y, lineWidth / 2, 0, Math.PI * 2); + ctx.fill(); + }); + }); + + // points + maskStrokes?.forEach((stroke: Stroke) => { + stroke.forEach((point: { x: number; y: number; lineWidth: number }) => { + const lineWidth = 4; + ctx.fillStyle = 'rgba(0,0,0,1)'; + ctx.beginPath(); + ctx.arc(point.x, point.y, lineWidth / 2, 0, Math.PI * 2); + ctx.fill(); + }); + }); + + const newImageData = ctx.getImageData(0, 0, width, height); + self.postMessage({ type: 'done', imageData: newImageData }, [ + newImageData.data.buffer + ]); + } +}; + +export {}; diff --git a/src/components/image-editor/offscreen-worker.ts b/src/components/image-editor/offscreen-worker.ts new file mode 100644 index 00000000..ee093035 --- /dev/null +++ b/src/components/image-editor/offscreen-worker.ts @@ -0,0 +1,111 @@ +let offscreenCanvas: OffscreenCanvas; +let ctx: OffscreenCanvasRenderingContext2D; + +const COLOR = 'rgba(0, 0, 255, 0.3)'; + +type Point = { x: number; y: number; lineWidth: number }; +type Stroke = Point[]; + +const postDone = () => { + const imageData = ctx.getImageData( + 0, + 0, + offscreenCanvas.width, + offscreenCanvas.height + ); + self.postMessage({ type: 'done', imageData }); +}; + +const drawFillRect = ( + ctx: OffscreenCanvasRenderingContext2D, + stroke: Stroke, + options: any +) => { + const { color } = options; + + stroke?.forEach(({ x, y }) => { + const width = options.lineWidth || 10; + + ctx.save(); + ctx.fillStyle = 'rgba(0,0,0,1)'; + ctx.globalCompositeOperation = 'destination-out'; + ctx.fillRect(x - width / 2, y - width / 2, width, width); + + ctx.globalCompositeOperation = 'source-over'; + ctx.fillStyle = color; + ctx.fillRect(x - width / 2, y - width / 2, width, width); + ctx.restore(); + }); +}; + +const drawStrokes = (strokes: Stroke[]) => { + strokes?.forEach((stroke) => { + drawFillRect(ctx, stroke, { + color: COLOR + }); + }); +}; + +const drawLine = ( + stroke: Stroke | Point[], + options: { + lineWidth?: number; + color: string; + compositeOperation: 'source-over' | 'destination-out'; + } +) => { + const { color, compositeOperation } = options; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.globalCompositeOperation = compositeOperation; + ctx.save(); + + ctx.beginPath(); + + stroke?.forEach((point, i) => { + const { x, y } = point; + ctx.lineWidth = point.lineWidth || 10; + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + }); + if (compositeOperation === 'source-over') { + ctx.strokeStyle = color; + } + ctx.stroke(); + ctx.restore(); +}; + +const drawLines = (strokes: Stroke[]) => { + strokes?.forEach((stroke: Point[], index) => { + drawLine(stroke, { + color: COLOR, + compositeOperation: 'destination-out' + }); + + drawLine(stroke, { + color: COLOR, + compositeOperation: 'source-over' + }); + }); +}; + +self.onmessage = (event) => { + if (event.data.type === 'init') { + offscreenCanvas = event.data.canvas; + ctx = offscreenCanvas!.getContext('2d')!; + return; + } + + if (event.data.type === 'draw') { + ctx?.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height); + const { maskStrokes, strokes } = event.data; + drawStrokes(maskStrokes); + drawLines(strokes); + postDone(); + } +}; + +export {}; diff --git a/src/components/image-editor/tools-bar.tsx b/src/components/image-editor/tools-bar.tsx index 5021741f..4fe19b1a 100644 --- a/src/components/image-editor/tools-bar.tsx +++ b/src/components/image-editor/tools-bar.tsx @@ -1,5 +1,6 @@ import IconFont from '@/components/icon-font'; import { KeyMap } from '@/config/hotkeys'; +import UploadImg from '@/pages/playground/components/upload-img'; import { ClearOutlined, DownloadOutlined, @@ -16,11 +17,14 @@ interface ToolsBarProps { disabled: boolean; loading: boolean; lineWidth: number; - uploadButton: React.ReactNode; + uploadButton?: React.ReactNode; + invertMask?: boolean; handleBrushSizeChange: (value: number) => void; undo: () => void; onClear: () => void; handleFitView: () => void; + handleUpdateImageList: (fileList: any[]) => void; + handleUpdateMaskList: (fileList: any[]) => void; } const ToolsBar: React.FC = (props) => { @@ -28,11 +32,14 @@ const ToolsBar: React.FC = (props) => { disabled, loading, lineWidth, + uploadButton, + invertMask, handleBrushSizeChange, undo, onClear, - uploadButton, - handleFitView + handleFitView, + handleUpdateImageList, + handleUpdateMaskList } = props; const intl = useIntl(); return ( @@ -88,6 +95,22 @@ const ToolsBar: React.FC = (props) => { {uploadButton} + + } + disabled={loading || invertMask} + handleUpdateImgList={handleUpdateMaskList} + size="middle" + accept="image/*" + >