shift virtual keyboard
This commit is contained in:
parent
3b0e5b7ada
commit
f6a91abccd
37
src/main.js
37
src/main.js
|
|
@ -9900,6 +9900,43 @@ function piano() {
|
||||||
// Prevent text selection
|
// Prevent text selection
|
||||||
piano_cvs.addEventListener("selectstart", (e) => e.preventDefault());
|
piano_cvs.addEventListener("selectstart", (e) => e.preventDefault());
|
||||||
|
|
||||||
|
// Add header controls for octave and velocity
|
||||||
|
piano_cvs.headerControls = function() {
|
||||||
|
const controls = [];
|
||||||
|
|
||||||
|
// Octave control
|
||||||
|
const octaveLabel = document.createElement("span");
|
||||||
|
octaveLabel.style.marginLeft = "auto";
|
||||||
|
octaveLabel.style.marginRight = "10px";
|
||||||
|
octaveLabel.style.fontSize = "12px";
|
||||||
|
octaveLabel.textContent = `Octave: ${piano_cvs.virtualPiano.octaveOffset >= 0 ? '+' : ''}${piano_cvs.virtualPiano.octaveOffset} (Z/X)`;
|
||||||
|
|
||||||
|
// Velocity control
|
||||||
|
const velocityLabel = document.createElement("span");
|
||||||
|
velocityLabel.style.marginRight = "10px";
|
||||||
|
velocityLabel.style.fontSize = "12px";
|
||||||
|
velocityLabel.textContent = `Velocity: ${piano_cvs.virtualPiano.velocity} (C/V)`;
|
||||||
|
|
||||||
|
// Update function to refresh labels
|
||||||
|
const updateLabels = () => {
|
||||||
|
octaveLabel.textContent = `Octave: ${piano_cvs.virtualPiano.octaveOffset >= 0 ? '+' : ''}${piano_cvs.virtualPiano.octaveOffset} (Z/X)`;
|
||||||
|
velocityLabel.textContent = `Velocity: ${piano_cvs.virtualPiano.velocity} (C/V)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for keyboard events to update labels
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (['z', 'x', 'c', 'v'].includes(e.key.toLowerCase())) {
|
||||||
|
// Delay slightly to let the piano widget update first
|
||||||
|
setTimeout(updateLabels, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controls.push(octaveLabel);
|
||||||
|
controls.push(velocityLabel);
|
||||||
|
|
||||||
|
return controls;
|
||||||
|
};
|
||||||
|
|
||||||
return piano_cvs;
|
return piano_cvs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
132
src/widgets.js
132
src/widgets.js
|
|
@ -4218,15 +4218,23 @@ class VirtualPiano extends Widget {
|
||||||
this.visibleStartNote = 48; // C3 - will be adjusted based on pane width
|
this.visibleStartNote = 48; // C3 - will be adjusted based on pane width
|
||||||
this.visibleEndNote = 72; // C5 - will be adjusted based on pane width
|
this.visibleEndNote = 72; // C5 - will be adjusted based on pane width
|
||||||
|
|
||||||
|
// Keyboard control state
|
||||||
|
this.octaveOffset = 0; // Octave transpose (-2 to +2)
|
||||||
|
this.velocity = 100; // Default velocity (0-127)
|
||||||
|
this.sustainActive = false; // Sustain pedal (Tab key)
|
||||||
|
this.activeKeyPresses = new Map(); // Map of keyboard key -> MIDI note that's currently playing
|
||||||
|
this.sustainedNotes = new Set(); // Notes being held by sustain
|
||||||
|
|
||||||
// MIDI note mapping (white keys in an octave: C, D, E, F, G, A, B)
|
// MIDI note mapping (white keys in an octave: C, D, E, F, G, A, B)
|
||||||
this.whiteKeysInOctave = [0, 2, 4, 5, 7, 9, 11]; // Semitones from C
|
this.whiteKeysInOctave = [0, 2, 4, 5, 7, 9, 11]; // Semitones from C
|
||||||
// Black keys indexed by white key position (after which white key the black key appears)
|
// Black keys indexed by white key position (after which white key the black key appears)
|
||||||
// Position 0 (after C), 1 (after D), null (no black after E), 3 (after F), 4 (after G), 5 (after A), null (no black after B)
|
// Position 0 (after C), 1 (after D), null (no black after E), 3 (after F), 4 (after G), 5 (after A), null (no black after B)
|
||||||
this.blackKeysInOctave = [1, 3, null, 6, 8, 10, null]; // Actual semitone values
|
this.blackKeysInOctave = [1, 3, null, 6, 8, 10, null]; // Actual semitone values
|
||||||
|
|
||||||
// Keyboard bindings matching piano layout
|
// Keyboard bindings matching piano layout (QWERTY)
|
||||||
// Black keys: W E (one group) T Y U (other group)
|
// TODO: Auto-detect keyboard layout and generate mapping dynamically
|
||||||
// White keys: A S D F G H J K
|
// Black keys: W E (one group) T Y U (other group) O P (next group)
|
||||||
|
// White keys: A S D F G H J K L ; '
|
||||||
this.keyboardMap = {
|
this.keyboardMap = {
|
||||||
'a': 60, // C4
|
'a': 60, // C4
|
||||||
'w': 61, // C#4
|
'w': 61, // C#4
|
||||||
|
|
@ -4241,6 +4249,11 @@ class VirtualPiano extends Widget {
|
||||||
'u': 70, // A#4
|
'u': 70, // A#4
|
||||||
'j': 71, // B4
|
'j': 71, // B4
|
||||||
'k': 72, // C5
|
'k': 72, // C5
|
||||||
|
'o': 73, // C#5
|
||||||
|
'l': 74, // D5
|
||||||
|
'p': 75, // D#5
|
||||||
|
';': 76, // E5
|
||||||
|
"'": 77, // F5
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reverse mapping for displaying keyboard keys on piano keys
|
// Reverse mapping for displaying keyboard keys on piano keys
|
||||||
|
|
@ -4259,17 +4272,96 @@ class VirtualPiano extends Widget {
|
||||||
setupKeyboardListeners() {
|
setupKeyboardListeners() {
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (e.repeat) return; // Ignore key repeats
|
if (e.repeat) return; // Ignore key repeats
|
||||||
const midiNote = this.keyboardMap[e.key.toLowerCase()];
|
|
||||||
if (midiNote !== undefined) {
|
const key = e.key.toLowerCase();
|
||||||
this.noteOn(midiNote, 100); // Default velocity 100
|
|
||||||
|
// Handle sustain (Tab key)
|
||||||
|
if (key === 'tab') {
|
||||||
|
this.sustainActive = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle control keys (Z, X for octave, C, V for velocity)
|
||||||
|
if (key === 'z') {
|
||||||
|
this.octaveOffset = Math.max(-2, this.octaveOffset - 1);
|
||||||
|
// Trigger a redraw to update the visible piano range
|
||||||
|
if (window.context && window.context.pianoRedraw) {
|
||||||
|
window.context.pianoRedraw();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'x') {
|
||||||
|
this.octaveOffset = Math.min(2, this.octaveOffset + 1);
|
||||||
|
// Trigger a redraw to update the visible piano range
|
||||||
|
if (window.context && window.context.pianoRedraw) {
|
||||||
|
window.context.pianoRedraw();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'c') {
|
||||||
|
this.velocity = Math.max(1, this.velocity - 10);
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'v') {
|
||||||
|
this.velocity = Math.min(127, this.velocity + 10);
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle piano keys
|
||||||
|
const baseNote = this.keyboardMap[key];
|
||||||
|
if (baseNote !== undefined) {
|
||||||
|
// Note: octave offset is applied by shifting the visible piano range
|
||||||
|
// so we play the base note directly
|
||||||
|
const note = baseNote + (this.octaveOffset * 12);
|
||||||
|
// Clamp to valid MIDI range (0-127)
|
||||||
|
if (note >= 0 && note <= 127) {
|
||||||
|
// Track which key is playing which note
|
||||||
|
this.activeKeyPresses.set(key, note);
|
||||||
|
this.noteOn(note, this.velocity);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keyup', (e) => {
|
window.addEventListener('keyup', (e) => {
|
||||||
const midiNote = this.keyboardMap[e.key.toLowerCase()];
|
const key = e.key.toLowerCase();
|
||||||
if (midiNote !== undefined) {
|
|
||||||
this.noteOff(midiNote);
|
// Handle sustain release
|
||||||
|
if (key === 'tab') {
|
||||||
|
this.sustainActive = false;
|
||||||
|
// Release only the sustained notes that aren't currently being held by a key
|
||||||
|
const currentlyPlayingNotes = new Set(this.activeKeyPresses.values());
|
||||||
|
for (const note of this.sustainedNotes) {
|
||||||
|
if (!currentlyPlayingNotes.has(note)) {
|
||||||
|
this.noteOff(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sustainedNotes.clear();
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore control keys on keyup
|
||||||
|
if (['z', 'x', 'c', 'v'].includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up which note this key was playing
|
||||||
|
const transposedNote = this.activeKeyPresses.get(key);
|
||||||
|
if (transposedNote !== undefined) {
|
||||||
|
this.activeKeyPresses.delete(key);
|
||||||
|
|
||||||
|
// If sustain is active, add to sustained notes instead of releasing
|
||||||
|
if (this.sustainActive) {
|
||||||
|
this.sustainedNotes.add(transposedNote);
|
||||||
|
} else {
|
||||||
|
this.noteOff(transposedNote);
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -4349,15 +4441,15 @@ class VirtualPiano extends Widget {
|
||||||
// Calculate how many white keys can fit in the pane (ceiling to fill space)
|
// Calculate how many white keys can fit in the pane (ceiling to fill space)
|
||||||
const whiteKeysFit = Math.ceil(width / whiteKeyWidth);
|
const whiteKeysFit = Math.ceil(width / whiteKeyWidth);
|
||||||
|
|
||||||
// Keyboard-mapped range is C4 (60) to C5 (72)
|
// Keyboard-mapped range is C4 (60) to C5 (72), shifted by octave offset
|
||||||
// This contains 8 white keys: C, D, E, F, G, A, B, C
|
// This contains 8 white keys: C, D, E, F, G, A, B, C
|
||||||
const keyboardCenter = 60; // C4
|
const keyboardCenter = 60 + (this.octaveOffset * 12); // C4 + octave shift
|
||||||
const keyboardWhiteKeys = 8;
|
const keyboardWhiteKeys = 8;
|
||||||
|
|
||||||
if (whiteKeysFit <= keyboardWhiteKeys) {
|
if (whiteKeysFit <= keyboardWhiteKeys) {
|
||||||
// Not enough space to show all keyboard keys, just center what we have
|
// Not enough space to show all keyboard keys, just center what we have
|
||||||
this.visibleStartNote = 60; // C4
|
this.visibleStartNote = keyboardCenter;
|
||||||
this.visibleEndNote = 72; // C5
|
this.visibleEndNote = keyboardCenter + 12; // One octave up
|
||||||
const totalWhiteKeyWidth = keyboardWhiteKeys * whiteKeyWidth;
|
const totalWhiteKeyWidth = keyboardWhiteKeys * whiteKeyWidth;
|
||||||
const offsetX = (width - totalWhiteKeyWidth) / 2;
|
const offsetX = (width - totalWhiteKeyWidth) / 2;
|
||||||
return { offsetX, whiteKeyWidth };
|
return { offsetX, whiteKeyWidth };
|
||||||
|
|
@ -4368,8 +4460,8 @@ class VirtualPiano extends Widget {
|
||||||
const leftExtra = Math.floor(extraWhiteKeys / 2);
|
const leftExtra = Math.floor(extraWhiteKeys / 2);
|
||||||
const rightExtra = extraWhiteKeys - leftExtra;
|
const rightExtra = extraWhiteKeys - leftExtra;
|
||||||
|
|
||||||
// Start from C4 and go back leftExtra white keys
|
// Start from shifted keyboard center and go back leftExtra white keys
|
||||||
let startNote = 60; // C4
|
let startNote = keyboardCenter;
|
||||||
let leftCount = 0;
|
let leftCount = 0;
|
||||||
while (leftCount < leftExtra && startNote > 0) {
|
while (leftCount < leftExtra && startNote > 0) {
|
||||||
startNote--;
|
startNote--;
|
||||||
|
|
@ -4507,7 +4599,7 @@ class VirtualPiano extends Widget {
|
||||||
const { offsetX, whiteKeyWidth } = this.calculateVisibleRange(width, height);
|
const { offsetX, whiteKeyWidth } = this.calculateVisibleRange(width, height);
|
||||||
const key = this.findKeyAtPosition(x, y, height, whiteKeyWidth, offsetX);
|
const key = this.findKeyAtPosition(x, y, height, whiteKeyWidth, offsetX);
|
||||||
if (key !== null) {
|
if (key !== null) {
|
||||||
this.noteOn(key, 100);
|
this.noteOn(key, this.velocity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4574,7 +4666,9 @@ class VirtualPiano extends Widget {
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Keyboard mapping label (if exists)
|
// Keyboard mapping label (if exists)
|
||||||
const keyLabel = this.noteToKeyMap[note];
|
// Subtract octave offset to get the base note for label lookup
|
||||||
|
const baseNote = note - (this.octaveOffset * 12);
|
||||||
|
const keyLabel = this.noteToKeyMap[baseNote];
|
||||||
if (keyLabel) {
|
if (keyLabel) {
|
||||||
ctx.fillStyle = isPressed ? '#000000' : '#333333';
|
ctx.fillStyle = isPressed ? '#000000' : '#333333';
|
||||||
ctx.font = 'bold 16px sans-serif';
|
ctx.font = 'bold 16px sans-serif';
|
||||||
|
|
@ -4637,7 +4731,9 @@ class VirtualPiano extends Widget {
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Keyboard mapping label (if exists)
|
// Keyboard mapping label (if exists)
|
||||||
const keyLabel = this.noteToKeyMap[note];
|
// Subtract octave offset to get the base note for label lookup
|
||||||
|
const baseNote = note - (this.octaveOffset * 12);
|
||||||
|
const keyLabel = this.noteToKeyMap[baseNote];
|
||||||
if (keyLabel) {
|
if (keyLabel) {
|
||||||
ctx.fillStyle = isPressed ? '#ffffff' : 'rgba(255, 255, 255, 0.7)';
|
ctx.fillStyle = isPressed ? '#ffffff' : 'rgba(255, 255, 255, 0.7)';
|
||||||
ctx.font = 'bold 14px sans-serif';
|
ctx.font = 'bold 14px sans-serif';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue