@@ -10,6 +10,7 @@ import { colord } from "colord";
10
10
import { useClipboard , watchThrottled } from ' @vueuse/core'
11
11
import { computed , onMounted , ref , shallowRef } from ' vue'
12
12
import { Note } from ' tonal'
13
+ import { useGesture } from ' @vueuse/gesture'
13
14
14
15
const props = defineProps ({
15
16
letters: { type: Boolean , default: true },
@@ -75,6 +76,115 @@ watchThrottled(loaded, l => {
75
76
console .error (' Loaded color scheme is not valid' )
76
77
}
77
78
})
79
+
80
+ const svg = ref ()
81
+ const currentNote = ref (null )
82
+ const touchPoints = new Map ()
83
+
84
+ useGesture ({
85
+ onTouchstart: handleTouchStart,
86
+ onTouchmove: handleTouchMove,
87
+ onTouchend: handleTouchEnd,
88
+ onTouchcancel: handleTouchEnd,
89
+ onDrag : ({ event , first, last, active }) => {
90
+ event .preventDefault ()
91
+ const element = document .elementFromPoint (event .clientX , event .clientY )
92
+ if (! element) return
93
+
94
+ const keyElement = element .closest (' .key' )
95
+ if (! keyElement) return
96
+
97
+ const noteData = flower .value [parseInt (keyElement .dataset .pitch )]
98
+ if (! noteData) return
99
+
100
+ if (first) {
101
+ playNote (noteData .midi )
102
+ currentNote .value = noteData .midi
103
+ } else if (last) {
104
+ stopNote (currentNote .value )
105
+ currentNote .value = null
106
+ } else if (active && currentNote .value !== noteData .midi ) {
107
+ stopNote (currentNote .value )
108
+ playNote (noteData .midi )
109
+ currentNote .value = noteData .midi
110
+ }
111
+ },
112
+ onDragEnd : () => {
113
+ if (currentNote .value !== null ) {
114
+ stopNote (currentNote .value )
115
+ currentNote .value = null
116
+ }
117
+ }
118
+ }, {
119
+ domTarget: svg,
120
+ eventOptions: { passive: false },
121
+ triggerAllEvents: true ,
122
+ dragDelay: 0 ,
123
+ })
124
+
125
+ function handleTouchStart ({ event }) {
126
+ event .preventDefault ()
127
+ Array .from (event .changedTouches ).forEach (touch => {
128
+ const element = document .elementFromPoint (touch .clientX , touch .clientY )
129
+ if (! element) return
130
+
131
+ const keyElement = element .closest (' .key' )
132
+ if (! keyElement) return
133
+
134
+ const noteData = flower .value [parseInt (keyElement .dataset .pitch )]
135
+ if (! noteData) return
136
+
137
+ touchPoints .set (touch .identifier , noteData .midi )
138
+ playNote (noteData .midi )
139
+ })
140
+ }
141
+
142
+ function handleTouchMove ({ event }) {
143
+ event .preventDefault ()
144
+ Array .from (event .changedTouches ).forEach (touch => {
145
+ const oldNote = touchPoints .get (touch .identifier )
146
+ const element = document .elementFromPoint (touch .clientX , touch .clientY )
147
+ if (! element) {
148
+ if (oldNote !== undefined ) {
149
+ stopNote (oldNote)
150
+ touchPoints .delete (touch .identifier )
151
+ }
152
+ return
153
+ }
154
+
155
+ const keyElement = element .closest (' .key' )
156
+ if (! keyElement) {
157
+ if (oldNote !== undefined ) {
158
+ stopNote (oldNote)
159
+ touchPoints .delete (touch .identifier )
160
+ }
161
+ return
162
+ }
163
+
164
+ const noteData = flower .value [parseInt (keyElement .dataset .pitch )]
165
+ if (! noteData) return
166
+
167
+ const newNote = noteData .midi
168
+ if (newNote !== oldNote) {
169
+ if (oldNote !== undefined ) {
170
+ stopNote (oldNote)
171
+ }
172
+ touchPoints .set (touch .identifier , newNote)
173
+ playNote (newNote)
174
+ }
175
+ })
176
+ }
177
+
178
+ function handleTouchEnd ({ event }) {
179
+ event .preventDefault ()
180
+ Array .from (event .changedTouches ).forEach (touch => {
181
+ const note = touchPoints .get (touch .identifier )
182
+ if (note !== undefined ) {
183
+ stopNote (note)
184
+ touchPoints .delete (touch .identifier )
185
+ }
186
+ })
187
+ }
78
188
</script >
79
189
80
190
<template lang="pug">
@@ -102,12 +212,14 @@ watchThrottled(loaded, l => {
102
212
)
103
213
104
214
svg.w-full.min-w-full (
215
+ ref ="svg"
105
216
version ="1.1" ,
106
217
baseProfile ="full" ,
107
218
:viewBox ="`${-pad} ${-pad} ${size + 2 * pad} ${size + 2 * pad}`" ,
108
219
xmlns ="http://www.w3.org/2000/svg" ,
109
220
text-anchor ="middle"
110
- dominant-baseline ="middle"
221
+ dominant-baseline ="middle"
222
+ style ="touch-action: none"
111
223
@touchstart ="pressed = true"
112
224
@touchend ="pressed = false"
113
225
@mousedown ="pressed = true"
@@ -143,12 +255,7 @@ watchThrottled(loaded, l => {
143
255
g( :transform ="`translate(${size / 2}, ${size / 2}) `" )
144
256
g.keys ( v-for ="(note, pitch) in flower" : key= "note" )
145
257
g.key.cursor-pointer (
146
- @mousedown.passive ="keyPlay(note.midi, $event);"
147
- @mouseup.passive ="keyPlay(note.midi, $event, true)"
148
- @mouseenter.passive ="pressed && keyPlay(note.midi, $event);"
149
- @touchstart.prevent.stop ="keyPlay(note.midi, $event)"
150
- @touchend.prevent.stop ="keyPlay(note.midi, $event, true)"
151
- @mouseleave ="keyPlay(note.midi, $event, true)"
258
+ :data-pitch ="pitch"
152
259
)
153
260
g.petal (
154
261
:transform ="`rotate(${pitch * 30}) translate(2,-120) `"
@@ -315,4 +422,12 @@ input[type="color"]::-webkit-color-swatch {
315
422
border : none;
316
423
border-radius : 50 % ;
317
424
}
425
+
426
+ svg {
427
+ touch-action : none;
428
+ }
429
+
430
+ .key {
431
+ touch-action : none;
432
+ }
318
433
</style >
0 commit comments