pretty big changes: added highlighted keys refactored Octave component to use a PianoKeyComponent and updated TS types to enums

This commit is contained in:
Clément Le Bihan
2023-03-20 00:13:57 +01:00
parent a9cd0f16ae
commit efede253dc
4 changed files with 313 additions and 124 deletions
+85 -94
View File
@@ -4,11 +4,10 @@ import {
NoteNameBehavior,
octaveKeys,
Accidental,
HighlightedKey,
} from "../../models/Piano";
import { Box, Row, Pressable, ZStack, Text } from "native-base";
const notesList: Array<Note> = ["C", "D", "E", "F", "G", "A", "B"];
const accidentalsList: Array<Accidental> = ["#", "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<typeof Box>[0] & {
endNote: Note;
showNoteNames: NoteNameBehavior;
showOctaveNumber: boolean;
whiteKeyBg: string;
whiteKeyBgPressed: string;
whiteKeyBgHovered: string;
blackKeyBg: string;
blackKeyBgPressed: string;
blackKeyBgHovered: string;
highlightedNotes: Array<HighlightedKey>;
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) => {
<Box {...props}>
<Row height={"100%"} width={"100%"}>
{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 (
<Pressable
width={whiteKeyWidthExpr}
height={whiteKeyHeightExpr}
key={i}
onPressIn={() => onNoteDown(key)}
onPressOut={() => onNoteUp(key)}
>
{({ isHovered, isPressed }) => (
<Box
bg={
isHovered ? (isPressed ? "gray.300" : "gray.100") : "white"
}
w="100%"
h="100%"
borderWidth="1px"
borderColor="black"
justifyContent="flex-end"
alignItems="center"
>
{isNoteVisible(
showNoteNames,
isPressed,
isHovered,
false
) && (
<Text
style={{
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
}}
fontSize="xl"
>
{key.note}
</Text>
)}
</Box>
)}
</Pressable>
<PianoKeyComp
key={i}
pianoKey={key}
bg={isHighlighted ? highlightColor :whiteKeyBg}
bgPressed={isHighlighted ? highlightColor : whiteKeyBgPressed}
bgHovered={isHighlighted ? highlightColor : whiteKeyBgHovered}
onKeyDown={() => 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 (
<Pressable
<PianoKeyComp
key={i}
onPressIn={() => 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 }) => (
<Box
bg={
isHovered ? (isPressed ? "gray.700" : "gray.800") : "black"
}
w="100%"
h="100%"
borderWidth="1px"
borderColor="black"
color="white"
style={{
justifyContent: "flex-end",
alignItems: "center",
}}
>
{isNoteVisible(
showNoteNames,
isPressed,
isHovered,
false
) && (
<Text
style={{
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
}}
fontSize="xs"
color="white"
>
{key.note + key.accidental}
</Text>
)}
</Box>
)}
</Pressable>
text={{
color: "white",
fontSize: "xs",
}}
/>
);
})}
</Row>
@@ -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: () => {},
};
@@ -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<typeof Text>[0];
style: StyleProp<ViewStyle>;
};
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<typeof Text>[0];
const textProps = { ...textDefaultProps, ...text };
return (
<Pressable
key={key}
onPressIn={onKeyDown}
onPressOut={onKeyUp}
style={style}
>
{({ isHovered, isPressed }) => (
<Box
bg={(() => {
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) && (
<Text {...textProps}>{keyToStr(pianoKey)}</Text>
)}
</Box>
)}
</Pressable>
);
};
PianoKeyComp.defaultProps = {
key: octaveKeys[0],
showNoteNames: NoteNameBehavior.onhover,
keyBg: "white",
keyBgPressed: "gray.200",
keyBgHovered: "gray.100",
onKeyDown: () => {},
onKeyUp: () => {},
text: {},
style: {},
};
export default PianoKeyComp;
+11 -3
View File
@@ -36,7 +36,15 @@ const VirtualPiano = ({
keyPressStyle,
vividKeyPressColor,
}: VirtualPianoProps) => {
const notesList: Array<Note> = ["C", "D", "E", "F", "G", "A", "B"];
const notesList: Array<Note> = [
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",
+108 -27
View File
@@ -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<PianoKey> = [
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),
];