chore: load mask by pix
This commit is contained in:
@@ -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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = base64Image;
|
img.src = file;
|
||||||
|
img.onload = () => resolve(img);
|
||||||
img.onload = function () {
|
img.onerror = () => reject(new Error('Image loading failed'));
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
URL.revokeObjectURL(img.src);
|
|
||||||
|
|
||||||
resolve({ blackPixels, whitePixels });
|
|
||||||
};
|
|
||||||
|
|
||||||
img.onerror = function () {
|
|
||||||
reject(new Error('Image loading failed'));
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
export default extractImageColors;
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||||
|
const whiteBlocks = getWhiteBlocks(imageData);
|
||||||
|
|
||||||
|
URL.revokeObjectURL(img.src);
|
||||||
|
return whiteBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 dayjs from 'dayjs';
|
||||||
import _ from 'lodash';
|
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -19,8 +8,10 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import IconFont from '../icon-font';
|
import useDrawing from './hooks/use-drawing';
|
||||||
|
import useZoom from './hooks/use-zoom';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
import { ImageActionsBar, ToolsBar } from './tools-bar';
|
||||||
|
|
||||||
type Point = { x: number; y: number; lineWidth: number };
|
type Point = { x: number; y: number; lineWidth: number };
|
||||||
type Stroke = Point[];
|
type Stroke = Point[];
|
||||||
@@ -53,222 +44,77 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
imageSrc,
|
imageSrc,
|
||||||
disabled: isDisabled,
|
disabled: isDisabled,
|
||||||
imageStatus,
|
imageStatus,
|
||||||
clearUploadMask,
|
|
||||||
onSave,
|
onSave,
|
||||||
onScaleImageSize,
|
onScaleImageSize,
|
||||||
imguid,
|
|
||||||
uploadButton,
|
uploadButton,
|
||||||
maskUpload
|
maskUpload
|
||||||
},
|
},
|
||||||
ref
|
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 containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [lineWidth, setLineWidth] = useState<number>(60);
|
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 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 negativeMaskRef = useRef<boolean>(false);
|
||||||
const [invertMask, setInvertMask] = useState<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(() => {
|
const disabled = useMemo(() => {
|
||||||
return isDisabled || invertMask || !!maskUpload?.length;
|
return isDisabled || invertMask || !!maskUpload?.length;
|
||||||
}, [isDisabled, invertMask, maskUpload]);
|
}, [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
|
// update the canvas size
|
||||||
const updateCanvasSize = useCallback(() => {
|
const updateCanvasSize = useCallback(() => {
|
||||||
const canvas = canvasRef.current!;
|
const canvas = canvasRef.current!;
|
||||||
const offscreenCanvas = offscreenCanvasRef.current!;
|
|
||||||
const overlayCanvas = overlayCanvasRef.current!;
|
const overlayCanvas = overlayCanvasRef.current!;
|
||||||
|
|
||||||
overlayCanvas.width = canvas.width;
|
overlayCanvas.width = canvas.width;
|
||||||
overlayCanvas.height = canvas.height;
|
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 downloadMask = useCallback(() => {
|
||||||
const mask = generateMask();
|
const mask = generateMask();
|
||||||
|
|
||||||
@@ -278,7 +124,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
link.click();
|
link.click();
|
||||||
}, [generateMask]);
|
}, [generateMask]);
|
||||||
|
|
||||||
const drawStroke = useCallback(
|
const drawFillRect = useCallback(
|
||||||
(
|
(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
stroke: Stroke | Point[],
|
stroke: Stroke | Point[],
|
||||||
@@ -289,194 +135,22 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { color, compositeOperation } = options;
|
const { color, compositeOperation } = options;
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
ctx.globalCompositeOperation = compositeOperation;
|
ctx.globalCompositeOperation = compositeOperation;
|
||||||
|
|
||||||
ctx.beginPath();
|
stroke.forEach((point) => {
|
||||||
|
|
||||||
stroke.forEach((point, i) => {
|
|
||||||
const { x, y } = getTransformedPoint(point.x, point.y);
|
const { x, y } = getTransformedPoint(point.x, point.y);
|
||||||
console.log('Drawing point:');
|
ctx.save();
|
||||||
ctx.lineWidth = getTransformLineWidth(point.lineWidth);
|
const width = getTransformLineWidth(point.lineWidth);
|
||||||
if (i === 0) {
|
ctx.fillStyle = color;
|
||||||
ctx.moveTo(x, y);
|
|
||||||
} else {
|
ctx.fillRect(x - width / 2, y - width / 2, width, width);
|
||||||
ctx.lineTo(x, y);
|
ctx.restore();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (compositeOperation === 'source-over') {
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
},
|
},
|
||||||
[getTransformLineWidth, getTransformedPoint]
|
[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(() => {
|
const onReset = useCallback(() => {
|
||||||
clearOverlayCanvas();
|
clearOverlayCanvas();
|
||||||
setStrokes([]);
|
setStrokes([]);
|
||||||
@@ -488,26 +162,17 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
const redrawStrokes = useCallback(
|
const redrawStrokes = useCallback(
|
||||||
(strokes: Stroke[], type?: string) => {
|
(strokes: Stroke[], type?: string) => {
|
||||||
console.log('Redrawing strokes:', strokes, type);
|
console.log('Redrawing strokes:', strokes, type);
|
||||||
if (!offscreenCanvasRef.current) {
|
clearOverlayCanvas();
|
||||||
createOffscreenCanvas();
|
|
||||||
}
|
|
||||||
if (!strokes.length) {
|
if (!strokes.length) {
|
||||||
clearOverlayCanvas();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const offscreenCanvas = offscreenCanvasRef.current!;
|
|
||||||
const overlayCanvas = overlayCanvasRef.current!;
|
const overlayCanvas = overlayCanvasRef.current!;
|
||||||
const offscreenCtx = offscreenCanvas.getContext('2d')!;
|
|
||||||
const overlayCtx = overlayCanvas!.getContext('2d')!;
|
const overlayCtx = overlayCanvas!.getContext('2d')!;
|
||||||
|
|
||||||
offscreenCanvas.width = overlayCanvas!.width;
|
|
||||||
offscreenCanvas.height = overlayCanvas!.height;
|
|
||||||
|
|
||||||
// clear offscreen canvas
|
// clear offscreen canvas
|
||||||
|
|
||||||
clearOverlayCanvas();
|
|
||||||
|
|
||||||
setTransform();
|
setTransform();
|
||||||
|
|
||||||
strokes?.forEach((stroke: Point[], index) => {
|
strokes?.forEach((stroke: Point[], index) => {
|
||||||
@@ -527,6 +192,35 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
[drawStroke]
|
[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 = () => {
|
const undo = () => {
|
||||||
if (strokesRef.current.length === 0) return;
|
if (strokesRef.current.length === 0) return;
|
||||||
|
|
||||||
@@ -610,26 +304,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
});
|
});
|
||||||
}, [imageSrc]);
|
}, [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 invertPainting = (isChecked: boolean) => {
|
||||||
const ctx = overlayCanvasRef.current!.getContext('2d');
|
const ctx = overlayCanvasRef.current!.getContext('2d');
|
||||||
|
|
||||||
@@ -704,65 +378,8 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
invertMask
|
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 = () => {
|
const handleFitView = () => {
|
||||||
resetCanvas();
|
fitView();
|
||||||
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})`;
|
|
||||||
setActiveScale(autoScale.current);
|
setActiveScale(autoScale.current);
|
||||||
updateCursorSize();
|
updateCursorSize();
|
||||||
redrawStrokes(strokesRef.current);
|
redrawStrokes(strokesRef.current);
|
||||||
@@ -786,7 +403,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
}, [initializeImage]);
|
}, [initializeImage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
createOffscreenCanvas();
|
|
||||||
const handleUndoShortcut = (e: KeyboardEvent) => {
|
const handleUndoShortcut = (e: KeyboardEvent) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
|
||||||
undo();
|
undo();
|
||||||
@@ -821,7 +437,11 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
clearMask: handleDeleteMask
|
clearMask: handleDeleteMask,
|
||||||
|
loadMaskPixs(strokes: Stroke[]) {
|
||||||
|
setStrokes(strokes);
|
||||||
|
loadMaskPixs(strokes);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -840,130 +460,26 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<div className="editor-wrapper">
|
<div className="editor-wrapper">
|
||||||
<div className="flex-between">
|
<div className="flex-between">
|
||||||
<div className="tools">
|
<ToolsBar
|
||||||
<Tooltip
|
handleBrushSizeChange={handleBrushSizeChange}
|
||||||
placement="bottomLeft"
|
undo={undo}
|
||||||
arrow={false}
|
onReset={onReset}
|
||||||
overlayInnerStyle={{
|
uploadButton={uploadButton}
|
||||||
background: 'var(--color-white-1)',
|
handleFitView={handleFitView}
|
||||||
display: 'flex',
|
disabled={disabled}
|
||||||
alignItems: 'center',
|
lineWidth={lineWidth}
|
||||||
justifyContent: 'flex-start',
|
loading={loading}
|
||||||
width: 160
|
></ToolsBar>
|
||||||
}}
|
|
||||||
title={
|
<ImageActionsBar
|
||||||
<div className="flex-column" style={{ width: '100%' }}>
|
handleOnChangeMask={handleOnChangeMask}
|
||||||
<span className="text-secondary">
|
invertMask={invertMask}
|
||||||
{intl.formatMessage({ id: 'playground.image.brushSize' })}
|
downloadMask={downloadMask}
|
||||||
</span>
|
download={download}
|
||||||
<Slider
|
isOriginal={imageStatus.isOriginal}
|
||||||
disabled={disabled}
|
disabled={isDisabled || !!maskUpload?.length}
|
||||||
style={{ marginBlock: '4px 6px', marginLeft: 0, flex: 1 }}
|
maskUpload={maskUpload}
|
||||||
vertical={false}
|
></ImageActionsBar>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="editor-content"
|
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) {
|
if (expandable && enableSelection) {
|
||||||
return (
|
return (
|
||||||
<div className="header-row-prefix-wrapper flex-center">
|
<div
|
||||||
|
className="header-row-prefix-wrapper flex-center"
|
||||||
|
style={{ paddingLeft: 15 }}
|
||||||
|
>
|
||||||
<span style={{ marginRight: 5 }}>
|
<span style={{ marginRight: 5 }}>
|
||||||
{_.isBoolean(expandable) ? (
|
{_.isBoolean(expandable) ? (
|
||||||
<Button type="text" size="small" onClick={handleToggleExpand}>
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={handleToggleExpand}
|
||||||
|
style={{ paddingInline: 6 }}
|
||||||
|
>
|
||||||
{expandAll ? (
|
{expandAll ? (
|
||||||
<IconFont
|
<IconFont
|
||||||
type="icon-collapse_all"
|
type="icon-collapse_all"
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export default {
|
|||||||
'The higher the value, the greater the modification to the original image.',
|
'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.edit.tips': 'Click or drag image to this area to upload',
|
||||||
'playground.image.saveMask': 'Save Mask',
|
'playground.image.saveMask': 'Save Mask',
|
||||||
'playground.image.negativeMask': 'Negative Mask',
|
'playground.image.negativeMask': 'Invert Mask',
|
||||||
'playground.image.brushSize': 'Brush Size',
|
'playground.image.brushSize': 'Brush Size',
|
||||||
'playground.image.download': 'Download Image',
|
'playground.image.download': 'Download Image',
|
||||||
'playground.image.generate': 'Generate',
|
'playground.image.generate': 'Generate',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import AlertInfo from '@/components/alert-info';
|
|||||||
import SingleImage from '@/components/auto-image/single-image';
|
import SingleImage from '@/components/auto-image/single-image';
|
||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
import CanvasImageEditor from '@/components/image-editor';
|
import CanvasImageEditor from '@/components/image-editor';
|
||||||
|
import { processImage } from '@/components/image-editor/extract-image-colors';
|
||||||
import routeCachekey from '@/config/route-cachekey';
|
import routeCachekey from '@/config/route-cachekey';
|
||||||
import UploadImg from '@/pages/playground/components/upload-img';
|
import UploadImg from '@/pages/playground/components/upload-img';
|
||||||
import { base64ToFile, generateRandomNumber } from '@/utils';
|
import { base64ToFile, generateRandomNumber } from '@/utils';
|
||||||
@@ -281,11 +282,12 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateMaskList = useCallback(async (base64List: any) => {
|
const handleUpdateMaskList = useCallback(async (base64List: any) => {
|
||||||
setMaskUpload(base64List);
|
// setMaskUpload(base64List);
|
||||||
const mask = _.get(base64List, '[0].dataUrl', '');
|
const mask = _.get(base64List, '[0].dataUrl', '');
|
||||||
// const maskColors = await extractImageColors(mask);
|
const maskColors = await processImage(mask);
|
||||||
// console.log('maskColors:', maskColors);
|
console.log('maskColors:', maskColors);
|
||||||
setMask(mask);
|
imageEditorRef.current?.loadMaskPixs(maskColors || []);
|
||||||
|
// setMask(mask);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClearUploadMask = useCallback(() => {
|
const handleClearUploadMask = useCallback(() => {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const PlaygroundEmbedding: React.FC = () => {
|
|||||||
const handleViewCode = useCallback(() => {
|
const handleViewCode = useCallback(() => {
|
||||||
ref.current?.viewCode?.();
|
ref.current?.viewCode?.();
|
||||||
}, [ref.current]);
|
}, [ref.current]);
|
||||||
|
|
||||||
const handleToggleCollapse = useCallback(() => {
|
const handleToggleCollapse = useCallback(() => {
|
||||||
ref.current?.setCollapse?.();
|
ref.current?.setCollapse?.();
|
||||||
}, [ref.current]);
|
}, [ref.current]);
|
||||||
|
|||||||
@@ -77,7 +77,6 @@
|
|||||||
|
|
||||||
.message-content-input {
|
.message-content-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.has-img {
|
&.has-img {
|
||||||
border: 1px solid var(--ant-color-fill-secondary);
|
border: 1px solid var(--ant-color-fill-secondary);
|
||||||
|
|||||||
Reference in New Issue
Block a user