chore: image edit scale

main
jialin 1 year ago
parent c323b6a8a9
commit e089ddb223

@ -1,5 +1,6 @@
import {
DownloadOutlined,
ExpandOutlined,
FormatPainterOutlined,
SyncOutlined,
UndoOutlined
@ -34,6 +35,9 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
onSave,
uploadButton
}) => {
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);
@ -45,43 +49,66 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
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 [imgLoaded, setImgLoaded] = useState(false);
const translatePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const animationFrameIdRef = useRef<number | null>(null);
const originRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const preAutoScale = useRef<number>(1);
let scale = 1;
let offsetX = 0;
let offsetY = 0;
const getTransformedPoint = (offsetX: number, offsetY: number) => {
const { current: scale } = autoScale;
const MIN_SCALE = 0.5;
const MAX_SCALE = 5;
const { x: translateX, y: translateY } = translatePos.current;
const getTransformedPoint = (event: React.MouseEvent<HTMLCanvasElement>) => {
const overlayCanvas = overlayCanvasRef.current!;
const rect = overlayCanvas.getBoundingClientRect();
const transformedX = (offsetX + lineWidth / 2 - translateX) / scale;
const transformedY = (offsetY + lineWidth / 2 - translateY) / scale;
return {
x: Math.round(transformedX),
y: Math.round(transformedY)
};
};
const getTransformLineWidth = (lineWidth: number) => {
return lineWidth / autoScale.current;
};
const setCanvasTransformOrigin = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (autoScale.current <= MIN_SCALE) {
return;
}
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
if (autoScale.current >= MAX_SCALE) {
return;
}
const transformedX = x - overlayCanvas.width / 2;
const transformedY = y - overlayCanvas.height / 2;
console.log('Setting transform origin:', autoScale.current);
const rect = overlayCanvasRef.current!.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
console.log('Mouse Coordinates (Transformed):', transformedX, transformedY);
const originX = mouseX / rect.width;
const originY = mouseY / rect.height;
return { x: transformedX, y: transformedY };
overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
};
const handleMouseEnter = (e: React.MouseEvent<HTMLCanvasElement>) => {
overlayCanvasRef.current!.style.cursor = 'none';
cursorRef.current!.style.display = 'block';
cursorRef.current!.style.top = `${e.clientY - lineWidth / 2}px`;
cursorRef.current!.style.left = `${e.clientX - lineWidth / 2}px`;
cursorRef.current!.style.top = `${e.clientY}px`;
cursorRef.current!.style.left = `${e.clientX}px`;
};
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
overlayCanvasRef.current!.style.cursor = 'none';
cursorRef.current!.style.display = 'block';
cursorRef.current!.style.top = `${e.clientY - lineWidth / 2}px`;
cursorRef.current!.style.left = `${e.clientX - lineWidth / 2}px`;
cursorRef.current!.style.top = `${e.clientY}px`;
cursorRef.current!.style.left = `${e.clientX}px`;
};
const handleMouseLeave = () => {
@ -94,28 +121,11 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
offscreenCanvasRef.current = document.createElement('canvas');
offscreenCanvasRef.current.width = overlayCanvasRef.current!.width;
offscreenCanvasRef.current.height = overlayCanvasRef.current!.height;
const offscreenCtx = offscreenCanvasRef.current.getContext('2d')!;
offscreenCtx.translate(
overlayCanvasRef.current!.width / 2,
overlayCanvasRef.current!.height / 2
);
}
};
const setCanvasCenter = useCallback(() => {
if (!canvasRef.current || !overlayCanvasRef.current) return;
const overlayCtx = overlayCanvasRef.current!.getContext('2d');
const ctx = canvasRef.current!.getContext('2d');
const offscreenCtx = offscreenCanvasRef.current!.getContext('2d');
// Set the origin to the center
overlayCtx!.translate(ctx!.canvas.width / 2, ctx!.canvas.height / 2);
ctx!.translate(ctx!.canvas.width / 2, ctx!.canvas.height / 2);
offscreenCtx!.translate(ctx!.canvas.width / 2, ctx!.canvas.height / 2);
}, [canvasRef.current, overlayCanvasRef.current]);
const scaleCanvasSize = useCallback(() => {
// update the canvas size
const updateCanvasSize = useCallback(() => {
const canvas = canvasRef.current!;
const offscreenCanvas = offscreenCanvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!;
@ -131,10 +141,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
strokesRef.current = strokes;
};
const scaleLineWidth = useCallback(() => {
// setLineWidth(lineWidth * autoScale.current);
}, [lineWidth]);
const generateMask = useCallback(() => {
const overlayCanvas = overlayCanvasRef.current!;
const maskCanvas = document.createElement('canvas');
@ -207,11 +213,12 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
ctx.beginPath();
stroke.forEach((point, i) => {
ctx.lineWidth = point.lineWidth;
const { x, y } = getTransformedPoint(point.x, point.y);
ctx.lineWidth = getTransformLineWidth(point.lineWidth);
if (i === 0) {
ctx.moveTo(point.x, point.y);
ctx.moveTo(x, y);
} else {
ctx.lineTo(point.x, point.y);
ctx.lineTo(x, y);
}
});
if (compositeOperation === 'source-over') {
@ -234,12 +241,14 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
) => {
const { lineWidth, color, compositeOperation } = options;
ctx.lineWidth = lineWidth;
ctx.lineWidth = getTransformLineWidth(lineWidth);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = compositeOperation;
ctx.lineTo(point.x, point.y);
const { x, y } = getTransformedPoint(point.x, point.y);
ctx.lineTo(x, y);
if (compositeOperation === 'source-over') {
ctx.strokeStyle = color;
}
@ -248,14 +257,29 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
[lineWidth]
);
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 (!isDrawing.current) return;
const { x, y } = getTransformedPoint(e);
console.log('Drawing:', e.nativeEvent, { x, y });
const { offsetX, offsetY } = e.nativeEvent;
currentStroke.current.push({
x,
y,
x: offsetX,
y: offsetY,
lineWidth
});
@ -265,12 +289,12 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
drawLine(
ctx!,
{ x, y, lineWidth },
{ x: offsetX, y: offsetY, lineWidth },
{ lineWidth, color: COLOR, compositeOperation: 'destination-out' }
);
drawLine(
ctx!,
{ x, y, lineWidth },
{ x: offsetX, y: offsetY, lineWidth },
{ lineWidth, color: COLOR, compositeOperation: 'source-over' }
);
@ -281,14 +305,16 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
isDrawing.current = true;
currentStroke.current = [];
const { x, y } = getTransformedPoint(e);
const { offsetX, offsetY } = e.nativeEvent;
currentStroke.current.push({
x,
y,
x: offsetX,
y: offsetY,
lineWidth
});
const ctx = overlayCanvasRef.current!.getContext('2d');
setTransform();
const { x, y } = getTransformedPoint(offsetX, offsetY);
ctx!.beginPath();
ctx!.moveTo(x, y);
@ -312,9 +338,10 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
const clearOverlayCanvas = useCallback(() => {
const ctx = overlayCanvasRef.current!.getContext('2d');
ctx!.resetTransform();
ctx!.clearRect(
-overlayCanvasRef.current!.width / 2,
-overlayCanvasRef.current!.height / 2,
0,
0,
overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height
);
@ -323,24 +350,18 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
const clearCanvas = useCallback(() => {
const canvas = canvasRef.current!;
const ctx = canvasRef.current!.getContext('2d');
ctx!.clearRect(
-canvas.width / 2,
-canvas.height / 2,
canvas.width,
canvas.height
);
ctx!.resetTransform();
ctx!.clearRect(0, 0, canvas.width, canvas.height);
}, []);
const clearOffscreenCanvas = useCallback(() => {
const offscreenCanvas = offscreenCanvasRef.current!;
const offscreenCtx = offscreenCanvas.getContext('2d')!;
offscreenCtx.clearRect(
-offscreenCanvas.width / 2,
-offscreenCanvas.height / 2,
offscreenCanvas.width,
offscreenCanvas.height
);
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(() => {
@ -373,6 +394,8 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
clearOverlayCanvas();
setTransform();
strokes?.forEach((stroke: Point[], index) => {
overlayCtx.save();
drawStroke(overlayCtx, stroke, {
@ -409,21 +432,6 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
link.remove();
};
const scaleStrokes = (scale: number): Stroke[] => {
const strokes: Stroke[] = _.cloneDeep(strokesRef.current);
const newStrokes = strokes.map((stroke) => {
return stroke.map((point) => {
return {
x: point.x * scale,
y: point.y * scale,
lineWidth: point.lineWidth
};
});
});
setStrokes(newStrokes);
return newStrokes;
};
const drawImage = useCallback(async () => {
if (!containerRef.current || !canvasRef.current) return;
return new Promise<void>((resolve) => {
@ -433,53 +441,46 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
const canvas = canvasRef.current!;
const ctx = canvas!.getContext('2d');
const container = containerRef.current;
const scale = Math.min(
baseScale.current = Math.min(
container!.offsetWidth / img.width,
container!.offsetHeight / img.height,
1
);
canvas!.width = img.width * scale;
canvas!.height = img.height * scale;
autoScale.current = scale / autoScale.current;
canvas!.width = img.width * baseScale.current;
canvas!.height = img.height * baseScale.current;
scaleLineWidth();
scaleCanvasSize();
setCanvasCenter();
// fit the image to the container
autoScale.current = autoScale.current || 1;
updateCanvasSize();
clearCanvas();
ctx!.drawImage(
img,
-canvas.width / 2,
-canvas.height / 2,
canvas!.width,
canvas!.height
);
ctx!.drawImage(img, 0, 0, canvas!.width, canvas!.height);
resolve();
};
});
}, [
imageSrc,
containerRef.current,
canvasRef.current,
scaleCanvasSize,
setCanvasCenter,
scaleLineWidth
]);
const handleResize = useCallback(
async (entries: ResizeObserverEntry[]) => {
const contentRect = entries[0].contentRect;
if (!contentRect.width || !contentRect.height || !imgLoaded) return;
await drawImage();
if (imageStatus.isOriginal) {
redrawStrokes(strokesRef.current, 'resize');
}
},
[drawImage, scaleStrokes, redrawStrokes, onReset, imageStatus, imgLoaded]
);
}, [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');
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 initializeImage = useCallback(async () => {
setImgLoaded(false);
@ -490,74 +491,71 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
redrawStrokes(strokesRef.current, 'initialize');
} else if (imageStatus.isResetNeeded) {
onReset();
resetCanvas();
}
}, [drawImage, onReset, redrawStrokes, imageStatus]);
const calcTransformedPoint = (event: React.MouseEvent<HTMLCanvasElement>) => {
const overlayCanvas = overlayCanvasRef.current!;
const rect = overlayCanvas.getBoundingClientRect();
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;
// 获取鼠标在画布上的原始坐标
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
autoScale.current = newScale;
// 考虑缩放比例和偏移量
const transformedX = (x - offsetX) / scale;
const transformedY = (y - offsetY) / scale;
const newTranslateX = mouseX - centerX * newScale;
const newTranslateY = mouseY - centerY * newScale;
return { x: transformedX, y: transformedY };
translatePos.current = { x: newTranslateX, y: newTranslateY };
};
const handleOnWheel = (event: WheelEvent) => {
event.preventDefault();
const handleZoom = (event: React.WheelEvent<HTMLCanvasElement>) => {
const scaleChange = event.deltaY > 0 ? -ZOOM_SPEED : ZOOM_SPEED;
const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9;
const newScale = Math.min(
MAX_SCALE,
Math.max(MIN_SCALE, scale * zoomFactor)
);
// current mouse position
const canvas = overlayCanvasRef.current!;
const rect = canvas.getBoundingClientRect();
const rect = canvasRef.current!.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
// 计算新的偏移量
offsetX = mouseX - (mouseX - offsetX) * (newScale / scale);
offsetY = mouseY - (mouseY - offsetY) * (newScale / scale);
setCanvasTransformOrigin(event);
// 更新缩放比例
scale = newScale;
updateZoom(scaleChange, mouseX, mouseY);
// 设置画布的变换
const overlayCtx = overlayCanvasRef.current!.getContext('2d')!;
overlayCtx.setTransform(scale, 0, 0, scale, offsetX, offsetY);
const canvasCtx = canvasRef.current!.getContext('2d')!;
canvasCtx.setTransform(scale, 0, 0, scale, offsetX, offsetY);
overlayCanvasRef.current!.style.transform = `scale(${scale})`;
canvasRef.current!.style.transform = `scale(${scale})`;
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
canvasRef.current!.style.transform = `scale(${autoScale.current})`;
};
console.log('Zoom:', scale, offsetX, offsetY);
const updateCursorSize = () => {
cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`;
cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`;
};
const handleOnWheel = (event: any) => {
handleZoom(event);
updateCursorSize();
};
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})`;
updateCursorSize();
redrawStrokes(strokesRef.current);
};
useEffect(() => {
initializeImage();
}, [initializeImage]);
// useEffect(() => {
// const container = containerRef.current;
// if (!container) return;
// if (container) {
// resizeObserver.current = new ResizeObserver(
// _.throttle(handleResize, 100)
// );
// resizeObserver.current.observe(container);
// }
// return () => {
// resizeObserver.current?.disconnect();
// };
// }, [handleResize, containerRef.current]);
useEffect(() => {
createOffscreenCanvas();
const handleUndoShortcut = (e: KeyboardEvent) => {
@ -569,6 +567,9 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
window.addEventListener('keydown', handleUndoShortcut);
return () => {
window.removeEventListener('keydown', handleUndoShortcut);
if (animationFrameIdRef.current !== null) {
cancelAnimationFrame(animationFrameIdRef.current);
}
};
}, []);
@ -581,7 +582,12 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
}, [disabled]);
return (
<div className="editor-wrapper">
<div
className="editor-wrapper"
style={{
border: '1px solid #ddd'
}}
>
<div className="flex-between">
<div className="tools">
<Tooltip
@ -636,6 +642,18 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
<SyncOutlined className="font-size-14" />
</Button>
</Tooltip>
<Tooltip
title={intl.formatMessage({ id: 'playground.image.fitview' })}
>
<Button
onClick={handleFitView}
size="middle"
type="text"
disabled={disabled}
>
<ExpandOutlined className="font-size-14" />
</Button>
</Tooltip>
</div>
<div className="tools">
<Tooltip
@ -671,6 +689,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
onMouseDown={startDrawing}
onMouseUp={endDrawing}
onMouseEnter={handleMouseEnter}
onWheel={handleOnWheel}
onMouseMove={(e) => {
handleMouseMove(e);
draw(e);
@ -690,7 +709,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
backgroundColor: COLOR,
borderRadius: '50%',
pointerEvents: 'none',
zIndex: 3
zIndex: 100
}}
/>
</div>

@ -136,5 +136,6 @@ export default {
'playground.image.brushSize': 'Brush Size',
'playground.image.download': 'Download Image',
'playground.image.generate': 'Generate',
'playground.image.edit': 'Edit'
'playground.image.edit': 'Edit',
'playground.image.fitview': 'Fit View'
};

@ -131,5 +131,6 @@ export default {
'playground.image.brushSize': '画笔大小',
'playground.image.download': '下载图片',
'playground.image.generate': '生成图片',
'playground.image.edit': '编辑图片'
'playground.image.edit': '编辑图片',
'playground.image.fitview': '适应视图'
};

@ -215,10 +215,12 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
}, [parameters.n]);
const imageFile = useMemo(() => {
if (!image) return null;
return base64ToFile(image, 'image');
}, [image]);
const maskFile = useMemo(() => {
if (!mask) return null;
return base64ToFile(mask, 'mask');
}, [mask]);
@ -664,23 +666,25 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
);
}
return (
<UploadImg
accept="image/png"
drag={true}
multiple={false}
handleUpdateImgList={handleUpdateImageList}
>
<div
className="flex-column flex-center gap-10 justify-center"
style={{ width: 150, height: 150 }}
<>
<UploadImg
accept="image/png"
drag={true}
multiple={false}
handleUpdateImgList={handleUpdateImageList}
>
<IconFont
type="icon-upload_image"
className="font-size-24"
></IconFont>
<h3>{intl.formatMessage({ id: 'playground.image.edit.tips' })}</h3>
</div>
</UploadImg>
<div
className="flex-column flex-center gap-10 justify-center"
style={{ width: 150, height: 150 }}
>
<IconFont
type="icon-upload_image"
className="font-size-24"
></IconFont>
<h3>{intl.formatMessage({ id: 'playground.image.edit.tips' })}</h3>
</div>
</UploadImg>
</>
);
}, [image, loading, imageStatus, handleOnSave, handleUpdateImageList]);
@ -911,7 +915,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
</span>
}
loading={loading}
disabled={!parameters.model || mask === ''}
disabled={!parameters.model}
isEmpty={!imageList.length}
handleSubmit={handleSendMessage}
handleAbortFetch={handleStopConversation}

Loading…
Cancel
Save