diff --git a/src/components/Sing/SequencerLoopControl.vue b/src/components/Sing/SequencerLoopControl.vue index e79ea6d1db..335563633e 100644 --- a/src/components/Sing/SequencerLoopControl.vue +++ b/src/components/Sing/SequencerLoopControl.vue @@ -2,12 +2,13 @@
+
@@ -83,6 +87,9 @@ import { useLoopControl } from "@/composables/useLoopControl"; import { useCursorState, CursorState } from "@/composables/useCursorState"; import { tickToBaseX, baseXToTick } from "@/sing/viewHelper"; import { getNoteDuration } from "@/sing/domain"; +import ContextMenu, { + ContextMenuItemData, +} from "@/components/Menu/ContextMenu.vue"; const props = defineProps<{ width: number; @@ -133,10 +140,10 @@ const adjustedHeight = computed(() => ); const onLoopAreaMouseDown = (event: MouseEvent) => { + if (event.button !== 0 || (event.ctrlKey && event.button === 0)) return; if (isDragging.value) { void stopDragging(); } - if (event.button !== 0) return; const target = event.currentTarget as HTMLElement; const rect = target.getBoundingClientRect(); const x = event.clientX - rect.left + props.offset; @@ -163,8 +170,14 @@ const onEndHandleMouseDown = (event: MouseEvent) => { startDragging("end", event); }; +const onHandleDoubleClick = () => { + // ハンドルのダブルクリックでループを0地点に設定する + void setLoopRange(0, 0); +}; + // ドラッグ開始処理 const startDragging = (target: "start" | "end", event: MouseEvent) => { + if (event.button !== 0) return; isDragging.value = true; dragTarget.value = target; dragStartX.value = event.clientX; @@ -178,6 +191,7 @@ const startDragging = (target: "start" | "end", event: MouseEvent) => { // ドラッグ中処理 const onDrag = (event: MouseEvent) => { if (!isDragging.value || !dragTarget.value) return; + if (event.button !== 0) return; // ドラッグ中のX座標 const dx = event.clientX - dragStartX.value; @@ -241,6 +255,34 @@ const stopDragging = async () => { window.removeEventListener("mouseup", stopDragging); }; +const onContextMenu = (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); +}; +const contextMenu = ref>(); +const contextMenuData = computed(() => { + return [ + { + type: "button", + label: isLoopEnabled.value ? "ループ無効" : "ループ有効", + onClick: () => { + contextMenu.value?.hide(); + void setLoopEnabled(!isLoopEnabled.value); + }, + disableWhenUiLocked: true, + }, + { + type: "button", + label: "ループを0地点にリセット", + onClick: () => { + contextMenu.value?.hide(); + void setLoopRange(0, 0); + }, + disableWhenUiLocked: true, + }, + ]; +}); + onUnmounted(() => { setCursorState(CursorState.UNSET); }); @@ -253,52 +295,78 @@ onUnmounted(() => { left: 0; width: 100%; pointer-events: auto; + cursor: pointer; - &:not(.cursor-ew-resize) { - cursor: pointer; + &.cursor-ew-resize { + cursor: ew-resize; } - &.loop-dragging .loop-range-visible { - fill: var(--scheme-color-outline-variant); + // ホバー時のループエリア + &:hover .loop-area { + fill: var(--scheme-color-sing-loop-area); } } +// ループエリア .loop-area { fill: transparent; + transition: fill 0.1s ease-out; } +// ループ範囲 .loop-range { - fill: transparent; -} - -.loop-range-visible { - fill: var(--scheme-color-primary-fixed-dim); -} + &-area { + fill: transparent; + } -.loop-disabled .loop-range-visible { fill: var(--scheme-color-outline); + opacity: 1; } +// ループハンドル .loop-handle { - fill: var(--scheme-color-primary-fixed-dim); - stroke: var(--scheme-color-primary-fixed-dim); - stroke-width: 2; + fill: var(--scheme-color-outline); + stroke: var(--scheme-color-outline); + stroke-width: 0; stroke-linejoin: round; - &.loop-handle-disabled { - fill: var(--scheme-color-secondary); - stroke: var(--scheme-color-secondary); + &-no-length { + opacity: 0.5; } } -.loop-disabled .loop-handle { - fill: var(--scheme-color-outline); - stroke: var(--scheme-color-outline); -} - +// ドラッグエリア .loop-drag-area { fill: transparent; cursor: ew-resize; pointer-events: all; } + +// ドラッグ中の状態 +.loop-dragging { + .loop-area { + fill: var(--scheme-color-sing-loop-area); + } + + .loop-range { + opacity: 0.6; + } +} + +// ループが有効な状態 +.loop-enabled { + .loop-range { + fill: var(--scheme-color-primary-fixed-dim); + } + + .loop-handle { + fill: var(--scheme-color-primary-fixed-dim); + stroke: var(--scheme-color-primary-fixed-dim); + + &-no-length { + fill: var(--scheme-color-outline); + stroke: var(--scheme-color-outline); + } + } +} diff --git a/src/components/Sing/ToolBar/ToolBar.vue b/src/components/Sing/ToolBar/ToolBar.vue index b2be429aea..ae0ef298e9 100644 --- a/src/components/Sing/ToolBar/ToolBar.vue +++ b/src/components/Sing/ToolBar/ToolBar.vue @@ -438,7 +438,7 @@ const goToZero = () => { const { isLoopEnabled, setLoopEnabled } = useLoopControl(); const toggleLoop = () => { - setLoopEnabled(!isLoopEnabled.value); + void setLoopEnabled(!isLoopEnabled.value); }; const volume = computed({ diff --git a/src/styles/v2/sing-colors.scss b/src/styles/v2/sing-colors.scss index 96c4189d30..ab741b62d8 100644 --- a/src/styles/v2/sing-colors.scss +++ b/src/styles/v2/sing-colors.scss @@ -408,6 +408,10 @@ SASSのmapなどで構造化+mixin一括処理などで処理可能ですが、 --scheme-color-sing-shadow-note: oklch( var(--lr-84) var(--neutral-variant-c) var(--neutral-variant-h) ); + + --scheme-color-sing-loop-area: oklch( + var(--lr-86) var(--neutral-variant-c) var(--neutral-variant-h) + ); } /* ダークテーマ */ @@ -671,4 +675,8 @@ SASSのmapなどで構造化+mixin一括処理などで処理可能ですが、 --scheme-color-sing-shadow-note: oklch( var(--lr-40) var(--neutral-variant-c) var(--neutral-variant-h) ); + + --scheme-color-sing-loop-area: oklch( + var(--lr-34) var(--neutral-variant-c) var(--neutral-variant-h) + ); }