From 8d2e0ebb4cd2799ace31637e097fc2b7e12f83f7 Mon Sep 17 00:00:00 2001 From: Sig Date: Fri, 26 Apr 2024 00:17:53 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=94=E3=83=83=E3=83=81=E7=B7=A8=E9=9B=86?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=20(#2003)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ピッチ編集機能を追加 * 修正 * 色を調整、UIを修正 * ペンの画像を追加、ピッチ編集時のカーソルをペンに変更 * TODOコメントを追加 * ピッチ編集をundo/redoできるようにした * 有声区間のみピッチ編集を適用するように変更 * ピッチ編集データを平滑化してからセットするようにした * Update src/components/Dialog/SettingDialog.vue Co-authored-by: Hiroshiba * Update src/components/Sing/ScoreSequencer.vue Co-authored-by: Hiroshiba * 例外を使用して網羅性チェックを行うようにした * pitchEdit.dataのところにコメントを追加、データが無いことを表す値を定数で定義 * prevCursorFrameとprevCursorFrequencyをまとめてprevCursorPosに変更、コメントを追加 * 修正 * ピッチ編集機能が無効になったときに行う処理をSingEditorに移動 * 1フレームのピッチ変更を適用しない理由をコメントで説明 * 編集モードの判定をcomputed内で行うようにした * プレビュー時に実行される関数の処理を説明するコメントを追加 * コメントを追加 * EditModeをEditTargetに変更 * DataSectionのハッシュの型をブランド型にした * データ区間のマップを更新する関数の処理を説明するコメントを追加、コメントを修正 * フレームレート周りを一旦変更 * Colorクラスの値の範囲をコメントで書いた * FramewiseDataSectionに変更 * watchのimmediateをtrueに設定 * データ区間のマップを生成する処理を関数に切り出した * concatからpushに変更、spliceを使用してデータを書き換える形に変更 * SET_PITCH_EDIT_DATAのところにコメントを追加 * ピッチ編集を適用する処理のところにコメントを追加 * 修正 * DataSectionMapの更新処理が開始順で完了せずピッチの表示が更新されないことがある不具合を修正 * 修正 * Revert "concatからpushに変更、spliceを使用してデータを書き換える形に変更" This reverts commit a0c3260c624dd37b2ec736314cbe94187e9862d6. * ExhaustiveErrorを移動 * NOTEコメントを追加 * 修正 * structuredCloneを使用してディープコピーするようにした * asyncProcessで例外が発生したときにisRunningとisRunRequestedがfalseにならないのを修正 * 非同期処理の実行部分を変更 * AsyncLockを使用する形にした * DEPRECATEDにしてコメントを追加 * CLEAR_PITCH_EDIT_DATAを削除 * 縦方向のズームの下限を調整 * Update src/type/utility.ts Co-authored-by: Hiroshiba * Update src/sing/utility.ts Co-authored-by: Hiroshiba * 元のピッチの色の彩度を12から15にした * concatからpushに変更、spliceを使用してデータを書き換える形に変更 * macでピッチの消去ができるように修正 * to mouseButton --------- Co-authored-by: Hiroshiba --- public/draw-cursor.png | Bin 0 -> 931 bytes src/components/Dialog/SettingDialog.vue | 13 +- src/components/Sing/ScoreSequencer.vue | 491 ++++++++++++++++++------ src/components/Sing/SequencerNote.vue | 54 ++- src/components/Sing/SequencerPitch.vue | 481 ++++++++++++++++------- src/components/Sing/SingEditor.vue | 13 +- src/components/Sing/ToolBar.vue | 30 ++ src/domain/project/schema.ts | 1 + src/sing/domain.ts | 63 +++ src/sing/graphics/lineStrip.ts | 62 ++- src/sing/storeHelper.ts | 3 + src/sing/utility.ts | 57 ++- src/sing/viewHelper.ts | 46 ++- src/store/setting.ts | 2 +- src/store/singing.ts | 129 ++++++- src/store/type.ts | 27 ++ src/type/preload.ts | 2 +- src/type/utility.ts | 10 + 18 files changed, 1175 insertions(+), 309 deletions(-) create mode 100644 public/draw-cursor.png diff --git a/public/draw-cursor.png b/public/draw-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..0bcf5ab112458383ee9b16a7d0cfc84a386e6a13 GIT binary patch literal 931 zcmV;U16=%xP)EX>4Tx04R}tkv&MmKpe$i(@LdO9Lyl%kfC<6i;6gwDi*;)X)CnqU~=gfG-*jv zTpR`0f`cE6RR1{AqG2)092nat9cFYRHD{NPe0^sRX>A(Ki)F^E$^5Qe|JoS4HjwhDpQ$O}p;#X>95?h1vF!p2FSg@UElmU4oGLlA@u`8z!< zMByOCHG-l9K_Y0A92S>SJBiJ0VspwfMDHbTY|LNnvNQAl%@=3oI}r;y4a` z#K}zSP_0%EjYgvua29w6tV-JWCj#O)4xDq(fR(YM^=vkKtX$4G`M(5nx1ag3@ z&bd2t&48KR0Pg?6WC$FSwCz0uX0`&XrHFOTdkM_!B=9(e?GEtVYYEJ(0K5i{>=WlJ za7NO5Zy7MNJg}L9d>6Ph3;Aya)M~XQ;3;q-MU3l`wr1@E69o*1!w10m6#lD{*1dK@ zGrI-cP2u}U(&fKx#laYPB_;XGAP8z+{{k~R1-t;3_wm^VPD}dYL%>pEeY;YrEJsm9 zyWOVMYE82L3|#OR;GbZ=gh zRq_=K_5{7%>;G?n50XC4bF@ol;CLpJ*=aVLyZ_a2`yB;ZgGU8lqj&%S002ovPDHLk FV1fs -
ソング:ピッチを表示
+
ソング:ピッチ編集機能
ONの場合、ソングエディターでピッチ(音の高さ)が表示されます。ピッチ編集機能を有効にします。ピッチ編集モードに切り替えられるようになります。
diff --git a/src/components/Sing/ScoreSequencer.vue b/src/components/Sing/ScoreSequencer.vue index 006977035b..fa6dad911b 100644 --- a/src/components/Sing/ScoreSequencer.vue +++ b/src/components/Sing/ScoreSequencer.vue @@ -18,7 +18,10 @@
- +
@@ -234,13 +244,15 @@ import ContextMenu, { } from "@/components/Menu/ContextMenu.vue"; import { isMac } from "@/type/preload"; import { useStore } from "@/store"; -import { Note } from "@/store/type"; +import { Note, SequencerEditTarget } from "@/store/type"; import { getEndTicksOfPhrase, getMeasureDuration, getNoteDuration, getNumOfMeasures, getStartTicksOfPhrase, + noteNumberToFrequency, + tickToSecond, } from "@/sing/domain"; import { getKeyBaseHeight, @@ -260,6 +272,7 @@ import { DoubleClickDetector, NoteAreaInfo, GridAreaInfo, + getButton, } from "@/sing/viewHelper"; import SequencerRuler from "@/components/Sing/SequencerRuler.vue"; import SequencerKeys from "@/components/Sing/SequencerKeys.vue"; @@ -270,10 +283,21 @@ import SequencerPitch from "@/components/Sing/SequencerPitch.vue"; import { isOnCommandOrCtrlKeyDown } from "@/store/utility"; import { createLogger } from "@/domain/frontend/log"; import { useHotkeyManager } from "@/plugins/hotkeyPlugin"; -import { useShiftKey } from "@/composables/useModifierKey"; +import { + useCommandOrControlKey, + useShiftKey, +} from "@/composables/useModifierKey"; +import { applyGaussianFilter, linearInterpolation } from "@/sing/utility"; import { useLyricInput } from "@/composables/useLyricInput"; +import { ExhaustiveError } from "@/type/utility"; -type PreviewMode = "ADD" | "MOVE" | "RESIZE_RIGHT" | "RESIZE_LEFT"; +type PreviewMode = + | "ADD" + | "MOVE" + | "RESIZE_RIGHT" + | "RESIZE_LEFT" + | "DRAW_PITCH" + | "ERASE_PITCH"; // 直接イベントが来ているかどうか const isSelfEventTarget = (event: UIEvent) => { @@ -392,9 +416,9 @@ const phraseInfos = computed(() => { }); }); -const showPitch = computed(() => { - return state.experimentalSetting.showPitchInSongEditor; -}); +const ctrlKey = useCommandOrControlKey(); +const editTarget = computed(() => state.sequencerEditTarget); +const editFrameRate = computed(() => state.editFrameRate); const scrollBarWidth = ref(12); const sequencerBody = ref(null); @@ -417,16 +441,25 @@ const onNoteLyricBlur = () => { // プレビュー // FIXME: 関連する値を1つのobjectにまとめる const nowPreviewing = ref(false); -const previewNotes = ref([]); -const copiedNotesForPreview = new Map(); let previewMode: PreviewMode = "ADD"; let previewRequestId = 0; +let previewStartEditTarget: SequencerEditTarget = "NOTE"; +let executePreviewProcess = false; +// ノート編集のプレビュー +const previewNotes = ref([]); +const copiedNotesForPreview = new Map(); let dragStartTicks = 0; let dragStartNoteNumber = 0; let dragStartGuideLineTicks = 0; let draggingNoteId = ""; // FIXME: 無効状態はstring以外の型にする -let executePreviewProcess = false; let edited = false; // プレビュー終了時にstore.stateの更新を行うかどうかを表す変数 +// ピッチ編集のプレビュー +const previewPitchEdit = ref< + | { type: "draw"; data: number[]; startFrame: number } + | { type: "erase"; startFrame: number; frameLength: number } + | undefined +>(undefined); +const prevCursorPos = { frame: 0, frequency: 0 }; // 前のカーソル位置 // ダブルクリック let mouseDownAreaInfo: NoteAreaInfo | GridAreaInfo | undefined; @@ -607,6 +640,112 @@ const previewResizeLeft = () => { guideLineX.value = guideLineBaseX * zoomX.value; }; +// ピッチを描く処理を行う +const previewDrawPitch = () => { + if (previewPitchEdit.value == undefined) { + throw new Error("previewPitchEdit.value is undefined."); + } + if (previewPitchEdit.value.type !== "draw") { + throw new Error("previewPitchEdit.value.type is not draw."); + } + const frameRate = editFrameRate.value; + const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; + const cursorBaseY = (scrollY.value + cursorY.value) / zoomY.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); + const cursorFrame = Math.round(cursorSeconds * frameRate); + const cursorNoteNumber = baseYToNoteNumber(cursorBaseY, false); + const cursorFrequency = noteNumberToFrequency(cursorNoteNumber); + if (cursorFrame < 0) { + return; + } + const tempPitchEdit = { + ...previewPitchEdit.value, + data: [...previewPitchEdit.value.data], + }; + + if (cursorFrame < tempPitchEdit.startFrame) { + const numOfFramesToUnshift = tempPitchEdit.startFrame - cursorFrame; + tempPitchEdit.data = new Array(numOfFramesToUnshift) + .fill(0) + .concat(tempPitchEdit.data); + tempPitchEdit.startFrame = cursorFrame; + } + + const lastFrame = tempPitchEdit.startFrame + tempPitchEdit.data.length - 1; + if (cursorFrame > lastFrame) { + const numOfFramesToPush = cursorFrame - lastFrame; + tempPitchEdit.data = tempPitchEdit.data.concat( + new Array(numOfFramesToPush).fill(0), + ); + } + + if (cursorFrame === prevCursorPos.frame) { + const i = cursorFrame - tempPitchEdit.startFrame; + tempPitchEdit.data[i] = cursorFrequency; + } else if (cursorFrame < prevCursorPos.frame) { + for (let i = cursorFrame; i <= prevCursorPos.frame; i++) { + tempPitchEdit.data[i - tempPitchEdit.startFrame] = Math.exp( + linearInterpolation( + cursorFrame, + Math.log(cursorFrequency), + prevCursorPos.frame, + Math.log(prevCursorPos.frequency), + i, + ), + ); + } + } else { + for (let i = prevCursorPos.frame; i <= cursorFrame; i++) { + tempPitchEdit.data[i - tempPitchEdit.startFrame] = Math.exp( + linearInterpolation( + prevCursorPos.frame, + Math.log(prevCursorPos.frequency), + cursorFrame, + Math.log(cursorFrequency), + i, + ), + ); + } + } + + previewPitchEdit.value = tempPitchEdit; + prevCursorPos.frame = cursorFrame; + prevCursorPos.frequency = cursorFrequency; +}; + +// ドラッグした範囲のピッチ編集データを消去する処理を行う +const previewErasePitch = () => { + if (previewPitchEdit.value == undefined) { + throw new Error("previewPitchEdit.value is undefined."); + } + if (previewPitchEdit.value.type !== "erase") { + throw new Error("previewPitchEdit.value.type is not erase."); + } + const frameRate = editFrameRate.value; + const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); + const cursorFrame = Math.round(cursorSeconds * frameRate); + if (cursorFrame < 0) { + return; + } + const tempPitchEdit = { ...previewPitchEdit.value }; + + if (tempPitchEdit.startFrame > cursorFrame) { + tempPitchEdit.frameLength += tempPitchEdit.startFrame - cursorFrame; + tempPitchEdit.startFrame = cursorFrame; + } + + const lastFrame = tempPitchEdit.startFrame + tempPitchEdit.frameLength - 1; + if (lastFrame < cursorFrame) { + tempPitchEdit.frameLength += cursorFrame - lastFrame; + } + + previewPitchEdit.value = tempPitchEdit; + prevCursorPos.frame = cursorFrame; +}; + const preview = () => { if (executePreviewProcess) { if (previewMode === "ADD") { @@ -621,6 +760,12 @@ const preview = () => { if (previewMode === "RESIZE_LEFT") { previewResizeLeft(); } + if (previewMode === "DRAW_PITCH") { + previewDrawPitch(); + } + if (previewMode === "ERASE_PITCH") { + previewErasePitch(); + } executePreviewProcess = false; } previewRequestId = requestAnimationFrame(preview); @@ -662,78 +807,175 @@ const startPreview = (event: MouseEvent, mode: PreviewMode, note?: Note) => { } const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value; const cursorBaseY = (scrollY.value + cursorY.value) / zoomY.value; - const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); - const cursorNoteNumber = baseYToNoteNumber(cursorBaseY); - // NOTE: 入力を補助する線の判定の境目はスナップ幅の3/4の位置 - const guideLineTicks = - Math.round(cursorTicks / snapTicks.value - 0.25) * snapTicks.value; - const copiedNotes: Note[] = []; - if (mode === "ADD") { - if (cursorNoteNumber < 0) { - return; - } - note = { - id: uuidv4(), - position: guideLineTicks, - duration: snapTicks.value, - noteNumber: cursorNoteNumber, - lyric: getDoremiFromNoteNumber(cursorNoteNumber), - }; - store.dispatch("DESELECT_ALL_NOTES"); - copiedNotes.push(note); - } else { - if (!note) { - throw new Error("note is undefined."); - } - if (event.shiftKey) { - let minIndex = notes.value.length - 1; - let maxIndex = 0; - for (let i = 0; i < notes.value.length; i++) { - const noteId = notes.value[i].id; - if (state.selectedNoteIds.has(noteId) || noteId === note.id) { - minIndex = Math.min(minIndex, i); - maxIndex = Math.max(maxIndex, i); - } + + if (editTarget.value === "NOTE") { + // 編集ターゲットがノートのときの処理 + + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorNoteNumber = baseYToNoteNumber(cursorBaseY, true); + // NOTE: 入力を補助する線の判定の境目はスナップ幅の3/4の位置 + const guideLineTicks = + Math.round(cursorTicks / snapTicks.value - 0.25) * snapTicks.value; + const copiedNotes: Note[] = []; + if (mode === "ADD") { + if (cursorNoteNumber < 0) { + return; } - const noteIdsToSelect: string[] = []; - for (let i = minIndex; i <= maxIndex; i++) { - const noteId = notes.value[i].id; - if (!state.selectedNoteIds.has(noteId)) { - noteIdsToSelect.push(noteId); + note = { + id: uuidv4(), + position: guideLineTicks, + duration: snapTicks.value, + noteNumber: cursorNoteNumber, + lyric: getDoremiFromNoteNumber(cursorNoteNumber), + }; + store.dispatch("DESELECT_ALL_NOTES"); + copiedNotes.push(note); + } else { + if (!note) { + throw new Error("note is undefined."); + } + if (event.shiftKey) { + let minIndex = notes.value.length - 1; + let maxIndex = 0; + for (let i = 0; i < notes.value.length; i++) { + const noteId = notes.value[i].id; + if (state.selectedNoteIds.has(noteId) || noteId === note.id) { + minIndex = Math.min(minIndex, i); + maxIndex = Math.max(maxIndex, i); + } + } + const noteIdsToSelect: string[] = []; + for (let i = minIndex; i <= maxIndex; i++) { + const noteId = notes.value[i].id; + if (!state.selectedNoteIds.has(noteId)) { + noteIdsToSelect.push(noteId); + } } + store.dispatch("SELECT_NOTES", { noteIds: noteIdsToSelect }); + } else if (isOnCommandOrCtrlKeyDown(event)) { + store.dispatch("SELECT_NOTES", { noteIds: [note.id] }); + } else if (!state.selectedNoteIds.has(note.id)) { + selectOnlyThis(note); + } + for (const note of selectedNotes.value) { + copiedNotes.push({ ...note }); } - store.dispatch("SELECT_NOTES", { noteIds: noteIdsToSelect }); - } else if (isOnCommandOrCtrlKeyDown(event)) { - store.dispatch("SELECT_NOTES", { noteIds: [note.id] }); - } else if (!state.selectedNoteIds.has(note.id)) { - selectOnlyThis(note); } - for (const note of selectedNotes.value) { - copiedNotes.push({ ...note }); + dragStartTicks = cursorTicks; + dragStartNoteNumber = cursorNoteNumber; + dragStartGuideLineTicks = guideLineTicks; + draggingNoteId = note.id; + edited = mode === "ADD"; + copiedNotesForPreview.clear(); + for (const copiedNote of copiedNotes) { + copiedNotesForPreview.set(copiedNote.id, copiedNote); } + previewNotes.value = copiedNotes; + } else if (editTarget.value === "PITCH") { + // 編集ターゲットがピッチのときの処理 + + const frameRate = editFrameRate.value; + const cursorTicks = baseXToTick(cursorBaseX, tpqn.value); + const cursorSeconds = tickToSecond(cursorTicks, tempos.value, tpqn.value); + const cursorFrame = Math.round(cursorSeconds * frameRate); + const cursorNoteNumber = baseYToNoteNumber(cursorBaseY, false); + const cursorFrequency = noteNumberToFrequency(cursorNoteNumber); + if (mode === "DRAW_PITCH") { + previewPitchEdit.value = { + type: "draw", + data: [cursorFrequency], + startFrame: cursorFrame, + }; + } else if (mode === "ERASE_PITCH") { + previewPitchEdit.value = { + type: "erase", + startFrame: cursorFrame, + frameLength: 1, + }; + } else { + throw new Error("Unknown preview mode."); + } + prevCursorPos.frame = cursorFrame; + prevCursorPos.frequency = cursorFrequency; + } else { + throw new ExhaustiveError(editTarget.value); } previewMode = mode; - dragStartTicks = cursorTicks; - dragStartNoteNumber = cursorNoteNumber; - dragStartGuideLineTicks = guideLineTicks; - draggingNoteId = note.id; + previewStartEditTarget = editTarget.value; executePreviewProcess = true; - edited = mode === "ADD"; - copiedNotesForPreview.clear(); - for (const copiedNote of copiedNotes) { - copiedNotesForPreview.set(copiedNote.id, copiedNote); - } - previewNotes.value = copiedNotes; nowPreviewing.value = true; previewRequestId = requestAnimationFrame(preview); }; +const endPreview = () => { + cancelAnimationFrame(previewRequestId); + if (previewStartEditTarget === "NOTE") { + // 編集ターゲットがノートのときにプレビューを開始した場合の処理 + + if (edited) { + if (previewMode === "ADD") { + store.dispatch("COMMAND_ADD_NOTES", { notes: previewNotes.value }); + store.dispatch("SELECT_NOTES", { + noteIds: previewNotes.value.map((value) => value.id), + }); + } else { + store.dispatch("COMMAND_UPDATE_NOTES", { notes: previewNotes.value }); + } + if (previewNotes.value.length === 1) { + store.dispatch("PLAY_PREVIEW_SOUND", { + noteNumber: previewNotes.value[0].noteNumber, + duration: PREVIEW_SOUND_DURATION, + }); + } + } + } else if (previewStartEditTarget === "PITCH") { + // 編集ターゲットがピッチのときにプレビューを開始した場合の処理 + + if (previewPitchEdit.value == undefined) { + throw new Error("previewPitchEdit.value is undefined."); + } + const previewPitchEditType = previewPitchEdit.value.type; + if (previewPitchEditType === "draw") { + // カーソルを動かさずにマウスのボタンを離したときに1フレームのみの変更になり、 + // 1フレームの変更はピッチ編集ラインとして表示されないので、無視する + if (previewPitchEdit.value.data.length >= 2) { + // 平滑化を行う + let data = previewPitchEdit.value.data; + data = data.map((value) => Math.log(value)); + applyGaussianFilter(data, 0.7); + data = data.map((value) => Math.exp(value)); + + store.dispatch("COMMAND_SET_PITCH_EDIT_DATA", { + data, + startFrame: previewPitchEdit.value.startFrame, + }); + } + } else if (previewPitchEditType === "erase") { + store.dispatch("COMMAND_ERASE_PITCH_EDIT_DATA", { + startFrame: previewPitchEdit.value.startFrame, + frameLength: previewPitchEdit.value.frameLength, + }); + } else { + throw new ExhaustiveError(previewPitchEditType); + } + previewPitchEdit.value = undefined; + } else { + throw new ExhaustiveError(previewStartEditTarget); + } + nowPreviewing.value = false; +}; + const onNoteBarMouseDown = (event: MouseEvent, note: Note) => { - if (!isSelfEventTarget(event)) { + if (editTarget.value !== "NOTE" || !isSelfEventTarget(event)) { return; } - if (event.button === 0) { + const mouseButton = getButton(event); + // ダブルクリック用の処理を行う + if (mouseButton === "LEFT_BUTTON") { mouseDownAreaInfo = new NoteAreaInfo(note.id); + } + + if (mouseButton === "LEFT_BUTTON") { startPreview(event, "MOVE", note); } else if (!state.selectedNoteIds.has(note.id)) { selectOnlyThis(note); @@ -741,11 +983,16 @@ const onNoteBarMouseDown = (event: MouseEvent, note: Note) => { }; const onNoteLeftEdgeMouseDown = (event: MouseEvent, note: Note) => { - if (!isSelfEventTarget(event)) { + if (editTarget.value !== "NOTE" || !isSelfEventTarget(event)) { return; } - if (event.button === 0) { + const mouseButton = getButton(event); + // ダブルクリック用の処理を行う + if (mouseButton === "LEFT_BUTTON") { mouseDownAreaInfo = new NoteAreaInfo(note.id); + } + + if (mouseButton === "LEFT_BUTTON") { startPreview(event, "RESIZE_LEFT", note); } else if (!state.selectedNoteIds.has(note.id)) { selectOnlyThis(note); @@ -753,11 +1000,16 @@ const onNoteLeftEdgeMouseDown = (event: MouseEvent, note: Note) => { }; const onNoteRightEdgeMouseDown = (event: MouseEvent, note: Note) => { - if (!isSelfEventTarget(event)) { + if (editTarget.value !== "NOTE" || !isSelfEventTarget(event)) { return; } - if (event.button === 0) { + const mouseButton = getButton(event); + // ダブルクリック用の処理を行う + if (mouseButton === "LEFT_BUTTON") { mouseDownAreaInfo = new NoteAreaInfo(note.id); + } + + if (mouseButton === "LEFT_BUTTON") { startPreview(event, "RESIZE_RIGHT", note); } else if (!state.selectedNoteIds.has(note.id)) { selectOnlyThis(note); @@ -765,41 +1017,61 @@ const onNoteRightEdgeMouseDown = (event: MouseEvent, note: Note) => { }; const onNoteLyricMouseDown = (event: MouseEvent, note: Note) => { - if (!isSelfEventTarget(event)) { + if (editTarget.value !== "NOTE" || !isSelfEventTarget(event)) { return; } - if (event.button === 0) { + const mouseButton = getButton(event); + // ダブルクリック用の処理を行う + if (mouseButton === "LEFT_BUTTON") { mouseDownAreaInfo = new NoteAreaInfo(note.id); } + if (!state.selectedNoteIds.has(note.id)) { selectOnlyThis(note); } }; const onMouseDown = (event: MouseEvent) => { - if (!isSelfEventTarget(event)) { + if (editTarget.value === "NOTE" && !isSelfEventTarget(event)) { return; } - - // macOSの場合、Ctrl+クリックが右クリックのため、その場合はノートを追加しない - if (isMac && event.ctrlKey && event.button === 0) { - return; + const mouseButton = getButton(event); + // ダブルクリック用の処理を行う + if (mouseButton === "LEFT_BUTTON") { + mouseDownAreaInfo = new GridAreaInfo(); } // TODO: メニューが表示されている場合はメニュー非表示のみ行いたい - - // 選択中のノートが無い場合、プレビューを開始しノートIDをリセット - if (event.button === 0) { - mouseDownAreaInfo = new GridAreaInfo(); - if (event.shiftKey) { - isRectSelecting.value = true; - rectSelectStartX.value = cursorX.value; - rectSelectStartY.value = cursorY.value; + if (editTarget.value === "NOTE") { + if (mouseButton === "LEFT_BUTTON") { + if (event.shiftKey) { + isRectSelecting.value = true; + rectSelectStartX.value = cursorX.value; + rectSelectStartY.value = cursorY.value; + } else { + startPreview(event, "ADD"); + } } else { - startPreview(event, "ADD"); + store.dispatch("DESELECT_ALL_NOTES"); + } + } else if (editTarget.value === "PITCH") { + if (isMac) { + // Macの場合、左ボタンでDRAW、右ボタンでERASE + if (mouseButton === "LEFT_BUTTON") { + startPreview(event, "DRAW_PITCH"); + } else if (mouseButton === "RIGHT_BUTTON") { + startPreview(event, "ERASE_PITCH"); + } + } else if (mouseButton === "LEFT_BUTTON") { + // Mac以外の場合、左ボタンでDRAW、左ボタン+CtrlでERASE + if (event.ctrlKey) { + startPreview(event, "ERASE_PITCH"); + } else { + startPreview(event, "DRAW_PITCH"); + } } } else { - store.dispatch("DESELECT_ALL_NOTES"); + throw new ExhaustiveError(editTarget.value); } }; @@ -826,39 +1098,22 @@ const onMouseMove = (event: MouseEvent) => { }; const onMouseUp = (event: MouseEvent) => { - if (event.button !== 0) { + const mouseButton = getButton(event); + if (mouseButton !== "LEFT_BUTTON") { return; } + // ダブルクリック用の処理を行う if (mouseDownAreaInfo) { doubleClickDetector.recordClick(event.detail, mouseDownAreaInfo); } - ignoreDoubleClick = nowPreviewing.value && edited; + ignoreDoubleClick = + editTarget.value !== "NOTE" || (nowPreviewing.value && edited); if (isRectSelecting.value) { rectSelect(isOnCommandOrCtrlKeyDown(event)); - return; - } - if (!nowPreviewing.value) { - return; - } - cancelAnimationFrame(previewRequestId); - if (edited) { - if (previewMode === "ADD") { - store.dispatch("COMMAND_ADD_NOTES", { notes: previewNotes.value }); - store.dispatch("SELECT_NOTES", { - noteIds: previewNotes.value.map((value) => value.id), - }); - } else { - store.dispatch("COMMAND_UPDATE_NOTES", { notes: previewNotes.value }); - } - if (previewNotes.value.length === 1) { - store.dispatch("PLAY_PREVIEW_SOUND", { - noteNumber: previewNotes.value[0].noteNumber, - duration: PREVIEW_SOUND_DURATION, - }); - } + } else if (nowPreviewing.value) { + endPreview(); } - nowPreviewing.value = false; }; /** @@ -1451,4 +1706,10 @@ const contextMenuData = ref([ border: 2px solid rgba(colors.$primary-rgb, 0.5); background: rgba(colors.$primary-rgb, 0.25); } + +.cursor-draw { + cursor: + url("/draw-cursor.png") 2 30, + auto; +} diff --git a/src/components/Sing/SequencerNote.vue b/src/components/Sing/SequencerNote.vue index 6467bc9289..0523e2452f 100644 --- a/src/components/Sing/SequencerNote.vue +++ b/src/components/Sing/SequencerNote.vue @@ -6,7 +6,7 @@ 'preview-lyric': props.previewLyric != undefined, overlapping: hasOverlappingError, 'invalid-phrase': hasPhraseError, - 'below-pitch': showPitch, + 'below-pitch': editTargetIsPitch, }" :style="{ width: `${width}px`, @@ -14,10 +14,32 @@ transform: `translate3d(${positionX}px,${positionY}px,0)`, }" > -
-
-
- +
+
+
+
{ } return "NORMAL"; }); +const editTargetIsNote = computed(() => { + return state.sequencerEditTarget === "NOTE"; +}); +const editTargetIsPitch = computed(() => { + return state.sequencerEditTarget === "PITCH"; +}); // ノートの重なりエラー const hasOverlappingError = computed(() => { @@ -159,9 +187,6 @@ const temporaryLyric = ref(undefined); const showLyricInput = computed(() => { return state.editingLyricNoteId === props.note.id; }); -const showPitch = computed(() => { - return state.experimentalSetting.showPitchInSongEditor; -}); const contextMenu = ref>(); const contextMenuData = ref([ { @@ -290,7 +315,7 @@ const onLyricInput = (event: Event) => { &.below-pitch { .note-bar { - background-color: rgba(hsl(33, 100%, 50%), 0.18); + background-color: rgba(colors.$primary-rgb, 0.18); } } } @@ -353,7 +378,6 @@ const onLyricInput = (event: Event) => { background-color: colors.$primary; border: 1px solid rgba(colors.$background-rgb, 0.5); border-radius: 2px; - cursor: move; } .note-left-edge { @@ -362,7 +386,6 @@ const onLyricInput = (event: Event) => { left: -1px; width: 5px; height: 100%; - cursor: ew-resize; } .note-right-edge { @@ -371,7 +394,6 @@ const onLyricInput = (event: Event) => { right: -1px; width: 5px; height: 100%; - cursor: ew-resize; } .note-lyric-input { @@ -384,4 +406,12 @@ const onLyricInput = (event: Event) => { outline: 2px solid colors.$primary; border-radius: 0.25rem; } + +.cursor-move { + cursor: move; +} + +.cursor-ew-resize { + cursor: ew-resize; +} diff --git a/src/components/Sing/SequencerPitch.vue b/src/components/Sing/SequencerPitch.vue index 3a4b7878eb..0a2cca7da5 100644 --- a/src/components/Sing/SequencerPitch.vue +++ b/src/components/Sing/SequencerPitch.vue @@ -5,39 +5,56 @@