799 lines
26 KiB
JavaScript
799 lines
26 KiB
JavaScript
(() => {
|
|
const shapeList = document.getElementById('shape-list');
|
|
const btnAdd = document.getElementById('btn-add-shape');
|
|
const btnRestore = document.getElementById('btn-restore-shapes');
|
|
const editor = document.getElementById('shape-editor');
|
|
const nameInput = document.getElementById('shape-name-input');
|
|
const fretInputs = document.getElementById('shape-fret-inputs');
|
|
const btnEditorSave = document.getElementById('btn-shape-save');
|
|
const btnEditorCancel = document.getElementById('btn-shape-cancel');
|
|
const qualitySelect = document.getElementById('shape-quality-select');
|
|
const rootSelect = document.getElementById('shape-root-select');
|
|
const voicingRow = document.getElementById('voicing-row');
|
|
const btnSearch = document.getElementById('btn-shape-search');
|
|
const resultsContainer = document.getElementById('shapes-results');
|
|
const loadingEl = document.getElementById('shapes-loading');
|
|
const setSelect = document.getElementById('score-set-select');
|
|
|
|
const roots = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
|
let scoreSetsData = { sets: [], selected: 0 };
|
|
let shapes = [];
|
|
let selectedIndex = 0;
|
|
let editingIndex = -1;
|
|
|
|
function activeSet() {
|
|
return scoreSetsData.sets[scoreSetsData.selected];
|
|
}
|
|
|
|
function syncShapesRef() {
|
|
const set = activeSet();
|
|
shapes = set ? set.shapes : [];
|
|
}
|
|
|
|
function init() {
|
|
if (!window.go || !window.go.main || !window.go.main.App) return;
|
|
window.go.main.App.GetScoreSets().then(data => {
|
|
scoreSetsData = data;
|
|
if (!scoreSetsData.sets || scoreSetsData.sets.length === 0) {
|
|
scoreSetsData = { sets: [{ name: 'Standard Shapes', type: '', shapes: [] }], selected: 0 };
|
|
window.go.main.App.GetDefaultShapes().then(defaults => {
|
|
scoreSetsData.sets[0].shapes = defaults;
|
|
syncShapesRef();
|
|
renderSetSelector();
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
});
|
|
return;
|
|
}
|
|
syncShapesRef();
|
|
selectedIndex = 0;
|
|
renderSetSelector();
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
});
|
|
|
|
roots.forEach((r, i) => {
|
|
const opt = document.createElement('option');
|
|
opt.value = i;
|
|
opt.textContent = r;
|
|
rootSelect.appendChild(opt);
|
|
});
|
|
|
|
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 opt = document.createElement('option');
|
|
opt.value = q;
|
|
opt.textContent = q;
|
|
qualitySelect.appendChild(opt);
|
|
});
|
|
});
|
|
|
|
btnAdd.addEventListener('click', () => openEditor(-1));
|
|
btnRestore.addEventListener('click', restoreDefaults);
|
|
btnEditorSave.addEventListener('click', saveEditor);
|
|
btnEditorCancel.addEventListener('click', closeEditor);
|
|
btnSearch.addEventListener('click', doSearch);
|
|
setSelect.addEventListener('change', () => {
|
|
if (window.dbg) window.dbg('[shapes] setSelect change:', setSelect.value);
|
|
if (setSelect.value === '__editor__') {
|
|
setSelect.value = scoreSetsData.selected;
|
|
if (window.dbg) window.dbg('[shapes] opening editor, sets:', scoreSetsData.sets.length);
|
|
window.switchToView('sets-editor');
|
|
if (window.initSetsEditor) window.initSetsEditor();
|
|
return;
|
|
}
|
|
switchSet(parseInt(setSelect.value));
|
|
});
|
|
|
|
resultsContainer.addEventListener('click', (e) => {
|
|
const item = e.target.closest('[data-chord-midi]');
|
|
if (!item || !window.playChord) return;
|
|
window.playChord(JSON.parse(item.dataset.chordMidi));
|
|
});
|
|
}
|
|
|
|
function isDensitySet() {
|
|
const set = activeSet();
|
|
return set && set.type === 'density';
|
|
}
|
|
|
|
function renderSetSelector() {
|
|
setSelect.innerHTML = '';
|
|
scoreSetsData.sets.forEach((set, i) => {
|
|
const opt = document.createElement('option');
|
|
opt.value = i;
|
|
opt.textContent = set.name;
|
|
if (i === scoreSetsData.selected) opt.selected = true;
|
|
setSelect.appendChild(opt);
|
|
});
|
|
const sep = document.createElement('option');
|
|
sep.disabled = true;
|
|
sep.textContent = '───';
|
|
setSelect.appendChild(sep);
|
|
const editorOpt = document.createElement('option');
|
|
editorOpt.value = '__editor__';
|
|
editorOpt.textContent = 'Open Sets Editor';
|
|
setSelect.appendChild(editorOpt);
|
|
}
|
|
|
|
function switchSet(i) {
|
|
if (i < 0 || i >= scoreSetsData.sets.length) return;
|
|
scoreSetsData.selected = i;
|
|
syncShapesRef();
|
|
selectedIndex = 0;
|
|
closeEditor();
|
|
renderSetSelector();
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
persistSets();
|
|
}
|
|
|
|
function persistSets() {
|
|
if (!window.go) return;
|
|
const set = activeSet();
|
|
if (set) set.shapes = shapes;
|
|
window.go.main.App.SaveScoreSets(scoreSetsData);
|
|
}
|
|
|
|
function renderShapeList() {
|
|
shapeList.innerHTML = '';
|
|
shapes.forEach((s, i) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'shape-item' + (i === selectedIndex ? ' selected' : '');
|
|
|
|
const name = document.createElement('span');
|
|
name.className = 'shape-name';
|
|
name.textContent = s.name;
|
|
|
|
const frets = document.createElement('span');
|
|
frets.className = 'shape-frets';
|
|
frets.textContent = s.frets.map(f => f === -1 ? 'x' : f).join(' ');
|
|
|
|
const actions = document.createElement('span');
|
|
actions.className = 'shape-actions';
|
|
|
|
if (i > 0) {
|
|
const upBtn = document.createElement('button');
|
|
upBtn.textContent = '\u25B2';
|
|
upBtn.title = 'Move up';
|
|
upBtn.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
[shapes[i - 1], shapes[i]] = [shapes[i], shapes[i - 1]];
|
|
if (selectedIndex === i) selectedIndex = i - 1;
|
|
else if (selectedIndex === i - 1) selectedIndex = i;
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
persistSets();
|
|
});
|
|
actions.appendChild(upBtn);
|
|
}
|
|
|
|
if (i < shapes.length - 1) {
|
|
const downBtn = document.createElement('button');
|
|
downBtn.textContent = '\u25BC';
|
|
downBtn.title = 'Move down';
|
|
downBtn.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
[shapes[i], shapes[i + 1]] = [shapes[i + 1], shapes[i]];
|
|
if (selectedIndex === i) selectedIndex = i + 1;
|
|
else if (selectedIndex === i + 1) selectedIndex = i;
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
persistSets();
|
|
});
|
|
actions.appendChild(downBtn);
|
|
}
|
|
|
|
const editBtn = document.createElement('button');
|
|
editBtn.textContent = 'edit';
|
|
editBtn.addEventListener('click', e => { e.stopPropagation(); openEditor(i); });
|
|
|
|
const delBtn = document.createElement('button');
|
|
delBtn.textContent = 'del';
|
|
delBtn.addEventListener('click', e => {
|
|
e.stopPropagation();
|
|
shapes.splice(i, 1);
|
|
if (selectedIndex >= shapes.length) selectedIndex = Math.max(0, shapes.length - 1);
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
persistSets();
|
|
});
|
|
|
|
actions.appendChild(editBtn);
|
|
actions.appendChild(delBtn);
|
|
|
|
item.appendChild(name);
|
|
item.appendChild(frets);
|
|
item.appendChild(actions);
|
|
|
|
item.addEventListener('click', () => {
|
|
selectedIndex = i;
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
});
|
|
|
|
shapeList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function buildVoicingInputs() {
|
|
voicingRow.innerHTML = '';
|
|
const shape = shapes[selectedIndex];
|
|
if (!shape) return;
|
|
|
|
const labels = ['1','2','3','4','5','6'];
|
|
for (let i = 0; i < 6; i++) {
|
|
const inp = document.createElement('input');
|
|
inp.type = 'text';
|
|
inp.placeholder = labels[i];
|
|
inp.dataset.string = i;
|
|
if (shape.frets[i] === -1) {
|
|
inp.disabled = true;
|
|
inp.value = '';
|
|
inp.placeholder = 'x';
|
|
}
|
|
voicingRow.appendChild(inp);
|
|
}
|
|
}
|
|
|
|
function openEditor(idx) {
|
|
editingIndex = idx;
|
|
editor.style.display = '';
|
|
fretInputs.innerHTML = '';
|
|
|
|
if (idx >= 0) {
|
|
nameInput.value = shapes[idx].name;
|
|
for (let i = 0; i < 6; i++) {
|
|
const inp = document.createElement('input');
|
|
inp.type = 'text';
|
|
inp.value = shapes[idx].frets[i] === -1 ? 'x' : shapes[idx].frets[i];
|
|
fretInputs.appendChild(inp);
|
|
}
|
|
} else {
|
|
nameInput.value = '';
|
|
for (let i = 0; i < 6; i++) {
|
|
const inp = document.createElement('input');
|
|
inp.type = 'text';
|
|
inp.value = '0';
|
|
fretInputs.appendChild(inp);
|
|
}
|
|
}
|
|
}
|
|
|
|
function closeEditor() {
|
|
editor.style.display = 'none';
|
|
editingIndex = -1;
|
|
}
|
|
|
|
function saveEditor() {
|
|
const name = nameInput.value.trim();
|
|
if (!name) return;
|
|
|
|
const inputs = fretInputs.querySelectorAll('input');
|
|
const frets = [];
|
|
for (const inp of inputs) {
|
|
const v = inp.value.trim().toLowerCase();
|
|
if (v === 'x' || v === '-1') {
|
|
frets.push(-1);
|
|
} else {
|
|
const n = parseInt(v);
|
|
if (isNaN(n) || n < 0) {
|
|
frets.push(0);
|
|
} else {
|
|
frets.push(n);
|
|
}
|
|
}
|
|
}
|
|
|
|
const shape = { name: name, frets: frets };
|
|
if (editingIndex >= 0) {
|
|
shapes[editingIndex] = shape;
|
|
} else {
|
|
shapes.push(shape);
|
|
selectedIndex = shapes.length - 1;
|
|
}
|
|
|
|
closeEditor();
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
persistSets();
|
|
}
|
|
|
|
function restoreDefaults() {
|
|
if (!window.go) return;
|
|
window.go.main.App.GetDefaultShapes().then(defaults => {
|
|
shapes.length = 0;
|
|
defaults.forEach(s => shapes.push(s));
|
|
const set = activeSet();
|
|
if (set) set.shapes = shapes;
|
|
selectedIndex = 0;
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
persistSets();
|
|
});
|
|
}
|
|
|
|
function doSearch() {
|
|
if (!window.go) return;
|
|
const shape = shapes[selectedIndex];
|
|
if (!shape) return;
|
|
|
|
const voicingInputs = voicingRow.querySelectorAll('input');
|
|
const voicing = [];
|
|
voicingInputs.forEach(inp => {
|
|
voicing.push(inp.disabled ? '' : inp.value.trim());
|
|
});
|
|
|
|
const query = {
|
|
shape: shape,
|
|
target_quality: qualitySelect.value,
|
|
target_root: parseInt(rootSelect.value),
|
|
voicing: voicing,
|
|
base_tuning: window.currentTuningMIDI ? window.currentTuningMIDI.slice() : [],
|
|
baseline_shift: parseInt(document.getElementById('baseline-shift').value) || 0,
|
|
range_down: parseInt(document.getElementById('range-down').value) || 0,
|
|
range_up: parseInt(document.getElementById('range-up').value) || 0,
|
|
};
|
|
|
|
loadingEl.style.display = '';
|
|
loadingEl.textContent = 'Searching tunings...';
|
|
resultsContainer.innerHTML = '';
|
|
|
|
if (isDensitySet()) {
|
|
window.go.main.App.FindDensityTunings(query).then(results => {
|
|
loadingEl.style.display = 'none';
|
|
renderDensityResults(results || []);
|
|
}).catch(err => {
|
|
loadingEl.style.display = 'none';
|
|
loadingEl.style.display = '';
|
|
loadingEl.textContent = 'Error: ' + err;
|
|
});
|
|
} else {
|
|
window.go.main.App.FindShapeTunings(query, shapes).then(results => {
|
|
loadingEl.style.display = 'none';
|
|
renderResults(results || [], shape.name);
|
|
}).catch(err => {
|
|
loadingEl.style.display = 'none';
|
|
loadingEl.style.display = '';
|
|
loadingEl.textContent = 'Error: ' + err;
|
|
});
|
|
}
|
|
}
|
|
|
|
const PAGE_SIZE = 10;
|
|
|
|
function renderResults(results, searchShapeName) {
|
|
resultsContainer.innerHTML = '';
|
|
|
|
if (results.length === 0) {
|
|
const msg = document.createElement('div');
|
|
msg.className = 'loading';
|
|
msg.textContent = 'No tunings found.';
|
|
resultsContainer.appendChild(msg);
|
|
return;
|
|
}
|
|
|
|
const compat = results.filter(r => r.high_compat).length;
|
|
const summary = document.createElement('div');
|
|
summary.style.cssText = 'font-size:0.8rem;color:#888;margin-bottom:0.75rem;';
|
|
summary.textContent = results.length.toLocaleString() + ' possible tuning' + (results.length !== 1 ? 's' : '') + ' found'
|
|
+ (compat ? ' \u2014 ' + compat.toLocaleString() + ' highly compatible' : '');
|
|
resultsContainer.appendChild(summary);
|
|
|
|
const listEl = document.createElement('div');
|
|
resultsContainer.appendChild(listEl);
|
|
|
|
let shown = 0;
|
|
let loadMoreBtn = null;
|
|
|
|
function renderPage() {
|
|
const end = Math.min(shown + PAGE_SIZE, results.length);
|
|
for (let idx = shown; idx < end; idx++) {
|
|
listEl.appendChild(buildTuningCard(results[idx], searchShapeName));
|
|
}
|
|
shown = end;
|
|
|
|
if (shown < results.length) {
|
|
if (!loadMoreBtn) {
|
|
loadMoreBtn = document.createElement('button');
|
|
loadMoreBtn.className = 'btn-load-more';
|
|
loadMoreBtn.addEventListener('click', renderPage);
|
|
resultsContainer.appendChild(loadMoreBtn);
|
|
}
|
|
loadMoreBtn.textContent = 'Load more (' + (results.length - shown) + ' remaining)';
|
|
} else if (loadMoreBtn) {
|
|
loadMoreBtn.remove();
|
|
loadMoreBtn = null;
|
|
}
|
|
}
|
|
|
|
renderPage();
|
|
}
|
|
|
|
function buildTuningCard(tc, searchShapeName) {
|
|
const card = document.createElement('div');
|
|
card.className = 'tuning-card';
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'tuning-card-header';
|
|
|
|
const h3 = document.createElement('h3');
|
|
h3.textContent = tc.chord;
|
|
|
|
if (tc.high_compat) {
|
|
card.classList.add('high-compat');
|
|
const star = document.createElement('span');
|
|
star.className = 'compat-star';
|
|
star.textContent = '\u2605';
|
|
star.title = 'Highly compatible: ' + tc.maj_min_count + ' major/minor triads';
|
|
h3.appendChild(star);
|
|
}
|
|
|
|
const stats = document.createElement('span');
|
|
stats.className = 'tuning-stats';
|
|
stats.textContent = tc.valid_chords + '/' + (tc.companions ? tc.companions.length : 0) + ' chords';
|
|
|
|
const notes = document.createElement('span');
|
|
notes.className = 'tuning-notes';
|
|
notes.textContent = tc.tuning.join(' ');
|
|
|
|
const arrow = document.createElement('span');
|
|
arrow.className = 'expand-icon';
|
|
arrow.textContent = '\u25B6';
|
|
|
|
header.appendChild(h3);
|
|
header.appendChild(stats);
|
|
header.appendChild(notes);
|
|
header.appendChild(arrow);
|
|
|
|
header.addEventListener('click', () => {
|
|
card.classList.toggle('expanded');
|
|
});
|
|
|
|
const body = document.createElement('div');
|
|
body.className = 'tuning-card-body';
|
|
|
|
if (tc.companions && tc.companions.length > 0) {
|
|
const grid = document.createElement('div');
|
|
grid.className = 'companion-grid';
|
|
|
|
tc.companions.forEach(comp => {
|
|
const item = document.createElement('div');
|
|
item.className = 'companion-item';
|
|
|
|
const label = document.createElement('div');
|
|
label.className = 'companion-label';
|
|
label.textContent = comp.shape;
|
|
if (comp.shape === searchShapeName) {
|
|
label.style.color = '#1dc9fe';
|
|
}
|
|
|
|
const chord = document.createElement('div');
|
|
chord.className = 'companion-chord';
|
|
chord.textContent = comp.chord;
|
|
|
|
item.appendChild(label);
|
|
item.appendChild(chord);
|
|
|
|
const shapeDef = shapes.find(s => s.name === comp.shape);
|
|
if (shapeDef) {
|
|
const fingering = shapeDef.frets.map(f => f === -1 ? 'x' : String(f));
|
|
const maxFret = Math.max(...shapeDef.frets.filter(f => f >= 0), 1);
|
|
const fb = document.createElement('div');
|
|
fb.className = 'fretboard alternative-fretboard';
|
|
renderSingleFretboard(fb, fingering, Math.max(maxFret, 4));
|
|
item.appendChild(fb);
|
|
|
|
const midi = [];
|
|
for (let si = 0; si < 6; si++) {
|
|
if (shapeDef.frets[si] !== -1 && tc.tuning_midi && tc.tuning_midi[si] != null) {
|
|
midi.push(tc.tuning_midi[si] + shapeDef.frets[si]);
|
|
}
|
|
}
|
|
if (midi.length) item.dataset.chordMidi = JSON.stringify(midi);
|
|
}
|
|
|
|
const notesDiv = document.createElement('div');
|
|
notesDiv.className = 'companion-notes';
|
|
notesDiv.textContent = (comp.notes || []).join(' ');
|
|
item.appendChild(notesDiv);
|
|
|
|
grid.appendChild(item);
|
|
});
|
|
|
|
body.appendChild(grid);
|
|
}
|
|
|
|
const setBtn = document.createElement('button');
|
|
setBtn.className = 'btn-small';
|
|
setBtn.textContent = 'Set to instrument tuning';
|
|
setBtn.style.marginTop = '0.75rem';
|
|
setBtn.addEventListener('click', () => {
|
|
if (window.setTuningFromExplorer) window.setTuningFromExplorer(tc.tuning);
|
|
});
|
|
body.appendChild(setBtn);
|
|
|
|
card.appendChild(header);
|
|
card.appendChild(body);
|
|
return card;
|
|
}
|
|
|
|
function renderDensityResults(results) {
|
|
resultsContainer.innerHTML = '';
|
|
|
|
if (results.length === 0) {
|
|
const msg = document.createElement('div');
|
|
msg.className = 'loading';
|
|
msg.textContent = 'No tunings found.';
|
|
resultsContainer.appendChild(msg);
|
|
return;
|
|
}
|
|
|
|
const explain = document.createElement('p');
|
|
explain.className = 'density-explanation';
|
|
explain.textContent = 'Tunings ranked by total playable chords within current fret/finger settings. Expand a card to see all identified chords with their simplest fingerings.';
|
|
resultsContainer.appendChild(explain);
|
|
|
|
const compat = results.filter(r => r.high_compat).length;
|
|
const summary = document.createElement('div');
|
|
summary.style.cssText = 'font-size:0.8rem;color:#888;margin-bottom:0.75rem;';
|
|
summary.textContent = results.length.toLocaleString() + ' tuning' + (results.length !== 1 ? 's' : '') + ' found'
|
|
+ (compat ? ' \u2014 ' + compat.toLocaleString() + ' highly compatible' : '');
|
|
resultsContainer.appendChild(summary);
|
|
|
|
const listEl = document.createElement('div');
|
|
resultsContainer.appendChild(listEl);
|
|
|
|
let shown = 0;
|
|
let loadMoreBtn = null;
|
|
|
|
function renderPage() {
|
|
const end = Math.min(shown + PAGE_SIZE, results.length);
|
|
for (let idx = shown; idx < end; idx++) {
|
|
listEl.appendChild(buildDensityCard(results[idx]));
|
|
}
|
|
shown = end;
|
|
|
|
if (shown < results.length) {
|
|
if (!loadMoreBtn) {
|
|
loadMoreBtn = document.createElement('button');
|
|
loadMoreBtn.className = 'btn-load-more';
|
|
loadMoreBtn.addEventListener('click', renderPage);
|
|
resultsContainer.appendChild(loadMoreBtn);
|
|
}
|
|
loadMoreBtn.textContent = 'Load more (' + (results.length - shown) + ' remaining)';
|
|
} else if (loadMoreBtn) {
|
|
loadMoreBtn.remove();
|
|
loadMoreBtn = null;
|
|
}
|
|
}
|
|
|
|
renderPage();
|
|
}
|
|
|
|
function buildDensityCard(dc) {
|
|
const card = document.createElement('div');
|
|
card.className = 'tuning-card';
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'tuning-card-header';
|
|
|
|
const h3 = document.createElement('h3');
|
|
h3.textContent = dc.chord;
|
|
|
|
if (dc.high_compat) {
|
|
card.classList.add('high-compat');
|
|
const star = document.createElement('span');
|
|
star.className = 'compat-star';
|
|
star.textContent = '\u2605';
|
|
star.title = dc.maj_min_count + ' major/minor triads';
|
|
h3.appendChild(star);
|
|
}
|
|
|
|
const stats = document.createElement('span');
|
|
stats.className = 'tuning-stats';
|
|
stats.textContent = dc.valid_chords + ' chords, avg ' + dc.avg_fingers.toFixed(1) + ' fingers';
|
|
|
|
const notes = document.createElement('span');
|
|
notes.className = 'tuning-notes';
|
|
notes.textContent = dc.tuning.join(' ');
|
|
|
|
const arrow = document.createElement('span');
|
|
arrow.className = 'expand-icon';
|
|
arrow.textContent = '\u25B6';
|
|
|
|
header.appendChild(h3);
|
|
header.appendChild(stats);
|
|
header.appendChild(notes);
|
|
header.appendChild(arrow);
|
|
|
|
header.addEventListener('click', () => {
|
|
card.classList.toggle('expanded');
|
|
});
|
|
|
|
const body = document.createElement('div');
|
|
body.className = 'tuning-card-body';
|
|
|
|
if (dc.chords && dc.chords.length > 0) {
|
|
const grid = document.createElement('div');
|
|
grid.className = 'companion-grid';
|
|
|
|
dc.chords.forEach(ch => {
|
|
const item = document.createElement('div');
|
|
item.className = 'companion-item';
|
|
|
|
const chordLabel = document.createElement('div');
|
|
chordLabel.className = 'companion-chord';
|
|
chordLabel.textContent = ch.chord;
|
|
|
|
const fingerBadge = document.createElement('span');
|
|
fingerBadge.className = 'finger-count';
|
|
fingerBadge.textContent = ch.fingers + 'f';
|
|
chordLabel.appendChild(fingerBadge);
|
|
|
|
item.appendChild(chordLabel);
|
|
|
|
const fingering = ch.frets.map(f => f === -1 ? 'x' : String(f));
|
|
const maxFretVal = Math.max(...ch.frets.filter(f => f >= 0), 1);
|
|
const fb = document.createElement('div');
|
|
fb.className = 'fretboard alternative-fretboard';
|
|
renderSingleFretboard(fb, fingering, Math.max(maxFretVal, 4));
|
|
item.appendChild(fb);
|
|
|
|
const midi = [];
|
|
for (let si = 0; si < dc.tuning_midi.length; si++) {
|
|
if (ch.frets[si] !== -1 && dc.tuning_midi[si] != null) {
|
|
midi.push(dc.tuning_midi[si] + ch.frets[si]);
|
|
}
|
|
}
|
|
if (midi.length) item.dataset.chordMidi = JSON.stringify(midi);
|
|
|
|
const notesDiv = document.createElement('div');
|
|
notesDiv.className = 'companion-notes';
|
|
notesDiv.textContent = (ch.notes || []).join(' ');
|
|
item.appendChild(notesDiv);
|
|
|
|
grid.appendChild(item);
|
|
});
|
|
|
|
body.appendChild(grid);
|
|
}
|
|
|
|
const setBtn = document.createElement('button');
|
|
setBtn.className = 'btn-small';
|
|
setBtn.textContent = 'Set to instrument tuning';
|
|
setBtn.style.marginTop = '0.75rem';
|
|
setBtn.addEventListener('click', () => {
|
|
if (window.setTuningFromExplorer) window.setTuningFromExplorer(dc.tuning);
|
|
});
|
|
body.appendChild(setBtn);
|
|
|
|
card.appendChild(header);
|
|
card.appendChild(body);
|
|
return card;
|
|
}
|
|
|
|
function renderSingleFretboard(fb, fingering, maxFret) {
|
|
const numStrings = fingering.length;
|
|
fb.innerHTML = '';
|
|
fb.style.display = 'inline-block';
|
|
|
|
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';
|
|
|
|
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 wrapper = document.createElement('div');
|
|
wrapper.className = 'fretboard';
|
|
|
|
for (let s = numStrings - 1; s >= 0; s--) {
|
|
const row = document.createElement('div');
|
|
row.className = 'fret-row';
|
|
for (let f = 1; f <= maxFret; f++) {
|
|
row.appendChild(fretMatrix[s][f - 1]);
|
|
}
|
|
wrapper.appendChild(row);
|
|
}
|
|
|
|
fb.appendChild(wrapper);
|
|
|
|
if (barreFretNum !== null) {
|
|
const barreCols = [];
|
|
for (let s = 0; s < numStrings; s++) {
|
|
if (parseInt(fingering[s]) === barreFretNum) barreCols.push(s);
|
|
}
|
|
if (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++;
|
|
}
|
|
}
|
|
if (!dotCount) return;
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
window.initShapeExplorer = init;
|
|
window.renderSingleFretboard = renderSingleFretboard;
|
|
window.getScoreSetsData = () => scoreSetsData;
|
|
window.setScoreSetsData = (data) => {
|
|
scoreSetsData = data;
|
|
syncShapesRef();
|
|
renderSetSelector();
|
|
renderShapeList();
|
|
buildVoicingInputs();
|
|
};
|
|
window.persistSets = persistSets;
|
|
})();
|