@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.md +493 -250
- package/README_EN.md +498 -250
- package/docs/assets/index-BcqmlFff.js +1 -0
- package/docs/assets/{index-Dqqq7qvI.css → index-dq8tjOSG.css} +1 -1
- package/docs/index.html +2 -2
- package/package.json +2 -1
- package/project-docs/ARCHITECTURE.md +345 -354
- package/project-docs/REQUIREMENTS.md +232 -500
- package/skills/project-context.md +402 -0
- package/src/App.vue +502 -17
- package/src/components/PointAnnotation.vue +639 -219
- package/src/elements/PointAnnotationElement.ts +115 -17
- package/src/types/index.ts +22 -5
- package/src/utils/CanvasBrush.ts +20 -0
- package/docs/assets/index-CPn8AE3g.js +0 -1
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- 🎨 **
|
|
12
|
-
-
|
|
13
|
-
- 📤 **Multi-format Export** - JSON/COCO/YOLO/Mask
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
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
|
-
|
|
72
|
+
> ⚠️ **Note**: `vue@^3.3.0` is a peer dependency (not automatically installed, must exist in the host project).
|
|
40
73
|
|
|
41
|
-
|
|
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
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
68
|
-
url: 'https://example.com/image.jpg'
|
|
105
|
+
url: 'https://example.com/sample.jpg'
|
|
69
106
|
}))
|
|
70
107
|
|
|
71
|
-
|
|
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: '#
|
|
118
|
+
color: '#1890ff',
|
|
79
119
|
opacity: 0.55,
|
|
80
120
|
size: 100
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
maskExportForeground: 'black'
|
|
84
|
-
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
85
123
|
|
|
86
|
-
|
|
124
|
+
function handlePointChange(points: any[]) {
|
|
87
125
|
console.log('Points changed:', points)
|
|
88
126
|
}
|
|
89
127
|
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Refer to [`src/types/index.ts`](src/types/index.ts) for default values.
|
|
116
221
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
480
|
+
### Backend Upload Mask (Blob/File)
|
|
185
481
|
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
493
|
+
const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
|
|
194
494
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
507
|
+
### Point-Only Annotation (Disable Brush)
|
|
206
508
|
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
533
|
+
## Keyboard Shortcuts
|
|
320
534
|
|
|
321
|
-
|
|
535
|
+
> Effective when: **Canvas has focus** or **mouse hovers over canvas**
|
|
322
536
|
|
|
323
|
-
|
|
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
|
-
|
|
551
|
+
---
|
|
326
552
|
|
|
327
|
-
|
|
553
|
+
## Development & Build
|
|
328
554
|
|
|
329
|
-
|
|
555
|
+
```bash
|
|
556
|
+
# Install dependencies
|
|
557
|
+
pnpm install
|
|
330
558
|
|
|
331
|
-
|
|
559
|
+
# Local development (App.vue as demo entry)
|
|
560
|
+
pnpm dev
|
|
332
561
|
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
565
|
+
# Build demo site (docs/)
|
|
566
|
+
pnpm docs:build
|
|
339
567
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
- Safari 12+
|
|
343
|
-
- Edge 79+
|
|
568
|
+
# Build library + demo site simultaneously
|
|
569
|
+
pnpm build:all
|
|
344
570
|
|
|
345
|
-
|
|
571
|
+
# Type check
|
|
572
|
+
pnpm tsc --noEmit
|
|
573
|
+
```
|
|
346
574
|
|
|
347
|
-
|
|
348
|
-
- LeaferUI 2.0.8+
|
|
349
|
-
- Tinykeys 3.0.0+
|
|
350
|
-
- @zzalai/leafer-undo-redo 1.0.3+
|
|
575
|
+
### Project Structure
|
|
351
576
|
|
|
352
|
-
|
|
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
|
-
|
|
599
|
+
### Release Process
|
|
355
600
|
|
|
356
|
-
|
|
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
|
-
|
|
607
|
+
---
|
|
359
608
|
|
|
360
|
-
##
|
|
609
|
+
## License
|
|
361
610
|
|
|
362
|
-
|
|
363
|
-
- [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS undo/redo plugin
|
|
611
|
+
MIT © zzalai
|