Skip to content

Commit

Permalink
Scale view now working
Browse files Browse the repository at this point in the history
  • Loading branch information
Krystex committed Jan 27, 2024
1 parent 4c09dc0 commit 233f568
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 13 deletions.
61 changes: 58 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,61 @@ const Navbar = () => (
)


const ScalesPage = () => {
const tuning = ["E", "A", "D", "G", "B", "E"]
const [rootNote, setRootNote] = useState("C")
const [scaleName, setScaleName] = useState("major")

const scale = useMemo(() => new Scale(rootNote, scaleName), [rootNote, scaleName])
const board = useMemo(() => new FretboardCtrl(13, tuning), [])
const colorFunc = (note) => Scale.Colormap[Note.dist(rootNote, note)]

return (
<div className="flex justify-center flex-col m-20">
<div className="mb-8">
{/* Key select */}
<select value={rootNote} onChange={e => setRootNote(e.target.value)}
className="bg-gray-600 border border-gray-400 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 w-[8rem] p-2.5 mr-4 text-center outline-none">
{Scale.Chromatic.map(notes =>
<option key={notes[0]} value={notes[0]} >
{notes.length == 1 && `${notes[0]}`}
{notes.length > 1 && `${notes[1]} / ${notes[0]}`}
</option>
)}
</select>

{/* Scale select */}
<select value={scaleName} onChange={e => setScaleName(e.target.value)}
className="bg-gray-600 border border-gray-400 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 w-[8rem] p-2.5 mr-4 text-center outline-none">
{Scale.supportedScales.map(supportedScaleName =>
<option key={supportedScaleName} value={supportedScaleName}>{supportedScaleName}</option>
)}
</select>
</div>

<svg width="800" height="200">
<Fretboard width={800} board={board}
colorFunc={colorFunc}
noteFunc={(note) => scale.has(note)}
onMouseEnter={(x, y, note) => null} onMouseOut={(x, y, note) => null} />
</svg>
<Row className="w-[500px] ml-[8rem] justify-between">
<div className="w-[200px] ml-[150px]">
<div className="text-white font-semibold text-center pb-2">Chromatic notes</div>
<svg width="200" height="200">
<ChromaticNoteCircle x={100} y={100} innerRadius={30} outerRadius={100}
noteFunc={(note) => scale.has(note)}
colorFunc={colorFunc}
onClick={(note) => setRootNote(note)}
/>
</svg>
</div>
</Row>
</div>
)
}


