Musical keyboard component written in React.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

329 lignes
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