web-tuner/frontend/dist/chords.js

249 lines
7.8 KiB
JavaScript

(() => {
const container = document.getElementById('chord-container');
const rootFilters = document.getElementById('root-filters');
const qualityFilters = document.getElementById('quality-filters');
const roots = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
let activeRoots = new Set();
let activeQualities = new Set();
let allChords = [];
let maxFret = 4;
let numStrings = 6;
function initFilters() {
roots.forEach(r => {
const pill = document.createElement('button');
pill.className = 'filter-pill';
pill.textContent = r;
pill.addEventListener('click', () => {
if (activeRoots.has(r)) {
activeRoots.delete(r);
pill.classList.remove('active');
} else {
activeRoots.add(r);
pill.classList.add('active');
}
applyFilters();
});
rootFilters.appendChild(pill);
});
if (window.go && window.go.main && window.go.main.App) {
window.go.main.App.GetChordDefinitions().then(defs => {
const qualities = new Set();
for (const cat of Object.keys(defs)) {
for (const q of Object.keys(defs[cat])) {
qualities.add(q);
}
}
Array.from(qualities).sort().forEach(q => {
const pill = document.createElement('button');
pill.className = 'filter-pill';
pill.textContent = q;
pill.addEventListener('click', () => {
if (activeQualities.has(q)) {
activeQualities.delete(q);
pill.classList.remove('active');
} else {
activeQualities.add(q);
pill.classList.add('active');
}
applyFilters();
});
qualityFilters.appendChild(pill);
});
});
}
}
function applyFilters() {
const cards = container.querySelectorAll('.chord-card');
cards.forEach((card, i) => {
const chord = allChords[i];
if (!chord) return;
let show = true;
if (activeRoots.size > 0 && !activeRoots.has(chord.root)) {
show = false;
}
if (activeQualities.size > 0 && !activeQualities.has(chord.quality)) {
show = false;
}
card.style.display = show ? '' : 'none';
});
}
function buildChordCards(chords, mf, ns) {
allChords = chords;
maxFret = mf || maxFret;
numStrings = ns || numStrings;
container.innerHTML = '';
chords.forEach(match => {
const card = document.createElement('div');
card.className = 'chord-card';
const h2 = document.createElement('h2');
h2.textContent = match.chord;
card.appendChild(h2);
const fb = document.createElement('div');
fb.className = 'fretboard';
fb.dataset.fingering = JSON.stringify(match.fingering);
card.appendChild(fb);
if (match.alternatives && match.alternatives.length > 0) {
const altSection = document.createElement('div');
altSection.className = 'alternatives';
const h3 = document.createElement('h3');
h3.textContent = 'Alternatives:';
altSection.appendChild(h3);
const altContainer = document.createElement('div');
altContainer.className = 'alternatives-container';
match.alternatives.forEach(alt => {
const altFb = document.createElement('div');
altFb.className = 'fretboard alternative-fretboard';
altFb.dataset.fingering = JSON.stringify(alt);
altContainer.appendChild(altFb);
});
altSection.appendChild(altContainer);
card.appendChild(altSection);
}
container.appendChild(card);
});
renderFretboards();
applyFilters();
}
function renderFretboards() {
const fretboards = document.querySelectorAll('.fretboard');
fretboards.forEach(fb => {
const fingering = JSON.parse(fb.dataset.fingering);
const wrapper = document.createElement('div');
wrapper.className = 'fretboard';
fb.innerHTML = '';
fb.style.display = 'inline-block';
fb.style.marginBottom = '1rem';
const fretCounts = {};
fingering.forEach(f => {
if (!isNaN(f)) fretCounts[f] = (fretCounts[f] || 0) + 1;
});
const entries = Object.entries(fretCounts)
.filter(([, count]) => count >= 2)
.map(([f]) => parseInt(f));
let barreFretNum = null;
for (const f of entries.sort((a, b) => a - b)) {
if (fingering.every(x => x === 'x' || isNaN(x) || parseInt(x) >= f)) {
barreFretNum = f;
break;
}
}
const fretMatrix = [];
for (let s = 0; s < numStrings; s++) {
const stringRow = [];
for (let f = 1; f <= maxFret; f++) {
const fret = document.createElement('div');
fret.className = 'fret';
fret.dataset.row = s;
fret.dataset.col = f;
const fretValue = fingering[s];
const numericFret = parseInt(fretValue, 10);
if (fretValue === 'x' && f === 1) {
fret.setAttribute('muted', '');
fret.textContent = 'x';
} else if (fretValue !== 'x' && numericFret === f) {
fret.dataset.dot = 'true';
if (barreFretNum !== null && numericFret === barreFretNum) {
fret.classList.add('barre');
}
}
stringRow.push(fret);
}
fretMatrix.push(stringRow);
}
const barreCols = [];
if (barreFretNum !== null) {
for (let s = 0; s < numStrings; s++) {
if (parseInt(fingering[s]) === barreFretNum) barreCols.push(s);
}
}
for (let s = numStrings - 1; s >= 0; s--) {
const stringRow = document.createElement('div');
stringRow.className = 'fret-row';
for (let f = 1; f <= maxFret; f++) {
stringRow.appendChild(fretMatrix[s][f - 1]);
}
wrapper.appendChild(stringRow);
}
fb.appendChild(wrapper);
if (barreFretNum !== null && barreCols.length >= 2) {
const start = Math.min(...barreCols);
const end = Math.max(...barreCols);
const line = document.createElement('div');
line.className = 'barre-line';
requestAnimationFrame(() => {
let totalDotCenter = 0;
let dotCount = 0;
for (let s = start; s <= end; s++) {
const dotFret = fretMatrix[s][barreFretNum - 1];
if (dotFret) {
const rect = dotFret.getBoundingClientRect();
totalDotCenter += (rect.left + rect.right) / 2;
dotCount++;
}
}
const avgDotCenter = totalDotCenter / dotCount;
const parentRect = wrapper.getBoundingClientRect();
const dotCenter = avgDotCenter - parentRect.left;
const firstDot = fretMatrix[start][barreFretNum - 1];
const lastDot = fretMatrix[end][barreFretNum - 1];
const rect1 = firstDot.getBoundingClientRect();
const rect2 = lastDot.getBoundingClientRect();
const top = Math.min(rect1.top, rect2.top) - parentRect.top;
const bottom = Math.max(rect1.bottom, rect2.bottom) - parentRect.top;
const height = bottom - top;
line.style.top = Math.round(top) + 'px';
line.style.height = Math.round(height) + 'px';
line.style.left = Math.round(dotCenter) + 'px';
line.textContent = '|';
wrapper.appendChild(line);
});
}
});
}
container.addEventListener('click', (e) => {
const fb = e.target.closest('[data-fingering]');
if (!fb || !window.playChord || !window.currentTuningMIDI) return;
const fing = JSON.parse(fb.dataset.fingering);
const midi = [];
fing.forEach((f, i) => {
if (f === 'x') return;
midi.push(window.currentTuningMIDI[i] + parseInt(f));
});
if (midi.length) window.playChord(midi);
});
window.buildChordCards = buildChordCards;
initFilters();
})();