diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 9fdf6b1ff6..dfef6f43a4 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -18,10 +18,10 @@
@@ -92,16 +92,22 @@ /> +
(DragMode.NONE); - const dragId = ref(0); - const dragMoveCurrentX = ref(); - const dragMoveCurrentY = ref(); - const dragDurationCurrentX = ref(); // 分解能(Ticks Per Quarter Note) const tpqn = computed(() => state.score.tpqn); // ノート @@ -234,15 +226,11 @@ export default defineComponent({ const snapTicks = computed(() => { return getNoteDuration(state.sequencerSnapType, tpqn.value); }); - const snapBaseWidth = computed(() => { - return tickToBaseX(snapTicks.value, tpqn.value); - }); - const snapWidth = computed(() => { - return snapBaseWidth.value * zoomX.value; - }); // シーケンサグリッド const gridCellTicks = snapTicks; // ひとまずスナップ幅=グリッドセル幅 - const gridCellBaseWidth = snapBaseWidth; + const gridCellBaseWidth = computed(() => { + return tickToBaseX(gridCellTicks.value, tpqn.value); + }); const gridCellWidth = computed(() => { return gridCellBaseWidth.value * zoomX.value; }); @@ -294,8 +282,12 @@ export default defineComponent({ const baseX = tickToBaseX(playheadTicks.value, tpqn.value); return Math.floor(baseX * zoomX.value); }); + const unselectedNotes = computed(() => { + const selectedNoteIds = state.selectedNoteIds; + return notes.value.filter((value) => !selectedNoteIds.has(value.id)); + }); const selectedNotes = computed(() => { - const selectedNoteIds = new Set(state.selectedNoteIds); + const selectedNoteIds = state.selectedNoteIds; return notes.value.filter((value) => selectedNoteIds.has(value.id)); }); const phraseInfos = computed(() => { @@ -308,256 +300,336 @@ export default defineComponent({ }); }); const scrollBarWidth = ref(12); - const sequencerBody = ref(null); - // ノートの追加 - const addNote = (event: MouseEvent) => { - const eventOffsetBaseX = event.offsetX / zoomX.value; - const eventOffsetBaseY = event.offsetY / zoomY.value; - const positionBaseX = - gridCellBaseWidth.value * - Math.floor(eventOffsetBaseX / gridCellBaseWidth.value); - const position = baseXToTick(positionBaseX, tpqn.value); - const noteNumber = baseYToNoteNumber(eventOffsetBaseY); - if (noteNumber < 0) { - return; + // プレビュー + // FIXME: 関連する値を1つのobjectにまとめる + const nowPreviewing = ref(false); + const previewNotes = ref([]); + const copiedNotesForPreview = new Map(); + let previewMode: PreviewMode = "ADD"; + let previewRequestId = 0; + let currentCursorX = 0; + let currentCursorY = 0; + let dragStartTicks = 0; + let dragStartNoteNumber = 0; + let draggingNoteId = ""; // FIXME: 無効状態はstring以外の型にする + let edited = false; + // ダブルクリック + const clickedNoteIds: [string | undefined, string | undefined] = [ + undefined, + undefined, + ]; + let cancelDoubleClick = false; + + const previewAdd = () => { + const cursorBaseX = (scrollX.value + currentCursorX) / zoomX.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const draggingNote = copiedNotesForPreview.get(draggingNoteId); + if (!draggingNote) { + throw new Error("draggingNote is undefined."); + } + const noteEndPos = draggingNote.position + draggingNote.duration; + const newNoteEndPos = + Math.round(cursorTicks / snapTicks.value) * snapTicks.value; + const movingTicks = newNoteEndPos - noteEndPos; + + const editedNotes = new Map(); + for (const note of previewNotes.value) { + const copiedNote = copiedNotesForPreview.get(note.id); + if (!copiedNote) { + throw new Error("copiedNote is undefined."); + } + const notePos = copiedNote.position; + const noteEndPos = copiedNote.position + copiedNote.duration; + const duration = Math.max( + snapTicks.value, + noteEndPos + movingTicks - notePos + ); + if (note.duration !== duration) { + editedNotes.set(note.id, { ...note, duration }); + } } - // NOTE: ノートの長さはスナップをベース(最小の長さは1/8) - const noteType = Math.min(8, state.sequencerSnapType); - const duration = getNoteDuration(noteType, tpqn.value); - const lyric = getDoremiFromNoteNumber(noteNumber); - // NOTE: 仮ID - const id = uuidv4(); - store.dispatch("ADD_NOTES", { - notes: [{ id, position, noteNumber, duration, lyric }], - }); - store.dispatch("PLAY_PREVIEW_SOUND", { - noteNumber, - duration: PREVIEW_SOUND_DURATION, - }); - }; - - // マウスダウン - // 選択中のノートがある場合は選択リセット - const handleMouseDown = () => { - if (0 < selectedNotes.value.length) { - store.dispatch("DESELECT_ALL_NOTES"); + if (editedNotes.size !== 0) { + previewNotes.value = previewNotes.value.map((value) => { + return editedNotes.get(value.id) ?? value; + }); } }; - // マウス移動 - // ドラッグ中の場合はカーソル位置を保持 - const handleMouseMove = (event: MouseEvent) => { - if (dragMode.value !== DragMode.NONE) { - cursorX.value = event.clientX; - cursorY.value = event.clientY; + const previewMove = () => { + const cursorBaseX = (scrollX.value + currentCursorX) / zoomX.value; + const cursorBaseY = (scrollY.value + currentCursorY) / zoomY.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorNoteNumber = baseYToNoteNumber(cursorBaseY); + const draggingNote = copiedNotesForPreview.get(draggingNoteId); + if (!draggingNote) { + throw new Error("draggingNote is undefined."); + } + const dragTicks = cursorTicks - dragStartTicks; + const notePos = draggingNote.position; + const newNotePos = + Math.round((notePos + dragTicks) / snapTicks.value) * snapTicks.value; + const movingTicks = newNotePos - notePos; + const movingSemitones = cursorNoteNumber - dragStartNoteNumber; + + const editedNotes = new Map(); + for (const note of previewNotes.value) { + const copiedNote = copiedNotesForPreview.get(note.id); + if (!copiedNote) { + throw new Error("copiedNote is undefined."); + } + const position = copiedNote.position + movingTicks; + const noteNumber = copiedNote.noteNumber + movingSemitones; + if (note.position !== position || note.noteNumber !== noteNumber) { + editedNotes.set(note.id, { ...note, noteNumber, position }); + } + } + for (const note of editedNotes.values()) { + if (note.position < 0) { + // 左端より前はドラッグしない + return; + } + } + if (editedNotes.size !== 0) { + previewNotes.value = previewNotes.value.map((value) => { + return editedNotes.get(value.id) ?? value; + }); + edited = true; } }; - // マウスアップ - // ドラッグしていた場合はドラッグを終了 - const handleMouseUp = () => { - if (dragMode.value !== DragMode.NONE) { - cancelAnimationFrame(dragId.value); - dragMode.value = DragMode.NONE; - - if (selectedNotes.value.length === 1) { - store.dispatch("PLAY_PREVIEW_SOUND", { - noteNumber: selectedNotes.value[0].noteNumber, - duration: PREVIEW_SOUND_DURATION, - }); + const previewResizeRight = () => { + const cursorBaseX = (scrollX.value + currentCursorX) / zoomX.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const draggingNote = copiedNotesForPreview.get(draggingNoteId); + if (!draggingNote) { + throw new Error("draggingNote is undefined."); + } + const dragTicks = cursorTicks - dragStartTicks; + const noteEndPos = draggingNote.position + draggingNote.duration; + const newNoteEndPos = + Math.round((noteEndPos + dragTicks) / snapTicks.value) * + snapTicks.value; + const movingTicks = newNoteEndPos - noteEndPos; + + const editedNotes = new Map(); + for (const note of previewNotes.value) { + const copiedNote = copiedNotesForPreview.get(note.id); + if (!copiedNote) { + throw new Error("copiedNote is undefined."); } + const notePos = copiedNote.position; + const noteEndPos = copiedNote.position + copiedNote.duration; + const duration = Math.max( + snapTicks.value, + noteEndPos + movingTicks - notePos + ); + if (note.duration !== duration) { + editedNotes.set(note.id, { ...note, duration }); + } + } + if (editedNotes.size !== 0) { + previewNotes.value = previewNotes.value.map((value) => { + return editedNotes.get(value.id) ?? value; + }); + edited = true; } }; - // ドラッグでのノートの移動 - const dragMove = () => { - if (dragMode.value !== DragMode.MOVE) { - cancelAnimationFrame(dragId.value); - return; - } - // X方向, Y方向の移動距離 - const distanceX = cursorX.value - dragMoveCurrentX.value; - const distanceY = cursorY.value - dragMoveCurrentY.value; - - // カーソル位置に応じてノート移動量を計算 - let amountPositionX = 0; - if (gridCellWidth.value <= Math.abs(distanceX)) { - amountPositionX = 0 < distanceX ? snapTicks.value : -snapTicks.value; - const dragMoveCurrentXNext = - dragMoveCurrentX.value + - (0 < amountPositionX ? gridCellWidth.value : -gridCellWidth.value); - dragMoveCurrentX.value = dragMoveCurrentXNext; - } - let amountPositionY = 0; - if (gridCellHeight.value <= Math.abs(distanceY)) { - amountPositionY = 0 < distanceY ? -1 : 1; - const dragMoveCurrentYNext = - dragMoveCurrentY.value + - (0 > amountPositionY ? gridCellHeight.value : -gridCellHeight.value); - dragMoveCurrentY.value = dragMoveCurrentYNext; - } - - // 選択中のノートのpositionとnoteNumberを変更 - const editedNotes: Note[] = []; - for (const note of selectedNotes.value) { - if (amountPositionX === 0 && amountPositionY === 0) { - continue; + const previewResizeLeft = () => { + const cursorBaseX = (scrollX.value + currentCursorX) / zoomX.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const draggingNote = copiedNotesForPreview.get(draggingNoteId); + if (!draggingNote) { + throw new Error("draggingNote is undefined."); + } + const dragTicks = cursorTicks - dragStartTicks; + const notePos = draggingNote.position; + const newNotePos = + Math.round((notePos + dragTicks) / snapTicks.value) * snapTicks.value; + const movingTicks = newNotePos - notePos; + + const editedNotes = new Map(); + for (const note of previewNotes.value) { + const copiedNote = copiedNotesForPreview.get(note.id); + if (!copiedNote) { + throw new Error("copiedNote is undefined."); + } + const notePos = copiedNote.position; + const noteEndPos = copiedNote.position + copiedNote.duration; + const position = Math.min( + noteEndPos - snapTicks.value, + notePos + movingTicks + ); + const duration = noteEndPos - position; + if (note.position !== position && note.duration !== duration) { + editedNotes.set(note.id, { ...note, position, duration }); } - const position = note.position + amountPositionX; - const noteNumber = note.noteNumber + amountPositionY; - editedNotes.push({ ...note, noteNumber, position }); } - - // 左端より前はドラッグしない - if (editedNotes.some((note) => note.position < 0)) { - dragId.value = requestAnimationFrame(dragMove); - return; + for (const note of editedNotes.values()) { + if (note.position < 0) { + // 左端より前はドラッグしない + return; + } } - if (editedNotes.length !== 0) { - store.dispatch("UPDATE_NOTES", { notes: editedNotes }); + if (editedNotes.size !== 0) { + previewNotes.value = previewNotes.value.map((value) => { + return editedNotes.get(value.id) ?? value; + }); + edited = true; } - dragId.value = requestAnimationFrame(dragMove); }; - // ノートドラッグ開始 - const handleDragMoveStart = (event: MouseEvent) => { - if (selectedNotes.value.length > 0) { - dragMode.value = DragMode.MOVE; - setTimeout(() => { - dragMoveCurrentX.value = event.clientX; - dragMoveCurrentY.value = event.clientY; - dragId.value = requestAnimationFrame(dragMove); - }, 360); + const preview = () => { + if (previewMode === "ADD") { + previewAdd(); } - }; - - // ノート右ドラッグ - // FIXME: 左右ドラッグロジックを統一する - const dragRight = () => { - if (dragMode.value !== DragMode.NOTE_RIGHT) { - cancelAnimationFrame(dragId.value); - return; + if (previewMode === "MOVE") { + previewMove(); } - const distanceX = cursorX.value - dragDurationCurrentX.value; - if (snapWidth.value <= Math.abs(distanceX)) { - const editedNotes: Note[] = []; - for (const note of selectedNotes.value) { - const duration = - note.duration + - (0 < distanceX ? snapTicks.value : -snapTicks.value); - if (duration < Math.max(snapTicks.value, 0) || note.position < 0) { - continue; - } - editedNotes.push({ ...note, duration }); - } - const dragDurationCurrentXNext = - dragDurationCurrentX.value + - (0 < distanceX ? snapWidth.value : -snapWidth.value); - dragDurationCurrentX.value = dragDurationCurrentXNext; - dragId.value = requestAnimationFrame(dragRight); - if (editedNotes.length !== 0) { - store.dispatch("UPDATE_NOTES", { notes: editedNotes }); - } + if (previewMode === "RESIZE_RIGHT") { + previewResizeRight(); } - dragId.value = requestAnimationFrame(dragRight); + if (previewMode === "RESIZE_LEFT") { + previewResizeLeft(); + } + previewRequestId = requestAnimationFrame(preview); }; - // ノート右ドラッグ開始 - const handleDragRightStart = (event: MouseEvent) => { - dragMode.value = DragMode.NOTE_RIGHT; - setTimeout(() => { - dragDurationCurrentX.value = event.clientX; - dragId.value = requestAnimationFrame(dragRight); - }, 360); + const getXInBorderBox = (clientX: number, element: HTMLElement) => { + return clientX - element.getBoundingClientRect().left; }; - // ノート左ドラッグ - // FIXME: 左右ドラッグロジックを統一する - const dragLeft = () => { - if (dragMode.value !== DragMode.NOTE_LEFT) { - cancelAnimationFrame(dragId.value); - return; - } - const distanceX = cursorX.value - dragDurationCurrentX.value; - if (snapWidth.value <= Math.abs(distanceX)) { - const editedNotes: Note[] = []; - for (const note of selectedNotes.value) { - const position = - note.position + - (0 < distanceX ? snapTicks.value : -snapTicks.value); - const duration = - note.duration + - (0 > distanceX ? snapTicks.value : -snapTicks.value); - if (duration < Math.max(snapTicks.value, 0) || note.position < 0) { - continue; - } - editedNotes.push({ ...note, position, duration }); - } - const dragDurationCurrentXNext = - dragDurationCurrentX.value + - (0 < distanceX ? snapWidth.value : -snapWidth.value); - dragDurationCurrentX.value = dragDurationCurrentXNext; - dragId.value = requestAnimationFrame(dragLeft); - if (editedNotes.length !== 0) { - store.dispatch("UPDATE_NOTES", { notes: editedNotes }); - } - } - dragId.value = requestAnimationFrame(dragLeft); + const getYInBorderBox = (clientY: number, element: HTMLElement) => { + return clientY - element.getBoundingClientRect().top; }; - // ノート左ドラッグ開始 - const handleDragLeftStart = (event: MouseEvent) => { - dragMode.value = DragMode.NOTE_LEFT; - setTimeout(() => { - dragDurationCurrentX.value = event.clientX; - dragId.value = requestAnimationFrame(dragLeft); - }, 360); - }; - - // X軸ズーム - const setZoomX = (event: Event) => { + const startPreview = ( + event: MouseEvent, + mode: PreviewMode, + note?: Note + ) => { + if (nowPreviewing.value) { + // RESIZE_RIGHT・RESIZE_LEFTのあとにADDも発生するので、その場合は無視する + // TODO: stopPropagation付けたり、他のイベントではエラーを投げるようにする + return; + } const sequencerBodyElement = sequencerBody.value; if (!sequencerBodyElement) { throw new Error("sequencerBodyElement is null."); } - if (event.target instanceof HTMLInputElement) { - // 画面の中央を基準に水平方向のズームを行う - const oldZoomX = zoomX.value; - const newZoomX = Number(event.target.value); - const scrollLeft = sequencerBodyElement.scrollLeft; - const scrollTop = sequencerBodyElement.scrollTop; - const clientWidth = sequencerBodyElement.clientWidth; - - store.dispatch("SET_ZOOM_X", { zoomX: newZoomX }).then(() => { - const centerBaseX = (scrollLeft + clientWidth / 2) / oldZoomX; - const newScrollLeft = centerBaseX * newZoomX - clientWidth / 2; - sequencerBodyElement.scrollTo(newScrollLeft, scrollTop); + const cursorX = getXInBorderBox(event.clientX, sequencerBodyElement); + const cursorY = getYInBorderBox(event.clientY, sequencerBodyElement); + if (cursorX >= sequencerBodyElement.clientWidth) { + return; + } + if (cursorY >= sequencerBodyElement.clientHeight) { + return; + } + const cursorBaseX = (scrollX.value + cursorX) / zoomX.value; + const cursorBaseY = (scrollY.value + cursorY) / zoomY.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorNoteNumber = baseYToNoteNumber(cursorBaseY); + const copiedNotes: Note[] = []; + if (mode === "ADD") { + if (cursorNoteNumber < 0) { + return; + } + note = { + id: uuidv4(), + position: Math.round(cursorTicks / snapTicks.value) * snapTicks.value, + duration: snapTicks.value, + noteNumber: cursorNoteNumber, + lyric: getDoremiFromNoteNumber(cursorNoteNumber), + }; + store.dispatch("DESELECT_ALL_NOTES"); + store.dispatch("PLAY_PREVIEW_SOUND", { + noteNumber: note.noteNumber, + duration: PREVIEW_SOUND_DURATION, }); + copiedNotes.push(note); + } else { + if (!note) { + throw new Error("note is undefined."); + } + if (state.selectedNoteIds.has(note.id)) { + for (const note of selectedNotes.value) { + copiedNotes.push({ ...note }); + } + } else { + store.dispatch("DESELECT_ALL_NOTES"); + store.dispatch("SELECT_NOTES", { noteIds: [note.id] }); + store.dispatch("PLAY_PREVIEW_SOUND", { + noteNumber: note.noteNumber, + duration: PREVIEW_SOUND_DURATION, + }); + copiedNotes.push({ ...note }); + } } + previewMode = mode; + currentCursorX = cursorX; + currentCursorY = cursorY; + dragStartTicks = cursorTicks; + dragStartNoteNumber = cursorNoteNumber; + draggingNoteId = note.id; + edited = false; + if (event.detail === 1) { + cancelDoubleClick = false; + } + copiedNotesForPreview.clear(); + for (const copiedNote of copiedNotes) { + copiedNotesForPreview.set(copiedNote.id, copiedNote); + } + previewNotes.value = copiedNotes; + nowPreviewing.value = true; + previewRequestId = requestAnimationFrame(preview); }; - // Y軸ズーム - const setZoomY = (event: Event) => { + const onMouseMove = (event: MouseEvent) => { + if (!nowPreviewing.value) { + return; + } const sequencerBodyElement = sequencerBody.value; if (!sequencerBodyElement) { throw new Error("sequencerBodyElement is null."); } - if (event.target instanceof HTMLInputElement) { - // 画面の中央を基準に垂直方向のズームを行う - const oldZoomY = zoomY.value; - const newZoomY = Number(event.target.value); - const scrollLeft = sequencerBodyElement.scrollLeft; - const scrollTop = sequencerBodyElement.scrollTop; - const clientHeight = sequencerBodyElement.clientHeight; + currentCursorX = getXInBorderBox(event.clientX, sequencerBodyElement); + currentCursorY = getYInBorderBox(event.clientY, sequencerBodyElement); + }; - store.dispatch("SET_ZOOM_Y", { zoomY: newZoomY }).then(() => { - const centerBaseY = (scrollTop + clientHeight / 2) / oldZoomY; - const newScrollTop = centerBaseY * newZoomY - clientHeight / 2; - sequencerBodyElement.scrollTo(scrollLeft, newScrollTop); + const onMouseUp = () => { + clickedNoteIds[0] = clickedNoteIds[1]; + clickedNoteIds[1] = nowPreviewing.value ? draggingNoteId : undefined; + if (!nowPreviewing.value) { + return; + } + cancelAnimationFrame(previewRequestId); + if (previewMode === "ADD") { + store.dispatch("ADD_NOTES", { notes: previewNotes.value }); + cancelDoubleClick = true; + } else if (edited) { + store.dispatch("UPDATE_NOTES", { notes: previewNotes.value }); + cancelDoubleClick = true; + } + if (previewNotes.value.length === 1) { + store.dispatch("PLAY_PREVIEW_SOUND", { + noteNumber: previewNotes.value[0].noteNumber, + duration: PREVIEW_SOUND_DURATION, }); } + nowPreviewing.value = false; + }; + + const onDoubleClick = () => { + if ( + cancelDoubleClick || + clickedNoteIds[0] !== clickedNoteIds[1] || + clickedNoteIds[1] == undefined + ) { + return; + } + store.dispatch("REMOVE_NOTES", { noteIds: [clickedNoteIds[1]] }); }; // キーボードイベント @@ -605,6 +677,10 @@ export default defineComponent({ const position = note.position + snapTicks.value; editedNotes.push({ ...note, position }); } + if (editedNotes.length === 0) { + // TODO: 例外処理は`UPDATE_NOTES`内に移す? + return; + } store.dispatch("UPDATE_NOTES", { notes: editedNotes }); }; @@ -614,17 +690,28 @@ export default defineComponent({ const position = note.position - snapTicks.value; editedNotes.push({ ...note, position }); } - if (editedNotes.some((note) => note.position < 0)) { + if ( + editedNotes.length === 0 || + editedNotes.some((note) => note.position < 0) + ) { return; } store.dispatch("UPDATE_NOTES", { notes: editedNotes }); }; const handleNotesBackspaceOrDelete = () => { + if (state.selectedNoteIds.size === 0) { + // TODO: 例外処理は`REMOVE_SELECTED_NOTES`内に移す? + return; + } store.dispatch("REMOVE_SELECTED_NOTES"); }; - const handleNotesKeydown = (event: KeyboardEvent) => { + const handleKeydown = (event: KeyboardEvent) => { + // プレビュー中の操作は想定外の挙動をしそうなので防止 + if (nowPreviewing.value) { + return; + } switch (event.key) { case "ArrowUp": handleNotesArrowUp(); @@ -649,24 +736,65 @@ export default defineComponent({ } }; - const onWheel = (event: WheelEvent) => { + // X軸ズーム + const setZoomX = (event: Event) => { const sequencerBodyElement = sequencerBody.value; if (!sequencerBodyElement) { throw new Error("sequencerBodyElement is null."); } - if (event.ctrlKey) { - // マウスカーソル位置を基準に水平方向のズームを行う + if (event.target instanceof HTMLInputElement) { + // 画面の中央を基準に水平方向のズームを行う const oldZoomX = zoomX.value; + const newZoomX = Number(event.target.value); const scrollLeft = sequencerBodyElement.scrollLeft; const scrollTop = sequencerBodyElement.scrollTop; - const clientRect = sequencerBodyElement.getBoundingClientRect(); - // sequencerBody要素のborderとpaddingが0であることを前提にマウスカーソル位置を算出 - const cursorX = event.clientX - clientRect.left; + const clientWidth = sequencerBodyElement.clientWidth; + store.dispatch("SET_ZOOM_X", { zoomX: newZoomX }).then(() => { + const centerBaseX = (scrollLeft + clientWidth / 2) / oldZoomX; + const newScrollLeft = centerBaseX * newZoomX - clientWidth / 2; + sequencerBodyElement.scrollTo(newScrollLeft, scrollTop); + }); + } + }; + + // Y軸ズーム + const setZoomY = (event: Event) => { + const sequencerBodyElement = sequencerBody.value; + if (!sequencerBodyElement) { + throw new Error("sequencerBodyElement is null."); + } + if (event.target instanceof HTMLInputElement) { + // 画面の中央を基準に垂直方向のズームを行う + const oldZoomY = zoomY.value; + const newZoomY = Number(event.target.value); + const scrollLeft = sequencerBodyElement.scrollLeft; + const scrollTop = sequencerBodyElement.scrollTop; + const clientHeight = sequencerBodyElement.clientHeight; + + store.dispatch("SET_ZOOM_Y", { zoomY: newZoomY }).then(() => { + const centerBaseY = (scrollTop + clientHeight / 2) / oldZoomY; + const newScrollTop = centerBaseY * newZoomY - clientHeight / 2; + sequencerBodyElement.scrollTo(scrollLeft, newScrollTop); + }); + } + }; + + const onWheel = (event: WheelEvent) => { + const sequencerBodyElement = sequencerBody.value; + if (!sequencerBodyElement) { + throw new Error("sequencerBodyElement is null."); + } + if (event.ctrlKey) { + // マウスカーソル位置を基準に水平方向のズームを行う + const cursorX = getXInBorderBox(event.clientX, sequencerBodyElement); + const oldZoomX = zoomX.value; let newZoomX = zoomX.value; newZoomX -= event.deltaY * (ZOOM_X_STEP * 0.01); newZoomX = Math.min(ZOOM_X_MAX, newZoomX); newZoomX = Math.max(ZOOM_X_MIN, newZoomX); + const scrollLeft = sequencerBodyElement.scrollLeft; + const scrollTop = sequencerBodyElement.scrollTop; store.dispatch("SET_ZOOM_X", { zoomX: newZoomX }).then(() => { const cursorBaseX = (scrollLeft + cursorX) / oldZoomX; @@ -727,12 +855,16 @@ export default defineComponent({ store.dispatch("ADD_PLAYHEAD_POSITION_CHANGE_LISTENER", { listener: playheadPositionChangeListener, }); + + document.addEventListener("keydown", handleKeydown); }); onUnmounted(() => { store.dispatch("REMOVE_PLAYHEAD_POSITION_CHANGE_LISTENER", { listener: playheadPositionChangeListener, }); + + document.removeEventListener("keydown", handleKeydown); }); return { @@ -753,24 +885,22 @@ export default defineComponent({ notes, zoomX, zoomY, - cursorX, - cursorY, scrollX, scrollY, playheadX, phraseInfos, scrollBarWidth, sequencerBody, + nowPreviewing, + previewNotes, + selectedNotes, + unselectedNotes, setZoomX, setZoomY, - addNote, - handleMouseDown, - handleMouseMove, - handleMouseUp, - handleNotesKeydown, - handleDragMoveStart, - handleDragRightStart, - handleDragLeftStart, + onMouseMove, + onMouseUp, + startPreview, + onDoubleClick, onWheel, onScroll, }; @@ -813,10 +943,6 @@ export default defineComponent({ backface-visibility: hidden; overflow: auto; position: relative; - - &.move { - cursor: move; - } } .sequencer-overlay { diff --git a/src/components/Sing/SequencerNote.vue b/src/components/Sing/SequencerNote.vue index 1e689e3722..b1e5921465 100644 --- a/src/components/Sing/SequencerNote.vue +++ b/src/components/Sing/SequencerNote.vue @@ -1,6 +1,6 @@