JSFiddle - React, Tailwind, and code Playground

by gustav75

HTML

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Virtuelles Klavier</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="piano">
        <div class="key white" data-note="C" data-key="A"></div>
        <div class="key black" data-note="C#" data-key="W" style="left: 50px;"></div>
        <div class="key white" data-note="D" data-key="S"></div>
        <div class="key black" data-note="D#" data-key="E" style="left: 130px;"></div>
        <div class="key white" data-note="E" data-key="D"></div>
        <div class="key white" data-note="F" data-key="F"></div>
        <div class="key black" data-note="F#" data-key="T" style="left: 290px;"></div>
        <div class="key white" data-note="G" data-key="G"></div>
        <div class="key black" data-note="G#" data-key="Y" style="left: 370px;"></div>
        <div class="key white" data-note="A" data-key="H"></div>
        <div class="key black" data-note="A#" data-key="U" style="left: 450px;"></div>
        <div class="key white" data-note="B" data-key="J"></div>
        <div class="key white" data-note="C2" data-key="K"></div>
    </div>
    <script src="script.js"></script>
</body>
</html>

CSS

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
    background-color: #333;
}

#piano {
    position: relative;
    width: 760px;
}

.key {
    width: 80px;
    height: 300px;
    border: 1px solid #000;
    box-sizing: border-box;
    position: relative;
    cursor: pointer;
    transition: background-color 0.1s;
}

.white {
    background-color: white;
    z-index: 1;
    float: left;
}

.black {
    background-color: black;
    width: 60px;
    height: 180px;
    position: absolute;
    top: 0;
    z-index: 2;
}

.key.playing.white {
    background-color: lightgray;
}

.key.playing.black {
    background-color: #555;
}

JavaScript

const notes = {
    'C': 'https://lagustav.ch/wp-content/uploads/2024/08/C-6.mp3',
    'C#': 'https://lagustav.ch/wp-content/uploads/2024/08/C-7.mp3',
    'D': 'https://lagustav.ch/wp-content/uploads/2024/08/D-6.mp3',
    'D#': 'https://lagustav.ch/wp-content/uploads/2024/08/D-7.mp3',
    'E': 'https://lagustav.ch/wp-content/uploads/2024/08/E-3.mp3',
    'F': 'https://lagustav.ch/wp-content/uploads/2024/08/F-6.mp3',
    'F#': 'https://lagustav.ch/wp-content/uploads/2024/08/F-7.mp3',
    'G': 'https://lagustav.ch/wp-content/uploads/2024/08/G-6.mp3',
    'G#': 'https://lagustav.ch/wp-content/uploads/2024/08/G-7.mp3',
    'A': 'https://lagustav.ch/wp-content/uploads/2024/08/A-6.mp3',
    'A#': 'https://lagustav.ch/wp-content/uploads/2024/08/A-7.mp3',
    'B': 'https://lagustav.ch/wp-content/uploads/2024/08/H-3.mp3',
    'C2': 'https://lagustav.ch/wp-content/uploads/2024/08/C2-3.mp3'
};

const keyMap = {
    'A': 'C',
    'W': 'C#',
    'S': 'D',
    'E': 'D#',
    'D': 'E',
    'F': 'F',
    'T': 'F#',
    'G': 'G',
    'Y': 'G#',
    'H': 'A',
    'U': 'A#',
    'J': 'B',
    'K': 'C2'
};

document.querySelectorAll('.key').forEach(key => {
    key.addEventListener('mousedown', () => playNote(key));
    key.addEventListener('mouseup', () => removePlayingClass(key));
    key.addEventListener('mouseleave', () => removePlayingClass(key));
});

document.addEventListener('keydown', (event) => {
    const note = keyMap[event.key.toUpperCase()];
    if (note) {
        const key = document.querySelector(`.key[data-note="${note}"]`);
        if (key) {
            playNote(key);
        }
    }
});

document.addEventListener('keyup', (event) => {
    const note = keyMap[event.key.toUpperCase()];
    if (note) {
        const key = document.querySelector(`.key[data-note="${note}"]`);
        if (key) {
            removePlayingClass(key);
        }
    }
});

function playNote(key) {
    const note = key.dataset.note;
    const audio = new Audio(notes[note]);
   ...