Skip to content

Commit c4cbe73

Browse files
committed
feat(App.vue): 引入PanelGuitar组件并注释掉其使用
- 在App.vue中引入PanelGuitar组件以支持吉他功能。 - 暂时注释掉PanelGuitar的使用,待后续实现相关功能。
1 parent ce9b055 commit c4cbe73

File tree

3 files changed

+271
-0
lines changed

3 files changed

+271
-0
lines changed

examples/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
@export-file="handleExportFile"
88
/>
99
<PanelPiano />
10+
<!-- <PanelGuitar /> -->
1011
<div
1112
class="max-w-[1200px] mt-5 mx-auto w-full h-auto max-h-[800px] flex min-h-[70vh] gap-5 flex-row max-[1200px]:flex-col max-[1200px]:w-auto max-[1200px]:max-h-max max-[1200px]:overflow-x-auto"
1213
>
@@ -56,6 +57,7 @@ import Header from './components/Header.vue';
5657
import PanelPiano from './components/PanelPiano.vue';
5758
import PanelSnOptions from './components/PanelSnOptions.vue';
5859
import NoteContextMenu from './components/NoteContextMenu.vue';
60+
import PanelGuitar from './components/PanelGuitar.vue';
5961
import { usePianoStore } from './stores';
6062
import { usePlayer } from './use/usePlayer';
6163
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<template>
2+
<div
3+
class="max-w-[1200px] w-full mt-5 mx-auto p-[2.5px] rounded-xl bg-gradient-to-br from-[#ff6b3d] to-[#7b5aff] shadow-md flex flex-row gap-4 overflow-hidden box-border hover:shadow-lg hover:-translate-y-0.5 transition duration-300"
4+
style="background-color: #f0f0f0"
5+
>
6+
<div
7+
class="relative h-[120px] w-full select-none bg-white bg-opacity-95 rounded-[11px] overflow-hidden flex flex-col"
8+
>
9+
<div class="flex-1 grid grid-cols-layout h-full">
10+
<div
11+
class="col-start-1 col-end-2 grid grid-rows-6 h-full border-r border-[#bbb] bg-[#eee]"
12+
>
13+
<div
14+
v-for="stringIndex in 6"
15+
:key="`open-${stringIndex}`"
16+
class="flex items-center justify-center text-xs font-bold text-[#333] h-full cursor-pointer"
17+
@click="handleGuitarPositionClick(stringIndex, 0)"
18+
>
19+
|
20+
<div
21+
v-if="isPositionHighlighted(stringIndex, 0)"
22+
class="absolute w-4 h-4 rounded-full bg-yellow-400 bg-opacity-75 pointer-events-none z-40"
23+
></div>
24+
</div>
25+
</div>
26+
27+
<div
28+
class="flex-1 col-start-2 col-end-end h-full relative bg-[#a0866e]"
29+
>
30+
<div class="grid grid-rows-6 h-full" :style="gridTemplateColumns">
31+
<template v-for="fret in numFrets" :key="`fret-dot-row-${fret}`">
32+
<div
33+
v-if="[3, 5, 7, 9, 15].includes(fret)"
34+
class="w-2 h-2 bg-gray-200 rounded-full absolute z-10"
35+
:style="getFretDotStyle(fret, 'single')"
36+
></div>
37+
<template v-if="fret === 12">
38+
<div
39+
class="w-2 h-2 bg-gray-200 rounded-full absolute z-10"
40+
:style="getFretDotStyle(fret, 'double-top')"
41+
></div>
42+
<div
43+
class="w-2 h-2 bg-gray-200 rounded-full absolute z-10"
44+
:style="getFretDotStyle(fret, 'double-bottom')"
45+
></div>
46+
</template>
47+
</template>
48+
49+
<div
50+
v-for="fret in numFrets + 1"
51+
:key="`fret-line-${fret}`"
52+
class="h-full bg-gray-300 col-start-auto col-end-auto row-start-1 row-end-7 z-15"
53+
:style="getFretStyle(fret - 1)"
54+
></div>
55+
56+
<template
57+
v-for="stringIndex in 6"
58+
:key="`string-row-${stringIndex}`"
59+
>
60+
<div
61+
v-for="fret in numFrets"
62+
:key="`string-${stringIndex}-fret-${fret}`"
63+
:style="{ 'grid-column': fret, 'grid-row': stringIndex }"
64+
class="relative flex items-center justify-center cursor-pointer z-30"
65+
@click="handleGuitarPositionClick(stringIndex, fret)"
66+
>
67+
<div
68+
class="bg-[#c0c0c0] shadow-sm rounded-full"
69+
:style="getStringSegmentStyle(stringIndex)"
70+
></div>
71+
<div
72+
v-if="isPositionHighlighted(stringIndex, fret)"
73+
class="absolute w-4 h-4 rounded-full bg-yellow-400 bg-opacity-75 pointer-events-none z-40"
74+
></div>
75+
</div>
76+
</template>
77+
</div>
78+
</div>
79+
</div>
80+
</div>
81+
</div>
82+
</template>
83+
84+
<script setup lang="ts">
85+
import { computed } from 'vue';
86+
import { useTone } from '../use/useTone';
87+
import { useGuitarStore } from '../stores/guitar';
88+
89+
const guitarStore = useGuitarStore();
90+
91+
const guitarTuning: Record<number, string> = {
92+
6: 'E2', // Low E 低音 E
93+
5: 'A2', // A
94+
4: 'D3', // D
95+
3: 'G3', // G
96+
2: 'B3', // B
97+
1: 'E4', // High E 高音 E
98+
};
99+
100+
const numFrets = 17;
101+
102+
const fretPositions = computed(() => {
103+
const positions = [0]; // 从位置 0 开始 (弦枕)
104+
const scaleLength = 650; // 近似有效弦长 (毫米)
105+
for (let i = 1; i <= numFrets; i++) {
106+
// 使用17.817规则计算品丝位置 (近似)
107+
const fretDistance = scaleLength - positions[i - 1];
108+
positions.push(positions[i - 1] + fretDistance / 17.817); // 使用 17.817 以获得稍高的精度
109+
}
110+
// 将位置归一化为相对于品格区域总长度的百分比 (从弦枕到最后一个品的距离)
111+
const totalFrettedLength = positions[numFrets] - positions[0]; // 从弦枕到最后一个品的长度
112+
return positions.map((pos) => (pos / totalFrettedLength) * 100); // Normalize relative to the entire fretted area length
113+
});
114+
115+
const gridTemplateColumns = computed(() => {
116+
const fretSpaceWidths = [];
117+
const totalFrettedLength =
118+
fretPositions.value[numFrets] - fretPositions.value[0];
119+
for (let i = 0; i < numFrets; i++) {
120+
const spaceWidth = fretPositions.value[i + 1] - fretPositions.value[i];
121+
const spaceWidthPercentage = (spaceWidth / totalFrettedLength) * 100;
122+
fretSpaceWidths.push(`${spaceWidthPercentage}%`);
123+
}
124+
return `grid-template-columns: ${fretSpaceWidths.join(' ')};`;
125+
});
126+
127+
/**
128+
* 获取品丝的样式 (垂直线) - Positioned within the grid
129+
* @param {number} fretIndex - 品丝索引 (0-based, 0 是弦枕)
130+
* @returns {object}
131+
*/
132+
function getFretStyle(fretIndex: number) {
133+
const gridColumn = fretIndex;
134+
135+
if (fretIndex === 0) {
136+
// 弦枕的样式 (较粗的线)
137+
return {
138+
gridColumn: `${gridColumn} / span 1`,
139+
width: '5px', // 较粗的弦枕
140+
backgroundColor: '#555',
141+
};
142+
}
143+
return {
144+
gridColumn: `${gridColumn} / span 1`,
145+
width: '2px', // 标准品丝厚度
146+
backgroundColor: '#bbb',
147+
};
148+
}
149+
150+
/**
151+
* 获取品位点样式 - Positioned using absolute position within the container
152+
* @param {number} fret - 品位索引 (1-based)
153+
* @param {'single' | 'double-top' | 'double-bottom'} type - 品位点类型
154+
* @returns {object}
155+
*/
156+
function getFretDotStyle(
157+
fret: number,
158+
type: 'single' | 'double-top' | 'double-bottom',
159+
) {
160+
// 品位点应该居中在品丝 'fret - 1' 和 'fret' 之间的空间中。
161+
// 空间的左边缘在 fretPositions.value[fret - 1],右边缘在 fretPositions.value[fret]。
162+
163+
const spaceLeftPercentage = fretPositions.value[fret - 1]; // 空间左边缘百分比
164+
const spaceRightPercentage = fretPositions.value[fret]; // 空间右边缘百分比
165+
const spaceWidthPercentage = spaceRightPercentage - spaceLeftPercentage; // 空间宽度百分比
166+
167+
// 计算点在空间内的水平中心位置
168+
const dotLeftPercentage = spaceLeftPercentage + spaceWidthPercentage / 2;
169+
170+
let topPosition = 'calc(50% - 4px)'; // Default centered vertically
171+
if (type === 'double-top') {
172+
topPosition = 'calc(30% - 4px)'; // Top dot for double dots
173+
} else if (type === 'double-bottom') {
174+
topPosition = 'calc(70% - 4px)'; // Bottom dot for double dots
175+
}
176+
177+
return {
178+
top: topPosition,
179+
left: `${dotLeftPercentage}%`,
180+
};
181+
}
182+
183+
function getStringSegmentStyle(stringIndex: number) {
184+
return {
185+
position: 'absolute',
186+
top: '50%',
187+
transform: 'translateY(-50%)',
188+
left: '0',
189+
width: '100%',
190+
// 弦的粗细和颜色金属质感
191+
height: `${stringIndex / 3}px`,
192+
backgroundColor: stringIndex >= 4 ? '#d1b574' : '#e4e4e4',
193+
};
194+
}
195+
196+
function getStringFretNote(stringIndex: number, fret: number): string | null {
197+
const openNote = guitarTuning[stringIndex];
198+
if (!openNote) return null;
199+
const openMidi = noteNameToMidi(openNote);
200+
const playedMidi = openMidi + fret;
201+
return midiToNoteName(playedMidi);
202+
}
203+
204+
const { playNote, noteNameToMidi, midiToNoteName } = useTone();
205+
206+
async function handleGuitarPositionClick(stringIndex: number, fret: number) {
207+
const noteName = getStringFretNote(stringIndex, fret);
208+
if (noteName) {
209+
await playNote(noteName, '8n');
210+
guitarStore.setHighlightPositions([{ string: stringIndex, fret: fret }]);
211+
setTimeout(() => {
212+
guitarStore.clearHighlightPositions();
213+
}, 300);
214+
}
215+
}
216+
217+
const isPositionHighlighted = (string: number, fret: number) => {
218+
return guitarStore.highlightedPositions.some(
219+
(pos) => pos.string === string && pos.fret === fret,
220+
);
221+
};
222+
</script>
223+
224+
<style scoped>
225+
.grid-cols-layout {
226+
display: grid;
227+
grid-template-columns: 60px 1fr;
228+
}
229+
</style>

