@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 CHANGED
@@ -31,9 +31,23 @@ Canvas 기반 이미지 annotation 라이브러리로, **TypeScript Engine + Rea
31
31
  ### 설치
32
32
 
33
33
  ```bash
34
- npm install deepnoid-canvas
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.85",
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
+ ```