Feat: 캔버스에 사각형 그리기 기능 추가 - S11P21S002-135
This commit is contained in:
parent
7ccbde93b0
commit
65d494a15a
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import '@/index.css';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Footer from './index';
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import '@/index.css';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Header from './index';
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import '@/index.css';
|
||||
import { Meta } from '@storybook/react';
|
||||
import ImageCanvas from '.';
|
||||
|
||||
|
@ -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,7 +147,17 @@ 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' ? (
|
||||
<div>
|
||||
<Stage
|
||||
ref={stageRef}
|
||||
width={stageWidth}
|
||||
@ -108,12 +167,16 @@ export default function ImageCanvas() {
|
||||
onWheel={handleWheel}
|
||||
onMouseDown={handleClick}
|
||||
onTouchStart={handleClick}
|
||||
onMouseMove={handleMouseMove}
|
||||
onTouchMove={handleMouseMove}
|
||||
onMouseUp={endDrawRect}
|
||||
onTouchEnd={endDrawRect}
|
||||
scale={getScale()}
|
||||
>
|
||||
<Layer>
|
||||
<Image image={image} />
|
||||
</Layer>
|
||||
<Layer>
|
||||
<Layer listening={drawState === 'pointer'}>
|
||||
{labels.map((label) =>
|
||||
label.type === 'rect' ? (
|
||||
<LabelRect
|
||||
@ -134,9 +197,27 @@ export default function ImageCanvas() {
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{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}
|
||||
/>
|
||||
) : null}
|
||||
</Layer>
|
||||
|
||||
<Layer ref={dragLayerRef} />
|
||||
</Stage>
|
||||
<CanvasControlBar />
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import '@/index.css';
|
||||
import { useState } from 'react';
|
||||
import ImageUploadModal from './index';
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import '@/index.css';
|
||||
import MemberAddModal from '.';
|
||||
|
||||
export default {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import '@/index.css';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import ProjectCard from '.';
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import '@/index.css';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import ReviewRequestItem from './ReviewRequestItem';
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import '@/index.css';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import ReviewRequest from '.';
|
||||
|
||||
|
@ -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: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user