* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
padding: 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
overflow: hidden;
max-width: 1400px;
width: 100%;
}
.header {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
padding: 30px;
text-align: center;
}
h1 {
font-size: 32px;
margin-bottom: 8px;
}
.subtitle {
font-size: 14px;
opacity: 0.9;
}
.piano-content {
padding: 40px 30px;
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 10px;
}
.control-group label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.control-group select,
.control-group input {
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: border-color 0.3s;
}
.control-group input {
width: 80px;
}
.control-group select:focus,
.control-group input:focus {
outline: none;
border-color: #43e97b;
}
.piano-container {
position: relative;
display: flex;
justify-content: center;
margin: 0 auto;
max-width: 1200px;
height: 300px;
background: #1a1a1a;
padding: 20px;
border-radius: 12px;
box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.5);
}
.piano {
position: relative;
display: flex;
height: 100%;
}
.key {
position: relative;
cursor: pointer;
user-select: none;
transition: all 0.05s;
}
.white-key {
width: 60px;
height: 100%;
background: linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%);
border: 2px solid #333;
border-radius: 0 0 8px 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.white-key:hover {
background: linear-gradient(to bottom, #f0f0f0 0%, #e0e0e0 100%);
}
.white-key:active,
.white-key.active {
background: linear-gradient(to bottom, #43e97b 0%, #38f9d7 100%);
transform: translateY(2px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.black-key {
position: absolute;
width: 40px;
height: 60%;
background: linear-gradient(to bottom, #2a2a2a 0%, #000000 100%);
border: 2px solid #000;
border-radius: 0 0 6px 6px;
z-index: 10;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
.black-key:hover {
background: linear-gradient(to bottom, #3a3a3a 0%, #1a1a1a 100%);
}
.black-key:active,
.black-key.active {
background: linear-gradient(to bottom, #43e97b 0%, #38f9d7 100%);
transform: translateY(2px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.key-label {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
font-weight: 600;
color: #666;
background: rgba(255, 255, 255, 0.8);
padding: 2px 6px;
border-radius: 3px;
}
.black-key .key-label {
color: #fff;
background: rgba(0, 0, 0, 0.5);
}
.note-display {
text-align: center;
margin-top: 30px;
font-size: 18px;
color: #666;
min-height: 30px;
}
.note-name {
font-weight: 600;
color: #43e97b;
font-size: 24px;
}
.instructions {
text-align: center;
margin-top: 20px;
color: #999;
font-size: 13px;
}
@media (max-width: 768px) {
.white-key {
width: 45px;
}
.black-key {
width: 30px;
}
.piano-container {
height: 250px;
}
}
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const masterGain = audioContext.createGain();
masterGain.connect(audioContext.destination);
let activeNotes = new Map();
let sustainPedal = false;
let sustainedNotes = new Set();
const notes = [
{ note: 'C4', freq: 261.63, white: true, key: 'A' },
{ note: 'C#4', freq: 277.18, white: false, key: 'W' },
{ note: 'D4', freq: 293.66, white: true, key: 'S' },
{ note: 'D#4', freq: 311.13, white: false, key: 'E' },
{ note: 'E4', freq: 329.63, white: true, key: 'D' },
{ note: 'F4', freq: 349.23, white: true, key: 'F' },
{ note: 'F#4', freq: 369.99, white: false, key: 'T' },
{ note: 'G4', freq: 392.00, white: true, key: 'G' },
{ note: 'G#4', freq: 415.30, white: false, key: 'Y' },
{ note: 'A4', freq: 440.00, white: true, key: 'H' },
{ note: 'A#4', freq: 466.16, white: false, key: 'U' },
{ note: 'B4', freq: 493.88, white: true, key: 'J' },
{ note: 'C5', freq: 523.25, white: true, key: 'K' },
{ note: 'C#5', freq: 554.37, white: false, key: 'O' },
{ note: 'D5', freq: 587.33, white: true, key: 'L' },
{ note: 'D#5', freq: 622.25, white: false, key: 'P' },
{ note: 'E5', freq: 659.25, white: true, key: ';' },
{ note: 'F5', freq: 698.46, white: true, key: '\'' }
];
function createPiano() {
const piano = document.getElementById('piano');
let whiteKeyIndex = 0;
notes.forEach((noteData, index) => {
const key = document.createElement('div');
key.className = noteData.white ? 'white-key key' : 'black-key key';
key.dataset.note = noteData.note;
key.dataset.freq = noteData.freq;
key.dataset.key = noteData.key;
const label = document.createElement('div');
label.className = 'key-label';
label.textContent = noteData.key;
key.appendChild(label);
if (noteData.white) {
piano.appendChild(key);
whiteKeyIndex++;
} else {
key.style.left = `${(whiteKeyIndex * 60) - 20}px`;
piano.appendChild(key);
}
key.addEventListener('mousedown', () => playNote(noteData.freq, noteData.note, key));
key.addEventListener('mouseup', () => stopNote(noteData.note, key));
key.addEventListener('mouseleave', () => {
if (activeNotes.has(noteData.note)) {
stopNote(noteData.note, key);
}
});
key.addEventListener('touchstart', (e) => {
e.preventDefault();
playNote(noteData.freq, noteData.note, key);
});
key.addEventListener('touchend', (e) => {
e.preventDefault();
stopNote(noteData.note, key);
});
});
}
function playNote(frequency, noteName, keyElement) {
if (activeNotes.has(noteName)) return;
const now = audioContext.currentTime;
const volume = parseFloat(document.getElementById('volume').value);
const waveform = document.getElementById('waveform').value;
// Create oscillator
const oscillator = audioContext.createOscillator();
oscillator.type = waveform;
oscillator.frequency.setValueAtTime(frequency, now);
// Create gain envelope for ADSR
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(0, now);
// Attack (0.01s)
gainNode.gain.linearRampToValueAtTime(volume, now + 0.01);
// Decay to sustain level (0.1s)
gainNode.gain.linearRampToValueAtTime(volume * 0.7, now + 0.11);
oscillator.connect(gainNode);
gainNode.connect(masterGain);
oscillator.start(now);
activeNotes.set(noteName, { oscillator, gainNode, keyElement });
if (keyElement) {
keyElement.classList.add('active');
}
updateDisplay();
}
function stopNote(noteName, keyElement) {
if (!activeNotes.has(noteName)) return;
const noteData = activeNotes.get(noteName);
if (sustainPedal) {
sustainedNotes.add(noteName);
return;
}
const now = audioContext.currentTime;
// Release (fade out over 0.3s for more natural sound)
noteData.gainNode.gain.cancelScheduledValues(now);
noteData.gainNode.gain.setValueAtTime(noteData.gainNode.gain.value, now);
noteData.gainNode.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
noteData.oscillator.stop(now + 0.3);
activeNotes.delete(noteName);
if (keyElement) {
keyElement.classList.remove('active');
}
updateDisplay();
}
function updateDisplay() {
const activeNoteNames = Array.from(activeNotes.keys());
const display = document.getElementById('currentNote');
if (activeNoteNames.length === 0) {
display.innerHTML = 'Click or press a key to play';
} else {
display.innerHTML = `Playing: <span class="note-name">${activeNoteNames.join(', ')}</span>`;
}
}
function releaseSustainedNotes() {
sustainedNotes.forEach(noteName => {
if (activeNotes.has(noteName)) {
const noteData = activeNotes.get(noteName);
const now = audioContext.currentTime;
noteData.gainNode.gain.cancelScheduledValues(now);
noteData.gainNode.gain.setValueAtTime(noteData.gainNode.gain.value, now);
noteData.gainNode.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
noteData.oscillator.stop(now + 0.3);
activeNotes.delete(noteName);
if (noteData.keyElement) {
noteData.keyElement.classList.remove('active');
}
}
});
sustainedNotes.clear();
updateDisplay();
}
document.addEventListener('keydown', (e) => {
// Sustain pedal (space bar)
if (e.code === 'Space' && !e.repeat) {
e.preventDefault();
sustainPedal = true;
return;
}
const key = e.key.toUpperCase();
const noteData = notes.find(n => n.key === key);
if (noteData && !e.repeat) {
const keyElement = document.querySelector(`[data-key="${key}"]`);
playNote(noteData.freq, noteData.note, keyElement);
}
});
document.addEventListener('keyup', (e) => {
// Sustain pedal release
if (e.code === 'Space') {
e.preventDefault();
sustainPedal = false;
releaseSustainedNotes();
return;
}
const key = e.key.toUpperCase();
const noteData = notes.find(n => n.key === key);
if (noteData) {
const keyElement = document.querySelector(`[data-key="${key}"]`);
stopNote(noteData.note, keyElement);
}
});
createPiano();
No comments yet. Be the first!