diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 5e0335b1ff..23b27bddcb 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -3,6 +3,7 @@ import { Quasar, Dialog, Loading, Notify } from "quasar"; import iconSet from "quasar/icon-set/material-icons"; import { withThemeByDataAttribute } from "@storybook/addon-themes"; import { addActionsWithEmits } from "./utils/argTypesEnhancers"; +import { store, storeKey } from "@/store"; import { markdownItPlugin } from "@/plugins/markdownItPlugin"; import "@quasar/extras/material-icons/material-icons.css"; @@ -29,6 +30,7 @@ setup((app) => { }, }); app.use(markdownItPlugin); + app.use(store, storeKey); }); const preview: Preview = { diff --git a/src/components/Sing/SequencerRuler/Container.vue b/src/components/Sing/SequencerRuler/Container.vue index 07111d281f..dcfeab2fb1 100644 --- a/src/components/Sing/SequencerRuler/Container.vue +++ b/src/components/Sing/SequencerRuler/Container.vue @@ -1,34 +1,57 @@ diff --git a/src/components/Sing/SequencerRuler/GridLane/Container.vue b/src/components/Sing/SequencerRuler/GridLane/Container.vue new file mode 100644 index 0000000000..e22543bb25 --- /dev/null +++ b/src/components/Sing/SequencerRuler/GridLane/Container.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/components/Sing/SequencerRuler/GridLane/Presentation.vue b/src/components/Sing/SequencerRuler/GridLane/Presentation.vue new file mode 100644 index 0000000000..27fe1d201e --- /dev/null +++ b/src/components/Sing/SequencerRuler/GridLane/Presentation.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/src/components/Sing/SequencerRuler/LoopLane/Container.vue b/src/components/Sing/SequencerRuler/LoopLane/Container.vue new file mode 100644 index 0000000000..9246f6cca4 --- /dev/null +++ b/src/components/Sing/SequencerRuler/LoopLane/Container.vue @@ -0,0 +1,364 @@ + + + diff --git a/src/components/Sing/SequencerRuler/LoopLane/Presentation.vue b/src/components/Sing/SequencerRuler/LoopLane/Presentation.vue new file mode 100644 index 0000000000..1c06c659d8 --- /dev/null +++ b/src/components/Sing/SequencerRuler/LoopLane/Presentation.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/src/components/Sing/SequencerRuler/LoopLane/index.stories.ts b/src/components/Sing/SequencerRuler/LoopLane/index.stories.ts new file mode 100644 index 0000000000..416c41659e --- /dev/null +++ b/src/components/Sing/SequencerRuler/LoopLane/index.stories.ts @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from "@storybook/vue3"; +import { fn } from "@storybook/test"; + +import Presentation from "./Presentation.vue"; + +const meta: Meta = { + component: Presentation, + args: { + width: 1000, + offset: 0, + loopStartX: 100, + loopEndX: 300, + isLoopEnabled: true, + isDragging: false, + isEmpty: false, + cursorClass: "", + contextMenuData: [], + onLoopAreaMouseDown: fn(), + onLoopRangeClick: fn(), + onLoopRangeDoubleClick: fn(), + onStartHandleMouseDown: fn(), + onEndHandleMouseDown: fn(), + onContextMenu: fn(), + }, + render: (args) => ({ + components: { Presentation }, + setup() { + return { args }; + }, + template: `
`, + }), +}; + +// 表示のみ +// TODO: テスト追加 + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + name: "デフォルト", + args: {}, +}; + +export const Disabled: Story = { + name: "無効状態", + args: { + isLoopEnabled: false, + }, +}; + +export const Empty: Story = { + name: "空の状態", + args: { + isEmpty: true, + loopStartX: 0, + loopEndX: 0, + }, +}; + +export const DraggingEnabled: Story = { + name: "ドラッグ中(有効)", + args: { + isLoopEnabled: true, + isDragging: true, + cursorClass: "cursor-ew-resize", + }, +}; + +export const DraggingDisabled: Story = { + name: "ドラッグ中(無効)", + args: { + isLoopEnabled: false, + isDragging: true, + cursorClass: "cursor-ew-resize", + }, +}; + +export const LongLoop: Story = { + name: "長いループ範囲", + args: { + loopStartX: 100, + loopEndX: 800, + }, +}; + +export const ShortLoop: Story = { + name: "短いループ範囲", + args: { + loopStartX: 100, + loopEndX: 150, + }, +}; diff --git a/src/components/Sing/SequencerRuler/LoopLane/index.ts b/src/components/Sing/SequencerRuler/LoopLane/index.ts new file mode 100644 index 0000000000..b671f9f6af --- /dev/null +++ b/src/components/Sing/SequencerRuler/LoopLane/index.ts @@ -0,0 +1,3 @@ +import Container from "./Container.vue"; + +export default Container; diff --git a/src/components/Sing/SequencerRuler/Presentation.vue b/src/components/Sing/SequencerRuler/Presentation.vue index 89811ddcfb..59452cd115 100644 --- a/src/components/Sing/SequencerRuler/Presentation.vue +++ b/src/components/Sing/SequencerRuler/Presentation.vue @@ -1,223 +1,56 @@ diff --git a/src/components/Sing/SequencerRuler/ValueChangesLane/Container.vue b/src/components/Sing/SequencerRuler/ValueChangesLane/Container.vue new file mode 100644 index 0000000000..126932ad8a --- /dev/null +++ b/src/components/Sing/SequencerRuler/ValueChangesLane/Container.vue @@ -0,0 +1,357 @@ + + + diff --git a/src/components/Sing/SequencerRuler/ValueChangesLane/Presentation.vue b/src/components/Sing/SequencerRuler/ValueChangesLane/Presentation.vue new file mode 100644 index 0000000000..ec59362e3a --- /dev/null +++ b/src/components/Sing/SequencerRuler/ValueChangesLane/Presentation.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/src/components/Sing/SequencerRuler/index.stories.ts b/src/components/Sing/SequencerRuler/index.stories.ts index 9eaef47d25..73673d0ac8 100644 --- a/src/components/Sing/SequencerRuler/index.stories.ts +++ b/src/components/Sing/SequencerRuler/index.stories.ts @@ -1,34 +1,57 @@ import type { Meta, StoryObj } from "@storybook/vue3"; import { fn, expect, Mock } from "@storybook/test"; -import { ref } from "vue"; - +import { ref, computed } from "vue"; import Presentation from "./Presentation.vue"; -import { UnreachableError } from "@/type/utility"; +import GridLaneContainer from "./GridLane/Container.vue"; +import ValueChangesLaneContainer from "./ValueChangesLane/Container.vue"; +import LoopLaneContainer from "./LoopLane/Container.vue"; + import { ZOOM_X_MIN, ZOOM_X_MAX, ZOOM_X_STEP } from "@/sing/viewHelper"; +import { UnreachableError } from "@/type/utility"; +import { useSequencerRuler } from "@/composables/useSequencerRuler"; +import { Tempo, TimeSignature } from "@/store/type"; +import { useStore } from "@/store"; -const meta: Meta = { +const meta = { + title: "Components/Sing/SequencerRuler", component: Presentation, - args: { - tempos: [ - { - bpm: 120, - position: 0, + // NOTE: 混合コンポーネントのため実際のstoreをデコレーター経由で利用している + // 本来はPresentationのみのテストにすべきかもしれない + decorators: [ + (story, context) => ({ + components: { story }, + setup() { + // テンポや拍子の変更をエミュレートするため各storyのargsをstoreに設定する + const store = useStore(); + if (context.args.tempos) { + store.commit("SET_TEMPOS", { + tempos: context.args.tempos as Tempo[], + }); + } + if (context.args.timeSignatures) { + store.commit("SET_TIME_SIGNATURES", { + timeSignatures: context.args.timeSignatures as TimeSignature[], + }); + } + return {}; }, - ], + template: ``, + }), + ], + args: { + width: 1000, + numMeasures: 32, + offset: 0, + tempos: [{ bpm: 120, position: 0 }] as Tempo[], timeSignatures: [ - { - beats: 4, - beatType: 4, - measureNumber: 1, - }, - ], - sequencerZoomX: 0.25, + { beats: 4, beatType: 4, measureNumber: 1 }, + ] as TimeSignature[], tpqn: 480, - offset: 0, - numMeasures: 32, + sequencerZoomX: 0.25, sequencerSnapType: 16, uiLocked: false, - "onUpdate:playheadTicks": fn<(value: number) => void>(), + playheadX: 0, + "onUpdate:playheadTicks": fn(), onDeselectAllNotes: fn(), }, argTypes: { @@ -42,14 +65,67 @@ const meta: Meta = { }, }, render: (args) => ({ - components: { Presentation }, + components: { + Presentation, + GridLaneContainer, + ValueChangesLaneContainer, + LoopLaneContainer, + }, setup() { const playheadTicks = ref(0); - return { args, playheadTicks }; + const { width, playheadX, getSnappedTickFromOffsetX } = useSequencerRuler( + { + offset: computed(() => args.offset as number), + numMeasures: computed(() => args.numMeasures as number), + tpqn: computed(() => args.tpqn as number), + timeSignatures: computed( + () => args.timeSignatures as TimeSignature[], + ), + sequencerZoomX: computed(() => args.sequencerZoomX as number), + playheadTicks: computed(() => playheadTicks.value), + sequencerSnapType: computed(() => args.sequencerSnapType as number), + }, + ); + + return { + args, + playheadTicks, + width, + playheadX, + getSnappedTickFromOffsetX, + }; }, - template: ``, + template: ` + + + + + + `, }), -}; +} satisfies Meta; export default meta; type Story = StoryObj; diff --git a/src/components/Sing/ToolBar/ToolBar.vue b/src/components/Sing/ToolBar/ToolBar.vue index 507a935ff3..d4d4be3035 100644 --- a/src/components/Sing/ToolBar/ToolBar.vue +++ b/src/components/Sing/ToolBar/ToolBar.vue @@ -113,6 +113,15 @@ icon="stop" @click="stop" /> + @@ -412,6 +421,12 @@ const goToZero = () => { void store.actions.SET_PLAYHEAD_POSITION({ position: 0 }); }; +const isLoopEnabled = computed(() => store.state.isLoopEnabled); + +const toggleLoop = () => { + void store.actions.SET_LOOP_ENABLED({ isLoopEnabled: !isLoopEnabled.value }); +}; + const volume = computed({ get() { return store.state.volume * 100; @@ -688,6 +703,17 @@ const snapTypeSelectModel = computed({ } } +.sing-playback-loop { + margin-left: 8px; + color: var(--scheme-color-on-surface-variant); + background: transparent; + + &-enabled { + color: var(--scheme-color-primary); + background: var(--scheme-color-secondary-container); + } +} + .sing-playhead-position { margin-left: 16px; } diff --git a/src/composables/useSequencerRuler.ts b/src/composables/useSequencerRuler.ts new file mode 100644 index 0000000000..2501171446 --- /dev/null +++ b/src/composables/useSequencerRuler.ts @@ -0,0 +1,85 @@ +import { computed, ComputedRef } from "vue"; +import { TimeSignature } from "@/store/type"; +import { + getTimeSignaturePositions, + getMeasureDuration, + snapTicksToGrid, +} from "@/sing/domain"; +import { tickToBaseX, baseXToTick } from "@/sing/viewHelper"; + +/** + * シーケンサのルーラーに関わる計算ロジックをまとめたコンポーザブル。 + * storeに依存させず、計算に必要なパラメータはすべて外部から受け取る想定。 + */ +export const useSequencerRuler = (params: { + offset: ComputedRef; + numMeasures: ComputedRef; + tpqn: ComputedRef; + timeSignatures: ComputedRef; + sequencerZoomX: ComputedRef; + playheadTicks: ComputedRef; + sequencerSnapType: ComputedRef; +}) => { + // 拍子ごとのTick位置 + const tsPositions = computed(() => { + return getTimeSignaturePositions( + params.timeSignatures.value, + params.tpqn.value, + ); + }); + + // 終了tick位置 + const endTicks = computed(() => { + const tsList = params.timeSignatures.value; + if (tsList.length === 0) return 0; + const lastTs = tsList[tsList.length - 1]; + const positions = tsPositions.value; + const lastTsPosition = positions[positions.length - 1]; + const measureDuration = getMeasureDuration( + lastTs.beats, + lastTs.beatType, + params.tpqn.value, + ); + return ( + lastTsPosition + + measureDuration * (params.numMeasures.value - lastTs.measureNumber + 1) + ); + }); + + // ルーラーの幅(px) + const width = computed(() => { + return ( + tickToBaseX(endTicks.value, params.tpqn.value) * + params.sequencerZoomX.value + ); + }); + + // 再生ヘッドのX位置(px) + const playheadX = computed(() => { + return Math.floor( + tickToBaseX(params.playheadTicks.value, params.tpqn.value) * + params.sequencerZoomX.value, + ); + }); + + /** + * 任意のクリック位置(offsetX)から、スナップされたTickを返す + */ + const getSnappedTickFromOffsetX = (offsetX: number) => { + const baseX = (params.offset.value + offsetX) / params.sequencerZoomX.value; + const baseTick = baseXToTick(baseX, params.tpqn.value); + return snapTicksToGrid( + baseTick, + params.timeSignatures.value, + params.tpqn.value, + ); + }; + + return { + tsPositions, + endTicks, + width, + playheadX, + getSnappedTickFromOffsetX, + }; +}; diff --git a/src/domain/project/schema.ts b/src/domain/project/schema.ts index eba32d5989..bcbe1a6f1c 100644 --- a/src/domain/project/schema.ts +++ b/src/domain/project/schema.ts @@ -99,6 +99,12 @@ export const trackSchema = z.object({ pan: z.number(), }); +export const loopSchema = z.object({ + startTick: z.number(), // ループ開始ティック + endTick: z.number(), // ループ終了ティック + isLoopEnabled: z.boolean(), +}); + // プロジェクトファイルのスキーマ export const projectSchema = z.object({ appVersion: z.string(), @@ -114,6 +120,7 @@ export const projectSchema = z.object({ timeSignatures: z.array(timeSignatureSchema), tracks: z.record(trackIdSchema, trackSchema), trackOrder: z.array(trackIdSchema), + loop: loopSchema.optional(), }), }); diff --git a/src/sing/audioRendering.ts b/src/sing/audioRendering.ts index c96ef4d531..911ed36b5f 100644 --- a/src/sing/audioRendering.ts +++ b/src/sing/audioRendering.ts @@ -56,6 +56,10 @@ interface EventScheduler { * 再生、停止、再生位置の変更などの機能を提供します。 */ export class Transport { + loop = false; + loopStartTime = 0; + loopEndTime = 0; + private readonly audioContext: AudioContext; private readonly timer: Timer; private readonly scheduleAheadTime: number; @@ -67,6 +71,12 @@ export class Transport { private startContextTime = 0; private startTime = 0; private schedulers = new Map(); + private scheduledContextTime = 0; + private uncompletedLoopInfos: { + readonly contextTime: number; + readonly timeBeforeLoop: number; + readonly timeAfterLoop: number; + }[] = []; get state() { return this._state; @@ -77,10 +87,8 @@ export class Transport { */ get time() { if (this._state === "started") { - // 再生中の場合は、現在時刻から再生位置を計算する const contextTime = this.audioContext.currentTime; - const elapsedTime = contextTime - this.startContextTime; - this._time = this.startTime + elapsedTime; + this._time = this.calcTime(contextTime); } return this._time; } @@ -113,13 +121,38 @@ export class Transport { this.audioContext = audioContext; this.scheduleAheadTime = scheduleAheadTime; this.timer = new Timer(lookahead * 1000); + this.timer.start(() => { if (this._state === "started") { - this.schedule(this.audioContext.currentTime); + this.scheduleEvents(this.audioContext.currentTime); } }); } + /** + * 再生位置を計算します。再生中にのみ使用可能です。 + * @param contextTime コンテキスト時刻(この時刻から再生位置を計算) + * @returns 計算された再生位置(秒) + */ + private calcTime(contextTime: number) { + if (this._state !== "started") { + throw new Error("This method can only be used during playback."); + } + if (contextTime >= this.startContextTime) { + const elapsedTime = contextTime - this.startContextTime; + return this.startTime + elapsedTime; + } + while (this.uncompletedLoopInfos.length !== 0) { + const loopInfo = this.uncompletedLoopInfos[0]; + if (contextTime < loopInfo.contextTime) { + const timeUntilLoop = loopInfo.contextTime - contextTime; + return loopInfo.timeBeforeLoop - timeUntilLoop; + } + this.uncompletedLoopInfos.shift(); + } + throw new Error("Loop events are not scheduled correctly."); + } + /** * スケジューラーを作成します。 * @param sequence スケジューラーでスケジューリングを行うシーケンス @@ -138,13 +171,15 @@ export class Transport { } /** - * スケジューリングを行います。 + * シーケンスのイベントのスケジューリングを行います。 * @param contextTime スケジューリングを行う時刻(コンテキスト時刻) */ - private schedule(contextTime: number) { - // 再生位置を計算 - const elapsedTime = contextTime - this.startContextTime; - const time = this.startTime + elapsedTime; + private scheduleSequenceEvents(contextTime: number) { + if (contextTime < this.startContextTime) { + // NOTE: ループ未完了の場合にここに来る + return; + } + const time = this.calcTime(contextTime); // シーケンスの削除を反映 const removedSequences: Sequence[] = []; @@ -167,11 +202,73 @@ export class Transport { } }); + // スケジューリングを行う this.schedulers.forEach((scheduler) => { scheduler.schedule(time + this.scheduleAheadTime); }); } + /** + * ループイベントのスケジューリングを行います。 + * @param contextTime スケジューリングを行う時刻(コンテキスト時刻) + */ + private scheduleLoopEvents(contextTime: number) { + if ( + !this.loop || + this.loopEndTime <= this.loopStartTime || + this.startTime >= this.loopEndTime + ) { + return; + } + + const timeUntilLoop = this.loopEndTime - this.startTime; + let contextTimeToLoop = this.startContextTime + timeUntilLoop; + if (contextTimeToLoop < this.scheduledContextTime) { + return; + } + if (contextTimeToLoop < contextTime) { + contextTimeToLoop = contextTime; + } + + const loopDuration = this.loopEndTime - this.loopStartTime; + + while (contextTimeToLoop < contextTime + this.scheduleAheadTime) { + this.uncompletedLoopInfos.push({ + contextTime: contextTimeToLoop, + timeBeforeLoop: this.loopEndTime, + timeAfterLoop: this.loopStartTime, + }); + + this.startContextTime = contextTimeToLoop; + this.startTime = this.loopStartTime; + + this.schedulers.forEach((value) => { + value.stop(contextTimeToLoop); + }); + this.schedulers.clear(); + + this.sequences.forEach((sequence) => { + const scheduler = this.createScheduler(sequence); + scheduler.start(contextTimeToLoop, this.loopStartTime); + scheduler.schedule(this.loopStartTime + this.scheduleAheadTime); + this.schedulers.set(sequence, scheduler); + }); + + contextTimeToLoop += loopDuration; + } + } + + /** + * イベントのスケジューリングを行います。 + * @param contextTime スケジューリングを行う時刻(コンテキスト時刻) + */ + private scheduleEvents(contextTime: number) { + this.scheduleSequenceEvents(contextTime); + this.scheduleLoopEvents(contextTime); + + this.scheduledContextTime = contextTime + this.scheduleAheadTime; + } + /** * シーケンスを追加します。再生中に追加した場合は、次のスケジューリングで反映されます。 * @param sequence 追加するシーケンス @@ -205,8 +302,10 @@ export class Transport { this.startContextTime = contextTime; this.startTime = this._time; + this.scheduledContextTime = contextTime; + this.uncompletedLoopInfos = []; - this.schedule(contextTime); + this.scheduleEvents(contextTime); } /** @@ -217,8 +316,7 @@ export class Transport { const contextTime = this.audioContext.currentTime; // 停止する前に再生位置を更新する - const elapsedTime = contextTime - this.startContextTime; - this._time = this.startTime + elapsedTime; + this._time = this.calcTime(contextTime); this._state = "stopped"; diff --git a/src/sing/domain.ts b/src/sing/domain.ts index bbd4482977..977f660cc0 100644 --- a/src/sing/domain.ts +++ b/src/sing/domain.ts @@ -1021,7 +1021,52 @@ export const shouldPlayTracks = (tracks: Map): Set => { /** * 指定されたティックを直近のグリッドに合わせる + * @param ticks スナップ対象のtick位置 + * @param timeSignatures 拍子情報の配列 + * @param tpqn TPQNの値 + * @returns スナップ後のtick位置 */ -export function snapTicksToGrid(ticks: number, snapTicks: number): number { - return Math.round(ticks / snapTicks) * snapTicks; +export function snapTicksToGrid( + ticks: number, + timeSignatures: TimeSignature[], + tpqn: number, +): number { + const tsPositions = getTimeSignaturePositions(timeSignatures, tpqn); + const nextTsIndex = tsPositions.findIndex((pos) => pos > ticks); + const currentTsIndex = + nextTsIndex === -1 ? tsPositions.length - 1 : nextTsIndex - 1; + const currentTs = timeSignatures[currentTsIndex]; + + // 現在の拍子に基づくグリッドサイズを計算 + const gridSize = getBeatDuration(currentTs.beatType, tpqn); + + // 拍子の開始位置からの相対位置を計算 + const tsPosition = tsPositions[currentTsIndex]; + const relativePosition = ticks - tsPosition; + + // グリッドにスナップ + const snappedRelativePosition = + Math.round(relativePosition / gridSize) * gridSize; + + return tsPosition + snappedRelativePosition; } + +/* + * ループ範囲が有効かどうかを判定する + * @param startTick ループ開始位置(tick) + * @param endTick ループ終了位置(tick) + * @returns ループ範囲が有効な場合はtrue + */ +export const isValidLoopRange = ( + startTick: number, + endTick: number, +): boolean => { + return ( + // 負の値は許容しない + startTick >= 0 && + endTick >= 0 && + // 整数である必要がある + Number.isInteger(startTick) && + Number.isInteger(endTick) + ); +}; diff --git a/src/store/project.ts b/src/store/project.ts index 3e5a0862d4..579dbc6b0a 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -59,7 +59,8 @@ const applySongProjectToStore = async ( actions: DotNotationDispatch, songProject: LatestProjectType["song"], ) => { - const { tpqn, tempos, timeSignatures, tracks, trackOrder } = songProject; + const { tpqn, tempos, timeSignatures, tracks, trackOrder, loop } = + songProject; await actions.SET_TPQN({ tpqn }); await actions.SET_TEMPOS({ tempos }); @@ -73,6 +74,16 @@ const applySongProjectToStore = async ( }), ), }); + + // ループ情報を設定(ない場合はデフォルト値を使用) + // TODO: オプショナルチェインを避けたい(プロジェクト関連の仕様がよくわかっていない) + await actions.SET_LOOP_ENABLED({ + isLoopEnabled: loop?.isLoopEnabled ?? false, + }); + await actions.SET_LOOP_RANGE({ + loopStartTick: loop?.startTick ?? 0, + loopEndTick: loop?.endTick ?? 0, + }); }; export const projectStore = createPartialStore({ @@ -295,6 +306,9 @@ export const projectStore = createPartialStore({ timeSignatures, tracks, trackOrder, + isLoopEnabled, + loopStartTick, + loopEndTick, } = context.state; const projectData: LatestProjectType = { appVersion: appInfos.version, @@ -308,6 +322,11 @@ export const projectStore = createPartialStore({ timeSignatures, tracks: Object.fromEntries(tracks), trackOrder, + loop: { + isLoopEnabled, + startTick: loopStartTick, + endTick: loopEndTick, + }, }, }; diff --git a/src/store/singing.ts b/src/store/singing.ts index 5df2810978..55db44ace5 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -93,6 +93,7 @@ import { toEntirePhonemeTimings, adjustPhonemeTimingsAndPhraseEndFrames, phonemeTimingsToPhonemes, + isValidLoopRange, } from "@/sing/domain"; import { getOverlappingNoteIds } from "@/sing/storeHelper"; import { @@ -758,6 +759,9 @@ export const singingStoreState: SingingStoreState = { exportState: "NOT_EXPORTING", cancellationOfExportRequested: false, isSongSidebarOpen: false, + isLoopEnabled: false, + loopStartTick: 0, + loopEndTick: 0, }; export const singingStore = createPartialStore({ @@ -1488,6 +1492,21 @@ export const singingStore = createPartialStore({ } mutations.SET_PLAYBACK_STATE({ nowPlaying: true }); + // TODO: 以下の処理(ループの設定)は再生開始時に毎回行う必要はないので、 + // ソングエディタ初期化時に1回だけ行うようにする + // NOTE: 初期化のactionを作った方が良いかも + transport.loop = state.isLoopEnabled; + transport.loopStartTime = tickToSecond( + state.loopStartTick, + state.tempos, + state.tpqn, + ); + transport.loopEndTime = tickToSecond( + state.loopEndTick, + state.tempos, + state.tpqn, + ); + transport.start(); animationTimer.start(() => { playheadPosition.value = getters.SECOND_TO_TICK(transport.time); @@ -3448,6 +3467,54 @@ export const singingStore = createPartialStore({ }, }, + SET_LOOP_ENABLED: { + mutation(state, { isLoopEnabled }) { + state.isLoopEnabled = isLoopEnabled; + }, + async action({ mutations }, { isLoopEnabled }) { + if (!transport) { + throw new Error("transport is undefined"); + } + mutations.SET_LOOP_ENABLED({ isLoopEnabled }); + transport.loop = isLoopEnabled; + }, + }, + + SET_LOOP_RANGE: { + mutation(state, { loopStartTick, loopEndTick }) { + state.loopStartTick = loopStartTick; + state.loopEndTick = loopEndTick; + }, + async action({ state, mutations }, { loopStartTick, loopEndTick }) { + if (!transport) { + throw new Error("transport is undefined"); + } + + if (!isValidLoopRange(loopStartTick, loopEndTick)) { + throw new Error("The loop range is invalid."); + } + + mutations.SET_LOOP_RANGE({ loopStartTick, loopEndTick }); + + transport.loopStartTime = tickToSecond( + loopStartTick, + state.tempos, + state.tpqn, + ); + transport.loopEndTime = tickToSecond( + loopEndTick, + state.tempos, + state.tpqn, + ); + }, + }, + + CLEAR_LOOP_RANGE: { + action({ mutations }) { + mutations.SET_LOOP_RANGE({ loopStartTick: 0, loopEndTick: 0 }); + }, + }, + EXPORT_SONG_PROJECT: { action: createUILockAction( async ( @@ -4049,6 +4116,36 @@ export const singingCommandStore = transformCommandStore( }, ), }, + COMMAND_SET_LOOP_ENABLED: { + mutation(draft, { isLoopEnabled }) { + singingStore.mutations.SET_LOOP_ENABLED(draft, { isLoopEnabled }); + }, + action({ mutations }, { isLoopEnabled }) { + mutations.COMMAND_SET_LOOP_ENABLED({ isLoopEnabled }); + }, + }, + COMMAND_SET_LOOP_RANGE: { + mutation(draft, { loopStartTick, loopEndTick }) { + singingStore.mutations.SET_LOOP_RANGE(draft, { + loopStartTick, + loopEndTick, + }); + }, + action({ mutations }, { loopStartTick, loopEndTick }) { + mutations.COMMAND_SET_LOOP_RANGE({ loopStartTick, loopEndTick }); + }, + }, + COMMAND_CLEAR_LOOP_RANGE: { + mutation(draft) { + singingStore.mutations.SET_LOOP_RANGE(draft, { + loopStartTick: 0, + loopEndTick: 0, + }); + }, + action({ mutations }) { + mutations.COMMAND_CLEAR_LOOP_RANGE(); + }, + }, }), "song", ); diff --git a/src/store/type.ts b/src/store/type.ts index a57e243881..a6115340e9 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -70,6 +70,7 @@ import { tempoSchema, timeSignatureSchema, trackSchema, + loopSchema, } from "@/domain/project/schema"; import { HotkeySettingType } from "@/domain/hotkeyAction"; import { @@ -760,6 +761,8 @@ export type Singer = z.infer; export type Track = z.infer; +export type Loop = z.infer; + export type PhraseState = | "SINGER_IS_NOT_SET" | "WAITING_TO_BE_RENDERED" @@ -907,6 +910,9 @@ export type SingingStoreState = { exportState: SongExportState; cancellationOfExportRequested: boolean; isSongSidebarOpen: boolean; + isLoopEnabled: boolean; + loopStartTick: number; + loopEndTick: number; }; export type SingingStoreTypes = { @@ -1381,6 +1387,20 @@ export type SingingStoreTypes = { action(payload: { device: string }): void; }; + SET_LOOP_ENABLED: { + mutation: { isLoopEnabled: boolean }; + action(payload: { isLoopEnabled: boolean }): void; + }; + + SET_LOOP_RANGE: { + mutation: { loopStartTick: number; loopEndTick: number }; + action(payload: { loopStartTick: number; loopEndTick: number }): void; + }; + + CLEAR_LOOP_RANGE: { + action(): void; + }; + EXPORT_SONG_PROJECT: { action(payload: { fileType: ExportSongProjectFileType; @@ -1547,6 +1567,21 @@ export type SingingCommandStoreTypes = { trackIndexes: number[]; }): void; }; + + COMMAND_SET_LOOP_ENABLED: { + mutation: { isLoopEnabled: boolean }; + action(payload: { isLoopEnabled: boolean }): void; + }; + + COMMAND_SET_LOOP_RANGE: { + mutation: { loopStartTick: number; loopEndTick: number }; + action(payload: { loopStartTick: number; loopEndTick: number }): void; + }; + + COMMAND_CLEAR_LOOP_RANGE: { + mutation: undefined; + action(): void; + }; }; /* diff --git a/src/styles/v2/variables.scss b/src/styles/v2/variables.scss index abe056c97a..f72f2e771a 100644 --- a/src/styles/v2/variables.scss +++ b/src/styles/v2/variables.scss @@ -34,6 +34,9 @@ $z-index-sing-background: 0; $z-index-sing-note: 10; $z-index-sing-note-lyric: 20; $z-index-sing-pitch: 30; -$z-index-sing-playhead: 40; -$z-index-sing-tool-palette: 50; -$z-index-sing-lyric-input: 60; +$z-index-sing-loop-background: 35; +$z-index-sing-loop-range: 40; +$z-index-sing-loop-handle: 45; +$z-index-sing-playhead: 50; +$z-index-sing-tool-palette: 60; +$z-index-sing-lyric-input: 70; diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" index 175e5c9728..35e8d149d8 100644 Binary files "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" and "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-dark-storybook-win32.png" index d9bd5289d8..44931de703 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-dark-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-light-storybook-win32.png" index 227b1735c0..f14c68042b 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-light-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--default-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--dense-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--dense-dark-storybook-win32.png" index 4c92c33a0b..261f38c88c 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--dense-dark-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--dense-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--dense-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--dense-light-storybook-win32.png" index 52168d3efa..c03023cc3a 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--dense-light-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--dense-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-bpm-change-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-bpm-change-dark-storybook-win32.png" index 70b2dfe918..1a2ef8568f 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-bpm-change-dark-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-bpm-change-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-bpm-change-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-bpm-change-light-storybook-win32.png" index bededfc443..7dba1efef6 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-bpm-change-light-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-bpm-change-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-offset-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-offset-dark-storybook-win32.png" index 85b09c1763..ac6efcacf6 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-offset-dark-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-offset-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-offset-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-offset-light-storybook-win32.png" index d12cbfde46..36d0cca2b1 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-offset-light-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-offset-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-time-signature-change-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-time-signature-change-dark-storybook-win32.png" index a5510d4b5e..171815de5f 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-time-signature-change-dark-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-time-signature-change-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-time-signature-change-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-time-signature-change-light-storybook-win32.png" index 32195209fe..42bdacf12b 100644 Binary files "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-time-signature-change-light-storybook-win32.png" and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler--with-time-signature-change-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--default-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--default-dark-storybook-win32.png" new file mode 100644 index 0000000000..d1d8a2f9f2 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--default-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--default-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--default-light-storybook-win32.png" new file mode 100644 index 0000000000..e248b631c9 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--default-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--disabled-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--disabled-dark-storybook-win32.png" new file mode 100644 index 0000000000..15daa53d8f Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--disabled-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--disabled-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--disabled-light-storybook-win32.png" new file mode 100644 index 0000000000..ae697de2b2 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--disabled-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-disabled-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-disabled-dark-storybook-win32.png" new file mode 100644 index 0000000000..ea5742e61b Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-disabled-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-disabled-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-disabled-light-storybook-win32.png" new file mode 100644 index 0000000000..e990fb434a Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-disabled-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-enabled-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-enabled-dark-storybook-win32.png" new file mode 100644 index 0000000000..be26a09401 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-enabled-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-enabled-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-enabled-light-storybook-win32.png" new file mode 100644 index 0000000000..1d0def345c Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--dragging-enabled-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--empty-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--empty-dark-storybook-win32.png" new file mode 100644 index 0000000000..218e36b35a Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--empty-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--empty-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--empty-light-storybook-win32.png" new file mode 100644 index 0000000000..798961d72f Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--empty-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--long-loop-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--long-loop-dark-storybook-win32.png" new file mode 100644 index 0000000000..8c9b56de1a Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--long-loop-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--long-loop-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--long-loop-light-storybook-win32.png" new file mode 100644 index 0000000000..60313210c0 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--long-loop-light-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--short-loop-dark-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--short-loop-dark-storybook-win32.png" new file mode 100644 index 0000000000..ab992e2300 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--short-loop-dark-storybook-win32.png" differ diff --git "a/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--short-loop-light-storybook-win32.png" "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--short-loop-light-storybook-win32.png" new file mode 100644 index 0000000000..20040af0a4 Binary files /dev/null and "b/tests/e2e/storybook/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.mts-snapshots/components-sing-sequencerruler-looplane--short-loop-light-storybook-win32.png" differ