Skip to content

Instantly share code, notes, and snippets.

@skybrian
Created March 26, 2023 03:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skybrian/eda5358637574759b119949281abc7bd to your computer and use it in GitHub Desktop.
Save skybrian/eda5358637574759b119949281abc7bd to your computer and use it in GitHub Desktop.
ChatGPT session to build an HTML Piano Keyboard

Model: GPT4

Could you write HTML to draw 12 rectangles horizontally, representing a piano keyboard? The cells should have a different CSS class for white notes and black notes.

To create a piano keyboard with 12 rectangles horizontally, you can use HTML and CSS. Here's a simple example:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Piano Keyboard</title>
<style>
  .keyboard {
    display: flex;
    align-items: flex-start;
    position: relative;
  }

  .white-note,
  .black-note {
    box-sizing: border-box;
    border: 1px solid #000;
  }

  .white-note {
    background-color: #fff;
    width: 50px;
    height: 150px;
  }

  .black-note {
    background-color: #000;
    width: 30px;
    height: 90px;
    margin-left: -15px;
    margin-right: -15px;
    z-index: 1;
  }
</style>
</head>
<body>
<div class="keyboard">
  <div class="white-note"></div>
  <div class="black-note"></div>
  <div class="white-note"></div>
  <div class="black-note"></div>
  <div class="white-note"></div>
  <div class="white-note"></div>
  <div class="black-note"></div>
  <div class="white-note"></div>
  <div class="black-note"></div>
  <div class="white-note"></div>
  <div class="black-note"></div>
  <div class="white-note"></div>
</div>
</body>
</html>

This code creates a simple piano keyboard with 12 rectangles (7 white notes and 5 black notes). The keyboard uses a flex container to align the notes horizontally, and the black notes are positioned relative to the white notes with a negative margin and a higher z-index.

That looks good, but I don't understand how it works. How is it that all the white notes are next to each other?

I apologize for any confusion. I'll explain how the code works, specifically focusing on how the white notes are positioned next to each other.

The key to understanding this lies in the .keyboard and .black-note CSS classes.

.keyboard {
  display: flex;
  align-items: flex-start;
  position: relative;
}

The .keyboard class sets the container's display to flex. This makes the container a flex container and its children (white and black notes) flex items. By default, flex items are displayed in a row, so they are placed horizontally next to each other.

.black-note {
  background-color: #000;
  width: 30px;
  height: 90px;
  margin-left: -15px;
  margin-right: -15px;
  z-index: 1;
}

The .black-note class has negative left and right margins (margin-left: -15px and margin-right: -15px). These negative margins cause the black notes to overlap the white notes on both sides. As a result, the white notes appear to be next to each other without any gaps.

The z-index: 1 property ensures that the black notes are positioned on top of the white notes, which creates the desired piano keyboard appearance.

In summary, the flex container places all the notes (white and black) next to each other horizontally, and the negative margins on the black notes cause them to overlap the white notes, making the white notes appear to be positioned directly next to each other.

Could you write a JavaScript function to make the HTML for the keyboard? It should take one argument, for the number of keys that the keyboard should have. It should return the DOM element for the div. Please write only the JavaScript function and I will modify the HTML.

Sure, here's a JavaScript function that creates a piano keyboard with a specified number of keys. It returns the DOM element for the div with the class "keyboard":

function createPianoKeyboard(numKeys) {
  const keyboard = document.createElement('div');
  keyboard.className = 'keyboard';

  let whiteNotes = 0;
  let blackNotes = 0;

  for (let i = 0; i < numKeys; i++) {
    if (whiteNotes % 7 === 2 || whiteNotes % 7 === 6) {
      // Place a black key
      const blackNote = document.createElement('div');
      blackNote.className = 'black-note';
      keyboard.appendChild(blackNote);
      blackNotes++;
    } else if (blackNotes + whiteNotes < numKeys) {
      // Place a white key
      const whiteNote = document.createElement('div');
      whiteNote.className = 'white-note';
      keyboard.appendChild(whiteNote);
      whiteNotes++;
    }
  }

  return keyboard;
}

You can call this function with the desired number of keys as an argument, and then append the returned DOM element to your HTML:

const pianoKeyboard = createPianoKeyboard(12);
document.body.appendChild(pianoKeyboard);

