@zzalai/leafer-point-annotation 1.0.0 → 1.1.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/README.md +2 -1
- package/README_EN.md +2 -1
- package/docs/assets/{index-L8gL3x2V.js → index-CPn8AE3g.js} +1 -1
- package/docs/assets/index-Dqqq7qvI.css +1 -0
- package/docs/index.html +2 -2
- package/package.json +3 -2
- package/project-docs/ARCHITECTURE.md +47 -4
- package/project-docs/IMPLEMENTATION_PLAN.md +37 -19
- package/project-docs/REQUIREMENTS.md +27 -8
- package/src/App.vue +8 -5
- package/src/components/PointAnnotation.vue +224 -25
- package/docs/assets/index-DGiYiG5f.css +0 -1
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
class="point-annotation"
|
|
4
|
+
:class="{ 'has-image': showTools }"
|
|
4
5
|
@focus="isCanvasFocused = true"
|
|
5
6
|
@blur="isCanvasFocused = false"
|
|
6
7
|
@mouseenter="isMouseOverCanvas = true"
|
|
7
8
|
@mouseleave="isMouseOverCanvas = false"
|
|
8
9
|
>
|
|
9
10
|
<!-- 画布容器 -->
|
|
10
|
-
<div
|
|
11
|
+
<div
|
|
12
|
+
ref="canvasContainer"
|
|
13
|
+
class="canvas-container"
|
|
14
|
+
tabindex="0"
|
|
15
|
+
@dragover="handleDragOver"
|
|
16
|
+
@dragleave="handleDragLeave"
|
|
17
|
+
@drop="handleDrop"
|
|
18
|
+
>
|
|
11
19
|
<!-- 加载占位 -->
|
|
12
20
|
<div
|
|
13
21
|
v-if="loadStatus === 'loading'"
|
|
@@ -17,14 +25,45 @@
|
|
|
17
25
|
<div class="loading-text">图片加载中</div>
|
|
18
26
|
</div>
|
|
19
27
|
|
|
28
|
+
<!-- 空状态/上传区域 -->
|
|
29
|
+
<div v-if="loadStatus === 'idle'" class="upload-overlay" :class="{ 'drag-over': isDragOver }">
|
|
30
|
+
<input
|
|
31
|
+
ref="fileInputRef"
|
|
32
|
+
type="file"
|
|
33
|
+
accept="image/*"
|
|
34
|
+
style="display: none"
|
|
35
|
+
@change="handleFileUpload"
|
|
36
|
+
/>
|
|
37
|
+
<div class="upload-icon">
|
|
38
|
+
<svg
|
|
39
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
40
|
+
width="48"
|
|
41
|
+
height="48"
|
|
42
|
+
viewBox="0 0 24 24"
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
stroke-width="1.5"
|
|
46
|
+
stroke-linecap="round"
|
|
47
|
+
stroke-linejoin="round"
|
|
48
|
+
>
|
|
49
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
50
|
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
51
|
+
<polyline points="21 15 16 10 5 21"></polyline>
|
|
52
|
+
</svg>
|
|
53
|
+
</div>
|
|
54
|
+
<p class="upload-text">点击选择本地图片</p>
|
|
55
|
+
<p class="upload-hint">或拖拽图片到此处</p>
|
|
56
|
+
<button class="upload-button" @click="openFileDialog">选择图片</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
20
59
|
<!-- 错误状态 -->
|
|
21
60
|
<div v-if="loadStatus === 'error'" class="error-overlay">
|
|
22
61
|
<p>加载失败</p>
|
|
23
62
|
<button @click="loadImage()">重试</button>
|
|
24
63
|
</div>
|
|
25
64
|
|
|
26
|
-
<!-- 缩放控制器 -->
|
|
27
|
-
<div class="zoom-controller">
|
|
65
|
+
<!-- 缩放控制器 - 只在有图片时显示 -->
|
|
66
|
+
<div v-if="showTools" class="zoom-controller">
|
|
28
67
|
<button class="zoom-button" title="缩小 (Ctrl+-)" @click="zoomOut">
|
|
29
68
|
<svg
|
|
30
69
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -69,8 +108,8 @@
|
|
|
69
108
|
</div>
|
|
70
109
|
</div>
|
|
71
110
|
|
|
72
|
-
<!-- 工具栏 -->
|
|
73
|
-
<div class="toolbar">
|
|
111
|
+
<!-- 工具栏 - 只在有图片时显示 -->
|
|
112
|
+
<div v-if="showTools" class="toolbar">
|
|
74
113
|
<button
|
|
75
114
|
class="tool-button"
|
|
76
115
|
:class="{ active: currentTool === 'select' }"
|
|
@@ -297,7 +336,8 @@ export interface OptionsSource {
|
|
|
297
336
|
const props = defineProps({
|
|
298
337
|
imageSource: {
|
|
299
338
|
type: Object as () => ImageSource,
|
|
300
|
-
required:
|
|
339
|
+
required: false,
|
|
340
|
+
default: null,
|
|
301
341
|
},
|
|
302
342
|
options: {
|
|
303
343
|
type: Object as () => OptionsSource,
|
|
@@ -315,9 +355,13 @@ const emit = defineEmits([
|
|
|
315
355
|
]);
|
|
316
356
|
|
|
317
357
|
const canvasContainer = ref<HTMLElement | undefined>(undefined);
|
|
358
|
+
const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
318
359
|
const loadStatus = ref<"idle" | "loading" | "success" | "error">("idle");
|
|
319
360
|
const imageWidth = ref<number | null>(null);
|
|
320
361
|
const imageHeight = ref<number | null>(null);
|
|
362
|
+
const hasLocalImage = ref(false);
|
|
363
|
+
const localImageUrl = ref<string>('');
|
|
364
|
+
const isDragOver = ref(false);
|
|
321
365
|
let app: App | null = null;
|
|
322
366
|
let imageBox: Image | null = null;
|
|
323
367
|
const contentLayer = new Group({ name: "contentLayer" });
|
|
@@ -339,6 +383,9 @@ const brushButtonRect = ref<DOMRect | null>(null);
|
|
|
339
383
|
const pointAnnotations = ref<PointAnnotation[]>([]);
|
|
340
384
|
const pointCounter = ref(1);
|
|
341
385
|
|
|
386
|
+
// 是否显示工具界面
|
|
387
|
+
const showTools = computed(() => loadStatus.value === 'success');
|
|
388
|
+
|
|
342
389
|
// 点标注样式配置
|
|
343
390
|
const pointStyle = computed<PointStyle>(() => ({
|
|
344
391
|
...DEFAULT_POINT_STYLE,
|
|
@@ -372,6 +419,31 @@ watch(
|
|
|
372
419
|
}
|
|
373
420
|
);
|
|
374
421
|
|
|
422
|
+
// 监听 props.imageSource 变化,重置本地图片状态
|
|
423
|
+
watch(
|
|
424
|
+
() => props.imageSource?.url,
|
|
425
|
+
(newUrl, oldUrl) => {
|
|
426
|
+
if (newUrl) {
|
|
427
|
+
hasLocalImage.value = false;
|
|
428
|
+
localImageUrl.value = '';
|
|
429
|
+
loadImage(newUrl);
|
|
430
|
+
} else if (oldUrl && !newUrl) {
|
|
431
|
+
// 如果原来有图片,现在没有了,清空画布
|
|
432
|
+
hasLocalImage.value = false;
|
|
433
|
+
localImageUrl.value = '';
|
|
434
|
+
clearAllAnnotationsAndBrush();
|
|
435
|
+
// 移除图片
|
|
436
|
+
if (imageBox) {
|
|
437
|
+
contentLayer.clear();
|
|
438
|
+
imageBox.destroy();
|
|
439
|
+
imageBox = null;
|
|
440
|
+
}
|
|
441
|
+
loadStatus.value = 'idle';
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{ immediate: true }
|
|
445
|
+
);
|
|
446
|
+
|
|
375
447
|
// 笔刷相关状态
|
|
376
448
|
let canvasBrush: CanvasBrush | null = null;
|
|
377
449
|
const isDrawing = ref(false);
|
|
@@ -448,8 +520,14 @@ const preloadImageSize = (
|
|
|
448
520
|
};
|
|
449
521
|
|
|
450
522
|
const loadImage = async (imageSrc?: string | undefined) => {
|
|
451
|
-
const _imageSrc = imageSrc
|
|
452
|
-
|
|
523
|
+
const _imageSrc = imageSrc
|
|
524
|
+
? imageSrc
|
|
525
|
+
: (hasLocalImage.value ? localImageUrl.value : (props.imageSource?.url || ''));
|
|
526
|
+
|
|
527
|
+
if (!app || !_imageSrc) {
|
|
528
|
+
loadStatus.value = "idle";
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
453
531
|
|
|
454
532
|
if (imageBox) {
|
|
455
533
|
contentLayer.clear();
|
|
@@ -501,17 +579,70 @@ const loadImage = async (imageSrc?: string | undefined) => {
|
|
|
501
579
|
|
|
502
580
|
const getImageInfo = () => {
|
|
503
581
|
return {
|
|
504
|
-
id: props.imageSource
|
|
505
|
-
url: props.imageSource
|
|
582
|
+
id: hasLocalImage.value ? 'local-image' : (props.imageSource?.id || ''),
|
|
583
|
+
url: hasLocalImage.value ? localImageUrl.value : (props.imageSource?.url || ''),
|
|
506
584
|
width: imageWidth.value,
|
|
507
585
|
height: imageHeight.value,
|
|
508
586
|
};
|
|
509
587
|
};
|
|
510
588
|
|
|
589
|
+
const handleFileUpload = (event: Event) => {
|
|
590
|
+
const target = event.target as HTMLInputElement;
|
|
591
|
+
const file = target.files?.[0];
|
|
592
|
+
if (file && file.type.startsWith('image/')) {
|
|
593
|
+
const reader = new FileReader();
|
|
594
|
+
reader.onload = (e) => {
|
|
595
|
+
const result = e.target?.result as string;
|
|
596
|
+
localImageUrl.value = result;
|
|
597
|
+
hasLocalImage.value = true;
|
|
598
|
+
loadImage(result);
|
|
599
|
+
};
|
|
600
|
+
reader.readAsDataURL(file);
|
|
601
|
+
}
|
|
602
|
+
target.value = '';
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const openFileDialog = () => {
|
|
606
|
+
fileInputRef.value?.click();
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const handleDragOver = (event: DragEvent) => {
|
|
610
|
+
event.preventDefault();
|
|
611
|
+
event.stopPropagation();
|
|
612
|
+
isDragOver.value = true;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const handleDragLeave = (event: DragEvent) => {
|
|
616
|
+
event.preventDefault();
|
|
617
|
+
event.stopPropagation();
|
|
618
|
+
isDragOver.value = false;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const handleDrop = (event: DragEvent) => {
|
|
622
|
+
event.preventDefault();
|
|
623
|
+
event.stopPropagation();
|
|
624
|
+
isDragOver.value = false;
|
|
625
|
+
|
|
626
|
+
const files = event.dataTransfer?.files;
|
|
627
|
+
if (files && files.length > 0) {
|
|
628
|
+
const file = files[0];
|
|
629
|
+
if (file.type.startsWith('image/')) {
|
|
630
|
+
const reader = new FileReader();
|
|
631
|
+
reader.onload = (e) => {
|
|
632
|
+
const result = e.target?.result as string;
|
|
633
|
+
localImageUrl.value = result;
|
|
634
|
+
hasLocalImage.value = true;
|
|
635
|
+
loadImage(result);
|
|
636
|
+
};
|
|
637
|
+
reader.readAsDataURL(file);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
511
642
|
const exportCanvasJSON = (): string => {
|
|
512
643
|
const exportData = {
|
|
513
644
|
version: '1.0',
|
|
514
|
-
imageUrl: props.imageSource
|
|
645
|
+
imageUrl: hasLocalImage.value ? localImageUrl.value : (props.imageSource?.url || ''),
|
|
515
646
|
imageWidth: imageWidth.value,
|
|
516
647
|
imageHeight: imageHeight.value,
|
|
517
648
|
pointAnnotations: [...pointAnnotations.value],
|
|
@@ -588,7 +719,7 @@ const exportMaskImage = (format?: 'png' | 'jpeg' | 'jpg', foregroundColor?: 'bla
|
|
|
588
719
|
const exportCOCO = (): string => {
|
|
589
720
|
const coco = exportCOCOFormat(
|
|
590
721
|
pointAnnotations.value,
|
|
591
|
-
props.imageSource
|
|
722
|
+
hasLocalImage.value ? localImageUrl.value : (props.imageSource?.url || ''),
|
|
592
723
|
imageWidth.value || 0,
|
|
593
724
|
imageHeight.value || 0
|
|
594
725
|
);
|
|
@@ -1329,10 +1460,12 @@ declare global {
|
|
|
1329
1460
|
--leafer-point-color-text: #333;
|
|
1330
1461
|
--leafer-point-color-text-secondary: #666;
|
|
1331
1462
|
--leafer-point-color-text-tertiary: #999999;
|
|
1463
|
+
--leafer-point-color-placeholder: #999999;
|
|
1332
1464
|
--leafer-point-color-border: #ddd;
|
|
1333
1465
|
--leafer-point-color-border-light: #e0e0e0;
|
|
1334
1466
|
--leafer-point-color-error: #e74c3c;
|
|
1335
1467
|
--leafer-point-color-button: #3498db;
|
|
1468
|
+
--leafer-point-color-button-rgb: 52, 152, 219;
|
|
1336
1469
|
--leafer-point-color-button-hover: #2980b9;
|
|
1337
1470
|
|
|
1338
1471
|
--leafer-point-padding-toolbar: 10px;
|
|
@@ -1368,12 +1501,16 @@ declare global {
|
|
|
1368
1501
|
|
|
1369
1502
|
.canvas-container {
|
|
1370
1503
|
width: 100%;
|
|
1371
|
-
height:
|
|
1504
|
+
height: 100%;
|
|
1372
1505
|
position: relative;
|
|
1373
1506
|
overflow: hidden;
|
|
1374
1507
|
outline: none;
|
|
1375
1508
|
}
|
|
1376
1509
|
|
|
1510
|
+
.point-annotation.has-image .canvas-container {
|
|
1511
|
+
height: calc(100% - 55px);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1377
1514
|
.canvas-container:focus {
|
|
1378
1515
|
outline: 2px solid var(--leafer-point-color-primary);
|
|
1379
1516
|
outline-offset: -2px;
|
|
@@ -1425,46 +1562,108 @@ declare global {
|
|
|
1425
1562
|
position: relative;
|
|
1426
1563
|
z-index: 1;
|
|
1427
1564
|
color: white;
|
|
1428
|
-
font-size:
|
|
1565
|
+
font-size: 18px;
|
|
1429
1566
|
font-weight: 500;
|
|
1430
1567
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
1431
1568
|
}
|
|
1432
1569
|
|
|
1570
|
+
.upload-overlay {
|
|
1571
|
+
position: absolute;
|
|
1572
|
+
top: 0;
|
|
1573
|
+
left: 0;
|
|
1574
|
+
right: 0;
|
|
1575
|
+
bottom: 0;
|
|
1576
|
+
background-color: var(--leafer-point-color-white);
|
|
1577
|
+
display: flex;
|
|
1578
|
+
flex-direction: column;
|
|
1579
|
+
justify-content: center;
|
|
1580
|
+
align-items: center;
|
|
1581
|
+
z-index: 1000;
|
|
1582
|
+
border: 3px dashed var(--leafer-point-color-border);
|
|
1583
|
+
transition: all 0.2s ease;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.upload-icon {
|
|
1587
|
+
color: var(--leafer-point-color-placeholder);
|
|
1588
|
+
margin-bottom: 24px;
|
|
1589
|
+
transform: scale(1.2);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
.upload-text {
|
|
1593
|
+
color: var(--leafer-point-color-text);
|
|
1594
|
+
font-size: 18px;
|
|
1595
|
+
font-weight: 500;
|
|
1596
|
+
margin-bottom: 12px;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
.upload-hint {
|
|
1600
|
+
color: var(--leafer-point-color-placeholder);
|
|
1601
|
+
font-size: 14px;
|
|
1602
|
+
margin-bottom: 28px;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
.upload-button {
|
|
1606
|
+
padding: 12px 32px;
|
|
1607
|
+
background-color: var(--leafer-point-color-button);
|
|
1608
|
+
color: white;
|
|
1609
|
+
border: none;
|
|
1610
|
+
border-radius: var(--leafer-point-border-radius-tool-button);
|
|
1611
|
+
cursor: pointer;
|
|
1612
|
+
font-size: 15px;
|
|
1613
|
+
font-weight: 500;
|
|
1614
|
+
transition: all 0.2s ease;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
.upload-button:hover {
|
|
1618
|
+
background-color: var(--leafer-point-color-button-hover);
|
|
1619
|
+
transform: translateY(-1px);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
.upload-overlay.drag-over {
|
|
1623
|
+
border-color: var(--leafer-point-color-button);
|
|
1624
|
+
background-color: rgba(var(--leafer-point-color-button-rgb), 0.05);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
.upload-overlay.drag-over .upload-icon {
|
|
1628
|
+
color: var(--leafer-point-color-button);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1433
1631
|
.error-overlay {
|
|
1434
1632
|
position: absolute;
|
|
1435
|
-
top:
|
|
1436
|
-
left:
|
|
1437
|
-
|
|
1633
|
+
top: 0;
|
|
1634
|
+
left: 0;
|
|
1635
|
+
right: 0;
|
|
1636
|
+
bottom: 0;
|
|
1438
1637
|
background-color: var(--leafer-point-color-white);
|
|
1439
|
-
border-radius: var(--leafer-point-border-radius-overlay);
|
|
1440
|
-
box-shadow: var(--leafer-point-shadow-overlay);
|
|
1441
|
-
padding: var(--leafer-point-padding-error);
|
|
1442
1638
|
display: flex;
|
|
1443
1639
|
flex-direction: column;
|
|
1444
1640
|
justify-content: center;
|
|
1445
1641
|
align-items: center;
|
|
1446
1642
|
z-index: 1000;
|
|
1447
|
-
min-width: 200px;
|
|
1448
1643
|
}
|
|
1449
1644
|
|
|
1450
1645
|
.error-overlay p {
|
|
1451
|
-
margin-bottom:
|
|
1646
|
+
margin-bottom: 24px;
|
|
1452
1647
|
color: var(--leafer-point-color-error);
|
|
1453
|
-
font-size:
|
|
1648
|
+
font-size: 18px;
|
|
1649
|
+
font-weight: 500;
|
|
1454
1650
|
}
|
|
1455
1651
|
|
|
1456
1652
|
.error-overlay button {
|
|
1457
|
-
padding:
|
|
1653
|
+
padding: 12px 32px;
|
|
1458
1654
|
background-color: var(--leafer-point-color-button);
|
|
1459
1655
|
color: white;
|
|
1460
1656
|
border: none;
|
|
1461
1657
|
border-radius: var(--leafer-point-border-radius-tool-button);
|
|
1462
1658
|
cursor: pointer;
|
|
1463
|
-
font-size:
|
|
1659
|
+
font-size: 15px;
|
|
1660
|
+
font-weight: 500;
|
|
1661
|
+
transition: all 0.2s ease;
|
|
1464
1662
|
}
|
|
1465
1663
|
|
|
1466
1664
|
.error-overlay button:hover {
|
|
1467
1665
|
background-color: var(--leafer-point-color-button-hover);
|
|
1666
|
+
transform: translateY(-1px);
|
|
1468
1667
|
}
|
|
1469
1668
|
|
|
1470
1669
|
.zoom-controller {
|
|
@@ -1 +0,0 @@
|
|
|
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}
|