Skip to content

contrabass finger practice #252

@Hatsuka2030

Description

@Hatsuka2030
<title>コントラバス自動運指(多重オクターブガイド線版)</title> <script src="https://unpkg.com/opensheetmusicdisplay@1.8.8/build/opensheetmusicdisplay.min.js"></script> <style> body { background: #f0f0f0; text-align: center; font-family: sans-serif; margin: 0; } .controls { background: #2c3e50; padding: 15px; position: sticky; top: 0; z-index: 1000; color: white; } #wrapper { position: relative; display: block; margin: 0 auto; background: white; width: 1000px; } #score-container { position: relative; z-index: 1; width: 100%; } #finger-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 100; pointer-events: none; background: transparent !important; } button { padding: 12px 25px; cursor: pointer; border: none; color: white; border-radius: 5px; font-weight: bold; background: #27ae60; font-size: 1.1em; } #status { font-size: 14px; margin-top: 5px; color: #bdc3c7; } </style>
★運指を表示
MusicXMLファイルを選択してください
<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>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions