diff --git a/src/components/image-editor/extract-image-colors.ts b/src/components/image-editor/extract-image-colors.ts index b890121c..15d136f1 100644 --- a/src/components/image-editor/extract-image-colors.ts +++ b/src/components/image-editor/extract-image-colors.ts @@ -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>} - List of white blocks, each containing pixel coordinates. + */ +function getWhiteBlocks( + imageData: ImageData +): Array> { + const { data, width, height } = imageData; + let visited: boolean[][] = Array.from({ length: height }, () => + new Array(width).fill(false) + ); + let whiteBlocks: Array> = []; + + 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} - The loaded image element. + */ +function loadImage(file: string): Promise { 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>>} - List of white blocks with pixel coordinates. + */ +async function processImage( + file: string +): Promise>> { + 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 }; diff --git a/src/components/image-editor/hooks/use-drawing.ts b/src/components/image-editor/hooks/use-drawing.ts new file mode 100644 index 00000000..8ad7f4d9 --- /dev/null +++ b/src/components/image-editor/hooks/use-drawing.ts @@ -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(false); + const canvasRef = useRef(null); + const overlayCanvasRef = useRef(null); + const currentStroke = useRef([]); + const strokesRef = useRef([]); + const isDrawing = useRef(false); + const cursorRef = useRef(null); + const autoScale = useRef(1); + const baseScale = useRef(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) => { + 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) => { + 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) => { + 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) => { + 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) => { + 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) => { + 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 + }; +} diff --git a/src/components/image-editor/hooks/use-zoom.ts b/src/components/image-editor/hooks/use-zoom.ts new file mode 100644 index 00000000..6fd902bb --- /dev/null +++ b/src/components/image-editor/hooks/use-zoom.ts @@ -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; + baseScale: MutableRefObject; + 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(1); + + const setCanvasTransformOrigin = (e: React.MouseEvent) => { + 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) => { + 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 + }; +} diff --git a/src/components/image-editor/index.tsx b/src/components/image-editor/index.tsx index b4041bb5..aad54a95 100644 --- a/src/components/image-editor/index.tsx +++ b/src/components/image-editor/index.tsx @@ -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 = 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(null); - const overlayCanvasRef = useRef(null); const containerRef = useRef(null); const [lineWidth, setLineWidth] = useState(60); - const isDrawing = useRef(false); - const currentStroke = useRef([]); - const strokesRef = useRef([]); - const offscreenCanvasRef = useRef(null); - const autoScale = useRef(1); - const baseScale = useRef(1); - const cursorRef = useRef(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({}); - const preImguid = useRef(''); - const [activeScale, setActiveScale] = useState(1); const negativeMaskRef = useRef(false); const [invertMask, setInvertMask] = useState(false); - const mouseDownState = useRef(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 - ) => { - 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) => { - 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) => { - 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) => { - 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) => { - 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 = forwardRef( link.click(); }, [generateMask]); - const drawStroke = useCallback( + const drawFillRect = useCallback( ( ctx: CanvasRenderingContext2D, stroke: Stroke | Point[], @@ -289,194 +135,22 @@ const CanvasImageEditor: React.FC = 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) => { - 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) => { - 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) => { - 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 = 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 = 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 = 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 = 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) => { - 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 = 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 = forwardRef( }; useImperativeHandle(ref, () => ({ - clearMask: handleDeleteMask + clearMask: handleDeleteMask, + loadMaskPixs(strokes: Stroke[]) { + setStrokes(strokes); + loadMaskPixs(strokes); + } })); useEffect(() => { @@ -840,130 +460,26 @@ const CanvasImageEditor: React.FC = forwardRef( return (
-
- - - {intl.formatMessage({ id: 'playground.image.brushSize' })} - - -
- } - > - - - - [{KeyMap.UNDO.textKeybinding}] - - {intl.formatMessage({ id: 'common.button.undo' })} - - - } - > - - - - - - {uploadButton} - - - -
-
- {imageStatus.isOriginal && ( - <> - - {intl.formatMessage({ - id: 'playground.image.negativeMask.tips' - })} - - } - > - - - {intl.formatMessage({ - id: 'playground.image.negativeMask' - })} - - - - - - - - )} - {!imageStatus.isOriginal && ( - - - - )} -
+ + +
void; + undo: () => void; + onReset: () => void; + handleFitView: () => void; +} + +const ToolsBar: React.FC = (props) => { + const { + disabled, + loading, + lineWidth, + handleBrushSizeChange, + undo, + onReset, + uploadButton, + handleFitView + } = props; + const intl = useIntl(); + return ( +
+ + + {intl.formatMessage({ id: 'playground.image.brushSize' })} + + +
+ } + > + + + + [{KeyMap.UNDO.textKeybinding}] + + {intl.formatMessage({ id: 'common.button.undo' })} + + + } + > + + + + + + {uploadButton} + + + +
+ ); +}; + +interface ImageActionsBarProps { + disabled: boolean; + maskUpload?: any[]; + isOriginal: boolean; + invertMask: boolean; + handleOnChangeMask: (e: CheckboxChangeEvent) => void; + downloadMask: () => void; + download: () => void; +} + +const ImageActionsBar: React.FC = (props) => { + const intl = useIntl(); + const { + disabled, + isOriginal, + invertMask, + handleOnChangeMask, + downloadMask, + download + } = props; + return ( +
+ {isOriginal && ( + <> + + {intl.formatMessage({ + id: 'playground.image.negativeMask.tips' + })} + + } + > + + + {intl.formatMessage({ + id: 'playground.image.negativeMask' + })} + + + + + + + + )} + {!isOriginal && ( + + + + )} +
+ ); +}; + +export { ImageActionsBar, ToolsBar }; diff --git a/src/components/seal-table/components/header-prefix.tsx b/src/components/seal-table/components/header-prefix.tsx index 89efc494..d8e86fe7 100644 --- a/src/components/seal-table/components/header-prefix.tsx +++ b/src/components/seal-table/components/header-prefix.tsx @@ -36,10 +36,18 @@ const HeaderPrefix: React.FC = (props) => { } if (expandable && enableSelection) { return ( -
+
{_.isBoolean(expandable) ? ( -