Feat: 캔버스에 사각형 그리기 기능 추가 - S11P21S002-135

This commit is contained in:
jhynsoo 2024-09-05 09:30:54 +09:00
parent 7ccbde93b0
commit 65d494a15a
12 changed files with 148 additions and 40 deletions

View File

@ -1,9 +1,13 @@
import useCanvasStore from '@/stores/useCanvasStore';
import { LucideIcon, MousePointer2, PenTool, Square } from 'lucide-react';
import { cn } from '@/lib/utils';
export default function CanvasControlBar() {
const drawState = useCanvasStore((state) => state.drawState);
const setDrawState = useCanvasStore((state) => state.setDrawState);
const buttonBaseClassName = 'rounded-lg p-2 transition-colors ';
const buttonClassName = 'hover:bg-gray-100';
const activeButtonClassName = 'bg-primary stroke-white';
const controls: { [key: string]: LucideIcon } = {
pointer: MousePointer2,
rect: Square,
@ -17,9 +21,13 @@ export default function CanvasControlBar() {
return (
<button
key={control}
className={cn(buttonClassName, buttonBaseClassName)}
className={cn(drawState === control ? activeButtonClassName : buttonClassName, buttonBaseClassName)}
onClick={() => setDrawState(control as typeof drawState)}
>
<Icon size={20} />
<Icon
size={20}
color={drawState === control ? 'white' : 'black'}
/>
</button>
);
})}

View File

@ -1,3 +1,4 @@
import '@/index.css';
import type { Meta, StoryObj } from '@storybook/react';
import Footer from './index';

View File

@ -1,3 +1,4 @@
import '@/index.css';
import type { Meta, StoryObj } from '@storybook/react';
import Header from './index';

View File

@ -1,3 +1,4 @@
import '@/index.css';
import { Meta } from '@storybook/react';
import ImageCanvas from '.';

View File

