Lesson 2: replace text tab with fretboard SVG
This commit is contained in:
1 parent
bc6d4c12e7
commit
2a79d766fd
2 files changed
+616
-92
No files matched your search
+547
-71
@@ -35,11 +35,11 @@ function autoCorrelate(buf, sr) {
|
||||
}
|
||||
|
||||
const CHORDS = {
|
||||
Em:{ dots:[{s:4,f:2,n:"2"},{s:3,f:2,n:"3"}], open:[0,1,2,5], muted:[], label:"x x 0 2 2 0" },
|
||||
Am:{ dots:[{s:1,f:2,n:"1"},{s:2,f:2,n:"2"},{s:3,f:2,n:"3"}], open:[0,1], muted:[5], label:"x 0 2 2 1 0" },
|
||||
G: { dots:[{s:5,f:3,n:"2"},{s:0,f:3,n:"4"},{s:4,f:2,n:"1"},{s:1,f:2,n:"3"}], open:[2,3], muted:[], label:"3 2 0 0 3 3" },
|
||||
C: { dots:[{s:4,f:3,n:"3"},{s:3,f:2,n:"2"},{s:1,f:1,n:"1"}], open:[0,2], muted:[5], label:"x 3 2 0 1 0" },
|
||||
D: { dots:[{s:2,f:2,n:"1"},{s:1,f:3,n:"3"},{s:0,f:2,n:"2"}], open:[3], muted:[4,5], label:"x x 0 2 3 2" },
|
||||
Em:{ dots:[{s:1,f:2,n:"2"},{s:2,f:2,n:"3"}], open:[3,4,5], muted:[], label:"x x 0 2 2 0" },
|
||||
Am:{ dots:[{s:4,f:2,n:"1"},{s:3,f:2,n:"2"},{s:2,f:2,n:"3"}], open:[4,5], muted:[0], label:"x 0 2 2 1 0" },
|
||||
G: { dots:[{s:0,f:3,n:"2"},{s:5,f:3,n:"4"},{s:1,f:2,n:"1"},{s:4,f:2,n:"3"}], open:[2,3], muted:[], label:"3 2 0 0 3 3" },
|
||||
C: { dots:[{s:1,f:3,n:"3"},{s:2,f:2,n:"2"},{s:4,f:1,n:"1"}], open:[3,5], muted:[0], label:"x 3 2 0 1 0" },
|
||||
D: { dots:[{s:3,f:2,n:"1"},{s:4,f:3,n:"3"},{s:5,f:2,n:"2"}], open:[2], muted:[0,1], label:"x x 0 2 3 2" },
|
||||
};
|
||||
|
||||
const SONG_LIBRARY = [
|
||||
@@ -129,59 +129,84 @@ const SONG_LIBRARY = [
|
||||
function ChordDiagram({ name }) {
|
||||
const sh = CHORDS[name];
|
||||
if (!sh) return null;
|
||||
const NX=8, NG=14, NS=6, NF=4, NY=18, FH=16;
|
||||
const W = NX*2 + NG*(NS-1), H = NY + FH*NF + 8;
|
||||
|
||||
const NUM_FRETS=5, NS=6, W=320, H=200;
|
||||
const TOP=44, BOT=H-24, LEFT=44, RIGHT=W-12;
|
||||
const strGap=(BOT-TOP)/(NS-1);
|
||||
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
|
||||
const STRING_LABELS=["E","A","D","G","B","e"];
|
||||
|
||||
// Build note list from chord shape: open strings + fretted dots
|
||||
const notes = [];
|
||||
sh.open.forEach(s=>{ notes.push({str:s, fret:0, label:"o", muted:false}); });
|
||||
sh.muted.forEach(s=>{ notes.push({str:s, fret:-1, label:"x", muted:true}); });
|
||||
sh.dots.forEach(({s,f,n})=>{ notes.push({str:s, fret:f, label:n, muted:false, finger:true}); });
|
||||
|
||||
return (
|
||||
<div style={{textAlign:"center"}}>
|
||||
<div style={{color:C.amber,fontWeight:700,fontSize:16,marginBottom:8}}>{name}</div>
|
||||
<svg width={W} height={H}>
|
||||
<rect x={NX} y={NY-3} width={NG*(NS-1)} height={3} fill={C.text}/>
|
||||
{Array.from({length:NF+1},(_,f)=>(
|
||||
<line key={f} x1={NX} y1={NY+f*FH} x2={NX+NG*(NS-1)} y2={NY+f*FH} stroke={C.fret} strokeWidth={1}/>
|
||||
<div style={{background:"#0A0A0E",borderRadius:10,padding:"10px 6px",
|
||||
border:"1px solid "+C.fret,marginBottom:8}}>
|
||||
<div style={{textAlign:"center",marginBottom:6,fontWeight:700,fontSize:15,color:C.amber}}>{name}</div>
|
||||
<svg width={W} height={H} style={{display:"block",margin:"0 auto"}}>
|
||||
<line x1={LEFT} y1={TOP-2} x2={LEFT} y2={BOT+2} stroke={C.text} strokeWidth={3}/>
|
||||
{Array.from({length:NUM_FRETS+1},(_,f)=>(
|
||||
<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)=>(
|
||||
<line key={s} x1={NX+s*NG} y1={NY} x2={NX+s*NG} y2={NY+NF*FH} stroke={C.string} strokeWidth={1}/>
|
||||
<line key={s} x1={LEFT} y1={TOP+s*strGap} x2={RIGHT} y2={TOP+s*strGap}
|
||||
stroke={C.string} strokeWidth={s===0?0.8:s===5?2:1.2}/>
|
||||
))}
|
||||
{sh.dots.map(({s,f,n},i)=>{
|
||||
const cx=NX+s*NG, cy=NY+(f-0.5)*FH;
|
||||
{STRING_LABELS.map((n,s)=>(
|
||||
<text key={s} x={LEFT-10} y={TOP+s*strGap+4} textAnchor="middle"
|
||||
fontSize={10} fill={C.amber} fontFamily="monospace" fontWeight="600">{n}</text>
|
||||
))}
|
||||
{Array.from({length:NUM_FRETS},(_,f)=>(
|
||||
<text key={f} x={LEFT+(f+0.5)*fretGap} y={BOT+16} textAnchor="middle"
|
||||
fontSize={9} fill={C.muted} fontFamily="monospace">{f+1}</text>
|
||||
))}
|
||||
{sh.muted.map(s=>{
|
||||
const cy=TOP+s*strGap;
|
||||
return (
|
||||
<g key={i}>
|
||||
<circle cx={cx} cy={cy} r={6} fill={C.amber}/>
|
||||
<text x={cx} y={cy+4} textAnchor="middle" fontSize={8} fill="#1A1A1F">{n}</text>
|
||||
<g key={"m"+s}>
|
||||
<line x1={LEFT-18} y1={cy-6} x2={LEFT-8} y2={cy+6} stroke={C.coral} strokeWidth={2}/>
|
||||
<line x1={LEFT-8} y1={cy-6} x2={LEFT-18} y2={cy+6} stroke={C.coral} strokeWidth={2}/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{sh.open.map(s=>(
|
||||
<circle key={s} cx={NX+s*NG} cy={NY-9} r={4} fill="none" stroke={C.sage} strokeWidth={1.5}/>
|
||||
<circle key={"o"+s} cx={LEFT-13} cy={TOP+s*strGap} r={5}
|
||||
fill="none" stroke={C.sage} strokeWidth={2}/>
|
||||
))}
|
||||
{sh.muted.map(s=>{
|
||||
const cx=NX+s*NG;
|
||||
{sh.dots.map(({s,f,n},i)=>{
|
||||
const cx = LEFT+(f-0.5)*fretGap;
|
||||
const cy = TOP+s*strGap;
|
||||
return (
|
||||
<g key={s}>
|
||||
<line x1={cx-4} y1={NY-13} x2={cx+4} y2={NY-5} stroke={C.coral} strokeWidth={1.5}/>
|
||||
<line x1={cx+4} y1={NY-13} x2={cx-4} y2={NY-5} stroke={C.coral} strokeWidth={1.5}/>
|
||||
<g key={i}>
|
||||
<circle cx={cx} cy={cy} r={14} fill="rgba(212,168,71,0.12)"/>
|
||||
<circle cx={cx} cy={cy} r={11} fill={C.amber}/>
|
||||
<text x={cx} y={cy+5} textAnchor="middle" fontSize={13}
|
||||
fill="#1A1A1F" fontWeight="900" fontFamily="monospace">{n}</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
<div style={{fontFamily:"monospace",fontSize:10,color:C.muted,marginTop:6}}>{sh.label}</div>
|
||||
<div style={{textAlign:"center",fontFamily:"monospace",fontSize:10,color:C.muted,marginTop:4}}>{sh.label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const NOTE_FRET_MAP = {
|
||||
"E2":{str:5,fret:0},"F2":{str:5,fret:1},"F#2":{str:5,fret:2},"G2":{str:5,fret:3},
|
||||
"G#2":{str:5,fret:4},"A2":{str:4,fret:0},"A#2":{str:4,fret:1},"Bb2":{str:4,fret:1},
|
||||
"B2":{str:4,fret:2},"C3":{str:4,fret:3},"C#3":{str:4,fret:4},"D3":{str:3,fret:0},
|
||||
"D#3":{str:3,fret:1},"E3":{str:3,fret:2},"F3":{str:3,fret:3},"F#3":{str:3,fret:4},
|
||||
"G3":{str:2,fret:0},"G#3":{str:2,fret:1},"A3":{str:2,fret:2},"A#3":{str:2,fret:3},
|
||||
"Bb3":{str:2,fret:3},"B3":{str:1,fret:0},"C4":{str:1,fret:1},"C#4":{str:1,fret:2},
|
||||
"D4":{str:1,fret:3},"D#4":{str:1,fret:4},"E4":{str:0,fret:0},"F4":{str:0,fret:1},
|
||||
"F#4":{str:0,fret:2},"G4":{str:0,fret:3},"G#4":{str:0,fret:4},"A4":{str:0,fret:5},
|
||||
"E2":{str:0,fret:0},"F2":{str:0,fret:1},"F#2":{str:0,fret:2},"G2":{str:0,fret:3},
|
||||
"G#2":{str:0,fret:4},"A2":{str:1,fret:0},"A#2":{str:1,fret:1},"Bb2":{str:1,fret:1},
|
||||
"B2":{str:1,fret:2},"C3":{str:1,fret:3},"C#3":{str:1,fret:4},"D3":{str:2,fret:0},
|
||||
"D#3":{str:2,fret:1},"E3":{str:2,fret:2},"F3":{str:2,fret:3},"F#3":{str:2,fret:4},
|
||||
"G3":{str:3,fret:0},"G#3":{str:3,fret:1},"A3":{str:3,fret:2},"A#3":{str:3,fret:3},
|
||||
"Bb3":{str:3,fret:3},"B3":{str:4,fret:0},"C4":{str:4,fret:1},"C#4":{str:4,fret:2},
|
||||
"D4":{str:4,fret:3},"D#4":{str:4,fret:4},"E4":{str:5,fret:0},"F4":{str:5,fret:1},
|
||||
"F#4":{str:5,fret:2},"G4":{str:5,fret:3},"G#4":{str:5,fret:4},"A4":{str:5,fret:5},
|
||||
};
|
||||
|
||||
const STRING_NAMES = ["e","B","G","D","A","E"];
|
||||
const STRING_NAMES = ["E","A","D","G","B","e"];
|
||||
|
||||
function Tip({ children }) {
|
||||
return (
|
||||
@@ -303,10 +328,6 @@ function SongPlayer({ parts, detected, listening, onStartMic }) {
|
||||
setPartIdx(pi);
|
||||
setNoteIdx(ni);
|
||||
setDotStates(s=>({...s,[pi+"-"+ni]:"cur"}));
|
||||
timerRef.current = setTimeout(()=>{
|
||||
setDotStates(s=>({...s,[pi+"-"+ni]:"miss"}));
|
||||
advance(si+1, seq);
|
||||
}, 6000);
|
||||
},[]);
|
||||
|
||||
const start = useCallback(()=>{
|
||||
@@ -333,8 +354,8 @@ function SongPlayer({ parts, detected, listening, onStartMic }) {
|
||||
|
||||
useEffect(()=>()=>{ if(timerRef.current) clearTimeout(timerRef.current); },[]);
|
||||
|
||||
const NUM_FRETS=6, NS=6, W=320, H=240;
|
||||
const TOP=24, BOT=H-24, LEFT=44, RIGHT=W-12;
|
||||
const NUM_FRETS=6, NS=6, W=320, H=260;
|
||||
const TOP=44, BOT=H-24, LEFT=44, RIGHT=W-12;
|
||||
const strGap=(BOT-TOP)/(NS-1);
|
||||
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
|
||||
|
||||
@@ -468,41 +489,128 @@ const OPEN_NOTES = [
|
||||
{label:"G string",note:"G3"},{label:"B string",note:"B3"},{label:"High e string",note:"E4"},
|
||||
];
|
||||
|
||||
function OpenStringPractice({ detected, onTarget }) {
|
||||
function OpenStringPractice({ detected, onTarget, onComplete }) {
|
||||
const [idx, setIdx] = useState(0);
|
||||
const [done, setDone] = useState({});
|
||||
const [heard, setHeard] = useState(false);
|
||||
const [doneSet, setDoneSet] = useState({});
|
||||
const [finished, setFinished] = useState(false);
|
||||
const [kudos] = useState(()=>{
|
||||
const msgs = ["Great job!", "You nailed it!", "Awesome work!", "Sounding good!", "Perfect!", "Nice playing!"];
|
||||
return msgs[Math.floor(Math.random()*msgs.length)];
|
||||
});
|
||||
|
||||
useEffect(()=>{ onTarget(idx < OPEN_NOTES.length ? OPEN_NOTES[idx].note : null); },[idx]);
|
||||
const current = OPEN_NOTES[idx];
|
||||
|
||||
useEffect(()=>{
|
||||
if (!detected || idx >= OPEN_NOTES.length) return;
|
||||
if (detected === OPEN_NOTES[idx].note){
|
||||
setDone(d=>({...d,[idx]:true}));
|
||||
setTimeout(()=>setIdx(i=>i+1), 700);
|
||||
if (!finished) onTarget(current ? current.note : null);
|
||||
},[idx, finished]);
|
||||
|
||||
useEffect(()=>{
|
||||
if (!detected || heard || finished || !current) return;
|
||||
if (detected === current.note) setHeard(true);
|
||||
},[detected, heard, finished, current]);
|
||||
|
||||
const handleNext = () => {
|
||||
setDoneSet(d=>({...d,[idx]:true}));
|
||||
setHeard(false);
|
||||
if (idx + 1 >= OPEN_NOTES.length) {
|
||||
setFinished(true);
|
||||
onTarget(null);
|
||||
if (onComplete) onComplete();
|
||||
} else {
|
||||
setIdx(i=>i+1);
|
||||
}
|
||||
},[detected, idx]);
|
||||
};
|
||||
|
||||
const NUM_FRETS=5, NS=6, W=320, H=200;
|
||||
const TOP=44, BOT=H-24, LEFT=44, RIGHT=W-12;
|
||||
const strGap=(BOT-TOP)/(NS-1);
|
||||
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
|
||||
const STRING_LABELS=["E","A","D","G","B","e"];
|
||||
|
||||
return (
|
||||
<div style={{background:"#0E0E12",border:"1px dashed "+C.fret,borderRadius:12,padding:28,textAlign:"center",margin:"20px 0"}}>
|
||||
{idx < OPEN_NOTES.length ? (
|
||||
<div style={{margin:"20px 0"}}>
|
||||
{!finished ? (
|
||||
<>
|
||||
<div style={{fontSize:14,color:C.muted,marginBottom:6}}>Pluck this string:</div>
|
||||
<div style={{fontSize:44,fontWeight:800,color:C.amber,lineHeight:1,margin:"8px 0"}}>{OPEN_NOTES[idx].label}</div>
|
||||
<div style={{fontFamily:"monospace",fontSize:13,color:C.muted}}>Target: {OPEN_NOTES[idx].note}</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{fontSize:18,fontWeight:600,color:C.sage}}>All 6 open strings done!</div>
|
||||
<div style={{background:"#0A0A0E",borderRadius:12,padding:"16px 6px",border:"1px solid "+(heard?C.sage:C.fret),marginBottom:12,transition:"border-color 0.4s"}}>
|
||||
<div style={{textAlign:"center",marginBottom:10}}>
|
||||
<div style={{fontSize:13,color:C.muted,marginBottom:4}}>Pluck this string:</div>
|
||||
<div style={{fontSize:32,fontWeight:800,color:heard?C.sage:C.amber,lineHeight:1,transition:"color 0.3s"}}>{current.label}</div>
|
||||
{!heard && <div style={{fontSize:12,color:C.muted,marginTop:4}}>Target: {current.note}</div>}
|
||||
{heard && <div style={{fontSize:13,color:C.sage,fontWeight:600,marginTop:4}}>Heard it!</div>}
|
||||
</div>
|
||||
<svg width={W} height={H} style={{display:"block",margin:"0 auto"}}>
|
||||
<line x1={LEFT} y1={TOP-2} x2={LEFT} y2={BOT+2} stroke={C.text} strokeWidth={3}/>
|
||||
{Array.from({length:NUM_FRETS+1},(_,f)=>(
|
||||
<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)=>{
|
||||
const isTarget = s === current.note && false;
|
||||
const strIdx = NS-1-s;
|
||||
const targetStrIdx = OPEN_NOTES.indexOf(current);
|
||||
const isHighlighted = s === idx;
|
||||
return (
|
||||
<g key={s}>
|
||||
{isHighlighted && (
|
||||
<rect x={LEFT} y={TOP+s*strGap-8} width={RIGHT-LEFT} height={16}
|
||||
fill={heard?"rgba(107,143,110,0.12)":"rgba(212,168,71,0.08)"} rx={4}/>
|
||||
)}
|
||||
<div style={{display:"flex",gap:8,justifyContent:"center",flexWrap:"wrap",marginTop:16}}>
|
||||
<line x1={LEFT} y1={TOP+s*strGap} x2={RIGHT} y2={TOP+s*strGap}
|
||||
stroke={isHighlighted?(heard?C.sage:C.amber):C.string}
|
||||
strokeWidth={isHighlighted?(heard?3:2.5):(s===0?0.8:s===5?2:1.2)}
|
||||
style={{filter:isHighlighted?("drop-shadow(0 0 4px "+(heard?C.sage:C.amber)+")"):"none"}}/>
|
||||
<text x={LEFT-10} y={TOP+s*strGap+4} textAnchor="middle"
|
||||
fontSize={10} fill={isHighlighted?(heard?C.sage:C.amber):C.amber}
|
||||
fontFamily="monospace" fontWeight={isHighlighted?"800":"400"}>
|
||||
{STRING_LABELS[s]}
|
||||
</text>
|
||||
{doneSet[s] && (
|
||||
<text x={RIGHT+10} y={TOP+s*strGap+4} textAnchor="middle"
|
||||
fontSize={12} fill={C.sage}>✓</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{Array.from({length:NUM_FRETS},(_,f)=>(
|
||||
<text key={f} x={LEFT+(f+0.5)*fretGap} y={BOT+16} textAnchor="middle"
|
||||
fontSize={9} fill={C.muted} fontFamily="monospace">{f+1}</text>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{heard && (
|
||||
<button onClick={handleNext}
|
||||
style={{width:"100%",padding:"13px",borderRadius:10,border:"2px solid "+C.sage,
|
||||
background:C.sage,color:"#fff",fontFamily:"sans-serif",fontSize:15,
|
||||
fontWeight:700,cursor:"pointer",marginBottom:8}}>
|
||||
{idx + 1 >= OPEN_NOTES.length ? "All done! See how I did" : "Next string →"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div style={{display:"flex",gap:6,justifyContent:"center",flexWrap:"wrap",marginTop:8}}>
|
||||
{OPEN_NOTES.map((s,i)=>(
|
||||
<div key={i} style={{padding:"3px 10px",borderRadius:6,fontFamily:"monospace",fontSize:12,
|
||||
border:"1px solid "+(done[i]?C.sage:i===idx?C.amber:C.fret),
|
||||
color:done[i]?C.sage:i===idx?C.amber:C.muted,
|
||||
background:done[i]?"rgba(107,143,110,0.1)":i===idx?"rgba(212,168,71,0.1)":"transparent"}}>
|
||||
{s.note}
|
||||
border:"1px solid "+(doneSet[i]?C.sage:i===idx?C.amber:C.fret),
|
||||
color:doneSet[i]?C.sage:i===idx?C.amber:C.muted,
|
||||
background:doneSet[i]?"rgba(107,143,110,0.1)":i===idx?"rgba(212,168,71,0.1)":"transparent"}}>
|
||||
{doneSet[i]?"✓":s.note}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{background:"#0A0A0E",borderRadius:12,padding:28,border:"1px solid "+C.sage,textAlign:"center"}}>
|
||||
<div style={{fontSize:40,marginBottom:12}}>🎸</div>
|
||||
<div style={{fontSize:22,fontWeight:800,color:C.sage,marginBottom:8}}>{kudos}</div>
|
||||
<div style={{fontSize:14,color:C.muted}}>You just played all 6 open strings. Click Next to continue.</div>
|
||||
<div style={{display:"flex",gap:6,justifyContent:"center",flexWrap:"wrap",marginTop:16}}>
|
||||
{OPEN_NOTES.map((s,i)=>(
|
||||
<div key={i} style={{padding:"3px 10px",borderRadius:6,fontFamily:"monospace",fontSize:12,
|
||||
border:"1px solid "+C.sage,color:C.sage,background:"rgba(107,143,110,0.1)"}}>✓</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -742,11 +850,17 @@ function LessonBody({ id, detected, onTarget, listening, onStartMic }) {
|
||||
<Tip>Read left to right. 0 = open string. 2 = press 2nd fret. Numbers stacked vertically = chord.</Tip>
|
||||
</div>
|
||||
);
|
||||
const [stringsComplete, setStringsComplete] = useState(false);
|
||||
if (id === 2) return (
|
||||
<div>
|
||||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:4}}>Start with open strings. Enable the mic and the app confirms each note as you pluck.</p>
|
||||
<OpenStringPractice detected={detected} onTarget={onTarget}/>
|
||||
<Tip>Press just behind the fret wire, not on top of it.</Tip>
|
||||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:4}}>Pluck each open string one at a time. The app listens and lights it up green when it hears you - then tap Next to move on.</p>
|
||||
<OpenStringPractice detected={detected} onTarget={onTarget} onComplete={()=>setStringsComplete(true)}/>
|
||||
{!stringsComplete && <Tip>Open strings means no fingers on the frets - just pluck the string.</Tip>}
|
||||
{stringsComplete && (
|
||||
<div style={{display:"flex",gap:10,marginTop:16}}>
|
||||
<Btn onClick={()=>setStringsComplete(false)} variant="secondary">Start over</Btn>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
if (id === 3) return (
|
||||
@@ -829,8 +943,328 @@ function MicBar({ note, hz, feedback, listening, onToggle }) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const OPEN_STRINGS = [
|
||||
{note:"E2", name:"Low E", freq:82.41, string:6},
|
||||
{note:"A2", name:"A", freq:110.00, string:5},
|
||||
{note:"D3", name:"D", freq:146.83, string:4},
|
||||
{note:"G3", name:"G", freq:196.00, string:3},
|
||||
{note:"B3", name:"B", freq:246.94, string:2},
|
||||
{note:"E4", name:"High e", freq:329.63, string:1},
|
||||
];
|
||||
|
||||
function HamburgerMenu({ onTuner, onBasics, onSongSearch, onClose }) {
|
||||
return (
|
||||
<>
|
||||
<div onClick={onClose}
|
||||
style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.5)",zIndex:200}}/>
|
||||
<div style={{position:"fixed",top:0,right:0,bottom:0,width:260,background:C.bg2,
|
||||
borderLeft:"1px solid "+C.fret,zIndex:201,display:"flex",flexDirection:"column",
|
||||
boxShadow:"-8px 0 32px rgba(0,0,0,0.4)"}}>
|
||||
<div style={{padding:"18px 20px",borderBottom:"1px solid "+C.fret,
|
||||
display:"flex",alignItems:"center",justifyContent:"space-between"}}>
|
||||
<div style={{fontWeight:800,fontSize:18}}>
|
||||
<span style={{color:C.amber}}>Guitar</span><span style={{color:C.text}}>Teacher</span>
|
||||
</div>
|
||||
<button onClick={onClose}
|
||||
style={{background:"none",border:"none",color:C.muted,fontSize:22,cursor:"pointer",
|
||||
lineHeight:1,padding:4}}>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<div style={{padding:"12px 0",flex:1}}>
|
||||
{[
|
||||
{icon:"🎵", label:"Guitar Tuner", sub:"Tune before you play", action:onTuner},
|
||||
{icon:"📖", label:"Guitar Basics", sub:"8 lessons for beginners", action:onBasics},
|
||||
{icon:"🔍", label:"Song Search", sub:"70+ songs with tab", action:onSongSearch},
|
||||
].map(({icon,label,sub,action})=>(
|
||||
<button key={label} onClick={action}
|
||||
style={{width:"100%",padding:"16px 20px",background:"none",border:"none",
|
||||
cursor:"pointer",textAlign:"left",fontFamily:"sans-serif",
|
||||
display:"flex",alignItems:"center",gap:14,
|
||||
borderBottom:"1px solid "+C.fret}}>
|
||||
<span style={{fontSize:22,flexShrink:0}}>{icon}</span>
|
||||
<div>
|
||||
<div style={{fontWeight:600,fontSize:14,color:C.text}}>{label}</div>
|
||||
<div style={{fontSize:11,color:C.muted,marginTop:2}}>{sub}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{padding:"16px 20px",borderTop:"1px solid "+C.fret,fontSize:11,color:C.muted,textAlign:"center"}}>
|
||||
GuitarTeacher - Learn at your own pace
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function GuitarTuner({ onBack, onGoTuner, onGoBasics, onGoSongSearch }) {
|
||||
const [listening, setListening] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [detectedNote, setDetectedNote] = useState("");
|
||||
const [detectedFreq, setDetectedFreq] = useState(0);
|
||||
const [cents, setCents] = useState(0);
|
||||
const [closestString, setClosestString] = useState(null);
|
||||
const [lockedString, setLockedString] = useState(null);
|
||||
const [tuned, setTuned] = useState({});
|
||||
const [currentStringIdx, setCurrentStringIdx] = useState(0);
|
||||
const ctxRef = useRef(null);
|
||||
const streamRef = useRef(null);
|
||||
const rafRef = useRef(null);
|
||||
|
||||
const stopMic = useCallback(()=>{
|
||||
if(streamRef.current) streamRef.current.getTracks().forEach(t=>t.stop());
|
||||
if(rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
if(ctxRef.current) ctxRef.current.close();
|
||||
ctxRef.current=null; streamRef.current=null;
|
||||
setListening(false); setDetectedNote(""); setDetectedFreq(0); setCents(0); setClosestString(null);
|
||||
},[]);
|
||||
|
||||
const startMic = useCallback(async()=>{
|
||||
if(listening){ stopMic(); return; }
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({audio:true});
|
||||
const ctx = new (window.AudioContext||window.webkitAudioContext)();
|
||||
const analyser = ctx.createAnalyser();
|
||||
analyser.fftSize = 4096;
|
||||
ctx.createMediaStreamSource(stream).connect(analyser);
|
||||
ctxRef.current=ctx; streamRef.current=stream;
|
||||
setListening(true);
|
||||
const buf = new Float32Array(analyser.fftSize);
|
||||
const loop = ()=>{
|
||||
rafRef.current = requestAnimationFrame(loop);
|
||||
analyser.getFloatTimeDomainData(buf);
|
||||
const freq = autoCorrelate(buf, ctx.sampleRate);
|
||||
if(freq > 60 && freq < 1200){
|
||||
const midi = Math.round(12*Math.log2(freq/440)+69);
|
||||
const name = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"][midi%12];
|
||||
const oct = Math.floor(midi/12)-1;
|
||||
const exactHz = 440*Math.pow(2,(midi-69)/12);
|
||||
const c = Math.round(1200*Math.log2(freq/exactHz));
|
||||
setDetectedNote(name+oct);
|
||||
setDetectedFreq(Math.round(freq*10)/10);
|
||||
setCents(c);
|
||||
const closest = lockedString || OPEN_STRINGS.reduce((best,s)=>{
|
||||
const diff = Math.abs(freq - s.freq);
|
||||
return diff < Math.abs(freq - best.freq) ? s : best;
|
||||
}, OPEN_STRINGS[0]);
|
||||
setClosestString(closest);
|
||||
}
|
||||
};
|
||||
loop();
|
||||
} catch(e){ alert("Mic access denied. Please allow microphone access and try again."); }
|
||||
},[listening, stopMic]);
|
||||
|
||||
useEffect(()=>()=>stopMic(),[]);
|
||||
|
||||
const inTune = Math.abs(cents) <= 8;
|
||||
const slightlyOff = Math.abs(cents) <= 20;
|
||||
const needleAngle = Math.max(-45, Math.min(45, cents * 0.9));
|
||||
const needleColor = inTune ? C.sage : slightlyOff ? C.amber : C.coral;
|
||||
const tuneMsg = inTune ? "In tune!" : cents < 0 ? "Tune up" : "Tune down";
|
||||
const tuneMsgColor = inTune ? C.sage : slightlyOff ? C.amber : C.coral;
|
||||
const allTuned = Object.keys(tuned).length === OPEN_STRINGS.length;
|
||||
const activeString = lockedString || OPEN_STRINGS[currentStringIdx];
|
||||
|
||||
return (
|
||||
<div style={{background:C.bg,color:C.text,minHeight:"100vh",fontFamily:"sans-serif",display:"flex",flexDirection:"column"}}>
|
||||
<div style={{padding:"18px 32px",borderBottom:"1px solid "+C.fret,display:"flex",alignItems:"center",gap:16}}>
|
||||
<button onClick={onBack}
|
||||
style={{background:"none",border:"none",color:C.muted,fontSize:20,cursor:"pointer",padding:0,lineHeight:1}}>
|
||||
←
|
||||
</button>
|
||||
<div style={{fontWeight:800,fontSize:22}}>
|
||||
<span style={{color:C.amber}}>Guitar</span><span>Teacher</span>
|
||||
<span style={{fontWeight:600,fontSize:14,color:C.muted,marginLeft:8}}>/ Tuner</span>
|
||||
</div>
|
||||
<button onClick={()=>setMenuOpen(true)}
|
||||
style={{background:"none",border:"none",cursor:"pointer",padding:4,display:"flex",flexDirection:"column",gap:5}}>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
</button>
|
||||
</div>
|
||||
{menuOpen && <HamburgerMenu
|
||||
onTuner={()=>{setMenuOpen(false);}}
|
||||
onBasics={()=>{setMenuOpen(false);onGoBasics();}}
|
||||
onSongSearch={()=>{setMenuOpen(false);onGoSongSearch();}}
|
||||
onClose={()=>setMenuOpen(false)}
|
||||
/>}
|
||||
|
||||
<div style={{flex:1,display:"flex",alignItems:"center",justifyContent:"center",padding:"24px 20px 60px"}}>
|
||||
<div style={{maxWidth:480,width:"100%"}}>
|
||||
<div style={{textAlign:"center",marginBottom:28}}>
|
||||
<div style={{fontSize:13,color:C.muted,marginBottom:6}}>Pluck any string and tune to the closest note</div>
|
||||
</div>
|
||||
|
||||
{/* Needle meter */}
|
||||
<div style={{background:"#0A0A0E",borderRadius:16,padding:"28px 20px 20px",border:"1px solid "+C.fret,marginBottom:20,textAlign:"center"}}>
|
||||
<svg width={300} height={160} style={{display:"block",margin:"0 auto",overflow:"visible"}}>
|
||||
{/* Arc background */}
|
||||
<path d="M 30 140 A 120 120 0 0 1 270 140" fill="none" stroke={C.fret} strokeWidth={8} strokeLinecap="round"/>
|
||||
{/* Colored arc zones */}
|
||||
<path d="M 30 140 A 120 120 0 0 1 75 50" fill="none" stroke="rgba(224,92,75,0.3)" strokeWidth={8} strokeLinecap="round"/>
|
||||
<path d="M 75 50 A 120 120 0 0 1 118 22" fill="none" stroke="rgba(212,168,71,0.3)" strokeWidth={8} strokeLinecap="round"/>
|
||||
<path d="M 118 22 A 120 120 0 0 1 182 22" fill="none" stroke="rgba(107,143,110,0.5)" strokeWidth={10} strokeLinecap="round"/>
|
||||
<path d="M 182 22 A 120 120 0 0 1 225 50" fill="none" stroke="rgba(212,168,71,0.3)" strokeWidth={8} strokeLinecap="round"/>
|
||||
<path d="M 225 50 A 120 120 0 0 1 270 140" fill="none" stroke="rgba(224,92,75,0.3)" strokeWidth={8} strokeLinecap="round"/>
|
||||
{/* Center tick */}
|
||||
<line x1={150} y1={25} x2={150} y2={40} stroke={C.sage} strokeWidth={2}/>
|
||||
{/* Needle */}
|
||||
{detectedFreq > 0 && (
|
||||
<g transform={"rotate("+needleAngle+", 150, 140)"}>
|
||||
<line x1={150} y1={140} x2={150} y2={35} stroke={needleColor} strokeWidth={2.5} strokeLinecap="round"/>
|
||||
<circle cx={150} cy={140} r={6} fill={needleColor}/>
|
||||
</g>
|
||||
)}
|
||||
{/* Center dot when no signal */}
|
||||
{detectedFreq === 0 && (
|
||||
<circle cx={150} cy={140} r={5} fill={C.fret}/>
|
||||
)}
|
||||
{/* Labels */}
|
||||
<text x={18} y={155} fontSize={10} fill={C.muted} textAnchor="middle">-50</text>
|
||||
<text x={150} y={18} fontSize={10} fill={C.sage} textAnchor="middle">0</text>
|
||||
<text x={282} y={155} fontSize={10} fill={C.muted} textAnchor="middle">+50</text>
|
||||
</svg>
|
||||
|
||||
{detectedFreq > 0 ? (
|
||||
<>
|
||||
<div style={{fontSize:48,fontWeight:800,color:needleColor,lineHeight:1,margin:"8px 0 4px"}}>{detectedNote}</div>
|
||||
<div style={{fontSize:13,color:C.muted,marginBottom:6}}>{detectedFreq} Hz</div>
|
||||
<div style={{fontSize:18,fontWeight:700,color:tuneMsgColor}}>{tuneMsg}</div>
|
||||
{!inTune && (
|
||||
<div style={{fontSize:12,color:C.muted,marginTop:4}}>
|
||||
{Math.abs(cents)} cents {cents < 0 ? "flat" : "sharp"}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div style={{fontSize:15,color:C.muted,marginTop:8}}>
|
||||
{listening ? "Pluck a string..." : "Tap the mic to start"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* String selector tabs */}
|
||||
<div style={{display:"flex",gap:6,marginBottom:12,flexWrap:"wrap"}}>
|
||||
{OPEN_STRINGS.map((s,i)=>{
|
||||
const isActive = activeString && activeString.note===s.note;
|
||||
const isDone = tuned[s.note];
|
||||
return (
|
||||
<button key={s.note} onClick={()=>{setLockedString(s);setCurrentStringIdx(i);}}
|
||||
style={{flex:1,padding:"8px 4px",borderRadius:8,border:"1px solid "+(isDone?C.sage:isActive?C.amber:C.fret),
|
||||
background:isDone?"rgba(107,143,110,0.15)":isActive?"rgba(212,168,71,0.1)":"transparent",
|
||||
cursor:"pointer",fontFamily:"sans-serif",textAlign:"center",minWidth:44}}>
|
||||
<div style={{fontSize:15,fontWeight:800,color:isDone?C.sage:isActive?C.amber:C.text}}>
|
||||
{s.name==="Low E"?"E":s.name==="High e"?"e":s.name}
|
||||
</div>
|
||||
<div style={{fontSize:9,color:isDone?C.sage:C.muted,marginTop:1}}>{isDone?"✓":s.note}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Guitar string visual */}
|
||||
<div style={{background:"#0A0A0E",borderRadius:12,padding:"18px 16px",border:"1px solid "+(inTune&&detectedFreq>0?C.sage:C.fret),marginBottom:12,transition:"border-color 0.3s"}}>
|
||||
<div style={{fontSize:12,fontWeight:600,textTransform:"uppercase",letterSpacing:"1px",
|
||||
color:C.muted,marginBottom:14,textAlign:"center"}}>
|
||||
{detectedFreq>0 ? (
|
||||
<span>Plucking <span style={{color:inTune?C.sage:C.amber,fontWeight:800}}>
|
||||
{activeString.name} string
|
||||
</span> {inTune ? "- In tune!" : (cents<0?"- Tune up (too flat)":"- Tune down (too sharp)")}</span>
|
||||
) : <span>Tap a string above, then pluck it on your guitar</span>}
|
||||
</div>
|
||||
<svg width="100%" viewBox="0 0 340 130" style={{display:"block"}}>
|
||||
{OPEN_STRINGS.map((s,i)=>{
|
||||
const isActive = activeString && activeString.note===s.note;
|
||||
const x1 = 20, x2 = 320;
|
||||
const y = 18 + i*19;
|
||||
const thickness = i===0?2.5:i===1?2.2:i===2?1.8:i===3?1.4:i===4?1.1:0.9;
|
||||
const stringCol = isActive?(inTune?C.sage:C.amber):"#5A5A6A";
|
||||
const glowOpacity = isActive ? 1 : 0;
|
||||
return (
|
||||
<g key={s.note}>
|
||||
{isActive && (
|
||||
<line x1={x1} y1={y} x2={x2} y2={y}
|
||||
stroke={inTune?C.sage:C.amber} strokeWidth={thickness+6} opacity={0.15}/>
|
||||
)}
|
||||
<line x1={x1} y1={y} x2={x2} y2={y}
|
||||
stroke={stringCol} strokeWidth={thickness}
|
||||
strokeLinecap="round"
|
||||
style={{filter:isActive?("drop-shadow(0 0 4px "+(inTune?C.sage:C.amber)+")"):"none"}}/>
|
||||
<text x={8} y={y+4} textAnchor="middle" fontSize={9}
|
||||
fill={isActive?(inTune?C.sage:C.amber):C.muted}
|
||||
fontFamily="monospace" fontWeight={isActive?"800":"400"}>
|
||||
{s.name==="Low E"?"E":s.name==="High e"?"e":s.name}
|
||||
</text>
|
||||
<text x={332} y={y+4} textAnchor="middle" fontSize={9}
|
||||
fill={isActive?(inTune?C.sage:C.amber):C.muted}
|
||||
fontFamily="monospace" fontWeight={isActive?"800":"400"}>
|
||||
{s.note}
|
||||
</text>
|
||||
{isActive && (
|
||||
<>
|
||||
<circle cx={160} cy={y} r={8} fill={inTune?C.sage:C.amber} opacity={0.9}/>
|
||||
<text x={160} y={y+4} textAnchor="middle" fontSize={9}
|
||||
fill="#1A1A1F" fontWeight="900" fontFamily="monospace">
|
||||
{inTune?"✓":cents<0?"↑":"↓"}
|
||||
</text>
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
<line x1={20} y1={10} x2={20} y2={120} stroke={C.muted} strokeWidth={3} strokeLinecap="round"/>
|
||||
<line x1={320} y1={10} x2={320} y2={120} stroke={C.fret} strokeWidth={1}/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Mark tuned + Next */}
|
||||
<div style={{display:"flex",gap:10,marginBottom:12}}>
|
||||
{detectedFreq>0 && inTune && (
|
||||
<button onClick={()=>{
|
||||
setTuned(t=>({...t,[activeString.note]:true}));
|
||||
const nextIdx = currentStringIdx+1 < OPEN_STRINGS.length ? currentStringIdx+1 : currentStringIdx;
|
||||
setCurrentStringIdx(nextIdx);
|
||||
setLockedString(nextIdx!==currentStringIdx ? OPEN_STRINGS[nextIdx] : lockedString);
|
||||
}}
|
||||
style={{flex:1,padding:"11px",borderRadius:10,border:"2px solid "+C.sage,
|
||||
background:"rgba(107,143,110,0.1)",color:C.sage,fontFamily:"sans-serif",
|
||||
fontSize:14,fontWeight:700,cursor:"pointer"}}>
|
||||
Mark as tuned - Next string
|
||||
</button>
|
||||
)}
|
||||
{allTuned && (
|
||||
<button onClick={onBack}
|
||||
style={{flex:1,padding:"11px",borderRadius:10,border:"2px solid "+C.sage,
|
||||
background:C.sage,color:"#fff",fontFamily:"sans-serif",
|
||||
fontSize:14,fontWeight:700,cursor:"pointer"}}>
|
||||
All tuned! Back to menu
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mic button */}
|
||||
<button onClick={startMic}
|
||||
style={{width:"100%",padding:"14px",borderRadius:12,border:"2px solid "+(listening?C.coral:C.amber),
|
||||
background:listening?"rgba(224,92,75,0.1)":"rgba(212,168,71,0.08)",
|
||||
color:listening?C.coral:C.amber,fontFamily:"sans-serif",fontSize:15,fontWeight:700,
|
||||
cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",gap:10}}>
|
||||
<span style={{fontSize:20}}>🎙️</span>
|
||||
{listening ? "Stop Listening" : "Start Listening"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [splash, setSplash] = useState(true);
|
||||
const [showTuner, setShowTuner] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [lesson, setLesson] = useState(0);
|
||||
const [listening, setListening] = useState(false);
|
||||
const [pitchNote, setPitchNote] = useState("");
|
||||
@@ -897,12 +1331,32 @@ export default function App() {
|
||||
|
||||
const css = "*{box-sizing:border-box;margin:0;padding:0} body{background:#1A1A1F} input::placeholder{color:#9A9490} p+p{margin-top:10px}";
|
||||
|
||||
if (showTuner) return <GuitarTuner
|
||||
onBack={()=>setShowTuner(false)}
|
||||
onGoTuner={()=>{}}
|
||||
onGoBasics={()=>{setShowTuner(false);setSplash(false);setLesson(0);}}
|
||||
onGoSongSearch={()=>{setShowTuner(false);setSplash(false);setLesson(7);}}
|
||||
/>;
|
||||
|
||||
if (splash) return (
|
||||
<div style={{background:C.bg,color:C.text,minHeight:"100vh",fontFamily:"sans-serif",display:"flex",flexDirection:"column"}}>
|
||||
<style>{css}</style>
|
||||
<div style={{padding:"18px 32px",borderBottom:"1px solid "+C.fret,fontWeight:800,fontSize:22}}>
|
||||
<span style={{color:C.amber}}>Guitar</span><span>Teacher</span>
|
||||
<div style={{padding:"18px 32px",borderBottom:"1px solid "+C.fret,fontWeight:800,fontSize:22,
|
||||
display:"flex",alignItems:"center",justifyContent:"space-between"}}>
|
||||
<div><span style={{color:C.amber}}>Guitar</span><span>Teacher</span></div>
|
||||
<button onClick={()=>setMenuOpen(true)}
|
||||
style={{background:"none",border:"none",cursor:"pointer",padding:4,display:"flex",flexDirection:"column",gap:5}}>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
</button>
|
||||
</div>
|
||||
{menuOpen && <HamburgerMenu
|
||||
onTuner={()=>{setMenuOpen(false);setShowTuner(true);setSplash(false);}}
|
||||
onBasics={()=>{setMenuOpen(false);setShowTuner(false);setSplash(false);go(0);}}
|
||||
onSongSearch={()=>{setMenuOpen(false);setShowTuner(false);setSplash(false);go(7);}}
|
||||
onClose={()=>setMenuOpen(false)}
|
||||
/>}
|
||||
<div style={{flex:1,display:"flex",alignItems:"center",justifyContent:"center",padding:"32px 20px 80px"}}>
|
||||
<div style={{maxWidth:520,width:"100%",textAlign:"center"}}>
|
||||
<div style={{fontSize:50,marginBottom:14}}>🎸</div>
|
||||
@@ -912,7 +1366,15 @@ export default function App() {
|
||||
<p style={{fontSize:15,color:"#C8C5BE",lineHeight:1.7,maxWidth:400,margin:"0 auto 32px"}}>
|
||||
Start from the very beginning or jump straight to playing songs you love.
|
||||
</p>
|
||||
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:14,marginBottom:20}}>
|
||||
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:14,marginBottom:12}}>
|
||||
<button onClick={()=>setShowTuner(true)}
|
||||
style={{background:C.bg2,border:"1px solid "+C.sage,borderRadius:14,padding:"22px 16px",
|
||||
cursor:"pointer",textAlign:"left",fontFamily:"sans-serif",gridColumn:"1/-1"}}>
|
||||
<div style={{fontSize:26,marginBottom:8}}>🎵</div>
|
||||
<div style={{fontWeight:700,fontSize:15,color:C.text,marginBottom:5}}>Tune your guitar</div>
|
||||
<div style={{fontSize:12,color:C.muted,lineHeight:1.5}}>Pluck a string and the tuner tells you if you are in tune. Always tune up before playing.</div>
|
||||
<div style={{marginTop:12,fontSize:12,fontWeight:600,color:C.sage}}>Chromatic tuner - standard tuning</div>
|
||||
</button>
|
||||
<button onClick={()=>go(0)}
|
||||
style={{background:C.bg2,border:"1px solid "+C.amber,borderRadius:14,padding:"22px 16px",
|
||||
cursor:"pointer",textAlign:"left",fontFamily:"sans-serif"}}>
|
||||
@@ -926,7 +1388,7 @@ export default function App() {
|
||||
cursor:"pointer",textAlign:"left",fontFamily:"sans-serif"}}>
|
||||
<div style={{fontSize:26,marginBottom:8}}>🔍</div>
|
||||
<div style={{fontWeight:700,fontSize:15,color:C.text,marginBottom:5}}>Search any song</div>
|
||||
<div style={{fontSize:12,color:C.muted,lineHeight:1.5}}>Know the basics? Search 32 songs and play with mic feedback.</div>
|
||||
<div style={{fontSize:12,color:C.muted,lineHeight:1.5}}>Know the basics? Search 70+ songs and play with mic feedback.</div>
|
||||
<div style={{marginTop:12,fontSize:12,fontWeight:600,color:C.muted}}>Jump to lesson 8</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -944,14 +1406,28 @@ export default function App() {
|
||||
style={{fontWeight:800,fontSize:21,background:"none",border:"none",cursor:"pointer",color:C.text,padding:0,fontFamily:"sans-serif"}}>
|
||||
<span style={{color:C.amber}}>Guitar</span><span>Teacher</span>
|
||||
</button>
|
||||
<div style={{display:"flex",alignItems:"center",gap:10,fontSize:12,color:C.muted}}>
|
||||
<div style={{display:"flex",alignItems:"center",gap:16}}>
|
||||
<div style={{display:"flex",alignItems:"center",gap:8,fontSize:12,color:C.muted}}>
|
||||
<span>{lesson+1}/{LESSONS.length}</span>
|
||||
<div style={{width:100,height:3,background:C.bg3,borderRadius:2,overflow:"hidden"}}>
|
||||
<div style={{width:80,height:3,background:C.bg3,borderRadius:2,overflow:"hidden"}}>
|
||||
<div style={{height:"100%",background:C.amber,width:prog+"%",borderRadius:2,transition:"width 0.4s"}}/>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={()=>setMenuOpen(true)}
|
||||
style={{background:"none",border:"none",cursor:"pointer",padding:4,display:"flex",flexDirection:"column",gap:5}}>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{menuOpen && <HamburgerMenu
|
||||
onTuner={()=>{setMenuOpen(false);setShowTuner(true);setSplash(false);}}
|
||||
onBasics={()=>{setMenuOpen(false);setShowTuner(false);setSplash(false);setLesson(0);}}
|
||||
onSongSearch={()=>{setMenuOpen(false);setShowTuner(false);setSplash(false);setLesson(7);}}
|
||||
onClose={()=>setMenuOpen(false)}
|
||||
/>}
|
||||
<div style={{maxWidth:820,margin:"0 auto",padding:"28px 20px 150px"}}>
|
||||
<div style={{display:"flex",gap:6,flexWrap:"wrap",marginBottom:24}}>
|
||||
{LESSONS.map((l,i)=>(
|
||||
|
||||
+69
-21
@@ -844,33 +844,81 @@ function LessonBody({ id, detected, onTarget, listening, onStartMic }) {
|
||||
<p style={{fontSize:14,color:C.muted}}>Strings thick to thin: <strong style={{color:C.text}}>E A D G B e</strong> - remember with "Elephants And Donkeys Grow Big Ears."</p>
|
||||
</div>
|
||||
);
|
||||
if (id === 1) return (
|
||||
if (id === 1) {
|
||||
const NUM_FRETS=5, NS=6, W=320, H=200;
|
||||
const TOP=44, BOT=H-24, LEFT=44, RIGHT=W-12;
|
||||
const strGap=(BOT-TOP)/(NS-1);
|
||||
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
|
||||
const LABELS=["E","A","D","G","B","e"];
|
||||
// Two example notes: open E (str0 fret0), G chord shape highlight (str5 fret3)
|
||||
// Single notes to demo: 1=open E2(str0,fr0), 2=open e(str5,fr0), 3=G chord(str0,fr3 + str5,fr3)
|
||||
const demoNotes=[
|
||||
{str:0,fret:0,label:"1",note:"E2",desc:"Open Low E"},
|
||||
{str:5,fret:0,label:"2",note:"e4",desc:"Open High e"},
|
||||
{str:0,fret:3,label:"3",note:"G",desc:"Fret 3"},
|
||||
{str:5,fret:3,label:"4",note:"G",desc:"Fret 3"},
|
||||
{str:1,fret:2,label:"5",note:"B",desc:"Fret 2"},
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:14}}>Tab shows you exactly where to put your fingers. No music theory needed. Each line is a string, numbers are frets.</p>
|
||||
<div style={{background:"#111115",borderRadius:10,padding:"18px 22px",border:"1px solid "+C.fret,overflowX:"auto"}}>
|
||||
{[
|
||||
{s:"e",v:["--","0","--","--","--","3","--"]},
|
||||
{s:"B",v:["--","--","0","--","--","3","--"]},
|
||||
{s:"G",v:["--","--","--","0","--","0","--"]},
|
||||
{s:"D",v:["--","--","--","--","--","0","--"]},
|
||||
{s:"A",v:["--","--","--","--","--","2","--"]},
|
||||
{s:"E",v:["0","--","--","--","--","3","--"]},
|
||||
].map(({s,v})=>(
|
||||
<div key={s} style={{display:"flex",alignItems:"center",height:26}}>
|
||||
<span style={{fontFamily:"monospace",fontSize:13,fontWeight:600,color:C.amber,width:22}}>{s}</span>
|
||||
<span style={{fontFamily:"monospace",fontSize:13,color:C.string,marginRight:4}}>|</span>
|
||||
{v.map((val,i)=>(
|
||||
<span key={i} style={{fontFamily:"monospace",fontSize:14,width:22,textAlign:"center",
|
||||
color:val==="--"?C.string:i>=5?C.amber:C.text,fontWeight:val!=="--"?600:400}}>
|
||||
{val}
|
||||
</span>
|
||||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:14}}>
|
||||
Each line is a string on your guitar. Numbers tell you which fret to press. 0 means open - no fingers needed.
|
||||
</p>
|
||||
<div style={{background:"#0A0A0E",borderRadius:10,padding:"10px 6px",border:"1px solid "+C.fret,marginBottom:16}}>
|
||||
<div style={{textAlign:"center",fontSize:12,color:C.muted,marginBottom:8}}>Example - a G chord</div>
|
||||
<svg width={W} height={H} style={{display:"block",margin:"0 auto"}}>
|
||||
<line x1={LEFT} y1={TOP-2} x2={LEFT} y2={BOT+2} stroke={C.text} strokeWidth={3}/>
|
||||
{Array.from({length:NUM_FRETS+1},(_,f)=>(
|
||||
<line key={f} x1={LEFT+f*fretGap} y1={TOP} x2={LEFT+f*fretGap} y2={BOT} stroke={C.fret} strokeWidth={1}/>
|
||||
))}
|
||||
</div>
|
||||
{Array.from({length:NS},(_,s)=>(
|
||||
<line key={s} x1={LEFT} y1={TOP+s*strGap} x2={RIGHT} y2={TOP+s*strGap}
|
||||
stroke={C.string} strokeWidth={s===0?2:s===5?0.8:1.2}/>
|
||||
))}
|
||||
{LABELS.map((n,s)=>(
|
||||
<text key={s} x={LEFT-10} y={TOP+s*strGap+4} textAnchor="middle"
|
||||
fontSize={10} fill={C.amber} fontFamily="monospace" fontWeight="600">{n}</text>
|
||||
))}
|
||||
{Array.from({length:NUM_FRETS},(_,f)=>(
|
||||
<text key={f} x={LEFT+(f+0.5)*fretGap} y={BOT+16} textAnchor="middle"
|
||||
fontSize={9} fill={C.muted} fontFamily="monospace">{f+1}</text>
|
||||
))}
|
||||
{/* Open string circles left of nut */}
|
||||
<circle cx={LEFT-13} cy={TOP+0*strGap} r={7} fill="none" stroke={C.sage} strokeWidth={2}/>
|
||||
<text x={LEFT-13} y={TOP+0*strGap+4} textAnchor="middle" fontSize={9} fill={C.sage} fontFamily="monospace" fontWeight="700">0</text>
|
||||
<circle cx={LEFT-13} cy={TOP+5*strGap} r={7} fill="none" stroke={C.sage} strokeWidth={2}/>
|
||||
<text x={LEFT-13} y={TOP+5*strGap+4} textAnchor="middle" fontSize={9} fill={C.sage} fontFamily="monospace" fontWeight="700">0</text>
|
||||
{/* Fretted dots */}
|
||||
{[{s:0,f:3,n:"G"},{s:1,f:2,n:"A"},{s:4,f:2,n:"B"},{s:5,f:3,n:"G"}].map(({s,f,n},i)=>{
|
||||
const cx=LEFT+(f-0.5)*fretGap, cy=TOP+s*strGap;
|
||||
return (
|
||||
<g key={i}>
|
||||
<circle cx={cx} cy={cy} r={14} fill="rgba(212,168,71,0.12)"/>
|
||||
<circle cx={cx} cy={cy} r={11} fill={C.amber}/>
|
||||
<text x={cx} y={cy+5} textAnchor="middle" fontSize={12}
|
||||
fill="#1A1A1F" fontWeight="900" fontFamily="monospace">{n}</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{/* Muted strings */}
|
||||
{[2,3].map(s=>{
|
||||
const cy=TOP+s*strGap;
|
||||
return (
|
||||
<g key={s}>
|
||||
<line x1={LEFT-18} y1={cy-6} x2={LEFT-8} y2={cy+6} stroke={C.coral} strokeWidth={2}/>
|
||||
<line x1={LEFT-8} y1={cy-6} x2={LEFT-18} y2={cy+6} stroke={C.coral} strokeWidth={2}/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
<Tip>Read left to right. 0 = open string. 2 = press 2nd fret. Numbers stacked vertically = chord.</Tip>
|
||||
<Tip>0 = open string (no fingers). A number = press that fret. X = don't play that string. Green O = open, play it.</Tip>
|
||||
<p style={{color:"#C8C5BE",fontSize:14,lineHeight:1.7,marginTop:12}}>
|
||||
The top string (E) is the thickest - the one closest to you when you hold the guitar. The bottom (e) is the thinnest.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const [stringsComplete, setStringsComplete] = useState(false);
|
||||
if (id === 2) return (
|
||||
<div>
|
||||
|
||||
Reference in new issue
Block a user