Keyboard map is the one responsible for handling mouse, touch, and keyboard events. This is to keep the keyboard rendering and external events handling separate.master
@@ -0,0 +1 @@ | |||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1" /> |
@@ -3,6 +3,7 @@ import * as PropTypes from 'prop-types' | |||||
import StyledAccidentalKey from '../StyledAccidentalKey/StyledAccidentalKey' | import StyledAccidentalKey from '../StyledAccidentalKey/StyledAccidentalKey' | ||||
import StyledNaturalKey from '../StyledNaturalKey/StyledNaturalKey' | import StyledNaturalKey from '../StyledNaturalKey/StyledNaturalKey' | ||||
import Keyboard, { propTypes } from './Keyboard' | import Keyboard, { propTypes } from './Keyboard' | ||||
import KeyboardMap from '../KeyboardMap/KeyboardMap' | |||||
const Wrapper: React.FC = (props) => ( | const Wrapper: React.FC = (props) => ( | ||||
<div | <div | ||||
@@ -144,3 +145,102 @@ export const AnotherStyled = (props?: Partial<Props>) => ( | |||||
</Wrapper> | </Wrapper> | ||||
</div> | </div> | ||||
) | ) | ||||
const HasMapComponent = () => { | |||||
const [keyChannels, setKeyChannels] = React.useState<{ key: number; velocity: number; channel: number }[]>([]) | |||||
const midiAccess = React.useRef<any>(undefined) | |||||
const handleKeyOn = (newKeys: { key: number; velocity: number; channel: number; id: number }[]) => { | |||||
setKeyChannels((oldKeys) => { | |||||
const oldKeysKeys = oldKeys.map((k) => k.key) | |||||
const newKeysKeys = newKeys.map((k) => k.key) | |||||
const keysOff = oldKeys | |||||
.filter((ok) => !newKeysKeys.includes(ok.key)) | |||||
.map((k) => ({ | |||||
...k, | |||||
velocity: k.velocity > 1 ? 1 : k.velocity < 0 ? 0 : k.velocity, | |||||
})) | |||||
const keysOn = newKeys | |||||
.filter((nk) => !oldKeysKeys.includes(nk.key)) | |||||
.map((k) => ({ | |||||
...k, | |||||
velocity: k.velocity > 1 ? 1 : k.velocity < 0 ? 0 : k.velocity, | |||||
})) | |||||
keysOn.forEach((k) => { | |||||
midiAccess.current?.send([0b10010000 + k.channel, k.key, Math.floor(k.velocity * 127)]) | |||||
}) | |||||
keysOff.forEach((k) => { | |||||
midiAccess.current?.send([0b10000000 + k.channel, k.key, Math.floor(k.velocity * 127)]) | |||||
}) | |||||
return newKeys | |||||
}) | |||||
} | |||||
React.useEffect(() => { | |||||
const { navigator: maybeNavigator } = window | |||||
const navigator = maybeNavigator as Navigator & { | |||||
requestMIDIAccess: () => Promise<{ outputs: Map<string, unknown> }> | |||||
} | |||||
if ('requestMIDIAccess' in navigator) { | |||||
navigator.requestMIDIAccess().then((m) => { | |||||
midiAccess.current = Array.from(m.outputs.values())[0] | |||||
}) | |||||
} | |||||
}, []) | |||||
return ( | |||||
<Wrapper> | |||||
<Keyboard hasMap startKey={21} endKey={108} keyChannels={keyChannels}> | |||||
<KeyboardMap | |||||
channel={0} | |||||
onChange={handleKeyOn} | |||||
keyboardMapping={{ | |||||
KeyQ: 60, | |||||
Digit2: 61, | |||||
KeyW: 62, | |||||
Digit3: 63, | |||||
KeyE: 64, | |||||
KeyR: 65, | |||||
Digit5: 66, | |||||
KeyT: 67, | |||||
Digit6: 68, | |||||
KeyY: 69, | |||||
Digit7: 70, | |||||
KeyU: 71, | |||||
KeyI: 72, | |||||
Digit9: 73, | |||||
KeyO: 74, | |||||
Digit0: 75, | |||||
KeyP: 76, | |||||
BracketLeft: 77, | |||||
Equal: 78, | |||||
BracketRight: 79, | |||||
KeyZ: 48, | |||||
KeyS: 49, | |||||
KeyX: 50, | |||||
KeyD: 51, | |||||
KeyC: 52, | |||||
KeyV: 53, | |||||
KeyG: 54, | |||||
KeyB: 55, | |||||
KeyH: 56, | |||||
KeyN: 57, | |||||
KeyJ: 58, | |||||
KeyM: 59, | |||||
Comma: 60, | |||||
KeyL: 61, | |||||
Period: 62, | |||||
Semicolon: 63, | |||||
Slash: 64, | |||||
}} | |||||
/> | |||||
</Keyboard> | |||||
</Wrapper> | |||||
) | |||||
} | |||||
export const HasMap = () => <HasMapComponent /> |
@@ -18,6 +18,11 @@ export const propTypes = { | |||||
*/ | */ | ||||
endKey: PropTypes.number.isRequired, | endKey: PropTypes.number.isRequired, | ||||
/** | |||||
* Does the component have a clickable map? | |||||
*/ | |||||
hasMap: PropTypes.bool, | |||||
//octaveDivision: PropTypes.number, | //octaveDivision: PropTypes.number, | ||||
/** | /** | ||||
@@ -61,12 +66,12 @@ type Props = PropTypes.InferProps<typeof propTypes> | |||||
* Component for displaying musical notes in the form of a piano keyboard. | * Component for displaying musical notes in the form of a piano keyboard. | ||||
* @param startKey - MIDI note of the first key. | * @param startKey - MIDI note of the first key. | ||||
* @param endKey - MIDI note of the last key. | * @param endKey - MIDI note of the last key. | ||||
* @param hasMap - The component's clickable map component. | |||||
* @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. | * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. | ||||
* @param keyChannels - Current active keys and their channel assignments. | * @param keyChannels - Current active keys and their channel assignments. | ||||
* @param width - Width of the component. | * @param width - Width of the component. | ||||
* @param keyComponents - Components to use for each kind of key. | * @param keyComponents - Components to use for each kind of key. | ||||
* @param height - Height of the component. | * @param height - Height of the component. | ||||
* @constructor | |||||
*/ | */ | ||||
const Keyboard: React.FC<Props> = ({ | const Keyboard: React.FC<Props> = ({ | ||||
startKey, | startKey, | ||||
@@ -77,6 +82,7 @@ const Keyboard: React.FC<Props> = ({ | |||||
width = '100%', | width = '100%', | ||||
keyComponents = {}, | keyComponents = {}, | ||||
height = 80, | height = 80, | ||||
children, | |||||
}) => { | }) => { | ||||
const [clientSide, setClientSide] = React.useState(false) | const [clientSide, setClientSide] = React.useState(false) | ||||
const [clientSideKeys, setClientSideKeys] = React.useState<number[]>([]) | const [clientSideKeys, setClientSideKeys] = React.useState<number[]>([]) | ||||
@@ -86,6 +92,7 @@ const Keyboard: React.FC<Props> = ({ | |||||
const getKeyWidth = React.useCallback((k) => getKeyWidthUnmemoized(startKey, endKey)(k), [startKey, endKey]) | const getKeyWidth = React.useCallback((k) => getKeyWidthUnmemoized(startKey, endKey)(k), [startKey, endKey]) | ||||
const getKeyLeft = React.useCallback((k) => getKeyLeftUnmemoized(startKey, endKey)(k), [startKey, endKey]) | const getKeyLeft = React.useCallback((k) => getKeyLeftUnmemoized(startKey, endKey)(k), [startKey, endKey]) | ||||
const isNaturalKey = React.useCallback((k) => isNaturalKeyUnmemoized(k), []) | const isNaturalKey = React.useCallback((k) => isNaturalKeyUnmemoized(k), []) | ||||
const baseRef = React.useRef<HTMLDivElement>(null) | |||||
React.useEffect(() => { | React.useEffect(() => { | ||||
setClientSide(true) | setClientSide(true) | ||||
@@ -107,20 +114,62 @@ const Keyboard: React.FC<Props> = ({ | |||||
overflow: 'hidden', | overflow: 'hidden', | ||||
}} | }} | ||||
role="presentation" | role="presentation" | ||||
ref={baseRef} | |||||
> | > | ||||
{keys.map((key) => { | {keys.map((key) => { | ||||
const isNatural = isNaturalKey(key) | const isNatural = isNaturalKey(key) | ||||
const Component: any = isNatural ? NaturalKey! : AccidentalKey! | const Component: any = isNatural ? NaturalKey! : AccidentalKey! | ||||
const currentKeyChannels = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === key) : null | const currentKeyChannels = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === key) : null | ||||
const width = getKeyWidth(key) | |||||
const left = getKeyLeft(key) | |||||
let leftBounds: number | |||||
let rightBounds: number | |||||
switch (key % 12) { | |||||
case 0: | |||||
case 5: | |||||
leftBounds = left | |||||
rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1) | |||||
break | |||||
case 4: | |||||
case 11: | |||||
leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1) | |||||
rightBounds = left + width | |||||
break | |||||
case 2: | |||||
case 7: | |||||
case 9: | |||||
leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1) | |||||
rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1) | |||||
break | |||||
default: | |||||
leftBounds = left | |||||
rightBounds = left + width | |||||
break | |||||
} | |||||
const octaveStart = Math.floor(key / 12) * 12 | |||||
const octaveEnd = octaveStart + 11 | |||||
const octaveLeftBounds = getKeyLeft(octaveStart) | |||||
const octaveRightBounds = getKeyLeft(octaveEnd) + getKeyWidth(octaveEnd) | |||||
return ( | return ( | ||||
<div | <div | ||||
key={key} | key={key} | ||||
data-key={key} | |||||
data-octave-left-bounds={octaveLeftBounds} | |||||
data-octave-right-bounds={octaveRightBounds} | |||||
data-left-bounds={leftBounds} | |||||
data-right-bounds={rightBounds} | |||||
data-left-full-bounds={isNatural ? left : undefined} | |||||
data-right-full-bounds={isNatural ? left + width : undefined} | |||||
style={{ | style={{ | ||||
zIndex: isNatural ? 0 : 2, | zIndex: isNatural ? 0 : 2, | ||||
width: getKeyWidth(key) + '%', | |||||
width: width + '%', | |||||
height: (isNatural ? 100 : 100 * accidentalKeyLengthRatio!) + '%', | height: (isNatural ? 100 : 100 * accidentalKeyLengthRatio!) + '%', | ||||
left: getKeyLeft(key) + '%', | |||||
left: left + '%', | |||||
position: 'absolute', | position: 'absolute', | ||||
top: 0, | top: 0, | ||||
}} | }} | ||||
@@ -129,6 +178,15 @@ const Keyboard: React.FC<Props> = ({ | |||||
</div> | </div> | ||||
) | ) | ||||
})} | })} | ||||
{children! && | |||||
React.Children.map(children, (unknownChild) => { | |||||
const child = unknownChild as React.ReactElement | |||||
const { props = {} } = child | |||||
return React.cloneElement(child, { | |||||
...props, | |||||
accidentalKeyLengthRatio, | |||||
}) | |||||
})} | |||||
</div> | </div> | ||||
) | ) | ||||
} | } | ||||
@@ -0,0 +1,328 @@ | |||||
import * as React from 'react' | |||||
import * as PropTypes from 'prop-types' | |||||
import reverseGetKeyFromPoint from '../../services/reverseGetKeyFromPoint' | |||||
const propTypes = { | |||||
/** | |||||
* Event handler triggered upon change in activated keys in the component. | |||||
*/ | |||||
onChange: PropTypes.func, | |||||
/** | |||||
* Map from key code to key number. | |||||
*/ | |||||
keyboardMapping: PropTypes.object, | |||||
/** | |||||
* Active MIDI channel for registering keys. | |||||
*/ | |||||
channel: PropTypes.number.isRequired, | |||||
} | |||||
type Props = PropTypes.InferProps<typeof propTypes> & { accidentalKeyLengthRatio?: number } | |||||
/** | |||||
* Keyboard map for allowing interactivity with the keyboard. | |||||
* @param channel - Active MIDI channel for registering keys. | |||||
* @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. This is set by the Keyboard component. | |||||
* @param onChange - Event handler triggered upon change in activated keys in the component. | |||||
* @param keyboardMapping - Map from key code to key number. | |||||
*/ | |||||
const KeyboardMap: React.FC<Props> = ({ channel, accidentalKeyLengthRatio, onChange, keyboardMapping = {} }) => { | |||||
const baseRef = React.useRef<HTMLDivElement>(null) | |||||
const keysOnRef = React.useRef<any[]>([]) | |||||
const lastVelocity = React.useRef<number | undefined>(undefined) | |||||
const isTouch = React.useRef<boolean>(false) | |||||
const handleContextMenu: React.EventHandler<any> = (e) => { | |||||
e.preventDefault() | |||||
} | |||||
const handleDragStart: React.DragEventHandler = (e) => { | |||||
e.preventDefault() | |||||
} | |||||
const handleMouseDown: React.MouseEventHandler = (e) => { | |||||
if (isTouch.current) { | |||||
return | |||||
} | |||||
if (baseRef.current === null) { | |||||
return | |||||
} | |||||
if (baseRef.current.parentElement === null) { | |||||
return | |||||
} | |||||
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)( | |||||
e.clientX, | |||||
e.clientY, | |||||
) | |||||
if (keyData! === null) { | |||||
return | |||||
} | |||||
if (e.buttons === 1) { | |||||
if (lastVelocity.current === undefined) { | |||||
lastVelocity.current = keyData.velocity | |||||
} | |||||
keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, channel, id: -1 }] | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
} | |||||
} | |||||
const handleTouchStart: React.TouchEventHandler = (e) => { | |||||
isTouch.current = true | |||||
if (baseRef.current === null) { | |||||
return | |||||
} | |||||
if (baseRef.current.parentElement === null) { | |||||
return | |||||
} | |||||
Array.from(e.changedTouches).forEach((t) => { | |||||
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)( | |||||
t.clientX, | |||||
t.clientY, | |||||
) | |||||
if (keyData! === null) { | |||||
return | |||||
} | |||||
if (lastVelocity.current === undefined) { | |||||
lastVelocity.current = keyData.velocity | |||||
} | |||||
keysOnRef.current = [ | |||||
...keysOnRef.current, | |||||
{ ...keyData, velocity: lastVelocity.current, channel, id: t.identifier }, | |||||
] | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
}) | |||||
} | |||||
React.useEffect(() => { | |||||
const handleTouchMove = (e: TouchEvent) => { | |||||
if (baseRef.current === null) { | |||||
return | |||||
} | |||||
if (baseRef.current.parentElement === null) { | |||||
return | |||||
} | |||||
e.preventDefault() | |||||
Array.from(e.changedTouches).forEach((t) => { | |||||
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)( | |||||
t.clientX, | |||||
t.clientY, | |||||
) | |||||
if (keyData! === null) { | |||||
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier) | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
return | |||||
} | |||||
const [mouseKey = null] = keysOnRef.current.filter((k) => k.id === t.identifier) | |||||
if (mouseKey === null) { | |||||
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier) | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
return | |||||
} | |||||
if (mouseKey.key !== keyData.key) { | |||||
keysOnRef.current = [ | |||||
...keysOnRef.current.filter((k) => k.id !== t.identifier), | |||||
{ | |||||
...keyData, | |||||
channel, | |||||
velocity: lastVelocity.current, | |||||
id: t.identifier, | |||||
}, | |||||
] | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
} | |||||
}) | |||||
} | |||||
window.addEventListener('touchmove', handleTouchMove, { passive: false }) | |||||
return () => { | |||||
window.removeEventListener('touchmove', handleTouchMove) | |||||
} | |||||
}, [accidentalKeyLengthRatio, channel, onChange]) | |||||
React.useEffect(() => { | |||||
const handleMouseMove = (e: MouseEvent) => { | |||||
e.preventDefault() | |||||
if (baseRef.current === null) { | |||||
return | |||||
} | |||||
if (baseRef.current.parentElement === null) { | |||||
return | |||||
} | |||||
if (e.buttons === 1) { | |||||
const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement, accidentalKeyLengthRatio!)( | |||||
e.clientX, | |||||
e.clientY, | |||||
) | |||||
if (keyData! === null) { | |||||
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1) | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
return | |||||
} | |||||
const [mouseKey = null] = keysOnRef.current.filter((k) => k.id === -1) | |||||
if (mouseKey === null) { | |||||
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1) | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
return | |||||
} | |||||
if (mouseKey.key !== keyData.key) { | |||||
keysOnRef.current = [ | |||||
...keysOnRef.current.filter((k) => k.id !== -1), | |||||
{ ...keyData, velocity: lastVelocity.current, channel, id: -1 }, | |||||
] | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
} | |||||
} | |||||
} | |||||
window.addEventListener('mousemove', handleMouseMove) | |||||
return () => { | |||||
window.removeEventListener('mousemove', handleMouseMove) | |||||
} | |||||
}, [accidentalKeyLengthRatio, channel, onChange]) | |||||
React.useEffect(() => { | |||||
const handleTouchEnd = (e: TouchEvent) => { | |||||
if (baseRef.current === null) { | |||||
return | |||||
} | |||||
if (baseRef.current.parentElement === null) { | |||||
return | |||||
} | |||||
Array.from(e.changedTouches).forEach((t) => { | |||||
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier) | |||||
lastVelocity.current = undefined | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
}) | |||||
} | |||||
window.addEventListener('touchend', handleTouchEnd) | |||||
return () => { | |||||
window.removeEventListener('touchend', handleTouchEnd) | |||||
} | |||||
}) | |||||
React.useEffect(() => { | |||||
const handleMouseUp = (e: MouseEvent) => { | |||||
e.preventDefault() | |||||
if (baseRef.current === null) { | |||||
return | |||||
} | |||||
if (baseRef.current.parentElement === null) { | |||||
return | |||||
} | |||||
keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1) | |||||
lastVelocity.current = undefined | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
} | |||||
window.addEventListener('mouseup', handleMouseUp) | |||||
return () => { | |||||
window.removeEventListener('mouseup', handleMouseUp) | |||||
} | |||||
}, [accidentalKeyLengthRatio, channel, onChange]) | |||||
React.useEffect(() => { | |||||
const baseRefComponent = baseRef.current | |||||
const handleKeyDown = (e: KeyboardEvent) => { | |||||
if (!keyboardMapping!) { | |||||
return | |||||
} | |||||
const { [e.code]: key = null } = keyboardMapping as Record<string, number> | |||||
if (key === null) { | |||||
return | |||||
} | |||||
if (keysOnRef.current.some((k) => k.key === key && k.id === -2)) { | |||||
return | |||||
} | |||||
keysOnRef.current = [...keysOnRef.current, { key, velocity: 0.75, channel, id: -2 }] | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
} | |||||
if (baseRefComponent) { | |||||
baseRefComponent.addEventListener('keydown', handleKeyDown) | |||||
} | |||||
return () => { | |||||
if (baseRefComponent) { | |||||
baseRefComponent.removeEventListener('keydown', handleKeyDown) | |||||
} | |||||
} | |||||
}) | |||||
React.useEffect(() => { | |||||
const handleKeyUp = (e: KeyboardEvent) => { | |||||
if (!keyboardMapping!) { | |||||
return | |||||
} | |||||
const { [e.code]: key = null } = keyboardMapping as Record<string, number> | |||||
if (key === null) { | |||||
return | |||||
} | |||||
keysOnRef.current = keysOnRef.current.filter((k) => k.key !== key) | |||||
if (typeof onChange! === 'function') { | |||||
onChange(keysOnRef.current) | |||||
} | |||||
} | |||||
window.addEventListener('keyup', handleKeyUp) | |||||
return () => { | |||||
window.removeEventListener('keyup', handleKeyUp) | |||||
} | |||||
}) | |||||
return ( | |||||
<div | |||||
ref={baseRef} | |||||
style={{ | |||||
position: 'absolute', | |||||
top: 0, | |||||
left: 0, | |||||
width: '100%', | |||||
height: '100%', | |||||
zIndex: 4, | |||||
}} | |||||
onContextMenu={handleContextMenu} | |||||
onDragStart={handleDragStart} | |||||
onMouseDown={handleMouseDown} | |||||
onTouchStart={handleTouchStart} | |||||
tabIndex={0} | |||||
/> | |||||
) | |||||
} | |||||
KeyboardMap.propTypes = propTypes | |||||
export default KeyboardMap |
@@ -1,7 +1,8 @@ | |||||
import Keyboard from './components/Keyboard/Keyboard' | import Keyboard from './components/Keyboard/Keyboard' | ||||
import KeyboardMap from './components/KeyboardMap/KeyboardMap' | |||||
import StyledNaturalKey from './components/StyledNaturalKey/StyledNaturalKey' | import StyledNaturalKey from './components/StyledNaturalKey/StyledNaturalKey' | ||||
import StyledAccidentalKey from './components/StyledAccidentalKey/StyledAccidentalKey' | import StyledAccidentalKey from './components/StyledAccidentalKey/StyledAccidentalKey' | ||||
export default Keyboard | export default Keyboard | ||||
export { StyledNaturalKey, StyledAccidentalKey } | |||||
export { StyledNaturalKey, StyledAccidentalKey, KeyboardMap } |
@@ -0,0 +1,59 @@ | |||||
type ReverseGetKeyFromPoint = ( | |||||
baseElement: HTMLElement, | |||||
accidentalKeyLengthRatio: number, | |||||
) => (clientX: number, clientY?: number) => { key: number; velocity: number } | null | |||||
const reverseGetKeyFromPoint: ReverseGetKeyFromPoint = (baseElement, accidentalKeyLengthRatio) => { | |||||
const { top, left, width, height } = baseElement.getBoundingClientRect() | |||||
return (clientX, clientY = top) => { | |||||
const realTop = clientY - top | |||||
const realLeft = clientX - left | |||||
// convert the clientX to units in which keys are displayed (percentage) | |||||
const leftInKeyUnits = (realLeft / width) * 100 | |||||
const maybeAccidental = realTop <= height * accidentalKeyLengthRatio! | |||||
const keysArray = Array.from(baseElement.children) as HTMLElement[] | |||||
const keys = keysArray.filter((c) => 'key' in c.dataset) | |||||
const currentOctave = keys.filter((k) => { | |||||
const octaveLeftBounds = Number(k.dataset.octaveLeftBounds) | |||||
const octaveRightBounds = Number(k.dataset.octaveRightBounds) | |||||
return octaveLeftBounds <= leftInKeyUnits && leftInKeyUnits < octaveRightBounds | |||||
}) | |||||
const key: HTMLElement | undefined = currentOctave.reduce<HTMLElement | undefined>((selectedKey, octaveKey) => { | |||||
if (maybeAccidental) { | |||||
if (selectedKey !== undefined) { | |||||
return selectedKey | |||||
} | |||||
const keyLeftBounds = Number(octaveKey.dataset.leftBounds) | |||||
const keyRightBounds = Number(octaveKey.dataset.rightBounds) | |||||
if (keyLeftBounds <= leftInKeyUnits && leftInKeyUnits < keyRightBounds) { | |||||
return octaveKey | |||||
} | |||||
return selectedKey | |||||
} | |||||
if (selectedKey !== undefined) { | |||||
return selectedKey | |||||
} | |||||
if ( | |||||
'leftFullBounds' in octaveKey.dataset && | |||||
'rightFullBounds' in octaveKey.dataset && | |||||
Number(octaveKey.dataset.leftFullBounds) <= leftInKeyUnits && | |||||
leftInKeyUnits < Number(octaveKey.dataset.rightFullBounds) | |||||
) { | |||||
return octaveKey | |||||
} | |||||
return selectedKey | |||||
}, undefined) | |||||
if (key! === undefined) { | |||||
return null | |||||
} | |||||
const { height: keyHeight } = key.getBoundingClientRect() | |||||
return { | |||||
velocity: realTop / keyHeight, | |||||
key: Number(key.dataset.key), | |||||
} | |||||
} | |||||
} | |||||
export default reverseGetKeyFromPoint |