chore: load mask by pix

main
jialin 11 months ago
parent 9ac679d437
commit 2287196746

@ -1,45 +1,149 @@
const extractImageColors = (base64Image: string) => {
/**
* Creates a canvas element and its rendering context.
* @param {number} width - Canvas width.
* @param {number} height - Canvas height.
* @returns {{ canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D }} - The created canvas and context.
*/
function createCanvas(
width: number,
height: number
): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return { canvas, ctx: canvas.getContext('2d')! };
}
/**
* Checks if a pixel is white.
* @param {Uint8ClampedArray} pixels - The image pixel data.
* @param {number} x - The pixel's X coordinate.
* @param {number} y - The pixel's Y coordinate.
* @param {number} width - The image width.
* @returns {boolean} - Whether the pixel is white.
*/
function isWhite(
pixels: Uint8ClampedArray,
x: number,
y: number,
width: number
): boolean {
let index = (y * width + x) * 4;
return (
pixels[index] === 255 &&
pixels[index + 1] === 255 &&
pixels[index + 2] === 255
);
}
/**
* Performs a flood fill to find all connected white pixels.
* @param {Uint8ClampedArray} pixels - The image pixel data.
* @param {number} width - The image width.
* @param {number} height - The image height.
* @param {number} x - The starting X coordinate.
* @param {number} y - The starting Y coordinate.
* @param {boolean[][]} visited - A 2D array to track visited pixels.
* @param {Array<{ x: number, y: number }>} block - The list of coordinates forming a white block.
*/
function floodFill(
pixels: Uint8ClampedArray,
width: number,
height: number,
x: number,
y: number,
visited: boolean[][],
block: Array<{ x: number; y: number }>
): void {
let stack: Array<[number, number]> = [[x, y]];
let directions: Array<[number, number]> = [
[1, 0],
[-1, 0],
[0, 1],
[0, -1], // 4-directional search (can be extended to 8-directional)
[1, 1],
[-1, -1],
[1, -1],
[-1, 1] // Diagonal directions
];
while (stack.length) {
let [cx, cy] = stack.pop()!;
if (
cx < 0 ||
cy < 0 ||
cx >= width ||
cy >= height ||
visited[cy][cx] ||
!isWhite(pixels, cx, cy, width)
) {
continue;
}
visited[cy][cx] = true;
block.push({ x: cx, y: cy });
directions.forEach(([dx, dy]) => stack.push([cx + dx, cy + dy]));
}
}
/**
* Extracts all white blocks from an image.
* @param {ImageData} imageData - The image pixel data.
* @returns {Array<Array<{ x: number, y: number }>>} - List of white blocks, each containing pixel coordinates.
*/
function getWhiteBlocks(
imageData: ImageData
): Array<Array<{ x: number; y: number }>> {
const { data, width, height } = imageData;
let visited: boolean[][] = Array.from({ length: height }, () =>
new Array(width).fill(false)
);
let whiteBlocks: Array<Array<{ x: number; y: number }>> = [];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (isWhite(data, x, y, width) && !visited[y][x]) {
let block: Array<{ x: number; y: number }> = [];
floodFill(data, width, height, x, y, visited, block);
whiteBlocks.push(block);
}
}
}
return whiteBlocks;
}
/**
* Loads an image from a file.
* @param {File} file - The image file.
* @returns {Promise<HTMLImageElement>} - The loaded image element.
*/
function loadImage(file: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = base64Image;
img.onload = function () {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const pixels = imageData.data;
let blackPixels = [];
let whitePixels = [];
for (let i = 0; i < pixels.length; i += 4) {
let r = pixels[i],
g = pixels[i + 1],
b = pixels[i + 2];
let x = (i / 4) % img.width;
let y = Math.floor(i / 4 / img.width);
if (r === 0 && g === 0 && b === 0) {
blackPixels.push({ x, y });
} else if (r === 255 && g === 255 && b === 255) {
whitePixels.push({ x, y });
}
}
img.src = file;
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Image loading failed'));
});
}
URL.revokeObjectURL(img.src);
/**
* Processes the image file and extracts white blocks.
* @param {File} file - The uploaded image file.
* @returns {Promise<Array<Array<{ x: number, y: number }>>>} - List of white blocks with pixel coordinates.
*/
async function processImage(
file: string
): Promise<Array<Array<{ x: number; y: number }>>> {
const img = await loadImage(file);
const { canvas, ctx } = createCanvas(img.width, img.height);
ctx.drawImage(img, 0, 0);
resolve({ blackPixels, whitePixels });
};
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const whiteBlocks = getWhiteBlocks(imageData);
img.onerror = function () {
reject(new Error('Image loading failed'));
};
});
};
URL.revokeObjectURL(img.src);
return whiteBlocks;
}
export default extractImageColors;
export { processImage };

