Skip to content

Commit d0f0a5f

Browse files
committed
add piano vis helper
1 parent 358bfad commit d0f0a5f

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<template>
2+
<div class="piano-range-container">
3+
<div class="piano-wrapper" ref="pianoWrapper">
4+
<svg :width="svgWidth" :height="keyHeight + 10" class="piano-svg">
5+
<!-- Background to show full piano range for context -->
6+
<g class="background-keys" opacity="0.15">
7+
<template v-for="i in 88" :key="`bg-${i}`">
8+
<rect
9+
v-if="isWhiteKey(i + 20)"
10+
:x="getKeyPosition(i + 20)"
11+
y="0"
12+
:width="whiteKeyWidth"
13+
:height="keyHeight"
14+
class="white-key"
15+
stroke="#999"
16+
stroke-width="1"
17+
/>
18+
</template>
19+
<template v-for="i in 88" :key="`bg-black-${i}`">
20+
<rect
21+
v-if="!isWhiteKey(i + 20)"
22+
:x="getKeyPosition(i + 20)"
23+
y="0"
24+
:width="blackKeyWidth"
25+
:height="blackKeyHeight"
26+
class="black-key"
27+
/>
28+
</template>
29+
</g>
30+
31+
<!-- Selected range highlighted -->
32+
<g class="selected-range">
33+
<!-- White keys in selected range -->
34+
<template v-for="note in 128" :key="`white-${note}`">
35+
<rect
36+
v-if="isWhiteKey(note) && note >= lowestNote && note <= highestNote"
37+
:x="getKeyPosition(note)"
38+
y="0"
39+
:width="whiteKeyWidth"
40+
:height="keyHeight"
41+
class="white-key active"
42+
stroke="#666"
43+
stroke-width="1"
44+
/>
45+
</template>
46+
47+
<!-- Black keys in selected range -->
48+
<template v-for="note in 128" :key="`black-${note}`">
49+
<rect
50+
v-if="!isWhiteKey(note) && note >= lowestNote && note <= highestNote"
51+
:x="getKeyPosition(note)"
52+
y="0"
53+
:width="blackKeyWidth"
54+
:height="blackKeyHeight"
55+
class="black-key active"
56+
/>
57+
</template>
58+
</g>
59+
60+
<!-- Range markers -->
61+
<g class="range-markers">
62+
<rect
63+
:x="getKeyPosition(lowestNote) - 2"
64+
y="0"
65+
:width="4"
66+
:height="keyHeight + 5"
67+
class="range-marker start"
68+
/>
69+
<rect
70+
:x="getKeyPosition(highestNote) + whiteKeyWidth - 2"
71+
y="0"
72+
:width="4"
73+
:height="keyHeight + 5"
74+
class="range-marker end"
75+
/>
76+
</g>
77+
78+
<!-- Note labels at extremes -->
79+
<text :x="getKeyPosition(lowestNote) + 3" :y="keyHeight + 12" class="note-label">
80+
{{ getNoteLabel(lowestNote) }}
81+
</text>
82+
<text
83+
:x="getKeyPosition(highestNote) + whiteKeyWidth - 3"
84+
:y="keyHeight + 12"
85+
text-anchor="end"
86+
class="note-label"
87+
>
88+
{{ getNoteLabel(highestNote) }}
89+
</text>
90+
</svg>
91+
</div>
92+
</div>
93+
</template>
94+
95+
<script lang="ts">
96+
import { defineComponent, ref, computed, onMounted } from 'vue';
97+
98+
export default defineComponent({
99+
name: 'PianoRangeVisualizer',
100+
101+
props: {
102+
lowestNote: {
103+
type: Number,
104+
required: true,
105+
},
106+
highestNote: {
107+
type: Number,
108+
required: true,
109+
},
110+
// Optional max width to fit in parent container
111+
maxWidth: {
112+
type: Number,
113+
default: 1000,
114+
},
115+
},
116+
117+
setup(props) {
118+
const pianoWrapper = ref<HTMLElement | null>(null);
119+
const containerWidth = ref(800);
120+
121+
// Piano dimensions
122+
const keyHeight = 100;
123+
const blackKeyHeight = 60;
124+
125+
// Calculate piano dimensions based on range
126+
// const keysInFullRange = 88; // Standard piano: A0 (21) to C8 (108)
127+
const totalWhiteKeys = computed(() => {
128+
let count = 0;
129+
for (let note = 21; note <= 108; note++) {
130+
if (isWhiteKey(note)) count++;
131+
}
132+
return count;
133+
});
134+
135+
const whiteKeyWidth = computed(() => {
136+
return Math.min(22, containerWidth.value / Math.max(25, totalWhiteKeys.value));
137+
});
138+
139+
const blackKeyWidth = computed(() => whiteKeyWidth.value * 0.6);
140+
141+
const svgWidth = computed(() => {
142+
return Math.min(props.maxWidth, whiteKeyWidth.value * totalWhiteKeys.value + 10);
143+
});
144+
145+
// Determine if a note is a white key
146+
const isWhiteKey = (noteNumber: number) => {
147+
const note = noteNumber % 12;
148+
return [0, 2, 4, 5, 7, 9, 11].includes(note);
149+
};
150+
151+
// Position calculation for keys
152+
const getKeyPosition = (noteNumber: number) => {
153+
let whiteKeyCount = 0;
154+
for (let i = 21; i < noteNumber; i++) {
155+
if (isWhiteKey(i)) whiteKeyCount++;
156+
}
157+
158+
if (isWhiteKey(noteNumber)) {
159+
return whiteKeyCount * whiteKeyWidth.value;
160+
} else {
161+
// For black keys, position them between white keys
162+
return whiteKeyCount * whiteKeyWidth.value - blackKeyWidth.value / 2;
163+
}
164+
};
165+
166+
const getNoteLabel = (noteNumber: number) => {
167+
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
168+
const note = noteNumber % 12;
169+
const octave = Math.floor(noteNumber / 12) - 1;
170+
return `${noteNames[note]}${octave}`;
171+
};
172+
173+
onMounted(() => {
174+
if (pianoWrapper.value) {
175+
containerWidth.value = pianoWrapper.value.clientWidth;
176+
177+
// Add resize listener to adjust for window size changes
178+
const resizeObserver = new ResizeObserver((entries) => {
179+
for (const entry of entries) {
180+
containerWidth.value = entry.contentRect.width;
181+
}
182+
});
183+
184+
resizeObserver.observe(pianoWrapper.value);
185+
}
186+
});
187+
188+
return {
189+
pianoWrapper,
190+
keyHeight,
191+
blackKeyHeight,
192+
whiteKeyWidth,
193+
blackKeyWidth,
194+
svgWidth,
195+
isWhiteKey,
196+
getKeyPosition,
197+
getNoteLabel,
198+
};
199+
},
200+
});
201+
</script>
202+
203+
<style scoped>
204+
.piano-range-container {
205+
margin: 20px 0;
206+
}
207+
208+
.piano-wrapper {
209+
width: 100%;
210+
overflow-x: hidden;
211+
margin: 10px 0;
212+
}
213+
214+
.piano-svg {
215+
display: block;
216+
width: 100%;
217+
height: auto;
218+
}
219+
220+
.white-key {
221+
fill: white;
222+
}
223+
224+
.black-key {
225+
fill: #333;
226+
}
227+
228+
.white-key.active {
229+
fill: #e6f2ff;
230+
}
231+
232+
.black-key.active {
233+
fill: #1a1a1a;
234+
}
235+
236+
.range-marker {
237+
fill: #ff5252;
238+
}
239+
240+
.note-label {
241+
font-size: 12px;
242+
fill: #333;
243+
font-weight: bold;
244+
}
245+
</style>

0 commit comments

Comments
 (0)