This will create a piano keyboard with 12 keys (or any other number you specify) and append it to the body of your HTML document. Make sure you include the CSS from the previous example to style the keyboard correctly.

When I ran this function, it outputted two white notes and then all black notes. Can you fix it?

Apologies for the confusion. I misunderstood your requirements earlier. Here's an updated version of the createPianoKeyboard function that generates the correct sequence of white and black keys:

function createPianoKeyboard(numKeys) {
  const keyboard = document.createElement('div');
  keyboard.className = 'keyboard';

  let whiteNotes = 0;
  let blackNotes = 0;

  while (whiteNotes + blackNotes < numKeys) {
    // Place a white key
    const whiteNote = document.createElement('div');
    whiteNote.className = 'white-note';
    keyboard.appendChild(whiteNote);
    whiteNotes++;

    // Check if a black key should be placed
    if ((whiteNotes % 7 === 1 || whiteNotes % 7 === 4) && whiteNotes + blackNotes < numKeys) {
      // Place a black key
      const blackNote = document.createElement('div');
      blackNote.className = 'black-note';
      keyboard.appendChild(blackNote);
      blackNotes++;
    }
  }

  return keyboard;
}

This updated function first places a white key and then checks if a black key should be placed based on the position of the white key. It repeats this process until the desired number of keys is reached.

You can call this function and append the returned DOM element to your HTML, just like before:

const pianoKeyboard = createPianoKeyboard(12);
document.body.appendChild(pianoKeyboard);

This should now generate the correct sequence of white and black keys for your piano keyboard. Don't forget to include the CSS from the previous example to style the keyboard correctly.

When I call the new function asking for 12 notes, it generates nine white notes and three black notes. Can you describe how to fix it to generate the right number of back notes?

Apologies for the oversight in my previous response. To correctly generate the desired number of black and white keys, you can slightly modify the createPianoKeyboard function. Here's the updated version:

function createPianoKeyboard(numKeys) {
  const keyboard = document.createElement('div');
  keyboard.className = 'keyboard';

  let noteCount = 0;

  while (noteCount < numKeys) {
    const whiteNote = document.createElement('div');
    whiteNote.className = 'white-note';
    keyboard.appendChild(whiteNote);
    noteCount++;

    if (
      (noteCount % 12 === 1 ||
        noteCount % 12 === 4 ||
        noteCount % 12 === 6 ||
        noteCount % 12 === 9 ||
        noteCount % 12 === 11) &&
      noteCount < numKeys
    ) {
      const blackNote = document.createElement('div');
      blackNote.className = 'black-note';
      keyboard.appendChild(blackNote);
      noteCount++;
    }
  }

  return keyboard;
}

In this version of the function, we keep track of the total number of notes created (noteCount). We place a white note first and then check whether a black note should be placed based on the current value of noteCount. A black note should be placed after every 1st, 4th, 6th, 9th, and 11th note in a group of 12 notes.

You can call this function and append the returned DOM element to your HTML, just like before:

const pianoKeyboard = createPianoKeyboard(12);
document.body.appendChild(pianoKeyboard);

This should now generate the correct sequence of white and black keys for your piano keyboard. Don't forget to include the CSS from the previous example to style the keyboard correctly.

That works fine, but now I want to generate keyboards that don't start with 'A'. Instead I'd like to pass in a MIDI note number as the starting note. Could you make that change?

Sure! I've modified the createPianoKeyboard function to accept a starting MIDI note number as its second argument. The function will now generate a piano keyboard starting from the specified MIDI note.

function createPianoKeyboard(numKeys, startMidiNote) {
  const keyboard = document.createElement('div');
  keyboard.className = 'keyboard';

  let noteCount = 0;

  while (noteCount < numKeys) {
    const midiNote = startMidiNote + noteCount;
    const noteInOctave = midiNote % 12;

    if (noteInOctave === 1 || noteInOctave === 3 || noteInOctave === 6 || noteInOctave === 8 || noteInOctave === 10) {
      const blackNote = document.createElement('div');
      blackNote.className = 'black-note';
      keyboard.appendChild(blackNote);
      noteCount++;
    } else {
      const whiteNote = document.createElement('div');
      whiteNote.className = 'white-note';
      keyboard.appendChild(whiteNote);
      noteCount++;
    }
  }

  return keyboard;
}