@ -0,0 +1,396 @@
import _ from 'lodash';
import { useCallback, useMemo, useRef } from 'react';
type Point = { x: number; y: number; lineWidth: number };
type Stroke = Point[];
const COLOR = 'rgba(0, 0, 255, 0.3)';
export default function useDrawing(props: {
isDisabled?: boolean;
invertMask: boolean;
maskUpload?: any[];
lineWidth: number;
translatePos: { current: { x: number; y: number } };
onSave: (imageData: { mask: string | null; img: string }) => void;
}) {
const {
isDisabled,
invertMask,
maskUpload,
lineWidth,
translatePos,
onSave
} = props;
const mouseDownState = useRef<boolean>(false);
const canvasRef = useRef<HTMLCanvasElement>(null);
const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
const currentStroke = useRef<Point[]>([]);
const strokesRef = useRef<Stroke[]>([]);
const isDrawing = useRef<boolean>(false);
const cursorRef = useRef<HTMLDivElement>(null);
const autoScale = useRef<number>(1);
const baseScale = useRef<number>(1);
const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const disabled = useMemo(() => {
return isDisabled || invertMask || !!maskUpload?.length;
}, [isDisabled, invertMask, maskUpload]);
const setStrokes = (strokes: Stroke[]) => {
strokesRef.current = strokes;
};
const inpaintArea = useCallback(
(data: Uint8ClampedArray<ArrayBufferLike>) => {
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
if (alpha > 0) {
data[i] = 255; // Red
data[i + 1] = 255; // Green
data[i + 2] = 255; // Blue
data[i + 3] = 255; // Alpha
}
}
},
[]
);
const generateImage = useCallback(() => {
const canvas = canvasRef.current!;
return canvas.toDataURL('image/png');
}, []);
const generateMask = useCallback(() => {
if (strokesRef.current.length === 0) {
return null;
}
const overlayCanvas = overlayCanvasRef.current!;
const maskCanvas = document.createElement('canvas');
maskCanvas.width = overlayCanvas.width;
maskCanvas.height = overlayCanvas.height;
const maskCtx = maskCanvas.getContext('2d')!;
const overlayCtx = overlayCanvas.getContext('2d')!;
const imageData = overlayCtx.getImageData(
0,
0,
overlayCanvas.width,
overlayCanvas.height
);
const data = imageData.data;
inpaintArea(data);
maskCtx.putImageData(imageData, 0, 0);
maskCtx.globalCompositeOperation = 'destination-over';
maskCtx.fillStyle = 'black';
maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
return maskCanvas.toDataURL('image/png');
}, []);
const saveImage = () => {
const mask = generateMask();
const img = generateImage();
onSave({ mask, img });
};
const setTransform = useCallback(() => {
const ctx = canvasRef.current?.getContext('2d');
const overlayCtx = overlayCanvasRef.current?.getContext('2d');
if (!ctx || !overlayCtx) return;
ctx!.resetTransform();
overlayCtx!.resetTransform();
const { current: scale } = autoScale;
const { x: translateX, y: translateY } = translatePos.current;
ctx!.setTransform(scale, 0, 0, scale, translateX, translateY);
overlayCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
}, []);
const getTransformedPoint = useCallback(
(offsetX: number, offsetY: number) => {
const { current: scale } = autoScale;
const { x: translateX, y: translateY } = translatePos.current;
const transformedX = (offsetX - translateX) / scale;
const transformedY = (offsetY - translateY) / scale;
return {
x: transformedX,
y: transformedY
};
},
[]
);
const getTransformLineWidth = useCallback((lineWidth = 1) => {
return lineWidth / autoScale.current;
}, []);
const drawLine = useCallback(
(
ctx: CanvasRenderingContext2D,
point: Point,
options: {
lineWidth: number;
color: string;
compositeOperation: 'source-over' | 'destination-out';
}
) => {
const { lineWidth, color, compositeOperation } = options;
ctx.lineWidth = getTransformLineWidth(lineWidth);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = compositeOperation;
const { x, y } = getTransformedPoint(point.x, point.y);
ctx.lineTo(x, y);
if (compositeOperation === 'source-over') {
ctx.strokeStyle = color;
}
ctx.stroke();
},
[getTransformLineWidth]
);
const drawStroke = useCallback(
(
ctx: CanvasRenderingContext2D,
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.beginPath();
stroke.forEach((point, i) => {
const { x, y } = getTransformedPoint(point.x, point.y);
console.log('Drawing point:');
ctx.lineWidth = getTransformLineWidth(point.lineWidth);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
if (compositeOperation === 'source-over') {
ctx.strokeStyle = color;
}
ctx.stroke();
},
[getTransformLineWidth, getTransformedPoint]
);
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
console.log(
'Drawing:',
isDrawing.current,
currentStroke.current,
strokesRef.current
);
if (!isDrawing.current || !mouseDownState.current) return;
const { offsetX, offsetY } = e.nativeEvent;
const currentX = offsetX;
const currentY = offsetY;
console.log('currentStroke:', currentStroke.current);
currentStroke.current.push({
x: currentX,
y: currentY,
lineWidth
});
const ctx = overlayCanvasRef.current!.getContext('2d');
ctx!.save();
drawLine(
ctx!,
{ x: currentX, y: currentY, lineWidth },
{ lineWidth, color: COLOR, compositeOperation: 'destination-out' }
);
drawLine(
ctx!,
{ x: currentX, y: currentY, lineWidth },
{ lineWidth, color: COLOR, compositeOperation: 'source-over' }
);
ctx!.restore();
};
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
isDrawing.current = true;
currentStroke.current = [];
const { offsetX, offsetY } = e.nativeEvent;
const currentX = offsetX;
const currentY = offsetY;
currentStroke.current.push({
x: currentX,
y: currentY,
lineWidth
});
const ctx = overlayCanvasRef.current!.getContext('2d');
setTransform();
const { x, y } = getTransformedPoint(currentX, currentY);
ctx!.beginPath();
ctx!.moveTo(x, y);
draw(e);
};
const endDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
if (!isDrawing.current) {
return;
}
console.log('End Drawing:', e);
isDrawing.current = false;
strokesRef.current.push(_.cloneDeep(currentStroke.current));
currentStroke.current = [];
saveImage();
};
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
overlayCanvasRef.current!.style.cursor = 'none';
cursorRef.current!.style.display = 'block';
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const handleMouseLeave = () => {
if (disabled) {
return;
}
isDrawing.current = false;
overlayCanvasRef.current!.style.cursor = 'default';
cursorRef.current!.style.display = 'none';
};
const handleMouseEnter = (e: React.MouseEvent<HTMLCanvasElement>) => {
console.log('mouse enter:', mouseDownState.current);
if (disabled) {
overlayCanvasRef.current!.style.cursor = 'default';
return;
}
// if (mouseDownState.current) {
// isDrawing.current = true;
// }
overlayCanvasRef.current!.style.cursor = 'none';
cursorRef.current!.style.display = 'block';
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const clearOverlayCanvas = useCallback(() => {
const ctx = overlayCanvasRef.current!.getContext('2d');
ctx!.resetTransform();
ctx!.clearRect(
0,
0,
overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height
);
}, []);
const clearCanvas = useCallback(() => {
const canvas = canvasRef.current!;
const ctx = canvasRef.current!.getContext('2d');
ctx!.resetTransform();
ctx!.clearRect(0, 0, canvas.width, canvas.height);
}, []);
const resetCanvas = useCallback(() => {
const canvas = canvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!;
const ctx = canvas.getContext('2d');
const overlayCtx = overlayCanvas.getContext('2d');
autoScale.current = 1;
baseScale.current = 1;
translatePos.current = { x: 0, y: 0 };
contentPos.current = { x: 0, y: 0 };
canvas.style.transform = 'scale(1)';
overlayCanvas.style.transform = 'scale(1)';
cursorRef.current!.style.width = `${lineWidth}px`;
cursorRef.current!.style.height = `${lineWidth}px`;
ctx!.resetTransform();
overlayCtx!.resetTransform();
}, []);
const fitView = () => {
resetCanvas();
autoScale.current = baseScale.current;
translatePos.current = { x: 0, y: 0 };
setTransform();
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
canvasRef.current!.style.transform = `scale(${autoScale.current})`;
};
return {
canvasRef,
overlayCanvasRef,
cursorRef,
strokesRef,
currentStroke,
isDrawing,
mouseDownState,
autoScale,
baseScale,
fitView,
setStrokes,
resetCanvas,
draw,
getTransformLineWidth,
getTransformedPoint,
drawStroke,
startDrawing,
endDrawing,
handleMouseMove,
handleMouseLeave,
handleMouseEnter,
saveImage,
generateMask,
generateImage,
setTransform,
clearOverlayCanvas,
clearCanvas
};
}

