@deepnoid/canvas 0.1.85 → 0.1.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/package.json +3 -2
- package/skills/deepnoid-canvas/DEPLOY.md +57 -0
- package/skills/deepnoid-canvas/README.md +56 -0
- package/skills/deepnoid-canvas/SKILL.md +131 -0
- package/skills/deepnoid-canvas/references/apply-style.md +189 -0
- package/skills/deepnoid-canvas/references/components.md +197 -0
- package/skills/deepnoid-canvas/references/types.md +231 -0
- package/skills/deepnoid-canvas/rules/annotations.md +152 -0
- package/skills/deepnoid-canvas/rules/events.md +124 -0
- package/skills/deepnoid-canvas/rules/layout.md +171 -0
- package/skills/deepnoid-canvas/rules/rendering.md +142 -0
- package/skills/deepnoid-canvas/setup.md +147 -0
package/README.md
CHANGED
|
@@ -31,9 +31,23 @@ Canvas 기반 이미지 annotation 라이브러리로, **TypeScript Engine + Rea
|
|
|
31
31
|
### 설치
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npm install deepnoid
|
|
34
|
+
npm install @deepnoid/canvas
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
### Claude Code 스킬 설치 (선택)
|
|
38
|
+
|
|
39
|
+
소비자 프로젝트에서 Claude Code가 `@deepnoid/canvas` API를 정확하게 사용할 수 있도록 스킬을 복사합니다.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# .claude/skills 디렉토리가 없으면 생성
|
|
43
|
+
mkdir -p .claude/skills
|
|
44
|
+
|
|
45
|
+
# 스킬 복사
|
|
46
|
+
cp -r node_modules/@deepnoid/canvas/skills/deepnoid-canvas .claude/skills/
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
복사 후 Claude Code에서 `@deepnoid/canvas`를 import하면 자동으로 올바른 사용법, Props, applyStyle 작성법 등을 안내합니다.
|
|
50
|
+
|
|
37
51
|
### 기본 사용법
|
|
38
52
|
|
|
39
53
|
```tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepnoid/canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.86",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"skills"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"build": "npx tsc -p tsconfig.json",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# npm 배포 시 스킬 포함 방법
|
|
2
|
+
|
|
3
|
+
## 1. package.json 설정
|
|
4
|
+
|
|
5
|
+
`package.json`의 `files` 필드에 `skills`를 추가하여 npm 배포 시 스킬이 포함되도록 합니다.
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"name": "@deepnoid/canvas",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"skills"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 2. 배포
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 빌드
|
|
21
|
+
yarn build
|
|
22
|
+
|
|
23
|
+
# 버전 업
|
|
24
|
+
npm version patch # 또는 minor, major
|
|
25
|
+
|
|
26
|
+
# 배포
|
|
27
|
+
npm publish
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 3. 소비자 프로젝트에서 설치
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 패키지 설치
|
|
34
|
+
yarn add @deepnoid/canvas
|
|
35
|
+
|
|
36
|
+
# 스킬 복사 (최초 1회 또는 업데이트 시)
|
|
37
|
+
cp -r node_modules/@deepnoid/canvas/skills/deepnoid-canvas .claude/skills/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 4. 확인
|
|
41
|
+
|
|
42
|
+
배포 후 패키지에 스킬이 포함되었는지 확인:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 패키지 내용 확인
|
|
46
|
+
npm pack --dry-run
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
출력에 `skills/deepnoid-canvas/SKILL.md` 등이 포함되어야 합니다.
|
|
50
|
+
|
|
51
|
+
## 5. 스킬 업데이트 시
|
|
52
|
+
|
|
53
|
+
라이브러리 API가 변경되면 스킬도 함께 업데이트해야 합니다:
|
|
54
|
+
|
|
55
|
+
1. `skills/deepnoid-canvas/` 내 관련 파일 수정
|
|
56
|
+
2. 패키지 버전 업 후 배포
|
|
57
|
+
3. 소비자 프로젝트에서 `cp -r` 명령으로 스킬 재복사
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @deepnoid/canvas Skill
|
|
2
|
+
|
|
3
|
+
`@deepnoid/canvas` 라이브러리의 Claude Code용 지식 패키지. 소비자 프로젝트에서 Claude Code가 Canvas 어노테이션 API를 정확하게 사용할 수 있도록 합니다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. 패키지 설치
|
|
9
|
+
yarn add @deepnoid/canvas
|
|
10
|
+
|
|
11
|
+
# 2. 스킬 복사
|
|
12
|
+
cp -r node_modules/@deepnoid/canvas/skills/deepnoid-canvas .claude/skills/
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 기능
|
|
16
|
+
|
|
17
|
+
- **SKILL.md**: 핵심 원칙, 컴포넌트/모드 선택 가이드, 기본 사용 패턴
|
|
18
|
+
- **rules/**: 올바른 사용법 vs 잘못된 사용법 코드 예시
|
|
19
|
+
- `rendering.md` — applyStyle 렌더링 규칙
|
|
20
|
+
- `annotations.md` — 어노테이션 데이터 구조 규칙
|
|
21
|
+
- `events.md` — 이벤트 핸들러, 단축키 규칙
|
|
22
|
+
- `layout.md` — 캔버스 레이아웃, 줌/팬 규칙
|
|
23
|
+
- **references/**: 완전한 API 레퍼런스
|
|
24
|
+
- `components.md` — AnnotationEditor, AnnotationViewer Props
|
|
25
|
+
- `types.md` — Annotation, DrawMode, Label 등 전체 타입
|
|
26
|
+
- `apply-style.md` — applyStyle 함수 상세 가이드 + 3가지 실전 예제
|
|
27
|
+
- **setup.md**: 소비자 프로젝트 초기 설정 가이드
|
|
28
|
+
- **DEPLOY.md**: npm 배포 시 스킬 포함 방법
|
|
29
|
+
|
|
30
|
+
## 파일 구조
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
skills/deepnoid-canvas/
|
|
34
|
+
├── SKILL.md ← 핵심 진입점 (트리거 조건 포함)
|
|
35
|
+
├── README.md ← 이 파일
|
|
36
|
+
├── DEPLOY.md ← npm 배포 방식 안내
|
|
37
|
+
├── setup.md ← 소비자 프로젝트 설정 가이드
|
|
38
|
+
├── rules/
|
|
39
|
+
│ ├── rendering.md ← applyStyle 렌더링 규칙
|
|
40
|
+
│ ├── annotations.md ← 어노테이션 데이터 규칙
|
|
41
|
+
│ ├── events.md ← 이벤트/단축키 규칙
|
|
42
|
+
│ └── layout.md ← 레이아웃/줌/팬 규칙
|
|
43
|
+
└── references/
|
|
44
|
+
├── components.md ← 컴포넌트 Props 레퍼런스
|
|
45
|
+
├── types.md ← 타입 레퍼런스
|
|
46
|
+
└── apply-style.md ← applyStyle 함수 레퍼런스
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 자동 트리거
|
|
50
|
+
|
|
51
|
+
이 스킬은 다음 상황에서 Claude Code에 의해 자동 활성화됩니다:
|
|
52
|
+
|
|
53
|
+
- import 경로에 `@deepnoid/canvas`가 포함될 때
|
|
54
|
+
- `AnnotationEditor`, `AnnotationViewer`, `DrawMode`, `ApplyAnnotationStyle` 키워드 사용 시
|
|
55
|
+
- Canvas 위 이미지 어노테이션(bounding box, polygon) 관련 질문 시
|
|
56
|
+
- `applyStyle` 콜백 함수를 작성하거나 수정할 때
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deepnoid-canvas
|
|
3
|
+
description: >
|
|
4
|
+
@deepnoid/canvas 라이브러리의 완전한 사용 가이드.
|
|
5
|
+
트리거 조건: import 경로에 '@deepnoid/canvas'가 포함될 때,
|
|
6
|
+
AnnotationEditor/AnnotationViewer/DrawMode/ApplyAnnotationStyle 키워드가 사용될 때,
|
|
7
|
+
Canvas 위 이미지 어노테이션(bounding box, polygon) 관련 질문이 있을 때,
|
|
8
|
+
applyStyle 콜백 함수를 작성하거나 수정할 때 자동 활성화.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# @deepnoid/canvas 스킬
|
|
12
|
+
|
|
13
|
+
Canvas 기반 이미지 어노테이션 React 라이브러리. 이미지 위에 사각형(BBox)/폴리곤 어노테이션을 그리고, 편집하고, 표시하는 기능을 제공합니다.
|
|
14
|
+
|
|
15
|
+
## 핵심 원칙
|
|
16
|
+
|
|
17
|
+
1. **applyStyle은 필수**: 모든 어노테이션의 시각적 표현은 소비자가 `applyStyle` 콜백으로 완전 제어합니다. 라이브러리는 스타일을 자체 적용하지 않습니다.
|
|
18
|
+
2. **컨테이너 크기 필수**: `AnnotationEditor`/`AnnotationViewer`를 감싸는 부모 `div`에 반드시 고정된 width/height (또는 flex: 1)를 설정해야 합니다.
|
|
19
|
+
3. **zoom 보정 필수**: `applyStyle` 내에서 `lineWidth`, `font size`, `setLineDash` 등의 값은 반드시 `/ zoom`으로 나누어야 줌 레벨에 관계없이 일정하게 보입니다.
|
|
20
|
+
4. **Editor vs Viewer 분리**: 편집이 필요하면 `AnnotationEditor`, 읽기 전용이면 `AnnotationViewer`를 사용합니다. 두 컴포넌트의 props가 다릅니다.
|
|
21
|
+
5. **좌표는 원본 이미지 기준**: 모든 어노테이션 좌표(x, y, width, height, points)는 원본 이미지의 픽셀 좌표입니다. 캔버스 표시 좌표와 다릅니다.
|
|
22
|
+
|
|
23
|
+
## Critical Rules
|
|
24
|
+
|
|
25
|
+
| 규칙 파일 | 언제 읽어야 하는지 |
|
|
26
|
+
|-----------|-------------------|
|
|
27
|
+
| [rules/rendering.md](rules/rendering.md) | applyStyle 함수를 작성하거나 수정할 때 |
|
|
28
|
+
| [rules/annotations.md](rules/annotations.md) | 어노테이션 데이터를 생성/변환하거나 상태를 관리할 때 |
|
|
29
|
+
| [rules/events.md](rules/events.md) | 이벤트 핸들러, 단축키, annotationSelected 연동 시 |
|
|
30
|
+
| [rules/layout.md](rules/layout.md) | 캔버스 컨테이너 레이아웃, 줌/팬, 리사이즈 관련 작업 시 |
|
|
31
|
+
|
|
32
|
+
## 컴포넌트 선택 가이드
|
|
33
|
+
|
|
34
|
+
| 상황 | 사용할 컴포넌트 | 설명 |
|
|
35
|
+
|------|----------------|------|
|
|
36
|
+
| 어노테이션 생성/편집 필요 | `AnnotationEditor` | drawing.mode로 그리기 모드 설정 |
|
|
37
|
+
| 읽기 전용 표시만 필요 | `AnnotationViewer` | mode 불필요, 편집 비활성 |
|
|
38
|
+
| ref로 캔버스 제어 필요 | 둘 다 지원 | `AnnotationCanvasHandle` ref 사용 |
|
|
39
|
+
|
|
40
|
+
## 그리기 모드 선택 가이드
|
|
41
|
+
|
|
42
|
+
| 상황 | DrawMode | 설명 |
|
|
43
|
+
|------|----------|------|
|
|
44
|
+
| 바운딩 박스 (사각형) | `DrawMode.RECTANGLE` | 드래그로 사각형 그리기 |
|
|
45
|
+
| 다각형 영역 | `DrawMode.POLYGON` | 클릭으로 점 찍기, 더블클릭/시작점 클릭으로 완성 |
|
|
46
|
+
|
|
47
|
+
## 핵심 사용 패턴
|
|
48
|
+
|
|
49
|
+
### 기본 Editor 사용
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { useState } from 'react';
|
|
53
|
+
import { AnnotationEditor, DrawMode } from '@deepnoid/canvas';
|
|
54
|
+
import type { Annotation, ApplyAnnotationStyle } from '@deepnoid/canvas';
|
|
55
|
+
|
|
56
|
+
const applyStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
|
|
57
|
+
if (variant === 'drawRect') {
|
|
58
|
+
context.strokeStyle = annotation.selected ? '#00FF00' : '#FF4136';
|
|
59
|
+
context.lineWidth = drawLineSize / zoom;
|
|
60
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
61
|
+
context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
|
|
62
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
63
|
+
context.beginPath();
|
|
64
|
+
annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
|
|
65
|
+
context.closePath();
|
|
66
|
+
context.stroke();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (variant === 'drawText') {
|
|
70
|
+
if (annotation.label) {
|
|
71
|
+
context.fillStyle = '#FF4136';
|
|
72
|
+
context.font = `bold ${14 / zoom}px sans-serif`;
|
|
73
|
+
const tx = annotation.type === DrawMode.RECTANGLE ? annotation.x : Math.min(...annotation.points.map(p => p.x));
|
|
74
|
+
const ty = annotation.type === DrawMode.RECTANGLE ? annotation.y - 5 : Math.min(...annotation.points.map(p => p.y)) - 5;
|
|
75
|
+
context.fillText(annotation.label.name, tx, ty);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function MyEditor() {
|
|
81
|
+
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div style={{ width: 800, height: 600 }}>
|
|
85
|
+
<AnnotationEditor
|
|
86
|
+
image="https://example.com/image.jpg"
|
|
87
|
+
annotations={annotations}
|
|
88
|
+
setAnnotations={setAnnotations}
|
|
89
|
+
drawing={{
|
|
90
|
+
mode: DrawMode.RECTANGLE,
|
|
91
|
+
color: '#FF4136',
|
|
92
|
+
lineSize: 2,
|
|
93
|
+
applyStyle,
|
|
94
|
+
}}
|
|
95
|
+
events={{}}
|
|
96
|
+
enableHotkeys
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 기본 Viewer 사용
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { AnnotationViewer } from '@deepnoid/canvas';
|
|
107
|
+
|
|
108
|
+
<div style={{ width: 800, height: 600 }}>
|
|
109
|
+
<AnnotationViewer
|
|
110
|
+
image="https://example.com/image.jpg"
|
|
111
|
+
annotations={annotations}
|
|
112
|
+
drawing={{ lineSize: 2, applyStyle }}
|
|
113
|
+
events={{}}
|
|
114
|
+
options={{ panZoomEnabled: true }}
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API 레퍼런스
|
|
120
|
+
|
|
121
|
+
상세 API 문서는 [references/](references/) 디렉토리를 참조하세요.
|
|
122
|
+
|
|
123
|
+
| 레퍼런스 파일 | 내용 |
|
|
124
|
+
|--------------|------|
|
|
125
|
+
| [references/components.md](references/components.md) | AnnotationEditor, AnnotationViewer 전체 Props |
|
|
126
|
+
| [references/types.md](references/types.md) | Annotation, DrawMode, Label 등 모든 타입 |
|
|
127
|
+
| [references/apply-style.md](references/apply-style.md) | applyStyle 함수 시그니처, variant별 가이드, 예제 |
|
|
128
|
+
|
|
129
|
+
## 초기 설정
|
|
130
|
+
|
|
131
|
+
소비자 프로젝트에서의 설치 및 설정 방법은 [setup.md](setup.md)를 참조하세요.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# applyStyle 함수 레퍼런스
|
|
2
|
+
|
|
3
|
+
## 시그니처
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import type { ApplyAnnotationStyle } from '@deepnoid/canvas';
|
|
7
|
+
|
|
8
|
+
const applyStyle: ApplyAnnotationStyle = (params) => {
|
|
9
|
+
const { variant, context, annotation, drawLineSize, zoom } = params;
|
|
10
|
+
// ...
|
|
11
|
+
};
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 파라미터 상세
|
|
15
|
+
|
|
16
|
+
| 파라미터 | 타입 | 설명 |
|
|
17
|
+
|----------|------|------|
|
|
18
|
+
| `variant` | `'drawRect' \| 'drawText'` | 현재 렌더링 단계 |
|
|
19
|
+
| `context` | `CanvasRenderingContext2D` | Canvas 2D 그리기 컨텍스트 |
|
|
20
|
+
| `annotation` | `Annotation` | 렌더링 대상 어노테이션 (type으로 판별 가능) |
|
|
21
|
+
| `drawLineSize` | `number` | 설정된 선 두께 (현재 2px 고정 전달) |
|
|
22
|
+
| `zoom` | `number` | 현재 줌 배율 (1 = 100%) |
|
|
23
|
+
|
|
24
|
+
## 호출 순서
|
|
25
|
+
|
|
26
|
+
엔진이 각 어노테이션에 대해 다음 순서로 `applyStyle`을 호출합니다:
|
|
27
|
+
|
|
28
|
+
1. `variant: 'drawRect'` — 도형 외곽선/채우기 렌더링
|
|
29
|
+
2. `variant: 'drawText'` — 라벨 텍스트 렌더링
|
|
30
|
+
|
|
31
|
+
**중요**: 두 variant 모두에서 실제 Canvas2D 그리기 API(`strokeRect`, `fillText` 등)를 소비자가 직접 호출해야 합니다.
|
|
32
|
+
|
|
33
|
+
## 예제 1: 기본 스타일 (Fense 스타일)
|
|
34
|
+
|
|
35
|
+
color 필드에 따라 다른 색상으로 도형을 그리는 패턴.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { DrawMode } from '@deepnoid/canvas';
|
|
39
|
+
import type { ApplyAnnotationStyle } from '@deepnoid/canvas';
|
|
40
|
+
|
|
41
|
+
const fenseStyle: ApplyAnnotationStyle = ({ variant, context, annotation, zoom, drawLineSize }) => {
|
|
42
|
+
if (variant === 'drawRect') {
|
|
43
|
+
// color 필드로 색상 결정
|
|
44
|
+
const colorMap: Record<string, string> = {
|
|
45
|
+
danger: 'rgba(255, 70, 132, 1)',
|
|
46
|
+
warning: 'rgba(237, 164, 16, 1)',
|
|
47
|
+
success: 'rgba(36, 162, 91, 1)',
|
|
48
|
+
};
|
|
49
|
+
context.strokeStyle = colorMap[annotation.color ?? 'danger'] ?? 'rgba(255, 70, 132, 1)';
|
|
50
|
+
context.lineWidth = drawLineSize * (1 / zoom);
|
|
51
|
+
|
|
52
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
53
|
+
const { x, y, width, height } = annotation;
|
|
54
|
+
context.strokeRect(x, y, width, height);
|
|
55
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
56
|
+
context.beginPath();
|
|
57
|
+
annotation.points.forEach((p, idx) => {
|
|
58
|
+
if (idx === 0) context.moveTo(p.x, p.y);
|
|
59
|
+
else context.lineTo(p.x, p.y);
|
|
60
|
+
});
|
|
61
|
+
context.closePath();
|
|
62
|
+
context.stroke();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// drawText는 생략 — 라벨 표시 불필요한 경우
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 예제 2: 고급 스타일 (Seed 스타일)
|
|
70
|
+
|
|
71
|
+
선택 상태에 따른 색상 변경, 반투명 채우기, 십자 마커, 라벨 텍스트를 포함한 풀 스타일.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const seedStyle: ApplyAnnotationStyle = ({ variant, context, annotation, zoom, drawLineSize }) => {
|
|
75
|
+
const DEFAULT_COLOR = '#A020F0';
|
|
76
|
+
const ACTIVE_COLOR = '#FF4136';
|
|
77
|
+
const baseColor = annotation.selected ? ACTIVE_COLOR : DEFAULT_COLOR;
|
|
78
|
+
|
|
79
|
+
if (variant === 'drawRect') {
|
|
80
|
+
// 스타일 설정
|
|
81
|
+
context.strokeStyle = `${baseColor}cc`; // 80% 불투명
|
|
82
|
+
context.fillStyle = `${baseColor}1a`; // 10% 불투명
|
|
83
|
+
context.lineWidth = drawLineSize * (1 / zoom);
|
|
84
|
+
|
|
85
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
86
|
+
const { x, y, width, height } = annotation;
|
|
87
|
+
context.strokeRect(x, y, width, height);
|
|
88
|
+
context.fillRect(x, y, width, height);
|
|
89
|
+
|
|
90
|
+
// 중심 십자 마커
|
|
91
|
+
const cx = x + width / 2;
|
|
92
|
+
const cy = y + height / 2;
|
|
93
|
+
context.beginPath();
|
|
94
|
+
context.moveTo(cx, cy - 10);
|
|
95
|
+
context.lineTo(cx, cy + 10);
|
|
96
|
+
context.stroke();
|
|
97
|
+
context.beginPath();
|
|
98
|
+
context.moveTo(cx - 10, cy);
|
|
99
|
+
context.lineTo(cx + 10, cy);
|
|
100
|
+
context.stroke();
|
|
101
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
102
|
+
context.beginPath();
|
|
103
|
+
annotation.points.forEach((p, idx) => {
|
|
104
|
+
if (idx === 0) context.moveTo(p.x, p.y);
|
|
105
|
+
else context.lineTo(p.x, p.y);
|
|
106
|
+
});
|
|
107
|
+
context.closePath();
|
|
108
|
+
context.stroke();
|
|
109
|
+
context.fill();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (variant === 'drawText') {
|
|
114
|
+
if (annotation.label) {
|
|
115
|
+
context.fillStyle = baseColor;
|
|
116
|
+
context.font = `bold ${28 / zoom}px arial`;
|
|
117
|
+
|
|
118
|
+
let textX = 0, textY = 0;
|
|
119
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
120
|
+
textX = annotation.x;
|
|
121
|
+
textY = annotation.y - 5;
|
|
122
|
+
} else if (annotation.type === DrawMode.POLYGON && annotation.points.length > 0) {
|
|
123
|
+
textX = Math.min(...annotation.points.map(p => p.x));
|
|
124
|
+
textY = Math.min(...annotation.points.map(p => p.y)) - 5;
|
|
125
|
+
}
|
|
126
|
+
context.fillText(annotation.label.name, textX, textY);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 예제 3: 선택 상태 강조 (점선 + 글로우)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const highlightStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
|
|
136
|
+
if (variant === 'drawRect') {
|
|
137
|
+
const color = '#00BFFF';
|
|
138
|
+
context.strokeStyle = color;
|
|
139
|
+
context.lineWidth = drawLineSize / zoom;
|
|
140
|
+
|
|
141
|
+
// 선택 시 점선 + 그림자 효과
|
|
142
|
+
if (annotation.selected) {
|
|
143
|
+
context.setLineDash([6 / zoom, 3 / zoom]);
|
|
144
|
+
context.shadowColor = color;
|
|
145
|
+
context.shadowBlur = 8 / zoom;
|
|
146
|
+
} else {
|
|
147
|
+
context.setLineDash([]);
|
|
148
|
+
context.shadowColor = 'transparent';
|
|
149
|
+
context.shadowBlur = 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
153
|
+
context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
|
|
154
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
155
|
+
context.beginPath();
|
|
156
|
+
annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
|
|
157
|
+
context.closePath();
|
|
158
|
+
context.stroke();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 그림자 초기화 (다음 어노테이션에 영향 방지)
|
|
162
|
+
context.shadowColor = 'transparent';
|
|
163
|
+
context.shadowBlur = 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (variant === 'drawText') {
|
|
167
|
+
if (annotation.label) {
|
|
168
|
+
context.setLineDash([]); // 텍스트에 dash 적용 방지
|
|
169
|
+
context.fillStyle = '#00BFFF';
|
|
170
|
+
context.font = `${14 / zoom}px sans-serif`;
|
|
171
|
+
const tx = annotation.type === DrawMode.RECTANGLE ? annotation.x : Math.min(...annotation.points.map(p => p.x));
|
|
172
|
+
const ty = (annotation.type === DrawMode.RECTANGLE ? annotation.y : Math.min(...annotation.points.map(p => p.y))) - 5;
|
|
173
|
+
context.fillText(annotation.label.name, tx, ty);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 체크리스트
|
|
180
|
+
|
|
181
|
+
applyStyle 함수 작성 시 확인할 사항:
|
|
182
|
+
|
|
183
|
+
- [ ] `variant === 'drawRect'`와 `variant === 'drawText'` 분기 처리
|
|
184
|
+
- [ ] `annotation.type`으로 RECTANGLE/POLYGON 분기 처리
|
|
185
|
+
- [ ] `lineWidth`, `font`, `setLineDash` 등에 `/ zoom` 보정 적용
|
|
186
|
+
- [ ] `setLineDash` 사용 후 `context.setLineDash([])` 초기화
|
|
187
|
+
- [ ] `shadowBlur` 사용 후 초기화
|
|
188
|
+
- [ ] `annotation.color` 필드를 직접 CSS 색상으로 사용하지 않음 (매핑 필요)
|
|
189
|
+
- [ ] RECTANGLE과 POLYGON 모두에서 실제 도형 그리기 API 호출
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# 컴포넌트 API 레퍼런스
|
|
2
|
+
|
|
3
|
+
## AnnotationEditor
|
|
4
|
+
|
|
5
|
+
이미지 위에 어노테이션을 생성하고 편집할 수 있는 컴포넌트.
|
|
6
|
+
|
|
7
|
+
### Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { AnnotationEditor } from '@deepnoid/canvas';
|
|
11
|
+
import type { AnnotationCanvasHandle } from '@deepnoid/canvas';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Props
|
|
15
|
+
|
|
16
|
+
| Prop | Type | Required | Default | 설명 |
|
|
17
|
+
|------|------|----------|---------|------|
|
|
18
|
+
| `image` | `string` | ✅ | — | 이미지 URL |
|
|
19
|
+
| `drawing` | `AnnotationCanvasDrawing` | ✅ | — | 그리기 모드 및 스타일 설정 |
|
|
20
|
+
| `events` | `AnnotationCanvasEvents` | ✅ | — | 이미지 로드 이벤트 핸들러 (빈 객체 가능) |
|
|
21
|
+
| `annotations` | `Annotation[]` | — | `[]` | 어노테이션 목록 |
|
|
22
|
+
| `setAnnotations` | `Dispatch<SetStateAction<Annotation[] \| undefined>>` | — | — | 어노테이션 변경 콜백 |
|
|
23
|
+
| `options` | `AnnotationCanvasOptions` | — | — | 줌/팬 및 기타 옵션 |
|
|
24
|
+
| `enableHotkeys` | `boolean` | — | `true` | 단축키 활성화 여부 |
|
|
25
|
+
| `keepCrossOnImageChange` | `boolean` | — | `false` | 이미지 변경 시 가이드 십자선 유지 |
|
|
26
|
+
|
|
27
|
+
### ref (AnnotationCanvasHandle)
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
const canvasRef = useRef<AnnotationCanvasHandle>(null);
|
|
31
|
+
<AnnotationEditor ref={canvasRef} ... />
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Method | 설명 |
|
|
35
|
+
|--------|------|
|
|
36
|
+
| `resetImageSize()` | 캔버스 크기 및 줌을 초기 상태로 리셋 |
|
|
37
|
+
|
|
38
|
+
### 전체 사용 예시
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useState, useRef } from 'react';
|
|
42
|
+
import { AnnotationEditor, DrawMode } from '@deepnoid/canvas';
|
|
43
|
+
import type { Annotation, ApplyAnnotationStyle, AnnotationCanvasHandle } from '@deepnoid/canvas';
|
|
44
|
+
|
|
45
|
+
const applyStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
|
|
46
|
+
if (variant === 'drawRect') {
|
|
47
|
+
context.strokeStyle = annotation.selected ? '#00FF00' : '#FF4136';
|
|
48
|
+
context.lineWidth = drawLineSize / zoom;
|
|
49
|
+
if (annotation.type === DrawMode.RECTANGLE) {
|
|
50
|
+
context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
|
|
51
|
+
} else if (annotation.type === DrawMode.POLYGON) {
|
|
52
|
+
context.beginPath();
|
|
53
|
+
annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
|
|
54
|
+
context.closePath();
|
|
55
|
+
context.stroke();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (variant === 'drawText') {
|
|
59
|
+
if (annotation.label) {
|
|
60
|
+
context.fillStyle = '#FF4136';
|
|
61
|
+
context.font = `bold ${14 / zoom}px sans-serif`;
|
|
62
|
+
const tx = annotation.type === DrawMode.RECTANGLE ? annotation.x : Math.min(...annotation.points.map(p => p.x));
|
|
63
|
+
const ty = (annotation.type === DrawMode.RECTANGLE ? annotation.y : Math.min(...annotation.points.map(p => p.y))) - 5;
|
|
64
|
+
context.fillText(annotation.label.name, tx, ty);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function MyEditor() {
|
|
70
|
+
const canvasRef = useRef<AnnotationCanvasHandle>(null);
|
|
71
|
+
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
|
72
|
+
const [mode, setMode] = useState(DrawMode.RECTANGLE);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
76
|
+
<AnnotationEditor
|
|
77
|
+
ref={canvasRef}
|
|
78
|
+
image="https://example.com/image.jpg"
|
|
79
|
+
annotations={annotations}
|
|
80
|
+
setAnnotations={setAnnotations}
|
|
81
|
+
drawing={{
|
|
82
|
+
mode,
|
|
83
|
+
color: '#FF4136',
|
|
84
|
+
lineSize: 2,
|
|
85
|
+
applyStyle,
|
|
86
|
+
label: { id: 1, name: 'defect', type: 'bbox' },
|
|
87
|
+
}}
|
|
88
|
+
events={{
|
|
89
|
+
onImageLoadSuccess: () => console.log('loaded'),
|
|
90
|
+
onImageLoadError: (err) => console.error(err),
|
|
91
|
+
}}
|
|
92
|
+
options={{
|
|
93
|
+
panZoomEnabled: true,
|
|
94
|
+
zoom: { min: 0.5, max: 4, step: 0.9 },
|
|
95
|
+
}}
|
|
96
|
+
enableHotkeys
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## AnnotationViewer
|
|
106
|
+
|
|
107
|
+
이미지 위에 어노테이션을 읽기 전용으로 표시하는 컴포넌트.
|
|
108
|
+
|
|
109
|
+
### Import
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { AnnotationViewer } from '@deepnoid/canvas';
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Props
|
|
116
|
+
|
|
117
|
+
| Prop | Type | Required | Default | 설명 |
|
|
118
|
+
|------|------|----------|---------|------|
|
|
119
|
+
| `image` | `string` | ✅ | — | 이미지 URL |
|
|
120
|
+
| `drawing` | `Pick<AnnotationCanvasDrawing, 'lineSize' \| 'applyStyle'>` | ✅ | — | 렌더링 스타일 설정 (`mode`, `color`, `label` 불필요) |
|
|
121
|
+
| `events` | `Pick<AnnotationCanvasEvents, 'onImageLoadSuccess' \| 'onImageLoadError'>` | ✅ | — | 이미지 로드 이벤트 핸들러 (빈 객체 가능) |
|
|
122
|
+
| `annotations` | `Annotation[]` | — | `[]` | 표시할 어노테이션 목록 |
|
|
123
|
+
| `options` | `AnnotationCanvasOptions` | — | — | 줌/팬 및 기타 옵션 |
|
|
124
|
+
| `enableHotkeys` | `boolean` | — | `true` | 단축키 활성화 여부 |
|
|
125
|
+
|
|
126
|
+
> **AnnotationEditor와의 차이**: `drawing`에서 `mode`, `color`, `label`이 제외됩니다. `setAnnotations`도 불필요합니다. `keepCrossOnImageChange`도 없습니다.
|
|
127
|
+
|
|
128
|
+
### 전체 사용 예시
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { AnnotationViewer } from '@deepnoid/canvas';
|
|
132
|
+
|
|
133
|
+
function MyViewer({ annotations }) {
|
|
134
|
+
return (
|
|
135
|
+
<div style={{ width: '100%', height: 600 }}>
|
|
136
|
+
<AnnotationViewer
|
|
137
|
+
image="https://example.com/image.jpg"
|
|
138
|
+
annotations={annotations}
|
|
139
|
+
drawing={{
|
|
140
|
+
lineSize: 2,
|
|
141
|
+
applyStyle, // Editor와 동일한 함수 재사용 가능
|
|
142
|
+
}}
|
|
143
|
+
events={{
|
|
144
|
+
onImageLoadSuccess: () => console.log('loaded'),
|
|
145
|
+
}}
|
|
146
|
+
options={{
|
|
147
|
+
panZoomEnabled: true,
|
|
148
|
+
zoom: { min: 0.5, max: 4 },
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Props 상세 타입
|
|
159
|
+
|
|
160
|
+
### AnnotationCanvasDrawing
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
type AnnotationCanvasDrawing = {
|
|
164
|
+
lineSize: number; // 선 두께 (필수)
|
|
165
|
+
applyStyle: ApplyAnnotationStyle; // 커스텀 렌더링 함수 (필수)
|
|
166
|
+
label?: Label; // 새 어노테이션에 부여할 라벨 (Editor 전용)
|
|
167
|
+
mode?: DrawMode; // 그리기 모드 (Editor 전용)
|
|
168
|
+
color?: string; // HEX 색상, 새 어노테이션 그리기 시 사용 (Editor 전용)
|
|
169
|
+
};
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### AnnotationCanvasOptions
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
type AnnotationCanvasOptions = {
|
|
176
|
+
panZoomEnabled?: boolean; // 줌/팬 활성화 (기본: false)
|
|
177
|
+
zoom?: {
|
|
178
|
+
min?: number; // 최소 줌 (기본: 0.1)
|
|
179
|
+
max?: number; // 최대 줌 (기본: Infinity)
|
|
180
|
+
step?: number; // 줌 단계 (기본: 0.9)
|
|
181
|
+
};
|
|
182
|
+
ZoomButton?: ComponentType<{ // 커스텀 줌 버튼
|
|
183
|
+
onClick: () => void;
|
|
184
|
+
children: ReactNode;
|
|
185
|
+
}>;
|
|
186
|
+
resetOnImageChange?: boolean; // 이미지 변경 시 줌 리셋 (기본: false)
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### AnnotationCanvasEvents
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
type AnnotationCanvasEvents = {
|
|
194
|
+
onImageLoadSuccess?: () => void;
|
|
195
|
+
onImageLoadError?: (error: Error) => void;
|
|
196
|
+
};
|
|
197
|
+
```
|