Fretboard: timeline left-to-right layout, guitar icon moves smoothly
This commit is contained in:
+31
-33
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user