Skip to content

Commit

Permalink
分割およびstorybook追加
Browse files Browse the repository at this point in the history
  • Loading branch information
romot-co committed Jan 27, 2025
1 parent 2ea81f9 commit f5acf33
Show file tree
Hide file tree
Showing 9 changed files with 629 additions and 473 deletions.
52 changes: 50 additions & 2 deletions src/components/Sing/SequencerRuler/Container.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,47 @@
@setTempo="setTempo"
@setTimeSignature="setTimeSignature"
@deselectAllNotes="deselectAllNotes"
/>
>
<GridLaneContainer
:tpqn
:sequencerZoomX
:numMeasures
:timeSignatures
:offset
/>
<ValueMarkerLaneContainer
:tpqn
:sequencerZoomX
:tempos
:timeSignatures
:offset
:playheadTicks
:uiLocked
@setTempo="setTempo"
@removeTempo="removeTempo"
@setTimeSignature="setTimeSignature"
@removeTimeSignature="removeTimeSignature"
/>
<LoopLaneContainer :offset :width />
</Presentation>
</template>

<script setup lang="ts">
import { computed } from "vue";
import Presentation from "./Presentation.vue";
import GridLaneContainer from "./GridLane/Container.vue";
import ValueMarkerLaneContainer from "./ValueMarkerLane/Container.vue";
import LoopLaneContainer from "./LoopLane/Container.vue";
import { useStore } from "@/store";
import { Tempo, TimeSignature } from "@/store/type";
import { tickToBaseX } from "@/sing/viewHelper";
import { getTimeSignaturePositions, getMeasureDuration } from "@/sing/domain";
defineOptions({
name: "SequencerRuler",
});
withDefaults(
const props = withDefaults(
defineProps<{
offset: number;
numMeasures: number;
Expand Down Expand Up @@ -63,19 +90,40 @@ const setTempo = (tempo: Tempo) => {
tempo,
});
};
const setTimeSignature = (timeSignature: TimeSignature) => {
void store.actions.COMMAND_SET_TIME_SIGNATURE({
timeSignature,
});
};
const removeTempo = (position: number) => {
void store.actions.COMMAND_REMOVE_TEMPO({
position,
});
};
const removeTimeSignature = (measureNumber: number) => {
void store.actions.COMMAND_REMOVE_TIME_SIGNATURE({
measureNumber,
});
};
const width = computed(() => {
const lastTs = timeSignatures.value[timeSignatures.value.length - 1];
const tsPositions = getTimeSignaturePositions(
timeSignatures.value,
tpqn.value,
);
const lastTsPosition = tsPositions[tsPositions.length - 1];
const measureDuration = getMeasureDuration(
lastTs.beats,
lastTs.beatType,
tpqn.value,
);
const endTicks =
lastTsPosition +
measureDuration * (props.numMeasures - lastTs.measureNumber + 1);
return tickToBaseX(endTicks, tpqn.value) * sequencerZoomX.value;
});
</script>
20 changes: 20 additions & 0 deletions src/components/Sing/SequencerRuler/GridLane/Container.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<Presentation v-bind="$props" />
</template>

<script setup lang="ts">
import Presentation from "./Presentation.vue";
import { TimeSignature } from "@/store/type";
defineOptions({
name: "GridLaneContainer",
});
defineProps<{
tpqn: number;
sequencerZoomX: number;
numMeasures: number;
timeSignatures: TimeSignature[];
offset: number;
}>();
</script>
158 changes: 158 additions & 0 deletions src/components/Sing/SequencerRuler/GridLane/Presentation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width
:height
shape-rendering="crispEdges"
>
<defs>
<pattern
v-for="(gridPattern, patternIndex) in gridPatterns"
:id="`grid-lane-measure-${patternIndex}`"
:key="`pattern-${patternIndex}`"
patternUnits="userSpaceOnUse"
:x="-offset + gridPattern.x"
:width="gridPattern.patternWidth"
:height
>
<!-- 拍線(小節の最初を除く) -->
<line
v-for="n in gridPattern.beatsPerMeasure"
:key="n"
:x1="gridPattern.beatWidth * n"
:x2="gridPattern.beatWidth * n"
y1="28"
:y2="height"
class="grid-lane-beat-line"
/>
</pattern>
</defs>
<rect
v-for="(gridPattern, index) in gridPatterns"
:key="`grid-${index}`"
:x="0.5 + gridPattern.x - offset"
y="0"
:height
:width="gridPattern.width"
:fill="`url(#grid-lane-measure-${index})`"
/>
<!-- 小節線と小節番号 -->
<template v-for="measureInfo in measureInfos" :key="measureInfo.number">
<line
:x1="measureInfo.x - offset"
:x2="measureInfo.x - offset"
y1="0"
:y2="height"
class="grid-lane-measure-line"
:class="{ 'first-measure-line': measureInfo.number === 1 }"
/>
<text
:x="measureInfo.x - offset + 4"
y="16"
class="grid-lane-measure-number"
>
{{ measureInfo.number }}
</text>
</template>
</svg>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { TimeSignature } from "@/store/type";
import { useSequencerGrid } from "@/composables/useSequencerGridPattern";
import { getMeasureDuration, getTimeSignaturePositions } from "@/sing/domain";
import { tickToBaseX } from "@/sing/viewHelper";
defineOptions({
name: "GridLanePresentation",
});
const props = defineProps<{
tpqn: number;
sequencerZoomX: number;
numMeasures: number;
timeSignatures: TimeSignature[];
offset: number;
}>();
const height = ref(40);
const tsPositions = computed(() => {
return getTimeSignaturePositions(props.timeSignatures, props.tpqn);
});
const endTicks = computed(() => {
const lastTs = props.timeSignatures[props.timeSignatures.length - 1];
const lastTsPosition = tsPositions.value[tsPositions.value.length - 1];
return (
lastTsPosition +
getMeasureDuration(lastTs.beats, lastTs.beatType, props.tpqn) *
(props.numMeasures - lastTs.measureNumber + 1)
);
});
const width = computed(() => {
return tickToBaseX(endTicks.value, props.tpqn) * props.sequencerZoomX;
});
const gridPatterns = useSequencerGrid({
timeSignatures: computed(() => props.timeSignatures),
tpqn: computed(() => props.tpqn),
sequencerZoomX: computed(() => props.sequencerZoomX),
numMeasures: computed(() => props.numMeasures),
});
const measureInfos = computed(() => {
return props.timeSignatures.flatMap((timeSignature, i) => {
const measureDuration = getMeasureDuration(
timeSignature.beats,
timeSignature.beatType,
props.tpqn,
);
const nextTsPosition =
i !== props.timeSignatures.length - 1
? tsPositions.value[i + 1]
: endTicks.value;
const start = tsPositions.value[i];
const end = nextTsPosition;
const numMeasures = Math.floor((end - start) / measureDuration);
return Array.from({ length: numMeasures }, (_, index) => {
const measureNumber = timeSignature.measureNumber + index;
const measurePosition = start + index * measureDuration;
const baseX = tickToBaseX(measurePosition, props.tpqn);
return {
number: measureNumber,
x: Math.round(baseX * props.sequencerZoomX),
};
});
});
});
</script>

<style scoped lang="scss">
@use "@/styles/v2/variables" as vars;
.grid-lane-beat-line {
backface-visibility: hidden;
stroke: var(--scheme-color-sing-ruler-beat-line);
stroke-width: 1px;
}
.grid-lane-measure-line {
backface-visibility: hidden;
stroke: var(--scheme-color-sing-ruler-measure-line);
stroke-width: 1px;
&.first-measure-line {
stroke: var(--scheme-color-sing-ruler-surface);
}
}
.grid-lane-measure-number {
font-size: 12px;
font-weight: bold;
fill: var(--scheme-color-on-surface-variant);
user-select: none;
}
</style>
5 changes: 5 additions & 0 deletions src/components/Sing/SequencerRuler/LoopLane/Presentation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'is-empty': isEmpty,
[cursorClass]: true,
}"
:style="{ width: `${width}px` }"
@click.stop
@contextmenu.prevent="handleContextMenu"
>
Expand Down Expand Up @@ -95,6 +96,10 @@ import ContextMenu, {
ContextMenuItemData,
} from "@/components/Menu/ContextMenu/Presentation.vue";
defineOptions({
name: "LoopLanePresentation",
});
defineProps<{
width: number;
offset: number;
Expand Down
90 changes: 90 additions & 0 deletions src/components/Sing/SequencerRuler/LoopLane/index.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import { fn } from "@storybook/test";

import Presentation from "./Presentation.vue";

const meta: Meta<typeof Presentation> = {
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: `<div style="height: 40px; position: relative;"><Presentation v-bind="args" /></div>`,
}),
};

export default meta;
type Story = StoryObj<typeof Presentation>;

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,
},
};
Loading

0 comments on commit f5acf33

Please sign in to comment.