@ -0,0 +1,109 @@
import _ from 'lodash';
import { MutableRefObject, useState } from 'react';
export default function useZoom(props: {
overlayCanvasRef: any;
canvasRef: any;
cursorRef: any;
lineWidth: number;
autoScale: MutableRefObject<number>;
baseScale: MutableRefObject<number>;
translatePos: MutableRefObject<{ x: number; y: number }>;
}) {
const MIN_SCALE = 0.5;
const MAX_SCALE = 8;
const ZOOM_SPEED = 0.1;
const {
overlayCanvasRef,
canvasRef,
cursorRef,
lineWidth,
translatePos,
autoScale,
baseScale
} = props;
const [activeScale, setActiveScale] = useState<number>(1);
const setCanvasTransformOrigin = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (autoScale.current <= MIN_SCALE) {
return;
}
if (autoScale.current >= MAX_SCALE) {
return;
}
console.log('Setting transform origin:', autoScale.current);
const rect = overlayCanvasRef.current!.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const originX = mouseX / rect.width;
const originY = mouseY / rect.height;
overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
};
const updateZoom = (scaleChange: number, mouseX: number, mouseY: number) => {
const newScale = _.round(autoScale.current + scaleChange, 2);
if (newScale < MIN_SCALE || newScale > MAX_SCALE) return;
const { current: oldScale } = autoScale;
const { x: oldTranslateX, y: oldTranslateY } = translatePos.current;
const centerX = (mouseX - oldTranslateX) / oldScale;
const centerY = (mouseY - oldTranslateY) / oldScale;
autoScale.current = newScale;
const newTranslateX = mouseX - centerX * newScale;
const newTranslateY = mouseY - centerY * newScale;
translatePos.current = { x: newTranslateX, y: newTranslateY };
};
const handleZoom = (event: React.WheelEvent<HTMLCanvasElement>) => {
const scaleChange = event.deltaY > 0 ? -ZOOM_SPEED : ZOOM_SPEED;
// current mouse position
const canvas = overlayCanvasRef.current!;
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
canvasRef.current!.style.transform = `scale(${autoScale.current})`;
setCanvasTransformOrigin(event);
updateZoom(scaleChange, mouseX, mouseY);
};
const updateCursorSize = () => {
cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`;
cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`;
};
const updateCursorPosOnZoom = (e: any) => {
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const handleOnWheel = (event: any) => {
// stop
handleZoom(event);
updateCursorSize();
updateCursorPosOnZoom(event);
setActiveScale(autoScale.current);
};
return {
handleOnWheel,
setActiveScale,
activeScale,
autoScale,
baseScale
};
}

