Fretboard: timeline left-to-right layout, guitar icon moves smoothly

This commit is contained in:
Sterling Archer committed 2026-06-14 22:24:28 -07:00
1 parent bf5475e5c0
commit fed7fe165e
1 file changed
+31 -33
+31 -33
View File
@@ -382,77 +382,75 @@ function SongPlayer({ parts, detected, listening, onStartMic }) {
useEffect(()=>()=>{ if(timerRef.current) clearTimeout(timerRef.current); },[]); useEffect(()=>()=>{ if(timerRef.current) clearTimeout(timerRef.current); },[]);
const NUM_FRETS=6, NS=6, W=320, H=260; const NS=6, H=260;
const TOP=44, BOT=H-24, LEFT=44, RIGHT=W-12; const TOP=44, BOT=H-24, LEFT=44;
const strGap=(BOT-TOP)/(NS-1); const strGap=(BOT-TOP)/(NS-1);
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
const renderFretboard = (pIdx) => { const renderFretboard = (pIdx) => {
const pNotes = (allParts[pIdx]||{}).notes||[]; const pNotes = (allParts[pIdx]||{}).notes||[];
const N = pNotes.length || 1;
const W = Math.max(320, LEFT + N*52 + 20);
const noteX = (ni) => LEFT + ni*52 + 26;
return ( return (
<svg width={W} height={H} style={{display:"block",margin:"0 auto"}}> <div style={{overflowX:"auto"}}>
<line x1={LEFT} y1={TOP-2} x2={LEFT} y2={BOT+2} stroke={C.text} strokeWidth={3}/> <svg width={W} height={H} style={{display:"block",minWidth:W}}>
{Array.from({length:NUM_FRETS+1},(_,f)=>( {/* Strings */}
<line key={f} x1={LEFT+f*fretGap} y1={TOP} x2={LEFT+f*fretGap} y2={BOT} stroke={C.fret} strokeWidth={1}/>
))}
{Array.from({length:NS},(_,s)=>( {Array.from({length:NS},(_,s)=>(
<line key={s} x1={LEFT} y1={TOP+s*strGap} x2={RIGHT} y2={TOP+s*strGap} <line key={s} x1={LEFT-20} y1={TOP+s*strGap} x2={W-10} y2={TOP+s*strGap}
stroke={C.string} strokeWidth={s===0?0.8:s===5?2:1.2}/> stroke={C.string} strokeWidth={s===0?2:s===5?0.8:1.2}/>
))} ))}
{/* String labels */}
{STRING_NAMES.map((n,s)=>( {STRING_NAMES.map((n,s)=>(
<text key={s} x={LEFT-10} y={TOP+s*strGap+4} textAnchor="middle" <text key={s} x={LEFT-26} y={TOP+s*strGap+4} textAnchor="middle"
fontSize={10} fill={C.amber} fontFamily="monospace" fontWeight="600">{n}</text> fontSize={10} fill={C.amber} fontFamily="monospace" fontWeight="600">{n}</text>
))} ))}
{Array.from({length:NUM_FRETS},(_,f)=>( {/* Vertical beat lines */}
<text key={f} x={LEFT+(f+0.5)*fretGap} y={BOT+16} textAnchor="middle" {pNotes.map((_,ni)=>(
fontSize={9} fill={C.muted} fontFamily="monospace">{f+1}</text> <line key={ni} x1={noteX(ni)} y1={TOP-8} x2={noteX(ni)} y2={BOT+8}
stroke={C.fret} strokeWidth={1} opacity={0.3}/>
))} ))}
{/* Notes - left to right timeline */}
{pNotes.map((note,ni)=>{ {pNotes.map((note,ni)=>{
const p = NOTE_FRET_MAP[note]; const p = NOTE_FRET_MAP[note];
if(!p) return null; if(!p) return null;
const isOpen = p.fret===0; const isOpen = p.fret===0;
const baseX = isOpen ? LEFT+0.5*fretGap : LEFT+(p.fret-0.5)*fretGap; const cx = noteX(ni);
const baseY = TOP+p.str*strGap; const cy = TOP+p.str*strGap;
const sameSpot = pNotes.slice(0,ni).filter(n2=>{
const q=NOTE_FRET_MAP[n2]; return q&&q.fret===p.fret&&q.str===p.str;
}).length;
const cx = baseX + sameSpot*32;
const cy = baseY;
const stateKey = pIdx+"-"+ni; const stateKey = pIdx+"-"+ni;
const state = dotStates[stateKey]; const state = dotStates[stateKey];
const isCur = active && state==="cur"; const isCur = active && state==="cur";
const isHit = state==="hit"; const isHit = state==="hit";
const isMiss = state==="miss"; const isMiss = state==="miss";
const fill = isHit?C.sage:isMiss?C.coral:isCur?C.amber:C.amber; const fill = isHit?C.sage:isMiss?C.coral:C.amber;
const opacity = isHit||isCur||isMiss?1:0.7; const opacity = isHit||isCur||isMiss?1:0.7;
return ( return (
<g key={ni}> <g key={ni}>
<circle cx={cx} cy={cy} r={18} fill={isHit?"rgba(107,143,110,0.15)":isCur?"rgba(212,168,71,0.2)":"rgba(212,168,71,0.12)"}/> <circle cx={cx} cy={cy} r={18} fill={isHit?"rgba(107,143,110,0.15)":isCur?"rgba(212,168,71,0.25)":"rgba(212,168,71,0.1)"}/>
<circle cx={cx} cy={cy} r={14} <circle cx={cx} cy={cy} r={14}
fill={isOpen?(isHit?C.sage:isCur?C.amber:"transparent"):fill} fill={isOpen?(isHit?C.sage:isCur?C.amber:"transparent"):fill}
stroke={isOpen?(isHit?C.sage:isCur?C.amber:C.amber):"none"} stroke={isOpen?(isHit?C.sage:isCur?C.amber:C.amber):"none"}
strokeWidth={isOpen?2.5:0} strokeWidth={isOpen?2.5:0}
opacity={opacity}/> opacity={opacity}/>
{!isOpen && ( {!isOpen && (
<text x={cx} y={cy+5} textAnchor="middle" fontSize={14} <text x={cx} y={cy+5} textAnchor="middle" fontSize={13}
fill={isHit?"#1A1A1F":isCur?"#1A1A1F":"#1A1A1F"}
fontWeight="900" fontFamily="monospace">
{ni+1}
</text>
)}
{isOpen && (isHit||isCur) && (
<text x={cx} y={cy+5} textAnchor="middle" fontSize={12}
fill="#1A1A1F" fontWeight="900" fontFamily="monospace"> fill="#1A1A1F" fontWeight="900" fontFamily="monospace">
0 {p.fret}
</text> </text>
)} )}
{/* Fret label below dot */}
<text x={cx} y={BOT+16} textAnchor="middle" fontSize={9}
fill={isCur?C.amber:C.fret} fontFamily="monospace">
{isOpen?"0":p.fret}
</text>
{isCur && ( {isCur && (
<text x={cx} y={cy-24} textAnchor="middle" fontSize={20}>🎸</text> <text x={cx} y={TOP-16} textAnchor="middle" fontSize={20}>🎸</text>
)} )}
</g> </g>
); );
})} })}
</svg> </svg>
</div>
); );
}; };