Skip to content

Commit a31189d

Browse files
romot-cosigprogrammingHiroshibagithub-actions[bot]
authored
feat: ソング:ループの機能追加 (#2506)
Co-authored-by: Sig <[email protected]> Co-authored-by: Hiroshiba <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent e085883 commit a31189d

File tree

50 files changed

+2911
-706
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2911
-706
lines changed

.storybook/preview.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Quasar, Dialog, Loading, Notify } from "quasar";
33
import iconSet from "quasar/icon-set/material-icons";
44
import { withThemeByDataAttribute } from "@storybook/addon-themes";
55
import { addActionsWithEmits } from "./utils/argTypesEnhancers";
6+
import { store, storeKey } from "@/store";
67
import { markdownItPlugin } from "@/plugins/markdownItPlugin";
78

89
import "@quasar/extras/material-icons/material-icons.css";
@@ -29,6 +30,7 @@ setup((app) => {
2930
},
3031
});
3132
app.use(markdownItPlugin);
33+
app.use(store, storeKey);
3234
});
3335

3436
const preview: Preview = {

src/components/Sing/ScoreSequencer.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
class="sequencer-playhead"
172172
data-testid="sequencer-playhead"
173173
:style="{
174-
transform: `translateX(${playheadX - scrollX}px)`,
174+
transform: `translateX(${playheadX - scrollX - 1}px)`,
175175
}"
176176
></div>
177177
</div>
@@ -227,7 +227,7 @@ import type { InjectionKey } from "vue";
227227
228228
export const numMeasuresInjectionKey: InjectionKey<{
229229
numMeasures: ComputedRef<number>;
230-
}> = Symbol();
230+
}> = Symbol("sequencerNumMeasures");
231231
</script>
232232

233233
<script setup lang="ts">
Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,98 @@
11
<template>
22
<Presentation
3-
:offset
4-
:numMeasures
5-
:tpqn
6-
:tempos
7-
:timeSignatures
8-
:sequencerZoomX
9-
:uiLocked
10-
:playheadTicks
11-
:sequencerSnapType
12-
@update:playheadTicks="updatePlayheadTicks"
13-
@removeTempo="removeTempo"
14-
@removeTimeSignature="removeTimeSignature"
15-
@setTempo="setTempo"
16-
@setTimeSignature="setTimeSignature"
17-
@deselectAllNotes="deselectAllNotes"
18-
/>
3+
:width="rulerWidth"
4+
:playheadX
5+
:offset="currentOffset"
6+
@click="handleClick"
7+
>
8+
<template #grid>
9+
<GridLaneContainer />
10+
</template>
11+
<template #changes>
12+
<ValueChangesLaneContainer />
13+
</template>
14+
<template #loop>
15+
<LoopLaneContainer />
16+
</template>
17+
</Presentation>
1918
</template>
2019

