diff --git a/front/components/VirtualPiano/Octave.tsx b/front/components/VirtualPiano/Octave.tsx index 46c7640..6b76098 100644 --- a/front/components/VirtualPiano/Octave.tsx +++ b/front/components/VirtualPiano/Octave.tsx @@ -4,11 +4,10 @@ import { NoteNameBehavior, octaveKeys, Accidental, + HighlightedKey, } from "../../models/Piano"; -import { Box, Row, Pressable, ZStack, Text } from "native-base"; - -const notesList: Array = ["C", "D", "E", "F", "G", "A", "B"]; -const accidentalsList: Array = ["#", "b", "##", "bb"]; +import { Box, Row, Pressable, Text } from "native-base"; +import PianoKeyComp from "./PianoKeyComp"; const getKeyIndex = (n: Note, keys: PianoKey[]) => { for (let i = 0; i < keys.length; i++) { @@ -25,14 +24,14 @@ const isNoteVisible = ( isHovered: boolean, isHighlighted: boolean ) => { - if (showNoteNamesPolicy === "always") return true; - if (showNoteNamesPolicy === "never") return false; + if (showNoteNamesPolicy === NoteNameBehavior.always) return true; + if (showNoteNamesPolicy === NoteNameBehavior.never) return false; - if (showNoteNamesPolicy === "onpress") { + if (showNoteNamesPolicy === NoteNameBehavior.onpress) { return isPressed; - } else if (showNoteNamesPolicy === "onhover") { + } else if (showNoteNamesPolicy === NoteNameBehavior.onhover) { return isHovered; - } else if (showNoteNamesPolicy === "onhighlight") { + } else if (showNoteNamesPolicy === NoteNameBehavior.onhighlight) { return isHighlighted; } return false; @@ -44,13 +43,36 @@ type OctaveProps = Parameters[0] & { endNote: Note; showNoteNames: NoteNameBehavior; showOctaveNumber: boolean; + whiteKeyBg: string; + whiteKeyBgPressed: string; + whiteKeyBgHovered: string; + blackKeyBg: string; + blackKeyBgPressed: string; + blackKeyBgHovered: string; + highlightedNotes: Array; + defaultHighlightColor: string; onNoteDown: (note: PianoKey) => void; onNoteUp: (note: PianoKey) => void; }; const Octave = (props: OctaveProps) => { - const { number, startNote, endNote, showNoteNames, showOctaveNumber, onNoteDown, onNoteUp } = - props; + const { + number, + startNote, + endNote, + showNoteNames, + showOctaveNumber, + whiteKeyBg, + whiteKeyBgPressed, + whiteKeyBgHovered, + blackKeyBg, + blackKeyBgPressed, + blackKeyBgHovered, + highlightedNotes, + defaultHighlightColor, + onNoteDown, + onNoteUp, + } = props; const oK: PianoKey[] = octaveKeys.map((k) => { return new PianoKey(k.note, k.accidental, number); }); @@ -71,102 +93,61 @@ const Octave = (props: OctaveProps) => { {whiteKeys.map((key, i) => { + const highlightedKey = highlightedNotes.find( + (h) => + h.key.note === key.note && h.key.accidental === key.accidental + ); + const isHighlighted = highlightedKey !== undefined; + const highlightColor = + highlightedKey?.bgColor ?? defaultHighlightColor; return ( - onNoteDown(key)} - onPressOut={() => onNoteUp(key)} - > - {({ isHovered, isPressed }) => ( - - {isNoteVisible( - showNoteNames, - isPressed, - isHovered, - false - ) && ( - - {key.note} - - )} - - )} - + onNoteDown(key)} + onKeyUp={() => onNoteUp(key)} + style={{ + width: whiteKeyWidthExpr, + height: whiteKeyHeightExpr, + }} + + /> ); })} {blackKeys.map((key, i) => { + const highlightedKey = highlightedNotes.find( + (h) => + h.key.note === key.note && h.key.accidental === key.accidental + ); + const isHighlighted = highlightedKey !== undefined; + const highlightColor = + highlightedKey?.bgColor ?? defaultHighlightColor; return ( - onNoteDown(key)} - onPressOut={() => onNoteUp(key)} - width={blackKeyWidthExpr} - height={blackKeyHeightExpr} + pianoKey={key} + bg={isHighlighted ? highlightColor : blackKeyBg} + bgPressed={isHighlighted ? highlightColor : blackKeyBgPressed} + bgHovered={isHighlighted ? highlightColor : blackKeyBgHovered} + onKeyDown={() => onNoteDown(key)} + onKeyUp={() => onNoteUp(key)} style={{ + width: blackKeyWidthExpr, + height: blackKeyHeightExpr, position: "absolute", left: `calc(calc(${whiteKeyWidthExpr} * ${ i + ((i > 1) as unknown as number) + 1 }) - calc(${blackKeyWidthExpr} / 2))`, top: "0px", }} - > - {({ isHovered, isPressed }) => ( - - {isNoteVisible( - showNoteNames, - isPressed, - isHovered, - false - ) && ( - - {key.note + key.accidental} - - )} - - )} - + text={{ + color: "white", + fontSize: "xs", + }} + /> ); })} @@ -195,6 +176,16 @@ const Octave = (props: OctaveProps) => { Octave.defaultProps = { startNote: "C", endNote: "B", + showNoteNames: "onpress", + showOctaveNumber: false, + whiteKeyBg: "white", + whiteKeyBgPressed: "gray.200", + whiteKeyBgHovered: "gray.100", + blackKeyBg: "black", + blackKeyBgPressed: "gray.600", + blackKeyBgHovered: "gray.700", + highlightedNotes: [], + defaultHighlightColor: "#FF0000", onNoteDown: () => {}, onNoteUp: () => {}, }; diff --git a/front/components/VirtualPiano/PianoKeyComp.tsx b/front/components/VirtualPiano/PianoKeyComp.tsx new file mode 100644 index 0000000..fe6f87f --- /dev/null +++ b/front/components/VirtualPiano/PianoKeyComp.tsx @@ -0,0 +1,109 @@ +import { Box, Pressable, Text } from "native-base"; +import { Key } from "react"; +import { StyleProp, ViewStyle } from "react-native"; +import { + Note, + PianoKey, + NoteNameBehavior, + octaveKeys, + Accidental, + HighlightedKey, + keyToStr, +} from "../../models/Piano"; + +type PianoKeyProps = { + key?: Key; + pianoKey: PianoKey; + showNoteNames: NoteNameBehavior; + bg: string; + bgPressed: string; + bgHovered: string; + onKeyDown: () => void; + onKeyUp: () => void; + text: Parameters[0]; + style: StyleProp; +}; + +const isNoteVisible = ( + noteNameBehavior: NoteNameBehavior, + isPressed: boolean, + isHovered: boolean +) => { + if (noteNameBehavior === NoteNameBehavior.always) return true; + if (noteNameBehavior === NoteNameBehavior.never) return false; + + if (noteNameBehavior === NoteNameBehavior.onpress) { + return isPressed; + } else if (noteNameBehavior === NoteNameBehavior.onhover) { + return isHovered; + } + return false; +}; + +const PianoKeyComp = ({ + key, + pianoKey, + showNoteNames, + bg, + bgPressed, + bgHovered, + onKeyDown, + onKeyUp, + text, + style, +}: PianoKeyProps) => { + const textDefaultProps = { + style: { + userSelect: "none", + WebkitUserSelect: "none", + MozUserSelect: "none", + msUserSelect: "none", + }, + fontSize: "xl", + color: "black", + } as Parameters[0]; + + const textProps = { ...textDefaultProps, ...text }; + return ( + + {({ isHovered, isPressed }) => ( + { + if (isPressed) return bgPressed; + if (isHovered) return bgHovered; + return bg; + })()} + w="100%" + h="100%" + borderWidth="1px" + borderColor="black" + justifyContent="flex-end" + alignItems="center" + > + {isNoteVisible(showNoteNames, isPressed, isHovered) && ( + {keyToStr(pianoKey)} + )} + + )} + + ); +}; + +PianoKeyComp.defaultProps = { + key: octaveKeys[0], + showNoteNames: NoteNameBehavior.onhover, + keyBg: "white", + keyBgPressed: "gray.200", + keyBgHovered: "gray.100", + onKeyDown: () => {}, + onKeyUp: () => {}, + text: {}, + style: {}, +}; + +export default PianoKeyComp; diff --git a/front/components/VirtualPiano/VirtualPiano.tsx b/front/components/VirtualPiano/VirtualPiano.tsx index 8d5106f..29bef1f 100644 --- a/front/components/VirtualPiano/VirtualPiano.tsx +++ b/front/components/VirtualPiano/VirtualPiano.tsx @@ -36,7 +36,15 @@ const VirtualPiano = ({ keyPressStyle, vividKeyPressColor, }: VirtualPianoProps) => { - const notesList: Array = ["C", "D", "E", "F", "G", "A", "B"]; + const notesList: Array = [ + Note.C, + Note.D, + Note.E, + Note.F, + Note.G, + Note.A, + Note.B, + ]; const octaveList = []; for (let octaveNum = startOctave; octaveNum <= endOctave; octaveNum++) { @@ -73,9 +81,9 @@ VirtualPiano.defaultProps = { console.log("Note up: " + n); }, startOctave: 2, - startNote: "C", + startNote: Note.C, endOctave: 2, - endNote: "B", + endNote: Note.B, showNoteNames: "onhover", highlightedNotes: [], highlightColor: "red", diff --git a/front/models/Piano.ts b/front/models/Piano.ts index 5ef07d6..ec8a199 100644 --- a/front/models/Piano.ts +++ b/front/models/Piano.ts @@ -1,38 +1,119 @@ +export enum Note { + "C", + "D", + "E", + "F", + "G", + "A", + "B", +} +export enum Accidental { + "#", + "b", + "##", + "bb", +} -export type Note = "C" | "D" | "E" | "F" | "G" | "A" | "B"; -export type Accidental = "#" | "b" | "##" | "bb"; - -export type NoteNameBehavior = "always" | "onpress" | "onhighlight" | "onhover" | "never"; -export type KeyPressStyle = "subtle" | "vivid"; +export enum NoteNameBehavior { + "always", + "onpress", + "onhighlight", + "onhover", + "never", +} +export enum KeyPressStyle { + "subtle", + "vivid", +}; +export type HighlightedKey = { + key: PianoKey; + // if not specified, the default color for highlighted notes will be used + bgColor?: string; +}; export class PianoKey { - public note: Note; - public accidental?: Accidental; - public octave?: number; + public note: Note; + public accidental?: Accidental; + public octave?: number; - constructor(note: Note, accidental?: Accidental, octave?: number) { - this.note = note; - this.accidental = accidental; - this.octave = octave; - }; + constructor(note: Note, accidental?: Accidental, octave?: number) { + this.note = note; + this.accidental = accidental; + this.octave = octave; + } - public toString = () => { - return this.note + (this.accidental || "") + (this.octave || ""); + public toString = () => { + return this.note as unknown as string + (this.accidental || "") + (this.octave || ""); + }; +} + +export const strToKey = (str: string): PianoKey => { + let note : Note; + switch (str[0]) { + case "C": note = Note.C; break; + case "D": note = Note.D; break; + case "E": note = Note.E; break; + case "F": note = Note.F; break; + case "G": note = Note.G; break; + case "A": note = Note.A; break; + case "B": note = Note.B; break; + default: throw new Error("Invalid note name"); } + if (str.length === 1) { + return new PianoKey(note); + } + let accidental : Accidental; + switch (str[1]) { + case "#": accidental = Accidental["#"]; break; + case "b": accidental = Accidental["b"]; break; + case "x": accidental = Accidental["##"]; break; + case "n": accidental = Accidental["bb"]; break; + default: throw new Error("Invalid accidental"); + } + if (str.length === 2) { + return new PianoKey(note, accidental); + } + const octave = parseInt(str[2] as unknown as string); + return new PianoKey(note, accidental, octave); +}; + +export const keyToStr = (key: PianoKey): string => { + let s = ""; + switch (key.note) { + case Note.C: s += "C"; break; + case Note.D: s += "D"; break; + case Note.E: s += "E"; break; + case Note.F: s += "F"; break; + case Note.G: s += "G"; break; + case Note.A: s += "A"; break; + case Note.B: s += "B"; break; + } + if (key.accidental) { + switch (key.accidental) { + case Accidental["#"]: s += "#"; break; + case Accidental["b"]: s += "b"; break; + case Accidental["##"]: s += "x"; break; + case Accidental["bb"]: s += "n"; break; + } + } + if (key.octave) { + s += key.octave; + } + return s; }; export const octaveKeys: Array = [ - new PianoKey("C", undefined), - new PianoKey("C", "#"), - new PianoKey("D", undefined), - new PianoKey("D", "#"), - new PianoKey("E", undefined), - new PianoKey("F", undefined), - new PianoKey("F", "#"), - new PianoKey("G", undefined), - new PianoKey("G", "#"), - new PianoKey("A", undefined), - new PianoKey("A", "#"), - new PianoKey("B", undefined), + new PianoKey(Note.C), + new PianoKey(Note.C, Accidental["#"]), + new PianoKey(Note.D), + new PianoKey(Note.D, Accidental["#"]), + new PianoKey(Note.E), + new PianoKey(Note.F), + new PianoKey(Note.F, Accidental["#"]), + new PianoKey(Note.G), + new PianoKey(Note.G, Accidental["#"]), + new PianoKey(Note.A), + new PianoKey(Note.A, Accidental["#"]), + new PianoKey(Note.B), ]; \ No newline at end of file