Merge branch 'fe/feat/102-canvas-polygon' into 'fe/develop'

Feat: 캔버스 폴리곤 이동 기능 구현 - S11P21S002-102

See merge request s11-s-project/S11P21S002!34
This commit is contained in:
홍창기 2024-09-02 16:54:41 +09:00
commit 9a67b5ace1
4 changed files with 159 additions and 63 deletions

View File

@ -0,0 +1,41 @@
import { Label } from '@/types';
import Konva from 'konva';
import { useRef, useState } from 'react';
import { Group, Line } from 'react-konva';
import PolygonTransformer from './PolygonTransformer';
export default function LabelPolygon({
isSelected,
onSelect,
info,
}: {
isSelected: boolean;
onSelect: () => void;
info: Label;
}) {
const polyRef = useRef<Konva.Line>(null);
const [coordinates, setCoordinates] = useState<Array<[number, number]>>(info.coordinates);
return (
<Group zIndex={isSelected ? 9999 : 1}>
<Line
points={coordinates.flat()}
stroke={info.color}
strokeWidth={1}
ref={polyRef}
onMouseDown={onSelect}
onTouchStart={onSelect}
strokeScaleEnabled={false}
fillAfterStrokeEnabled={false}
fill={`${info.color}33`}
closed
/>
{isSelected && (
<PolygonTransformer
coordinates={coordinates}
setCoordinates={setCoordinates}
/>
)}
</Group>
);
}

View File