20+
<script lang="ts">
21+
import { Ref, InjectionKey } from "vue";
22+
23+
// Provide/Injectで使用するキー
24+
export const offsetInjectionKey: InjectionKey<Ref<number>> =
25+
Symbol("rulerOffset");
26+
</script>
27+
2128
<script setup lang="ts">
22-
import { computed } from "vue";
29+
import { computed, provide, readonly, toRef } from "vue";
2330
import Presentation from "./Presentation.vue";
31+
import GridLaneContainer from "./GridLane/Container.vue";
32+
import ValueChangesLaneContainer from "./ValueChangesLane/Container.vue";
33+
import LoopLaneContainer from "./LoopLane/Container.vue";
2434
import { useStore } from "@/store";
25-
import type { Tempo, TimeSignature } from "@/domain/project/type";
35+
import { useSequencerLayout } from "@/composables/useSequencerLayout";
36+
import { SEQUENCER_MIN_NUM_MEASURES, baseXToTick } from "@/sing/viewHelper";
37+
import { snapTickToBeat } from "@/sing/rulerHelper";
2638
2739
defineOptions({
2840
name: "SequencerRuler",
2941
});
3042
31-
withDefaults(
43+
const props = withDefaults(
3244
defineProps<{
3345
offset?: number;
3446
numMeasures?: number;
3547
}>(),
3648
{
3749
offset: 0,
38-
numMeasures: 32,
50+
numMeasures: SEQUENCER_MIN_NUM_MEASURES,
3951
},
4052
);
4153
4254
const store = useStore();
4355
56+
// provideするoffsetとnumMeasures
57+
// toRefでリアクティブ性を維持する
58+
const currentOffset = toRef(props, "offset");
59+
const currentNumMeasures = toRef(props, "numMeasures");
60+
61+
// provideする値はreadonlyにして子コンポーネントでの変更を防ぐ
62+
provide(offsetInjectionKey, readonly(currentOffset));
63+
4464
const tpqn = computed(() => store.state.tpqn);
45-
const tempos = computed(() => store.state.tempos);
4665
const timeSignatures = computed(() => store.state.timeSignatures);
4766
const sequencerZoomX = computed(() => store.state.sequencerZoomX);
48-
const uiLocked = computed(() => store.getters.UI_LOCKED);
49-
const sequencerSnapType = computed(() => store.state.sequencerSnapType);
67+
const playheadPosition = computed(() => store.getters.PLAYHEAD_POSITION);
5068
51-
const playheadTicks = computed(() => store.getters.PLAYHEAD_POSITION);
69+
const { rulerWidth, playheadX } = useSequencerLayout({
70+
timeSignatures,
71+
tpqn,
72+
playheadPosition,
73+
sequencerZoomX,
74+
offset: currentOffset,
75+
numMeasures: currentNumMeasures,
76+
});
5277
53-
const updatePlayheadTicks = (ticks: number) => {
78+
// 再生ヘッド位置の設定
79+
const setPlayheadPosition = (ticks: number) => {
5480
void store.actions.SET_PLAYHEAD_POSITION({ position: ticks });
5581
};
5682
83+
// ノートの選択解除
5784
const deselectAllNotes = () => {
5885
void store.actions.DESELECT_ALL_NOTES();
5986
};
6087
61-
const setTempo = (tempo: Tempo) => {
62-
void store.actions.COMMAND_SET_TEMPO({
63-
tempo,
64-
});
65-
};
66-
const setTimeSignature = (timeSignature: TimeSignature) => {
67-
void store.actions.COMMAND_SET_TIME_SIGNATURE({
68-
timeSignature,
69-
});
70-
};
71-
const removeTempo = (position: number) => {
72-
void store.actions.COMMAND_REMOVE_TEMPO({
73-
position,
74-
});
75-
};
76-
const removeTimeSignature = (measureNumber: number) => {
77-
void store.actions.COMMAND_REMOVE_TIME_SIGNATURE({
78-
measureNumber,
79-
});
88+
// クリックでスナップした位置に移動
89+
// 再生ヘッドも移動
90+
const handleClick = (event: MouseEvent) => {
91+
deselectAllNotes();
92+
const targetOffsetX = event.offsetX;
93+
const baseX = (currentOffset.value + targetOffsetX) / sequencerZoomX.value;
94+
const baseXTick = baseXToTick(baseX, tpqn.value);
95+
const nextTicks = snapTickToBeat(baseXTick, timeSignatures.value, tpqn.value);
96+
setPlayheadPosition(nextTicks);
8097
};
8198
</script>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<template>
2+
<Presentation
3+
:tpqn
4+
:sequencerZoomX
5+
:numMeasures="numMeasures.value"
6+
:offset="injectedOffset"
7+
:timeSignatures
8+
:tsPositions
9+
:width="rulerWidth"
10+
:totalTicks
11+
:measureInfos
12+
/>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import { computed, inject } from "vue";
17+
import { numMeasuresInjectionKey } from "../../ScoreSequencer.vue";
18+
import { offsetInjectionKey } from "../Container.vue";
19+
import Presentation from "./Presentation.vue";
20+
import { useStore } from "@/store";
21+
import { useSequencerLayout } from "@/composables/useSequencerLayout";
22+
23+
defineOptions({
24+
name: "GridLaneContainer",
25+
});
26+
27+
const store = useStore();
28+
29+
const injectedOffset = inject(offsetInjectionKey);
30+
if (injectedOffset == undefined) {
31+
throw new Error("injectedOffset is undefined.");
32+
}
33+
34+
const injectedNumMeasures = inject(numMeasuresInjectionKey);
35+
if (injectedNumMeasures == undefined) {
36+
throw new Error("injectedNumMeasures is undefined.");
37+
}
38+
39+
const numMeasures = computed(() => injectedNumMeasures.numMeasures);
40+
41+
const tpqn = computed(() => store.state.tpqn);
42+
const timeSignatures = computed(() => store.state.timeSignatures);
43+
const sequencerZoomX = computed(() => store.state.sequencerZoomX);
44+
const playheadPosition = computed(() => store.getters.PLAYHEAD_POSITION);
45+
46+
const { rulerWidth, tsPositions, totalTicks, measureInfos } =
47+
useSequencerLayout({
48+
timeSignatures,
49+
tpqn,
50+
playheadPosition,
51+
sequencerZoomX,
52+
offset: injectedOffset,
53+
numMeasures: numMeasures.value,
54+
});
55+
</script>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<template>
2+
<svg
3+
xmlns="http://www.w3.org/2000/svg"
4+
:width
5+
:height
6+
shape-rendering="crispEdges"
7+
class="grid-lane"
8+
>
9+
<defs>
10+
<pattern
11+
v-for="(gridPattern, patternIndex) in gridPatterns"
12+
:id="`grid-lane-measure-${patternIndex}`"
13+
:key="`pattern-${patternIndex}`"
14+
patternUnits="userSpaceOnUse"
15+
:x="-offset + gridPattern.x"
16+
:width="gridPattern.patternWidth"
17+
:height
18+
>
19+
<!-- 拍線(小節の最初を除く) -->
20+
<line
21+
v-for="n in gridPattern.beatsPerMeasure"
22+
:key="n"
23+
:x1="gridPattern.beatWidth * n"
24+
:x2="gridPattern.beatWidth * n"
25+
y1="32"
26+
:y2="height"
27+
class="grid-lane-beat-line"
28+
/>
29+
</pattern>
30+
</defs>
31+
<rect
32+
v-for="(gridPattern, index) in gridPatterns"
33+
:key="`grid-${index}`"
34+
:x="0.5 + gridPattern.x - offset"
35+
y="0"
36+
:height
37+
:width="gridPattern.width"
38+
:fill="`url(#grid-lane-measure-${index})`"
39+
/>
40+
<!-- 小節線と小節番号 -->
41+
<template v-for="measureInfo in measureInfos" :key="measureInfo.number">
42+
<line
43+
:x1="measureInfo.x - offset"
44+
:x2="measureInfo.x - offset"
45+
y1="0"
46+
:y2="height"
47+
class="grid-lane-measure-line"
48+
:class="{ 'first-measure-line': measureInfo.number === 1 }"
49+
/>
50+
<text
51+
:x="measureInfo.x - offset + 4"
52+
y="20"
53+
class="grid-lane-measure-number"
54+
>
55+
{{ measureInfo.number }}
56+
</text>
57+
</template>
58+
</svg>
59+
</template>
60+
61+
<script setup lang="ts">
62+
import { computed, ref } from "vue";
63+
import type { TimeSignature } from "@/domain/project/type";
64+
import { useSequencerGrid } from "@/composables/useSequencerGridPattern";
65+
import { MeasureInfo } from "@/composables/useSequencerLayout";
66+
67+
defineOptions({
68+
name: "GridLanePresentation",
69+
});
70+
71+
const props = defineProps<{
72+
tpqn: number;
73+
sequencerZoomX: number;
74+
numMeasures: number;
75+
timeSignatures: TimeSignature[];
76+
offset: number;
77+
width: number;
78+
totalTicks: number;
79+
tsPositions: number[];
80+
measureInfos: MeasureInfo[];
81+
}>();
82+
83+
const height = ref(40);
84+
85+
const gridPatterns = useSequencerGrid({
86+
timeSignatures: computed(() => props.timeSignatures),
87+
tpqn: computed(() => props.tpqn),
88+
sequencerZoomX: computed(() => props.sequencerZoomX),
89+
numMeasures: computed(() => props.numMeasures),
90+
});
91+
</script>
92+
93+
<style scoped lang="scss">
94+
@use "@/styles/v2/variables" as vars;
95+
96+
.grid-lane-beat-line {
97+
backface-visibility: hidden;
98+
stroke: var(--scheme-color-sing-ruler-beat-line);
99+
stroke-width: 1px;
100+
}
101+
102+
.grid-lane-measure-line {
103+
backface-visibility: hidden;
104+
stroke: var(--scheme-color-sing-ruler-measure-line);
105+
stroke-width: 1px;
106+
107+
// NOTE: 最初の小節線を非表示。必要に応じて再表示・位置合わせする
108+
&.first-measure-line {
109+
stroke: var(--scheme-color-sing-ruler-surface);
110+
}
111+
}
112+
113+
.grid-lane-measure-number {
114+
font-size: 12px;
115+
font-weight: bold;
116+
fill: var(--scheme-color-on-surface-variant);
117+
user-select: none;
118+
}
119+
</style>

0 commit comments

Comments
 (0)