Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ソング:矩形選択を追加 #1911

Merged
merged 12 commits into from
Mar 13, 2024
134 changes: 114 additions & 20 deletions src/components/Sing/ScoreSequencer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<div
ref="sequencerBody"
class="sequencer-body"
:class="{ 'rect-selecting': shiftKey }"
aria-label="シーケンサ"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
Expand Down Expand Up @@ -147,6 +148,17 @@
marginBottom: `${scrollBarWidth}px`,
}"
>
<div
ref="rectSelectHitbox"
class="rect-select-preview"
:style="{
display: isRectSelecting ? 'block' : 'none',
left: `${Math.min(rectSelectStartX, cursorX)}px`,
top: `${Math.min(rectSelectStartY, cursorY)}px`,
width: `${Math.abs(cursorX - rectSelectStartX)}px`,
height: `${Math.abs(cursorY - rectSelectStartY)}px`,
}"
/>
<SequencerPhraseIndicator
v-for="phraseInfo in phraseInfos"
:key="phraseInfo.key"
Expand Down Expand Up @@ -239,6 +251,7 @@ import SequencerPhraseIndicator from "@/components/Sing/SequencerPhraseIndicator
import CharacterPortrait from "@/components/Sing/CharacterPortrait.vue";
import SequencerPitch from "@/components/Sing/SequencerPitch.vue";
import { isOnCommandOrCtrlKeyDown } from "@/store/utility";
import { useShiftKey } from "@/composables/useModifierKey";

type PreviewMode = "ADD" | "MOVE" | "RESIZE_RIGHT" | "RESIZE_LEFT";

