Skip to content

Commit

Permalink
ソング:矩形選択を追加 (#1911)
Browse files Browse the repository at this point in the history
  • Loading branch information
sevenc-nanashi authored Mar 13, 2024
1 parent de3913a commit bb8aca8
Showing 1 changed file with 114 additions and 20 deletions.
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>

0 comments on commit bb8aca8

Please sign in to comment.