In this version of the function, we first calculate the current MIDI note by adding the noteCount to the starting MIDI note number. Then, we determine the note's position within an octave using the modulo operator (% 12). If the note is a black key (i.e., it's the 1st, 3rd, 6th, 8th, or 10th note in the octave), we place a black key; otherwise, we place a white key.

You can call this function with the desired number of keys and starting MIDI note number as arguments, then append the returned DOM element to your HTML:

const pianoKeyboard = createPianoKeyboard(12, 60); // Start with middle C (MIDI note 60)
document.body.appendChild(pianoKeyboard);

This should now generate a piano keyboard with the specified number of keys, starting from the given MIDI note. Don't forget to include the CSS from the previous example to style the keyboard correctly.

Okay, now I'd like to add callbacks when a key is clicked. Could you add "noteOn" and "noteOff" arguments to the function? The callback should be passed the MIDI number of the note to turn on and off.

Certainly! I've updated the createPianoKeyboard function to accept noteOn and noteOff callback functions as arguments. These callbacks will be invoked when a key is pressed and released, respectively, with the MIDI number of the note being passed to the callbacks.

Here's the updated function:

function createPianoKeyboard(numKeys, startMidiNote, noteOn, noteOff) {
  const keyboard = document.createElement('div');
  keyboard.className = 'keyboard';

  let noteCount = 0;

  function addNoteEvents(noteElement, midiNote) {
    noteElement.addEventListener('mousedown', () => {
      if (noteOn) noteOn(midiNote);
    });
    noteElement.addEventListener('mouseup', () => {
      if (noteOff) noteOff(midiNote);
    });
  }

  while (noteCount < numKeys) {
    const midiNote = startMidiNote + noteCount;
    const noteInOctave = midiNote % 12;
    let noteElement;

    if (noteInOctave === 1 || noteInOctave === 3 || noteInOctave === 6 || noteInOctave === 8 || noteInOctave === 10) {
      noteElement = document.createElement('div');
      noteElement.className = 'black-note';
      keyboard.appendChild(noteElement);
      noteCount++;
    } else {
      noteElement = document.createElement('div');
      noteElement.className = 'white-note';
      keyboard.appendChild(noteElement);
      noteCount++;
    }

    addNoteEvents(noteElement, midiNote);
  }

  return keyboard;
}

In this version of the function, I've added a helper function called addNoteEvents that takes a note element and its corresponding MIDI note number as arguments. This function adds mousedown and mouseup event listeners to the note element, which call the noteOn and noteOff callback functions, respectively, with the MIDI note number.

You can now call this function with the desired number of keys, starting MIDI note number, and noteOn and noteOff callbacks as arguments, and append the returned DOM element to your HTML:

const pianoKeyboard = createPianoKeyboard(
  12,
  60,
  (midiNote) => console.log(`Note on: ${midiNote}`),
  (midiNote) => console.log(`Note off: ${midiNote}`)
);
document.body.appendChild(pianoKeyboard);

This example logs the MIDI note numbers when keys are pressed and released. Replace the console.log statements with your desired functionality. Don't forget to include the CSS from the previous examples to style the keyboard correctly.

That works, but when I drag the mouse over multiple notes, I'd like each note to be played in turn. Can you fix it to do that?

Sure! I've updated the createPianoKeyboard function to support playing notes while dragging the mouse over multiple keys. The updated function now listens for mouseenter and mouseleave events in addition to mousedown and mouseup events. It also keeps track of whether the mouse button is currently pressed to determine if a note should be played when the mouse enters a new key.

Here's the updated function:

function createPianoKeyboard(numKeys, startMidiNote, noteOn, noteOff) {
  const keyboard = document.createElement('div');
  keyboard.className = 'keyboard';

  let noteCount = 0;
  let mouseDown = false;

  function addNoteEvents(noteElement, midiNote) {
    noteElement.addEventListener('mousedown', () => {
      if (noteOn) noteOn(midiNote);
      mouseDown = true;
    });

    noteElement.addEventListener('mouseup', () => {
      if (noteOff) noteOff(midiNote);
      mouseDown = false;
    });

    noteElement.addEventListener('mouseenter', () => {
      if (mouseDown && noteOn) noteOn(midiNote);
    });

    noteElement.addEventListener('mouseleave', () => {
      if (mouseDown && noteOff) noteOff(midiNote);
    });
  }

  while (noteCount < numKeys) {
    const midiNote = startMidiNote + noteCount;
    const noteInOctave = midiNote % 12;
    let noteElement;

    if (noteInOctave === 1 || noteInOctave === 3 || noteInOctave === 6 || noteInOctave === 8 || noteInOctave === 10) {
      noteElement = document.createElement('div');
      noteElement.className = 'black-note';
      keyboard.appendChild(noteElement);
      noteCount++;
    } else {
      noteElement = document.createElement('div');
      noteElement.className = 'white-note';
      keyboard.appendChild(noteElement);
      noteCount++;
    }

    addNoteEvents(noteElement, midiNote);
  }

  return keyboard;
}

In this version of the function, we've added two new event listeners, mouseenter and mouseleave, to the addNoteEvents helper function. The mouseenter event listener checks if the mouse button is currently pressed (mouseDown is true) and triggers the noteOn callback if it is. The mouseleave event listener works similarly, triggering the noteOff callback if the mouse button is currently pressed.

You can call this function with the desired number of keys, starting MIDI note number, and noteOn and noteOff callbacks as arguments, and append the returned DOM element to your HTML:

const pianoKeyboard = createPianoKeyboard(
  12,
  60,
  (midiNote) => console.log(`Note on: ${midiNote}`),
  (midiNote) => console.log(`Note off: ${midiNote}`)
);
document.body.appendChild(pianoKeyboard);

This example logs the MIDI note numbers when keys are pressed and released, as well as when dragging over multiple keys. Replace the console.log statements with your desired functionality. Don't forget to include the CSS from the previous examples to style the keyboard correctly.

It mostly works now, but sometimes when I happen to click on the border between two keys, the mouse cursor changes to a beachball. Why does this happen and is there a way to fix it? Please explain what code to add instead of writing out the whole function again.

The beachball cursor (or spinning wheel on some systems) appears when you click on the border between two keys because the browser is trying to select the border. The border is not part of any key element, so the browser treats it as a separate area, and clicking on it initiates a selection process.

To prevent this behavior, you can disable the user selection for the entire keyboard by adding the following CSS rule to your stylesheet:

.keyboard {
  user-select: none;
}

This rule disables user selection for the .keyboard element and its child elements, ensuring that the cursor doesn't change to a beachball (or spinning wheel) when you click on the border between two keys.

By adding this CSS rule, the browser will no longer attempt to select the border, and the issue with the beachball cursor should be resolved. The rest of the JavaScript code for handling key events can remain unchanged.

That worked, but I see another problem. When I hold the mouse button down, move the mouse outside the keyboard on the bottom, and move it back in again, the noteOn and noteOff callbacks aren't called anymore. (There is still a noteOff callback when releasing the mouse.)

Could you explain how to fix that? Please don't write out the whole function again, just tell me the code that I should add inside the createPianoKeyboard function.

To fix this issue, you can add a global mousemove event listener to track the mouse position and update the noteOn and noteOff callbacks accordingly. To do this, add the following code inside the createPianoKeyboard function:

document.addEventListener('mousemove', (event) => {
  const noteElement = event.target;

  if (!keyboard.contains(noteElement)) {
    return;
  }

  const midiNote = parseInt(noteElement.getAttribute('data-midi-note'));

  if (isNaN(midiNote)) {
    return;
  }

  if (mouseDown) {
    if (noteOn) noteOn(midiNote);
  } else {
    if (noteOff) noteOff(midiNote);
  }
});

This code snippet listens for mousemove events on the document. When the event is fired, it checks whether the event target (i.e., the element the mouse is currently over) is a child of the keyboard element. If it is not, the function returns immediately. If the event target is a child of the keyboard, it retrieves the MIDI note number from the data-midi-note attribute.

Make sure you also set the data-midi-note attribute for each note element in the createPianoKeyboard function:

noteElement.setAttribute('data-midi-note', midiNote);

Add this line of code right after creating a new noteElement (both white and black notes) in the loop that generates the keys.

By adding this code to your function, the noteOn and noteOff callbacks should be called correctly even when the mouse leaves and re-enters the keyboard area.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment