@deepnoid/canvas 0.1.84 → 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.
@@ -0,0 +1,171 @@
1
+ # 레이아웃, 줌/팬 규칙
2
+
3
+ ## 규칙 1: 컨테이너에 반드시 고정 크기를 설정한다
4
+
5
+ `AnnotationEditor`/`AnnotationViewer`는 부모 컨테이너의 100% 크기를 차지합니다.
6
+ 부모에 크기가 없으면 캔버스가 0px로 렌더링됩니다.
7
+
8
+ ```tsx
9
+ // ✅ 올바른 사용 — 고정 크기
10
+ <div style={{ width: 800, height: 600 }}>
11
+ <AnnotationEditor ... />
12
+ </div>
13
+
14
+ // ✅ flex 레이아웃
15
+ <div style={{ display: 'flex', height: '100vh' }}>
16
+ <aside style={{ width: 200 }}>사이드바</aside>
17
+ <div style={{ flex: 1 }}>
18
+ <AnnotationEditor ... />
19
+ </div>
20
+ </div>
21
+
22
+ // ❌ 잘못된 사용 — 크기 없는 컨테이너
23
+ <div>
24
+ <AnnotationEditor ... />
25
+ </div>
26
+ // → 캔버스 크기가 0이 되어 아무것도 보이지 않음
27
+ ```
28
+
29
+ ## 규칙 2: panZoomEnabled로 줌/팬을 제어한다
30
+
31
+ 줌(마우스 휠)과 팬(우클릭 드래그)은 `options.panZoomEnabled`로 제어합니다.
32
+
33
+ ```tsx
34
+ // ✅ 줌/팬 활성화
35
+ <AnnotationEditor
36
+ options={{ panZoomEnabled: true }}
37
+ ...
38
+ />
39
+
40
+ // ✅ 줌/팬 비활성화 (고정 뷰)
41
+ <AnnotationEditor
42
+ options={{ panZoomEnabled: false }}
43
+ ...
44
+ />
45
+
46
+ // ✅ 기본값: panZoomEnabled를 생략하면 옵션 객체 자체가 없어야 함
47
+ <AnnotationEditor options={undefined} ... />
48
+ ```
49
+
50
+ ## 규칙 3: zoom 옵션으로 줌 범위를 제한한다
51
+
52
+ 줌 배율의 최소/최대/단계를 설정할 수 있습니다.
53
+
54
+ ```tsx
55
+ // ✅ 올바른 사용 — 줌 범위 설정
56
+ <AnnotationEditor
57
+ options={{
58
+ panZoomEnabled: true,
59
+ zoom: {
60
+ min: 0.5, // 최소 50% (기본: 0.1)
61
+ max: 4, // 최대 400% (기본: Infinity)
62
+ step: 0.9, // 한 스텝당 배율 변화 (기본: 0.9)
63
+ },
64
+ }}
65
+ ...
66
+ />
67
+
68
+ // ✅ 일부만 설정 가능
69
+ <AnnotationEditor
70
+ options={{
71
+ panZoomEnabled: true,
72
+ zoom: { max: 10 }, // min, step은 기본값 사용
73
+ }}
74
+ ...
75
+ />
76
+ ```
77
+
78
+ ## 규칙 4: ZoomButton으로 줌 비율 표시 및 리셋
79
+
80
+ 커스텀 줌 버튼 컴포넌트를 주입하여 현재 줌 비율 표시 및 클릭 시 줌 리셋이 가능합니다.
81
+
82
+ ```tsx
83
+ // ✅ 올바른 사용 — 커스텀 ZoomButton
84
+ const ZoomButton = ({ onClick, children }: { onClick: () => void; children: ReactNode }) => (
85
+ <button
86
+ onClick={onClick}
87
+ style={{ position: 'absolute', top: 10, left: 10, zIndex: 10 }}
88
+ >
89
+ {children} {/* "125%" 같은 줌 비율 문자열 */}
90
+ </button>
91
+ );
92
+
93
+ <AnnotationEditor
94
+ options={{
95
+ panZoomEnabled: true,
96
+ ZoomButton,
97
+ }}
98
+ ...
99
+ />
100
+
101
+ // ❌ 잘못된 사용 — ZoomButton에 잘못된 시그니처
102
+ const ZoomButton = ({ zoom }) => <span>{zoom}</span>; // onClick과 children이 필요
103
+ ```
104
+
105
+ ## 규칙 5: resetOnImageChange로 이미지 전환 시 줌 상태를 제어한다
106
+
107
+ 이미지가 바뀔 때 줌/팬 상태를 초기화할지 유지할지 선택할 수 있습니다.
108
+
109
+ ```tsx
110
+ // ✅ 이미지 전환 시 줌 리셋
111
+ <AnnotationEditor
112
+ options={{ resetOnImageChange: true }}
113
+ ...
114
+ />
115
+
116
+ // ✅ 이미지 전환 시 줌 유지 (기본값: false)
117
+ <AnnotationEditor
118
+ options={{ resetOnImageChange: false }}
119
+ ...
120
+ />
121
+ ```
122
+
123
+ ## 규칙 6: ref로 캔버스 크기를 수동 리셋할 수 있다
124
+
125
+ `AnnotationCanvasHandle` ref를 통해 프로그래밍 방식으로 캔버스를 리셋합니다.
126
+
127
+ ```tsx
128
+ import { useRef } from 'react';
129
+ import type { AnnotationCanvasHandle } from '@deepnoid/canvas';
130
+
131
+ // ✅ 올바른 사용
132
+ const canvasRef = useRef<AnnotationCanvasHandle>(null);
133
+
134
+ <button onClick={() => canvasRef.current?.resetImageSize()}>
135
+ 크기 초기화
136
+ </button>
137
+ <AnnotationEditor ref={canvasRef} ... />
138
+
139
+ // ❌ 잘못된 사용 — 잘못된 타입
140
+ const canvasRef = useRef<HTMLCanvasElement>(null); // AnnotationCanvasHandle이어야 함
141
+ ```
142
+
143
+ ## 규칙 7: keepCrossOnImageChange로 가이드 십자선 유지
144
+
145
+ 이미지가 변경될 때 마우스 위치의 가이드 십자선(crosshair)을 유지할 수 있습니다.
146
+ `AnnotationEditor`에서만 사용 가능합니다.
147
+
148
+ ```tsx
149
+ // ✅ 이미지 전환 시 십자선 유지 (연속 작업 시 편리)
150
+ <AnnotationEditor keepCrossOnImageChange ... />
151
+
152
+ // ✅ 기본 동작 — 이미지 전환 시 십자선 제거
153
+ <AnnotationEditor keepCrossOnImageChange={false} ... />
154
+ ```
155
+
156
+ ## 규칙 8: 리사이즈 대응은 자동으로 처리된다
157
+
158
+ 컨테이너 크기가 변하면 내부 `ResizeObserver`가 자동으로 캔버스를 재설정합니다 (150ms debounce).
159
+ 별도의 리사이즈 처리가 필요 없습니다.
160
+
161
+ ```tsx
162
+ // ✅ 리사이즈 대응 — 별도 처리 불필요
163
+ <div style={{ width: '100%', height: '100%' }}>
164
+ <AnnotationEditor ... />
165
+ </div>
166
+
167
+ // ❌ 불필요한 사용 — 수동 리사이즈 핸들러
168
+ window.addEventListener('resize', () => {
169
+ canvasRef.current?.resetImageSize(); // 불필요, 자동 처리됨
170
+ });
171
+ ```
@@ -0,0 +1,142 @@
1
+ # applyStyle 렌더링 규칙
2
+
3
+ ## 규칙 1: applyStyle은 반드시 제공해야 한다
4
+
5
+ `drawing.applyStyle`은 필수 prop입니다. 이 함수 없이는 어노테이션이 화면에 표시되지 않습니다.
6
+
7
+ ```tsx
8
+ // ✅ 올바른 사용
9
+ <AnnotationEditor
10
+ drawing={{
11
+ lineSize: 2,
12
+ applyStyle: myStyleFunction,
13
+ mode: DrawMode.RECTANGLE,
14
+ color: '#FF4136',
15
+ }}
16
+ ...
17
+ />
18
+
19
+ // ❌ 잘못된 사용 — applyStyle 누락
20
+ <AnnotationEditor
21
+ drawing={{
22
+ lineSize: 2,
23
+ mode: DrawMode.RECTANGLE,
24
+ }}
25
+ ...
26
+ />
27
+ ```
28
+
29
+ ## 규칙 2: variant별로 분기 처리해야 한다
30
+
31
+ `applyStyle`은 `drawRect`과 `drawText` 두 번 호출됩니다. 각 variant에 맞는 Canvas2D 속성을 설정해야 합니다.
32
+
33
+ ```tsx
34
+ // ✅ 올바른 사용 — variant별 분기
35
+ const applyStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
36
+ if (variant === 'drawRect') {
37
+ context.strokeStyle = '#FF4136';
38
+ context.lineWidth = drawLineSize / zoom;
39
+ if (annotation.type === DrawMode.RECTANGLE) {
40
+ context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
41
+ } else if (annotation.type === DrawMode.POLYGON) {
42
+ context.beginPath();
43
+ annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
44
+ context.closePath();
45
+ context.stroke();
46
+ }
47
+ }
48
+ if (variant === 'drawText') {
49
+ context.fillStyle = '#FF4136';
50
+ context.font = `bold ${14 / zoom}px sans-serif`;
51
+ // 텍스트 렌더링 로직...
52
+ }
53
+ };
54
+
55
+ // ❌ 잘못된 사용 — variant 무시
56
+ const applyStyle: ApplyAnnotationStyle = ({ context, annotation }) => {
57
+ context.strokeStyle = '#FF4136';
58
+ context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
59
+ };
60
+ ```
61
+
62
+ ## 규칙 3: 모든 크기 값에 zoom 보정을 적용해야 한다
63
+
64
+ 줌 배율이 변해도 어노테이션 선 두께, 폰트 크기, 대시 간격 등이 일정하게 보이려면 반드시 `/ zoom`으로 나누어야 합니다.
65
+
66
+ ```tsx
67
+ // ✅ 올바른 사용 — zoom 보정
68
+ context.lineWidth = drawLineSize / zoom;
69
+ context.font = `bold ${14 / zoom}px sans-serif`;
70
+ context.setLineDash([6 / zoom, 3 / zoom]);
71
+
72
+ // ❌ 잘못된 사용 — zoom 보정 없음 (줌인하면 선이 점점 두꺼워짐)
73
+ context.lineWidth = drawLineSize;
74
+ context.font = `bold 14px sans-serif`;
75
+ context.setLineDash([6, 3]);
76
+ ```
77
+
78
+ ## 규칙 4: annotation.type으로 분기하여 도형을 직접 그려야 한다
79
+
80
+ `applyStyle`의 `drawRect` variant에서는 스타일 설정뿐 아니라 **실제 도형 그리기**까지 소비자가 담당합니다.
81
+
82
+ ```tsx
83
+ // ✅ 올바른 사용 — 타입별 도형 그리기
84
+ if (variant === 'drawRect') {
85
+ context.strokeStyle = '#FF4136';
86
+ context.lineWidth = drawLineSize / zoom;
87
+
88
+ if (annotation.type === DrawMode.RECTANGLE) {
89
+ const { x, y, width, height } = annotation;
90
+ context.strokeRect(x, y, width, height);
91
+ } else if (annotation.type === DrawMode.POLYGON) {
92
+ context.beginPath();
93
+ annotation.points.forEach((p, idx) => {
94
+ if (idx === 0) context.moveTo(p.x, p.y);
95
+ else context.lineTo(p.x, p.y);
96
+ });
97
+ context.closePath();
98
+ context.stroke();
99
+ }
100
+ }
101
+
102
+ // ❌ 잘못된 사용 — RECTANGLE만 처리하고 POLYGON 무시
103
+ if (variant === 'drawRect') {
104
+ context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
105
+ }
106
+ ```
107
+
108
+ ## 규칙 5: setLineDash 사용 후 반드시 초기화해야 한다
109
+
110
+ Canvas2D 컨텍스트는 상태를 유지합니다. `setLineDash`로 점선을 설정하면 다음 어노테이션에도 영향을 줍니다.
111
+
112
+ ```tsx
113
+ // ✅ 올바른 사용 — 조건부 dash + 초기화
114
+ if (annotation.selected) {
115
+ context.setLineDash([6 / zoom, 3 / zoom]);
116
+ } else {
117
+ context.setLineDash([]);
118
+ }
119
+
120
+ // ❌ 잘못된 사용 — selected일 때만 dash 설정, else 빠짐
121
+ if (annotation.selected) {
122
+ context.setLineDash([6 / zoom, 3 / zoom]);
123
+ }
124
+ // → 다음 어노테이션도 점선으로 그려짐
125
+ ```
126
+
127
+ ## 규칙 6: annotation.color를 활용한 상태별 스타일링
128
+
129
+ 어노테이션의 `color` 필드(`'success' | 'warning' | 'danger'`)를 활용하여 상태별 색상을 적용할 수 있습니다.
130
+
131
+ ```tsx
132
+ // ✅ 올바른 사용 — color 필드 매핑
133
+ const colorMap = {
134
+ success: 'rgba(36, 162, 91, 1)',
135
+ warning: 'rgba(237, 164, 16, 1)',
136
+ danger: 'rgba(255, 70, 132, 1)',
137
+ };
138
+ context.strokeStyle = colorMap[annotation.color ?? 'danger'];
139
+
140
+ // ❌ 잘못된 사용 — color 필드를 직접 CSS 색상으로 사용
141
+ context.strokeStyle = annotation.color; // 'danger'는 유효한 CSS 색상이 아님
142
+ ```
@@ -0,0 +1,147 @@
1
+ # 소비자 프로젝트 설정 가이드
2
+
3
+ ## 1. 설치
4
+
5
+ ```bash
6
+ # npm
7
+ npm install @deepnoid/canvas
8
+
9
+ # yarn
10
+ yarn add @deepnoid/canvas
11
+ ```
12
+
13
+ **Peer dependency**: React 19.0.0 이상 필수.
14
+
15
+ ## 2. Claude Code 스킬 설치
16
+
17
+ 패키지에 포함된 스킬을 프로젝트의 `.claude/skills/`에 복사합니다.
18
+
19
+ ```bash
20
+ cp -r node_modules/@deepnoid/canvas/skills/deepnoid-canvas .claude/skills/
21
+ ```
22
+
23
+ 이후 Claude Code가 `@deepnoid/canvas` 관련 코드를 작성할 때 자동으로 스킬을 참조합니다.
24
+
25
+ ## 3. 기본 설정
26
+
27
+ ### 최소 구성 (Editor)
28
+
29
+ ```tsx
30
+ import { useState } from 'react';
31
+ import { AnnotationEditor, DrawMode } from '@deepnoid/canvas';
32
+ import type { Annotation, ApplyAnnotationStyle } from '@deepnoid/canvas';
33
+
34
+ // Step 1: applyStyle 함수 정의 (필수)
35
+ const applyStyle: ApplyAnnotationStyle = ({ variant, context, annotation, drawLineSize, zoom }) => {
36
+ if (variant === 'drawRect') {
37
+ context.strokeStyle = annotation.selected ? '#00FF00' : '#FF4136';
38
+ context.lineWidth = drawLineSize / zoom;
39
+ if (annotation.type === DrawMode.RECTANGLE) {
40
+ context.strokeRect(annotation.x, annotation.y, annotation.width, annotation.height);
41
+ } else if (annotation.type === DrawMode.POLYGON) {
42
+ context.beginPath();
43
+ annotation.points.forEach((p, i) => i === 0 ? context.moveTo(p.x, p.y) : context.lineTo(p.x, p.y));
44
+ context.closePath();
45
+ context.stroke();
46
+ }
47
+ }
48
+ };
49
+
50
+ // Step 2: 컴포넌트 사용
51
+ function AnnotationPage() {
52
+ const [annotations, setAnnotations] = useState<Annotation[]>([]);
53
+
54
+ return (
55
+ // Step 3: 반드시 고정 크기의 컨테이너로 감싸기
56
+ <div style={{ width: '100%', height: '100vh' }}>
57
+ <AnnotationEditor
58
+ image="https://example.com/image.jpg"
59
+ annotations={annotations}
60
+ setAnnotations={setAnnotations}
61
+ drawing={{
62
+ mode: DrawMode.RECTANGLE,
63
+ color: '#FF4136',
64
+ lineSize: 2,
65
+ applyStyle,
66
+ }}
67
+ events={{}}
68
+ enableHotkeys
69
+ />
70
+ </div>
71
+ );
72
+ }
73
+ ```
74
+
75
+ ### 최소 구성 (Viewer)
76
+
77
+ ```tsx
78
+ import { AnnotationViewer } from '@deepnoid/canvas';
79
+
80
+ function ViewerPage({ annotations }) {
81
+ return (
82
+ <div style={{ width: '100%', height: '100vh' }}>
83
+ <AnnotationViewer
84
+ image="https://example.com/image.jpg"
85
+ annotations={annotations}
86
+ drawing={{ lineSize: 2, applyStyle }}
87
+ events={{}}
88
+ />
89
+ </div>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ## 4. 권장 설정
95
+
96
+ ### 줌/팬 활성화
97
+
98
+ ```tsx
99
+ <AnnotationEditor
100
+ options={{
101
+ panZoomEnabled: true,
102
+ zoom: { min: 0.5, max: 4, step: 0.9 },
103
+ }}
104
+ ...
105
+ />
106
+ ```
107
+
108
+ ### annotationSelected 이벤트 연동
109
+
110
+ 어노테이션 클릭 시 그리기 모드 자동 전환:
111
+
112
+ ```tsx
113
+ const [mode, setMode] = useState(DrawMode.RECTANGLE);
114
+
115
+ useEffect(() => {
116
+ const handler = (e: CustomEvent) => {
117
+ if (e.detail?.type) setMode(e.detail.type);
118
+ };
119
+ window.addEventListener('annotationSelected', handler);
120
+ return () => window.removeEventListener('annotationSelected', handler);
121
+ }, []);
122
+
123
+ <AnnotationEditor drawing={{ mode, ... }} ... />
124
+ ```
125
+
126
+ ### ref를 통한 줌 리셋 버튼
127
+
128
+ ```tsx
129
+ import { useRef } from 'react';
130
+ import type { AnnotationCanvasHandle } from '@deepnoid/canvas';
131
+
132
+ const canvasRef = useRef<AnnotationCanvasHandle>(null);
133
+
134
+ <button onClick={() => canvasRef.current?.resetImageSize()}>초기화</button>
135
+ <AnnotationEditor ref={canvasRef} ... />
136
+ ```
137
+
138
+ ## 5. 체크리스트
139
+
140
+ 소비자 프로젝트 설정 후 확인 사항:
141
+
142
+ - [ ] React 19+ 설치 확인
143
+ - [ ] `applyStyle` 함수에서 `drawRect`/`drawText` variant 분기 처리
144
+ - [ ] `applyStyle` 내 모든 크기 값에 `/ zoom` 보정 적용
145
+ - [ ] 컨테이너 `div`에 고정 크기(width/height) 설정
146
+ - [ ] RECTANGLE/POLYGON 모두에서 도형 그리기 처리
147
+ - [ ] `events` prop에 최소 빈 객체 `{}` 전달