From 2a79d766fd83c4cf3877327e6a8f06bbf0c3fbea Mon Sep 17 00:00:00 2001 From: Sterling Archer Date: Sat, 13 Jun 2026 21:10:01 -0700 Subject: [PATCH] Lesson 2: replace text tab with fretboard SVG --- guitar-teacher.jsx | 628 +++++++++++++++++++++++++++++++++++++++------ index.html | 98 +++++-- 2 files changed, 625 insertions(+), 101 deletions(-) diff --git a/guitar-teacher.jsx b/guitar-teacher.jsx index e474272..d4862aa 100644 --- a/guitar-teacher.jsx +++ b/guitar-teacher.jsx @@ -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 ( -
-
{name}
- - - {Array.from({length:NF+1},(_,f)=>( - +
+
{name}
+ + + {Array.from({length:NUM_FRETS+1},(_,f)=>( + ))} {Array.from({length:NS},(_,s)=>( - + ))} - {sh.dots.map(({s,f,n},i)=>{ - const cx=NX+s*NG, cy=NY+(f-0.5)*FH; + {STRING_LABELS.map((n,s)=>( + {n} + ))} + {Array.from({length:NUM_FRETS},(_,f)=>( + {f+1} + ))} + {sh.muted.map(s=>{ + const cy=TOP+s*strGap; return ( - - - {n} + + + ); })} {sh.open.map(s=>( - + ))} - {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 ( - - - + + + + {n} ); })} -
{sh.label}
+
{sh.label}
); } 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 ( -
- {idx < OPEN_NOTES.length ? ( +
+ {!finished ? ( <> -
Pluck this string:
-
{OPEN_NOTES[idx].label}
-
Target: {OPEN_NOTES[idx].note}
+
+
+
Pluck this string:
+
{current.label}
+ {!heard &&
Target: {current.note}
} + {heard &&
Heard it!
} +
+ + + {Array.from({length:NUM_FRETS+1},(_,f)=>( + + ))} + {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 ( + + {isHighlighted && ( + + )} + + + {STRING_LABELS[s]} + + {doneSet[s] && ( + ✓ + )} + + ); + })} + {Array.from({length:NUM_FRETS},(_,f)=>( + {f+1} + ))} + +
+ + {heard && ( + + )} + +
+ {OPEN_NOTES.map((s,i)=>( +
+ {doneSet[i]?"✓":s.note} +
+ ))} +
) : ( -
All 6 open strings done!
- )} -
- {OPEN_NOTES.map((s,i)=>( -
- {s.note} +
+
🎸
+
{kudos}
+
You just played all 6 open strings. Click Next to continue.
+
+ {OPEN_NOTES.map((s,i)=>( +
✓
+ ))}
- ))} -
+
+ )}
); } @@ -742,11 +850,17 @@ function LessonBody({ id, detected, onTarget, listening, onStartMic }) { Read left to right. 0 = open string. 2 = press 2nd fret. Numbers stacked vertically = chord.
); + const [stringsComplete, setStringsComplete] = useState(false); if (id === 2) return (
-

Start with open strings. Enable the mic and the app confirms each note as you pluck.

- - Press just behind the fret wire, not on top of it. +

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.

+ setStringsComplete(true)}/> + {!stringsComplete && Open strings means no fingers on the frets - just pluck the string.} + {stringsComplete && ( +
+ setStringsComplete(false)} variant="secondary">Start over +
+ )}
); 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 ( + <> +
+
+
+
+ GuitarTeacher +
+ +
+
+ {[ + {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})=>( + + ))} +
+
+ GuitarTeacher - Learn at your own pace +
+
+ + ); +} + + +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 ( +
+
+ +
+ GuitarTeacher + / Tuner +
+ +
+ {menuOpen && {setMenuOpen(false);}} + onBasics={()=>{setMenuOpen(false);onGoBasics();}} + onSongSearch={()=>{setMenuOpen(false);onGoSongSearch();}} + onClose={()=>setMenuOpen(false)} + />} + +
+
+
+
Pluck any string and tune to the closest note
+
+ + {/* Needle meter */} +
+ + {/* Arc background */} + + {/* Colored arc zones */} + + + + + + {/* Center tick */} + + {/* Needle */} + {detectedFreq > 0 && ( + + + + + )} + {/* Center dot when no signal */} + {detectedFreq === 0 && ( + + )} + {/* Labels */} + -50 + 0 + +50 + + + {detectedFreq > 0 ? ( + <> +
{detectedNote}
+
{detectedFreq} Hz
+
{tuneMsg}
+ {!inTune && ( +
+ {Math.abs(cents)} cents {cents < 0 ? "flat" : "sharp"} +
+ )} + + ) : ( +
+ {listening ? "Pluck a string..." : "Tap the mic to start"} +
+ )} +
+ + {/* String selector tabs */} +
+ {OPEN_STRINGS.map((s,i)=>{ + const isActive = activeString && activeString.note===s.note; + const isDone = tuned[s.note]; + return ( + + ); + })} +
+ + {/* Guitar string visual */} +
0?C.sage:C.fret),marginBottom:12,transition:"border-color 0.3s"}}> +
+ {detectedFreq>0 ? ( + Plucking + {activeString.name} string + {inTune ? "- In tune!" : (cents<0?"- Tune up (too flat)":"- Tune down (too sharp)")} + ) : Tap a string above, then pluck it on your guitar} +
+ + {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 ( + + {isActive && ( + + )} + + + {s.name==="Low E"?"E":s.name==="High e"?"e":s.name} + + + {s.note} + + {isActive && ( + <> + + + {inTune?"✓":cents<0?"↑":"↓"} + + + )} + + ); + })} + + + +
+ + {/* Mark tuned + Next */} +
+ {detectedFreq>0 && inTune && ( + + )} + {allTuned && ( + + )} +
+ + {/* Mic button */} + +
+
+
+ ); +} + 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 setShowTuner(false)} + onGoTuner={()=>{}} + onGoBasics={()=>{setShowTuner(false);setSplash(false);setLesson(0);}} + onGoSongSearch={()=>{setShowTuner(false);setSplash(false);setLesson(7);}} + />; + if (splash) return (
-
- GuitarTeacher +
+
GuitarTeacher
+
+ {menuOpen && {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)} + />}
🎸
@@ -912,7 +1366,15 @@ export default function App() {

Start from the very beginning or jump straight to playing songs you love.

-
+
+
@@ -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"}}> GuitarTeacher -
- {lesson+1}/{LESSONS.length} -
-
+
+
+ {lesson+1}/{LESSONS.length} +
+
+
+
+ {menuOpen && {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)} + />}
{LESSONS.map((l,i)=>( diff --git a/index.html b/index.html index 1562ad8..7fdc440 100644 --- a/index.html +++ b/index.html @@ -844,33 +844,81 @@ function LessonBody({ id, detected, onTarget, listening, onStartMic }) {

Strings thick to thin: E A D G B e - remember with "Elephants And Donkeys Grow Big Ears."

); - if (id === 1) return ( -
-

Tab shows you exactly where to put your fingers. No music theory needed. Each line is a string, numbers are frets.

-
- {[ - {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})=>( -
- {s} - | - {v.map((val,i)=>( - =5?C.amber:C.text,fontWeight:val!=="--"?600:400}}> - {val} - + 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 ( +
+

+ Each line is a string on your guitar. Numbers tell you which fret to press. 0 means open - no fingers needed. +

+
+
Example - a G chord
+ + + {Array.from({length:NUM_FRETS+1},(_,f)=>( + ))} -
- ))} + {Array.from({length:NS},(_,s)=>( + + ))} + {LABELS.map((n,s)=>( + {n} + ))} + {Array.from({length:NUM_FRETS},(_,f)=>( + {f+1} + ))} + {/* Open string circles left of nut */} + + 0 + + 0 + {/* 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 ( + + + + {n} + + ); + })} + {/* Muted strings */} + {[2,3].map(s=>{ + const cy=TOP+s*strGap; + return ( + + + + + ); + })} + +
+ 0 = open string (no fingers). A number = press that fret. X = don't play that string. Green O = open, play it. +

+ The top string (E) is the thickest - the one closest to you when you hold the guitar. The bottom (e) is the thinnest. +

- Read left to right. 0 = open string. 2 = press 2nd fret. Numbers stacked vertically = chord. -
- ); + ); + } const [stringsComplete, setStringsComplete] = useState(false); if (id === 2) return (