@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 otaku
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # @zzalai/leafer-point-annotation
2
+
3
+ [English](README_EN.md) | 中文
4
+
5
+ 基于 Vue3 + LeaferJS 的点标注与笔刷涂抹工具,支持导出 COCO/YOLO/JSON 格式,专为 AI 模型训练数据集标注设计。
6
+
7
+ ## 功能特点
8
+
9
+ - 📍 **点标注** - 在图片上添加可编辑的标注点
10
+ - 🖌️ **笔刷涂抹** - 自由涂抹,支持擦除
11
+ - 🎨 **自定义样式** - 笔刷颜色、大小、透明度可配置
12
+ - 🔄 **撤销/重做** - 完整的历史记录管理
13
+ - 📤 **多格式导出** - JSON/COCO/YOLO/二值图
14
+ - 🔍 **画布缩放** - 支持缩放、平移、重置
15
+ - ⌨️ **热键支持** - V/P/B/E/Ctrl+Z/Ctrl+Y 等
16
+ - 📱 **响应式设计** - Vue3 组件化架构
17
+
18
+ ## 安装
19
+
20
+ ### npm
21
+
22
+ ```bash
23
+ npm install @zzalai/leafer-point-annotation
24
+ ```
25
+
26
+ ### yarn
27
+
28
+ ```bash
29
+ yarn add @zzalai/leafer-point-annotation
30
+ ```
31
+
32
+ ### pnpm
33
+
34
+ ```bash
35
+ pnpm add @zzalai/leafer-point-annotation
36
+ ```
37
+
38
+ ## 快速开始
39
+
40
+ ### 基础使用
41
+
42
+ ```vue
43
+ <template>
44
+ <div class="demo-container">
45
+ <PointAnnotation
46
+ ref="annotationRef"
47
+ :imageSource="imageSource"
48
+ :options="options"
49
+ @pointChange="handlePointChange"
50
+ @loadSuccess="handleLoadSuccess"
51
+ />
52
+ <div class="controls">
53
+ <button @click="exportJSON">导出 JSON</button>
54
+ <button @click="exportMask">导出 Mask</button>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { ref, computed } from 'vue'
61
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
62
+
63
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
64
+ const imageSource = computed(() => ({
65
+ id: 'demo-image',
66
+ url: 'https://example.com/image.jpg'
67
+ }))
68
+
69
+ const options = ref({
70
+ pointStyle: {
71
+ circleFill: '#ff4d4f',
72
+ circleStroke: '#ffffff',
73
+ labelBackgroundColor: '#ffffff'
74
+ },
75
+ brushStyle: {
76
+ color: '#ff4d4f',
77
+ opacity: 0.55,
78
+ size: 100
79
+ },
80
+ maskExportFormat: 'png',
81
+ maskExportForeground: 'black'
82
+ })
83
+
84
+ const handlePointChange = (points) => {
85
+ console.log('标注点变化:', points)
86
+ }
87
+
88
+ const handleLoadSuccess = () => {
89
+ console.log('图片加载成功')
90
+ }
91
+
92
+ const exportJSON = () => {
93
+ const json = annotationRef.value?.exportCanvasJSON()
94
+ if (json) {
95
+ const blob = new Blob([json], { type: 'application/json' })
96
+ const url = URL.createObjectURL(blob)
97
+ const a = document.createElement('a')
98
+ a.href = url
99
+ a.download = 'annotation.json'
100
+ a.click()
101
+ }
102
+ }
103
+
104
+ const exportMask = async () => {
105
+ const mask = await annotationRef.value?.exportMaskImage('png', 'black')
106
+ if (mask) {
107
+ const a = document.createElement('a')
108
+ a.href = mask
109
+ a.download = 'mask.png'
110
+ a.click()
111
+ }
112
+ }
113
+ </script>
114
+
115
+ <style scoped>
116
+ .demo-container {
117
+ width: 100%;
118
+ height: 600px;
119
+ }
120
+
121
+ .controls {
122
+ margin-top: 16px;
123
+ display: flex;
124
+ gap: 12px;
125
+ }
126
+ </style>
127
+ ```
128
+
129
+ ## API 文档
130
+
131
+ ### Props
132
+
133
+ | 属性名 | 类型 | 默认值 | 说明 |
134
+ |--------|------|--------|------|
135
+ | imageSource | `{ id?: string; url: string }` | 必填 | 图片源配置 |
136
+ | options | `Object` | `{}` | 配置选项 |
137
+
138
+ #### Options 配置
139
+
140
+ ```typescript
141
+ interface Options {
142
+ pointStyle?: Partial<PointStyle>
143
+ brushStyle?: Partial<BrushStyle>
144
+ maskExportFormat?: 'png' | 'jpg' | 'jpeg'
145
+ maskExportForeground?: 'black' | 'white'
146
+ maxUndoSteps?: number
147
+ }
148
+ ```
149
+
150
+ #### PointStyle 配置
151
+
152
+ ```typescript
153
+ interface PointStyle {
154
+ circleRadius: number
155
+ circleFill: string
156
+ circleStroke: string
157
+ circleStrokeWidth: number
158
+ hoverCircleFill: string
159
+ hoverCircleStroke: string
160
+ selectedCircleFill: string
161
+ selectedCircleStroke: string
162
+ selectedCircleScale: number
163
+ labelBackgroundColor: string
164
+ labelTextColor: string
165
+ labelFontSize: number
166
+ labelPadding: number[]
167
+ fixedSizeOnZoom: boolean
168
+ fixedSizeScale: number
169
+ }
170
+ ```
171
+
172
+ #### BrushStyle 配置
173
+
174
+ ```typescript
175
+ interface BrushStyle {
176
+ color: string
177
+ opacity: number
178
+ size: number
179
+ minSize: number
180
+ maxSize: number
181
+ continuity: number
182
+ }
183
+ ```
184
+
185
+ ### Events
186
+
187
+ | 事件名 | 参数 | 说明 |
188
+ |--------|------|------|
189
+ | pointChange | `PointAnnotation[]` | 点标注数据变化时触发 |
190
+ | loadStart | - | 图片开始加载时触发 |
191
+ | loadSuccess | - | 图片加载成功时触发 |
192
+ | loadError | `error` | 图片加载失败时触发 |
193
+ | undoStateChange | - | 撤销状态变化时触发 |
194
+ | redoStateChange | - | 重做状态变化时触发 |
195
+
196
+ ### Methods
197
+
198
+ 组件暴露以下方法:
199
+
200
+ | 方法名 | 参数 | 返回值 | 说明 |
201
+ |--------|------|--------|------|
202
+ | getPointAnnotations | - | `PointAnnotation[]` | 获取所有点标注数据 |
203
+ | getImageInfo | - | `Object` | 获取图片信息 |
204
+ | exportCanvasJSON | - | `string` | 导出完整 JSON 数据 |
205
+ | exportMaskImage | `format?`, `fgColor?` | `Promise<string|null>` | 导出二值图 |
206
+ | exportCOCO | - | `string` | 导出 COCO 格式 JSON |
207
+ | exportYOLO | - | `{ annotations: string; classNames: string }` | 导出 YOLO 格式 |
208
+ | importCanvasJSON | `jsonString`, `options?` | `Promise<boolean>` | 导入 JSON 数据 |
209
+ | loadImage | `url?` | `Promise<void>` | 加载图片 |
210
+ | clearBrush | - | `void` | 清除笔刷内容 |
211
+ | zoomIn | - | `void` | 放大画布 |
212
+ | zoomOut | - | `void` | 缩小画布 |
213
+ | resetZoom | - | `void` | 重置缩放 |
214
+ | undo | - | `void` | 撤销操作 |
215
+ | redo | - | `void` | 重做操作 |
216
+ | getCurrentTool | - | `'select'\|'point'\|'brush'\|'eraser'` | 获取当前工具 |
217
+ | setTool | `tool` | `void` | 设置当前工具 |
218
+ | createPointAnnotation | `x`, `y` | `string\|null` | 创建标注点 |
219
+ | removePointAnnotation | `id` | `boolean` | 删除指定标注点 |
220
+
221
+ ## 热键说明
222
+
223
+ | 热键 | 功能 |
224
+ |------|------|
225
+ | V | 选择工具 |
226
+ | P | 点标注工具 |
227
+ | B | 笔刷工具 |
228
+ | E | 擦除工具 |
229
+ | Ctrl + Z | 撤销 |
230
+ | Ctrl + Y | 重做 |
231
+ | Delete | 删除选中/清除所有 |
232
+ | Ctrl + + | 放大 |
233
+ | Ctrl + - | 缩小 |
234
+ | Ctrl + 0 | 重置缩放 |
235
+ | Alt | 显示/隐藏热键提示 |
236
+
237
+ ## 导出格式
238
+
239
+ ### JSON Full
240
+
241
+ 包含完整的标注数据和笔刷 mask。
242
+
243
+ ```json
244
+ {
245
+ "version": "1.0",
246
+ "imageUrl": "https://example.com/image.jpg",
247
+ "imageWidth": 1280,
248
+ "imageHeight": 720,
249
+ "pointAnnotations": [
250
+ {
251
+ "id": "point_xxx",
252
+ "pixel": { "x": 100, "y": 200 },
253
+ "normalized": { "x": 0.078, "y": 0.278 },
254
+ "label": "#1",
255
+ "createdAt": 1716960000000,
256
+ "updatedAt": 1716960000000
257
+ }
258
+ ],
259
+ "brushMask": "data:image/png;base64,...",
260
+ "exportTime": 1716960000000
261
+ }
262
+ ```
263
+
264
+ ### COCO
265
+
266
+ 适用于关键点检测任务。
267
+
268
+ ### YOLO
269
+
270
+ 适用于 YOLO 系列模型训练。
271
+
272
+ ### Mask Image
273
+
274
+ PNG/JPG 格式的二值图,前景为黑色/白色,背景透明/白色。
275
+
276
+ ## 项目文档
277
+
278
+ - [需求文档](./project-docs/REQUIREMENTS.md) - 详细的功能需求说明
279
+ - [架构文档](./project-docs/ARCHITECTURE.md) - 系统架构设计
280
+ - [实现计划](./project-docs/IMPLEMENTATION_PLAN.md) - 开发任务规划
281
+ - [开发指南](./project-docs/leafer-development-guide/LEAFER_DEVELOPMENT_GUIDE.md) - LeaferJS 开发实战指南
282
+
283
+ ## 浏览器支持
284
+
285
+ - Chrome 60+
286
+ - Firefox 55+
287
+ - Safari 12+
288
+ - Edge 79+
289
+
290
+ ## 依赖
291
+
292
+ - Vue 3.3.0+
293
+ - LeaferUI 2.0.8+
294
+ - Tinykeys 3.0.0+
295
+ - @zzalai/leafer-undo-redo 1.0.3+
296
+
297
+ ## 许可证
298
+
299
+ MIT License
300
+
301
+ ## 贡献
302
+
303
+ 欢迎提交 Issue 和 Pull Request!
304
+
305
+ ## 相关项目
306
+
307
+ - [@zzalai/leafer-multi-roi](https://github.com/otaku1951/leafer-multi-roi) - 多区域 ROI 标注工具
308
+ - [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS 撤销/重做插件
package/README_EN.md ADDED
@@ -0,0 +1,308 @@
1
+ # @zzalai/leafer-point-annotation
2
+
3
+ English | [中文](README.md)
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
+
18
+ ## Installation
19
+
20
+ ### npm
21
+
22
+ ```bash
23
+ npm install @zzalai/leafer-point-annotation
24
+ ```
25
+
26
+ ### yarn
27
+
28
+ ```bash
29
+ yarn add @zzalai/leafer-point-annotation
30
+ ```
31
+
32
+ ### pnpm
33
+
34
+ ```bash
35
+ pnpm add @zzalai/leafer-point-annotation
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ### Basic Usage
41
+
42
+ ```vue
43
+ <template>
44
+ <div class="demo-container">
45
+ <PointAnnotation
46
+ ref="annotationRef"
47
+ :imageSource="imageSource"
48
+ :options="options"
49
+ @pointChange="handlePointChange"
50
+ @loadSuccess="handleLoadSuccess"
51
+ />
52
+ <div class="controls">
53
+ <button @click="exportJSON">Export JSON</button>
54
+ <button @click="exportMask">Export Mask</button>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { ref, computed } from 'vue'
61
+ import { PointAnnotation } from '@zzalai/leafer-point-annotation'
62
+
63
+ const annotationRef = ref<InstanceType<typeof PointAnnotation> | null>(null)
64
+ const imageSource = computed(() => ({
65
+ id: 'demo-image',
66
+ url: 'https://example.com/image.jpg'
67
+ }))
68
+
69
+ const options = ref({
70
+ pointStyle: {
71
+ circleFill: '#ff4d4f',
72
+ circleStroke: '#ffffff',
73
+ labelBackgroundColor: '#ffffff'
74
+ },
75
+ brushStyle: {
76
+ color: '#ff4d4f',
77
+ opacity: 0.55,
78
+ size: 100
79
+ },
80
+ maskExportFormat: 'png',
81
+ maskExportForeground: 'black'
82
+ })
83
+
84
+ const handlePointChange = (points) => {
85
+ console.log('Points changed:', points)
86
+ }
87
+
88
+ const handleLoadSuccess = () => {
89
+ console.log('Image loaded successfully')
90
+ }
91
+
92
+ const exportJSON = () => {
93
+ const json = annotationRef.value?.exportCanvasJSON()
94
+ if (json) {
95
+ const blob = new Blob([json], { type: 'application/json' })
96
+ const url = URL.createObjectURL(blob)
97
+ const a = document.createElement('a')
98
+ a.href = url
99
+ a.download = 'annotation.json'
100
+ a.click()
101
+ }
102
+ }
103
+
104
+ const exportMask = async () => {
105
+ const mask = await annotationRef.value?.exportMaskImage('png', 'black')
106
+ if (mask) {
107
+ const a = document.createElement('a')
108
+ a.href = mask
109
+ a.download = 'mask.png'
110
+ a.click()
111
+ }
112
+ }
113
+ </script>
114
+
115
+ <style scoped>
116
+ .demo-container {
117
+ width: 100%;
118
+ height: 600px;
119
+ }
120
+
121
+ .controls {
122
+ margin-top: 16px;
123
+ display: flex;
124
+ gap: 12px;
125
+ }
126
+ </style>
127
+ ```
128
+
129
+ ## API Documentation
130
+
131
+ ### Props
132
+
133
+ | Property | Type | Default | Description |
134
+ |----------|------|---------|-------------|
135
+ | imageSource | `{ id?: string; url: string }` | Required | Image source configuration |
136
+ | options | `Object` | `{}` | Configuration options |
137
+
138
+ #### Options Configuration
139
+
140
+ ```typescript
141
+ interface Options {
142
+ pointStyle?: Partial<PointStyle>
143
+ brushStyle?: Partial<BrushStyle>
144
+ maskExportFormat?: 'png' | 'jpg' | 'jpeg'
145
+ maskExportForeground?: 'black' | 'white'
146
+ maxUndoSteps?: number
147
+ }
148
+ ```
149
+
150
+ #### PointStyle Configuration
151
+
152
+ ```typescript
153
+ interface PointStyle {
154
+ circleRadius: number
155
+ circleFill: string
156
+ circleStroke: string
157
+ circleStrokeWidth: number
158
+ hoverCircleFill: string
159
+ hoverCircleStroke: string
160
+ selectedCircleFill: string
161
+ selectedCircleStroke: string
162
+ selectedCircleScale: number
163
+ labelBackgroundColor: string
164
+ labelTextColor: string
165
+ labelFontSize: number
166
+ labelPadding: number[]
167
+ fixedSizeOnZoom: boolean
168
+ fixedSizeScale: number
169
+ }
170
+ ```
171
+
172
+ #### BrushStyle Configuration
173
+
174
+ ```typescript
175
+ interface BrushStyle {
176
+ color: string
177
+ opacity: number
178
+ size: number
179
+ minSize: number
180
+ maxSize: number
181
+ continuity: number
182
+ }
183
+ ```
184
+
185
+ ### Events
186
+
187
+ | Event Name | Parameters | Description |
188
+ |------------|------------|-------------|
189
+ | pointChange | `PointAnnotation[]` | Triggered when point annotations change |
190
+ | loadStart | - | Triggered when image starts loading |
191
+ | loadSuccess | - | Triggered when image loads successfully |
192
+ | loadError | `error` | Triggered when image loading fails |
193
+ | undoStateChange | - | Triggered when undo state changes |
194
+ | redoStateChange | - | Triggered when redo state changes |
195
+
196
+ ### Methods
197
+
198
+ The component exposes the following methods:
199
+
200
+ | Method Name | Parameters | Return Value | Description |
201
+ |-------------|------------|--------------|-------------|
202
+ | getPointAnnotations | - | `PointAnnotation[]` | Get all point annotation data |
203
+ | getImageInfo | - | `Object` | Get image information |
204
+ | exportCanvasJSON | - | `string` | Export complete JSON data |
205
+ | exportMaskImage | `format?`, `fgColor?` | `Promise<string|null>` | Export mask image |
206
+ | exportCOCO | - | `string` | Export COCO format JSON |
207
+ | exportYOLO | - | `{ annotations: string; classNames: string }` | Export YOLO format |
208
+ | importCanvasJSON | `jsonString`, `options?` | `Promise<boolean>` | Import JSON data |
209
+ | loadImage | `url?` | `Promise<void>` | Load image |
210
+ | clearBrush | - | `void` | Clear brush content |
211
+ | zoomIn | - | `void` | Zoom in canvas |
212
+ | zoomOut | - | `void` | Zoom out canvas |
213
+ | resetZoom | - | `void` | Reset zoom |
214
+ | undo | - | `void` | Undo operation |
215
+ | redo | - | `void` | Redo operation |
216
+ | getCurrentTool | - | `'select'\|'point'\|'brush'\|'eraser'` | Get current tool |
217
+ | setTool | `tool` | `void` | Set current tool |
218
+ | createPointAnnotation | `x`, `y` | `string\|null` | Create point annotation |
219
+ | removePointAnnotation | `id` | `boolean` | Remove specific point annotation |
220
+
221
+ ## Hotkeys
222
+
223
+ | Hotkey | Function |
224
+ |--------|----------|
225
+ | V | Select tool |
226
+ | P | Point annotation tool |
227
+ | B | Brush tool |
228
+ | E | Eraser tool |
229
+ | Ctrl + Z | Undo |
230
+ | Ctrl + Y | Redo |
231
+ | Delete | Delete selected / Clear all |
232
+ | Ctrl + + | Zoom in |
233
+ | Ctrl + - | Zoom out |
234
+ | Ctrl + 0 | Reset zoom |
235
+ | Alt | Show/Hide hotkey hints |
236
+
237
+ ## Export Formats
238
+
239
+ ### JSON Full
240
+
241
+ Includes complete annotation data and brush mask.
242
+
243
+ ```json
244
+ {
245
+ "version": "1.0",
246
+ "imageUrl": "https://example.com/image.jpg",
247
+ "imageWidth": 1280,
248
+ "imageHeight": 720,
249
+ "pointAnnotations": [
250
+ {
251
+ "id": "point_xxx",
252
+ "pixel": { "x": 100, "y": 200 },
253
+ "normalized": { "x": 0.078, "y": 0.278 },
254
+ "label": "#1",
255
+ "createdAt": 1716960000000,
256
+ "updatedAt": 1716960000000
257
+ }
258
+ ],
259
+ "brushMask": "data:image/png;base64,...",
260
+ "exportTime": 1716960000000
261
+ }
262
+ ```
263
+
264
+ ### COCO
265
+
266
+ For keypoint detection tasks.
267
+
268
+ ### YOLO
269
+
270
+ For YOLO series model training.
271
+
272
+ ### Mask Image
273
+
274
+ PNG/JPG format binary image, foreground in black/white, background transparent/white.
275
+
276
+ ## Project Documentation
277
+
278
+ - [Requirements](./project-docs/REQUIREMENTS.md) - Detailed functional requirements
279
+ - [Architecture](./project-docs/ARCHITECTURE.md) - System architecture design
280
+ - [Implementation Plan](./project-docs/IMPLEMENTATION_PLAN.md) - Development task planning
281
+ - [Development Guide](./project-docs/leafer-development-guide/LEAFER_DEVELOPMENT_GUIDE.md) - LeaferJS development practical guide
282
+
283
+ ## Browser Support
284
+
285
+ - Chrome 60+
286
+ - Firefox 55+
287
+ - Safari 12+
288
+ - Edge 79+
289
+
290
+ ## Dependencies
291
+
292
+ - Vue 3.3.0+
293
+ - LeaferUI 2.0.8+
294
+ - Tinykeys 3.0.0+
295
+ - @zzalai/leafer-undo-redo 1.0.3+
296
+
297
+ ## License
298
+
299
+ MIT License
300
+
301
+ ## Contributing
302
+
303
+ Issues and Pull Requests are welcome!
304
+
305
+ ## Related Projects
306
+
307
+ - [@zzalai/leafer-multi-roi](https://github.com/otaku1951/leafer-multi-roi) - Multi-region ROI annotation tool
308
+ - [@zzalai/leafer-undo-redo](https://github.com/otaku1951/leafer-undo-redo) - LeaferJS undo/redo plugin
@@ -0,0 +1 @@
1
+ .brush-panel-overlay[data-v-10bc9982]{z-index:1500;background:#0000004d;position:fixed;inset:0}.brush-style-panel[data-v-10bc9982]{z-index:1501;background:#fff;border-radius:10px;min-width:240px;position:fixed;overflow:visible;box-shadow:0 4px 24px #00000026}.panel-header[data-v-10bc9982]{background:var(--leafer-point-color-background-light);border-bottom:1px solid var(--leafer-point-color-border);border-radius:10px 10px 0 0;justify-content:space-between;align-items:center;padding:10px 16px;display:flex}.panel-header span[data-v-10bc9982]{color:var(--leafer-point-color-text);font-size:14px;font-weight:600}.close-btn[data-v-10bc9982]{cursor:pointer;width:24px;height:24px;color:var(--leafer-point-color-text);background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;font-size:18px;transition:all .2s;display:flex}.close-btn[data-v-10bc9982]:hover{background:var(--leafer-point-color-border)}.panel-content[data-v-10bc9982]{padding:16px 16px 24px}.config-item[data-v-10bc9982]{align-items:center;margin-bottom:20px;display:flex}.config-item[data-v-10bc9982]:last-child{margin-bottom:0}.config-label[data-v-10bc9982]{color:var(--leafer-point-color-text);text-align:right;min-width:50px;padding-right:5px;font-size:12px;display:block}.config-value[data-v-10bc9982]{color:var(--leafer-point-color-text);width:30px;padding-left:5px;font-size:12px}.color-picker-wrapper[data-v-10bc9982]{width:100%;margin:-10px 0}.config-slider[data-v-10bc9982]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:3px;outline:none;width:200px;height:6px}.config-slider[data-v-10bc9982]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}.config-slider[data-v-10bc9982]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}:root{--leafer-point-color-primary:#007aff;--leafer-point-color-background:#f5f5f5;--leafer-point-color-background-light:#f0f0f0;--leafer-point-color-white:#fff;--leafer-point-color-text:#333;--leafer-point-color-text-secondary:#666;--leafer-point-color-text-tertiary:#999;--leafer-point-color-border:#ddd;--leafer-point-color-border-light:#e0e0e0;--leafer-point-color-error:#e74c3c;--leafer-point-color-button:#3498db;--leafer-point-color-button-hover:#2980b9;--leafer-point-padding-toolbar:10px;--leafer-point-padding-tool-button:8px;--leafer-point-size-tool-icon:18px;--leafer-point-size-zoom-button:36px;--leafer-point-size-zoom-value:60px;--leafer-point-font-size-hotkey:10px;--leafer-point-padding-hotkey:1px 3px;--leafer-point-padding-error:20px;--leafer-point-padding-error-button:8px 16px;--leafer-point-border-radius-tool-button:4px;--leafer-point-border-radius-hotkey:2px;--leafer-point-border-radius-overlay:8px;--leafer-point-border-radius-zoom:8px;--leafer-point-shadow-tool-button:0 2px 4px #0000001a;--leafer-point-shadow-tool-button-active:0 2px 4px #007aff4d;--leafer-point-shadow-tool-button-hover:0 4px 6px #0000001a;--leafer-point-shadow-overlay:0 4px 12px #0000001a;--leafer-point-shadow-zoom:0 2px 8px #00000026;--leafer-point-transition-time:.2s;--leafer-point-animation-gradient:2s}.point-annotation[data-v-b7ee4495]{width:100%;height:100%}.canvas-container[data-v-b7ee4495]{outline:none;width:100%;height:calc(100% - 55px);position:relative;overflow:hidden}.canvas-container[data-v-b7ee4495]:focus{outline:2px solid var(--leafer-point-color-primary);outline-offset:-2px}.loading-overlay[data-v-b7ee4495]{background-color:var(--leafer-point-color-background-light);border-radius:var(--leafer-point-border-radius-overlay);box-shadow:var(--leafer-point-shadow-overlay);z-index:1000;justify-content:center;align-items:center;min-width:100%;min-height:100%;display:flex;position:absolute;top:50%;left:50%;overflow:hidden;transform:translate(-50%,-50%)}.gradient-animation[data-v-b7ee4495]{animation:gradientShift-b7ee4495 var(--leafer-point-animation-gradient) ease-in-out infinite;opacity:.7;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%) 0 0/200% 200%;position:absolute;inset:0}@keyframes gradientShift-b7ee4495{0%{background-position:0%}50%{background-position:100%}to{background-position:0%}}.loading-text[data-v-b7ee4495]{z-index:1;color:#fff;text-shadow:0 2px 4px #0003;font-size:16px;font-weight:500;position:relative}.error-overlay[data-v-b7ee4495]{background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-overlay);box-shadow:var(--leafer-point-shadow-overlay);padding:var(--leafer-point-padding-error);z-index:1000;flex-direction:column;justify-content:center;align-items:center;min-width:200px;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.error-overlay p[data-v-b7ee4495]{color:var(--leafer-point-color-error);margin-bottom:20px;font-size:16px}.error-overlay button[data-v-b7ee4495]{padding:var(--leafer-point-padding-error-button);background-color:var(--leafer-point-color-button);color:#fff;border-radius:var(--leafer-point-border-radius-tool-button);cursor:pointer;border:none;font-size:14px}.error-overlay button[data-v-b7ee4495]:hover{background-color:var(--leafer-point-color-button-hover)}.zoom-controller[data-v-b7ee4495]{background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-zoom);box-shadow:var(--leafer-point-shadow-zoom);z-index:100;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;overflow:hidden}.zoom-button[data-v-b7ee4495]{width:var(--leafer-point-size-zoom-button);height:var(--leafer-point-size-zoom-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.zoom-button[data-v-b7ee4495]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.zoom-button[data-v-b7ee4495]:active{background-color:#e0e0e0}.zoom-value[data-v-b7ee4495]{min-width:var(--leafer-point-size-zoom-value);height:var(--leafer-point-size-zoom-button);line-height:var(--leafer-point-size-zoom-button);text-align:center;color:var(--leafer-point-color-text);cursor:pointer;border-left:1px solid var(--leafer-point-color-border-light);border-right:1px solid var(--leafer-point-color-border-light);transition:all var(--leafer-point-transition-time) ease;font-size:14px;font-weight:500;position:relative}.zoom-value .hotkey-hint[data-v-b7ee4495]{line-height:1}.zoom-value[data-v-b7ee4495]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.toolbar[data-v-b7ee4495]{padding:var(--leafer-point-padding-toolbar);background-color:var(--leafer-point-color-background);border-top:1px solid var(--leafer-point-color-border);justify-content:center;align-items:center;gap:10px;display:flex}.tool-button[data-v-b7ee4495]{padding:var(--leafer-point-padding-tool-button);border-radius:var(--leafer-point-border-radius-tool-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;box-shadow:var(--leafer-point-shadow-tool-button);border:none;justify-content:center;align-items:center;display:flex;position:relative}.tool-button[data-v-b7ee4495]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary);box-shadow:var(--leafer-point-shadow-tool-button-hover)}.tool-button[data-v-b7ee4495]:active{box-shadow:var(--leafer-point-shadow-tool-button);transform:translateY(1px)}.tool-button.active[data-v-b7ee4495]{background-color:var(--leafer-point-color-primary);color:#fff;box-shadow:var(--leafer-point-shadow-tool-button-active)}.hotkey-hint[data-v-b7ee4495]{font-size:var(--leafer-point-font-size-hotkey);color:#fff;padding:var(--leafer-point-padding-hotkey);border-radius:var(--leafer-point-border-radius-hotkey);pointer-events:none;white-space:nowrap;background-color:#0009;position:absolute;top:0;right:0}.size-control[data-v-b7ee4495]{padding:var(--leafer-point-padding-tool-button);background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-tool-button);box-shadow:var(--leafer-point-shadow-tool-button);align-items:center;gap:8px;display:flex}.size-label[data-v-b7ee4495]{color:var(--leafer-point-color-text);white-space:nowrap;font-size:12px}.size-slider[data-v-b7ee4495]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:4px;outline:none;width:120px;height:8px}.size-slider[data-v-b7ee4495]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-b7ee4495]::-webkit-slider-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]::-webkit-slider-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]:focus::-webkit-slider-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-b7ee4495]::-moz-range-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]::-moz-range-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]:focus::-moz-range-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-b7ee4495]:focus{outline:none}.size-value[data-v-b7ee4495]{text-align:center;min-width:30px;color:var(--leafer-point-color-primary);font-size:12px;font-weight:600}.app[data-v-c05a47b7]{max-width:1200px;margin:0 auto;padding:20px;font-family:Arial,sans-serif}h1[data-v-c05a47b7]{text-align:center;margin-bottom:30px}.editor-container[data-v-c05a47b7]{border:1px solid #ddd;border-radius:8px;width:100%;height:600px;margin-bottom:30px;overflow:hidden}.controls[data-v-c05a47b7]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}.control-group[data-v-c05a47b7]{margin-bottom:15px}label[data-v-c05a47b7]{margin-bottom:5px;font-weight:700;display:block}input[data-v-c05a47b7]{border:1px solid #ddd;border-radius:4px;width:100%;margin-bottom:10px;padding:8px}.mask-options[data-v-c05a47b7]{gap:15px;margin-bottom:10px;display:flex}.mask-options label[data-v-c05a47b7]{align-items:center;gap:5px;font-weight:400;display:flex}.mask-options select[data-v-c05a47b7]{border:1px solid #ddd;border-radius:4px;padding:4px 8px}button[data-v-c05a47b7]{color:#fff;cursor:pointer;background-color:#007bff;border:none;border-radius:4px;margin-right:10px;padding:8px 16px}button[data-v-c05a47b7]:hover{background-color:#0069d9}.output[data-v-c05a47b7]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}pre[data-v-c05a47b7]{white-space:pre-wrap;word-wrap:break-word;background-color:#fff;border-radius:4px;max-height:300px;padding:15px;overflow-y:auto}.status[data-v-c05a47b7]{background-color:#f5f5f5;border-radius:8px;padding:20px}