@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.
@@ -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 ref="canvasContainer" class="canvas-container" tabindex="0">
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: true,
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 ? imageSrc : props.imageSource.url;
452
- if (!app || !_imageSrc) return;
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.id,
505
- url: props.imageSource.url,
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.url || '',
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.url || '',
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: calc(100% - 55px);
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: 16px;
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: 50%;
1436
- left: 50%;
1437
- transform: translate(-50%, -50%);
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: 20px;
1646
+ margin-bottom: 24px;
1452
1647
  color: var(--leafer-point-color-error);
1453
- font-size: 16px;
1648
+ font-size: 18px;
1649
+ font-weight: 500;
1454
1650
  }
1455
1651
 
1456
1652
  .error-overlay button {
1457
- padding: var(--leafer-point-padding-error-button);
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: 14px;
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}