@zzalai/leafer-multi-roi 1.0.1
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 +21 -0
- package/README.md +293 -0
- package/README_EN.md +293 -0
- package/docs/assets/index-B2aZIWia.css +1 -0
- package/docs/assets/index-BrSsc-mD.js +1 -0
- package/docs/assets/vite-CMPW0ETM.svg +130 -0
- package/docs/index.html +14 -0
- package/index.html +13 -0
- package/package.json +61 -0
- package/project-docs/ARCHITECTURE.md +129 -0
- package/project-docs/REQUIREMENTS.md +113 -0
- package/src/App.vue +284 -0
- package/src/components/RoiEditor.vue +1544 -0
- package/src/index.ts +10 -0
- package/src/main.ts +4 -0
- package/src/types/index.ts +49 -0
- package/src/utils/coordinates.ts +46 -0
- package/src/utils/icons.ts +41 -0
- package/src/utils/uuid.ts +4 -0
- package/src/vite-env.d.ts +7 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +39 -0
- package/vite.docs.config.ts +29 -0
- package/vite.svg +130 -0
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,293 @@
|
|
|
1
|
+
# LeaferJS Multi ROI 组件
|
|
2
|
+
|
|
3
|
+
[English](README_EN.md) | 中文
|
|
4
|
+
|
|
5
|
+
一个基于 Vue3 和 LeaferJS 的多区域选择组件,用于在图片上进行区域标注和编辑。
|
|
6
|
+
|
|
7
|
+
## 功能特点
|
|
8
|
+
|
|
9
|
+
- 🖼️ 支持图片加载和显示
|
|
10
|
+
- 📐 支持矩形区域的绘制、编辑和删除
|
|
11
|
+
- 🔍 支持画布缩放和拖拽
|
|
12
|
+
- ⌨️ 支持键盘热键操作
|
|
13
|
+
- 🔄 支持撤销/重做功能
|
|
14
|
+
- 📤 支持画布信息 JSON 导出和导入
|
|
15
|
+
- 🎨 支持 CSS 变量自定义样式
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
### 使用 npm
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @zzalai/leafer-multi-roi
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 使用 yarn
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn add @zzalai/leafer-multi-roi
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 使用 pnpm
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add @zzalai/leafer-multi-roi
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 使用方法
|
|
38
|
+
|
|
39
|
+
### 全局注册
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// main.ts
|
|
43
|
+
import { createApp } from 'vue'
|
|
44
|
+
import App from './App.vue'
|
|
45
|
+
import RoiEditor from '@zzalai/leafer-multi-roi'
|
|
46
|
+
|
|
47
|
+
const app = createApp(App)
|
|
48
|
+
app.use(RoiEditor)
|
|
49
|
+
app.mount('#app')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 局部使用
|
|
53
|
+
|
|
54
|
+
```vue
|
|
55
|
+
<template>
|
|
56
|
+
<div class="app">
|
|
57
|
+
<RoiEditor
|
|
58
|
+
:imageSrc="imageSrc"
|
|
59
|
+
:options="editorOptions"
|
|
60
|
+
@roiChange="handleRoiChange"
|
|
61
|
+
@loadStart="handleLoadStart"
|
|
62
|
+
@loadSuccess="handleLoadSuccess"
|
|
63
|
+
@loadError="handleLoadError"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref, computed } from 'vue'
|
|
70
|
+
import { RoiEditor } from '@zzalai/leafer-multi-roi'
|
|
71
|
+
|
|
72
|
+
// 图片源
|
|
73
|
+
const imageUrl = ref('https://picsum.photos/1280/1080')
|
|
74
|
+
const imageSrc = computed(() => ({
|
|
75
|
+
id: 'test-image',
|
|
76
|
+
url: imageUrl.value
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
// 编辑器选项
|
|
80
|
+
const editorOptions = ref({
|
|
81
|
+
regionStyle: {
|
|
82
|
+
fill: 'rgba(100, 149, 237, 0.3)',
|
|
83
|
+
stroke: 'rgba(100, 149, 237, 0.8)',
|
|
84
|
+
strokeWidth: 2
|
|
85
|
+
},
|
|
86
|
+
selectedRegionStyle: {
|
|
87
|
+
fill: 'rgba(100, 149, 237, 0.5)',
|
|
88
|
+
stroke: 'rgba(100, 149, 237, 1)',
|
|
89
|
+
strokeWidth: 2
|
|
90
|
+
},
|
|
91
|
+
maxRegions: 20, // 最大区域数量限制,默认值为20
|
|
92
|
+
maxUndoSteps: 100 // 最大撤销/重做步数限制,默认值为100
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// 处理ROI变化
|
|
96
|
+
const handleRoiChange = (data: any) => {
|
|
97
|
+
console.log('ROI changed:', data)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 处理图片加载开始
|
|
101
|
+
const handleLoadStart = () => {
|
|
102
|
+
console.log('Image load started')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 处理图片加载成功
|
|
106
|
+
const handleLoadSuccess = () => {
|
|
107
|
+
console.log('Image load success')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 处理图片加载失败
|
|
111
|
+
const handleLoadError = (error: any) => {
|
|
112
|
+
console.error('Image load error:', error)
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 手动调用方法
|
|
118
|
+
|
|
119
|
+
```vue
|
|
120
|
+
<template>
|
|
121
|
+
<div class="app">
|
|
122
|
+
<RoiEditor
|
|
123
|
+
ref="roiEditor"
|
|
124
|
+
:imageSrc="imageSrc"
|
|
125
|
+
/>
|
|
126
|
+
<button @click="reloadImage">重新加载图片</button>
|
|
127
|
+
<button @click="exportCanvas">导出画布</button>
|
|
128
|
+
<input type="file" @change="importCanvas" accept=".json" />
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<script setup lang="ts">
|
|
133
|
+
import { ref, computed } from 'vue'
|
|
134
|
+
import { RoiEditor } from '@zzalai/leafer-multi-roi'
|
|
135
|
+
|
|
136
|
+
const roiEditor = ref<InstanceType<typeof RoiEditor> | null>(null)
|
|
137
|
+
const imageUrl = ref('https://picsum.photos/1280/1080')
|
|
138
|
+
const imageSrc = computed(() => ({
|
|
139
|
+
id: 'test-image',
|
|
140
|
+
url: imageUrl.value
|
|
141
|
+
}))
|
|
142
|
+
|
|
143
|
+
// 重新加载图片
|
|
144
|
+
const reloadImage = () => {
|
|
145
|
+
roiEditor.value?.loadImage()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 导出画布
|
|
149
|
+
const exportCanvas = () => {
|
|
150
|
+
const json = roiEditor.value?.exportCanvasJSON()
|
|
151
|
+
if (json) {
|
|
152
|
+
const blob = new Blob([json], { type: 'application/json' })
|
|
153
|
+
const url = URL.createObjectURL(blob)
|
|
154
|
+
const a = document.createElement('a')
|
|
155
|
+
a.href = url
|
|
156
|
+
a.download = 'canvas.json'
|
|
157
|
+
a.click()
|
|
158
|
+
URL.revokeObjectURL(url)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 导入画布
|
|
163
|
+
const importCanvas = async (event: Event) => {
|
|
164
|
+
const target = event.target as HTMLInputElement
|
|
165
|
+
const file = target.files?.[0]
|
|
166
|
+
if (file) {
|
|
167
|
+
const reader = new FileReader()
|
|
168
|
+
reader.onload = async (e) => {
|
|
169
|
+
const jsonString = e.target?.result as string
|
|
170
|
+
if (roiEditor.value) {
|
|
171
|
+
const success = await roiEditor.value.importCanvasJSON(jsonString, { resetZoom: true })
|
|
172
|
+
if (success) {
|
|
173
|
+
alert('画布导入成功!')
|
|
174
|
+
} else {
|
|
175
|
+
alert('画布导入失败。')
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
reader.readAsText(file)
|
|
180
|
+
}
|
|
181
|
+
target.value = ''
|
|
182
|
+
}
|
|
183
|
+
</script>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 适用场景
|
|
187
|
+
|
|
188
|
+
- **图片标注**:在图片上标注感兴趣的区域
|
|
189
|
+
- **目标检测**:为目标检测模型生成训练数据
|
|
190
|
+
- **图像分析**:标记图像中的特定区域进行分析
|
|
191
|
+
- **医疗影像**:在医疗影像上标记病变区域
|
|
192
|
+
- **电商产品**:在产品图片上标记不同的部件
|
|
193
|
+
|
|
194
|
+
## 热键操作
|
|
195
|
+
|
|
196
|
+
| 热键 | 功能 |
|
|
197
|
+
|------|------|
|
|
198
|
+
| V | 选择工具 |
|
|
199
|
+
| M | 框选工具 |
|
|
200
|
+
| Ctrl+Z | 撤销 |
|
|
201
|
+
| Ctrl+Y | 重做 |
|
|
202
|
+
| Delete | 删除选中区域 |
|
|
203
|
+
| Ctrl++ | 放大 |
|
|
204
|
+
| Ctrl+- | 缩小 |
|
|
205
|
+
| Ctrl+0 | 重置缩放 |
|
|
206
|
+
| Alt | 显示/隐藏热键提示 |
|
|
207
|
+
|
|
208
|
+
## 暴露的方法
|
|
209
|
+
|
|
210
|
+
- `getROIAnnotations()`:获取所有 ROI 标注数据
|
|
211
|
+
- `getImageInfo()`:获取图片信息
|
|
212
|
+
- `exportCanvasJSON()`:导出画布信息为 JSON 字符串
|
|
213
|
+
- `importCanvasJSON(jsonString, options)`:从 JSON 字符串导入画布信息
|
|
214
|
+
- `loadImage()`:手动加载图片
|
|
215
|
+
|
|
216
|
+
## CSS 可重置变量
|
|
217
|
+
|
|
218
|
+
```css
|
|
219
|
+
:root {
|
|
220
|
+
/* 颜色变量 */
|
|
221
|
+
--leafer-roi-color-primary: #007aff; /* 主题色 */
|
|
222
|
+
--leafer-roi-color-background: #f5f5f5; /* 背景色 */
|
|
223
|
+
--leafer-roi-color-background-light: #f0f0f0; /* 浅背景色 */
|
|
224
|
+
--leafer-roi-color-white: #fff; /* 白色 */
|
|
225
|
+
--leafer-roi-color-text: #333; /* 文本色 */
|
|
226
|
+
--leafer-roi-color-text-secondary: #666; /* 次要文本色 */
|
|
227
|
+
--leafer-roi-color-text-tertiary: #999999; /* 三级文本色 */
|
|
228
|
+
--leafer-roi-color-border: #ddd; /* 边框色 */
|
|
229
|
+
--leafer-roi-color-border-light: #e0e0e0; /* 浅边框色 */
|
|
230
|
+
--leafer-roi-color-error: #e74c3c; /* 错误色 */
|
|
231
|
+
--leafer-roi-color-button: #3498db; /* 按钮色 */
|
|
232
|
+
--leafer-roi-color-button-hover: #2980b9; /* 按钮悬停色 */
|
|
233
|
+
|
|
234
|
+
/* 尺寸变量 */
|
|
235
|
+
--leafer-roi-padding-toolbar: 10px; /* 工具栏内边距 */
|
|
236
|
+
--leafer-roi-padding-tool-button: 8px; /* 工具按钮内边距 */
|
|
237
|
+
--leafer-roi-size-tool-icon: 18px; /* 工具图标尺寸 */
|
|
238
|
+
--leafer-roi-size-zoom-button: 36px; /* 缩放按钮尺寸 */
|
|
239
|
+
--leafer-roi-size-zoom-value: 60px; /* 缩放值显示宽度 */
|
|
240
|
+
--leafer-roi-font-size-hotkey: 10px; /* 热键提示字体大小 */
|
|
241
|
+
--leafer-roi-padding-hotkey: 1px 3px; /* 热键提示内边距 */
|
|
242
|
+
--leafer-roi-padding-error: 20px; /* 错误提示内边距 */
|
|
243
|
+
--leafer-roi-padding-error-button: 8px 16px; /* 错误提示按钮内边距 */
|
|
244
|
+
|
|
245
|
+
/* 边框圆角 */
|
|
246
|
+
--leafer-roi-border-radius-tool-button: 4px; /* 工具按钮圆角 */
|
|
247
|
+
--leafer-roi-border-radius-hotkey: 2px; /* 热键提示圆角 */
|
|
248
|
+
--leafer-roi-border-radius-overlay: 8px; /* 遮罩圆角 */
|
|
249
|
+
--leafer-roi-border-radius-zoom: 8px; /* 缩放控制器圆角 */
|
|
250
|
+
|
|
251
|
+
/* 阴影 */
|
|
252
|
+
--leafer-roi-shadow-tool-button: 0 2px 4px rgba(0, 0, 0, 0.1); /* 工具按钮阴影 */
|
|
253
|
+
--leafer-roi-shadow-tool-button-active: 0 2px 4px rgba(0, 122, 255, 0.3); /* 工具按钮激活阴影 */
|
|
254
|
+
--leafer-roi-shadow-tool-button-hover: 0 4px 6px rgba(0, 0, 0, 0.1); /* 工具按钮悬停阴影 */
|
|
255
|
+
--leafer-roi-shadow-overlay: 0 4px 12px rgba(0, 0, 0, 0.1); /* 遮罩阴影 */
|
|
256
|
+
--leafer-roi-shadow-zoom: 0 2px 8px rgba(0, 0, 0, 0.15); /* 缩放控制器阴影 */
|
|
257
|
+
|
|
258
|
+
/* 动画 */
|
|
259
|
+
--leafer-roi-transition-time: 0.2s; /* 过渡动画时长 */
|
|
260
|
+
--leafer-roi-animation-gradient: 2s; /* 渐变动画时长 */
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## 事件
|
|
265
|
+
|
|
266
|
+
- `roiChange`:当 ROI 发生变化时触发
|
|
267
|
+
- `loadStart`:当图片开始加载时触发
|
|
268
|
+
- `loadSuccess`:当图片加载成功时触发
|
|
269
|
+
- `loadError`:当图片加载失败时触发
|
|
270
|
+
- `undoStateChange`:当撤销状态变化时触发
|
|
271
|
+
- `redoStateChange`:当重做状态变化时触发
|
|
272
|
+
|
|
273
|
+
## 浏览器兼容性
|
|
274
|
+
|
|
275
|
+
- Chrome 60+
|
|
276
|
+
- Firefox 55+
|
|
277
|
+
- Safari 12+
|
|
278
|
+
- Edge 79+
|
|
279
|
+
|
|
280
|
+
## 依赖
|
|
281
|
+
|
|
282
|
+
- Vue 3.3.0+
|
|
283
|
+
- LeaferUI 2.0.8+
|
|
284
|
+
- Tinykeys 3.0.0+
|
|
285
|
+
- @zzalai/leafer-undo-redo 1.0.3+
|
|
286
|
+
|
|
287
|
+
## 许可证
|
|
288
|
+
|
|
289
|
+
MIT License
|
|
290
|
+
|
|
291
|
+
## 贡献
|
|
292
|
+
|
|
293
|
+
欢迎提交 Issue 和 Pull Request!
|
package/README_EN.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# LeaferJS Multi ROI Component
|
|
2
|
+
|
|
3
|
+
[中文](README.md) | English
|
|
4
|
+
|
|
5
|
+
A Vue3 component based on LeaferJS for multi-region selection on images, used for area annotation and editing on images.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🖼️ Support image loading and display
|
|
10
|
+
- 📐 Support drawing, editing and deleting rectangular regions
|
|
11
|
+
- 🔍 Support canvas zooming and dragging
|
|
12
|
+
- ⌨️ Support keyboard hotkey operations
|
|
13
|
+
- 🔄 Support undo/redo functionality
|
|
14
|
+
- 📤 Support canvas information JSON export and import
|
|
15
|
+
- 🎨 Support CSS variables for custom styling
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Using npm
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @zzalai/leafer-multi-roi
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Using yarn
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn add @zzalai/leafer-multi-roi
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Using pnpm
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add @zzalai/leafer-multi-roi
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Global Registration
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// main.ts
|
|
43
|
+
import { createApp } from 'vue'
|
|
44
|
+
import App from './App.vue'
|
|
45
|
+
import RoiEditor from '@zzalai/leafer-multi-roi'
|
|
46
|
+
|
|
47
|
+
const app = createApp(App)
|
|
48
|
+
app.use(RoiEditor)
|
|
49
|
+
app.mount('#app')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Local Usage
|
|
53
|
+
|
|
54
|
+
```vue
|
|
55
|
+
<template>
|
|
56
|
+
<div class="app">
|
|
57
|
+
<RoiEditor
|
|
58
|
+
:imageSrc="imageSrc"
|
|
59
|
+
:options="editorOptions"
|
|
60
|
+
@roiChange="handleRoiChange"
|
|
61
|
+
@loadStart="handleLoadStart"
|
|
62
|
+
@loadSuccess="handleLoadSuccess"
|
|
63
|
+
@loadError="handleLoadError"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref, computed } from 'vue'
|
|
70
|
+
import { RoiEditor } from '@zzalai/leafer-multi-roi'
|
|
71
|
+
|
|
72
|
+
// Image source
|
|
73
|
+
const imageUrl = ref('https://picsum.photos/1280/1080')
|
|
74
|
+
const imageSrc = computed(() => ({
|
|
75
|
+
id: 'test-image',
|
|
76
|
+
url: imageUrl.value
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
// Editor options
|
|
80
|
+
const editorOptions = ref({
|
|
81
|
+
regionStyle: {
|
|
82
|
+
fill: 'rgba(100, 149, 237, 0.3)',
|
|
83
|
+
stroke: 'rgba(100, 149, 237, 0.8)',
|
|
84
|
+
strokeWidth: 2
|
|
85
|
+
},
|
|
86
|
+
selectedRegionStyle: {
|
|
87
|
+
fill: 'rgba(100, 149, 237, 0.5)',
|
|
88
|
+
stroke: 'rgba(100, 149, 237, 1)',
|
|
89
|
+
strokeWidth: 2
|
|
90
|
+
},
|
|
91
|
+
maxRegions: 20, // Maximum number of regions, default is 20
|
|
92
|
+
maxUndoSteps: 100 // Maximum undo/redo steps, default is 100
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Handle ROI change
|
|
96
|
+
const handleRoiChange = (data: any) => {
|
|
97
|
+
console.log('ROI changed:', data)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Handle image load start
|
|
101
|
+
const handleLoadStart = () => {
|
|
102
|
+
console.log('Image load started')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Handle image load success
|
|
106
|
+
const handleLoadSuccess = () => {
|
|
107
|
+
console.log('Image load success')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Handle image load error
|
|
111
|
+
const handleLoadError = (error: any) => {
|
|
112
|
+
console.error('Image load error:', error)
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Manual Method Calls
|
|
118
|
+
|
|
119
|
+
```vue
|
|
120
|
+
<template>
|
|
121
|
+
<div class="app">
|
|
122
|
+
<RoiEditor
|
|
123
|
+
ref="roiEditor"
|
|
124
|
+
:imageSrc="imageSrc"
|
|
125
|
+
/>
|
|
126
|
+
<button @click="reloadImage">Reload Image</button>
|
|
127
|
+
<button @click="exportCanvas">Export Canvas</button>
|
|
128
|
+
<input type="file" @change="importCanvas" accept=".json" />
|
|
129
|
+
</div>
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<script setup lang="ts">
|
|
133
|
+
import { ref, computed } from 'vue'
|
|
134
|
+
import { RoiEditor } from '@zzalai/leafer-multi-roi'
|
|
135
|
+
|
|
136
|
+
const roiEditor = ref<InstanceType<typeof RoiEditor> | null>(null)
|
|
137
|
+
const imageUrl = ref('https://picsum.photos/1280/1080')
|
|
138
|
+
const imageSrc = computed(() => ({
|
|
139
|
+
id: 'test-image',
|
|
140
|
+
url: imageUrl.value
|
|
141
|
+
}))
|
|
142
|
+
|
|
143
|
+
// Reload image
|
|
144
|
+
const reloadImage = () => {
|
|
145
|
+
roiEditor.value?.loadImage()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Export canvas
|
|
149
|
+
const exportCanvas = () => {
|
|
150
|
+
const json = roiEditor.value?.exportCanvasJSON()
|
|
151
|
+
if (json) {
|
|
152
|
+
const blob = new Blob([json], { type: 'application/json' })
|
|
153
|
+
const url = URL.createObjectURL(blob)
|
|
154
|
+
const a = document.createElement('a')
|
|
155
|
+
a.href = url
|
|
156
|
+
a.download = 'canvas.json'
|
|
157
|
+
a.click()
|
|
158
|
+
URL.revokeObjectURL(url)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Import canvas
|
|
163
|
+
const importCanvas = async (event: Event) => {
|
|
164
|
+
const target = event.target as HTMLInputElement
|
|
165
|
+
const file = target.files?.[0]
|
|
166
|
+
if (file) {
|
|
167
|
+
const reader = new FileReader()
|
|
168
|
+
reader.onload = async (e) => {
|
|
169
|
+
const jsonString = e.target?.result as string
|
|
170
|
+
if (roiEditor.value) {
|
|
171
|
+
const success = await roiEditor.value.importCanvasJSON(jsonString, { resetZoom: true })
|
|
172
|
+
if (success) {
|
|
173
|
+
alert('Canvas imported successfully!')
|
|
174
|
+
} else {
|
|
175
|
+
alert('Failed to import canvas.')
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
reader.readAsText(file)
|
|
180
|
+
}
|
|
181
|
+
target.value = ''
|
|
182
|
+
}
|
|
183
|
+
</script>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Application Scenarios
|
|
187
|
+
|
|
188
|
+
- **Image Annotation**:Annotate regions of interest on images
|
|
189
|
+
- **Object Detection**:Generate training data for object detection models
|
|
190
|
+
- **Image Analysis**:Mark specific regions in images for analysis
|
|
191
|
+
- **Medical Imaging**:Mark lesion areas on medical images
|
|
192
|
+
- **E-commerce Products**:Mark different parts on product images
|
|
193
|
+
|
|
194
|
+
## Hotkey Operations
|
|
195
|
+
|
|
196
|
+
| Hotkey | Function |
|
|
197
|
+
|--------|----------|
|
|
198
|
+
| V | Select tool |
|
|
199
|
+
| M | Rectangle tool |
|
|
200
|
+
| Ctrl+Z | Undo |
|
|
201
|
+
| Ctrl+Y | Redo |
|
|
202
|
+
| Delete | Delete selected region |
|
|
203
|
+
| Ctrl++ | Zoom in |
|
|
204
|
+
| Ctrl+- | Zoom out |
|
|
205
|
+
| Ctrl+0 | Reset zoom |
|
|
206
|
+
| Alt | Show/hide hotkey hints |
|
|
207
|
+
|
|
208
|
+
## Exposed Methods
|
|
209
|
+
|
|
210
|
+
- `getROIAnnotations()`:Get all ROI annotation data
|
|
211
|
+
- `getImageInfo()`:Get image information
|
|
212
|
+
- `exportCanvasJSON()`:Export canvas information as JSON string
|
|
213
|
+
- `importCanvasJSON(jsonString, options)`:Import canvas information from JSON string
|
|
214
|
+
- `loadImage()`:Manually load image
|
|
215
|
+
|
|
216
|
+
## CSS Customizable Variables
|
|
217
|
+
|
|
218
|
+
```css
|
|
219
|
+
:root {
|
|
220
|
+
/* Color variables */
|
|
221
|
+
--leafer-roi-color-primary: #007aff; /* Primary color */
|
|
222
|
+
--leafer-roi-color-background: #f5f5f5; /* Background color */
|
|
223
|
+
--leafer-roi-color-background-light: #f0f0f0; /* Light background color */
|
|
224
|
+
--leafer-roi-color-white: #fff; /* White color */
|
|
225
|
+
--leafer-roi-color-text: #333; /* Text color */
|
|
226
|
+
--leafer-roi-color-text-secondary: #666; /* Secondary text color */
|
|
227
|
+
--leafer-roi-color-text-tertiary: #999999; /* Tertiary text color */
|
|
228
|
+
--leafer-roi-color-border: #ddd; /* Border color */
|
|
229
|
+
--leafer-roi-color-border-light: #e0e0e0; /* Light border color */
|
|
230
|
+
--leafer-roi-color-error: #e74c3c; /* Error color */
|
|
231
|
+
--leafer-roi-color-button: #3498db; /* Button color */
|
|
232
|
+
--leafer-roi-color-button-hover: #2980b9; /* Button hover color */
|
|
233
|
+
|
|
234
|
+
/* Size variables */
|
|
235
|
+
--leafer-roi-padding-toolbar: 10px; /* Toolbar padding */
|
|
236
|
+
--leafer-roi-padding-tool-button: 8px; /* Tool button padding */
|
|
237
|
+
--leafer-roi-size-tool-icon: 18px; /* Tool icon size */
|
|
238
|
+
--leafer-roi-size-zoom-button: 36px; /* Zoom button size */
|
|
239
|
+
--leafer-roi-size-zoom-value: 60px; /* Zoom value display width */
|
|
240
|
+
--leafer-roi-font-size-hotkey: 10px; /* Hotkey hint font size */
|
|
241
|
+
--leafer-roi-padding-hotkey: 1px 3px; /* Hotkey hint padding */
|
|
242
|
+
--leafer-roi-padding-error: 20px; /* Error hint padding */
|
|
243
|
+
--leafer-roi-padding-error-button: 8px 16px; /* Error hint button padding */
|
|
244
|
+
|
|
245
|
+
/* Border radius */
|
|
246
|
+
--leafer-roi-border-radius-tool-button: 4px; /* Tool button border radius */
|
|
247
|
+
--leafer-roi-border-radius-hotkey: 2px; /* Hotkey hint border radius */
|
|
248
|
+
--leafer-roi-border-radius-overlay: 8px; /* Overlay border radius */
|
|
249
|
+
--leafer-roi-border-radius-zoom: 8px; /* Zoom controller border radius */
|
|
250
|
+
|
|
251
|
+
/* Shadows */
|
|
252
|
+
--leafer-roi-shadow-tool-button: 0 2px 4px rgba(0, 0, 0, 0.1); /* Tool button shadow */
|
|
253
|
+
--leafer-roi-shadow-tool-button-active: 0 2px 4px rgba(0, 122, 255, 0.3); /* Tool button active shadow */
|
|
254
|
+
--leafer-roi-shadow-tool-button-hover: 0 4px 6px rgba(0, 0, 0, 0.1); /* Tool button hover shadow */
|
|
255
|
+
--leafer-roi-shadow-overlay: 0 4px 12px rgba(0, 0, 0, 0.1); /* Overlay shadow */
|
|
256
|
+
--leafer-roi-shadow-zoom: 0 2px 8px rgba(0, 0, 0, 0.15); /* Zoom controller shadow */
|
|
257
|
+
|
|
258
|
+
/* Animations */
|
|
259
|
+
--leafer-roi-transition-time: 0.2s; /* Transition animation duration */
|
|
260
|
+
--leafer-roi-animation-gradient: 2s; /* Gradient animation duration */
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Events
|
|
265
|
+
|
|
266
|
+
- `roiChange`:Triggered when ROI changes
|
|
267
|
+
- `loadStart`:Triggered when image starts loading
|
|
268
|
+
- `loadSuccess`:Triggered when image loads successfully
|
|
269
|
+
- `loadError`:Triggered when image fails to load
|
|
270
|
+
- `undoStateChange`:Triggered when undo state changes
|
|
271
|
+
- `redoStateChange`:Triggered when redo state changes
|
|
272
|
+
|
|
273
|
+
## Browser Compatibility
|
|
274
|
+
|
|
275
|
+
- Chrome 60+
|
|
276
|
+
- Firefox 55+
|
|
277
|
+
- Safari 12+
|
|
278
|
+
- Edge 79+
|
|
279
|
+
|
|
280
|
+
## Dependencies
|
|
281
|
+
|
|
282
|
+
- Vue 3.3.0+
|
|
283
|
+
- LeaferUI 2.0.8+
|
|
284
|
+
- Tinykeys 3.0.0+
|
|
285
|
+
- @zzalai/leafer-undo-redo 1.0.3+
|
|
286
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
MIT License
|
|
290
|
+
|
|
291
|
+
## Contribution
|
|
292
|
+
|
|
293
|
+
Welcome to submit Issues and Pull Requests!
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--leafer-roi-color-primary:#007aff;--leafer-roi-color-background:#f5f5f5;--leafer-roi-color-background-light:#f0f0f0;--leafer-roi-color-white:#fff;--leafer-roi-color-text:#333;--leafer-roi-color-text-secondary:#666;--leafer-roi-color-text-tertiary:#999;--leafer-roi-color-border:#ddd;--leafer-roi-color-border-light:#e0e0e0;--leafer-roi-color-error:#e74c3c;--leafer-roi-color-button:#3498db;--leafer-roi-color-button-hover:#2980b9;--leafer-roi-padding-toolbar:10px;--leafer-roi-padding-tool-button:8px;--leafer-roi-size-tool-icon:18px;--leafer-roi-size-zoom-button:36px;--leafer-roi-size-zoom-value:60px;--leafer-roi-font-size-hotkey:10px;--leafer-roi-padding-hotkey:1px 3px;--leafer-roi-padding-error:20px;--leafer-roi-padding-error-button:8px 16px;--leafer-roi-border-radius-tool-button:4px;--leafer-roi-border-radius-hotkey:2px;--leafer-roi-border-radius-overlay:8px;--leafer-roi-border-radius-zoom:8px;--leafer-roi-shadow-tool-button:0 2px 4px #0000001a;--leafer-roi-shadow-tool-button-active:0 2px 4px #007aff4d;--leafer-roi-shadow-tool-button-hover:0 4px 6px #0000001a;--leafer-roi-shadow-overlay:0 4px 12px #0000001a;--leafer-roi-shadow-zoom:0 2px 8px #00000026;--leafer-roi-transition-time:.2s;--leafer-roi-animation-gradient:2s}.roi-editor[data-v-9c4af5f9]{width:100%;height:100%}.canvas-container[data-v-9c4af5f9]{outline:none;width:100%;height:calc(100% - 55px);position:relative;overflow:hidden}.canvas-container[data-v-9c4af5f9]:focus{outline:2px solid var(--leafer-roi-color-primary);outline-offset:-2px}.loading-overlay[data-v-9c4af5f9]{background-color:var(--leafer-roi-color-background-light);border-radius:var(--leafer-roi-border-radius-overlay);box-shadow:var(--leafer-roi-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-9c4af5f9]{animation:gradientShift-9c4af5f9 var(--leafer-roi-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-9c4af5f9{0%{background-position:0%}50%{background-position:100%}to{background-position:0%}}.loading-text[data-v-9c4af5f9]{z-index:1;color:#fff;text-shadow:0 2px 4px #0003;font-size:16px;font-weight:500;position:relative}.error-overlay[data-v-9c4af5f9]{background-color:var(--leafer-roi-color-white);border-radius:var(--leafer-roi-border-radius-overlay);box-shadow:var(--leafer-roi-shadow-overlay);padding:var(--leafer-roi-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-9c4af5f9]{color:var(--leafer-roi-color-error);margin-bottom:20px;font-size:16px}.error-overlay button[data-v-9c4af5f9]{padding:var(--leafer-roi-padding-error-button);background-color:var(--leafer-roi-color-button);color:#fff;border-radius:var(--leafer-roi-border-radius-tool-button);cursor:pointer;border:none;font-size:14px}.error-overlay button[data-v-9c4af5f9]:hover{background-color:var(--leafer-roi-color-button-hover)}.zoom-controller[data-v-9c4af5f9]{background-color:var(--leafer-roi-color-white);border-radius:var(--leafer-roi-border-radius-zoom);box-shadow:var(--leafer-roi-shadow-zoom);z-index:100;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;overflow:hidden}.zoom-button[data-v-9c4af5f9]{width:var(--leafer-roi-size-zoom-button);height:var(--leafer-roi-size-zoom-button);background-color:var(--leafer-roi-color-white);color:var(--leafer-roi-color-text);cursor:pointer;transition:all var(--leafer-roi-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.zoom-button[data-v-9c4af5f9]:hover{background-color:var(--leafer-roi-color-background-light);color:var(--leafer-roi-color-primary)}.zoom-button[data-v-9c4af5f9]:active{background-color:#e0e0e0}.zoom-value[data-v-9c4af5f9]{min-width:var(--leafer-roi-size-zoom-value);height:var(--leafer-roi-size-zoom-button);line-height:var(--leafer-roi-size-zoom-button);text-align:center;color:var(--leafer-roi-color-text);cursor:pointer;border-left:1px solid var(--leafer-roi-color-border-light);border-right:1px solid var(--leafer-roi-color-border-light);transition:all var(--leafer-roi-transition-time) ease;font-size:14px;font-weight:500;position:relative}.zoom-value .hotkey-hint[data-v-9c4af5f9]{line-height:1}.zoom-value[data-v-9c4af5f9]:hover{background-color:var(--leafer-roi-color-background-light);color:var(--leafer-roi-color-primary)}.toolbar[data-v-9c4af5f9]{padding:var(--leafer-roi-padding-toolbar);background-color:var(--leafer-roi-color-background);border-top:1px solid var(--leafer-roi-color-border);justify-content:center;align-items:center;gap:10px;display:flex}.tool-button[data-v-9c4af5f9]{padding:var(--leafer-roi-padding-tool-button);border-radius:var(--leafer-roi-border-radius-tool-button);background-color:var(--leafer-roi-color-white);color:var(--leafer-roi-color-text);cursor:pointer;box-shadow:var(--leafer-roi-shadow-tool-button);transition:all var(--leafer-roi-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.tool-button[data-v-9c4af5f9]:hover{box-shadow:var(--leafer-roi-shadow-tool-button-hover);background-color:#e3e3e3;transform:translateY(-1px)}.tool-button[data-v-9c4af5f9]:active{box-shadow:var(--leafer-roi-shadow-tool-button);transform:translateY(0)}.tool-button.active[data-v-9c4af5f9]{background-color:var(--leafer-roi-color-primary);color:#fff;box-shadow:var(--leafer-roi-shadow-tool-button-active)}.tool-button svg[data-v-9c4af5f9]{width:var(--leafer-roi-size-tool-icon);height:var(--leafer-roi-size-tool-icon)}.hotkey-hint[data-v-9c4af5f9]{font-size:var(--leafer-roi-font-size-hotkey);color:var(--leafer-roi-color-text-tertiary);background-color:var(--leafer-roi-color-background);padding:var(--leafer-roi-padding-hotkey);border-radius:var(--leafer-roi-border-radius-hotkey);pointer-events:none;font-weight:700;position:absolute;bottom:2px;right:2px}.tool-button.active .hotkey-hint[data-v-9c4af5f9]{color:#fffc;background-color:#0003}.app[data-v-4b675dba]{max-width:1200px;margin:0 auto;padding:20px;font-family:Arial,sans-serif}h1[data-v-4b675dba]{text-align:center;margin-bottom:30px}.editor-container[data-v-4b675dba]{border:1px solid #ddd;border-radius:8px;width:100%;height:600px;margin-bottom:30px;overflow:hidden}.controls[data-v-4b675dba]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}.control-group[data-v-4b675dba]{margin-bottom:15px}label[data-v-4b675dba]{margin-bottom:5px;font-weight:700;display:block}input[data-v-4b675dba]{border:1px solid #ddd;border-radius:4px;width:100%;margin-bottom:10px;padding:8px}button[data-v-4b675dba]{color:#fff;cursor:pointer;background-color:#007bff;border:none;border-radius:4px;margin-right:10px;padding:8px 16px}button[data-v-4b675dba]:hover{background-color:#0069d9}.output[data-v-4b675dba]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}pre[data-v-4b675dba]{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-4b675dba]{background-color:#f5f5f5;border-radius:8px;padding:20px}
|