examples/src/stores/guitar.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { defineStore } from 'pinia';
2+
import { computed, ref } from 'vue';
3+
4+
/**
5+
* 吉他高亮位置类型
6+
* 表示一个需要在指板上高亮的点:{ string: number, fret: number }
7+
* string: 弦的索引 (从低音弦 E 开始,索引 6 到 高音弦 E,索引 1)
8+
* fret: 品的索引 (0 表示空弦,1 表示第一品,以此类推)
9+
*/
10+
export interface GuitarPosition {
11+
string: number;
12+
fret: number;
13+
}
14+
15+
export const useGuitarStore = defineStore('guitar', () => {
16+
const highlightedPositions = ref<GuitarPosition[]>([]);
17+
18+
/**
19+
* 设置需要高亮的吉他位置
20+
* @param {GuitarPosition[]} positions - 需要高亮的吉他位置数组
21+
* @returns {void}
22+
*/
23+
function setHighlightPositions(positions: GuitarPosition[]) {
24+
highlightedPositions.value = positions;
25+
}
26+
27+
/**
28+
* 清除所有高亮位置
29+
* @returns {void}
30+
*/
31+
function clearHighlightPositions() {
32+
highlightedPositions.value = [];
33+
}
34+
35+
return {
36+
highlightedPositions: computed(() => highlightedPositions.value),
37+
setHighlightPositions,
38+
clearHighlightPositions,
39+
};
40+
});

0 commit comments

Comments
 (0)