Skip to content

Commit bd23bba

Browse files
committed
feat: got multitouch in action!
1 parent 4f4f68f commit bd23bba

File tree

4 files changed

+333
-40
lines changed

4 files changed

+333
-40
lines changed

components/chroma/ChromaFlower.vue

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { colord } from "colord";
1010
import { useClipboard, watchThrottled } from '@vueuse/core'
1111
import { computed, onMounted, ref, shallowRef } from 'vue'
1212
import { Note } from 'tonal'
13+
import { useGesture } from '@vueuse/gesture'
1314
1415
const props = defineProps({
1516
letters: { type: Boolean, default: true },
@@ -75,6 +76,115 @@ watchThrottled(loaded, l => {
7576
console.error('Loaded color scheme is not valid')
7677
}
7778
})
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+
}
78188
</script>
79189

80190
<template lang="pug">
@@ -102,12 +212,14 @@ watchThrottled(loaded, l => {
102212
)
103213

104214
svg.w-full.min-w-full(
215+
ref="svg"
105216
version="1.1",
106217
baseProfile="full",
107218
:viewBox="`${-pad} ${-pad} ${size + 2 * pad} ${size + 2 * pad}`",
108219
xmlns="http://www.w3.org/2000/svg",
109220
text-anchor="middle"
110-
dominant-baseline="middle"
221+
dominant-baseline="middle"
222+
style="touch-action: none"
111223
@touchstart="pressed = true"
112224
@touchend="pressed = false"
113225
@mousedown="pressed = true"
@@ -143,12 +255,7 @@ watchThrottled(loaded, l => {
143255
g(:transform="`translate(${size / 2}, ${size / 2}) `")
144256
g.keys(v-for="(note, pitch) in flower" :key="note")
145257
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"
152259
)
153260
g.petal(
154261
:transform="`rotate(${pitch * 30}) translate(2,-120) `"
@@ -315,4 +422,12 @@ input[type="color"]::-webkit-color-swatch {
315422
border: none;
316423
border-radius: 50%;
317424
}
425+
426+
svg {
427+
touch-action: none;
428+
}
429+
430+
.key {
431+
touch-action: none;
432+
}
318433
</style>

components/midi/grid/MidiGrid.vue

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,80 @@ const getGrid = (chroma = "101011010101", tonic = 0, start = 2, end = 5) => Arra
146146
147147
const midiGrid = computed(() => getGrid(scaleChroma.value.chroma, globalScale.tonic, begin.value, end.value))
148148
149-
// const noteGrid = computed(() => midiGrid.value.map((oct) => oct.map(note => Note.fromMidi(note))))
149+
const currentNote = ref(null)
150+
151+
const touchPoints = new Map() // Map<number, number> (touchId -> noteId)
152+
153+
useGesture({
154+
onTouchstart: handleTouchStart,
155+
onTouchmove: handleTouchMove,
156+
onTouchend: handleTouchEnd,
157+
onTouchcancel: handleTouchEnd
158+
}, {
159+
domTarget: area,
160+
eventOptions: { passive: false },
161+
triggerAllEvents: true
162+
})
163+
164+
function handleTouchStart({ event }) {
165+
event.preventDefault()
166+
Array.from(event.changedTouches).forEach(touch => {
167+
const element = document.elementFromPoint(touch.clientX, touch.clientY)
168+
if (!element) return
169+
170+
const noteElement = element.closest('.note-cell')
171+
if (!noteElement) return
172+
173+
const note = parseInt(noteElement.dataset.note)
174+
touchPoints.set(touch.identifier, note)
175+
playNote(note)
176+
})
177+
}
178+
179+
function handleTouchMove({ event }) {
180+
event.preventDefault()
181+
Array.from(event.changedTouches).forEach(touch => {
182+
const oldNote = touchPoints.get(touch.identifier)
183+
const element = document.elementFromPoint(touch.clientX, touch.clientY)
184+
if (!element) {
185+
if (oldNote !== undefined) {
186+
stopNote(oldNote)
187+
touchPoints.delete(touch.identifier)
188+
}
189+
return
190+
}
191+
192+
const noteElement = element.closest('.note-cell')
193+
if (!noteElement) {
194+
if (oldNote !== undefined) {
195+
stopNote(oldNote)
196+
touchPoints.delete(touch.identifier)
197+
}
198+
return
199+
}
200+
201+
const newNote = parseInt(noteElement.dataset.note)
202+
if (newNote !== oldNote) {
203+
if (oldNote !== undefined) {
204+
stopNote(oldNote)
205+
}
206+
touchPoints.set(touch.identifier, newNote)
207+
playNote(newNote)
208+
}
209+
})
210+
}
211+
212+
function handleTouchEnd({ event }) {
213+
event.preventDefault()
214+
Array.from(event.changedTouches).forEach(touch => {
215+
const note = touchPoints.get(touch.identifier)
216+
if (note !== undefined) {
217+
stopNote(note)
218+
touchPoints.delete(touch.identifier)
219+
}
220+
})
221+
}
222+
150223
151224
</script>
152225

@@ -249,37 +322,44 @@ svg.w-full.cursor-pointer.fullscreen-container.overflow-hidden.select-none.touch
249322
) {{ end }}
250323

251324
g.grid(ref="area")
252-
253325
g.row(v-for="(octave, oct) in midiGrid" :key="oct")
254-
g.cell(
255-
:class="{ 'brightness-140': activeNotes[note] }"
326+
g.note-cell(
256327
v-for="(note, n) in octave" :key="note"
328+
:data-note="note"
257329
:transform="`translate(${n * width / midiGrid[0].length} ${height - (oct + 1) * (height / midiGrid.length)})`"
258-
@pointerdown.prevent="playNote(note)",
330+
@pointerdown.prevent="playNote(note)"
259331
@pointerenter="pressed ? playNote(note) : null"
260-
@pointerleave="stopNote(note)",
261-
@pointerup.prevent="stopNote(note)",
332+
@pointerleave="pressed ? stopNote(note) : null"
333+
@pointerup.prevent="stopNote(note)"
262334
@pointercancel="stopNote(note)"
263335
)
264336
rect(
265-
:fill="noteColor(note - 9)"
337+
:fill="noteColor(note - 9, null, 1, activeNotes[note] ? 1 : 0.4)"
266338
:height="height / midiGrid.length"
267339
:width="width / midiGrid[0].length"
268340
)
269341
text(
270342
dominant-baseline="middle"
271-
:y="height / midiGrid.length / 2"
272-
:x=10
343+
:y="25"
344+
:x="10"
345+
font-size="24"
346+
) {{ Note.fromMidi(note) }}
347+
text(
348+
dominant-baseline="middle"
349+
text-anchor="end"
350+
opacity=".3"
351+
:y="height / midiGrid.length - 10"
352+
:x="width / midiGrid[0].length - 10"
273353
)
274-
tspan.text-2xl() {{ Note.fromMidi(note) }}
275-
tspan(dy="30" x=10) {{ note }}
354+
355+
tspan() {{ note }}
276356

277357
g.intervals
278358
g.interval(
279359
v-for="(note, n) in midiGrid[0]" :key="note"
280360
:transform="`translate(${n * width / midiGrid[0].length + 10} 20)`"
281361
)
282-
text {{ Interval.fromSemitones(note - midiGrid[0][0]) }}
362+
text(text-anchor="end" font-weight="bold" :y="10" :x="width / midiGrid[0].length - 20") {{ Interval.fromSemitones(note - midiGrid[0][0]) }}
283363
</template>
284364

285365
<style scoped>
@@ -296,4 +376,8 @@ svg {
296376
-webkit-user-modify: none;
297377
-webkit-highlight: none;
298378
}
379+
380+
.note-cell {
381+
touch-action: none;
382+
}
299383
</style>

0 commit comments

Comments
 (0)