Compare commits

...
10 Commits
+93 -66
View File
@@ -127,7 +127,10 @@ const SONG_LIBRARY = [
{"title":"The Bad Touch","artist":"Bloodhound Gang","genre":"Rock","diff":"Beginner","chords":["Em","G","D","A"],"capo":0,"parts":[{"name":"Main Riff","tab":{"e":"0---0---3---2---","B":"0---0---3---3---","G":"0---0---0---2---","D":"2---0---0---0---","A":"2---2---2---0---","E":"0---3---3---2---"},"notes":["E3","G4","D3","A2"]},{"name":"Verse","tab":{"e":"0---3---2---0---","B":"0---3---3---2---","G":"0---0---2---2---","D":"2---0---0---2---","A":"2---2---0---0---","E":"0---3---2---0---"},"notes":["E3","G4","D3","A2"]}],"tips":"Fun late 90s rock. Em-G-D-A drives the whole song. Keep the strum punchy and upbeat."},
{"title":"Zombie","artist":"The Cranberries","genre":"Rock","diff":"Beginner","chords":["Em","C","G","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"0---0---3---2---","B":"0---1---3---3---","G":"0---0---0---2---","D":"2---2---0---0---","A":"2---3---2---0---","E":"0---0---3---2---"},"notes":["E3","C4","G4","D3"]},{"name":"Chorus","tab":{"e":"0---0---3---2---","B":"0---1---3---3---","G":"0---0---0---2---","D":"2---2---0---0---","A":"2---3---2---0---","E":"0---0---3---2---"},"notes":["E3","C4","G4","D3"]}],"tips":"Em-C-G-D the whole song. Power chords on the chorus for extra punch. One of the most recognisable riffs of the 90s."},
{"title":"Linger","artist":"The Cranberries","genre":"Pop/Rock","diff":"Beginner","chords":["D","Dsus2","G","A"],"capo":0,"parts":[{"name":"Intro / Verse","tab":{"e":"2---0---3---0---","B":"3---3---3---2---","G":"2---2---0---2---","D":"0---0---0---2---","A":"0---0---2---0---","E":"2---2---3---0---"},"notes":["D3","D3","G4","A2"]},{"name":"Chorus","tab":{"e":"2---0---3---0---","B":"3---3---3---2---","G":"2---2---0---2---","D":"0---0---0---2---","A":"0---0---2---0---","E":"2---2---3---0---"},"notes":["D3","D3","G4","A2"]}],"tips":"Beautiful gentle song. The D to Dsus2 movement is the signature \u2014 just lift your index finger off the B string. Fingerpick or light strum."},
{"title":"Somebody That I Used To Know","artist":"Gotye ft. Kimbra","genre":"Pop/Rock","diff":"Intermediate","chords":["Dm","C","Bb","F"],"capo":0,"parts":[{"name":"Intro Riff","tab":{"e":"0---3---1---0---","B":"1---3---3---1---","G":"2---0---3---2---","D":"0---0---3---3---","A":"0---2---1---3---","E":"1---3---1---1---"},"notes":["D3","G4","Bb2","F3"]},{"name":"Verse","tab":{"e":"0---3---1---0---","B":"1---3---3---1---","G":"2---0---3---2---","D":"0---0---3---3---","A":"0---2---1---3---","E":"1---3---1---1---"},"notes":["D3","G4","Bb2","F3"]},{"name":"Chorus","tab":{"e":"1---0---3---1---","B":"3---1---3---3---","G":"3---2---0---3---","D":"3---3---0---3---","A":"1---3---2---1---","E":"1---1---3---1---"},"notes":["Bb2","F3","G4","Bb2"]}],"tips":"The xylophone melody from the original translates beautifully to guitar. Dm-C-Bb-F is the main loop. Fingerpick the intro, strum the chorus."}
{"title":"Somebody That I Used To Know","artist":"Gotye ft. Kimbra","genre":"Pop/Rock","diff":"Intermediate","chords":["Dm","C","Bb","F"],"capo":0,"parts":[{"name":"Intro Riff","tab":{"e":"0---3---1---0---","B":"1---3---3---1---","G":"2---0---3---2---","D":"0---0---3---3---","A":"0---2---1---3---","E":"1---3---1---1---"},"notes":["D3","G4","Bb2","F3"]},{"name":"Verse","tab":{"e":"0---3---1---0---","B":"1---3---3---1---","G":"2---0---3---2---","D":"0---0---3---3---","A":"0---2---1---3---","E":"1---3---1---1---"},"notes":["D3","G4","Bb2","F3"]},{"name":"Chorus","tab":{"e":"1---0---3---1---","B":"3---1---3---3---","G":"3---2---0---3---","D":"3---3---0---3---","A":"1---3---2---1---","E":"1---1---3---1---"},"notes":["Bb2","F3","G4","Bb2"]}],"tips":"The xylophone melody from the original translates beautifully to guitar. Dm-C-Bb-F is the main loop. Fingerpick the intro, strum the chorus."},
{"title":"Interstellar Theme","artist":"Hans Zimmer","genre":"Cinematic","diff":"Beginner","chords":["Am","F","C","G"],"capo":0,"parts":[{"name":"Main Theme","tab":{"e":"0---1---0---3---","B":"1---1---1---3---","G":"2---2---0---0---","D":"2---3---2---0---","A":"0---3---3---2---","E":"----1---0---3---"},"notes":["A2","F3","C4","G4"]},{"name":"Fingerpicking Pattern","tab":{"e":"0-0-1-1-0-0-3-3-","B":"1-1-1-1-1-1-3-3-","G":"2-2-2-2-0-0-0-0-","D":"2-2-3-3-2-2-0-0-","A":"0---3---3---2---","E":"----1---0---3---"},"notes":["A2","F3","C4","G4","A2","F3"]}],"tips":"Play fingerstyle with a slow, measured pace. Let every note ring fully into the next. The Am-F-C-G loop creates that iconic haunting feel."},
{"title":"Pink Pony Club","artist":"Chappell Roan","genre":"Pop","diff":"Beginner","chords":["G","D","Em","C"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"3---2---0---0---","B":"3---3---0---1---","G":"0---2---0---0---","D":"0---0---2---2---","A":"2-------2---3---","E":"3---2---0-------"},"notes":["G4","D3","E3","C4"]},{"name":"Chorus","tab":{"e":"3---2---0---0---","B":"3---3---0---1---","G":"0---2---0---0---","D":"0---0---2---2---","A":"2-------2---3---","E":"3---2---0-------"},"notes":["G4","D3","E3","C4"]}],"tips":"G-D-Em-C is the whole song. Big energetic strum on the chorus. Capo 2 to match the original key."},
{"title":"Self Aware","artist":"Temper City","genre":"Rock","diff":"Intermediate","chords":["Em","C","G","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"0---0---3---2---","B":"0---1---3---3---","G":"0---0---0---2---","D":"2---2---0---0---","A":"2---3---2-------","E":"0---0---3---2---"},"notes":["E3","C4","G4","D3"]},{"name":"Chorus","tab":{"e":"3---0---0---2---","B":"3---1---0---3---","G":"0---0---0---2---","D":"0---2---2---0---","A":"2---3---2-------","E":"3---0---0---2---"},"notes":["G4","C4","E3","D3"]}],"tips":"Em-C-G-D drives the whole track. Energetic down strumming on the chorus."}
];
@@ -141,6 +144,8 @@ const SONG_LIBRARY = [
function ChordDiagram({ name }) {
const sh = CHORDS[name];
if (!sh) return null;
@@ -377,69 +382,75 @@ function SongPlayer({ parts, detected, listening, onStartMic }) {
useEffect(()=>()=>{ if(timerRef.current) clearTimeout(timerRef.current); },[]);
const NUM_FRETS=6, NS=6, W=320, H=260;
const TOP=44, BOT=H-24, LEFT=44, RIGHT=W-12;
const NS=6, H=260;
const TOP=44, BOT=H-24, LEFT=44;
const strGap=(BOT-TOP)/(NS-1);
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
const renderFretboard = (pIdx) => {
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 (
<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 style={{overflowX:"auto"}}>
<svg width={W} height={H} style={{display:"block",minWidth:W}}>
{/* Strings */}
{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?0.8:s===5?2:1.2}/>
<line key={s} x1={LEFT-20} y1={TOP+s*strGap} x2={W-10} y2={TOP+s*strGap}
stroke={C.string} strokeWidth={s===0?2:s===5?0.8:1.2}/>
))}
{/* String labels */}
{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>
))}
{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>
{/* Vertical beat lines */}
{pNotes.map((_,ni)=>(
<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)=>{
const p = NOTE_FRET_MAP[note];
if(!p) return null;
const isOpen = p.fret===0;
const baseX = isOpen ? LEFT+0.5*fretGap : LEFT+(p.fret-0.5)*fretGap;
const baseY = 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 cx = noteX(ni);
const cy = TOP+p.str*strGap;
const stateKey = pIdx+"-"+ni;
const state = dotStates[stateKey];
const isCur = active && state==="cur";
const isHit = state==="hit";
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;
return (
<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}
fill={isOpen?(isHit?C.sage:isCur?C.amber:"transparent"):fill}
stroke={isOpen?(isHit?C.sage:isCur?C.amber:C.amber):"none"}
strokeWidth={isOpen?2.5:0}
opacity={opacity}/>
<text x={cx} y={cy+5} textAnchor="middle" fontSize={14}
fill={isOpen?(isHit?"#1A1A1F":isCur?"#1A1A1F":C.amber):"#1A1A1F"}
fontWeight="900" fontFamily="monospace">
{ni+1}
{!isOpen && (
<text x={cx} y={cy+5} textAnchor="middle" fontSize={13}
fill="#1A1A1F" fontWeight="900" fontFamily="monospace">
{p.fret}
</text>
)}
{/* Fret label above strings area */}
<text x={cx} y={TOP-6} textAnchor="middle" fontSize={9}
fill={isCur?C.amber:C.muted} fontFamily="monospace">
{isOpen?"open":("fr"+p.fret)}
</text>
{isCur && (
<text x={cx} y={cy-24} textAnchor="middle" fontSize={20}>🎸</text>
<text x={cx} y={TOP-16} textAnchor="middle" fontSize={20}>🎸</text>
)}
</g>
);
})}
</svg>
</div>
);
};
@@ -473,9 +484,9 @@ function SongPlayer({ parts, detected, listening, onStartMic }) {
const p = NOTE_FRET_MAP[n];
return (
<div key={i} style={{display:"flex",alignItems:"center",gap:3,fontSize:10,color:C.muted}}>
<span style={{color:C.amber,fontWeight:700}}>{i+1}</span>
{p&&p.fret!==0&&<span style={{color:C.amber,fontWeight:700}}>{i+1}</span>}
<span style={{fontFamily:"monospace",color:C.text}}>{n}</span>
{p&&<span style={{color:C.muted}}>({STRING_NAMES[p.str]} fr{p.fret})</span>}
{p&&<span style={{color:C.muted}}>{p.fret===0?"(open "+STRING_NAMES[p.str]+")":"("+STRING_NAMES[p.str]+" fr"+p.fret+")"}</span>}
</div>
);
})}
@@ -1174,6 +1185,7 @@ function GuitarTuner({ onBack, onGoTuner, onGoBasics, onGoSongSearch }) {
const [lockedString, setLockedString] = useState(null);
const [tuned, setTuned] = useState({});
const [currentStringIdx, setCurrentStringIdx] = useState(0);
const [readyToMark, setReadyToMark] = useState(false);
const ctxRef = useRef(null);
const streamRef = useRef(null);
const rafRef = useRef(null);
@@ -1224,6 +1236,7 @@ function GuitarTuner({ onBack, onGoTuner, onGoBasics, onGoSongSearch }) {
useEffect(()=>()=>stopMic(),[]);
const inTune = Math.abs(cents) <= 8;
useEffect(()=>{ if(inTune && detectedFreq>0 && !tuned[activeString?.note]) setReadyToMark(true); },[inTune, detectedFreq]);
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;
@@ -1317,7 +1330,7 @@ function GuitarTuner({ onBack, onGoTuner, onGoBasics, onGoSongSearch }) {
const isActive = activeString && activeString.note===s.note;
const isDone = tuned[s.note];
return (
<button key={s.note} onClick={()=>{setLockedString(s);setCurrentStringIdx(i);}}
<button key={s.note} onClick={()=>{setLockedString(s);setCurrentStringIdx(i);setReadyToMark(false);}}
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}}>
@@ -1387,9 +1400,10 @@ function GuitarTuner({ onBack, onGoTuner, onGoBasics, onGoSongSearch }) {
{/* Mark tuned + Next */}
<div style={{display:"flex",gap:10,marginBottom:12}}>
{detectedFreq>0 && inTune && (
{readyToMark && !tuned[activeString?.note] && (
<button onClick={()=>{
setTuned(t=>({...t,[activeString.note]:true}));
setReadyToMark(false);
const nextIdx = currentStringIdx+1 < OPEN_STRINGS.length ? currentStringIdx+1 : currentStringIdx;
setCurrentStringIdx(nextIdx);
setLockedString(nextIdx!==currentStringIdx ? OPEN_STRINGS[nextIdx] : lockedString);
@@ -1425,6 +1439,38 @@ function GuitarTuner({ onBack, onGoTuner, onGoBasics, onGoSongSearch }) {
);
}
function SplashCards({ onTuner, onBasics, onSearch }) {
const [hovered, setHovered] = useState(null);
const cards = [
{key:"tuner", icon:"🎵", title:"Tune your guitar", desc:"Get in tune before you play. Pluck a string and the tuner guides you.", action:onTuner},
{key:"basics", icon:"📖", title:"Start from basics", desc:"New to guitar? Learn step by step from the very beginning.", action:onBasics},
{key:"search", icon:"🔍", title:"Search any song", desc:"Already know the basics? Find a song and play along with the mic.", action:onSearch},
];
return (
<div style={{display:"flex",flexDirection:"column",gap:12}}>
{cards.map(({key,icon,title,desc,action})=>(
<button key={key} onClick={action}
onMouseEnter={()=>setHovered(key)}
onMouseLeave={()=>setHovered(null)}
style={{background:C.bg2,
border:"1px solid "+(hovered===key?C.sage:C.amber),
borderRadius:16,padding:"20px 22px",
cursor:"pointer",textAlign:"left",fontFamily:"sans-serif",
display:"flex",alignItems:"center",gap:18,
transition:"border-color 0.2s"}}>
<div style={{fontSize:36,flexShrink:0}}>{icon}</div>
<div style={{flex:1}}>
<div style={{fontWeight:700,fontSize:16,color:C.text,marginBottom:3}}>{title}</div>
<div style={{fontSize:13,color:C.muted,lineHeight:1.5}}>{desc}</div>
</div>
<div style={{color:hovered===key?C.sage:C.amber,fontSize:20,flexShrink:0,transition:"color 0.2s"}}>›</div>
</button>
))}
</div>
);
}
function App() {
const [splash, setSplash] = useState(true);
const [showTuner, setShowTuner] = useState(false);
@@ -1521,42 +1567,23 @@ function App() {
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>
<h1 style={{fontWeight:800,fontSize:34,lineHeight:1.15,marginBottom:12}}>
<div style={{flex:1,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",padding:"32px 24px 60px"}}>
<div style={{maxWidth:480,width:"100%"}}>
<div style={{textAlign:"center",marginBottom:36}}>
<div style={{fontSize:56,lineHeight:1,marginBottom:16}}>🎸</div>
<h1 style={{fontWeight:800,fontSize:32,lineHeight:1.2,marginBottom:10}}>
Learn guitar, <span style={{color:C.amber}}>your way.</span>
</h1>
<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 style={{fontSize:14,color:C.muted,lineHeight:1.7}}>
Pick where you want to start.
</p>
<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"}}>
<div style={{fontSize:26,marginBottom:8}}>📖</div>
<div style={{fontWeight:700,fontSize:15,color:C.text,marginBottom:5}}>Start from basics</div>
<div style={{fontSize:12,color:C.muted,lineHeight:1.5}}>Never played? We cover everything from holding the guitar to real songs.</div>
<div style={{marginTop:12,fontSize:12,fontWeight:600,color:C.amber}}>7 lessons + free play</div>
</button>
<button onClick={()=>go(7)}
style={{background:C.bg2,border:"1px solid "+C.fret,borderRadius:14,padding:"22px 16px",
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 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>
<p style={{fontSize:12,color:C.muted}}>You can jump between lessons anytime using the nav.</p>
<SplashCards
onTuner={()=>setShowTuner(true)}
onBasics={()=>go(0)}
onSearch={()=>go(7)}
/>
</div>
</div>
</div>
@@ -1615,7 +1642,7 @@ function App() {
{lesson > 0 && <Btn onClick={()=>setLesson(l=>l-1)} variant="secondary">Back</Btn>}
{LESSONS[lesson].next
? <Btn onClick={()=>setLesson(l=>l+1)}>{LESSONS[lesson].next}</Btn>
: <div style={{color:C.amber,fontWeight:600,padding:"10px 0"}}>You are all done! Keep playing.</div>}
: null}
</div>
</div>
</div>