@zzalai/leafer-point-annotation 1.1.1 → 1.1.3

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_EN.md CHANGED
@@ -2,19 +2,52 @@
2
2
 
3
3
  English | [中文](README.md)
4
4
 
5
- A point annotation and brush painting tool based on Vue3 + LeaferJS, supporting COCO/YOLO/JSON export, designed for AI model training dataset annotation.
6
-
7
- ## Features
8
-
9
- - 📍 **Point Annotation** - Add editable points on images
10
- - 🖌️ **Brush Painting** - Freehand painting with eraser support
11
- - 🎨 **Custom Styles** - Configurable brush color, size, and opacity
12
- - 🔄 **Undo/Redo** - Complete history management
13
- - 📤 **Multi-format Export** - JSON/COCO/YOLO/Mask Image
14
- - 🔍 **Canvas Zoom** - Zoom, pan, reset support
15
- - ⌨️ **Hotkeys** - V/P/B/E/Ctrl+Z/Ctrl+Y and more
16
- - 📱 **Responsive Design** - Vue3 component architecture
17
- - 🖼️ **Local Upload** - Support local image upload and drag-and-drop
5
+ > A point annotation and brush painting tool based on **Vue 3 + LeaferJS**, supporting COCO/YOLO/JSON/Mask export, designed for AI model training dataset annotation.
6
+
7
+ - 📍 **Point Annotation** - Draggable, editable points with auto-renumbering and hover/selected states
8
+ - 🖌️ **Multi-layer Brush Painting** - Painting and erasing with adjustable color, opacity, size, and continuity
9
+ - 🔀 **Brush Polygon from Points** - One-click generation of brush polygon area from point annotation trajectory
10
+ - 🖼️ **Local Image Upload** - Click to select or drag-and-drop local images, or use remote image URLs
11
+ - 🎨 **Complete Style Customization** - Full point and brush style configuration
12
+ - ⬅️ **Undo / Redo** - Complete command-based history management
13
+ - 📤 **Multi-format Export** - JSON / COCO / YOLO / Mask (dataURL / Blob / File)
14
+ - 📱 **Custom Toolbar Support** - Hide built-in toolbar and build custom UI via ref API
15
+ - 🔒 **Brush Disabling** - Use `enableBrush: false` to disable all brush functionality for point-only annotation
16
+ - ⌨️ **Rich Keyboard Shortcuts** - `v`, `p`, `b`, `e`, `Ctrl+Z`, `Ctrl+Y`, `Delete`, and more
17
+
18
+ ---
19
+
20
+ ## Table of Contents
21
+
22
+ - [Installation](#installation)
23
+ - [Quick Start](#quick-start)
24
+ - [Props Configuration](#props-configuration)
25
+ - [imageSource](#imagesource)
26
+ - [options](#options)
27
+ - [currentLayer (v-model:currentLayer)](#currentlayer-v-modelcurrentlayer)
28
+ - [Events](#events)
29
+ - [Ref API (Parent Component Calls)](#ref-api-parent-component-calls)
30
+ - [Point Annotation](#point-annotation)
31
+ - [Image & Canvas](#image--canvas)
32
+ - [Tool Switching](#tool-switching)
33
+ - [Delete & Clear](#delete--clear)
34
+ - [Brush Layers](#brush-layers)
35
+ - [Brush Style](#brush-style)
36
+ - [Point Trajectory to Brush Area](#point-trajectory-to-brush-area)
37
+ - [Zoom](#zoom)
38
+ - [Undo / Redo](#undo--redo)
39
+ - [Import / Export](#import--export)
40
+ - [Usage Examples](#usage-examples)
41
+ - [Minimal Example](#minimal-example)
42
+ - [Full Customization (Hide Built-in Toolbar)](#full-customization-hide-built-in-toolbar)
43
+ - [Multi-layer Brush](#multi-layer-brush)
44
+ - [Backend Upload Mask (Blob/File)](#backend-upload-mask-blobfile)
45
+ - [Point-Only Annotation (Disable Brush)](#point-only-annotation-disable-brush)
46
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
47
+ - [Development & Build](#development--build)
48
+ - [License](#license)
49
+
50
+ ---
18
51
 
19
52
  ## Installation
20
53
 
@@ -36,112 +69,330 @@ yarn add @zzalai/leafer-point-annotation
36
69
  pnpm add @zzalai/leafer-point-annotation
37
70
  ```
38
71
 
39
- ## Quick Start
72
+ > ⚠️ **Note**: `vue@^3.3.0` is a peer dependency (not automatically installed, must exist in the host project).
40
73
 
41
- ### Using Remote Image
74
+ > ⚠️ **Important**: You must manually import the CSS:
75
+ > ```ts
76
+ > import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
77
+ > ```
78
+
79
+ ---
80
+
81
+ ## Quick Start
42
82
 
43
83
  ```vue
44
84
  <template>
45
- <div class="demo-container">
46
- <PointAnnotation
47
- ref="annotationRef"
48
- :imageSource="imageSource"
49
- :options="options"
50
- @pointChange="handlePointChange"
51
- @loadSuccess="handleLoadSuccess"
52
- />
53
- <div class="controls">
54
- <button @click="exportJSON">Export JSON</button>
55
- <button @click="exportMask">Export Mask</button>
56
- </div>
57
- </div>
85
+ <PointAnnotation
86
+ ref="annotationRef"
87
+ :image-source="imageSource"
88
+ :options="options"
89
+ @point-change="handlePointChange"
90
+ @load-success="handleLoadSuccess"
91
+ />
58
92
  </template>
59
93
 
60
94
  <script setup lang="ts">
61
95
  import { ref, computed } from 'vue'
62
96
  import { PointAnnotation } from '@zzalai/leafer-point-annotation'
97
+
98
+ // 👇 Must manually import styles
63
99
  import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
64
100
 
65
101
  const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
102
+
103
+ // Method 1: Remote image
66
104
  const imageSource = computed(() => ({
67
- id: 'demo-image',
68
- url: 'https://example.com/image.jpg'
105
+ url: 'https://example.com/sample.jpg'
69
106
  }))
70
107
 
71
- const options = ref({
108
+ // Method 2: Don't pass imageSource - user can upload locally
109
+ // const imageSource = null
110
+
111
+ const options = {
112
+ enableBrush: true,
72
113
  pointStyle: {
73
114
  circleFill: '#ff4d4f',
74
- circleStroke: '#ffffff',
75
- labelBackgroundColor: '#ffffff'
115
+ circleStroke: '#ffffff'
76
116
  },
77
117
  brushStyle: {
78
- color: '#ff4d4f',
118
+ color: '#1890ff',
79
119
  opacity: 0.55,
80
120
  size: 100
81
- },
82
- maskExportFormat: 'png',
83
- maskExportForeground: 'black'
84
- })
121
+ }
122
+ }
85
123
 
86
- const handlePointChange = (points) => {
124
+ function handlePointChange(points: any[]) {
87
125
  console.log('Points changed:', points)
88
126
  }
89
127
 
90
- const handleLoadSuccess = () => {
91
- console.log('Image loaded successfully')
128
+ function handleLoadSuccess(info: any) {
129
+ console.log('Image loaded successfully:', info)
92
130
  }
131
+ </script>
132
+ ```
93
133
 
94
- const exportJSON = () => {
95
- const json = annotationRef.value?.exportCanvasJSON()
96
- if (json) {
97
- const blob = new Blob([json], { type: 'application/json' })
98
- const url = URL.createObjectURL(blob)
99
- const a = document.createElement('a')
100
- a.href = url
101
- a.download = 'annotation.json'
102
- a.click()
103
- }
134
+ ---
135
+
136
+ ## Props Configuration
137
+
138
+ ### imageSource
139
+
140
+ | Field | Type | Description |
141
+ |-------|------|-------------|
142
+ | `url` | `string` | Image URL (remote URL or dataURL) |
143
+ | `id` | `string` | Optional, business identifier |
144
+
145
+ > When `imageSource` is not provided, the component displays a large upload area supporting **click to select** and **drag-and-drop** file loading.
146
+
147
+ ### options
148
+
149
+ Complete `OptionsSource` interface:
150
+
151
+ ```ts
152
+ interface OptionsSource {
153
+ // ============ Feature Toggles ============
154
+ enableBrush?: boolean // Whether to enable brush (default: true);
155
+ // When false, brush buttons/panel are not rendered,
156
+ // and brushTool/eraserTool/mask export methods are disabled
157
+
158
+ // ============ UI Toggles ============
159
+ showToolbar?: boolean // Whether to show built-in toolbar (default: true)
160
+ showZoomController?: boolean // Whether to show built-in zoom controller (default: true)
161
+ canvasBackground?: string // Canvas background color (default: '#f6f6f6')
162
+
163
+ // ============ Zoom ============
164
+ zoomMin?: number // Minimum zoom ratio (default: 0.2)
165
+ zoomMax?: number // Maximum zoom ratio (default: 4)
166
+
167
+ // ============ Point Annotation ============
168
+ pointStyle?: Partial<PointStyle> // Point annotation style (overrides defaults)
169
+ maxPoints?: number // Maximum number of points (optional)
170
+
171
+ // ============ Brush ============
172
+ brushStyle?: Partial<BrushStyle> // Brush style (overrides defaults)
173
+ brushLayers?: BrushLayerConfig[] // Brush layer configuration (defaults to single layer if not provided)
174
+ maxBrushLayers?: number // Maximum number of layers (optional)
175
+
176
+ // ============ History ============
177
+ maxUndoSteps?: number // Maximum undo steps (default: 100)
178
+
179
+ // ============ Mask Export ============
180
+ maskExportFormat?: 'png' | 'jpeg' | 'jpg' // Default mask export format (default: png)
181
+ maskExportForeground?: 'black' | 'white' // Default mask foreground color (default: black)
104
182
  }
183
+ ```
105
184
 
106
- const exportMask = async () => {
107
- const mask = await annotationRef.value?.exportMaskImage('png', 'black')
108
- if (mask) {
109
- const a = document.createElement('a')
110
- a.href = mask
111
- a.download = 'mask.png'
112
- a.click()
113
- }
185
+ #### PointStyle (Point Annotation Style)
186
+
187
+ ```ts
188
+ interface PointStyle {
189
+ circleRadius: number
190
+ circleFill: string
191
+ circleStroke: string
192
+ circleStrokeWidth: number
193
+
194
+ // hover
195
+ hoverCircleFill: string
196
+ hoverCircleStroke: string
197
+
198
+ // selected
199
+ selectedCircleFill: string
200
+ selectedCircleStroke: string
201
+ selectedCircleScale: number
202
+
203
+ // text
204
+ circleTextFontSize: number
205
+ circleTextFontFamily: string
206
+ circleTextFill: string
207
+
208
+ // label
209
+ labelBackgroundColor: string
210
+ labelTextColor: string
211
+ labelFontSize: number
212
+ labelPadding: number | number[]
213
+
214
+ // fixed size
215
+ fixedSizeOnZoom?: boolean // When enabled, points don't grow with canvas zoom
216
+ fixedSizeScale?: number // Fixed size scale factor
114
217
  }
115
- </script>
218
+ ```
219
+
220
+ Refer to [`src/types/index.ts`](src/types/index.ts) for default values.
116
221
 
117
- <style scoped>
118
- .demo-container {
119
- width: 100%;
120
- height: 600px;
222
+ #### BrushStyle (Brush Style)
223
+
224
+ ```ts
225
+ interface BrushStyle {
226
+ color: string // Brush color (hex)
227
+ opacity: number // Opacity 0~1 (controlled via Group.opacity)
228
+ size: number // Brush size (pixels)
229
+ minSize: number // Slider minimum
230
+ maxSize: number // Slider maximum
231
+ continuity: number // Maximum distance threshold for continuous strokes
121
232
  }
233
+ ```
234
+
235
+ #### BrushLayerConfig (Multi-layer Configuration)
122
236
 
123
- .controls {
124
- margin-top: 16px;
125
- display: flex;
126
- gap: 12px;
237
+ ```ts
238
+ interface BrushLayerConfig {
239
+ label: string // Layer display name
240
+ value: string // Layer unique identifier
241
+ color?: string // Default color for this layer
242
+ opacity?: number // Default opacity for this layer
243
+ size?: number // Default brush size for this layer
127
244
  }
128
- </style>
129
245
  ```
130
246
 
131
- ### Using Local Image Upload
247
+ When `brushLayers` is not provided, defaults to a single layer `{label:'Default Layer', value:'default'}`.
248
+
249
+ ### currentLayer (v-model:currentLayer)
250
+
251
+ Controlled layer switching. For example:
252
+
253
+ ```vue
254
+ <PointAnnotation
255
+ :options="{ brushLayers: [
256
+ { label: 'Foreground', value: 'foreground' },
257
+ { label: 'Occlusion', value: 'occlusion' }
258
+ ]}"
259
+ v-model:current-layer="activeLayer"
260
+ />
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Events
266
+
267
+ | Event | Parameters | Triggered When |
268
+ |-------|-----------|----------------|
269
+ | `point-change` | `(points: PointAnnotation[])` | Point added / deleted / modified / renumbered |
270
+ | `load-start` | - | Image starts loading |
271
+ | `load-success` | `{ url, width, height }` | Image loads successfully |
272
+ | `load-error` | `{ error }` | Image loading fails |
273
+ | `undo-state-change` | `{ canUndo }` | Undo stack state changes |
274
+ | `redo-state-change` | `{ canRedo }` | Redo stack state changes |
275
+ | `update:currentLayer` | `layerValue` | Current brush layer changes (use with v-model) |
276
+ | `layer-change` | `layerValue` | Same as update:currentLayer |
277
+
278
+ ---
279
+
280
+ ## Ref API (Parent Component Calls)
281
+
282
+ After accessing the component instance via `ref`, you can call the following methods.
283
+
284
+ ```ts
285
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
286
+
287
+ // Example:
288
+ annotationRef.value?.pointTool() // Switch to point annotation tool
289
+ annotationRef.value?.createBrushFromPoints() // Generate brush area from point trajectory
290
+ annotationRef.value?.getMaskBlob() // Export current layer as Blob (for backend upload)
291
+ ```
292
+
293
+ ### Point Annotation
294
+
295
+ | Method | Description |
296
+ |--------|-------------|
297
+ | `getPointAnnotations(): PointAnnotation[]` | Get all current points |
298
+ | `createPointAnnotation(x: number, y: number, label?: string): boolean` | Programmatically add a point |
299
+ | `removePointAnnotation(id: string): boolean` | Programmatically delete a point |
300
+ | `updatePointAnnotationLabel(id: string, label: string): boolean` | Modify a point's label text |
301
+
302
+ ### Image & Canvas
303
+
304
+ | Method | Description |
305
+ |--------|-------------|
306
+ | `getImageInfo()` | `{ url, width, height }` |
307
+ | `loadImage(url: string)` | Dynamically load a new image |
308
+
309
+ ### Tool Switching
310
+
311
+ | Method | Description |
312
+ |--------|-------------|
313
+ | `getCurrentTool(): 'select' \| 'point' \| 'brush' \| 'eraser'` | - |
314
+ | `setTool(tool)` | Switch to specified tool (brush/eraser blocked when `enableBrush=false`) |
315
+ | `selectTool()` | Select tool |
316
+ | `pointTool()` | Point annotation tool |
317
+ | `brushTool(openPanel?: boolean)` | Brush tool |
318
+ | `eraserTool()` | Eraser tool |
319
+
320
+ ### Delete & Clear
321
+
322
+ | Method | Description |
323
+ |--------|-------------|
324
+ | `deleteSelected()` | Delete currently selected point (with confirm dialog) |
325
+ | `clearAllAnnotationsAndBrush()` | Clear all points + brush (with confirm dialog) |
326
+ | `clearBrush()` | Clear current layer's brush |
327
+ | `clearAllBrushLayers()` | Clear all layers' brush |
328
+
329
+ ### Brush Layers
330
+
331
+ | Method | Description |
332
+ |--------|-------------|
333
+ | `getCurrentLayer(): string` | Current active layer value |
334
+ | `setActiveLayer(value: string): boolean` | Switch to specified layer |
335
+ | `getAllLayers(): BrushLayerConfig[]` | All layer configurations |
336
+
337
+ ### Brush Style
338
+
339
+ | Method | Description |
340
+ |--------|-------------|
341
+ | `getBrushStyle(): BrushStyle` | Returns copy of current style |
342
+ | `updateBrushStyle(partial: Partial<BrushStyle>): void` | Dynamically update (e.g., color/opacity/size) |
343
+
344
+ ### Point Trajectory to Brush Area
345
+
346
+ | Method | Description |
347
+ |--------|-------------|
348
+ | `createBrushFromPoints(): boolean` | Connects pixel coordinates of points in `sequenceNumber` order to form closed polygon, fills with current brush style; no action when point count < 3 |
349
+
350
+ ### Zoom
351
+
352
+ | Method | Description |
353
+ |--------|-------------|
354
+ | `zoomIn()` | Zoom in |
355
+ | `zoomOut()` | Zoom out |
356
+ | `resetZoom()` | Reset to 100% |
132
357
 
133
- When not providing the `imageSource` prop, the local upload interface will be displayed, supporting click to select or drag and drop.
358
+ ### Undo / Redo
359
+
360
+ | Method | Description |
361
+ |--------|-------------|
362
+ | `undo()` | Undo last action |
363
+ | `redo()` | Redo last action |
364
+
365
+ ### Import / Export
366
+
367
+ | Method | Description |
368
+ |--------|-------------|
369
+ | `exportCanvasJSON(): string` | Full export (points + brush snapshot + image info) |
370
+ | `importCanvasJSON(data: string \| object): boolean` | Restore canvas from exported JSON |
371
+ | `exportMaskImage(format?, fg?)`: `Promise<string \| null>` | Current layer mask (dataURL) |
372
+ | `exportMaskImageByLayer(layerValue, format?, fg?)`: `Promise<string \| null>` | Specified layer mask |
373
+ | `exportAllMaskImages(format?, fg?)`: `Promise<Record<string, string>>` | All layer masks |
374
+ | `getMaskBlob(layerValue?, format?, fg?)`: `Promise<Blob \| null>` | Current/specified layer Blob (for backend upload) |
375
+ | `getMaskFile(layerValue?, filename?, format?, fg?)`: `Promise<File \| null>` | Current/specified layer File |
376
+ | `getAllMaskBlobs(format?, fg?)`: `Promise<Record<string, Blob>>` | All layer Blob collection |
377
+ | `exportCOCO(): string` | Export COCO JSON (points = keypoints) |
378
+ | `exportYOLO(): string` | Export YOLO annotations |
379
+
380
+ > Parameter notes: `format` = `'png' \| 'jpeg' \| 'jpg'`; `fg` = `'black' \| 'white'` (mask foreground color).
381
+ > Note: All Mask/Blob/File related methods require browser environment, and return null/{} when `enableBrush=false`.
382
+
383
+ ---
384
+
385
+ ## Usage Examples
386
+
387
+ ### Minimal Example
134
388
 
135
389
  ```vue
136
390
  <template>
137
- <div class="demo-container">
138
- <PointAnnotation
139
- ref="annotationRef"
140
- :options="options"
141
- @pointChange="handlePointChange"
142
- @loadSuccess="handleLoadSuccess"
143
- />
144
- </div>
391
+ <PointAnnotation
392
+ ref="annotationRef"
393
+ :image-source="{ url: 'https://example.com/image.jpg' }"
394
+ :options="{ enableBrush: false }"
395
+ />
145
396
  </template>
146
397
 
147
398
  <script setup lang="ts">
@@ -150,214 +401,211 @@ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
150
401
  import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
151
402
 
152
403
  const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
404
+ </script>
405
+ ```
153
406
 
154
- const options = ref({
155
- pointStyle: {
156
- circleFill: '#ff4d4f',
157
- circleStroke: '#ffffff',
158
- labelBackgroundColor: '#ffffff'
159
- },
160
- brushStyle: {
161
- color: '#ff4d4f',
162
- opacity: 0.55,
163
- size: 100
164
- }
165
- })
407
+ ### Full Customization (Hide Built-in Toolbar)
166
408
 
167
- const handlePointChange = (points) => {
168
- console.log('Points changed:', points)
409
+ ```vue
410
+ <template>
411
+ <div class="my-toolbar">
412
+ <button @click="() => annotationRef.value?.pointTool()">Point</button>
413
+ <button @click="() => annotationRef.value?.brushTool()">Brush</button>
414
+ <button @click="() => annotationRef.value?.eraserTool()">Eraser</button>
415
+ <button @click="() => annotationRef.value?.deleteSelected()">Delete</button>
416
+ <button @click="() => annotationRef.value?.clearAllAnnotationsAndBrush()">Clear All</button>
417
+ <button @click="() => annotationRef.value?.undo()">Undo</button>
418
+ <button @click="() => annotationRef.value?.redo()">Redo</button>
419
+ <button @click="() => annotationRef.value?.createBrushFromPoints()">Points→Polygon</button>
420
+ <button @click="uploadMask">Upload Mask</button>
421
+ </div>
422
+
423
+ <PointAnnotation
424
+ ref="annotationRef"
425
+ :image-source="{ url: 'https://example.com/image.jpg' }"
426
+ :options="{ showToolbar: false, showZoomController: false }"
427
+ @point-change="handlePoints"
428
+ />
429
+ </template>
430
+
431
+ <script setup lang="ts">
432
+ import { ref } from 'vue'
433
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
434
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
435
+
436
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
437
+
438
+ function handlePoints(points: any[]) {
439
+ console.log('points:', points)
169
440
  }
170
441
 
171
- const handleLoadSuccess = () => {
172
- console.log('Image loaded successfully')
442
+ async function uploadMask() {
443
+ const blob = await annotationRef.value?.getMaskBlob()
444
+ if (!blob) return
445
+ const fd = new FormData()
446
+ fd.append('file', blob, 'mask.png')
447
+ await fetch('/api/upload', { method: 'POST', body: fd })
173
448
  }
174
449
  </script>
450
+ ```
175
451
 
176
- <style scoped>
177
- .demo-container {
178
- width: 100%;
179
- height: 600px;
180
- }
181
- </style>
452
+ ### Multi-layer Brush
453
+
454
+ ```vue
455
+ <template>
456
+ <PointAnnotation
457
+ ref="annotationRef"
458
+ :image-source="{ url: 'https://example.com/image.jpg' }"
459
+ :options="{
460
+ brushLayers: [
461
+ { label: 'Foreground', value: 'foreground', color: '#1890ff', opacity: 0.55 },
462
+ { label: 'Occlusion', value: 'occlusion', color: '#faad14', opacity: 0.55 },
463
+ { label: 'Background', value: 'background', color: '#52c41a', opacity: 0.55 }
464
+ ]
465
+ }"
466
+ v-model:current-layer="activeLayer"
467
+ />
468
+ </template>
469
+
470
+ <script setup lang="ts">
471
+ import { ref } from 'vue'
472
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
473
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
474
+
475
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
476
+ const activeLayer = ref('foreground')
477
+ </script>
182
478
  ```
183
479
 
184
- ## API Documentation
480
+ ### Backend Upload Mask (Blob/File)
185
481
 
186
- ### Props
482
+ ```vue
483
+ <template>
484
+ <PointAnnotation ref="annotationRef" :image-source="{ url: '...' }" />
485
+ <button @click="uploadAllMasks">Upload All Layer Masks</button>
486
+ </template>
187
487
 
188
- | Property | Type | Default | Description |
189
- |----------|------|---------|-------------|
190
- | imageSource | `{ id?: string; url: string }` | `null` | Image source configuration (optional, shows upload UI when not provided) |
191
- | options | `Object` | `{}` | Configuration options |
488
+ <script setup lang="ts">
489
+ import { ref } from 'vue'
490
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
491
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
192
492
 
193
- #### Options Configuration
493
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
194
494
 
195
- ```typescript
196
- interface Options {
197
- pointStyle?: Partial<PointStyle>
198
- brushStyle?: Partial<BrushStyle>
199
- maskExportFormat?: 'png' | 'jpg' | 'jpeg'
200
- maskExportForeground?: 'black' | 'white'
201
- maxUndoSteps?: number
495
+ async function uploadAllMasks() {
496
+ const blobs = await annotationRef.value?.getAllMaskBlobs('png', 'black')
497
+ if (!blobs) return
498
+ for (const [layerValue, blob] of Object.entries(blobs)) {
499
+ const fd = new FormData()
500
+ fd.append('file', blob, `${layerValue}.png`)
501
+ await fetch('/api/mask-upload', { method: 'POST', body: fd })
502
+ }
202
503
  }
504
+ </script>
203
505
  ```
204
506
 
205
- #### PointStyle Configuration
507
+ ### Point-Only Annotation (Disable Brush)
206
508
 
207
- ```typescript
208
- interface PointStyle {
209
- circleRadius: number
210
- circleFill: string
211
- circleStroke: string
212
- circleStrokeWidth: number
213
- hoverCircleFill: string
214
- hoverCircleStroke: string
215
- selectedCircleFill: string
216
- selectedCircleStroke: string
217
- selectedCircleScale: number
218
- labelBackgroundColor: string
219
- labelTextColor: string
220
- labelFontSize: number
221
- labelPadding: number[]
222
- fixedSizeOnZoom: boolean
223
- fixedSizeScale: number
224
- }
225
- ```
509
+ ```vue
510
+ <template>
511
+ <PointAnnotation
512
+ ref="annotationRef"
513
+ :image-source="{ url: '...' }"
514
+ :options="{ enableBrush: false }"
515
+ @point-change="handlePoints"
516
+ />
517
+ </template>
226
518
 
227
- #### BrushStyle Configuration
519
+ <script setup lang="ts">
520
+ import { ref } from 'vue'
521
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
522
+ import '@zzalai/leafer-point-annotation/dist/leafer-point-annotation.css'
228
523
 
229
- ```typescript
230
- interface BrushStyle {
231
- color: string
232
- opacity: number
233
- size: number
234
- minSize: number
235
- maxSize: number
236
- continuity: number
524
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
525
+ function handlePoints(points: any[]) {
526
+ console.log('Points:', points)
237
527
  }
528
+ </script>
238
529
  ```
239
530
 
240
- ### Events
241
-
242
- | Event Name | Parameters | Description |
243
- |------------|------------|-------------|
244
- | pointChange | `PointAnnotation[]` | Triggered when point annotations change |
245
- | loadStart | - | Triggered when image starts loading |
246
- | loadSuccess | - | Triggered when image loads successfully |
247
- | loadError | `error` | Triggered when image loading fails |
248
- | undoStateChange | - | Triggered when undo state changes |
249
- | redoStateChange | - | Triggered when redo state changes |
250
-
251
- ### Methods
252
-
253
- The component exposes the following methods:
254
-
255
- | Method Name | Parameters | Return Value | Description |
256
- |-------------|------------|--------------|-------------|
257
- | getPointAnnotations | - | `PointAnnotation[]` | Get all point annotation data |
258
- | getImageInfo | - | `Object` | Get image information |
259
- | exportCanvasJSON | - | `string` | Export complete JSON data |
260
- | exportMaskImage | `format?`, `fgColor?` | `Promise<string|null>` | Export mask image |
261
- | exportCOCO | - | `string` | Export COCO format JSON |
262
- | exportYOLO | - | `{ annotations: string; classNames: string }` | Export YOLO format |
263
- | importCanvasJSON | `jsonString`, `options?` | `Promise<boolean>` | Import JSON data |
264
- | loadImage | `url?` | `Promise<void>` | Load image |
265
- | clearBrush | - | `void` | Clear brush content |
266
- | zoomIn | - | `void` | Zoom in canvas |
267
- | zoomOut | - | `void` | Zoom out canvas |
268
- | resetZoom | - | `void` | Reset zoom |
269
- | undo | - | `void` | Undo operation |
270
- | redo | - | `void` | Redo operation |
271
- | getCurrentTool | - | `'select'\|'point'\|'brush'\|'eraser'` | Get current tool |
272
- | setTool | `tool` | `void` | Set current tool |
273
- | createPointAnnotation | `x`, `y` | `string\|null` | Create point annotation |
274
- | removePointAnnotation | `id` | `boolean` | Remove specific point annotation |
275
-
276
- ## Hotkeys
277
-
278
- | Hotkey | Function |
279
- |--------|----------|
280
- | V | Select tool |
281
- | P | Point annotation tool |
282
- | B | Brush tool |
283
- | E | Eraser tool |
284
- | Ctrl + Z | Undo |
285
- | Ctrl + Y | Redo |
286
- | Delete | Delete selected / Clear all |
287
- | Ctrl + + | Zoom in |
288
- | Ctrl + - | Zoom out |
289
- | Ctrl + 0 | Reset zoom |
290
- | Alt | Show/Hide hotkey hints |
291
-
292
- ## Export Formats
293
-
294
- ### JSON Full
295
-
296
- Includes complete annotation data and brush mask.
297
-
298
- ```json
299
- {
300
- "version": "1.0",
301
- "imageUrl": "https://example.com/image.jpg",
302
- "imageWidth": 1280,
303
- "imageHeight": 720,
304
- "pointAnnotations": [
305
- {
306
- "id": "point_xxx",
307
- "pixel": { "x": 100, "y": 200 },
308
- "normalized": { "x": 0.078, "y": 0.278 },
309
- "label": "#1",
310
- "createdAt": 1716960000000,
311
- "updatedAt": 1716960000000
312
- }
313
- ],
314
- "brushMask": "data:image/png;base64,...",
315
- "exportTime": 1716960000000
316
- }
317
- ```
531
+ ---
318
532
 
319
- ### COCO
533
+ ## Keyboard Shortcuts
320
534
 
321
- For keypoint detection tasks.
535
+ > Effective when: **Canvas has focus** or **mouse hovers over canvas**
322
536
 
323
- ### YOLO
537
+ | Key | Function | Restriction |
538
+ |-----|----------|-------------|
539
+ | `v` | Select tool | - |
540
+ | `p` | Point annotation tool | - |
541
+ | `b` | Brush tool | Requires `enableBrush=true` |
542
+ | `e` | Eraser tool | Requires `enableBrush=true` |
543
+ | `Ctrl + Z` | Undo | - |
544
+ | `Ctrl + Y` | Redo | - |
545
+ | `Delete` | Delete selected point | - |
546
+ | `Ctrl + +` | Zoom in | - |
547
+ | `Ctrl + -` | Zoom out | - |
548
+ | `Ctrl + 0` | Reset zoom | - |
549
+ | `Alt` | Show/Hide shortcut hint overlay | - |
324
550
 
325
- For YOLO series model training.
551
+ ---
326
552
 
327
- ### Mask Image
553
+ ## Development & Build
328
554
 
329
- PNG/JPG format binary image, foreground in black/white, background transparent/white.
555
+ ```bash
556
+ # Install dependencies
557
+ pnpm install
330
558
 
331
- ## Project Documentation
559
+ # Local development (App.vue as demo entry)
560
+ pnpm dev
332
561
 
333
- - [Requirements](./project-docs/REQUIREMENTS.md) - Detailed functional requirements
334
- - [Architecture](./project-docs/ARCHITECTURE.md) - System architecture design
335
- - [Implementation Plan](./project-docs/IMPLEMENTATION_PLAN.md) - Development task planning
336
- - [Development Guide](./project-docs/leafer-development-guide/LEAFER_DEVELOPMENT_GUIDE.md) - LeaferJS development practical guide
562
+ # Build library output (dist/)
563
+ pnpm build
337
564
 
338
- ## Browser Support
565
+ # Build demo site (docs/)
566
+ pnpm docs:build
339
567
 
340
- - Chrome 60+
341
- - Firefox 55+
342
- - Safari 12+
343
- - Edge 79+
568
+ # Build library + demo site simultaneously
569
+ pnpm build:all
344
570
 
345
- ## Dependencies
571
+ # Type check
572
+ pnpm tsc --noEmit
573
+ ```
346
574
 
347
- - Vue 3.3.0+
348
- - LeaferUI 2.0.8+
349
- - Tinykeys 3.0.0+
350
- - @zzalai/leafer-undo-redo 1.0.3+
575
+ ### Project Structure
351
576
 
352
- ## License
577
+ ```
578
+ src/
579
+ ├── components/
580
+ │ ├── PointAnnotation.vue # Core main component (integrates all capabilities)
581
+ │ ├── BrushSizeSlider.vue # Brush size slider
582
+ │ └── BrushStylePanel.vue # Brush style configuration panel
583
+ ├── elements/
584
+ │ └── PointAnnotationElement.ts # Custom point element (Group + Ellipse + Text)
585
+ ├── utils/
586
+ │ ├── CanvasBrush.ts # Brush core (canvas + drawing snapshot)
587
+ │ ├── BrushCommands.ts # Brush undo commands
588
+ │ ├── PointCommands.ts # Point undo commands
589
+ │ ├── BrushStroke.ts # Brush stroke data
590
+ │ ├── COCOExporter.ts # COCO export
591
+ │ └── YOLOExporter.ts # YOLO export
592
+ ├── types/
593
+ │ └── index.ts # All public types and default values
594
+ ├── App.vue # Dev demo page
595
+ ├── index.ts # Public export
596
+ └── main.ts # Dev entry
597
+ ```
353
598
 
354
- MIT License
599
+ ### Release Process
355
600
 
356
- ## Contributing
601
+ 1. `pnpm install` → `pnpm build:all`
602
+ 2. Confirm `dist/` and `docs/` are up to date
603
+ 3. Update `version` in `package.json`
604
+ 4. `npm publish`
605
+ 5. `git push` to GitHub (triggers Pages redeployment)
357
606
 
358
- Issues and Pull Requests are welcome!
607
+ ---
359
608
 
360
- ## Related Projects
609
+ ## License
361
610
 
362
- - [@zzalai/leafer-multi-roi](https://github.com/otaku1951/leafer-multi-roi) - Multi-region ROI annotation tool
363
- - [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS undo/redo plugin
611
+ MIT © zzalai