@ -1,7 +1,7 @@
import { Label } from '@/types'; import { Label } from '@/types';
import Konva from 'konva'; import Konva from 'konva';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { Line, Transformer } from 'react-konva'; import { Group, Line, Transformer } from 'react-konva';
export default function LabelRect({ export default function LabelRect({
isSelected, isSelected,
@ -14,12 +14,16 @@ export default function LabelRect({
}) { }) {
const rectRef = useRef<Konva.Line>(null); const rectRef = useRef<Konva.Line>(null);
const trRef = useRef<Konva.Transformer>(null); const trRef = useRef<Konva.Transformer>(null);
const coordinates = [
info.coordinates[0],
[info.coordinates[0][0], info.coordinates[1][1]],
info.coordinates[1],
[info.coordinates[1][0], info.coordinates[0][1]],
].flat();
const handleTransformEnd = () => { const handleTransformEnd = () => {
const points = rectRef.current?.points(); const points = rectRef.current?.points();
if (points) {
console.log(points); console.log(points);
console.log(trRef.current?.getAbsoluteScale());
}
}; };
useEffect(() => { useEffect(() => {
@ -30,9 +34,9 @@ export default function LabelRect({
}, [isSelected]); }, [isSelected]);
return ( return (
<> <Group zIndex={isSelected ? 9999 : 1}>
<Line <Line
points={info.coordinates.flat()} points={coordinates}
stroke={info.color} stroke={info.color}
strokeWidth={1} strokeWidth={1}
ref={rectRef} ref={rectRef}
@ -57,6 +61,6 @@ export default function LabelRect({
onTransformEnd={handleTransformEnd} onTransformEnd={handleTransformEnd}
/> />
)} )}
</> </Group>
); );
} }

View File

@ -0,0 +1,64 @@
import Konva from 'konva';
import { Circle, Line } from 'react-konva';
interface PolygonTransformerProps {
coordinates: Array<[number, number]>;
setCoordinates: (coordinates: Array<[number, number]>) => void;
}
// TODO: scale 상관 없이 고정된 크기로 표시되도록 수정
export default function PolygonTransformer({ coordinates, setCoordinates }: PolygonTransformerProps) {
const handleDragMove = (index: number) => (e: Konva.KonvaEventObject<DragEvent>) => {
const circle = e.target as Konva.Circle;
const stage = circle.getStage();
const pos = circle.position();
const newCoordinates = [...coordinates];
newCoordinates[index] = [pos.x, pos.y];
setCoordinates(newCoordinates);
stage?.batchDraw();
};
const handleMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
const circle = e.target as Konva.Circle;
const stage = circle.getStage();
circle.strokeWidth(2);
circle.radius(15);
stage?.batchDraw();
};
const handleMouseOut = (e: Konva.KonvaEventObject<MouseEvent>) => {
const circle = e.target as Konva.Circle;
const stage = circle.getStage();
circle.strokeWidth(1);
circle.radius(10);
stage?.batchDraw();
};
return (
<>
<Line
points={coordinates.flat()}
stroke="#00a1ff"
strokeWidth={2}
closed
zIndex={1}
/>
{coordinates.map((point, index) => {
return (
<Circle
key={index}
x={point[0]}
y={point[1]}
radius={10}
stroke="#00a1ff"
strokeWidth={1}
fill="white"
draggable
onDragMove={handleDragMove(index)}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
/>
);
})}
</>
);
}

View File

@ -6,51 +6,34 @@ import useImage from 'use-image';
import { Label } from '@/types'; import { Label } from '@/types';
import LabelRect from './LabelRect'; import LabelRect from './LabelRect';
import { Vector2d } from 'konva/lib/types'; import { Vector2d } from 'konva/lib/types';
import LabelPolygon from './LabelPolygon';
const mockLabels: Label[] = [ const mockLabels: Label[] = Array.from({ length: 10 }, (_, i) => {
{ const startX = Math.random() * 1200 + 300;
id: 1, const startY = Math.random() * 2000 + 300;
name: 'Label 1', const color = Math.floor(Math.random() * 65535)
color: '#FFaa33', .toString(16)
type: 'rect', .padStart(4, '0');
coordinates: [
[100, 100], return {
[200, 100], id: i,
[200, 200], name: `label-${i}`,
[100, 200], type: i % 2 === 0 ? 'polygon' : 'rect',
color: i % 2 === 0 ? `#ff${color}` : `#${color}ff`,
coordinates:
i % 2 === 0
? [
[startX, startY],
[startX + 200, startY + 50],
[startX + 300, startY + 300],
[startX + 100, startY + 250],
]
: [
[startX, startY],
[startX + 300, startY + 300],
], ],
}, };
{ });
id: 2,
name: 'Label 3',
color: '#aa33ff',
type: 'rect',
coordinates: [
[1200, 200],
[1200, 400],
[1400, 400],
[1400, 200],
],
},
{
id: 3,
name: 'Label 3',
color: '#aaff33',
type: 'polygon',
coordinates: [
[500, 375],
[523, 232],
[600, 232],
[535, 175],
[560, 100],
[500, 150],
[440, 100],
[465, 175],
[400, 232],
[477, 232],
],
},
];
export default function ImageCanvas() { export default function ImageCanvas() {
const stageWidth = window.innerWidth; const stageWidth = window.innerWidth;
@ -106,8 +89,6 @@ export default function ImageCanvas() {
const heightRatio = stageHeight / image!.height; const heightRatio = stageHeight / image!.height;
scale.current = Math.min(widthRatio, heightRatio); scale.current = Math.min(widthRatio, heightRatio);
console.log(scale);
return { x: scale.current, y: scale.current }; return { x: scale.current, y: scale.current };
}; };
@ -118,11 +99,11 @@ export default function ImageCanvas() {
return imageStatus === 'loaded' ? ( return imageStatus === 'loaded' ? (
<Stage <Stage
className="overflow-hidden bg-gray-200" ref={stageRef}
width={stageWidth} width={stageWidth}
height={stageHeight} height={stageHeight}
className="overflow-hidden bg-gray-200"
draggable draggable
ref={stageRef}
onWheel={handleWheel} onWheel={handleWheel}
onMouseDown={handleClick} onMouseDown={handleClick}
onTouchStart={handleClick} onTouchStart={handleClick}
@ -131,19 +112,25 @@ export default function ImageCanvas() {
<Layer> <Layer>
<Image image={image} /> <Image image={image} />
</Layer> </Layer>
{labels.map((label) => ( <Layer>
<Layer key={label.id}> {labels.map((label) =>
{label.type === 'rect' ? ( label.type === 'rect' ? (
<LabelRect <LabelRect
key={label.id}
isSelected={label.id === selectedId} isSelected={label.id === selectedId}
onSelect={() => setSelectedId(label.id)} onSelect={() => setSelectedId(label.id)}
info={label} info={label}
/> />
) : ( ) : (
<></> <LabelPolygon
key={label.id}
isSelected={label.id === selectedId}
onSelect={() => setSelectedId(label.id)}
info={label}
/>
)
)} )}
</Layer> </Layer>
))}
</Stage> </Stage>
) : ( ) : (
<div></div> <div></div>