@ -1,15 +1,4 @@
import { KeyMap } from '@/config/hotkeys';
import {
ClearOutlined,
DownloadOutlined,
ExpandOutlined,
FormatPainterOutlined,
UndoOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Slider, Tooltip } from 'antd';
import dayjs from 'dayjs';
import _ from 'lodash';
import React, {
forwardRef,
useCallback,
@ -19,8 +8,10 @@ import React, {
useRef,
useState
} from 'react';
import IconFont from '../icon-font';
import useDrawing from './hooks/use-drawing';
import useZoom from './hooks/use-zoom';
import './index.less';
import { ImageActionsBar, ToolsBar } from './tools-bar';
type Point = { x: number; y: number; lineWidth: number };
type Stroke = Point[];
@ -53,222 +44,77 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
imageSrc,
disabled: isDisabled,
imageStatus,
clearUploadMask,
onSave,
onScaleImageSize,
imguid,
uploadButton,
maskUpload
},
ref
) => {
const MIN_SCALE = 0.5;
const MAX_SCALE = 8;
const ZOOM_SPEED = 0.1;
const intl = useIntl();
const canvasRef = useRef<HTMLCanvasElement>(null);
const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [lineWidth, setLineWidth] = useState<number>(60);
const isDrawing = useRef<boolean>(false);
const currentStroke = useRef<Point[]>([]);
const strokesRef = useRef<Stroke[]>([]);
const offscreenCanvasRef = useRef<HTMLCanvasElement | null>(null);
const autoScale = useRef<number>(1);
const baseScale = useRef<number>(1);
const cursorRef = useRef<HTMLDivElement>(null);
const translatePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const strokeCache = useRef<any>({});
const preImguid = useRef<string | number>('');
const [activeScale, setActiveScale] = useState<number>(1);
const negativeMaskRef = useRef<boolean>(false);
const [invertMask, setInvertMask] = useState<boolean>(false);
const mouseDownState = useRef<boolean>(false);
const {
canvasRef,
overlayCanvasRef,
cursorRef,
strokesRef,
currentStroke,
mouseDownState,
autoScale,
baseScale,
draw,
drawStroke,
startDrawing,
endDrawing,
handleMouseMove,
handleMouseLeave,
handleMouseEnter,
saveImage,
resetCanvas,
generateMask,
getTransformLineWidth,
getTransformedPoint,
setStrokes,
setTransform,
clearOverlayCanvas,
clearCanvas,
fitView
} = useDrawing({
lineWidth: lineWidth,
translatePos: translatePos,
isDisabled: isDisabled,
invertMask,
maskUpload,
onSave: onSave
});
const { handleOnWheel, setActiveScale, activeScale } = useZoom({
overlayCanvasRef,
canvasRef,
cursorRef,
lineWidth,
translatePos,
autoScale,
baseScale
});
const disabled = useMemo(() => {
return isDisabled || invertMask || !!maskUpload?.length;
}, [isDisabled, invertMask, maskUpload]);
const getTransformedPoint = useCallback(
(offsetX: number, offsetY: number) => {
const { current: scale } = autoScale;
const { x: translateX, y: translateY } = translatePos.current;
const transformedX = (offsetX - translateX) / scale;
const transformedY = (offsetY - translateY) / scale;
return {
x: transformedX,
y: transformedY
};
},
[]
);
const getTransformLineWidth = useCallback((lineWidth: number) => {
return lineWidth / autoScale.current;
}, []);
const setCanvasTransformOrigin = (
e: React.MouseEvent<HTMLCanvasElement>
) => {
if (autoScale.current <= MIN_SCALE) {
return;
}
if (autoScale.current >= MAX_SCALE) {
return;
}
console.log('Setting transform origin:', autoScale.current);
const rect = overlayCanvasRef.current!.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const originX = mouseX / rect.width;
const originY = mouseY / rect.height;
overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
};
const handleMouseEnter = (e: React.MouseEvent<HTMLCanvasElement>) => {
console.log('mouse enter:', mouseDownState.current);
if (disabled) {
overlayCanvasRef.current!.style.cursor = 'default';
return;
}
// if (mouseDownState.current) {
// isDrawing.current = true;
// }
overlayCanvasRef.current!.style.cursor = 'none';
cursorRef.current!.style.display = 'block';
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
overlayCanvasRef.current!.style.cursor = 'none';
cursorRef.current!.style.display = 'block';
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const handleMouseLeave = () => {
if (disabled) {
return;
}
isDrawing.current = false;
overlayCanvasRef.current!.style.cursor = 'default';
cursorRef.current!.style.display = 'none';
};
const createOffscreenCanvas = () => {
if (offscreenCanvasRef.current === null) {
offscreenCanvasRef.current = document.createElement('canvas');
offscreenCanvasRef.current.width = overlayCanvasRef.current!.width;
offscreenCanvasRef.current.height = overlayCanvasRef.current!.height;
}
};
// update the canvas size
const updateCanvasSize = useCallback(() => {
const canvas = canvasRef.current!;
const offscreenCanvas = offscreenCanvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!;
overlayCanvas.width = canvas.width;
overlayCanvas.height = canvas.height;
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
}, []);
const setStrokes = (strokes: Stroke[]) => {
strokesRef.current = strokes;
};
const inpaintArea = useCallback(
(data: Uint8ClampedArray<ArrayBufferLike>) => {
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
if (alpha > 0) {
data[i] = 255; // Red
data[i + 1] = 255; // Green
data[i + 2] = 255; // Blue
data[i + 3] = 255; // Alpha
}
}
},
[]
);
const inpaintBackground = useCallback(
(data: Uint8ClampedArray<ArrayBufferLike>) => {
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
if (alpha > 0) {
data[i] = 0; // Red
data[i + 1] = 0; // Green
data[i + 2] = 0; // Blue
data[i + 3] = 255; // Alpha
} else {
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 255;
data[i + 3] = 255;
}
}
},
[]
);
const generateMask = useCallback(() => {
if (strokesRef.current.length === 0) {
return null;
}
const overlayCanvas = overlayCanvasRef.current!;
const maskCanvas = document.createElement('canvas');
maskCanvas.width = overlayCanvas.width;
maskCanvas.height = overlayCanvas.height;
const maskCtx = maskCanvas.getContext('2d')!;
const overlayCtx = overlayCanvas.getContext('2d')!;
const imageData = overlayCtx.getImageData(
0,
0,
overlayCanvas.width,
overlayCanvas.height
);
const data = imageData.data;
inpaintArea(data);
maskCtx.putImageData(imageData, 0, 0);
maskCtx.globalCompositeOperation = 'destination-over';
maskCtx.fillStyle = 'black';
maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
return maskCanvas.toDataURL('image/png');
}, []);
const generateImage = useCallback(() => {
const canvas = canvasRef.current!;
return canvas.toDataURL('image/png');
}, []);
const saveImage = useCallback(() => {
const mask = generateMask();
const img = generateImage();
onSave({ mask, img });
}, [onSave, generateMask]);
const downloadMask = useCallback(() => {
const mask = generateMask();
@ -278,7 +124,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
link.click();
}, [generateMask]);
const drawStroke = useCallback(
const drawFillRect = useCallback(
(
ctx: CanvasRenderingContext2D,
stroke: Stroke | Point[],
@ -289,194 +135,22 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
}
) => {
const { color, compositeOperation } = options;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = compositeOperation;
ctx.beginPath();
ctx.globalCompositeOperation = compositeOperation;
stroke.forEach((point, i) => {
stroke.forEach((point) => {
const { x, y } = getTransformedPoint(point.x, point.y);
console.log('Drawing point:');
ctx.lineWidth = getTransformLineWidth(point.lineWidth);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
ctx.save();
const width = getTransformLineWidth(point.lineWidth);
ctx.fillStyle = color;
ctx.fillRect(x - width / 2, y - width / 2, width, width);
ctx.restore();
});
if (compositeOperation === 'source-over') {
ctx.strokeStyle = color;
}
ctx.stroke();
},
[getTransformLineWidth, getTransformedPoint]
);
const drawLine = useCallback(
(
ctx: CanvasRenderingContext2D,
point: Point,
options: {
lineWidth: number;
color: string;
compositeOperation: 'source-over' | 'destination-out';
}
) => {
const { lineWidth, color, compositeOperation } = options;
ctx.lineWidth = getTransformLineWidth(lineWidth);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = compositeOperation;
const { x, y } = getTransformedPoint(point.x, point.y);
ctx.lineTo(x, y);
if (compositeOperation === 'source-over') {
ctx.strokeStyle = color;
}
ctx.stroke();
},
[getTransformLineWidth]
);
const setTransform = useCallback(() => {
const ctx = canvasRef.current?.getContext('2d');
const overlayCtx = overlayCanvasRef.current?.getContext('2d');
if (!ctx || !overlayCtx) return;
ctx!.resetTransform();
overlayCtx!.resetTransform();
const { current: scale } = autoScale;
const { x: translateX, y: translateY } = translatePos.current;
ctx!.setTransform(scale, 0, 0, scale, translateX, translateY);
overlayCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
}, []);
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
console.log(
'Drawing:',
isDrawing.current,
currentStroke.current,
strokesRef.current
);
if (!isDrawing.current || !mouseDownState.current) return;
const { offsetX, offsetY } = e.nativeEvent;
const currentX = offsetX;
const currentY = offsetY;
console.log('currentStroke:', currentStroke.current);
currentStroke.current.push({
x: currentX,
y: currentY,
lineWidth
});
const ctx = overlayCanvasRef.current!.getContext('2d');
ctx!.save();
drawLine(
ctx!,
{ x: currentX, y: currentY, lineWidth },
{ lineWidth, color: COLOR, compositeOperation: 'destination-out' }
);
drawLine(
ctx!,
{ x: currentX, y: currentY, lineWidth },
{ lineWidth, color: COLOR, compositeOperation: 'source-over' }
);
ctx!.restore();
};
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
isDrawing.current = true;
currentStroke.current = [];
const { offsetX, offsetY } = e.nativeEvent;
const currentX = offsetX;
const currentY = offsetY;
currentStroke.current.push({
x: currentX,
y: currentY,
lineWidth
});
const ctx = overlayCanvasRef.current!.getContext('2d');
setTransform();
const { x, y } = getTransformedPoint(currentX, currentY);
ctx!.beginPath();
ctx!.moveTo(x, y);
draw(e);
};
const endDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
if (!isDrawing.current) {
return;
}
console.log('End Drawing:', e);
isDrawing.current = false;
strokesRef.current.push(_.cloneDeep(currentStroke.current));
currentStroke.current = [];
saveImage();
};
const clearOverlayCanvas = useCallback(() => {
const ctx = overlayCanvasRef.current!.getContext('2d');
ctx!.resetTransform();
ctx!.clearRect(
0,
0,
overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height
);
}, []);
const clearCanvas = useCallback(() => {
const canvas = canvasRef.current!;
const ctx = canvasRef.current!.getContext('2d');
ctx!.resetTransform();
ctx!.clearRect(0, 0, canvas.width, canvas.height);
}, []);
const clearOffscreenCanvas = useCallback(() => {
const offscreenCanvas = offscreenCanvasRef.current!;
const offscreenCtx = offscreenCanvas.getContext('2d')!;
offscreenCtx.resetTransform();
offscreenCtx.clearRect(
0,
0,
offscreenCanvas.width,
offscreenCanvas.height
);
const { current: scale } = autoScale;
const { x: translateX, y: translateY } = translatePos.current;
offscreenCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
}, []);
const onReset = useCallback(() => {
clearOverlayCanvas();
setStrokes([]);
@ -488,26 +162,17 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
const redrawStrokes = useCallback(
(strokes: Stroke[], type?: string) => {
console.log('Redrawing strokes:', strokes, type);
if (!offscreenCanvasRef.current) {
createOffscreenCanvas();
}
clearOverlayCanvas();
if (!strokes.length) {
clearOverlayCanvas();
return;
}
const offscreenCanvas = offscreenCanvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!;
const offscreenCtx = offscreenCanvas.getContext('2d')!;
const overlayCtx = overlayCanvas!.getContext('2d')!;
offscreenCanvas.width = overlayCanvas!.width;
offscreenCanvas.height = overlayCanvas!.height;
const overlayCtx = overlayCanvas!.getContext('2d')!;
// clear offscreen canvas
clearOverlayCanvas();
setTransform();
strokes?.forEach((stroke: Point[], index) => {
@ -527,6 +192,35 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
[drawStroke]
);
const loadMaskPixs = useCallback(
(strokes: Stroke[], type?: string) => {
clearOverlayCanvas();
if (!strokes.length) {
return;
}
console.log('loadin mask pixs:');
const overlayCanvas = overlayCanvasRef.current!;
const overlayCtx = overlayCanvas!.getContext('2d')!;
setTransform();
strokes?.forEach((stroke: Point[], index) => {
overlayCtx.save();
drawFillRect(overlayCtx, stroke, {
color: COLOR,
compositeOperation: 'destination-out'
});
drawFillRect(overlayCtx, stroke, {
color: COLOR,
compositeOperation: 'source-over'
});
overlayCtx.restore();
});
},
[drawFillRect]
);
const undo = () => {
if (strokesRef.current.length === 0) return;
@ -610,26 +304,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
});
}, [imageSrc]);
const resetCanvas = useCallback(() => {
const canvas = canvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!;
const ctx = canvas.getContext('2d');
const overlayCtx = overlayCanvas.getContext('2d');
autoScale.current = 1;
baseScale.current = 1;
translatePos.current = { x: 0, y: 0 };
contentPos.current = { x: 0, y: 0 };
canvas.style.transform = 'scale(1)';
overlayCanvas.style.transform = 'scale(1)';
cursorRef.current!.style.width = `${lineWidth}px`;
cursorRef.current!.style.height = `${lineWidth}px`;
ctx!.resetTransform();
overlayCtx!.resetTransform();
}, []);
const invertPainting = (isChecked: boolean) => {
const ctx = overlayCanvasRef.current!.getContext('2d');
@ -704,65 +378,8 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
invertMask
]);
const updateZoom = (
scaleChange: number,
mouseX: number,
mouseY: number
) => {
const newScale = _.round(autoScale.current + scaleChange, 2);
if (newScale < MIN_SCALE || newScale > MAX_SCALE) return;
const { current: oldScale } = autoScale;
const { x: oldTranslateX, y: oldTranslateY } = translatePos.current;
const centerX = (mouseX - oldTranslateX) / oldScale;
const centerY = (mouseY - oldTranslateY) / oldScale;
autoScale.current = newScale;
const newTranslateX = mouseX - centerX * newScale;
const newTranslateY = mouseY - centerY * newScale;
translatePos.current = { x: newTranslateX, y: newTranslateY };
};
const handleZoom = (event: React.WheelEvent<HTMLCanvasElement>) => {
const scaleChange = event.deltaY > 0 ? -ZOOM_SPEED : ZOOM_SPEED;
// current mouse position
const canvas = overlayCanvasRef.current!;
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
canvasRef.current!.style.transform = `scale(${autoScale.current})`;
setCanvasTransformOrigin(event);
updateZoom(scaleChange, mouseX, mouseY);
};
const updateCursorPosOnZoom = (e: any) => {
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const handleOnWheel = (event: any) => {
// stop
handleZoom(event);
updateCursorSize();
updateCursorPosOnZoom(event);
setActiveScale(autoScale.current);
};
const handleFitView = () => {
resetCanvas();
autoScale.current = baseScale.current;
translatePos.current = { x: 0, y: 0 };
setTransform();
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
canvasRef.current!.style.transform = `scale(${autoScale.current})`;
fitView();
setActiveScale(autoScale.current);
updateCursorSize();
redrawStrokes(strokesRef.current);
@ -786,7 +403,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
}, [initializeImage]);
useEffect(() => {
createOffscreenCanvas();
const handleUndoShortcut = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
undo();
@ -821,7 +437,11 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
};
useImperativeHandle(ref, () => ({
clearMask: handleDeleteMask
clearMask: handleDeleteMask,
loadMaskPixs(strokes: Stroke[]) {
setStrokes(strokes);
loadMaskPixs(strokes);
}
}));
useEffect(() => {
@ -840,130 +460,26 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
return (
<div className="editor-wrapper">
<div className="flex-between">
<div className="tools">
<Tooltip
placement="bottomLeft"
arrow={false}
overlayInnerStyle={{
background: 'var(--color-white-1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
width: 160
}}
title={
<div className="flex-column" style={{ width: '100%' }}>
<span className="text-secondary">
{intl.formatMessage({ id: 'playground.image.brushSize' })}
</span>
<Slider
disabled={disabled}
style={{ marginBlock: '4px 6px', marginLeft: 0, flex: 1 }}
vertical={false}
defaultValue={lineWidth}
min={10}
max={100}
onChange={handleBrushSizeChange}
/>
</div>
}
>
<Button size="middle" type="text">
<FormatPainterOutlined className="font-size-14" />
</Button>
</Tooltip>
<Tooltip
title={
<span>
[{KeyMap.UNDO.textKeybinding}]
<span className="m-l-5">
{intl.formatMessage({ id: 'common.button.undo' })}
</span>
</span>
}
>
<Button
onClick={undo}
size="middle"
type="text"
disabled={disabled}
>
<UndoOutlined className="font-size-14" />
</Button>
</Tooltip>
<Tooltip title={intl.formatMessage({ id: 'common.button.clear' })}>
<Button
onClick={onReset}
size="middle"
type="text"
disabled={disabled}
>
<ClearOutlined className="font-size-14" />
</Button>
</Tooltip>
{uploadButton}
<Tooltip
title={intl.formatMessage({ id: 'playground.image.fitview' })}
>
<Button
onClick={handleFitView}
size="middle"
type="text"
disabled={loading}
>
<ExpandOutlined className="font-size-14" />
</Button>
</Tooltip>
</div>
<div className="tools">
{imageStatus.isOriginal && (
<>
<Tooltip
title={
<span style={{ whiteSpace: 'pre-wrap' }}>
{intl.formatMessage({
id: 'playground.image.negativeMask.tips'
})}
</span>
}
>
<Checkbox
onChange={handleOnChangeMask}
className="flex-center"
checked={invertMask}
disabled={isDisabled || !!maskUpload?.length}
>
<span className="font-size-12">
{intl.formatMessage({
id: 'playground.image.negativeMask'
})}
</span>
</Checkbox>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id: 'playground.image.saveMask'
})}
>
<Button onClick={downloadMask} size="middle" type="text">
<IconFont
className="font-size-14"
type="icon-save2"
></IconFont>
</Button>
</Tooltip>
</>
)}
{!imageStatus.isOriginal && (
<Tooltip
title={intl.formatMessage({ id: 'playground.image.download' })}
>
<Button onClick={download} size="middle" type="text">
<DownloadOutlined className="font-size-14" />
</Button>
</Tooltip>
)}
</div>
<ToolsBar
handleBrushSizeChange={handleBrushSizeChange}
undo={undo}
onReset={onReset}
uploadButton={uploadButton}
handleFitView={handleFitView}
disabled={disabled}
lineWidth={lineWidth}
loading={loading}
></ToolsBar>
<ImageActionsBar
handleOnChangeMask={handleOnChangeMask}
invertMask={invertMask}
downloadMask={downloadMask}
download={download}
isOriginal={imageStatus.isOriginal}
disabled={isDisabled || !!maskUpload?.length}
maskUpload={maskUpload}
></ImageActionsBar>
</div>
<div
className="editor-content"

@ -0,0 +1,175 @@
import IconFont from '@/components/icon-font';
import { KeyMap } from '@/config/hotkeys';
import {
ClearOutlined,
DownloadOutlined,
ExpandOutlined,
FormatPainterOutlined,
UndoOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Slider, Tooltip } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React from 'react';
interface ToolsBarProps {
disabled: boolean;
loading: boolean;
lineWidth: number;
uploadButton: React.ReactNode;
handleBrushSizeChange: (value: number) => void;
undo: () => void;
onReset: () => void;
handleFitView: () => void;
}
const ToolsBar: React.FC<ToolsBarProps> = (props) => {
const {
disabled,
loading,
lineWidth,
handleBrushSizeChange,
undo,
onReset,
uploadButton,
handleFitView
} = props;
const intl = useIntl();
return (
<div className="tools">
<Tooltip
placement="bottomLeft"
arrow={false}
overlayInnerStyle={{
background: 'var(--color-white-1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
width: 160
}}
title={
<div className="flex-column" style={{ width: '100%' }}>
<span className="text-secondary">
{intl.formatMessage({ id: 'playground.image.brushSize' })}
</span>
<Slider
disabled={disabled}
style={{ marginBlock: '4px 6px', marginLeft: 0, flex: 1 }}
vertical={false}
defaultValue={lineWidth}
min={10}
max={100}
onChange={handleBrushSizeChange}
/>
</div>
}
>
<Button size="middle" type="text">
<FormatPainterOutlined className="font-size-14" />
</Button>
</Tooltip>
<Tooltip
title={
<span>
[{KeyMap.UNDO.textKeybinding}]
<span className="m-l-5">
{intl.formatMessage({ id: 'common.button.undo' })}
</span>
</span>
}
>
<Button onClick={undo} size="middle" type="text" disabled={disabled}>
<UndoOutlined className="font-size-14" />
</Button>
</Tooltip>
<Tooltip title={intl.formatMessage({ id: 'common.button.clear' })}>
<Button onClick={onReset} size="middle" type="text" disabled={disabled}>
<ClearOutlined className="font-size-14" />
</Button>
</Tooltip>
{uploadButton}
<Tooltip title={intl.formatMessage({ id: 'playground.image.fitview' })}>
<Button
onClick={handleFitView}
size="middle"
type="text"
disabled={loading}
>
<ExpandOutlined className="font-size-14" />
</Button>
</Tooltip>
</div>
);
};
interface ImageActionsBarProps {
disabled: boolean;
maskUpload?: any[];
isOriginal: boolean;
invertMask: boolean;
handleOnChangeMask: (e: CheckboxChangeEvent) => void;
downloadMask: () => void;
download: () => void;
}
const ImageActionsBar: React.FC<ImageActionsBarProps> = (props) => {
const intl = useIntl();
const {
disabled,
isOriginal,
invertMask,
handleOnChangeMask,
downloadMask,
download
} = props;
return (
<div className="tools">
{isOriginal && (
<>
<Tooltip
title={
<span style={{ whiteSpace: 'pre-wrap' }}>
{intl.formatMessage({
id: 'playground.image.negativeMask.tips'
})}
</span>
}
>
<Checkbox
onChange={handleOnChangeMask}
className="flex-center"
checked={invertMask}
disabled={disabled}
>
<span className="font-size-12">
{intl.formatMessage({
id: 'playground.image.negativeMask'
})}
</span>
</Checkbox>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id: 'playground.image.saveMask'
})}
>
<Button onClick={downloadMask} size="middle" type="text">
<IconFont className="font-size-14" type="icon-save2"></IconFont>
</Button>
</Tooltip>
</>
)}
{!isOriginal && (
<Tooltip
title={intl.formatMessage({ id: 'playground.image.download' })}
>
<Button onClick={download} size="middle" type="text">
<DownloadOutlined className="font-size-14" />
</Button>
</Tooltip>
)}
</div>
);
};
export { ImageActionsBar, ToolsBar };