@ -1,12 +1,13 @@
import useCanvasStore from '@/stores/useCanvasStore';
import Konva from 'konva';
import { useEffect, useRef, useState } from 'react';
import { Image, Layer, Stage } from 'react-konva';
import { Image, Layer, Rect, Stage } from 'react-konva';
import useImage from 'use-image';
import { Label } from '@/types';
import LabelRect from './LabelRect';
import { Vector2d } from 'konva/lib/types';
import LabelPolygon from './LabelPolygon';
import CanvasControlBar from '../CanvasControlBar';
const mockLabels: Label[] = Array.from({ length: 10 }, (_, i) => {
const startX = Math.random() * 1200 + 300;
@ -45,11 +46,59 @@ export default function ImageCanvas() {
const labels = useCanvasStore((state) => state.labels) ?? [];
const [selectedId, setSelectedId] = useState<number | null>(null);
const [image, imageStatus] = useImage(imageUrl);
const [rectPoints, setRectPoints] = useState<[number, number][]>([]);
const drawState = useCanvasStore((state) => state.drawState);
const addLabel = useCanvasStore((state) => state.addLabel);
const startDrawRect = () => {
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
setRectPoints([
[x, y],
[x, y],
]);
};
const updateDrawingRect = () => {
if (rectPoints.length === 0) return;
const { x, y } = stageRef.current!.getRelativePointerPosition()!;
setRectPoints([rectPoints[0], [x, y]]);
};
const endDrawRect = () => {
if (drawState !== 'rect' || rectPoints.length === 0) return;
if (rectPoints[0][0] === rectPoints[1][0] && rectPoints[0][1] === rectPoints[1][1]) {
setRectPoints([]);
return;
}
setRectPoints([]);
const color = Math.floor(Math.random() * 65535)
.toString(16)
.padStart(4, '0');
addLabel({
id: labels.length,
name: 'label',
type: 'rect',
color: `#${color}ff`,
coordinates: rectPoints,
});
};
const handleClick = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
const isLeftMouseClicked = e.evt.type === 'mousedown' && (e.evt as MouseEvent).button === 0;
if (drawState !== 'pointer' && isLeftMouseClicked) {
stageRef.current?.stopDrag();
if (drawState === 'rect') {
startDrawRect();
}
return;
}
if (e.target === e.target.getStage() || e.target.getClassName() === 'Image') {
setSelectedId(null);
}
};
const handleMouseMove = () => {
if (drawState === 'rect' && rectPoints.length) {
updateDrawingRect();
}
};
const handleZoom = (e: Konva.KonvaEventObject<WheelEvent>) => {
const scaleBy = 1.05;
const oldScale = scale.current;
@ -98,45 +147,77 @@ export default function ImageCanvas() {
useCanvasStore.setState({ labels: mockLabels });
}, []);
useEffect(() => {
if (!stageRef.current) return;
stageRef.current.container().style.cursor = drawState === 'pointer' ? 'default' : 'crosshair';
if (drawState !== 'pointer') {
setSelectedId(null);
}
}, [drawState]);
return imageStatus === 'loaded' ? (
<Stage
ref={stageRef}
width={stageWidth}
height={stageHeight}
className="overflow-hidden bg-gray-200"
draggable
onWheel={handleWheel}
onMouseDown={handleClick}
onTouchStart={handleClick}
scale={getScale()}
>
<Layer>
<Image image={image} />
</Layer>
<Layer>
{labels.map((label) =>
label.type === 'rect' ? (
<LabelRect
key={label.id}
isSelected={label.id === selectedId}
onSelect={() => setSelectedId(label.id)}
info={label}
dragLayer={dragLayerRef.current as Konva.Layer}
<div>
<Stage
ref={stageRef}
width={stageWidth}
height={stageHeight}
className="overflow-hidden bg-gray-200"
draggable
onWheel={handleWheel}
onMouseDown={handleClick}
onTouchStart={handleClick}
onMouseMove={handleMouseMove}
onTouchMove={handleMouseMove}
onMouseUp={endDrawRect}
onTouchEnd={endDrawRect}
scale={getScale()}
>
<Layer>
<Image image={image} />
</Layer>
<Layer listening={drawState === 'pointer'}>
{labels.map((label) =>
label.type === 'rect' ? (
<LabelRect
key={label.id}
isSelected={label.id === selectedId}
onSelect={() => setSelectedId(label.id)}
info={label}
dragLayer={dragLayerRef.current as Konva.Layer}
/>
) : (
<LabelPolygon
key={label.id}
isSelected={label.id === selectedId}
onSelect={() => setSelectedId(label.id)}
info={label}
stage={stageRef.current as Konva.Stage}
dragLayer={dragLayerRef.current as Konva.Layer}
/>
)
)}
{rectPoints.length ? (
<Rect
x={rectPoints[0][0]}
y={rectPoints[0][1]}
width={rectPoints[1][0] - rectPoints[0][0]}
height={rectPoints[1][1] - rectPoints[0][1]}
stroke={'#ff0000'}
strokeWidth={1}
strokeScaleEnabled={false}
fillAfterStrokeEnabled={false}
fill="#ff000033"
shadowForStrokeEnabled={false}
listening={false}
/>
) : (
<LabelPolygon
key={label.id}
isSelected={label.id === selectedId}
onSelect={() => setSelectedId(label.id)}
info={label}
stage={stageRef.current as Konva.Stage}
dragLayer={dragLayerRef.current as Konva.Layer}
/>
)
)}
</Layer>
<Layer ref={dragLayerRef} />
</Stage>
) : null}
</Layer>
<Layer ref={dragLayerRef} />
</Stage>
<CanvasControlBar />
</div>
) : (
<div></div>
);

View File

@ -1,3 +1,4 @@
import '@/index.css';
import { useState } from 'react';
import ImageUploadModal from './index';

View File

@ -1,3 +1,4 @@
import '@/index.css';
import MemberAddModal from '.';
export default {

View File

@ -1,3 +1,4 @@
import '@/index.css';
import { Meta, StoryObj } from '@storybook/react';
import ProjectCard from '.';

View File

@ -1,3 +1,4 @@
import '@/index.css';
import type { Meta, StoryObj } from '@storybook/react';
import ReviewRequestItem from './ReviewRequestItem';

View File

@ -1,3 +1,4 @@
import '@/index.css';
import type { Meta, StoryObj } from '@storybook/react';
import ReviewRequest from '.';

View File

@ -1,3 +1,4 @@
import '@/index.css';
import { Meta } from '@storybook/react';
import WorkspaceLabelBar from '.';
import { Label } from '@/types';
@ -17,16 +18,22 @@ const labels: Label[] = [
id: 1,
name: 'Label 1',
color: '#FFaa33',
type: 'rect',
coordinates: [],
},
{
id: 2,
name: 'Label 2',
color: '#aaFF55',
type: 'rect',
coordinates: [],
},
{
id: 3,
name: 'Label 3',
color: '#77aaFF',
type: 'rect',
coordinates: [],
},
];

View File

@ -4,19 +4,23 @@ import { create } from 'zustand';
interface CanvasState {
image: string;
labels: Label[];
drawState: 'pen' | 'rect' | 'pointer';
changeImage: (image: string, labels: Label[]) => void;
addLabel: (label: Label) => void;
removeLabel: (labelId: number) => void;
updateLabel: (label: Label) => void;
setDrawState: (state: 'pen' | 'rect' | 'pointer') => void;
}
const useCanvasStore = create<CanvasState>()((set) => ({
image: '',
labels: [],
drawState: 'pointer',
changeImage: (image: string, labels: Label[]) => set({ image, labels }),
addLabel: (label: Label) => set((state) => ({ labels: [...state.labels, label] })),
removeLabel: (labelId: number) => set((state) => ({ labels: state.labels.filter((label) => label.id !== labelId) })),
updateLabel: (label: Label) => set((state) => ({ labels: state.labels.map((l) => (l.id === label.id ? label : l)) })),
setDrawState: (drawState) => set({ drawState }),
}));
export default useCanvasStore;