@zzalai/leafer-point-annotation 1.0.0

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/src/App.vue ADDED
@@ -0,0 +1,464 @@
1
+ <template>
2
+ <div class="app">
3
+ <h1>LeaferJS Point Annotation Test</h1>
4
+
5
+ <div class="editor-container">
6
+ <PointAnnotation
7
+ ref="pointAnnotation"
8
+ :imageSource="imageSource"
9
+ :options="editorOptions"
10
+ @pointChange="handlePointChange"
11
+ @loadStart="handleLoadStart"
12
+ @loadSuccess="handleLoadSuccess"
13
+ @loadError="handleLoadError"
14
+ />
15
+ </div>
16
+
17
+ <div class="controls">
18
+ <h2>Controls</h2>
19
+ <div class="control-group">
20
+ <label for="imageUrl">Image URL:</label>
21
+ <input
22
+ type="text"
23
+ id="imageUrl"
24
+ v-model="imageUrl"
25
+ placeholder="Enter image URL"
26
+ />
27
+ </div>
28
+
29
+ <div class="control-group">
30
+ <button @click="fetchPointData">Get Point Data</button>
31
+ <button @click="exportData">Export Point Data</button>
32
+ <button @click="refreshImage">Refresh Image</button>
33
+ </div>
34
+
35
+ <div class="control-group">
36
+ <h3>Canvas Export/Import</h3>
37
+ <button @click="exportCanvasJSON">Export Canvas JSON</button>
38
+ <input
39
+ type="file"
40
+ ref="fileInput"
41
+ style="display: none"
42
+ accept=".json"
43
+ @change="importCanvasJSON"
44
+ />
45
+ <button @click="triggerFileInput">Import Canvas JSON</button>
46
+ </div>
47
+
48
+ <div class="control-group">
49
+ <h3>Brush Mask Export</h3>
50
+ <div class="mask-options">
51
+ <label>
52
+ Format:
53
+ <select v-model="maskFormat">
54
+ <option value="png">PNG</option>
55
+ <option value="jpeg">JPEG</option>
56
+ </select>
57
+ </label>
58
+ <label>
59
+ Foreground:
60
+ <select v-model="maskForeground">
61
+ <option value="black">Black</option>
62
+ <option value="white">White</option>
63
+ </select>
64
+ </label>
65
+ </div>
66
+ <button @click="exportMaskImage">Export Mask Image</button>
67
+ <button @click="clearBrush">Clear Brush</button>
68
+ </div>
69
+
70
+ <div class="control-group">
71
+ <h3>Annotation Format Export</h3>
72
+ <button @click="exportCOCO">Export COCO JSON</button>
73
+ <button @click="exportYOLO">Export YOLO</button>
74
+ </div>
75
+
76
+ <div class="control-group">
77
+ <h3>API Methods</h3>
78
+ <div class="api-row">
79
+ <button @click="testUndo">Undo</button>
80
+ <button @click="testRedo">Redo</button>
81
+ </div>
82
+ <div class="api-row">
83
+ <label>Tool:</label>
84
+ <select v-model="currentTool">
85
+ <option value="select">Select</option>
86
+ <option value="point">Point</option>
87
+ <option value="brush">Brush</option>
88
+ <option value="eraser">Eraser</option>
89
+ </select>
90
+ <button @click="callSetTool">Set Tool</button>
91
+ </div>
92
+ <div class="api-row">
93
+ <button @click="testAddPoint">Add Point (100,100)</button>
94
+ <button @click="testRemovePoint">Remove Last Point</button>
95
+ </div>
96
+ <div class="api-row">
97
+ <span>Current Tool: {{ currentToolDisplay }}</span>
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <div class="output">
103
+ <h2>Point Data</h2>
104
+ <pre>{{ pointData }}</pre>
105
+ </div>
106
+
107
+ <div class="status">
108
+ <h2>Status</h2>
109
+ <p>Image Load Status: {{ loadStatus }}</p>
110
+ </div>
111
+ </div>
112
+ </template>
113
+
114
+ <script setup lang="ts">
115
+ import { ref, computed } from 'vue'
116
+ import PointAnnotation from './components/PointAnnotation.vue'
117
+
118
+ // 图片URL
119
+ const imageUrl = ref('https://picsum.photos/1280/1080')
120
+ const imageSource = computed(() => ({
121
+ id: 'test-image',
122
+ url: imageUrl.value
123
+ }))
124
+
125
+ // 编辑器选项
126
+ const editorOptions = ref({
127
+ pointStyle: {
128
+ fill: '#f00',
129
+ stroke: '#fff',
130
+ strokeWidth: 2,
131
+ width: 16,
132
+ height: 16,
133
+ radius: 8
134
+ },
135
+ selectedPointStyle: {
136
+ fill: '#00f',
137
+ stroke: '#fff',
138
+ strokeWidth: 2,
139
+ radius: 10
140
+ }
141
+ })
142
+
143
+ // 状态
144
+ const loadStatus = ref('idle')
145
+ const pointData = ref('')
146
+ const pointAnnotation = ref<InstanceType<typeof PointAnnotation> | null>(null)
147
+ const maskFormat = ref<'png' | 'jpeg'>('png')
148
+ const maskForeground = ref<'black' | 'white'>('black')
149
+ const currentTool = ref<'select' | 'point' | 'brush' | 'eraser'>('select')
150
+ const currentToolDisplay = ref('select')
151
+ const lastAddedPointId = ref<string | null>(null)
152
+
153
+ // 处理点变化
154
+ const handlePointChange = (data: any) => {
155
+ pointData.value = JSON.stringify(data, null, 2)
156
+ }
157
+
158
+ // 处理图片加载开始
159
+ const handleLoadStart = () => {
160
+ loadStatus.value = 'loading'
161
+ }
162
+
163
+ // 处理图片加载成功
164
+ const handleLoadSuccess = () => {
165
+ loadStatus.value = 'success'
166
+ }
167
+
168
+ // 处理图片加载失败
169
+ const handleLoadError = (error: any) => {
170
+ loadStatus.value = 'error'
171
+ console.error('Image load error:', error)
172
+ }
173
+
174
+ // 重新加载图片
175
+ const refreshImage = () => {
176
+ if (pointAnnotation.value) {
177
+ pointAnnotation.value.loadImage()
178
+ }
179
+ }
180
+
181
+ // 导出点数据
182
+ const exportData = () => {
183
+ // 调用PointAnnotation的getPointAnnotations方法获取最新数据
184
+ if (pointAnnotation.value) {
185
+ const annotations = pointAnnotation.value.getPointAnnotations()
186
+ const data = JSON.stringify(annotations, null, 2)
187
+
188
+ const blob = new Blob([data], { type: 'application/json' })
189
+ const url = URL.createObjectURL(blob)
190
+ const a = document.createElement('a')
191
+ a.href = url
192
+ a.download = 'point-data.json'
193
+ a.click()
194
+ URL.revokeObjectURL(url)
195
+ }
196
+ }
197
+
198
+ // 手动获取点数据
199
+ const fetchPointData = () => {
200
+ if (pointAnnotation.value) {
201
+ const annotations = pointAnnotation.value.getPointAnnotations()
202
+ pointData.value = JSON.stringify(annotations, null, 2)
203
+ }
204
+ }
205
+
206
+ // 导出画布JSON
207
+ const exportCanvasJSON = () => {
208
+ if (pointAnnotation.value) {
209
+ const json = pointAnnotation.value.exportCanvasJSON()
210
+
211
+ const blob = new Blob([json], { type: 'application/json' })
212
+ const url = URL.createObjectURL(blob)
213
+ const a = document.createElement('a')
214
+ a.href = url
215
+ a.download = 'canvas-data.json'
216
+ a.click()
217
+ URL.revokeObjectURL(url)
218
+ }
219
+ }
220
+
221
+ // 触发文件输入
222
+ const fileInput = ref<HTMLInputElement | null>(null)
223
+ const triggerFileInput = () => {
224
+ fileInput.value?.click()
225
+ }
226
+
227
+ // 导入画布JSON
228
+ const importCanvasJSON = async (event: Event) => {
229
+ const target = event.target as HTMLInputElement
230
+ const file = target.files?.[0]
231
+ if (file) {
232
+ const reader = new FileReader()
233
+ reader.onload = async (e) => {
234
+ const jsonString = e.target?.result as string
235
+ if (pointAnnotation.value) {
236
+ const success = await pointAnnotation.value.importCanvasJSON(jsonString, { resetZoom: true })
237
+ if (success) {
238
+ alert('Canvas imported successfully!')
239
+ } else {
240
+ alert('Failed to import canvas.')
241
+ }
242
+ }
243
+ }
244
+ reader.readAsText(file)
245
+ }
246
+ // 重置文件输入
247
+ target.value = ''
248
+ }
249
+
250
+ // 导出Mask图片
251
+ const exportMaskImage = async () => {
252
+ if (pointAnnotation.value) {
253
+ const maskData = await pointAnnotation.value.exportMaskImage(maskFormat.value, maskForeground.value)
254
+ if (maskData) {
255
+ const ext = maskFormat.value === 'png' ? 'png' : 'jpg'
256
+ const a = document.createElement('a')
257
+ a.href = maskData
258
+ a.download = `brush-mask.${ext}`
259
+ a.click()
260
+ } else {
261
+ alert('No brush data to export.')
262
+ }
263
+ }
264
+ }
265
+
266
+ // 导出COCO格式
267
+ const exportCOCO = () => {
268
+ if (pointAnnotation.value) {
269
+ const coco = pointAnnotation.value.exportCOCO()
270
+ const blob = new Blob([coco], { type: 'application/json' })
271
+ const url = URL.createObjectURL(blob)
272
+ const a = document.createElement('a')
273
+ a.href = url
274
+ a.download = 'annotation-coco.json'
275
+ a.click()
276
+ URL.revokeObjectURL(url)
277
+ }
278
+ }
279
+
280
+ // 导出YOLO格式
281
+ const exportYOLO = () => {
282
+ if (pointAnnotation.value) {
283
+ const yolo = pointAnnotation.value.exportYOLO()
284
+ const blob = new Blob([yolo.annotations], { type: 'text/plain' })
285
+ const url = URL.createObjectURL(blob)
286
+ const a = document.createElement('a')
287
+ a.href = url
288
+ a.download = 'annotation-yolo.txt'
289
+ a.click()
290
+ URL.revokeObjectURL(url)
291
+
292
+ const classBlob = new Blob([yolo.classNames], { type: 'text/plain' })
293
+ const classUrl = URL.createObjectURL(classBlob)
294
+ const classA = document.createElement('a')
295
+ classA.href = classUrl
296
+ classA.download = 'class-names.txt'
297
+ classA.click()
298
+ URL.revokeObjectURL(classUrl)
299
+ }
300
+ }
301
+
302
+ // 清除笔刷
303
+ const clearBrush = () => {
304
+ if (pointAnnotation.value) {
305
+ pointAnnotation.value.clearBrush?.()
306
+ alert('Brush cleared!')
307
+ }
308
+ }
309
+
310
+ // API 测试方法
311
+ const testUndo = () => {
312
+ if (pointAnnotation.value) {
313
+ pointAnnotation.value.undo()
314
+ }
315
+ }
316
+
317
+ const testRedo = () => {
318
+ if (pointAnnotation.value) {
319
+ pointAnnotation.value.redo()
320
+ }
321
+ }
322
+
323
+ const callSetTool = () => {
324
+ if (pointAnnotation.value) {
325
+ pointAnnotation.value.setTool(currentTool.value)
326
+ currentToolDisplay.value = currentTool.value
327
+ }
328
+ }
329
+
330
+ const testAddPoint = () => {
331
+ if (pointAnnotation.value) {
332
+ const id = pointAnnotation.value.createPointAnnotation(100, 100)
333
+ if (id) {
334
+ lastAddedPointId.value = id
335
+ alert(`Point added with id: ${id}`)
336
+ }
337
+ }
338
+ }
339
+
340
+ const testRemovePoint = () => {
341
+ if (pointAnnotation.value && lastAddedPointId.value) {
342
+ const success = pointAnnotation.value.removePointAnnotation(lastAddedPointId.value)
343
+ if (success) {
344
+ lastAddedPointId.value = null
345
+ alert('Point removed')
346
+ }
347
+ } else {
348
+ const points = pointAnnotation.value?.getPointAnnotations()
349
+ if (points && points.length > 0) {
350
+ const lastPoint = points[points.length - 1]
351
+ const success = pointAnnotation.value?.removePointAnnotation(lastPoint.id)
352
+ if (success) {
353
+ alert('Last point removed')
354
+ }
355
+ } else {
356
+ alert('No points to remove')
357
+ }
358
+ }
359
+ }
360
+ </script>
361
+
362
+ <style scoped>
363
+ .app {
364
+ max-width: 1200px;
365
+ margin: 0 auto;
366
+ padding: 20px;
367
+ font-family: Arial, sans-serif;
368
+ }
369
+
370
+ h1 {
371
+ text-align: center;
372
+ margin-bottom: 30px;
373
+ }
374
+
375
+ .editor-container {
376
+ width: 100%;
377
+ height: 600px;
378
+ border: 1px solid #ddd;
379
+ border-radius: 8px;
380
+ overflow: hidden;
381
+ margin-bottom: 30px;
382
+ }
383
+
384
+ .controls {
385
+ margin-bottom: 30px;
386
+ padding: 20px;
387
+ background-color: #f5f5f5;
388
+ border-radius: 8px;
389
+ }
390
+
391
+ .control-group {
392
+ margin-bottom: 15px;
393
+ }
394
+
395
+ label {
396
+ display: block;
397
+ margin-bottom: 5px;
398
+ font-weight: bold;
399
+ }
400
+
401
+ input {
402
+ width: 100%;
403
+ padding: 8px;
404
+ border: 1px solid #ddd;
405
+ border-radius: 4px;
406
+ margin-bottom: 10px;
407
+ }
408
+
409
+ .mask-options {
410
+ display: flex;
411
+ gap: 15px;
412
+ margin-bottom: 10px;
413
+ }
414
+
415
+ .mask-options label {
416
+ display: flex;
417
+ align-items: center;
418
+ gap: 5px;
419
+ font-weight: normal;
420
+ }
421
+
422
+ .mask-options select {
423
+ padding: 4px 8px;
424
+ border: 1px solid #ddd;
425
+ border-radius: 4px;
426
+ }
427
+
428
+ button {
429
+ padding: 8px 16px;
430
+ background-color: #007bff;
431
+ color: white;
432
+ border: none;
433
+ border-radius: 4px;
434
+ cursor: pointer;
435
+ margin-right: 10px;
436
+ }
437
+
438
+ button:hover {
439
+ background-color: #0069d9;
440
+ }
441
+
442
+ .output {
443
+ margin-bottom: 30px;
444
+ padding: 20px;
445
+ background-color: #f5f5f5;
446
+ border-radius: 8px;
447
+ }
448
+
449
+ pre {
450
+ white-space: pre-wrap;
451
+ word-wrap: break-word;
452
+ background-color: white;
453
+ padding: 15px;
454
+ border-radius: 4px;
455
+ max-height: 300px;
456
+ overflow-y: auto;
457
+ }
458
+
459
+ .status {
460
+ padding: 20px;
461
+ background-color: #f5f5f5;
462
+ border-radius: 8px;
463
+ }
464
+ </style>
@@ -0,0 +1,190 @@
1
+ <template>
2
+ <Transition name="fade">
3
+ <div v-if="visible" class="brush-size-slider" :style="positionStyle">
4
+ <div class="slider-header">
5
+ <span class="title">笔刷大小</span>
6
+ <button class="close-btn" @click="$emit('close')">×</button>
7
+ </div>
8
+ <div class="slider-body">
9
+ <div class="preview-container">
10
+ <div class="preview-circle" :style="previewStyle"></div>
11
+ </div>
12
+ <input
13
+ type="range"
14
+ :min="min"
15
+ :max="max"
16
+ :value="modelValue"
17
+ @input="handleInput"
18
+ class="range-slider"
19
+ />
20
+ <div class="size-display">
21
+ <span class="size-value">{{ modelValue }}px</span>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </Transition>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed } from 'vue';
30
+
31
+ const props = defineProps<{
32
+ visible: boolean;
33
+ modelValue: number;
34
+ min: number;
35
+ max: number;
36
+ position?: { x: number; y: number };
37
+ color?: string;
38
+ }>();
39
+
40
+ const emit = defineEmits<{
41
+ 'update:modelValue': [value: number];
42
+ 'close': [];
43
+ }>();
44
+
45
+ const positionStyle = computed(() => ({
46
+ left: `${props.position?.x || 50}px`,
47
+ top: `${props.position?.y || 50}px`,
48
+ }));
49
+
50
+ const previewStyle = computed(() => ({
51
+ width: `${props.modelValue}px`,
52
+ height: `${props.modelValue}px`,
53
+ backgroundColor: props.color || '#000000',
54
+ }));
55
+
56
+ const handleInput = (event: Event) => {
57
+ const target = event.target as HTMLInputElement;
58
+ emit('update:modelValue', Number(target.value));
59
+ };
60
+ </script>
61
+
62
+ <style scoped>
63
+ .brush-size-slider {
64
+ position: fixed;
65
+ background: #ffffff;
66
+ border: 1px solid #e8e8e8;
67
+ border-radius: 8px;
68
+ padding: 12px;
69
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
70
+ z-index: 1000;
71
+ min-width: 220px;
72
+ user-select: none;
73
+ }
74
+
75
+ .slider-header {
76
+ display: flex;
77
+ justify-content: space-between;
78
+ align-items: center;
79
+ margin-bottom: 12px;
80
+ padding-bottom: 8px;
81
+ border-bottom: 1px solid #f0f0f0;
82
+ }
83
+
84
+ .title {
85
+ font-size: 14px;
86
+ font-weight: 500;
87
+ color: #333333;
88
+ }
89
+
90
+ .close-btn {
91
+ width: 24px;
92
+ height: 24px;
93
+ border: none;
94
+ background: #f5f5f5;
95
+ border-radius: 50%;
96
+ cursor: pointer;
97
+ font-size: 16px;
98
+ color: #666666;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ transition: background 0.2s;
103
+ }
104
+
105
+ .close-btn:hover {
106
+ background: #e8e8e8;
107
+ }
108
+
109
+ .slider-body {
110
+ display: flex;
111
+ flex-direction: column;
112
+ align-items: center;
113
+ gap: 12px;
114
+ }
115
+
116
+ .preview-container {
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ width: 60px;
121
+ height: 60px;
122
+ }
123
+
124
+ .preview-circle {
125
+ border-radius: 50%;
126
+ opacity: 0.8;
127
+ transition: width 0.1s, height 0.1s;
128
+ }
129
+
130
+ .range-slider {
131
+ width: 100%;
132
+ height: 6px;
133
+ -webkit-appearance: none;
134
+ appearance: none;
135
+ background: #f0f0f0;
136
+ border-radius: 3px;
137
+ outline: none;
138
+ }
139
+
140
+ .range-slider::-webkit-slider-thumb {
141
+ -webkit-appearance: none;
142
+ appearance: none;
143
+ width: 18px;
144
+ height: 18px;
145
+ background: #1890ff;
146
+ border-radius: 50%;
147
+ cursor: pointer;
148
+ box-shadow: 0 2px 4px rgba(24, 144, 255, 0.3);
149
+ transition: transform 0.2s;
150
+ }
151
+
152
+ .range-slider::-webkit-slider-thumb:hover {
153
+ transform: scale(1.1);
154
+ }
155
+
156
+ .range-slider::-moz-range-thumb {
157
+ width: 18px;
158
+ height: 18px;
159
+ background: #1890ff;
160
+ border-radius: 50%;
161
+ cursor: pointer;
162
+ border: none;
163
+ box-shadow: 0 2px 4px rgba(24, 144, 255, 0.3);
164
+ }
165
+
166
+ .size-display {
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ }
171
+
172
+ .size-value {
173
+ font-size: 14px;
174
+ font-weight: 500;
175
+ color: #666666;
176
+ min-width: 50px;
177
+ text-align: center;
178
+ }
179
+
180
+ .fade-enter-active,
181
+ .fade-leave-active {
182
+ transition: opacity 0.2s ease, transform 0.2s ease;
183
+ }
184
+
185
+ .fade-enter-from,
186
+ .fade-leave-to {
187
+ opacity: 0;
188
+ transform: translateY(-8px);
189
+ }
190
+ </style>