Musical keyboard component written in React.
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

329 wiersze
9.2 KiB

  1. import * as React from 'react'
  2. import * as PropTypes from 'prop-types'
  3. import reverseGetKeyFromPoint from '../../services/reverseGetKeyFromPoint'
  4. const propTypes = {
  5. /**
  6. * Event handler triggered upon change in activated keys in the component.
  7. */
  8. onChange: PropTypes.func,
  9. /**
  10. * Map from key code to key number.
  11. */
  12. keyboardMapping: PropTypes.object,
  13. /**
  14. * Active MIDI channel for registering keys.
  15. */
  16. channel: PropTypes.number.isRequired,
  17. }
  18. type Props = PropTypes.InferProps<typeof propTypes> & { accidentalKeyLengthRatio?: number }
  19. /**
  20. * Keyboard map for allowing interactivity with the keyboard.
  21. * @param channel - Active MIDI channel for registering keys.
  22. * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. This is set by the Keyboard component.
  23. * @param onChange - Event handler triggered upon change in activated keys in the component.
  24. * @param keyboardMapping - Map from key code to key number.
  25. */
  26. const KeyboardMap: React.FC<Props> = ({ channel, accidentalKeyLengthRatio, onChange, keyboardMapping = {} }) => {
  27. const baseRef = React.useRef<HTMLDivElement>(null)
  28. const keysOnRef = React.useRef<any[]>([])
  29. const lastVelocity = React.useRef<number | undefined>(undefined)
  30. const isTouch = React.useRef<boolean>(false)
  31. const handleContextMenu: React.EventHandler<any> = (e) => {
  32. e.preventDefault()
  33. }
  34. const handleDragStart: React.DragEventHandler = (e) => {
  35. e.preventDefault()
  36. }
  37. const handleMouseDown: React.MouseEventHandler = (e) => {
  38. if (isTouch.current) {
  39. return
  40. }
  41. if (baseRef.current === null) {
  42. return
  43. }
  44. if (baseRef.current.parentElement === null) {
  45. return
  46. }
  47. const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)(
  48. e.clientX,
  49. e.clientY,
  50. )
  51. if (keyData! === null) {
  52. return
  53. }
  54. if (e.buttons === 1) {
  55. if (lastVelocity.current === undefined) {
  56. lastVelocity.current = keyData.velocity
  57. }
  58. keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, channel, id: -1 }]
  59. if (typeof onChange! === 'function') {
  60. onChange(keysOnRef.current)
  61. }
  62. }
  63. }
  64. const handleTouchStart: React.TouchEventHandler = (e) => {
  65. isTouch.current = true
  66. if (baseRef.current === null) {
  67. return
  68. }
  69. if (baseRef.current.parentElement === null) {
  70. return
  71. }
  72. Array.from(e.changedTouches).forEach((t) => {
  73. const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)(
  74. t.clientX,
  75. t.clientY,
  76. )
  77. if (keyData! === null) {
  78. return
  79. }
  80. if (lastVelocity.current === undefined) {
  81. lastVelocity.current = keyData.velocity
  82. }
  83. keysOnRef.current = [
  84. ...keysOnRef.current,
  85. { ...keyData, velocity: lastVelocity.current, channel, id: t.identifier },
  86. ]
  87. if (typeof onChange! === 'function') {
  88. onChange(keysOnRef.current)
  89. }
  90. })
  91. }
  92. React.useEffect(() => {
  93. const handleTouchMove = (e: TouchEvent) => {
  94. if (baseRef.current === null) {
  95. return
  96. }
  97. if (baseRef.current.parentElement === null) {
  98. return
  99. }
  100. e.preventDefault()
  101. Array.from(e.changedTouches).forEach((t) => {
  102. const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement!, accidentalKeyLengthRatio!)(
  103. t.clientX,
  104. t.clientY,
  105. )
  106. if (keyData! === null) {
  107. keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier)
  108. if (typeof onChange! === 'function') {
  109. onChange(keysOnRef.current)
  110. }
  111. return
  112. }
  113. const [mouseKey = null] = keysOnRef.current.filter((k) => k.id === t.identifier)
  114. if (mouseKey === null) {
  115. keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier)
  116. if (typeof onChange! === 'function') {
  117. onChange(keysOnRef.current)
  118. }
  119. return
  120. }
  121. if (mouseKey.key !== keyData.key) {
  122. keysOnRef.current = [
  123. ...keysOnRef.current.filter((k) => k.id !== t.identifier),
  124. {
  125. ...keyData,
  126. channel,
  127. velocity: lastVelocity.current,
  128. id: t.identifier,
  129. },
  130. ]
  131. if (typeof onChange! === 'function') {
  132. onChange(keysOnRef.current)
  133. }
  134. }
  135. })
  136. }
  137. window.addEventListener('touchmove', handleTouchMove, { passive: false })
  138. return () => {
  139. window.removeEventListener('touchmove', handleTouchMove)
  140. }
  141. }, [accidentalKeyLengthRatio, channel, onChange])
  142. React.useEffect(() => {
  143. const handleMouseMove = (e: MouseEvent) => {
  144. e.preventDefault()
  145. if (baseRef.current === null) {
  146. return
  147. }
  148. if (baseRef.current.parentElement === null) {
  149. return
  150. }
  151. if (e.buttons === 1) {
  152. const keyData = reverseGetKeyFromPoint(baseRef.current!.parentElement, accidentalKeyLengthRatio!)(
  153. e.clientX,
  154. e.clientY,
  155. )
  156. if (keyData! === null) {
  157. keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1)
  158. if (typeof onChange! === 'function') {
  159. onChange(keysOnRef.current)
  160. }
  161. return
  162. }
  163. const [mouseKey = null] = keysOnRef.current.filter((k) => k.id === -1)
  164. if (mouseKey === null) {
  165. keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1)
  166. if (typeof onChange! === 'function') {
  167. onChange(keysOnRef.current)
  168. }
  169. return
  170. }
  171. if (mouseKey.key !== keyData.key) {
  172. keysOnRef.current = [
  173. ...keysOnRef.current.filter((k) => k.id !== -1),
  174. { ...keyData, velocity: lastVelocity.current, channel, id: -1 },
  175. ]
  176. if (typeof onChange! === 'function') {
  177. onChange(keysOnRef.current)
  178. }
  179. }
  180. }
  181. }
  182. window.addEventListener('mousemove', handleMouseMove)
  183. return () => {
  184. window.removeEventListener('mousemove', handleMouseMove)
  185. }
  186. }, [accidentalKeyLengthRatio, channel, onChange])
  187. React.useEffect(() => {
  188. const handleTouchEnd = (e: TouchEvent) => {
  189. if (baseRef.current === null) {
  190. return
  191. }
  192. if (baseRef.current.parentElement === null) {
  193. return
  194. }
  195. Array.from(e.changedTouches).forEach((t) => {
  196. keysOnRef.current = keysOnRef.current.filter((k) => k.id !== t.identifier)
  197. lastVelocity.current = undefined
  198. if (typeof onChange! === 'function') {
  199. onChange(keysOnRef.current)
  200. }
  201. })
  202. }
  203. window.addEventListener('touchend', handleTouchEnd)
  204. return () => {
  205. window.removeEventListener('touchend', handleTouchEnd)
  206. }
  207. })
  208. React.useEffect(() => {
  209. const handleMouseUp = (e: MouseEvent) => {
  210. e.preventDefault()
  211. if (baseRef.current === null) {
  212. return
  213. }
  214. if (baseRef.current.parentElement === null) {
  215. return
  216. }
  217. keysOnRef.current = keysOnRef.current.filter((k) => k.id !== -1)
  218. lastVelocity.current = undefined
  219. if (typeof onChange! === 'function') {
  220. onChange(keysOnRef.current)
  221. }
  222. }
  223. window.addEventListener('mouseup', handleMouseUp)
  224. return () => {
  225. window.removeEventListener('mouseup', handleMouseUp)
  226. }
  227. }, [accidentalKeyLengthRatio, channel, onChange])
  228. React.useEffect(() => {
  229. const baseRefComponent = baseRef.current
  230. const handleKeyDown = (e: KeyboardEvent) => {
  231. if (!keyboardMapping!) {
  232. return
  233. }
  234. const { [e.code]: key = null } = keyboardMapping as Record<string, number>
  235. if (key === null) {
  236. return
  237. }
  238. if (keysOnRef.current.some((k) => k.key === key && k.id === -2)) {
  239. return
  240. }
  241. keysOnRef.current = [...keysOnRef.current, { key, velocity: 0.75, channel, id: -2 }]
  242. if (typeof onChange! === 'function') {
  243. onChange(keysOnRef.current)
  244. }
  245. }
  246. if (baseRefComponent) {
  247. baseRefComponent.addEventListener('keydown', handleKeyDown)
  248. }
  249. return () => {
  250. if (baseRefComponent) {
  251. baseRefComponent.removeEventListener('keydown', handleKeyDown)
  252. }
  253. }
  254. })
  255. React.useEffect(() => {
  256. const handleKeyUp = (e: KeyboardEvent) => {
  257. if (!keyboardMapping!) {
  258. return
  259. }
  260. const { [e.code]: key = null } = keyboardMapping as Record<string, number>
  261. if (key === null) {
  262. return
  263. }
  264. keysOnRef.current = keysOnRef.current.filter((k) => k.key !== key)
  265. if (typeof onChange! === 'function') {
  266. onChange(keysOnRef.current)
  267. }
  268. }
  269. window.addEventListener('keyup', handleKeyUp)
  270. return () => {
  271. window.removeEventListener('keyup', handleKeyUp)
  272. }
  273. })
  274. return (
  275. <div
  276. ref={baseRef}
  277. style={{
  278. position: 'absolute',
  279. top: 0,
  280. left: 0,
  281. width: '100%',
  282. height: '100%',
  283. zIndex: 4,
  284. }}
  285. onContextMenu={handleContextMenu}
  286. onDragStart={handleDragStart}
  287. onMouseDown={handleMouseDown}
  288. onTouchStart={handleTouchStart}
  289. tabIndex={0}
  290. />
  291. )
  292. }
  293. KeyboardMap.propTypes = propTypes
  294. export default KeyboardMap