-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
<script>
let osmd = null;
document.getElementById('file-input').onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (ev) => {
if (!osmd) {
osmd = new opensheetmusicdisplay.OpenSheetMusicDisplay("score-container", {
autoResize: true, backend: "svg", drawFingerings: false
});
}
await osmd.load(ev.target.result);
osmd.render();
setTimeout(() => {
document.getElementById('finger-layer').style.height = document.getElementById('score-container').offsetHeight + "px";
document.getElementById('status').innerText = "準備完了。";
}, 1000);
};
reader.readAsText(file);
};
document.getElementById('scan-btn').onclick = function() {
const score = document.getElementById('score-container');
const svgLayer = document.getElementById('finger-layer');
const scoreRect = score.getBoundingClientRect();
const viewTop = window.scrollY;
const viewBottom = viewTop + window.innerHeight;
svgLayer.innerHTML = "";
const allPaths = Array.from(score.querySelectorAll("path"));
const allGraphics = Array.from(score.querySelectorAll("path, rect"))
.sort((a, b) => a.getBoundingClientRect().left - b.getBoundingClientRect().left);
const clefs = allPaths.filter(p => {
const r = p.getBoundingClientRect();
return r.height > 20 && r.height < 70 && r.width > 15 && r.width < 55;
}).map(c => {
const r = c.getBoundingClientRect();
return { top: r.top + window.scrollY, height: r.height, left: r.left };
}).filter((clef, index, self) =>
index === self.findIndex((t) => Math.abs(t.top - clef.top) < 5)
);
const heads = Array.from(score.querySelectorAll(".vf-notehead, ellipse, path")).filter(el => {
const r = el.getBoundingClientRect();
const isCorrectSize = r.width > 8 && r.width < 22 && r.height > 5 && r.height < 18;
const isNoteClass = el.classList.contains('vf-notehead') || el.tagName === 'ellipse';
const isInsideNote = el.closest('.vf-note') !== null;
return isCorrectSize && (isNoteClass || isInsideNote);
});
const accidentals = allPaths.filter(p => {
const r = p.getBoundingClientRect();
const isAccSize = r.height > 12 && r.height < 45 && r.width > 4 && r.width < 25;
if (!isAccSize) return false;
const ratio = r.height / r.width;
if (ratio > 3.36 && ratio < 3.42) return false;
const pTop = r.top + window.scrollY;
let isOverlapNoteX = false;
const targetClef = clefs.reduce((prev, curr) =>
Math.abs(curr.top - pTop) < Math.abs(prev.top - pTop) ? curr : prev
, clefs[0]);
if (targetClef) {
heads.forEach(h => {
const hRect = h.getBoundingClientRect();
const hTop = hRect.top + window.scrollY;
const isSameStaff = Math.abs(hTop - targetClef.top) < 60;
if (isSameStaff) {
const overlapX = r.left < hRect.right - 1 && r.right > hRect.left + 1;
if (overlapX) isOverlapNoteX = true;
}
});
}
return !isOverlapNoteX;
});
const activeAccidentals = [];
let accidentalCounter = 1;
// --- ダブルシャープの検出(グループ化された要素を探す) ---
const allGroups = Array.from(score.querySelectorAll("g"));
const doubleSharpCandidates = [];
allGroups.forEach(g => {
const childPaths = Array.from(g.querySelectorAll("path"));
if (childPaths.length >= 2 && childPaths.length <= 4) {
const gRect = g.getBoundingClientRect();
if (gRect.width > 8 && gRect.width < 25 && gRect.height > 8 && gRect.height < 25) {
const ratio = gRect.height / gRect.width;
// ダブルシャープは正方形に近い(比率0.8〜1.2程度)
if (ratio > 0.8 && ratio < 1.3) {
doubleSharpCandidates.push({
element: g,
rect: gRect,
type: "doublesharp"
});
// デバッグ表示
const debugBox = document.createElementNS("http://www.w3.org/2000/svg", "rect");
debugBox.setAttribute("x", gRect.left - scoreRect.left);
debugBox.setAttribute("y", gRect.top - scoreRect.top);
debugBox.setAttribute("width", gRect.width);
debugBox.setAttribute("height", gRect.height);
debugBox.setAttribute("fill", "none");
debugBox.setAttribute("stroke", "orange");
debugBox.setAttribute("stroke-width", "2");
svgLayer.appendChild(debugBox);
const ratioTxt = document.createElementNS("http://www.w3.org/2000/svg", "text");
ratioTxt.setAttribute("x", gRect.left - scoreRect.left);
ratioTxt.setAttribute("y", gRect.top - scoreRect.top - 2);
ratioTxt.setAttribute("fill", "orange");
ratioTxt.setAttribute("font-size", "10px");
ratioTxt.textContent = `DS比:${ratio.toFixed(2)}`;
svgLayer.appendChild(ratioTxt);
}
}
}
});
// 1. 初期の運指計算(臨時記号優先)
heads.forEach(head => {
const r = head.getBoundingClientRect();
const noteTop = r.top + window.scrollY;
if (noteTop < viewTop || noteTop > viewBottom) return;
const myClef = clefs.reduce((prev, curr) => Math.abs(curr.top - noteTop) < Math.abs(prev.top - noteTop) ? curr : prev, clefs[0]);
if (!myClef) return;
let accType = "none";
let searchAreaH = 12;
let searchAreaY = (r.top + (r.height / 2)) - (searchAreaH / 2) - scoreRect.top;
accidentals.forEach(acc => {
const aRect = acc.getBoundingClientRect();
const isLeftNear = (r.left - aRect.right > -2) && (r.left - aRect.right < 10);
const accCenterY = aRect.top - scoreRect.top + (aRect.height / 2);
const isNearY = Math.abs(accCenterY - (searchAreaY + searchAreaH/2)) < (searchAreaH / 2 + 2);
if (isLeftNear && isNearY) {
const ratio = aRect.height / aRect.width;
let currentAcc = "none";
// ダブルシャープチェック(音符の近くにダブルシャープ候補があるか)
const nearDoubleSharp = doubleSharpCandidates.find(ds => {
const dsRect = ds.rect;
const isNearX = Math.abs(dsRect.left - aRect.left) < 3;
const isNearY = Math.abs(dsRect.top - aRect.top) < 3;
return isNearX && isNearY;
});
if (nearDoubleSharp) currentAcc = "doublesharp";
else if (ratio >= 3.45) currentAcc = "natural";
else if (ratio >= 3.25 && ratio <= 3.44) currentAcc = "sharp";
else if (ratio >= 2.8 && ratio <= 3.24) currentAcc = "flat";
if (currentAcc !== "none") {
console.log("Detected accidental:", currentAcc, "at", aRect.left); // デバッグ用
let barlineX = aRect.right + 800;
allGraphics.forEach(p => {
const pRect = p.getBoundingClientRect();
const isVertical = pRect.height > 30 && pRect.width < 4;
const isSameStaff = Math.abs(pRect.top + window.scrollY - myClef.top) < 120;
if (isVertical && isSameStaff && pRect.left > aRect.right && pRect.left < barlineX) {
let isStem = false;
heads.forEach(h => {
const hRect = h.getBoundingClientRect();
const isOverlapX = pRect.left >= hRect.left - 2 && pRect.left <= hRect.right + 2;
const isOverlapY = hRect.bottom > pRect.top - 5 && hRect.top < pRect.bottom + 5;
if (isOverlapX && isOverlapY) isStem = true;
});
if (!isStem) barlineX = pRect.left;
}
});
activeAccidentals.push({
type: currentAcc,
left: aRect.left,
right: barlineX,
top: r.top,
id: accidentalCounter
});
const accNumTxt = document.createElementNS("http://www.w3.org/2000/svg", "text");
accNumTxt.setAttribute("x", aRect.left - scoreRect.left - 5);
accNumTxt.setAttribute("y", aRect.top - scoreRect.top - 5);
accNumTxt.setAttribute("fill", "#000");
accNumTxt.setAttribute("font-size", "14px");
accNumTxt.setAttribute("font-weight", "bold");
accNumTxt.textContent = accidentalCounter;
svgLayer.appendChild(accNumTxt);
accidentalCounter++;
}
}
});
const overlappingEffects = activeAccidentals.filter(a => r.left >= a.left && r.left <= a.right && Math.abs(r.top - a.top) < 2);
if (overlappingEffects.length > 0) accType = overlappingEffects.reduce((prev, curr) => (curr.left > prev.left) ? curr : prev).type;
const baseRatio = (noteTop - myClef.top) / myClef.height;
let finger = "?", color = "#000000";
if (baseRatio < -0.80) finger = "10"; else if (baseRatio < -0.65) finger = "9";
else if (baseRatio < -0.50) finger = "7"; else if (baseRatio < -0.35) finger = "5"; else if (baseRatio < -0.20) finger = "4"; else if (baseRatio < -0.05) finger = "2"; else if (baseRatio < 0.10) finger = "0"; else if (baseRatio < 0.28) finger = "3"; else if (baseRatio < 0.38) finger = "2"; else if (baseRatio < 0.48) finger = "0"; else if (baseRatio < 0.78) finger = "3"; else if (baseRatio < 0.88) finger = "2"; else if (baseRatio < 0.98) finger = "0"; else if (baseRatio < 1.18) finger = "3"; else if (baseRatio < 1.38) finger = "1"; else finger = "0";
if (baseRatio < 0.10) color = "#2ecc71"; else if (baseRatio < 0.48) color = "#3498db"; else if (baseRatio < 0.98) color = "#e74c3c";
if (finger !== "?" && !isNaN(finger)) {
let fNum = parseInt(finger);
if (accType === "sharp") fNum += 1;
else if (accType === "flat") {
if (fNum === 0) {
finger = "4";
if (color === "#2ecc71") color = "#3498db"; else if (color === "#3498db") color = "#e74c3c"; else if (color === "#e74c3c") color = "#000000";
} else fNum -= 1;
}
// --- [加筆] ダブルフラットの運指計算(-2) ---
else if (accType === "doubleflat") {
if (fNum === 0) {
// 開放弦から2つ下がるケース
finger = "3";
if (color === "#2ecc71") color = "#3498db";
else if (color === "#3498db") color = "#e74c3c";
else if (color === "#e74c3c") color = "#000000";
} else if (fNum === 1) {
finger = "4";
if (color === "#2ecc71") color = "#3498db";
else if (color === "#3498db") color = "#e74c3c";
else if (color === "#e74c3c") color = "#000000";
} else if (fNum >= 2) {
fNum -= 2;
finger = fNum.toString();
}
}
else if (accType === "doublesharp") {
fNum += 2; // 全音上げる
finger = fNum.toString();
}
if (accType === "natural") { /* ナチュラルの場合は補正なし(baseRatioに従う) */ }
if (finger !== "4" || accType === "sharp" || accType === "doublesharp") finger = fNum.toString();
}
const txt = document.createElementNS("http://www.w3.org/2000/svg", "text");
txt.setAttribute("x", r.left - scoreRect.left + (r.width / 2));
txt.setAttribute("y", r.top - scoreRect.top + 30);
txt.setAttribute("fill", color);
txt.setAttribute("font-size", "22px");
txt.setAttribute("font-weight", "bold");
txt.setAttribute("text-anchor", "middle");
txt.setAttribute("class", "finger-label");
txt.textContent = finger;
svgLayer.appendChild(txt);
if (overlappingEffects.length > 0) {
const refAcc = overlappingEffects.reduce((prev, curr) => (curr.left > prev.left) ? curr : prev);
const refNumTxt = document.createElementNS("http://www.w3.org/2000/svg", "text");
refNumTxt.setAttribute("x", r.left - scoreRect.left + (r.width / 2) + 12);
refNumTxt.setAttribute("y", r.top - scoreRect.top + 15);
refNumTxt.setAttribute("fill", "#7f8c8d");
refNumTxt.setAttribute("font-size", "10px");
refNumTxt.textContent = "(" + refAcc.id + ")";
svgLayer.appendChild(refNumTxt);
}
if (accType !== "none") {
const debugRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
let strokeColor = "#000";
if (accType === "doubleflat") strokeColor = "#9b59b6";
else if (accType === "flat") strokeColor = "#3498db";
else if (accType === "natural") strokeColor = "#2ecc71";
else if (accType === "sharp") strokeColor = "#e74c3c";
debugRect.setAttribute("x", r.left - scoreRect.left - 2);
debugRect.setAttribute("y", r.top - scoreRect.top - 2);
debugRect.setAttribute("width", r.width + 4);
debugRect.setAttribute("height", r.height + 4);
debugRect.setAttribute("fill", "none");
debugRect.setAttribute("stroke", strokeColor);
debugRect.setAttribute("stroke-width", "2");
svgLayer.appendChild(debugRect);
}
});
activeAccidentals.forEach(area => {
const areaRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
let areaColor = "rgba(0, 0, 0, 0.05)";
if (area.type === "doubleflat") areaColor = "rgba(155, 89, 182, 0.1)";
else if (area.type === "flat") areaColor = "rgba(52, 152, 219, 0.1)";
else if (area.type === "natural") areaColor = "rgba(46, 204, 113, 0.1)";
else if (area.type === "sharp") areaColor = "rgba(231, 76, 60, 0.1)";
areaRect.setAttribute("x", area.left - scoreRect.left);
areaRect.setAttribute("y", area.top - scoreRect.top - 10);
areaRect.setAttribute("width", area.right - area.left);
areaRect.setAttribute("height", 20);
areaRect.setAttribute("fill", areaColor);
areaRect.setAttribute("stroke", areaColor.replace("0.1", "0.5"));
areaRect.setAttribute("stroke-dasharray", "2,2");
svgLayer.appendChild(areaRect);
});
// 2. 調合判定(オクターブ・ガイド線の描画)
const extendedKeySigs = [];
clefs.forEach(c => {
const cR = score.querySelectorAll("path")[allPaths.indexOf(allPaths.find(p => p.getBoundingClientRect().left === c.left))].getBoundingClientRect();
const staffSigs = accidentals.filter(acc => {
const aR = acc.getBoundingClientRect();
return aR.left > cR.right - 5 && aR.left < cR.right + 40 && Math.abs(aR.top + window.scrollY - c.top) < 60;
});
staffSigs.forEach(sig => {
const sR = sig.getBoundingClientRect();
const ratio = sR.height / sR.width;
let type = (ratio >= 3.25 && ratio <= 3.44) ? "sharp" : (ratio >= 2.8 && ratio <= 3.24) ? "flat" : "none";
if (type !== "none") {
let yOffsetRatio = (type === "flat") ? 0.78 : 0.55;
let baseAdjustedY = sR.top - scoreRect.top + (sR.height * yOffsetRatio) - 1;
const startX = sR.left - scoreRect.left; // 符号のX座標
// 座標データ(left)を保存し、描画開始位置(startX)を指定
extendedKeySigs.push({ type, y: baseAdjustedY, clef: c, left: sR.left });
drawGuideLine(svgLayer, baseAdjustedY, scoreRect.width, "rgba(255, 0, 255, 0.8)", "1", startX);
[-1, 1].forEach(octave => {
let octY = baseAdjustedY - (c.height * 1.10 * octave);
extendedKeySigs.push({ type, y: octY, clef: c, left: sR.left });
drawGuideLine(svgLayer, octY, scoreRect.width, "rgba(255, 0, 255, 0.4)", "0.5", startX);
});
const pinkBox = document.createElementNS("http://www.w3.org/2000/svg", "rect");
pinkBox.setAttribute("x", sR.left - scoreRect.left - 2);
pinkBox.setAttribute("y", sR.top - scoreRect.top - 2);
pinkBox.setAttribute("width", sR.width + 4);
pinkBox.setAttribute("height", sR.height + 4);
pinkBox.setAttribute("fill", "none");
pinkBox.setAttribute("stroke", "#ff00ff");
pinkBox.setAttribute("stroke-width", "2");
svgLayer.appendChild(pinkBox);
}
});
});
// --- [加筆] 譜中の調号(小節線のすぐ右隣かつ直後に音符がないもの)を探し出す機能 ---
accidentals.forEach(acc => {
const aR = acc.getBoundingClientRect();
const aTop = aR.top + window.scrollY;
const myC = clefs.reduce((p, c) => Math.abs(c.top - aTop) < Math.abs(p.top - aTop) ? c : p, clefs[0]);
// 1. この記号がすでに「譜頭の調号」として処理されていないか確認(重複防止)
const isAlreadyProcessed = aR.left < myC.left + 75;
if (isAlreadyProcessed) return;
// 2. 直近の左側にある小節線を探す
let nearestBarline = null;
allGraphics.forEach(p => {
const pRect = p.getBoundingClientRect();
const isVertical = pRect.height > 30 && pRect.width < 4;
const isSameStaff = Math.abs(pRect.top + window.scrollY - myC.top) < 20;
if (isVertical && isSameStaff && pRect.left < aR.left) {
if (!nearestBarline || pRect.left > nearestBarline.left) {
nearestBarline = pRect;
}
}
});
// 3. 小節線から30px以内にあり、かつ右側(25px以内)に音符がないかチェック
if (nearestBarline && (aR.left - nearestBarline.right < 30)) {
const hasNoteImmediately = heads.some(h => {
const hR = h.getBoundingClientRect();
return hR.left > aR.left && hR.left < aR.right + 25 && Math.abs(hR.top - aR.top) < 50;
});
if (!hasNoteImmediately) {
const ratio = aR.height / aR.width;
let type = "none";
if (ratio >= 3.45) type = "natural";
else if (ratio >= 3.25 && ratio <= 3.44) type = "sharp";
else if (ratio >= 2.8 && ratio <= 3.24) type = "flat";
if (type !== "none") {
let yOffsetRatio = (type === "flat") ? 0.78 : (type === "natural" ? 0.5 : 0.55);
let baseAdjustedY = aR.top - scoreRect.top + (aR.height * yOffsetRatio) - 1;
// ガイド線リストに追加
const startX = aR.left - scoreRect.left; // 譜中調号の開始座標
extendedKeySigs.push({ type, y: baseAdjustedY, clef: myC, left: aR.left });
drawGuideLine(svgLayer, baseAdjustedY, scoreRect.width, "rgba(255, 0, 255, 0.8)", "1", startX);
// オクターブガイド線
[-1, 1].forEach(octave => {
let octY = baseAdjustedY - (myC.height * 1.10 * octave);
extendedKeySigs.push({ type, y: octY, clef: myC, left: aR.left });
drawGuideLine(svgLayer, octY, scoreRect.width, "rgba(255, 0, 255, 0.4)", "0.5", startX);
});
// デバッグ用:譜中の調号をピンク枠で囲む
const midAccBox = document.createElementNS("http://www.w3.org/2000/svg", "rect");
midAccBox.setAttribute("x", aR.left - scoreRect.left - 2);
midAccBox.setAttribute("y", aR.top - scoreRect.top - 2);
midAccBox.setAttribute("width", aR.width + 4);
midAccBox.setAttribute("height", aR.height + 4);
midAccBox.setAttribute("fill", "none");
midAccBox.setAttribute("stroke", "#ff00ff");
midAccBox.setAttribute("stroke-width", "2");
svgLayer.appendChild(midAccBox);
}
}
}
});
// 3. 仮想ガイド線に基づく調合の適用
heads.forEach(head => {
const r = head.getBoundingClientRect();
const nTopAbs = r.top + window.scrollY;
if (nTopAbs < viewTop || nTopAbs > viewBottom) return;
const myC = clefs.reduce((p, c) => Math.abs(c.top - nTopAbs) < Math.abs(p.top - nTopAbs) ? c : p, clefs[0]);
const hasLocalAcc = activeAccidentals.some(a => r.left >= a.left && r.left <= a.right && Math.abs(r.top - a.top) < 2);
if (!hasLocalAcc) {
const matchedSig = extendedKeySigs.find(s => {
// 条件1: 段が一致すること
if (s.clef.top !== myC.top) return false;
// 条件2: 【重要】音符が符号より右側にあること
if (r.left < (s.left - 2)) return false;
const nCenterY = r.top - scoreRect.top + (r.height * 0.5);
const threshold = myC.height / 30;
// 条件3: 高さが一致すること
return Math.abs(nCenterY - s.y) < threshold;
});
if (matchedSig) {
// 重複がある場合、最も右側(leftが最大)の調号を適用する
const finalSig = extendedKeySigs.filter(s => {
if (s.clef.top !== myC.top) return false;
if (r.left < (s.left - 2)) return false;
const nCenterY = r.top - scoreRect.top + (r.height * 0.5);
const threshold = myC.height / 30;
return Math.abs(nCenterY - s.y) < threshold;
}).reduce((prev, curr) => (curr.left > prev.left) ? curr : prev, matchedSig);
const relX = r.left - scoreRect.left + (r.width / 2);
const relY = r.top - scoreRect.top;
svgLayer.querySelectorAll(".finger-label").forEach(l => {
if (Math.abs(parseFloat(l.getAttribute("x")) - relX) < 1 && Math.abs(parseFloat(l.getAttribute("y")) - (relY + 30)) < 1) l.remove();
});
// --- 運指計算の開始 ---
const baseRatio = (nTopAbs - myC.top) / myC.height;
let finger = "0", color = "#000000";
// ガイド線の重複チェック
const matchedSigsCount = extendedKeySigs.filter(s => {
if (s.clef.top !== myC.top) return false;
if (r.left < (s.left - 2)) return false;
const nCenterY = r.top - scoreRect.top + (r.height * 0.5);
const threshold = myC.height / 30;
return Math.abs(nCenterY - s.y) < threshold;
}).length;
// 重複がある場合は最新(最も右側にある符号)を優先、なければ通常計算
if (matchedSigsCount >= 2) {
// 合致したガイド線のうち、開始位置(left)が最大のものを取得
const latestSig = extendedKeySigs.filter(s => {
if (s.clef.top !== myC.top) return false;
if (r.left < (s.left - 2)) return false;
const nCenterY = r.top - scoreRect.top + (r.height * 0.5);
const threshold = myC.height / 30;
return Math.abs(nCenterY - s.y) < threshold;
}).reduce((prev, current) => (prev.left > current.left) ? prev : current);
// 最新の調号タイプ(sharp/flat/natural)に基づいてfingerを決定(ここでは仮に計算ロジックを再適用)
// 実際には matchedSig が最新の latestSig を指すように調整
if (baseRatio < -0.80) finger = "10"; else if (baseRatio < -0.65) finger = "9"; else if (baseRatio < -0.50) finger = "7"; else if (baseRatio < -0.35) finger = "5"; else if (baseRatio < -0.20) finger = "4"; else if (baseRatio < -0.05) finger = "2"; else if (baseRatio < 0.10) finger = "0"; else if (baseRatio < 0.28) finger = "3"; else if (baseRatio < 0.38) finger = "2"; else if (baseRatio < 0.48) finger = "0"; else if (baseRatio < 0.78) finger = "3"; else if (baseRatio < 0.88) finger = "2"; else if (baseRatio < 0.98) finger = "0"; else if (baseRatio < 1.18) finger = "3"; else if (baseRatio < 1.38) finger = "1"; else finger = "0";
} else {
if (baseRatio < -0.80) finger = "10"; else if (baseRatio < -0.65) finger = "9"; else if (baseRatio < -0.50) finger = "7"; else if (baseRatio < -0.35) finger = "5"; else if (baseRatio < -0.20) finger = "4"; else if (baseRatio < -0.05) finger = "2"; else if (baseRatio < 0.10) finger = "0"; else if (baseRatio < 0.28) finger = "3"; else if (baseRatio < 0.38) finger = "2"; else if (baseRatio < 0.48) finger = "0"; else if (baseRatio < 0.78) finger = "3"; else if (baseRatio < 0.88) finger = "2"; else if (baseRatio < 0.98) finger = "0"; else if (baseRatio < 1.18) finger = "3"; else if (baseRatio < 1.38) finger = "1"; else finger = "0";
}
if (baseRatio < 0.10) color = "#2ecc71"; else if (baseRatio < 0.48) color = "#3498db"; else if (baseRatio < 0.98) color = "#e74c3c";
let fNum = parseInt(finger);
if (finalSig.type === "sharp") fNum += 1;
else if (finalSig.type === "flat") {if (fNum === 0) { finger = "4"; if (color === "#2ecc71") color = "#3498db"; else if (color === "#3498db") color = "#e74c3c"; else if (color === "#e74c3c") color = "#000000"; } else fNum -= 1;
}
else if (finalSig.type === "doubleflat") {
if (fNum === 0) {
finger = "3";
if (color === "#2ecc71") color = "#3498db";
else if (color === "#3498db") color = "#e74c3c";
else if (color === "#e74c3c") color = "#000000";
} else if (fNum === 1) {
finger = "4";
if (color === "#2ecc71") color = "#3498db";
else if (color === "#3498db") color = "#e74c3c";
else if (color === "#e74c3c") color = "#000000";
} else if (fNum >= 2) {
fNum -= 2;
finger = fNum.toString();
}
}
if (color !== "#2ecc71") {
if (finger !== "4" || matchedSig.type === "sharp") finger = fNum.toString();
} else {
finger = fNum.toString();
}
const txt = document.createElementNS("http://www.w3.org/2000/svg", "text");
txt.setAttribute("x", relX); txt.setAttribute("y", relY + 30); txt.setAttribute("fill", color); txt.setAttribute("font-size", "22px"); txt.setAttribute("font-weight", "bold"); txt.setAttribute("text-anchor", "middle"); txt.setAttribute("class", "finger-label"); txt.textContent = finger;
svgLayer.appendChild(txt);
const pinkRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
pinkRect.setAttribute("x", r.left - scoreRect.left - 2);
pinkRect.setAttribute("y", r.top - scoreRect.top - 2);
pinkRect.setAttribute("width", r.width + 4);
pinkRect.setAttribute("height", r.height + 4);
pinkRect.setAttribute("fill", "none");
pinkRect.setAttribute("stroke", "#ff00ff");
pinkRect.setAttribute("stroke-width", "2");
svgLayer.appendChild(pinkRect);
}
}
});
svgLayer.querySelectorAll(".finger-label").forEach(label => {
const lRect = label.getBoundingClientRect();
allPaths.forEach(p => {
const pRect = p.getBoundingClientRect();
if (pRect.height > 15 && pRect.height < 28 && pRect.width > 5 && pRect.width < 15 && lRect.left < pRect.right && lRect.right > pRect.left && Math.abs(lRect.top - pRect.top) < 60) label.remove();
});
});
};
function drawGuideLine(layer, y, width, color, opacity, startX = 0) {
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", startX);
line.setAttribute("y1", y);
line.setAttribute("x2", width);
line.setAttribute("y2", y);
line.setAttribute("stroke", color);
line.setAttribute("stroke-width", "1");
line.setAttribute("stroke-dasharray", "4,2");
line.setAttribute("opacity", opacity);
layer.appendChild(line);
}
</script>