const CircleOfFifthsPage = () => {
const tuning = ["E", "A", "D", "G", "B", "E"]
const board = useMemo(() => new FretboardCtrl(13, tuning), [])
Expand All @@ -68,15 +123,15 @@ const CircleOfFifthsPage = () => {
<div className="text-white font-semibold text-center">Circle of Fifths</div>
<svg width="200" height="200">
<CircleOfFifths x={100} y={100} innerRadius={30} outerRadius={100}
highlightNote={cofNote} colorFunc={colorFunc}
noteFunc={(note) => Note.eq(note, cofNote)} colorFunc={colorFunc}
onNoteEnter={note => setCofNote(note)} onNoteOut={_ => setCofNote(null)} />
</svg>
</div>
<div className="w-[200px]">
<div className="text-white font-semibold text-center">Chromatic Circle</div>
<svg width="200" height="200">
<ChromaticNoteCircle x={100} y={100} innerRadius={30} outerRadius={100}
highlightNote={cofNote} colorFunc={colorFunc}
noteFunc={(note) => Note.eq(note, cofNote)} colorFunc={colorFunc}
onNoteEnter={note => setCofNote(note)} onNoteOut={_ => setCofNote(null)} />
</svg>
</div>
Expand All @@ -96,7 +151,7 @@ const router = createHashRouter([
},
{
path: "scales",
element: <div>Scales</div>
element: <ScalesPage />
}
]
},
Expand Down
16 changes: 10 additions & 6 deletions src/Fretboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,28 @@ const Text = ({ x, y, children, ...rest }) => (
* @param {Number} props.outerRadius outer radius of circle
* @param {(String) => boolean} props.onNoteEnter function gets triggered if note is hovered
* @param {(String) => boolean} props.onNoteOut function gets triggered if note is not hovered anymore
* @param {(String) => String} props.colorFunc color function which determines which color should be shown for specific note
* @param {(String) => string} props.onClick function gets triggered if note (arc) is clicked. parameter is the note itself
* @param {(String) => boolean} porps.noteFunc function which determines if note is highlighted or not. get called for every note with note as parameter
* @param {(String) => String} props.colorFunc color function which determines which color should be shown for specific note. gets called for every note with note as parameter
* @param {Array<string>} props.scale scale to display (eg. `Scale.Chromatic`)
*/
const Circle12Notes = ({ x, y, innerRadius, outerRadius, highlightNote, onNoteEnter = null, onNoteOut = null, colorFunc, scale }) => {
const Circle12Notes = ({ x, y, innerRadius, outerRadius, noteFunc, onNoteEnter=null, onNoteOut=null, onClick=null, colorFunc, scale }) => {
const angle = 360. / 12
const halfAngle = angle / 2 - 1 // used for shifting arcs so they are centered. minus one just because it looks a little nicer
const padAngle = 2

return (
<>
{ /* Circle of fifths circle */}
{ /* Circle arcs */}
{scale.map((notes, i) => (
<Arc key={i} x={x} y={y} innerRadius={innerRadius} outerRadius={outerRadius}
startAngle={i * angle - halfAngle} endAngle={(i + 1) * angle - halfAngle} padAngle={padAngle}
onMouseEnter={() => onNoteEnter(notes[0])} onMouseOut={() => onNoteOut()}
fill={Note.eq(highlightNote, notes[0]) ? colorFunc(notes[0]) : "white"} />
onMouseEnter={() => onNoteEnter && onNoteEnter(notes[0])}
onMouseOut={() => onNoteOut && onNoteOut()}
onClick={() => onClick && onClick(notes[0])}
fill={noteFunc(notes[0]) ? colorFunc(notes[0]) : "white"} />
))}
{ /* Circle of fifths texts */}
{ /* Circle texts */}
{scale.map((notes, i) => {
const startAngle = i * angle - halfAngle, endAngle = (i + 1) * angle - halfAngle
// radiusOffset is used if two notes are displayed (eg. F# and Gb)
Expand Down
24 changes: 20 additions & 4 deletions src/musictheory.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,15 @@ class Scale {
this.key = key
this.scale = scale
let constructed = [key] // scale starts with key note
let ints = undefined // intervals
if (scale == "major") { ints = [2, 2, 1, 2, 2, 2, 1] }
else if (scale == "minor" || scale == "natural minor") { ints = [2, 1, 2, 2, 1, 2, 2] }
else { throw `Scale ${scale} not yet implemented` }

if (!Scale.supportedScales.includes(scale)) {
throw `Scale ${scale} not yet implemented`
}
const ints = {
"major": [2, 2, 1, 2, 2, 2, 1],
"minor": [2, 1, 2, 2, 1, 2, 2],
"natural minor": [2, 1, 2, 2, 1, 2, 2],
}[scale]
for (const interval of ints) {
// we get next note and then sharp or flat the note. this ensures that every note only appears once in the scale
const lastNote = constructed[constructed.length - 1] // get last constructed note
Expand All @@ -141,6 +146,17 @@ class Scale {
}
this.notes = constructed
}
/** Returns if given note is in scale or not
* @param {string} note Note to compare to scale
* @returns {boolean} is in scale
* @example new Scale("C", "major").has("D") == true
*/
has(note) {
return this.notes.some(scaleNote => Note.eq(scaleNote, note))
}

static supportedScales = ["major", "minor", "natural minor"]

/**
* Notes of circle of fifths with enharmonic equivalents
*/
Expand Down

0 comments on commit 233f568

Please sign in to comment.