diff --git a/.eslintrc b/.eslintrc index 760bb4c9..1779df7a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,12 @@ "rules": { // disable the rule for all files "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-explicit-any": "off" + "@typescript-eslint/no-explicit-any": "off", + "new-cap": [ + "error", { + "capIsNewExceptionPattern": "^Immu*" + } + ] }, "root": true } diff --git a/package.json b/package.json index 3f1963b5..7d969a3a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "osc": "^2.4.1", "react": "17.0.2", "react-dom": "17.0.2", - "react-piano": "^3.1.3", "react-redux": "^7.2.4", "redux": "^4.1.0", "redux-thunk": "^2.3.0", diff --git a/src/components/PianoKeyboard.tsx b/src/components/PianoKeyboard.tsx index 77907b18..6a65cf6f 100644 --- a/src/components/PianoKeyboard.tsx +++ b/src/components/PianoKeyboard.tsx @@ -1,59 +1,231 @@ -import { memo, useCallback } from "react"; -import { MidiNumbers } from "react-piano"; -import { DimensionsProvider } from "../contexts/dimension"; -import ResponsivePiano from "./ResponsivePiano"; -import { triggerRemoteMidiNoteEvent } from "../actions/device"; +import { FunctionComponent, memo, PointerEvent, useCallback, useEffect, useRef, useState } from "react"; +import { Set as ImmuSet } from "immutable"; import { useAppDispatch } from "../hooks/useAppDispatch"; +import { triggerRemoteMidiNoteEvent } from "../actions/device"; import styled from "styled-components"; +import { clamp } from "../lib/util"; const MIDIWrapper = styled.div` display: flex; justify-content: center; + user-select: none; + touch-action: none; +`; + + +const KeyWidth = 30; +const OctaveWidth = KeyWidth * 7; +const BaseOctave = 4; + +interface NoteElementProps { + index: number; + isActive: boolean; + isWhiteKey: boolean; +} + +const NoteElement = styled.div.attrs(({ + index, + isWhiteKey +}) => ({ + style: { + height: isWhiteKey ? "100%" : "60%", + left: isWhiteKey ? index * KeyWidth : index * KeyWidth + 0.5 * KeyWidth, + zIndex: isWhiteKey ? 2 : 3 + } +}))` + + background-color: ${({ isActive, isWhiteKey, theme }) => isActive ? theme.colors.primary : isWhiteKey ? "#fff" : "#333" }; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + border-color: gray; + border-style: solid; + border-width: 1px; + position: absolute; + top: 0; + width: ${KeyWidth}px; +`; + +const Note: FunctionComponent<{ + index: number; + isActive: boolean; + isWhiteKey: boolean; + note: number; + onNoteOn: (n: number) => any; + onNoteOff: (n: number) => any; +}> = memo(({ + index, + isActive, + isWhiteKey, + note, + onNoteOff, + onNoteOn +}) => { + + const onPointerDown = (e: PointerEvent) => { + if (isActive) return; + onNoteOn(note); + }; + + const onPointerEnter = (e: PointerEvent) => { + if (e.pointerType === "mouse" && !e.buttons) return; + onNoteOn(note); + }; + + const onPointerLeave = (e: PointerEvent) => { + if (!isActive) return; + if (e.pointerType === "mouse" && !e.buttons) return; + onNoteOff(note); + }; + + const onPointerUp = (e: PointerEvent) => { + if (!isActive) return; + onNoteOff(note); + }; + + return ( + + ); +}); + +Note.displayName = "Note"; - .keyboardContainer { - height: 15rem; - width: 80%; +const OctaveElement = styled.div` + height: 150px; + position: relative; + user-select: none; + width: ${OctaveWidth}px; + + &:not(:last-child) { + border-right: none; } - .keyboardContainer > div { + > div { height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; } +`; + +const Octave: FunctionComponent<{ + octave: number; + activeNotes: ImmuSet; + onNoteOn: (note: number) => any; + onNoteOff: (note: number) => any; +}> = memo(({ + octave, + activeNotes, + onNoteOn, + onNoteOff +}) => { + + const start = 12 * octave; + const whiteNotes: JSX.Element[] = []; + const blackNotes: JSX.Element[] = []; + + for (let i = 0, key = start; i < 7; i++) { - @media screen and (max-width: 35.5em) { - padding-top: 5rem; - .keyboardContainer { - height: 10rem; - width: 100%; + // create a white key for every entry + whiteNotes.push(); + key++; + + // create black key?! + if (i !== 2 && i !== 6) { + blackNotes.push( + + ); + key++; } } -`; -const noteRange = { - first: MidiNumbers.fromNote("c3"), - last: MidiNumbers.fromNote("f4") -}; + return ( + +
+ { blackNotes } + { whiteNotes } +
+
+ ); +}); -const PianoKeyboard = memo(function WrappedPianoKeyboard() { +Octave.displayName = "OctaveName"; + +export const PianoKeyboard: FunctionComponent<{}> = memo(() => { const dispatch = useAppDispatch(); + const containerRef = useRef(); + const [activeNotes, setActiveNotes] = useState(ImmuSet()); + const [noOfOctaves, setNoOfOctaves] = useState(4); const onNoteOn = useCallback((p: number) => { dispatch(triggerRemoteMidiNoteEvent(p, true)); + setActiveNotes((notes: ImmuSet) => notes.add(p)); }, [dispatch]); const onNoteOff = useCallback((p: number) => { dispatch(triggerRemoteMidiNoteEvent(p, false)); + setActiveNotes((notes: ImmuSet) => notes.delete(p)); }, [dispatch]); - return ( - -
- - - -
+ useEffect(() => { + const onResize = (ev?: UIEvent) => { + if (!containerRef.current) return; + const { width } = containerRef.current.getBoundingClientRect(); + setNoOfOctaves(clamp(Math.floor(width / OctaveWidth), 1, 6)); + }; + + window.addEventListener("resize", onResize); + onResize(); + + return () => window.removeEventListener("resize", onResize); + }, []); + + const octs: JSX.Element[] = []; + + for (let i = 0; i < noOfOctaves; i++) { + octs.push(); + } + + return ( + + { octs } ); }); +PianoKeyboard.displayName = "PianoKeyboard"; + export default PianoKeyboard; diff --git a/src/components/ResponsivePiano.tsx b/src/components/ResponsivePiano.tsx deleted file mode 100644 index 91be1fea..00000000 --- a/src/components/ResponsivePiano.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { memo } from "react"; -import { Piano } from "react-piano"; -import "react-piano/dist/styles.css"; -import { useDimensions } from "../hooks/useDimensions"; - -interface ResponsivePianoParams { - noteRange: { first: number; last: number}; - onNoteOn: (p: number) => void; - onNoteOff: (p: number) => void; -} - -const ResponsivePiano = memo(function WrappedResponsivePiano({ noteRange, onNoteOn, onNoteOff }: ResponsivePianoParams) { - - const dimensions = useDimensions(); - - return ( - - ); -}); - -export default ResponsivePiano; diff --git a/src/contexts/dimension.tsx b/src/contexts/dimension.tsx deleted file mode 100644 index 2fe9d9d3..00000000 --- a/src/contexts/dimension.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { createContext, useEffect, useRef, useState, FunctionComponent } from 'react'; - -export type Dimensions = { - width: number; - height: number -} - -export const DimensionsContext = createContext(null); - -export const DimensionsProvider: FunctionComponent<{}> = ({ children }) => { - - const container = useRef(null); - const [dim, setDim] = useState({ width: 0, height: 0 }); - - useEffect(() => { - if (container.current) { - container.current.addEventListener("resize", () => { - const width = container.current.clientWidth; - const height = container.current.clientHeight; - - setDim({ width, height }); - }); - } - }, [container]) - - return
- - { children } - -
-} - -export const DimensionsConsumer = DimensionsContext.Consumer; diff --git a/src/hooks/useDimensions.ts b/src/hooks/useDimensions.ts deleted file mode 100644 index dc4fcbc9..00000000 --- a/src/hooks/useDimensions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useContext } from "react"; -import { DimensionsContext } from "../contexts/dimension"; - -export const useDimensions = () => { - const dimensions = useContext(DimensionsContext); - return dimensions; -}; diff --git a/src/lib/util.ts b/src/lib/util.ts index 42ce65db..0d2144ed 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1 +1,9 @@ export const sleep = (t: number): Promise => new Promise(resolve => setTimeout(resolve, t)); + +export const clamp = (num: number, min: number, max: number): number => { + return Math.min(Math.max(num, min), max); +}; + +export const scale = (x: number, inLow: number, inHigh: number, outLow: number, outHigh: number): number => { + return (x - inLow) * (outHigh - outLow) / (inHigh - inLow) + outLow; +}; diff --git a/yarn.lock b/yarn.lock index 844738f4..b2e841ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1957,11 +1957,6 @@ classnames@2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== -classnames@^2.2.6: - version "2.3.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" - integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== - code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -3294,11 +3289,6 @@ jsonfile@^6.0.1: array-includes "^3.1.2" object.assign "^4.1.2" -just-range@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/just-range/-/just-range-2.2.0.tgz#75c858f79b474ecf9ac463d2cad0320ea86a2472" - integrity sha512-JKHygNvIu+tX/+oOI+zNznQE0M8jM241fsjuac2i9OBlIXD7w4CGGuakXYc6Dne5vv9pEXgmCv8+WEFKwiGFTg== - language-subtag-registry@~0.3.2: version "0.3.21" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" @@ -3363,11 +3353,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.difference@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" - integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -4006,7 +3991,7 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prop-types@15.7.2, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.7.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -4131,16 +4116,6 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-piano@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/react-piano/-/react-piano-3.1.3.tgz#b4b679747afbb88e5047721bcdffc54397f5f25b" - integrity sha512-5SDFzprP+ICEMOJRkB0iqHo1qGcZpinRJ0ZW4HZu0pT6NftK+MpVN19VhXzd+MMN9C7QyGNB0nO4nd/cpGko/Q== - dependencies: - classnames "^2.2.6" - just-range "^2.1.0" - lodash.difference "^4.5.0" - prop-types "^15.6.2" - react-redux@^7.2.4: version "7.2.4" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"