@vue-lynx-example/7guis 0.2.0 → 0.2.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.
@@ -1,27 +1,40 @@
1
1
  <script setup>
2
- import { ref, shallowReactive } from 'vue'
2
+ import { ref, shallowReactive, computed } from 'vue'
3
3
 
4
4
  const history = shallowReactive([[]])
5
5
  const historyIndex = ref(0)
6
6
  const circles = ref([])
7
7
  const selectedId = ref(-1)
8
- const adjusting = ref(false)
8
+ const showModal = ref(false)
9
9
 
10
10
  let nextId = 0
11
11
 
12
+ // Modal slider state
13
+ const sliderWidth = 200 // Width of slider track in px
14
+ const minRadius = 5
15
+ const maxRadius = 150
16
+
17
+ // Double tap detection
18
+ const lastTapTime = ref(0)
19
+ const lastTapId = ref(-1)
20
+ const DOUBLE_TAP_DELAY = 300 // ms
21
+
22
+ const selectedCircle = computed(() => {
23
+ return circles.value.find(c => c.id === selectedId.value)
24
+ })
25
+
12
26
  function onCanvasTap(e) {
13
- if (adjusting.value) {
14
- adjusting.value = false
15
- selectedId.value = -1
16
- push()
27
+ // If modal is open, close it without creating new circle
28
+ if (showModal.value) {
29
+ closeModal()
17
30
  return
18
31
  }
19
32
 
20
- // Check if tapped on an existing circle
33
+ // Get touch coordinates - relative to the full view
21
34
  const x = e.detail?.x ?? e.touches?.[0]?.pageX ?? 0
22
35
  const y = e.detail?.y ?? e.touches?.[0]?.pageY ?? 0
23
- console.log('[circle-drawer] tap event:', { x, y, detail: e.detail, type: e.type, raw: { clientX: e.clientX, clientY: e.clientY } })
24
36
 
37
+ // Check if tapped on an existing circle
25
38
  const hit = [...circles.value].reverse().find((c) => {
26
39
  const dx = c.x - x
27
40
  const dy = c.y - y
@@ -29,7 +42,7 @@ function onCanvasTap(e) {
29
42
  })
30
43
 
31
44
  if (hit) {
32
- selectedId.value = hit.id
45
+ handleCircleTap(hit.id)
33
46
  } else {
34
47
  circles.value.push({ id: nextId++, x, y, r: 30 })
35
48
  selectedId.value = -1
@@ -37,37 +50,108 @@ function onCanvasTap(e) {
37
50
  }
38
51
  }
39
52
 
40
- function startAdjust() {
41
- if (selectedId.value >= 0) {
42
- adjusting.value = true
53
+ function handleCircleTap(circleId) {
54
+ const now = Date.now()
55
+ const timeSinceLastTap = now - lastTapTime.value
56
+
57
+ if (lastTapId.value === circleId && timeSinceLastTap < DOUBLE_TAP_DELAY) {
58
+ // Double tap detected - open modal
59
+ showModal.value = true
60
+ lastTapId.value = -1
61
+ lastTapTime.value = 0
62
+ } else {
63
+ // Single tap - just select
64
+ selectedId.value = circleId
65
+ lastTapId.value = circleId
66
+ lastTapTime.value = now
43
67
  }
44
68
  }
45
69
 
46
- function adjustRadius(delta) {
47
- const circle = circles.value.find((c) => c.id === selectedId.value)
48
- if (circle) {
49
- circle.r = Math.max(5, Math.min(150, circle.r + delta))
70
+ function onCircleTap(e, circle) {
71
+ // Use catchtap to stop event bubbling to parent
72
+ e?.stopPropagation?.()
73
+ handleCircleTap(circle.id)
74
+ }
75
+
76
+ function closeModal() {
77
+ if (showModal.value) {
78
+ showModal.value = false
79
+ push()
50
80
  }
51
81
  }
52
82
 
83
+ function onModalTap(e) {
84
+ // Stop propagation to prevent canvas interaction
85
+ e?.stopPropagation?.()
86
+ }
87
+
88
+ function onOverlayTap(e) {
89
+ // Close modal when tapping overlay background
90
+ e?.stopPropagation?.()
91
+ closeModal()
92
+ }
93
+
94
+ // Slider drag handling
95
+ const isDragging = ref(false)
96
+ const sliderValue = computed({
97
+ get() {
98
+ return selectedCircle.value?.r ?? minRadius
99
+ },
100
+ set(val) {
101
+ if (selectedCircle.value) {
102
+ selectedCircle.value.r = Math.max(minRadius, Math.min(maxRadius, val))
103
+ }
104
+ }
105
+ })
106
+
107
+ function onSliderTouchStart(e) {
108
+ isDragging.value = true
109
+ updateSliderFromTouch(e)
110
+ }
111
+
112
+ function onSliderTouchMove(e) {
113
+ if (isDragging.value) {
114
+ updateSliderFromTouch(e)
115
+ }
116
+ }
117
+
118
+ function onSliderTouchEnd(e) {
119
+ isDragging.value = false
120
+ }
121
+
122
+ function updateSliderFromTouch(e) {
123
+ const touch = e.touches?.[0] ?? e.changedTouches?.[0]
124
+ if (!touch) return
125
+
126
+ // Calculate position relative to slider track
127
+ const trackLeft = (globalThis.window?.innerWidth ?? 375) / 2 - sliderWidth / 2
128
+ const relativeX = touch.pageX - trackLeft
129
+ const percentage = Math.max(0, Math.min(1, relativeX / sliderWidth))
130
+ sliderValue.value = minRadius + percentage * (maxRadius - minRadius)
131
+ }
132
+
53
133
  function push() {
54
134
  history.length = ++historyIndex.value
55
135
  history.push(circles.value.map((c) => ({ ...c })))
56
136
  }
57
137
 
58
- function undo() {
138
+ function undo(e) {
139
+ // Use catchtap to prevent event from reaching canvas
140
+ e?.stopPropagation?.()
59
141
  if (historyIndex.value > 0) {
60
142
  circles.value = history[--historyIndex.value].map((c) => ({ ...c }))
61
143
  selectedId.value = -1
62
- adjusting.value = false
144
+ showModal.value = false
63
145
  }
64
146
  }
65
147
 
66
- function redo() {
148
+ function redo(e) {
149
+ // Use catchtap to prevent event from reaching canvas
150
+ e?.stopPropagation?.()
67
151
  if (historyIndex.value < history.length - 1) {
68
152
  circles.value = history[++historyIndex.value].map((c) => ({ ...c }))
69
153
  selectedId.value = -1
70
- adjusting.value = false
154
+ showModal.value = false
71
155
  }
72
156
  }
73
157
  </script>
@@ -78,7 +162,7 @@ function redo() {
78
162
  :style="{ width: '100%', height: '100vh', minHeight: '400px', backgroundColor: '#f0f0f0', position: 'relative' }"
79
163
  @tap="onCanvasTap"
80
164
  >
81
- <!-- Hint text -->
165
+ <!-- Hint text for empty state -->
82
166
  <text
83
167
  v-if="circles.length === 0"
84
168
  :style="{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', color: '#bbb', fontSize: 14, textAlign: 'center' }"
@@ -97,10 +181,11 @@ function redo() {
97
181
  width: circle.r * 2 + 'px',
98
182
  height: circle.r * 2 + 'px',
99
183
  borderRadius: circle.r + 'px',
100
- backgroundColor: circle.id === selectedId ? '#ccc' : '#fff',
101
- borderWidth: 1,
102
- borderColor: '#333',
184
+ backgroundColor: circle.id === selectedId ? '#0077ff' : '#fff',
185
+ borderWidth: circle.id === selectedId ? 3 : 1,
186
+ borderColor: circle.id === selectedId ? '#0055cc' : '#333',
103
187
  }"
188
+ @tap="(e) => onCircleTap(e, circle)"
104
189
  />
105
190
 
106
191
  <!-- Undo / Redo overlaid at the top -->
@@ -119,33 +204,109 @@ function redo() {
119
204
  </view>
120
205
  </view>
121
206
 
122
- <!-- Adjust controls overlaid at the bottom -->
123
- <view v-if="selectedId >= 0" :style="{ position: 'absolute', bottom: 20, left: 0, right: 0, alignItems: 'center', gap: 8 }">
124
- <view v-if="!adjusting">
207
+ <!-- Hint for interaction -->
208
+ <text
209
+ v-if="circles.length > 0 && !showModal"
210
+ :style="{ position: 'absolute', bottom: 20, left: 0, right: 0, textAlign: 'center', color: '#666', fontSize: 12 }"
211
+ >
212
+ Double-tap a circle to adjust radius
213
+ </text>
214
+
215
+ <!-- Modal Overlay -->
216
+ <view
217
+ v-if="showModal"
218
+ :style="{
219
+ position: 'absolute',
220
+ top: 0,
221
+ left: 0,
222
+ right: 0,
223
+ bottom: 0,
224
+ backgroundColor: 'rgba(0,0,0,0.5)',
225
+ display: 'flex',
226
+ alignItems: 'center',
227
+ justifyContent: 'center',
228
+ zIndex: 100,
229
+ }"
230
+ @tap="onOverlayTap"
231
+ >
232
+ <!-- Modal Content -->
233
+ <view
234
+ :style="{
235
+ backgroundColor: '#fff',
236
+ borderRadius: 12,
237
+ padding: '24px 32px',
238
+ alignItems: 'center',
239
+ minWidth: '280px',
240
+ }"
241
+ @tap="onModalTap"
242
+ >
243
+ <text :style="{ fontSize: 18, fontWeight: 'bold', color: '#333', marginBottom: 16 }">
244
+ Adjust Radius
245
+ </text>
246
+
247
+ <text :style="{ fontSize: 24, fontWeight: 'bold', color: '#0077ff', marginBottom: 20 }">
248
+ {{ Math.round(selectedCircle?.r ?? 0) }}px
249
+ </text>
250
+
251
+ <!-- Slider Track -->
125
252
  <view
126
- :style="{ padding: '8px 20px', backgroundColor: '#0077ff', borderRadius: 6 }"
127
- @tap="startAdjust"
253
+ :style="{
254
+ width: sliderWidth + 'px',
255
+ height: 6,
256
+ backgroundColor: '#e0e0e0',
257
+ borderRadius: 3,
258
+ position: 'relative',
259
+ }"
260
+ @touchstart="onSliderTouchStart"
261
+ @touchmove="onSliderTouchMove"
262
+ @touchend="onSliderTouchEnd"
128
263
  >
129
- <text :style="{ color: '#fff', fontSize: 14 }">Adjust Radius</text>
130
- </view>
131
- </view>
132
- <view v-else :style="{ alignItems: 'center', gap: 6 }">
133
- <text :style="{ fontSize: 14 }">
134
- Radius: {{ circles.find(c => c.id === selectedId)?.r ?? 0 }}
135
- </text>
136
- <view :style="{ display: 'flex', flexDirection: 'row', gap: 8 }">
264
+ <!-- Filled portion -->
137
265
  <view
138
- :style="{ padding: '6px 16px', backgroundColor: '#eee', borderRadius: 4 }"
139
- @tap="adjustRadius(-5)"
140
- >
141
- <text :style="{ fontSize: 16 }">-</text>
142
- </view>
266
+ :style="{
267
+ position: 'absolute',
268
+ left: 0,
269
+ top: 0,
270
+ height: 6,
271
+ width: (((selectedCircle?.r ?? minRadius) - minRadius) / (maxRadius - minRadius)) * sliderWidth + 'px',
272
+ backgroundColor: '#0077ff',
273
+ borderRadius: 3,
274
+ }"
275
+ />
276
+ <!-- Thumb -->
143
277
  <view
144
- :style="{ padding: '6px 16px', backgroundColor: '#eee', borderRadius: 4 }"
145
- @tap="adjustRadius(5)"
146
- >
147
- <text :style="{ fontSize: 16 }">+</text>
148
- </view>
278
+ :style="{
279
+ position: 'absolute',
280
+ top: -7,
281
+ left: (((selectedCircle?.r ?? minRadius) - minRadius) / (maxRadius - minRadius)) * sliderWidth - 10 + 'px',
282
+ width: 20,
283
+ height: 20,
284
+ backgroundColor: '#0077ff',
285
+ borderRadius: 10,
286
+ borderWidth: 2,
287
+ borderColor: '#fff',
288
+ boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
289
+ }"
290
+ />
291
+ </view>
292
+
293
+ <!-- Slider Labels -->
294
+ <view :style="{ width: sliderWidth + 'px', display: 'flex', flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 }">
295
+ <text :style="{ fontSize: 12, color: '#999' }">{{ minRadius }}px</text>
296
+ <text :style="{ fontSize: 12, color: '#999' }">{{ maxRadius }}px</text>
297
+ </view>
298
+
299
+ <!-- Done Button -->
300
+ <view
301
+ :style="{
302
+ marginTop: 24,
303
+ padding: '10px 32px',
304
+ backgroundColor: '#0077ff',
305
+ borderRadius: 6,
306
+ }"
307
+ @tap="closeModal"
308
+ >
309
+ <text :style="{ color: '#fff', fontSize: 16, fontWeight: 'bold' }">Done</text>
149
310
  </view>
150
311
  </view>
151
312
  </view>