NativeAngels's picture
Add 3 files
1050e61 verified
<!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">
<!-- Circle of Fifths Section -->
<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">
<!-- Circle of Fifths will be generated by JavaScript -->
</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>
<!-- Piano & Chords Section -->
<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">
<!-- Piano keys will be generated by JavaScript -->
</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>
<!-- Theory Reference Section -->
<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() {
// Circle of Fifths Data
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 }
];
// Scale and chord data
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'];
// State variables
let currentMode = 'major';
let selectedKey = null;
// DOM Elements
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');
// Initialize the app
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) {
// Remove active class from all buttons
document.querySelectorAll('.key-btn').forEach(b => b.classList.remove('active'));
// Add active class to selected button
btn.classList.add('active');
// Update selected key
selectedKey = btn.dataset.key;
currentKeyDisplay.textContent = selectedKey;
// Update key info
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>
`;
// Update scale notes
const scaleNotes = getScaleNotes(selectedKey, btn.dataset.mode);
scaleNotesDisplay.innerHTML = `
<strong>Scale Notes:</strong> ${scaleNotes.join(' - ')}
`;
// Highlight piano keys for this scale
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) {
// Find the index of the root note
let rootIndex = noteOrder.indexOf(root.replace('b', '#') === 'F#' ? 'Gb' : root.replace('b', '#'));
// Adjust for enharmonic equivalents
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#', ''];
// Create white keys
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);
});
// Create black keys
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) {
// In a real app, you would play the note using Web Audio API
keyElement.classList.add('pressed');
// Highlight corresponding note in scale if a key is selected
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) {
// Reset all keys
document.querySelectorAll('.piano-key').forEach(key => {
key.classList.remove('highlight');
});
// Highlight scale notes
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];
// Find root note index
let rootIndex = noteOrder.indexOf(rootNote.replace('b', '#') === 'F#' ? 'Gb' : rootNote.replace('b', '#'));
// Adjust for enharmonic equivalents
if (rootIndex === -1) {
if (rootNote === 'Gb') rootIndex = noteOrder.indexOf('F#');
if (rootNote === 'Db') rootIndex = noteOrder.indexOf('C#');
if (rootNote === 'Cb') rootIndex = noteOrder.indexOf('B');
}
// Get chord notes
const chordNotes = formula.map(interval => {
return noteOrder[(rootIndex + interval) % 12];
});
chordNotesDisplay.innerHTML = `
<strong>${rootNote} ${chordType} chord:</strong> ${chordNotes.join(' - ')}
`;
// Highlight chord notes on piano
highlightChordOnPiano(chordNotes);
}
function highlightChordOnPiano(notes) {
// Reset all keys
document.querySelectorAll('.piano-key').forEach(key => {
key.classList.remove('highlight');
});
// Highlight chord notes
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) {
// Find and select the same key in the new mode
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);
// Chord progression buttons
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}`);
// In a real app, you would play the chord progression
});
});
}
});
</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>