diff --git a/src/components/image-editor/index.tsx b/src/components/image-editor/index.tsx index c960642c..22a51dd8 100644 --- a/src/components/image-editor/index.tsx +++ b/src/components/image-editor/index.tsx @@ -1,7 +1,6 @@ import { KeyMap } from '@/config/hotkeys'; import { ClearOutlined, - CloseOutlined, DownloadOutlined, ExpandOutlined, FormatPainterOutlined, @@ -12,8 +11,10 @@ import { Button, Checkbox, Slider, Tooltip } from 'antd'; import dayjs from 'dayjs'; import _ from 'lodash'; import React, { + forwardRef, useCallback, useEffect, + useImperativeHandle, useMemo, useRef, useState @@ -25,6 +26,7 @@ type Point = { x: number; y: number; lineWidth: number }; type Stroke = Point[]; type CanvasImageEditorProps = { + ref?: any; imageSrc: string; disabled?: boolean; imguid: string | number; @@ -43,947 +45,983 @@ type CanvasImageEditorProps = { const COLOR = 'rgba(0, 0, 255, 0.3)'; -const CanvasImageEditor: React.FC = ({ - imageSrc, - disabled: isDisabled, - imageStatus, - clearUploadMask, - onSave, - onScaleImageSize, - imguid, - uploadButton, - maskUpload -}) => { - 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 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 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 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 transformedX = (offsetX - translateX) / scale; - const transformedY = (offsetY - translateY) / scale; + const getTransformLineWidth = useCallback((lineWidth: number) => { + return lineWidth / autoScale.current; + }, []); - return { - x: transformedX, - y: transformedY - }; - }, - [] - ); + const setCanvasTransformOrigin = ( + e: React.MouseEvent + ) => { + if (autoScale.current <= MIN_SCALE) { + return; + } - const getTransformLineWidth = useCallback((lineWidth: number) => { - return lineWidth / autoScale.current; - }, []); + if (autoScale.current >= MAX_SCALE) { + return; + } - const setCanvasTransformOrigin = (e: React.MouseEvent) => { - if (autoScale.current <= MIN_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; - if (autoScale.current >= MAX_SCALE) { - return; - } + const originX = mouseX / rect.width; + const originY = mouseY / rect.height; - console.log('Setting transform origin:', autoScale.current); - const rect = overlayCanvasRef.current!.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; + overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`; + canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`; + }; - const originX = mouseX / rect.width; - const originY = mouseY / rect.height; + 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`; + }; - overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`; - canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`; - }; + 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 handleMouseEnter = (e: React.MouseEvent) => { - console.log('mouse enter:', mouseDownState.current); - if (disabled) { + const handleMouseLeave = () => { + if (disabled) { + return; + } + isDrawing.current = false; 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`; - }; + cursorRef.current!.style.display = 'none'; + }; - 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; + } + }; - 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!; - // 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; - overlayCanvas.width = canvas.width; - overlayCanvas.height = canvas.height; + offscreenCanvas.width = canvas.width; + offscreenCanvas.height = canvas.height; + }, []); - offscreenCanvas.width = canvas.width; - offscreenCanvas.height = canvas.height; - }, [canvasRef.current, overlayCanvasRef.current, offscreenCanvasRef.current]); + const setStrokes = (strokes: Stroke[]) => { + strokesRef.current = strokes; + }; - 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 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 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 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(); + + const link = document.createElement('a'); + link.download = `mask_${dayjs().format('YYYYMMDDHHmmss')}.png`; + link.href = mask || ''; + link.click(); + }, [generateMask]); + + const drawStroke = useCallback( + ( + ctx: CanvasRenderingContext2D, + stroke: Stroke | Point[], + options: { + lineWidth?: number; + color: string; + compositeOperation: 'source-over' | 'destination-out'; } - } - }, - [] - ); - - 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 { 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 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(); - - const link = document.createElement('a'); - link.download = `mask_${dayjs().format('YYYYMMDDHHmmss')}.png`; - link.href = mask || ''; - link.click(); - }, [generateMask]); - - 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(); + 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; - 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); + + ctx.lineTo(x, y); + if (compositeOperation === 'source-over') { + ctx.strokeStyle = color; } - }); - 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.stroke(); + }, + [getTransformLineWidth] + ); - ctx.lineWidth = getTransformLineWidth(lineWidth); - ctx.lineCap = 'round'; - ctx.lineJoin = 'round'; - ctx.globalCompositeOperation = compositeOperation; + const setTransform = useCallback(() => { + const ctx = canvasRef.current?.getContext('2d'); + const overlayCtx = overlayCanvasRef.current?.getContext('2d'); - const { x, y } = getTransformedPoint(point.x, point.y); + if (!ctx || !overlayCtx) return; - 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!.resetTransform(); + overlayCtx!.resetTransform(); - ctx!.restore(); - }; + const { current: scale } = autoScale; + const { x: translateX, y: translateY } = translatePos.current; + ctx!.setTransform(scale, 0, 0, scale, translateX, translateY); - const startDrawing = (e: React.MouseEvent) => { - if (disabled) { - return; - } + overlayCtx!.setTransform(scale, 0, 0, scale, translateX, translateY); + }, []); - isDrawing.current = true; + 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 + }); - currentStroke.current = []; - const { offsetX, offsetY } = e.nativeEvent; + const ctx = overlayCanvasRef.current!.getContext('2d'); - const currentX = offsetX; - const currentY = offsetY; + ctx!.save(); - currentStroke.current.push({ - x: currentX, - y: currentY, - lineWidth - }); + 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' } + ); - const ctx = overlayCanvasRef.current!.getContext('2d'); - setTransform(); - const { x, y } = getTransformedPoint(currentX, currentY); - ctx!.beginPath(); - ctx!.moveTo(x, y); + ctx!.restore(); + }; - draw(e); - }; + const startDrawing = (e: React.MouseEvent) => { + if (disabled) { + return; + } - const endDrawing = (e: React.MouseEvent) => { - if (disabled) { - return; - } - if (!isDrawing.current) { - return; - } + isDrawing.current = true; - console.log('End Drawing:', e); + currentStroke.current = []; + const { offsetX, offsetY } = e.nativeEvent; - isDrawing.current = false; + const currentX = offsetX; + const currentY = offsetY; - strokesRef.current.push(_.cloneDeep(currentStroke.current)); + currentStroke.current.push({ + x: currentX, + y: currentY, + lineWidth + }); - currentStroke.current = []; + const ctx = overlayCanvasRef.current!.getContext('2d'); + setTransform(); + const { x, y } = getTransformedPoint(currentX, currentY); + ctx!.beginPath(); + ctx!.moveTo(x, y); - saveImage(); - }; + draw(e); + }; - 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([]); - currentStroke.current = []; - saveImage(); - console.log('Resetting strokes', currentStroke.current); - }, []); - - const redrawStrokes = useCallback( - (strokes: Stroke[], type?: string) => { - console.log('Redrawing strokes:', strokes, type); - if (!offscreenCanvasRef.current) { - createOffscreenCanvas(); + const endDrawing = (e: React.MouseEvent) => { + if (disabled) { + return; } - if (!strokes.length) { - clearOverlayCanvas(); + if (!isDrawing.current) { return; } - const offscreenCanvas = offscreenCanvasRef.current!; - const overlayCanvas = overlayCanvasRef.current!; - const offscreenCtx = offscreenCanvas.getContext('2d')!; - const overlayCtx = overlayCanvas!.getContext('2d')!; + console.log('End Drawing:', e); - offscreenCanvas.width = overlayCanvas!.width; - offscreenCanvas.height = overlayCanvas!.height; + isDrawing.current = false; - // clear offscreen canvas + strokesRef.current.push(_.cloneDeep(currentStroke.current)); - clearOverlayCanvas(); + currentStroke.current = []; - setTransform(); + saveImage(); + }; - strokes?.forEach((stroke: Point[], index) => { - overlayCtx.save(); - drawStroke(overlayCtx, stroke, { - color: COLOR, - compositeOperation: 'destination-out' - }); + 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); + }, []); - drawStroke(overlayCtx, stroke, { - color: COLOR, - compositeOperation: 'source-over' - }); - overlayCtx.restore(); - }); - }, - [drawStroke] - ); + const onReset = useCallback(() => { + clearOverlayCanvas(); + setStrokes([]); + saveImage(); + currentStroke.current = []; + console.log('Resetting strokes', currentStroke.current); + }, []); + + const redrawStrokes = useCallback( + (strokes: Stroke[], type?: string) => { + console.log('Redrawing strokes:', strokes, type); + if (!offscreenCanvasRef.current) { + createOffscreenCanvas(); + } + if (!strokes.length) { + clearOverlayCanvas(); + return; + } - const undo = () => { - if (strokesRef.current.length === 0) return; + const offscreenCanvas = offscreenCanvasRef.current!; + const overlayCanvas = overlayCanvasRef.current!; + const offscreenCtx = offscreenCanvas.getContext('2d')!; + const overlayCtx = overlayCanvas!.getContext('2d')!; - const newStrokes = strokesRef.current.slice(0, -1); - console.log('New strokes:', newStrokes, strokesRef.current); - setStrokes(newStrokes); + offscreenCanvas.width = overlayCanvas!.width; + offscreenCanvas.height = overlayCanvas!.height; - redrawStrokes(newStrokes); - }; + // clear offscreen canvas - const downloadOriginImage = () => { - const canvas = canvasRef.current!; - const link = document.createElement('a'); - link.download = `image_${dayjs().format('YYYYMMDDHHmmss')}.png`; - link.href = canvas.toDataURL('image/png'); - link.click(); - link.remove(); - }; + clearOverlayCanvas(); + + setTransform(); - const downloadNewImage = () => { - if (!imageSrc) return; + strokes?.forEach((stroke: Point[], index) => { + overlayCtx.save(); + drawStroke(overlayCtx, stroke, { + color: COLOR, + compositeOperation: 'destination-out' + }); - const img = new Image(); - img.src = imageSrc; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = imageStatus.width; - canvas.height = imageStatus.height; + drawStroke(overlayCtx, stroke, { + color: COLOR, + compositeOperation: 'source-over' + }); + overlayCtx.restore(); + }); + }, + [drawStroke] + ); - const ctx = canvas.getContext('2d')!; - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + const undo = () => { + if (strokesRef.current.length === 0) return; - const url = canvas.toDataURL('image/png'); - const filename = `${canvas.width}x${canvas.height}_${dayjs().format('YYYYMMDDHHmmss')}.png`; + const newStrokes = strokesRef.current.slice(0, -1); + console.log('New strokes:', newStrokes, strokesRef.current); + setStrokes(newStrokes); + + redrawStrokes(newStrokes); + }; + const downloadOriginImage = () => { + const canvas = canvasRef.current!; const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); + link.download = `image_${dayjs().format('YYYYMMDDHHmmss')}.png`; + link.href = canvas.toDataURL('image/png'); link.click(); link.remove(); }; - }; - const download = () => { - if (imageStatus.isOriginal) { - downloadOriginImage(); - } else { - downloadNewImage(); - } - }; + const downloadNewImage = () => { + if (!imageSrc) return; - const drawImage = useCallback(async () => { - if (!containerRef.current || !canvasRef.current) return; - return new Promise((resolve) => { const img = new Image(); img.src = imageSrc; img.onload = () => { - const canvas = canvasRef.current!; - const ctx = canvas!.getContext('2d'); - const container = containerRef.current; - baseScale.current = Math.min( - container!.offsetWidth / img.width, - container!.offsetHeight / img.height, - 1 - ); - - // if need to fit the image to the container, show * baseScale.current - canvas!.width = img.width; - canvas!.height = img.height; - - // fit the image to the container - autoScale.current = autoScale.current || 1; - updateCanvasSize(); - - clearCanvas(); - - ctx!.drawImage(img, 0, 0, canvas!.width, canvas!.height); - resolve(); + const canvas = document.createElement('canvas'); + canvas.width = imageStatus.width; + canvas.height = imageStatus.height; + + const ctx = canvas.getContext('2d')!; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + const url = canvas.toDataURL('image/png'); + const filename = `${canvas.width}x${canvas.height}_${dayjs().format('YYYYMMDDHHmmss')}.png`; + + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + link.remove(); }; - }); - }, [imageSrc, containerRef.current, canvasRef.current, updateCanvasSize]); + }; - const resetCanvas = useCallback(() => { - const canvas = canvasRef.current!; - const overlayCanvas = overlayCanvasRef.current!; - const ctx = canvas.getContext('2d'); - const overlayCtx = overlayCanvas.getContext('2d'); + const download = () => { + if (imageStatus.isOriginal) { + downloadOriginImage(); + } else { + downloadNewImage(); + } + }; - 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)'; + const drawImage = useCallback(async () => { + if (!containerRef.current || !canvasRef.current) return; + return new Promise((resolve) => { + const img = new Image(); + img.src = imageSrc; + img.onload = () => { + const canvas = canvasRef.current!; + const ctx = canvas!.getContext('2d'); + const container = containerRef.current; + baseScale.current = Math.min( + container!.offsetWidth / img.width, + container!.offsetHeight / img.height, + 1 + ); + + // if need to fit the image to the container, show * baseScale.current + canvas!.width = img.width; + canvas!.height = img.height; + + // fit the image to the container + autoScale.current = autoScale.current || 1; + + updateCanvasSize(); + clearCanvas(); + + ctx!.drawImage(img, 0, 0, canvas!.width, canvas!.height); + resolve(); + }; + }); + }, [imageSrc]); - cursorRef.current!.style.width = `${lineWidth}px`; - cursorRef.current!.style.height = `${lineWidth}px`; + const resetCanvas = useCallback(() => { + const canvas = canvasRef.current!; + const overlayCanvas = overlayCanvasRef.current!; + const ctx = canvas.getContext('2d'); + const overlayCtx = overlayCanvas.getContext('2d'); - ctx!.resetTransform(); - overlayCtx!.resetTransform(); - }, []); + 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)'; - const invertPainting = (isChecked: boolean) => { - const ctx = overlayCanvasRef.current!.getContext('2d'); + cursorRef.current!.style.width = `${lineWidth}px`; + cursorRef.current!.style.height = `${lineWidth}px`; - if (!ctx) return; + ctx!.resetTransform(); + overlayCtx!.resetTransform(); + }, []); - if (isChecked) { - const canvasWidth = overlayCanvasRef.current!.width; - const canvasHeight = overlayCanvasRef.current!.height; + const invertPainting = (isChecked: boolean) => { + const ctx = overlayCanvasRef.current!.getContext('2d'); - clearOverlayCanvas(); + if (!ctx) return; - ctx.fillStyle = COLOR; - ctx.fillRect(0, 0, canvasWidth, canvasHeight); + if (isChecked) { + const canvasWidth = overlayCanvasRef.current!.width; + const canvasHeight = overlayCanvasRef.current!.height; - setTransform(); - ctx.globalCompositeOperation = 'destination-out'; + clearOverlayCanvas(); - strokesRef.current.forEach((stroke) => { - stroke.forEach((point: Point) => { - const { x, y } = getTransformedPoint(point.x, point.y); - const lineWidth = getTransformLineWidth(point.lineWidth); - ctx.fillStyle = 'rgba(0,0,0,1)'; - ctx.beginPath(); - ctx.arc(x, y, lineWidth / 2, 0, Math.PI * 2); - ctx.fill(); + ctx.fillStyle = COLOR; + ctx.fillRect(0, 0, canvasWidth, canvasHeight); + + setTransform(); + ctx.globalCompositeOperation = 'destination-out'; + + strokesRef.current.forEach((stroke) => { + stroke.forEach((point: Point) => { + const { x, y } = getTransformedPoint(point.x, point.y); + const lineWidth = getTransformLineWidth(point.lineWidth); + ctx.fillStyle = 'rgba(0,0,0,1)'; + ctx.beginPath(); + ctx.arc(x, y, lineWidth / 2, 0, Math.PI * 2); + ctx.fill(); + }); }); + ctx.globalCompositeOperation = 'source-out'; + } else { + redrawStrokes(strokesRef.current); + } + }; + + const updateCursorSize = () => { + cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`; + cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`; + }; + + const initializeImage = useCallback(async () => { + await drawImage(); + onScaleImageSize?.({ + width: canvasRef.current!.width, + height: canvasRef.current!.height }); - ctx.globalCompositeOperation = 'source-out'; - } else { - redrawStrokes(strokesRef.current); - } - }; - const updateCursorSize = () => { - cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`; - cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`; - }; + console.log('Image status:', imageStatus, invertMask); + + if (imageStatus.isResetNeeded) { + onReset(); + resetCanvas(); + } else if ( + strokesRef.current.length && + imageStatus.isOriginal && + !invertMask + ) { + redrawStrokes(strokesRef.current); + saveImage(); + } else if ( + strokesRef.current.length && + imageStatus.isOriginal && + invertMask + ) { + invertPainting(true); + saveImage(); + } - const initializeImage = useCallback(async () => { - if (imguid === preImguid.current) { - return; - } - - await drawImage(); - onScaleImageSize?.({ - width: canvasRef.current!.width, - height: canvasRef.current!.height - }); - - if (strokeCache.current[imguid]) { - strokeCache.current[preImguid.current] = strokesRef.current; - strokesRef.current = strokeCache.current[imguid]; - } else if (preImguid.current !== imguid) { - strokeCache.current[preImguid.current] = strokesRef.current; - onReset(); - resetCanvas(); - } - preImguid.current = imguid; - - if (strokesRef.current.length && imageStatus.isOriginal) { - redrawStrokes(strokesRef.current); - saveImage(); - } - updateCursorSize(); - }, [drawImage, onReset, redrawStrokes, imguid]); + updateCursorSize(); + }, [ + drawImage, + imageStatus.isOriginal, + imageStatus.isResetNeeded, + invertMask + ]); + + const updateZoom = ( + scaleChange: number, + mouseX: number, + mouseY: number + ) => { + const newScale = _.round(autoScale.current + scaleChange, 2); - const updateZoom = (scaleChange: number, mouseX: number, mouseY: number) => { - const newScale = _.round(autoScale.current + scaleChange, 2); + if (newScale < MIN_SCALE || newScale > MAX_SCALE) return; - if (newScale < MIN_SCALE || newScale > MAX_SCALE) return; + const { current: oldScale } = autoScale; + const { x: oldTranslateX, y: oldTranslateY } = translatePos.current; - const { current: oldScale } = autoScale; - const { x: oldTranslateX, y: oldTranslateY } = translatePos.current; + const centerX = (mouseX - oldTranslateX) / oldScale; + const centerY = (mouseY - oldTranslateY) / oldScale; - const centerX = (mouseX - oldTranslateX) / oldScale; - const centerY = (mouseY - oldTranslateY) / oldScale; + autoScale.current = newScale; - autoScale.current = newScale; + const newTranslateX = mouseX - centerX * newScale; + const newTranslateY = mouseY - centerY * newScale; - const newTranslateX = mouseX - centerX * newScale; - const newTranslateY = mouseY - centerY * newScale; + translatePos.current = { x: newTranslateX, y: newTranslateY }; + }; - translatePos.current = { x: newTranslateX, y: newTranslateY }; - }; + const handleZoom = (event: React.WheelEvent) => { + const scaleChange = event.deltaY > 0 ? -ZOOM_SPEED : ZOOM_SPEED; - 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(); - // current mouse position - const canvas = overlayCanvasRef.current!; - const rect = canvas.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; - const mouseX = event.clientX - rect.left; - const mouseY = event.clientY - rect.top; + setCanvasTransformOrigin(event); - setCanvasTransformOrigin(event); + updateZoom(scaleChange, mouseX, mouseY); - updateZoom(scaleChange, mouseX, mouseY); + overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`; + canvasRef.current!.style.transform = `scale(${autoScale.current})`; + }; - overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`; - canvasRef.current!.style.transform = `scale(${autoScale.current})`; - }; + 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 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 handleOnWheel = (event: any) => { - // stop - handleZoom(event); - updateCursorSize(); - updateCursorPosOnZoom(event); - setActiveScale(autoScale.current); - }; + const handleFitView = () => { + 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); + updateCursorSize(); + redrawStrokes(strokesRef.current); + }; - const handleFitView = () => { - 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); - updateCursorSize(); - redrawStrokes(strokesRef.current); - }; + const handleBrushSizeChange = (value: number) => { + setLineWidth(value); + cursorRef.current!.style.width = `${value}px`; + cursorRef.current!.style.height = `${value}px`; + }; - const handleBrushSizeChange = (value: number) => { - setLineWidth(value); - cursorRef.current!.style.width = `${value}px`; - cursorRef.current!.style.height = `${value}px`; - }; + const handleOnChangeMask = (e: any) => { + negativeMaskRef.current = e.target.checked; + invertPainting(e.target.checked); + setInvertMask(e.target.checked); + saveImage(); + }; - const handleOnChangeMask = (e: any) => { - negativeMaskRef.current = e.target.checked; - invertPainting(e.target.checked); - setInvertMask(e.target.checked); - saveImage(); - }; + useEffect(() => { + initializeImage(); + }, [initializeImage]); - useEffect(() => { - initializeImage(); - }, [initializeImage]); + useEffect(() => { + createOffscreenCanvas(); + const handleUndoShortcut = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'z') { + undo(); + } + }; - useEffect(() => { - createOffscreenCanvas(); - const handleUndoShortcut = (e: KeyboardEvent) => { - if ((e.ctrlKey || e.metaKey) && e.key === 'z') { - undo(); - } - }; + const handleMouseDown = (e: MouseEvent) => { + mouseDownState.current = true; + }; - const handleMouseDown = (e: MouseEvent) => { - mouseDownState.current = true; - }; + const handleMouseUp = (e: MouseEvent) => { + mouseDownState.current = false; + }; - const handleMouseUp = (e: MouseEvent) => { - mouseDownState.current = false; - }; + window.addEventListener('keydown', handleUndoShortcut); + // mouse down + window.addEventListener('mousedown', handleMouseDown); - window.addEventListener('keydown', handleUndoShortcut); - // mouse down - window.addEventListener('mousedown', handleMouseDown); + // mouse up + window.addEventListener('mouseup', handleMouseUp); + return () => { + window.removeEventListener('keydown', handleUndoShortcut); + window.removeEventListener('mousedown', handleMouseDown); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, []); - // mouse up - window.addEventListener('mouseup', handleMouseUp); - return () => { - window.removeEventListener('keydown', handleUndoShortcut); - window.removeEventListener('mousedown', handleMouseDown); - window.removeEventListener('mouseup', handleMouseUp); + const handleDeleteMask = () => { + setInvertMask(false); + redrawStrokes(strokesRef.current); + saveImage(); }; - }, []); - const handleDeleteMask = () => { - clearUploadMask?.(); - setInvertMask(false); - redrawStrokes(strokesRef.current); - }; + useImperativeHandle(ref, () => ({ + clearMask: handleDeleteMask + })); - useEffect(() => { - if (maskUpload?.length) { - // clear the overlay canvas - clearOverlayCanvas(); - } - }, [maskUpload]); - - return ( -
-
-
- - - {intl.formatMessage({ id: 'playground.image.brushSize' })} - - -
- } - > - - - - [{KeyMap.UNDO.textKeybinding}] - - {intl.formatMessage({ id: 'common.button.undo' })} - - - } - > - - - - + + + [{KeyMap.UNDO.textKeybinding}] + + {intl.formatMessage({ id: 'common.button.undo' })} + + + } > - - - - {uploadButton} - - + + + + + {uploadButton} + - - - -
-
- {maskUpload?.length ? ( - - - {intl.formatMessage({ id: 'playground.image.mask.uploaded' })} - ... - - - ) : ( - <> - {imageStatus.isOriginal && ( - <> - - - {intl.formatMessage({ - id: 'playground.image.negativeMask' + disabled={disabled} + > + + + +
+
+ {maskUpload?.length ? ( + + + {intl.formatMessage({ id: 'playground.image.mask.uploaded' })} + + + ) : ( + <> + {imageStatus.isOriginal && ( + <> + + + + {intl.formatMessage({ + id: 'playground.image.negativeMask' + })} + + + + - - - - - - )} - - )} - - - + > + + + + )} + + )} + {!imageStatus.isOriginal && ( + + + + )} +
- -
- - { - mouseDownState.current = true; - startDrawing(event); - }} - onMouseUp={(event) => { - mouseDownState.current = false; - endDrawing(event); - }} - onMouseEnter={handleMouseEnter} - onWheel={handleOnWheel} - onMouseMove={(e) => { - handleMouseMove(e); - draw(e); - }} - onMouseLeave={(e) => { - endDrawing(e); - handleMouseLeave(); - }} - />
+ > + + { + mouseDownState.current = true; + startDrawing(event); + }} + onMouseUp={(event) => { + mouseDownState.current = false; + endDrawing(event); + }} + onMouseEnter={handleMouseEnter} + onWheel={handleOnWheel} + onMouseMove={(e) => { + handleMouseMove(e); + draw(e); + }} + onMouseLeave={(e) => { + endDrawing(e); + handleMouseLeave(); + }} + /> +
+
-
- ); -}; + ); + } +); export default React.memo(CanvasImageEditor); diff --git a/src/locales/en-US/playground.ts b/src/locales/en-US/playground.ts index 70ad729b..8c1ba8c7 100644 --- a/src/locales/en-US/playground.ts +++ b/src/locales/en-US/playground.ts @@ -141,9 +141,12 @@ export default { 'playground.image.fitview': 'Fit View', 'playground.chat.aithought': 'CoT', 'playground.image.mask.uploaded': 'Mask Uploaded', - 'playground.image.mask.upload': 'Upload Mask: takes priority in submission.', + 'playground.image.mask.upload': + 'Upload Mask: No additional drawing allowed after upload.', 'playground.params.frequency_penalty.tips': `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`, 'playground.params.presence_penalty.tips': `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`, 'playground.image.origin': 'Original', - 'playground.image.mask': 'Mask' + 'playground.image.mask': 'Mask', + 'playground.image.negativeMask.tips': + 'After selection, no further masking can be drawn; therefore, you should draw the mask first and then check the option.' }; diff --git a/src/locales/ru-RU/playground.ts b/src/locales/ru-RU/playground.ts index 49705e2c..889fd625 100644 --- a/src/locales/ru-RU/playground.ts +++ b/src/locales/ru-RU/playground.ts @@ -138,9 +138,12 @@ export default { 'playground.image.fitview': 'Подогнать размер', 'playground.chat.aithought': 'Рассуждение (CoT)', 'playground.image.mask.uploaded': 'Маска загружена', - 'playground.image.mask.upload':'Загрузить маску: имеет приоритет при отправке', + 'playground.image.mask.upload': + 'TODO: Translate key "playground.image.mask.upload"', 'playground.params.frequency_penalty.tips': `Число от -2.0 до 2.0. Положительные значения снижают вероятность повторения токенов, уже часто встречающихся в тексте, уменьшая склонность модели дословно повторять одни и те же фразы.`, 'playground.params.presence_penalty.tips': `Число от -2.0 до 2.0. Положительные значения снижают вероятность повторения любых токенов, присутствующих в тексте, повышая склонность модели к обсуждению новых тем.`, 'playground.image.origin': 'Оригинал', - 'playground.image.mask': 'Маска' + 'playground.image.mask': 'Маска', + 'playground.image.negativeMask.tips': + 'TODO: Translate key "playground.image.negativeMask.tips"' }; diff --git a/src/locales/zh-CN/playground.ts b/src/locales/zh-CN/playground.ts index 618290c3..d81d3e04 100644 --- a/src/locales/zh-CN/playground.ts +++ b/src/locales/zh-CN/playground.ts @@ -136,9 +136,11 @@ export default { 'playground.image.fitview': '适应视图', 'playground.chat.aithought': '思考过程', 'playground.image.mask.uploaded': '遮罩已上传', - 'playground.image.mask.upload': '上传遮罩:在提交时优先使用', + 'playground.image.mask.upload': '上传遮罩:上传后将不可再绘制', 'playground.params.frequency_penalty.tips': `数值介于 -2.0 和 2.0 之间。正值会根据新词在文本中已出现的频率对其进行惩罚,从而降低模型逐字重复相同内容的可能性。`, 'playground.params.presence_penalty.tips': `数值介于 -2.0 和 2.0 之间。正值会根据新词是否已在文本中出现过对其进行惩罚,从而增加模型谈论新话题的可能性。`, 'playground.image.origin': '原图', - 'playground.image.mask': '遮罩' + 'playground.image.mask': '遮罩', + 'playground.image.negativeMask.tips': + '选择后,将不可再绘制遮罩;因此,你应该先绘制遮罩然后再勾选' }; diff --git a/src/pages/playground/components/image-edit.tsx b/src/pages/playground/components/image-edit.tsx index a18b6604..b588e2bb 100644 --- a/src/pages/playground/components/image-edit.tsx +++ b/src/pages/playground/components/image-edit.tsx @@ -64,6 +64,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { }); const doneImage = useRef(false); const [activeImgUid, setActiveImgUid] = useState(0); + const imageEditorRef = useRef(null); const { handleOnValuesChange, @@ -110,9 +111,8 @@ const GroundImages: React.FC = forwardRef((props, ref) => { }); const imageFile = useMemo(() => { - if (!image) return null; - return base64ToFile(image, 'image'); - }, [image]); + return base64ToFile(uploadList[0]?.dataUrl, 'image'); + }, [uploadList]); const maskFile = useMemo(() => { if (!mask) return null; @@ -123,7 +123,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { if (parameters.size === 'custom') { return { ..._.omit(parameters, ['width', 'height', 'preview', 'random_seed']), - image: imageFile, + image: base64ToFile(image, 'image'), mask: maskFile, size: parameters.width && parameters.height @@ -279,6 +279,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { const handleClearUploadMask = useCallback(() => { setMaskUpload([]); setMask(null); + imageEditorRef.current?.clearMask(); }, []); const handleOnSave = useCallback( @@ -299,6 +300,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { if (image) { return ( = forwardRef((props, ref) => { ]); const handleOnImgClick = useCallback((item: any, isOrigin: boolean) => { - if (item.progress < 100) { + if (item.progress < 100 && !isOrigin) { return; } setActiveImgUid(item.uid); @@ -421,8 +423,9 @@ const GroundImages: React.FC = forwardRef((props, ref) => { preview={false} loading={false} autoSize={false} - editable={false} + editable={true} autoBgColor={false} + onDelete={() => handleClearUploadMask()} label={ {intl.formatMessage({ id: 'playground.image.mask' })} } diff --git a/src/pages/playground/hooks/use-text-image.ts b/src/pages/playground/hooks/use-text-image.ts index ada18f2e..3f9985ea 100644 --- a/src/pages/playground/hooks/use-text-image.ts +++ b/src/pages/playground/hooks/use-text-image.ts @@ -187,6 +187,7 @@ export default function useTextImage(props: any) { const handleStopConversation = () => { requestToken.current?.abort?.(); + setImageList([]); setLoading(false); }; diff --git a/src/pages/resources/components/container-install.tsx b/src/pages/resources/components/container-install.tsx index 21eed440..79f780a0 100644 --- a/src/pages/resources/components/container-install.tsx +++ b/src/pages/resources/components/container-install.tsx @@ -118,7 +118,12 @@ const AddWorker: React.FC = (props) => { }} > )} - +

3. {intl.formatMessage({ id: 'resources.worker.add.step3' })}

diff --git a/src/pages/resources/config/index.ts b/src/pages/resources/config/index.ts index dc72a203..401780e2 100644 --- a/src/pages/resources/config/index.ts +++ b/src/pages/resources/config/index.ts @@ -44,6 +44,7 @@ export const addWorkerGuide: Record = { return `docker run -d --name gpustack-worker --restart=unless-stopped --gpus all -p 10150:10150 -p 40000-41024:40000-41024 -p 50000-51024:50000-51024 --ipc=host -v gpustack-worker-data:/var/lib/gpustack gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; } }, + npu: { getToken: 'Get-Content -Path (Join-Path -Path $env:APPDATA -ChildPath "gpustack\\token") -Raw', @@ -53,7 +54,31 @@ export const addWorkerGuide: Record = { token: string; workerip: string; }) { - return `docker run -d --name gpustack-worker --restart=unless-stopped -e ASCEND_VISIBLE_DEVICES=0 -p 10150:10150 -p 40000-41024:40000-41024 -p 50000-51024:50000-51024 --ipc=host -v gpustack-worker-data:/var/lib/gpustack gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; + return `docker run -d --name gpustack-worker \\ + --restart=unless-stopped \\ + --device /dev/davinci0 \\ + --device /dev/davinci1 \\ + --device /dev/davinci2 \\ + --device /dev/davinci3 \\ + --device /dev/davinci4 \\ + --device /dev/davinci5 \\ + --device /dev/davinci6 \\ + --device /dev/davinci7 \\ + --device /dev/davinci_manager \\ + --device /dev/devmm_svm \\ + --device /dev/hisi_hdc \\ + -v /usr/local/dcmi:/usr/local/dcmi \\ + -v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \\ + -v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \\ + -v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \\ + -v /etc/ascend_install.info:/etc/ascend_install.info \\ + -p 10150:10150 \\ + -p 40000-41024:40000-41024 \\ + -p 50000-51024:50000-51024 \\ + --ipc=host \\ + -v gpustack-worker-data:/var/lib/gpustack \\ + gpustack/gpustack:${params.tag} \\ + --server-url ${params.server} --token ${params.token} --worker-ip ${params.workerip}`; } }, musa: {