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 @@
(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 @@
@@ -63,25 +62,16 @@ import {
getKeyBaseHeight,
tickToBaseX,
noteNumberToBaseY,
- PREVIEW_SOUND_DURATION,
} from "@/helpers/singHelper";
export default defineComponent({
name: "SingSequencerNote",
props: {
note: { type: Object as PropType
, required: true },
- index: { type: Number, required: true },
- cursorX: { type: Number },
- cursorY: { type: Number },
+ isSelected: { type: Boolean },
+ isPreview: { type: Boolean },
},
-
- emits: [
- "handleNotesKeydown",
- "handleDragMoveStart",
- "handleDragRightStart",
- "handleDragLeftStart",
- ],
-
+ emits: ["bodyMousedown", "rightEdgeMousedown", "leftEdgeMousedown"],
setup(props, { emit }) {
const store = useStore();
const state = store.state;
@@ -104,20 +94,16 @@ export default defineComponent({
const noteEndBaseX = tickToBaseX(noteEndTicks, tpqn.value);
return (noteEndBaseX - noteStartBaseX) * zoomX.value;
});
- const classNamesStr = computed(() => {
- if (state.selectedNoteIds.includes(props.note.id)) {
+ const classNames = computed(() => {
+ if (props.isSelected) {
return "sequencer-note selected";
}
- if (state.overlappingNoteIds.includes(props.note.id)) {
+ if (state.overlappingNoteIds.has(props.note.id)) {
return "sequencer-note overlapping";
}
return "sequencer-note";
});
- const removeNote = () => {
- store.dispatch("REMOVE_NOTES", { noteIds: [props.note.id] });
- };
-
const setLyric = (event: Event) => {
if (!(event.target instanceof HTMLInputElement)) {
return;
@@ -128,38 +114,16 @@ export default defineComponent({
}
};
- const selectThisNote = () => {
- store.dispatch("SELECT_NOTES", { noteIds: [props.note.id] });
- store.dispatch("PLAY_PREVIEW_SOUND", {
- noteNumber: props.note.noteNumber,
- duration: PREVIEW_SOUND_DURATION,
- });
- };
-
- const handleKeydown = (event: KeyboardEvent) => {
- emit("handleNotesKeydown", event);
- };
-
- const handleMouseDown = (event: MouseEvent) => {
- if (!state.selectedNoteIds.includes(props.note.id)) {
- selectThisNote();
- } else {
- emit("handleDragMoveStart", event);
- }
+ const onBodyMouseDown = (event: MouseEvent) => {
+ emit("bodyMousedown", event);
};
- const handleDragRightStart = (event: MouseEvent) => {
- if (!state.selectedNoteIds.includes(props.note.id)) {
- selectThisNote();
- }
- emit("handleDragRightStart", event);
+ const onRightEdgeMouseDown = (event: MouseEvent) => {
+ emit("rightEdgeMousedown", event);
};
- const handleDragLeftStart = (event: MouseEvent) => {
- if (!state.selectedNoteIds.includes(props.note.id)) {
- selectThisNote();
- }
- emit("handleDragLeftStart", event);
+ const onLeftEdgeMouseDown = (event: MouseEvent) => {
+ emit("leftEdgeMousedown", event);
};
return {
@@ -169,13 +133,11 @@ export default defineComponent({
positionY,
barHeight,
barWidth,
- classNamesStr,
- removeNote,
+ classNames,
setLyric,
- handleKeydown,
- handleDragRightStart,
- handleDragLeftStart,
- handleMouseDown,
+ onBodyMouseDown,
+ onRightEdgeMouseDown,
+ onLeftEdgeMouseDown,
};
},
});
@@ -195,7 +157,6 @@ export default defineComponent({
&.selected {
.sequencer-note-bar-body {
fill: darkorange; // 仮
- cursor: move;
}
}
@@ -227,6 +188,7 @@ export default defineComponent({
display: block;
position: relative;
}
+
.sequencer-note-bar-body {
fill: colors.$primary;
stroke: #fff;
@@ -236,7 +198,11 @@ export default defineComponent({
left: 0;
}
-.sequencer-note-bar-draghandle {
+.cursor-move {
+ cursor: move;
+}
+
+.cursor-ew-resize {
cursor: ew-resize;
}
diff --git a/src/components/Sing/SequencerRuler.vue b/src/components/Sing/SequencerRuler.vue
index ac8ed663f5..6ec0025c78 100644
--- a/src/components/Sing/SequencerRuler.vue
+++ b/src/components/Sing/SequencerRuler.vue
@@ -139,6 +139,8 @@ export default defineComponent({
});
const onClick = (event: MouseEvent) => {
+ store.dispatch("DESELECT_ALL_NOTES");
+
const sequencerRulerElement = sequencerRuler.value;
if (!sequencerRulerElement) {
throw new Error("sequencerRulerElement is null.");
diff --git a/src/components/Sing/ToolBar.vue b/src/components/Sing/ToolBar.vue
index 3d2551d859..30c1c47a30 100644
--- a/src/components/Sing/ToolBar.vue
+++ b/src/components/Sing/ToolBar.vue
@@ -29,7 +29,6 @@
icon="stop"
@click="stop"
>
-
{{ playheadPositionStr }}
{
- return value <= maxSnapType;
+ return value <= MAX_SNAP_TYPE;
});
}
diff --git a/src/store/singing.ts b/src/store/singing.ts
index 68566b4ab0..91c9043169 100644
--- a/src/store/singing.ts
+++ b/src/store/singing.ts
@@ -336,10 +336,10 @@ class OverlappingNotesDetector {
}
getOverlappingNoteIds() {
- const overlappingNoteIds: string[] = [];
+ const overlappingNoteIds = new Set();
for (const [noteId, noteInfo] of this.noteInfos) {
if (noteInfo.overlappingNoteIds.size !== 0) {
- overlappingNoteIds.push(noteId);
+ overlappingNoteIds.add(noteId);
}
}
return overlappingNoteIds;
@@ -404,11 +404,9 @@ export const singingStoreState: SingingStoreState = {
isShowSinger: true,
sequencerZoomX: 0.5,
sequencerZoomY: 0.75,
- sequencerScrollY: 60, // Y軸 note number
- sequencerScrollX: 0, // X軸 tick(仮)
sequencerSnapType: 16, // スナップタイプ
- selectedNoteIds: [],
- overlappingNoteIds: [],
+ selectedNoteIds: new Set(),
+ overlappingNoteIds: new Set(),
nowPlaying: false,
volume: 0,
leftLocatorPosition: 0,
@@ -482,7 +480,7 @@ export const singingStore = createPartialStore({
mutation(state, { score }: { score: Score }) {
const oldNotes = state.score.notes;
state.score = score;
- state.selectedNoteIds = [];
+ state.selectedNoteIds.clear();
overlappingNotesDetector.removeNotes(oldNotes);
overlappingNotesDetector.addNotes(score.notes);
state.overlappingNoteIds =
@@ -654,7 +652,11 @@ export const singingStore = createPartialStore({
const scoreNotes = [...state.score.notes, ...notes];
scoreNotes.sort((a, b) => a.position - b.position);
state.score.notes = scoreNotes;
- state.selectedNoteIds = [...state.selectedNoteIds, ...noteIds];
+ const selectedNoteIds = new Set(state.selectedNoteIds);
+ for (const noteId of noteIds) {
+ selectedNoteIds.add(noteId);
+ }
+ state.selectedNoteIds = selectedNoteIds;
overlappingNotesDetector.addNotes(notes);
state.overlappingNoteIds =
overlappingNotesDetector.getOverlappingNoteIds();
@@ -713,9 +715,11 @@ export const singingStore = createPartialStore({
state.score.notes = state.score.notes.filter((value) => {
return !noteIdsSet.has(value.id);
});
- state.selectedNoteIds = state.selectedNoteIds.filter((value) => {
- return !noteIdsSet.has(value);
- });
+ const selectedNoteIds = new Set(state.selectedNoteIds);
+ for (const noteId of noteIds) {
+ selectedNoteIds.delete(noteId);
+ }
+ state.selectedNoteIds = selectedNoteIds;
overlappingNotesDetector.removeNotes(notes);
state.overlappingNoteIds =
overlappingNotesDetector.getOverlappingNoteIds();
@@ -736,47 +740,41 @@ export const singingStore = createPartialStore({
},
},
- SET_SELECTED_NOTE_IDS: {
+ SELECT_NOTES: {
mutation(state, { noteIds }: { noteIds: string[] }) {
- state.selectedNoteIds = noteIds;
+ const selectedNoteIds = new Set(state.selectedNoteIds);
+ for (const noteId of noteIds) {
+ selectedNoteIds.add(noteId);
+ }
+ state.selectedNoteIds = selectedNoteIds;
},
- },
-
- SELECT_NOTES: {
async action({ state, commit }, { noteIds }: { noteIds: string[] }) {
const scoreNotes = state.score.notes;
const existingIds = new Set(scoreNotes.map((value) => value.id));
if (!noteIds.every((value) => existingIds.has(value))) {
throw new Error("The note ids are invalid.");
}
- const selectedNoteIdsSet = new Set([
- ...state.selectedNoteIds,
- ...noteIds,
- ]);
- commit("SET_SELECTED_NOTE_IDS", { noteIds: [...selectedNoteIdsSet] });
+ commit("SELECT_NOTES", { noteIds });
},
},
DESELECT_ALL_NOTES: {
+ mutation(state) {
+ state.selectedNoteIds = new Set();
+ },
async action({ commit }) {
- commit("SET_SELECTED_NOTE_IDS", { noteIds: [] });
+ commit("DESELECT_ALL_NOTES");
},
},
REMOVE_SELECTED_NOTES: {
async action({ state, commit, dispatch }) {
- commit("REMOVE_NOTES", { noteIds: state.selectedNoteIds });
+ commit("REMOVE_NOTES", { noteIds: [...state.selectedNoteIds] });
dispatch("RENDER");
},
},
- SET_OVERLAPPING_NOTE_IDS: {
- mutation(state, { noteIds }: { noteIds: string[] }) {
- state.overlappingNoteIds = noteIds;
- },
- },
-
SET_PHRASE: {
mutation(
state,
@@ -857,28 +855,6 @@ export const singingStore = createPartialStore({
},
},
- SET_SCROLL_X: {
- mutation(state, { scrollX }: { scrollX: number }) {
- state.sequencerScrollX = scrollX;
- },
- async action({ commit }, { scrollX }) {
- commit("SET_SCROLL_X", {
- scrollX,
- });
- },
- },
-
- SET_SCROLL_Y: {
- mutation(state, { scrollY }: { scrollY: number }) {
- state.sequencerScrollY = scrollY;
- },
- async action({ commit }, { scrollY }) {
- commit("SET_SCROLL_Y", {
- scrollY,
- });
- },
- },
-
TICK_TO_SECOND: {
getter: (state) => (position) => {
const tpqn = state.score.tpqn;
@@ -1084,11 +1060,10 @@ export const singingStore = createPartialStore({
async action({ state, getters, commit, dispatch }) {
const deleteOverlappingNotes = (
score: Score,
- overlappingNoteIds: string[]
+ overlappingNoteIds: Set
) => {
- const overlappingNoteIdsSet = new Set(overlappingNoteIds);
score.notes = score.notes.filter((value) => {
- return !overlappingNoteIdsSet.has(value.id);
+ return !overlappingNoteIds.has(value.id);
});
};
diff --git a/src/store/type.ts b/src/store/type.ts
index 6ea5427958..7a3ba77cca 100644
--- a/src/store/type.ts
+++ b/src/store/type.ts
@@ -773,11 +773,9 @@ export type SingingStoreState = {
// NOTE: オーディオ再生はボイスと同様もしくは拡張して使う?
sequencerZoomX: number;
sequencerZoomY: number;
- sequencerScrollX: number;
- sequencerScrollY: number;
sequencerSnapType: number;
- selectedNoteIds: string[];
- overlappingNoteIds: string[];
+ selectedNoteIds: Set;
+ overlappingNoteIds: Set;
nowPlaying: boolean;
volume: number;
leftLocatorPosition: number;
@@ -840,15 +838,13 @@ export type SingingStoreTypes = {
action(payload: { noteIds: string[] }): void;
};
- SET_SELECTED_NOTE_IDS: {
- mutation: { noteIds: string[] };
- };
-
SELECT_NOTES: {
+ mutation: { noteIds: string[] };
action(payload: { noteIds: string[] }): void;
};
DESELECT_ALL_NOTES: {
+ mutation: undefined;
action(): void;
};
@@ -856,10 +852,6 @@ export type SingingStoreTypes = {
action(): void;
};
- SET_OVERLAPPING_NOTE_IDS: {
- mutation: { noteIds: string[] };
- };
-
SET_PHRASE: {
mutation: { phraseKey: string; phrase: Phrase };
};
@@ -895,16 +887,6 @@ export type SingingStoreTypes = {
action(payload: { zoomY: number }): void;
};
- SET_SCROLL_X: {
- mutation: { scrollX: number };
- action(payload: { scrollX: number }): void;
- };
-
- SET_SCROLL_Y: {
- mutation: { scrollY: number };
- action(payload: { scrollY: number }): void;
- };
-
SET_IS_DRAG: {
mutation: { isDrag: boolean };
action(payload: { isDrag: boolean }): void;
diff --git a/tests/unit/store/Vuex.spec.ts b/tests/unit/store/Vuex.spec.ts
index cb66fc23f7..45ae817b34 100644
--- a/tests/unit/store/Vuex.spec.ts
+++ b/tests/unit/store/Vuex.spec.ts
@@ -170,11 +170,9 @@ describe("store/vuex.js test", () => {
isShowSinger: true,
sequencerZoomX: 1,
sequencerZoomY: 1,
- sequencerScrollX: 0,
- sequencerScrollY: 60,
sequencerSnapType: 16,
- selectedNoteIds: [],
- overlappingNoteIds: [],
+ selectedNoteIds: new Set(),
+ overlappingNoteIds: new Set(),
nowPlaying: false,
volume: 0,
leftLocatorPosition: 0,