|
| 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> |
0 commit comments