I am trying to build a whiteboard app using canvas in react. so far I am able to create line and rectangle and move them on selection what I am aiming next is to resize shapes which will includes oval also. I think if I can draw a surrounding rectangle and know its coordinates then I'll check mouse is near to which corner or side and resize the shape inside accordingly but so far no good solution to the problem.
If anybody has a better approach please suggest.
My resize approach is similar to this site excalidraw
here is the code I have so far :
import React, { useRef, useState, useEffect } from 'react';
export interface SketchProps {
}
interface Point {
x: number,
y: number
}
interface IElement {
id: number;
X1: number;
Y1: number;
X2: number;
Y2: number;
type: string;
}
interface ISelectedData {
selectedElement: IElement;
selectedStartingPoint: Point;
}
const Sketch: React.SFC<SketchProps> = () => {
const tools = {
Line: 'line',
Rectangle: 'rectangle',
Selection: 'selection'
}
const actions = {
Drawing: 'drawing',
Moving: 'moving'
}
const [currentAction, setCurrentAction] = useState<string>('');
const [currentTool, setCurrentTool] = useState<string>(tools.Line);
const [selectedData, setSelectedData] = useState<ISelectedData | null>(null);
const canvasRef: any = useRef(null);
const contextRef: any = useRef(null);
const [elementArray, setElementArray] = useState<IElement[]>([]);
const createElement = (x1: number, y1: number, x2: number, y2: number) => {
return {
id: elementArray.length,
X1: x1,
Y1: y1,
X2: x2,
Y2: y2,
type: currentTool
}
}
const updateElement = (id: number, x1: number, y1: number, x2: number, y2: number, type: string) => {
let currentElement = { ...elementArray[id] };
currentElement.X1 = x1;
currentElement.Y1 = y1;
currentElement.X2 = x2;
currentElement.Y2 = y2;
let elementArrayClone = [...elementArray];
elementArrayClone[id] = currentElement;
setElementArray(elementArrayClone);
}
useEffect(() => {
let canvas: any = canvasRef.current
if (canvas !== null || canvas !== undefined) {
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
let context: any = canvas.getContext('2d');
if (context !== null || context !== undefined) {
context.strokeStyle = 'black';
context.lineWidth = 5;
contextRef.current = context;
}
}
}, [])
const clearCanvas = () => {
contextRef.current.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
}
const drawLine = (element: IElement) => {
contextRef.current.beginPath();
contextRef.current.moveTo(element.X1, element.Y1);
contextRef.current.lineTo(element.X2, element.Y2);
contextRef.current.stroke();
contextRef.current.closePath();
}
const drawRectangle = (element: IElement) => {
contextRef.current.beginPath();
contextRef.current.rect(element.X1, element.Y1, element.X2 - element.X1, element.Y2 - element.Y1);
contextRef.current.stroke();
contextRef.current.closePath();
}
const distance = (A: Point, B: Point) => {
return Math.sqrt(Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2))
}
const checkRectangle = (x: number, y: number, element: IElement) => {
let minX = Math.min(element.X1, element.X2);
let maxX = Math.max(element.X1, element.X2);
let minY = Math.min(element.Y1, element.Y2);
let maxY = Math.max(element.Y1, element.Y2);
return x >= minX && x <= maxX && y >= minY && y <= maxY;
}
const checkLine = (x1: number, y1: number, element: IElement) => {
const a: Point = { x: element.X1, y: element.Y1 };
const b: Point = { x: element.X2, y: element.Y2 };
const c: Point = { x: x1, y: y1 }
let offset = distance(a, b) - (distance(a, c) + distance(c, b));
console.log('offset', offset);
return Math.abs(offset) < 1;
}
const getElementAtPosition = (x: number, y: number) => {
for (let i = 0; i < elementArray.length; i++) {
if (elementArray[i].type === tools.Line) {
if (checkLine(x, y, elementArray[i]))
return elementArray[i];
}
else if (elementArray[i].type === tools.Rectangle) {
if (checkRectangle(x, y, elementArray[i]))
return elementArray[i];
}
}
}
useEffect(() => {
console.log('new render');
clearCanvas();
console.log('element array ', elementArray);
elementArray.map((element) => {
switch (element.type) {
case tools.Line:
drawLine(element);
break;
case tools.Rectangle:
drawRectangle(element);
break;
default:
drawLine(element);
break;
}
})
}, [elementArray])
const handleMouseDown = ({ nativeEvent }: any) => {
const { offsetX, offsetY } = nativeEvent;
if (currentTool === tools.Selection) {
let el = getElementAtPosition(offsetX, offsetY);
console.log('el', el);
if (el) {
setSelectedData({
selectedElement: el,
selectedStartingPoint: { x: offsetX, y: offsetY }
});
setCurrentAction(actions.Moving);
}
}
else {
setCurrentAction(actions.Drawing);
const element = createElement(offsetX, offsetY, offsetX, offsetY);
setElementArray((prev) => {
return (
[...prev, element]
)
});
}
}
const handleMouseMove = ({ nativeEvent }: any) => {
const { offsetX, offsetY } = nativeEvent;
if (currentAction === actions.Drawing) {
let index = elementArray.length - 1;
let currentElement = { ...elementArray[index] };
updateElement(index, currentElement.X1, currentElement.Y1, offsetX, offsetY, currentElement.type);
}
else if (currentAction === actions.Moving) {
console.log('moving');
if (selectedData !== null && selectedData !== undefined) {
let sElement = selectedData?.selectedElement
let sStartingPoint = selectedData?.selectedStartingPoint
let newX1 = offsetX - (sStartingPoint.x - sElement?.X1)
let newX2 = offsetX - (sStartingPoint.x - sElement?.X2)
let newY1 = offsetY - (sStartingPoint.y - sElement?.Y1)
let newY2 = offsetY - (sStartingPoint.y - sElement?.Y2)
updateElement(sElement.id, newX1, newY1, newX2, newY2, sElement.type);
}
}
}
const handleMouseUp = () => {
setCurrentAction('');
setSelectedData(null);
}
return (
<div>
<canvas
ref={canvasRef}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
>
Canvas
</canvas>
<button onClick={() => {
setCurrentTool(tools.Selection);
}}>select</button>
<button onClick={() => {
setElementArray([]);
clearCanvas()
}}>reset</button>
<button onClick={() => {
setCurrentTool(tools.Line);
}}>line</button>
<button onClick={() => {
setCurrentTool(tools.Rectangle);
}}>rectangle</button>
</div>
);
}
export default Sketch;
question from:
https://stackoverflow.com/questions/65879614/resizing-shapes-in-a-canvas-via-drawing-a-selection-rectangle-around-a-shape