Expand All @@ -251,12 +264,16 @@ const isSelfEventTarget = (event: UIEvent) => {

const store = useStore();
const state = store.state;

// 分解能(Ticks Per Quarter Note)
const tpqn = computed(() => state.tpqn);

// テンポ
const tempos = computed(() => state.tempos);

// 拍子
const timeSignatures = computed(() => state.timeSignatures);

// ノート
const notes = computed(() => store.getters.SELECTED_TRACK.notes);
const unselectedNotes = computed(() => {
Expand All @@ -267,13 +284,23 @@ const selectedNotes = computed(() => {
const selectedNoteIds = state.selectedNoteIds;
return notes.value.filter((value) => selectedNoteIds.has(value.id));
});

// 矩形選択
const shiftKey = useShiftKey();
const isRectSelecting = ref(false);
const rectSelectStartX = ref(0);
const rectSelectStartY = ref(0);
const rectSelectHitbox = ref<HTMLElement | undefined>(undefined);

// ズーム状態
const zoomX = computed(() => state.sequencerZoomX);
const zoomY = computed(() => state.sequencerZoomY);

// スナップ
const snapTicks = computed(() => {
return getNoteDuration(state.sequencerSnapType, tpqn.value);
});

// シーケンサグリッド
const gridCellTicks = snapTicks; // ひとまずスナップ幅=グリッドセル幅
const gridCellWidth = computed(() => {
Expand Down Expand Up @@ -318,15 +345,18 @@ const gridWidth = computed(() => {
const gridHeight = computed(() => {
return gridCellHeight.value * keyInfos.length;
});

// スクロール位置
const scrollX = ref(0);
const scrollY = ref(0);

// 再生ヘッドの位置
const playheadTicks = ref(0);
const playheadX = computed(() => {
const baseX = tickToBaseX(playheadTicks.value, tpqn.value);
return Math.floor(baseX * zoomX.value);
});

// フレーズ
const phraseInfos = computed(() => {
return [...state.phrases.entries()].map(([key, phrase]) => {
Expand All @@ -342,9 +372,11 @@ const showPitch = computed(() => {
});
const scrollBarWidth = ref(12);
const sequencerBody = ref<HTMLElement | null>(null);

// マウスカーソル位置
let cursorX = 0;
let cursorY = 0;
const cursorX = ref(0);
const cursorY = ref(0);

// プレビュー
// FIXME: 関連する値を1つのobjectにまとめる
const nowPreviewing = ref(false);
Expand All @@ -358,19 +390,21 @@ let dragStartGuideLineTicks = 0;
let draggingNoteId = ""; // FIXME: 無効状態はstring以外の型にする
let executePreviewProcess = false;
let edited = false; // プレビュー終了時にstore.stateの更新を行うかどうかを表す変数

// ダブルクリック
let mouseDownNoteId: string | undefined;
const clickedNoteIds: [string | undefined, string | undefined] = [
undefined,
undefined,
];
let ignoreDoubleClick = false;

// 入力を補助する線
const showGuideLine = ref(true);
const guideLineX = ref(0);

const previewAdd = () => {
const cursorBaseX = (scrollX.value + cursorX) / zoomX.value;
const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value;
const cursorTicks = baseXToTick(cursorBaseX, tpqn.value);
const draggingNote = copiedNotesForPreview.get(draggingNoteId);
if (!draggingNote) {
Expand Down Expand Up @@ -403,8 +437,8 @@ const previewAdd = () => {
};

const previewMove = () => {
const cursorBaseX = (scrollX.value + cursorX) / zoomX.value;
const cursorBaseY = (scrollY.value + cursorY) / zoomY.value;
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);
const draggingNote = copiedNotesForPreview.get(draggingNoteId);
Expand Down Expand Up @@ -451,7 +485,7 @@ const previewMove = () => {
};

const previewResizeRight = () => {
const cursorBaseX = (scrollX.value + cursorX) / zoomX.value;
const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value;
const cursorTicks = baseXToTick(cursorBaseX, tpqn.value);
const draggingNote = copiedNotesForPreview.get(draggingNoteId);
if (!draggingNote) {
Expand Down Expand Up @@ -491,7 +525,7 @@ const previewResizeRight = () => {
};

const previewResizeLeft = () => {
const cursorBaseX = (scrollX.value + cursorX) / zoomX.value;
const cursorBaseX = (scrollX.value + cursorX.value) / zoomX.value;
const cursorTicks = baseXToTick(cursorBaseX, tpqn.value);
const draggingNote = copiedNotesForPreview.get(draggingNoteId);
if (!draggingNote) {
Expand Down Expand Up @@ -582,16 +616,16 @@ const startPreview = (event: MouseEvent, mode: PreviewMode, note?: Note) => {
if (!sequencerBodyElement) {
throw new Error("sequencerBodyElement is null.");
}
cursorX = getXInBorderBox(event.clientX, sequencerBodyElement);
cursorY = getYInBorderBox(event.clientY, sequencerBodyElement);
if (cursorX >= sequencerBodyElement.clientWidth) {
cursorX.value = getXInBorderBox(event.clientX, sequencerBodyElement);
cursorY.value = getYInBorderBox(event.clientY, sequencerBodyElement);
if (cursorX.value >= sequencerBodyElement.clientWidth) {
return;
}
if (cursorY >= sequencerBodyElement.clientHeight) {
if (cursorY.value >= sequencerBodyElement.clientHeight) {
return;
}
const cursorBaseX = (scrollX.value + cursorX) / zoomX.value;
const cursorBaseY = (scrollY.value + cursorY) / zoomY.value;
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の位置
Expand Down Expand Up @@ -711,7 +745,13 @@ const onMouseDown = (event: MouseEvent) => {
return;
}
if (event.button === 0) {
startPreview(event, "ADD");
if (event.shiftKey) {
isRectSelecting.value = true;
rectSelectStartX.value = cursorX.value;
rectSelectStartY.value = cursorY.value;
} else {
startPreview(event, "ADD");
}
mouseDownNoteId = undefined;
} else {
store.dispatch("DESELECT_ALL_NOTES");
Expand All @@ -723,14 +763,14 @@ const onMouseMove = (event: MouseEvent) => {
if (!sequencerBodyElement) {
throw new Error("sequencerBodyElement is null.");
}
cursorX = getXInBorderBox(event.clientX, sequencerBodyElement);
cursorY = getYInBorderBox(event.clientY, sequencerBodyElement);
cursorX.value = getXInBorderBox(event.clientX, sequencerBodyElement);
cursorY.value = getYInBorderBox(event.clientY, sequencerBodyElement);

if (nowPreviewing.value) {
executePreviewProcess = true;
} else {
const scrollLeft = sequencerBodyElement.scrollLeft;
const cursorBaseX = (scrollLeft + cursorX) / zoomX.value;
const cursorBaseX = (scrollLeft + cursorX.value) / zoomX.value;
const cursorTicks = baseXToTick(cursorBaseX, tpqn.value);
// NOTE: 入力を補助する線の判定の境目はスナップ幅の3/4の位置
const guideLineTicks =
Expand All @@ -753,6 +793,11 @@ const onMouseUp = (event: MouseEvent) => {
ignoreDoubleClick = true;
}

if (isRectSelecting.value) {
rectSelect();
return;
}

if (!nowPreviewing.value) {
return;
}
Expand All @@ -776,6 +821,44 @@ const onMouseUp = (event: MouseEvent) => {
nowPreviewing.value = false;
};

const rectSelect = () => {
const rectSelectHitboxElement = rectSelectHitbox.value;
if (!rectSelectHitboxElement) {
throw new Error("rectSelectHitboxElement is null.");
}
isRectSelecting.value = false;
const left = Math.min(rectSelectStartX.value, cursorX.value);
const top = Math.min(rectSelectStartY.value, cursorY.value);
const width = Math.abs(cursorX.value - rectSelectStartX.value);
const height = Math.abs(cursorY.value - rectSelectStartY.value);
const startTicks = baseXToTick(
(scrollX.value + left) / zoomX.value,
tpqn.value
);
const endTicks = baseXToTick(
(scrollX.value + left + width) / zoomX.value,
tpqn.value
);
const endNoteNumber = baseYToNoteNumber((scrollY.value + top) / zoomY.value);
const startNoteNumber = baseYToNoteNumber(
(scrollY.value + top + height) / zoomY.value
);

const noteIdsToSelect: string[] = [];
for (const note of notes.value) {
if (
note.position + note.duration >= startTicks &&
note.position <= endTicks &&
startNoteNumber <= note.noteNumber &&
note.noteNumber <= endNoteNumber
) {
noteIdsToSelect.push(note.id);
}
}
store.dispatch("DESELECT_ALL_NOTES");
store.dispatch("SELECT_NOTES", { noteIds: noteIdsToSelect });
};

const onDoubleClick = () => {
if (
ignoreDoubleClick ||
Expand Down Expand Up @@ -954,7 +1037,7 @@ const onWheel = (event: WheelEvent) => {
throw new Error("sequencerBodyElement is null.");
}
if (isOnCommandOrCtrlKeyDown(event)) {
cursorX = getXInBorderBox(event.clientX, sequencerBodyElement);
cursorX.value = getXInBorderBox(event.clientX, sequencerBodyElement);
// マウスカーソル位置を基準に水平方向のズームを行う
const oldZoomX = zoomX.value;
let newZoomX = zoomX.value;
Expand All @@ -965,8 +1048,8 @@ const onWheel = (event: WheelEvent) => {
const scrollTop = sequencerBodyElement.scrollTop;

store.dispatch("SET_ZOOM_X", { zoomX: newZoomX }).then(() => {
const cursorBaseX = (scrollLeft + cursorX) / oldZoomX;
const newScrollLeft = cursorBaseX * newZoomX - cursorX;
const cursorBaseX = (scrollLeft + cursorX.value) / oldZoomX;
const newScrollLeft = cursorBaseX * newZoomX - cursorX.value;
sequencerBodyElement.scrollTo(newScrollLeft, scrollTop);
});
}
Expand Down Expand Up @@ -1100,6 +1183,10 @@ onDeactivated(() => {
backface-visibility: hidden;
overflow: auto;
position: relative;

&.rect-selecting {
cursor: crosshair;
}
}

.sequencer-grid {
Expand Down Expand Up @@ -1180,4 +1267,11 @@ onDeactivated(() => {
border-left: 1px solid rgba(colors.$background-rgb, 0.83);
border-right: 1px solid rgba(colors.$background-rgb, 0.83);
}

.rect-select-preview {
pointer-events: none;
position: absolute;
border: 2px solid rgba(colors.$primary-rgb, 0.5);
background: rgba(colors.$primary-rgb, 0.25);
}
</style>
Loading