fix: image edit invert mask

main
jialin 11 months ago
parent f7c597d957
commit a7dee45720

@ -33,6 +33,7 @@ export default function useDrawing(props: {
const baseScale = useRef<number>(1);
const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const maskStorkeRef = useRef<Stroke[]>([]);
const isLoadingMaskRef = useRef<boolean>(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,

@ -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<number>;
baseScale: MutableRefObject<number>;
translatePos: MutableRefObject<{ x: number; y: number }>;
isLoadingMaskRef: MutableRefObject<boolean>;
}) {
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<number>(1);
@ -96,6 +98,9 @@ export default function useZoom(props: {
};
const handleOnWheel = (event: any) => {
if (isLoadingMaskRef.current) {
return;
}
// stop
handleZoom(event);
updateCursorSize();

@ -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<CanvasImageEditorProps> = forwardRef(
imageStatus,
onSave,
onScaleImageSize,
handleUpdateImageList,
handleUpdateMaskList,
uploadButton,
maskUpload
},
ref
) => {
const invertWorkerRef = useRef<any>(null);
const loadMaksWorkerRef = useRef<any>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [lineWidth, setLineWidth] = useState<number>(60);
const translatePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
@ -98,6 +104,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
autoScale,
baseScale,
maskStorkeRef,
isLoadingMaskRef,
setMaskStrokes,
draw,
drawStroke,
@ -134,7 +141,8 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
lineWidth,
translatePos,
autoScale,
baseScale
baseScale,
isLoadingMaskRef
});
const disabled = useMemo(() => {
@ -201,54 +209,62 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = 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<CanvasImageEditorProps> = 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<CanvasImageEditorProps> = 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<CanvasImageEditorProps> = forwardRef(
undo={undo}
onClear={onReset}
handleFitView={handleFitView}
uploadButton={uploadButton}
handleUpdateImageList={handleUpdateImageList}
handleUpdateMaskList={handleUpdateMaskList}
disabled={disabled}
lineWidth={lineWidth}
invertMask={invertMask}
loading={loading}
></ToolsBar>
@ -556,20 +617,32 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = 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();
}}

@ -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 { 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<ToolsBarProps> = (props) => {
@ -28,11 +32,14 @@ const ToolsBar: React.FC<ToolsBarProps> = (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<ToolsBarProps> = (props) => {
</Button>
</Tooltip>
{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' })}>
<Button
onClick={handleFitView}

@ -263,7 +263,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
);
const handleUpdateImageList = useCallback(
(base64List: any) => {
(base64List: any[]) => {
const currentImg = _.get(base64List, '[0]', {});
const img = _.get(currentImg, 'dataUrl', '');
handleOnScaleImageSize(currentImg);
@ -281,13 +281,11 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
[handleOnScaleImageSize]
);
const handleUpdateMaskList = useCallback(async (base64List: any) => {
// setMaskUpload(base64List);
const handleUpdateMaskList = useCallback(async (base64List: any[]) => {
const mask = _.get(base64List, '[0].dataUrl', '');
const maskColors = await processImage(mask);
console.log('maskColors:', maskColors);
imageEditorRef.current?.loadMaskPixs(maskColors || []);
// setMask(mask);
}, []);
const handleClearUploadMask = useCallback(() => {
@ -322,27 +320,9 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
disabled={loading || !imageStatus.isOriginal}
onSave={handleOnSave}
clearUploadMask={handleClearUploadMask}
handleUpdateImageList={handleUpdateImageList}
handleUpdateMaskList={handleUpdateMaskList}
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>
);
}

@ -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);

@ -24,6 +24,8 @@
"./**/*.d.ts",
"./**/*.ts",
"./**/*.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"
]
}

Loading…
Cancel
Save