@ -36,10 +36,18 @@ const HeaderPrefix: React.FC<HeaderPrefixProps> = (props) => {
}
if (expandable && enableSelection) {
return (
<div className="header-row-prefix-wrapper flex-center">
<div
className="header-row-prefix-wrapper flex-center"
style={{ paddingLeft: 15 }}
>
<span style={{ marginRight: 5 }}>
{_.isBoolean(expandable) ? (
<Button type="text" size="small" onClick={handleToggleExpand}>
<Button
type="text"
size="small"
onClick={handleToggleExpand}
style={{ paddingInline: 6 }}
>
{expandAll ? (
<IconFont
type="icon-collapse_all"

@ -133,7 +133,7 @@ export default {
'The higher the value, the greater the modification to the original image.',
'playground.image.edit.tips': 'Click or drag image to this area to upload',
'playground.image.saveMask': 'Save Mask',
'playground.image.negativeMask': 'Negative Mask',
'playground.image.negativeMask': 'Invert Mask',
'playground.image.brushSize': 'Brush Size',
'playground.image.download': 'Download Image',
'playground.image.generate': 'Generate',

@ -3,6 +3,7 @@ import AlertInfo from '@/components/alert-info';
import SingleImage from '@/components/auto-image/single-image';
import IconFont from '@/components/icon-font';
import CanvasImageEditor from '@/components/image-editor';
import { processImage } from '@/components/image-editor/extract-image-colors';
import routeCachekey from '@/config/route-cachekey';
import UploadImg from '@/pages/playground/components/upload-img';
import { base64ToFile, generateRandomNumber } from '@/utils';
@ -281,11 +282,12 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
);
const handleUpdateMaskList = useCallback(async (base64List: any) => {
setMaskUpload(base64List);
// setMaskUpload(base64List);
const mask = _.get(base64List, '[0].dataUrl', '');
// const maskColors = await extractImageColors(mask);
// console.log('maskColors:', maskColors);
setMask(mask);
const maskColors = await processImage(mask);
console.log('maskColors:', maskColors);
imageEditorRef.current?.loadMaskPixs(maskColors || []);
// setMask(mask);
}, []);
const handleClearUploadMask = useCallback(() => {

@ -22,7 +22,6 @@ const PlaygroundEmbedding: React.FC = () => {
const handleViewCode = useCallback(() => {
ref.current?.viewCode?.();
}, [ref.current]);
const handleToggleCollapse = useCallback(() => {
ref.current?.setCollapse?.();
}, [ref.current]);

@ -77,7 +77,6 @@
.message-content-input {
flex: 1;
cursor: pointer;
&.has-img {
border: 1px solid var(--ant-color-fill-secondary);

Loading…
Cancel
Save