diff --git a/src/App.jsx b/src/App.jsx index d0ecc95..8925836 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 ( +
+
+ {/* Key select */} + + + {/* Scale select */} + +
+ + + scale.has(note)} + onMouseEnter={(x, y, note) => null} onMouseOut={(x, y, note) => null} /> + + +
+
Chromatic notes
+ + scale.has(note)} + colorFunc={colorFunc} + onClick={(note) => setRootNote(note)} + /> + +
+
+
+ ) +} + + const CircleOfFifthsPage = () => { const tuning = ["E", "A", "D", "G", "B", "E"] const board = useMemo(() => new FretboardCtrl(13, tuning), []) @@ -68,7 +123,7 @@ const CircleOfFifthsPage = () => {
Circle of Fifths
Note.eq(note, cofNote)} colorFunc={colorFunc} onNoteEnter={note => setCofNote(note)} onNoteOut={_ => setCofNote(null)} /> @@ -76,7 +131,7 @@ const CircleOfFifthsPage = () => {
Chromatic Circle
Note.eq(note, cofNote)} colorFunc={colorFunc} onNoteEnter={note => setCofNote(note)} onNoteOut={_ => setCofNote(null)} /> @@ -96,7 +151,7 @@ const router = createHashRouter([ }, { path: "scales", - element:
Scales
+ element: } ] }, diff --git a/src/Fretboard.jsx b/src/Fretboard.jsx index 29609cb..d0aef1a 100644 --- a/src/Fretboard.jsx +++ b/src/Fretboard.jsx @@ -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} 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) => ( 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) diff --git a/src/musictheory.js b/src/musictheory.js index 96e6921..50eceec 100644 --- a/src/musictheory.js +++ b/src/musictheory.js @@ -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 @@ -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 */