|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Piano Theory Master - Circle of Fifths</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
|
|
|
body { |
|
font-family: 'Inter', sans-serif; |
|
background-color: #f8fafc; |
|
color: #1e293b; |
|
} |
|
|
|
.circle-container { |
|
position: relative; |
|
width: 400px; |
|
height: 400px; |
|
margin: 0 auto; |
|
} |
|
|
|
.key-btn { |
|
position: absolute; |
|
width: 60px; |
|
height: 60px; |
|
border-radius: 50%; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-weight: 600; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.key-btn:hover { |
|
transform: scale(1.1); |
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.major { background-color: #6366f1; color: white; } |
|
.minor { background-color: #8b5cf6; color: white; } |
|
.active { transform: scale(1.2); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.5); } |
|
|
|
.piano-key { |
|
position: relative; |
|
height: 120px; |
|
border: 1px solid #1e293b; |
|
border-radius: 0 0 6px 6px; |
|
cursor: pointer; |
|
transition: all 0.1s ease; |
|
} |
|
|
|
.white-key { |
|
background-color: white; |
|
width: 40px; |
|
z-index: 1; |
|
} |
|
|
|
.black-key { |
|
background-color: #1e293b; |
|
width: 24px; |
|
height: 70px; |
|
margin-left: -12px; |
|
margin-right: -12px; |
|
z-index: 2; |
|
color: white; |
|
} |
|
|
|
.pressed { |
|
background-color: #6366f1; |
|
color: white; |
|
} |
|
|
|
.note-label { |
|
position: absolute; |
|
bottom: 8px; |
|
width: 100%; |
|
text-align: center; |
|
font-size: 12px; |
|
font-weight: 600; |
|
} |
|
|
|
.chord-progression-btn { |
|
transition: all 0.2s ease; |
|
} |
|
|
|
.chord-progression-btn:hover { |
|
transform: translateY(-2px); |
|
} |
|
|
|
.highlight { |
|
animation: highlight 0.5s ease; |
|
} |
|
|
|
@keyframes highlight { |
|
0% { background-color: rgba(99, 102, 241, 0.3); } |
|
100% { background-color: transparent; } |
|
} |
|
</style> |
|
</head> |
|
<body class="min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
<header class="text-center mb-8"> |
|
<h1 class="text-4xl font-bold text-indigo-600 mb-2">Piano Theory Master</h1> |
|
<p class="text-lg text-slate-600">Interactive Circle of Fifths & Music Theory Tool</p> |
|
</header> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
|
|
|
<div class="bg-white rounded-xl shadow-md p-6 lg:col-span-2"> |
|
<h2 class="text-2xl font-semibold mb-4 text-center">Circle of Fifths</h2> |
|
|
|
<div class="circle-container mb-6"> |
|
|
|
</div> |
|
|
|
<div class="flex justify-center space-x-4 mb-6"> |
|
<button id="toggle-mode" class="px-4 py-2 bg-indigo-100 text-indigo-700 rounded-lg font-medium hover:bg-indigo-200 transition"> |
|
<i class="fas fa-exchange-alt mr-2"></i> Toggle Major/Minor |
|
</button> |
|
<button id="reset-btn" class="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg font-medium hover:bg-slate-200 transition"> |
|
<i class="fas fa-redo mr-2"></i> Reset |
|
</button> |
|
</div> |
|
|
|
<div class="bg-indigo-50 rounded-lg p-4"> |
|
<h3 class="font-semibold text-indigo-800 mb-2">Key Signature Info</h3> |
|
<div id="key-info" class="text-slate-700"> |
|
Select a key from the circle to see details |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
<h2 class="text-2xl font-semibold mb-4 text-center">Interactive Piano</h2> |
|
|
|
<div class="flex justify-center mb-6"> |
|
<div class="flex" id="piano"> |
|
|
|
</div> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<h3 class="font-semibold mb-2">Current Key: <span id="current-key" class="text-indigo-600">None selected</span></h3> |
|
<div id="scale-notes" class="text-sm text-slate-600 mb-4"> |
|
Select a key from the circle to see scale notes |
|
</div> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<h3 class="font-semibold mb-2">Common Chord Progressions</h3> |
|
<div class="grid grid-cols-2 gap-2"> |
|
<button class="chord-progression-btn px-3 py-2 bg-emerald-100 text-emerald-800 rounded text-sm font-medium"> |
|
I - IV - V |
|
</button> |
|
<button class="chord-progression-btn px-3 py-2 bg-emerald-100 text-emerald-800 rounded text-sm font-medium"> |
|
I - V - vi - IV |
|
</button> |
|
<button class="chord-progression-btn px-3 py-2 bg-emerald-100 text-emerald-800 rounded text-sm font-medium"> |
|
ii - V - I |
|
</button> |
|
<button class="chord-progression-btn px-3 py-2 bg-emerald-100 text-emerald-800 rounded text-sm font-medium"> |
|
I - vi - IV - V |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="bg-slate-50 rounded-lg p-4"> |
|
<h3 class="font-semibold text-slate-800 mb-2">Chord Builder</h3> |
|
<div class="flex items-center mb-2"> |
|
<select id="chord-type" class="mr-2 px-2 py-1 border rounded text-sm"> |
|
<option value="major">Major</option> |
|
<option value="minor">Minor</option> |
|
<option value="dim">Diminished</option> |
|
<option value="aug">Augmented</option> |
|
<option value="7">Dominant 7th</option> |
|
<option value="maj7">Major 7th</option> |
|
<option value="m7">Minor 7th</option> |
|
</select> |
|
<button id="build-chord" class="px-3 py-1 bg-indigo-600 text-white rounded text-sm hover:bg-indigo-700"> |
|
Build Chord |
|
</button> |
|
</div> |
|
<div id="chord-notes" class="text-sm text-slate-700"> |
|
Select a root note and chord type |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-md p-6 mt-8"> |
|
<h2 class="text-2xl font-semibold mb-4 text-center">Music Theory Reference</h2> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
|
<div class="bg-violet-50 p-4 rounded-lg"> |
|
<h3 class="font-semibold text-violet-800 mb-2"><i class="fas fa-question-circle mr-2"></i>What is the Circle of Fifths?</h3> |
|
<p class="text-sm text-slate-700"> |
|
The circle of fifths is a diagram showing the relationship between the 12 tones of the chromatic scale, their corresponding key signatures, and the associated major and minor keys. |
|
</p> |
|
</div> |
|
|
|
<div class="bg-blue-50 p-4 rounded-lg"> |
|
<h3 class="font-semibold text-blue-800 mb-2"><i class="fas fa-music mr-2"></i>How to Use This Tool</h3> |
|
<ul class="text-sm text-slate-700 space-y-1"> |
|
<li>• Click keys on the circle to see their scales and chords</li> |
|
<li>• Play notes on the virtual piano</li> |
|
<li>• Explore common chord progressions</li> |
|
<li>• Build custom chords with the chord builder</li> |
|
</ul> |
|
</div> |
|
|
|
<div class="bg-emerald-50 p-4 rounded-lg"> |
|
<h3 class="font-semibold text-emerald-800 mb-2"><i class="fas fa-lightbulb mr-2"></i>Practice Tips</h3> |
|
<ul class="text-sm text-slate-700 space-y-1"> |
|
<li>• Practice scales in all 12 keys</li> |
|
<li>• Memorize the order of sharps and flats</li> |
|
<li>• Identify relative major/minor pairs</li> |
|
<li>• Experiment with chord progressions</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const circleData = [ |
|
{ key: 'C', major: 'C', minor: 'Am', pos: { x: 50, y: 50 }, sharps: 0, flats: 0 }, |
|
{ key: 'G', major: 'G', minor: 'Em', pos: { x: 75, y: 20 }, sharps: 1, flats: 0 }, |
|
{ key: 'D', major: 'D', minor: 'Bm', pos: { x: 90, y: 50 }, sharps: 2, flats: 0 }, |
|
{ key: 'A', major: 'A', minor: 'F#m', pos: { x: 75, y: 80 }, sharps: 3, flats: 0 }, |
|
{ key: 'E', major: 'E', minor: 'C#m', pos: { x: 50, y: 90 }, sharps: 4, flats: 0 }, |
|
{ key: 'B', major: 'B', minor: 'G#m', pos: { x: 25, y: 80 }, sharps: 5, flats: 0 }, |
|
{ key: 'F#', major: 'F#', minor: 'D#m', pos: { x: 10, y: 50 }, sharps: 6, flats: 0 }, |
|
{ key: 'C#', major: 'C#', minor: 'A#m', pos: { x: 25, y: 20 }, sharps: 7, flats: 0 }, |
|
{ key: 'F', major: 'F', minor: 'Dm', pos: { x: 50, y: 10 }, sharps: 0, flats: 1 }, |
|
{ key: 'Bb', major: 'Bb', minor: 'Gm', pos: { x: 75, y: 10 }, sharps: 0, flats: 2 }, |
|
{ key: 'Eb', major: 'Eb', minor: 'Cm', pos: { x: 90, y: 20 }, sharps: 0, flats: 3 }, |
|
{ key: 'Ab', major: 'Ab', minor: 'Fm', pos: { x: 90, y: 80 }, sharps: 0, flats: 4 }, |
|
{ key: 'Db', major: 'Db', minor: 'Bbm', pos: { x: 75, y: 90 }, sharps: 0, flats: 5 }, |
|
{ key: 'Gb', major: 'Gb', minor: 'Ebm', pos: { x: 50, y: 90 }, sharps: 0, flats: 6 }, |
|
{ key: 'Cb', major: 'Cb', minor: 'Abm', pos: { x: 25, y: 90 }, sharps: 0, flats: 7 } |
|
]; |
|
|
|
|
|
const scales = { |
|
major: ['C', 'D', 'E', 'F', 'G', 'A', 'B'], |
|
minor: ['A', 'B', 'C', 'D', 'E', 'F', 'G'] |
|
}; |
|
|
|
const chordFormulas = { |
|
major: [0, 4, 7], |
|
minor: [0, 3, 7], |
|
dim: [0, 3, 6], |
|
aug: [0, 4, 8], |
|
'7': [0, 4, 7, 10], |
|
'maj7': [0, 4, 7, 11], |
|
'm7': [0, 3, 7, 10] |
|
}; |
|
|
|
const noteOrder = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; |
|
|
|
|
|
let currentMode = 'major'; |
|
let selectedKey = null; |
|
|
|
|
|
const circleContainer = document.querySelector('.circle-container'); |
|
const keyInfo = document.getElementById('key-info'); |
|
const currentKeyDisplay = document.getElementById('current-key'); |
|
const scaleNotesDisplay = document.getElementById('scale-notes'); |
|
const pianoContainer = document.getElementById('piano'); |
|
const chordNotesDisplay = document.getElementById('chord-notes'); |
|
const toggleModeBtn = document.getElementById('toggle-mode'); |
|
const resetBtn = document.getElementById('reset-btn'); |
|
const buildChordBtn = document.getElementById('build-chord'); |
|
const chordTypeSelect = document.getElementById('chord-type'); |
|
|
|
|
|
initCircleOfFifths(); |
|
initPiano(); |
|
setupEventListeners(); |
|
|
|
function initCircleOfFifths() { |
|
circleContainer.innerHTML = ''; |
|
|
|
circleData.forEach(item => { |
|
const btn = document.createElement('div'); |
|
btn.className = `key-btn ${currentMode === 'major' ? 'major' : 'minor'}`; |
|
btn.style.left = `${item.pos.x}%`; |
|
btn.style.top = `${item.pos.y}%`; |
|
btn.textContent = currentMode === 'major' ? item.major : item.minor; |
|
btn.dataset.key = currentMode === 'major' ? item.major : item.minor; |
|
btn.dataset.mode = currentMode; |
|
btn.dataset.sharps = item.sharps; |
|
btn.dataset.flats = item.flats; |
|
|
|
btn.addEventListener('click', () => { |
|
selectKey(btn, item); |
|
}); |
|
|
|
circleContainer.appendChild(btn); |
|
}); |
|
} |
|
|
|
function selectKey(btn, data) { |
|
|
|
document.querySelectorAll('.key-btn').forEach(b => b.classList.remove('active')); |
|
|
|
|
|
btn.classList.add('active'); |
|
|
|
|
|
selectedKey = btn.dataset.key; |
|
currentKeyDisplay.textContent = selectedKey; |
|
|
|
|
|
const sharps = parseInt(btn.dataset.sharps); |
|
const flats = parseInt(btn.dataset.flats); |
|
|
|
let keySignature = ''; |
|
if (sharps > 0) { |
|
keySignature = `${sharps} sharp${sharps > 1 ? 's' : ''}: `; |
|
const sharpNotes = getSharpNotes(sharps); |
|
keySignature += sharpNotes.join(', '); |
|
} else if (flats > 0) { |
|
keySignature = `${flats} flat${flats > 1 ? 's' : ''}: `; |
|
const flatNotes = getFlatNotes(flats); |
|
keySignature += flatNotes.join(', '); |
|
} else { |
|
keySignature = 'No sharps or flats'; |
|
} |
|
|
|
keyInfo.innerHTML = ` |
|
<p><strong>Key:</strong> ${selectedKey} ${btn.dataset.mode === 'major' ? 'Major' : 'Minor'}</p> |
|
<p><strong>Key Signature:</strong> ${keySignature}</p> |
|
<p><strong>Relative ${btn.dataset.mode === 'major' ? 'Minor' : 'Major'}:</strong> ${btn.dataset.mode === 'major' ? data.minor : data.major}</p> |
|
`; |
|
|
|
|
|
const scaleNotes = getScaleNotes(selectedKey, btn.dataset.mode); |
|
scaleNotesDisplay.innerHTML = ` |
|
<strong>Scale Notes:</strong> ${scaleNotes.join(' - ')} |
|
`; |
|
|
|
|
|
highlightPianoKeys(scaleNotes); |
|
} |
|
|
|
function getSharpNotes(count) { |
|
const sharpOrder = ['F', 'C', 'G', 'D', 'A', 'E', 'B']; |
|
return sharpOrder.slice(0, count); |
|
} |
|
|
|
function getFlatNotes(count) { |
|
const flatOrder = ['B', 'E', 'A', 'D', 'G', 'C', 'F']; |
|
return flatOrder.slice(0, count); |
|
} |
|
|
|
function getScaleNotes(root, mode) { |
|
|
|
let rootIndex = noteOrder.indexOf(root.replace('b', '#') === 'F#' ? 'Gb' : root.replace('b', '#')); |
|
|
|
|
|
if (rootIndex === -1) { |
|
if (root === 'Gb') rootIndex = noteOrder.indexOf('F#'); |
|
if (root === 'Db') rootIndex = noteOrder.indexOf('C#'); |
|
if (root === 'Cb') rootIndex = noteOrder.indexOf('B'); |
|
} |
|
|
|
const scalePattern = mode === 'major' ? [0, 2, 4, 5, 7, 9, 11] : [0, 2, 3, 5, 7, 8, 10]; |
|
const notes = []; |
|
|
|
for (let i = 0; i < 7; i++) { |
|
const noteIndex = (rootIndex + scalePattern[i]) % 12; |
|
notes.push(noteOrder[noteIndex]); |
|
} |
|
|
|
return notes; |
|
} |
|
|
|
function initPiano() { |
|
const whiteKeys = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; |
|
const blackKeys = ['C#', 'D#', '', 'F#', 'G#', 'A#', '']; |
|
|
|
|
|
whiteKeys.forEach((note, i) => { |
|
const key = document.createElement('div'); |
|
key.className = 'piano-key white-key'; |
|
key.dataset.note = note; |
|
|
|
const label = document.createElement('div'); |
|
label.className = 'note-label'; |
|
label.textContent = note; |
|
|
|
key.appendChild(label); |
|
|
|
key.addEventListener('mousedown', () => playNote(note, key)); |
|
key.addEventListener('mouseup', () => stopNote(key)); |
|
key.addEventListener('mouseleave', () => stopNote(key)); |
|
|
|
pianoContainer.appendChild(key); |
|
}); |
|
|
|
|
|
blackKeys.forEach((note, i) => { |
|
if (note) { |
|
const key = document.createElement('div'); |
|
key.className = 'piano-key black-key'; |
|
key.dataset.note = note; |
|
|
|
const label = document.createElement('div'); |
|
label.className = 'note-label'; |
|
label.textContent = note; |
|
|
|
key.appendChild(label); |
|
|
|
key.addEventListener('mousedown', () => playNote(note, key)); |
|
key.addEventListener('mouseup', () => stopNote(key)); |
|
key.addEventListener('mouseleave', () => stopNote(key)); |
|
|
|
pianoContainer.insertBefore(key, pianoContainer.children[i * 2 + 1]); |
|
} |
|
}); |
|
} |
|
|
|
function playNote(note, keyElement) { |
|
|
|
keyElement.classList.add('pressed'); |
|
|
|
|
|
if (selectedKey) { |
|
const scaleNotes = getScaleNotes(selectedKey, currentMode); |
|
if (scaleNotes.includes(note)) { |
|
keyElement.classList.add('highlight'); |
|
setTimeout(() => { |
|
keyElement.classList.remove('highlight'); |
|
}, 500); |
|
} |
|
} |
|
} |
|
|
|
function stopNote(keyElement) { |
|
keyElement.classList.remove('pressed'); |
|
} |
|
|
|
function highlightPianoKeys(notes) { |
|
|
|
document.querySelectorAll('.piano-key').forEach(key => { |
|
key.classList.remove('highlight'); |
|
}); |
|
|
|
|
|
notes.forEach(note => { |
|
const key = document.querySelector(`.piano-key[data-note="${note}"]`); |
|
if (key) { |
|
key.classList.add('highlight'); |
|
setTimeout(() => { |
|
key.classList.remove('highlight'); |
|
}, 1000); |
|
} |
|
}); |
|
} |
|
|
|
function buildChord() { |
|
if (!selectedKey) { |
|
chordNotesDisplay.textContent = 'Please select a key first'; |
|
return; |
|
} |
|
|
|
const chordType = chordTypeSelect.value; |
|
const rootNote = selectedKey; |
|
const formula = chordFormulas[chordType]; |
|
|
|
|
|
let rootIndex = noteOrder.indexOf(rootNote.replace('b', '#') === 'F#' ? 'Gb' : rootNote.replace('b', '#')); |
|
|
|
|
|
if (rootIndex === -1) { |
|
if (rootNote === 'Gb') rootIndex = noteOrder.indexOf('F#'); |
|
if (rootNote === 'Db') rootIndex = noteOrder.indexOf('C#'); |
|
if (rootNote === 'Cb') rootIndex = noteOrder.indexOf('B'); |
|
} |
|
|
|
|
|
const chordNotes = formula.map(interval => { |
|
return noteOrder[(rootIndex + interval) % 12]; |
|
}); |
|
|
|
chordNotesDisplay.innerHTML = ` |
|
<strong>${rootNote} ${chordType} chord:</strong> ${chordNotes.join(' - ')} |
|
`; |
|
|
|
|
|
highlightChordOnPiano(chordNotes); |
|
} |
|
|
|
function highlightChordOnPiano(notes) { |
|
|
|
document.querySelectorAll('.piano-key').forEach(key => { |
|
key.classList.remove('highlight'); |
|
}); |
|
|
|
|
|
notes.forEach(note => { |
|
const key = document.querySelector(`.piano-key[data-note="${note}"]`); |
|
if (key) { |
|
key.classList.add('highlight'); |
|
} |
|
}); |
|
} |
|
|
|
function setupEventListeners() { |
|
toggleModeBtn.addEventListener('click', () => { |
|
currentMode = currentMode === 'major' ? 'minor' : 'major'; |
|
initCircleOfFifths(); |
|
if (selectedKey) { |
|
|
|
const currentItem = circleData.find(item => |
|
currentMode === 'major' ? item.major === selectedKey : item.minor === selectedKey |
|
); |
|
|
|
if (currentItem) { |
|
const newKey = currentMode === 'major' ? currentItem.major : currentItem.minor; |
|
const btn = document.querySelector(`.key-btn[data-key="${newKey}"]`); |
|
if (btn) { |
|
selectKey(btn, currentItem); |
|
} |
|
} |
|
} |
|
}); |
|
|
|
resetBtn.addEventListener('click', () => { |
|
selectedKey = null; |
|
currentKeyDisplay.textContent = 'None selected'; |
|
keyInfo.textContent = 'Select a key from the circle to see details'; |
|
scaleNotesDisplay.textContent = 'Select a key from the circle to see scale notes'; |
|
document.querySelectorAll('.key-btn').forEach(b => b.classList.remove('active')); |
|
document.querySelectorAll('.piano-key').forEach(k => k.classList.remove('highlight')); |
|
}); |
|
|
|
buildChordBtn.addEventListener('click', buildChord); |
|
|
|
|
|
document.querySelectorAll('.chord-progression-btn').forEach(btn => { |
|
btn.addEventListener('click', function() { |
|
if (!selectedKey) { |
|
alert('Please select a key first'); |
|
return; |
|
} |
|
|
|
const progression = this.textContent.trim(); |
|
alert(`Playing ${progression} progression in ${selectedKey} ${currentMode}`); |
|
|
|
}); |
|
}); |
|
} |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=NativeAngels/music-theory-assistant" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |