fix: image edit invert mask
This commit is contained in:
@@ -33,6 +33,7 @@ export default function useDrawing(props: {
|
|||||||
const baseScale = useRef<number>(1);
|
const baseScale = useRef<number>(1);
|
||||||
const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
const maskStorkeRef = useRef<Stroke[]>([]);
|
const maskStorkeRef = useRef<Stroke[]>([]);
|
||||||
|
const isLoadingMaskRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const disabled = useMemo(() => {
|
const disabled = useMemo(() => {
|
||||||
return isDisabled || invertMask || !!maskUpload?.length;
|
return isDisabled || invertMask || !!maskUpload?.length;
|
||||||
@@ -411,6 +412,7 @@ export default function useDrawing(props: {
|
|||||||
autoScale,
|
autoScale,
|
||||||
baseScale,
|
baseScale,
|
||||||
maskStorkeRef,
|
maskStorkeRef,
|
||||||
|
isLoadingMaskRef,
|
||||||
creatOffscreenCanvas,
|
creatOffscreenCanvas,
|
||||||
setMaskStrokes,
|
setMaskStrokes,
|
||||||
fitView,
|
fitView,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { MutableRefObject, useState } from 'react';
|
import React, { MutableRefObject, useState } from 'react';
|
||||||
|
|
||||||
export default function useZoom(props: {
|
export default function useZoom(props: {
|
||||||
overlayCanvasRef: any;
|
overlayCanvasRef: any;
|
||||||
@@ -10,6 +10,7 @@ export default function useZoom(props: {
|
|||||||
autoScale: MutableRefObject<number>;
|
autoScale: MutableRefObject<number>;
|
||||||
baseScale: MutableRefObject<number>;
|
baseScale: MutableRefObject<number>;
|
||||||
translatePos: MutableRefObject<{ x: number; y: number }>;
|
translatePos: MutableRefObject<{ x: number; y: number }>;
|
||||||
|
isLoadingMaskRef: MutableRefObject<boolean>;
|
||||||
}) {
|
}) {
|
||||||
const MIN_SCALE = 0.5;
|
const MIN_SCALE = 0.5;
|
||||||
const MAX_SCALE = 8;
|
const MAX_SCALE = 8;
|
||||||
@@ -22,7 +23,8 @@ export default function useZoom(props: {
|
|||||||
lineWidth,
|
lineWidth,
|
||||||
translatePos,
|
translatePos,
|
||||||
autoScale,
|
autoScale,
|
||||||
baseScale
|
baseScale,
|
||||||
|
isLoadingMaskRef
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [activeScale, setActiveScale] = useState<number>(1);
|
const [activeScale, setActiveScale] = useState<number>(1);
|
||||||
@@ -96,6 +98,9 @@ export default function useZoom(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOnWheel = (event: any) => {
|
const handleOnWheel = (event: any) => {
|
||||||
|
if (isLoadingMaskRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// stop
|
// stop
|
||||||
handleZoom(event);
|
handleZoom(event);
|
||||||
updateCursorSize();
|
updateCursorSize();
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ type CanvasImageEditorProps = {
|
|||||||
clearUploadMask?: () => void;
|
clearUploadMask?: () => void;
|
||||||
onSave: (imageData: { mask: string | null; img: string }) => void;
|
onSave: (imageData: { mask: string | null; img: string }) => void;
|
||||||
onScaleImageSize?: (data: { width: number; height: number }) => void;
|
onScaleImageSize?: (data: { width: number; height: number }) => void;
|
||||||
uploadButton: React.ReactNode;
|
handleUpdateImageList: (fileList: any[]) => void;
|
||||||
|
handleUpdateMaskList: (fileList: any[]) => void;
|
||||||
|
uploadButton?: React.ReactNode;
|
||||||
imageStatus: {
|
imageStatus: {
|
||||||
isOriginal: boolean;
|
isOriginal: boolean;
|
||||||
isResetNeeded: boolean;
|
isResetNeeded: boolean;
|
||||||
@@ -70,11 +72,15 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
imageStatus,
|
imageStatus,
|
||||||
onSave,
|
onSave,
|
||||||
onScaleImageSize,
|
onScaleImageSize,
|
||||||
|
handleUpdateImageList,
|
||||||
|
handleUpdateMaskList,
|
||||||
uploadButton,
|
uploadButton,
|
||||||
maskUpload
|
maskUpload
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const invertWorkerRef = useRef<any>(null);
|
||||||
|
const loadMaksWorkerRef = useRef<any>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [lineWidth, setLineWidth] = useState<number>(60);
|
const [lineWidth, setLineWidth] = useState<number>(60);
|
||||||
const translatePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
const translatePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
@@ -98,6 +104,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
autoScale,
|
autoScale,
|
||||||
baseScale,
|
baseScale,
|
||||||
maskStorkeRef,
|
maskStorkeRef,
|
||||||
|
isLoadingMaskRef,
|
||||||
setMaskStrokes,
|
setMaskStrokes,
|
||||||
draw,
|
draw,
|
||||||
drawStroke,
|
drawStroke,
|
||||||
@@ -134,7 +141,8 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
lineWidth,
|
lineWidth,
|
||||||
translatePos,
|
translatePos,
|
||||||
autoScale,
|
autoScale,
|
||||||
baseScale
|
baseScale,
|
||||||
|
isLoadingMaskRef
|
||||||
});
|
});
|
||||||
|
|
||||||
const disabled = useMemo(() => {
|
const disabled = useMemo(() => {
|
||||||
@@ -201,54 +209,62 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
console.log('Resetting strokes', currentStroke.current);
|
console.log('Resetting strokes', currentStroke.current);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadMaskPixs = async (strokes: Stroke[], isInitial?: boolean) => {
|
const loadMaskPixs = (maskStrokes: Stroke[], mainStrokes?: Stroke[]) => {
|
||||||
if (!strokes.length) {
|
try {
|
||||||
return;
|
if (!maskStrokes.length && !mainStrokes?.length) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const overlayCanvas = overlayCanvasRef.current!;
|
isLoadingMaskRef.current = true;
|
||||||
const overlayCtx = overlayCanvas.getContext('2d')!;
|
|
||||||
|
|
||||||
strokes.forEach((stroke: Point[], index) => {
|
const offscreenCanvas = new OffscreenCanvas(
|
||||||
drawFillRect(overlayCtx, stroke, {
|
overlayCanvasRef.current!.width,
|
||||||
color: COLOR,
|
overlayCanvasRef.current!.height
|
||||||
isInitial: isInitial
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 || []
|
||||||
});
|
});
|
||||||
});
|
} catch (error) {
|
||||||
console.log('Loading mask pixels-------:');
|
console.log('error---', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const redrawStrokes = (strokes: Stroke[]) => {
|
const redrawStrokes = async (strokes: Stroke[]) => {
|
||||||
clearOverlayCanvas();
|
|
||||||
setTransform();
|
|
||||||
console.log(
|
|
||||||
'Redrawing strokes:',
|
|
||||||
strokes.length,
|
|
||||||
maskStorkeRef.current.length
|
|
||||||
);
|
|
||||||
if (!strokes.length && !maskStorkeRef.current.length) {
|
if (!strokes.length && !maskStorkeRef.current.length) {
|
||||||
|
clearOverlayCanvas();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overlayCanvas = overlayCanvasRef.current!;
|
loadMaskPixs(maskStorkeRef.current, strokes);
|
||||||
|
|
||||||
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'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const undo = useCallback(() => {
|
const undo = useCallback(() => {
|
||||||
@@ -362,23 +378,45 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
|
|
||||||
clearOverlayCanvas();
|
clearOverlayCanvas();
|
||||||
|
|
||||||
ctx.fillStyle = COLOR;
|
const offscreenCanvas = new OffscreenCanvas(
|
||||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
overlayCanvasRef.current!.width,
|
||||||
|
overlayCanvasRef.current!.height
|
||||||
|
);
|
||||||
|
|
||||||
setTransform();
|
if (!invertWorkerRef.current) {
|
||||||
ctx.globalCompositeOperation = 'destination-out';
|
invertWorkerRef.current = new Worker(
|
||||||
|
new URL('./invert-worker.ts', import.meta.url),
|
||||||
|
{
|
||||||
|
type: 'module'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
[...strokesRef.current, ...maskStorkeRef.current].forEach((stroke) => {
|
invertWorkerRef.current.onmessage = (event: any) => {
|
||||||
stroke.forEach((point: Point) => {
|
if (event.data.type === 'done' && event.data.imageData) {
|
||||||
const { x, y } = getTransformedPoint(point.x, point.y);
|
// draw the data to the overlay canvas
|
||||||
const lineWidth = getTransformLineWidth(point.lineWidth);
|
const ctx = overlayCanvasRef.current!.getContext('2d')!;
|
||||||
ctx.fillStyle = 'rgba(0,0,0,1)';
|
ctx.putImageData(event.data.imageData, 0, 0);
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(x, y, lineWidth / 2, 0, Math.PI * 2);
|
isLoadingMaskRef.current = false;
|
||||||
ctx.fill();
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// 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 {
|
} else {
|
||||||
redrawStrokes(strokesRef.current);
|
redrawStrokes(strokesRef.current);
|
||||||
}
|
}
|
||||||
@@ -491,10 +529,31 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
setStrokes([]);
|
setStrokes([]);
|
||||||
setTransform();
|
setTransform();
|
||||||
clearOverlayCanvas();
|
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(() => {
|
useEffect(() => {
|
||||||
if (maskUpload?.length) {
|
if (maskUpload?.length) {
|
||||||
// clear the overlay canvas
|
// clear the overlay canvas
|
||||||
@@ -516,9 +575,11 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
undo={undo}
|
undo={undo}
|
||||||
onClear={onReset}
|
onClear={onReset}
|
||||||
handleFitView={handleFitView}
|
handleFitView={handleFitView}
|
||||||
uploadButton={uploadButton}
|
handleUpdateImageList={handleUpdateImageList}
|
||||||
|
handleUpdateMaskList={handleUpdateMaskList}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
lineWidth={lineWidth}
|
lineWidth={lineWidth}
|
||||||
|
invertMask={invertMask}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
></ToolsBar>
|
></ToolsBar>
|
||||||
|
|
||||||
@@ -556,20 +617,32 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
})}
|
})}
|
||||||
style={{ position: 'absolute', zIndex: 10, cursor: 'none' }}
|
style={{ position: 'absolute', zIndex: 10, cursor: 'none' }}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
|
if (isLoadingMaskRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mouseDownState.current = true;
|
mouseDownState.current = true;
|
||||||
startDrawing(event);
|
startDrawing(event);
|
||||||
}}
|
}}
|
||||||
onMouseUp={(event) => {
|
onMouseUp={(event) => {
|
||||||
|
if (isLoadingMaskRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mouseDownState.current = false;
|
mouseDownState.current = false;
|
||||||
endDrawing(event);
|
endDrawing(event);
|
||||||
}}
|
}}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onWheel={handleOnWheel}
|
onWheel={handleOnWheel}
|
||||||
onMouseMove={(e) => {
|
onMouseMove={(e) => {
|
||||||
|
if (isLoadingMaskRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
handleMouseMove(e);
|
handleMouseMove(e);
|
||||||
draw(e);
|
draw(e);
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
|
if (isLoadingMaskRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
endDrawing(e);
|
endDrawing(e);
|
||||||
handleMouseLeave();
|
handleMouseLeave();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
|
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 {};
|
||||||
@@ -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 {};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
import { KeyMap } from '@/config/hotkeys';
|
import { KeyMap } from '@/config/hotkeys';
|
||||||
|
import UploadImg from '@/pages/playground/components/upload-img';
|
||||||
import {
|
import {
|
||||||
ClearOutlined,
|
ClearOutlined,
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
@@ -16,11 +17,14 @@ interface ToolsBarProps {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
lineWidth: number;
|
lineWidth: number;
|
||||||
uploadButton: React.ReactNode;
|
uploadButton?: React.ReactNode;
|
||||||
|
invertMask?: boolean;
|
||||||
handleBrushSizeChange: (value: number) => void;
|
handleBrushSizeChange: (value: number) => void;
|
||||||
undo: () => void;
|
undo: () => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
handleFitView: () => void;
|
handleFitView: () => void;
|
||||||
|
handleUpdateImageList: (fileList: any[]) => void;
|
||||||
|
handleUpdateMaskList: (fileList: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolsBar: React.FC<ToolsBarProps> = (props) => {
|
const ToolsBar: React.FC<ToolsBarProps> = (props) => {
|
||||||
@@ -28,11 +32,14 @@ const ToolsBar: React.FC<ToolsBarProps> = (props) => {
|
|||||||
disabled,
|
disabled,
|
||||||
loading,
|
loading,
|
||||||
lineWidth,
|
lineWidth,
|
||||||
|
uploadButton,
|
||||||
|
invertMask,
|
||||||
handleBrushSizeChange,
|
handleBrushSizeChange,
|
||||||
undo,
|
undo,
|
||||||
onClear,
|
onClear,
|
||||||
uploadButton,
|
handleFitView,
|
||||||
handleFitView
|
handleUpdateImageList,
|
||||||
|
handleUpdateMaskList
|
||||||
} = props;
|
} = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
@@ -88,6 +95,22 @@ const ToolsBar: React.FC<ToolsBarProps> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{uploadButton}
|
{uploadButton}
|
||||||
|
<UploadImg
|
||||||
|
disabled={loading || invertMask}
|
||||||
|
handleUpdateImgList={handleUpdateImageList}
|
||||||
|
size="middle"
|
||||||
|
accept="image/*"
|
||||||
|
></UploadImg>
|
||||||
|
<UploadImg
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'playground.image.mask.upload'
|
||||||
|
})}
|
||||||
|
icon={<IconFont type="icon-mosaic-2"></IconFont>}
|
||||||
|
disabled={loading || invertMask}
|
||||||
|
handleUpdateImgList={handleUpdateMaskList}
|
||||||
|
size="middle"
|
||||||
|
accept="image/*"
|
||||||
|
></UploadImg>
|
||||||
<Tooltip title={intl.formatMessage({ id: 'playground.image.fitview' })}>
|
<Tooltip title={intl.formatMessage({ id: 'playground.image.fitview' })}>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleFitView}
|
onClick={handleFitView}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateImageList = useCallback(
|
const handleUpdateImageList = useCallback(
|
||||||
(base64List: any) => {
|
(base64List: any[]) => {
|
||||||
const currentImg = _.get(base64List, '[0]', {});
|
const currentImg = _.get(base64List, '[0]', {});
|
||||||
const img = _.get(currentImg, 'dataUrl', '');
|
const img = _.get(currentImg, 'dataUrl', '');
|
||||||
handleOnScaleImageSize(currentImg);
|
handleOnScaleImageSize(currentImg);
|
||||||
@@ -281,13 +281,11 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
[handleOnScaleImageSize]
|
[handleOnScaleImageSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateMaskList = useCallback(async (base64List: any) => {
|
const handleUpdateMaskList = useCallback(async (base64List: any[]) => {
|
||||||
// setMaskUpload(base64List);
|
|
||||||
const mask = _.get(base64List, '[0].dataUrl', '');
|
const mask = _.get(base64List, '[0].dataUrl', '');
|
||||||
const maskColors = await processImage(mask);
|
const maskColors = await processImage(mask);
|
||||||
console.log('maskColors:', maskColors);
|
console.log('maskColors:', maskColors);
|
||||||
imageEditorRef.current?.loadMaskPixs(maskColors || []);
|
imageEditorRef.current?.loadMaskPixs(maskColors || []);
|
||||||
// setMask(mask);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClearUploadMask = useCallback(() => {
|
const handleClearUploadMask = useCallback(() => {
|
||||||
@@ -322,27 +320,9 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
disabled={loading || !imageStatus.isOriginal}
|
disabled={loading || !imageStatus.isOriginal}
|
||||||
onSave={handleOnSave}
|
onSave={handleOnSave}
|
||||||
clearUploadMask={handleClearUploadMask}
|
clearUploadMask={handleClearUploadMask}
|
||||||
|
handleUpdateImageList={handleUpdateImageList}
|
||||||
|
handleUpdateMaskList={handleUpdateMaskList}
|
||||||
maskUpload={maskUpload}
|
maskUpload={maskUpload}
|
||||||
uploadButton={
|
|
||||||
<>
|
|
||||||
<UploadImg
|
|
||||||
disabled={loading}
|
|
||||||
handleUpdateImgList={handleUpdateImageList}
|
|
||||||
size="middle"
|
|
||||||
accept="image/*"
|
|
||||||
></UploadImg>
|
|
||||||
<UploadImg
|
|
||||||
title={intl.formatMessage({
|
|
||||||
id: 'playground.image.mask.upload'
|
|
||||||
})}
|
|
||||||
icon={<IconFont type="icon-mosaic-2"></IconFont>}
|
|
||||||
disabled={loading}
|
|
||||||
handleUpdateImgList={handleUpdateMaskList}
|
|
||||||
size="middle"
|
|
||||||
accept="image/*"
|
|
||||||
></UploadImg>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
></CanvasImageEditor>
|
></CanvasImageEditor>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import FieldComponent from '@/components/seal-form/field-component';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Form } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useId,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo
|
||||||
|
} from 'react';
|
||||||
|
import { ParamsSchema } from '../config/types';
|
||||||
|
|
||||||
|
type ParamsSettingsProps = {
|
||||||
|
ref?: any;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
onValuesChange?: (changeValues: any, value: Record<string, any>) => void;
|
||||||
|
paramsConfig?: ParamsSchema[];
|
||||||
|
initialValues?: Record<string, any>;
|
||||||
|
extra?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ParamsSettings: React.FC<ParamsSettingsProps> = forwardRef(
|
||||||
|
({ onValuesChange, style, paramsConfig, initialValues, extra }, ref) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const formId = useId();
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
form
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
...initialValues
|
||||||
|
});
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
|
const handleOnFinish = (values: any) => {
|
||||||
|
console.log('handleOnFinish', values);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnFinishFailed = (errorInfo: any) => {
|
||||||
|
console.log('handleOnFinishFailed', errorInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValuesChange = useCallback(
|
||||||
|
(changedValues: any, allValues: any) => {
|
||||||
|
onValuesChange?.(changedValues, allValues);
|
||||||
|
},
|
||||||
|
[onValuesChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFields = useMemo(() => {
|
||||||
|
if (!paramsConfig) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const formValues = form?.getFieldsValue();
|
||||||
|
return paramsConfig?.map((item: ParamsSchema) => {
|
||||||
|
return (
|
||||||
|
<Form.Item name={item.name} rules={item.rules} key={item.name}>
|
||||||
|
<FieldComponent
|
||||||
|
disabled={
|
||||||
|
item.disabledConfig
|
||||||
|
? item.disabledConfig?.when?.(formValues)
|
||||||
|
: item.disabled
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
item.description?.isLocalized
|
||||||
|
? intl.formatMessage({ id: item.description.text })
|
||||||
|
: item.description?.text
|
||||||
|
}
|
||||||
|
onChange={null}
|
||||||
|
{..._.omit(item, [
|
||||||
|
'name',
|
||||||
|
'rules',
|
||||||
|
'disabledConfig',
|
||||||
|
'description'
|
||||||
|
])}
|
||||||
|
></FieldComponent>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [paramsConfig, intl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
style={{ ...style }}
|
||||||
|
name={formId}
|
||||||
|
form={form}
|
||||||
|
onValuesChange={handleValuesChange}
|
||||||
|
onFinish={handleOnFinish}
|
||||||
|
onFinishFailed={handleOnFinishFailed}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{renderFields}
|
||||||
|
{extra}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default memo(ParamsSettings);
|
||||||
+3
-1
@@ -24,6 +24,8 @@
|
|||||||
"./**/*.d.ts",
|
"./**/*.d.ts",
|
||||||
"./**/*.ts",
|
"./**/*.ts",
|
||||||
"./**/*.tsx",
|
"./**/*.tsx",
|
||||||
"src/components/logs-viewer/parse-worker.ts"
|
"src/components/logs-viewer/parse-worker.ts",
|
||||||
|
"src/components/image-editor/invert-worker.ts",
|
||||||
|
"src/components/image-editor/offscreen-worker.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user