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.
 
 
 
 

197 lignes
5.9 KiB

  1. import * as React from 'react'
  2. import * as PropTypes from 'prop-types'
  3. import isNaturalKeyUnmemoized from '../../services/isNaturalKey'
  4. import getKeyWidthUnmemoized from '../../services/getKeyWidth'
  5. import getKeyLeftUnmemoized from '../../services/getKeyLeft'
  6. import generateKeys from '../../services/generateKeys'
  7. import DefaultAccidentalKey from '../AccidentalKey/AccidentalKey'
  8. import DefaultNaturalKey from '../NaturalKey/NaturalKey'
  9. export const propTypes = {
  10. /**
  11. * MIDI note of the first key.
  12. */
  13. startKey: PropTypes.number.isRequired,
  14. /**
  15. * MIDI note of the last key.
  16. */
  17. endKey: PropTypes.number.isRequired,
  18. /**
  19. * Does the component have a clickable map?
  20. */
  21. hasMap: PropTypes.bool,
  22. //octaveDivision: PropTypes.number,
  23. /**
  24. * Ratio of the length of the accidental keys to the natural keys.
  25. */
  26. accidentalKeyLengthRatio: PropTypes.number,
  27. /**
  28. * Current active keys and their channel assignments.
  29. */
  30. keyChannels: PropTypes.arrayOf(
  31. PropTypes.shape({
  32. channel: PropTypes.number.isRequired,
  33. key: PropTypes.number.isRequired,
  34. velocity: PropTypes.number.isRequired,
  35. }),
  36. ),
  37. /**
  38. * Components to use for each kind of key.
  39. */
  40. keyComponents: PropTypes.shape({
  41. natural: PropTypes.elementType,
  42. accidental: PropTypes.elementType,
  43. }),
  44. /**
  45. * Width of the component.
  46. */
  47. width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  48. /**
  49. * Height of the component.
  50. */
  51. height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  52. }
  53. type Props = PropTypes.InferProps<typeof propTypes>
  54. /**
  55. * Component for displaying musical notes in the form of a piano keyboard.
  56. * @param startKey - MIDI note of the first key.
  57. * @param endKey - MIDI note of the last key.
  58. * @param hasMap - The component's clickable map component.
  59. * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys.
  60. * @param keyChannels - Current active keys and their channel assignments.
  61. * @param width - Width of the component.
  62. * @param keyComponents - Components to use for each kind of key.
  63. * @param height - Height of the component.
  64. */
  65. const Keyboard: React.FC<Props> = ({
  66. startKey,
  67. endKey,
  68. //octaveDivision = 12,
  69. accidentalKeyLengthRatio = 0.65,
  70. keyChannels = [],
  71. width = '100%',
  72. keyComponents = {},
  73. height = 80,
  74. children,
  75. }) => {
  76. const [clientSide, setClientSide] = React.useState(false)
  77. const [clientSideKeys, setClientSideKeys] = React.useState<number[]>([])
  78. const { natural: NaturalKey = DefaultNaturalKey, accidental: AccidentalKey = DefaultAccidentalKey } = keyComponents!
  79. const getKeyWidth = React.useCallback((k) => getKeyWidthUnmemoized(startKey, endKey)(k), [startKey, endKey])
  80. const getKeyLeft = React.useCallback((k) => getKeyLeftUnmemoized(startKey, endKey)(k), [startKey, endKey])
  81. const isNaturalKey = React.useCallback((k) => isNaturalKeyUnmemoized(k), [])
  82. const baseRef = React.useRef<HTMLDivElement>(null)
  83. React.useEffect(() => {
  84. setClientSide(true)
  85. }, [])
  86. React.useEffect(() => {
  87. setClientSideKeys(generateKeys(startKey!, endKey!))
  88. }, [startKey, endKey])
  89. const keys = clientSide ? clientSideKeys : generateKeys(startKey, endKey)
  90. return (
  91. <div
  92. style={{
  93. width: width!,
  94. height: height!,
  95. position: 'relative',
  96. backgroundColor: 'currentColor',
  97. overflow: 'hidden',
  98. }}
  99. role="presentation"
  100. ref={baseRef}
  101. >
  102. {keys.map((key) => {
  103. const isNatural = isNaturalKey(key)
  104. const Component: any = isNatural ? NaturalKey! : AccidentalKey!
  105. const currentKeyChannels = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === key) : null
  106. const width = getKeyWidth(key)
  107. const left = getKeyLeft(key)
  108. let leftBounds: number
  109. let rightBounds: number
  110. switch (key % 12) {
  111. case 0:
  112. case 5:
  113. leftBounds = left
  114. rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1)
  115. break
  116. case 4:
  117. case 11:
  118. leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1)
  119. rightBounds = left + width
  120. break
  121. case 2:
  122. case 7:
  123. case 9:
  124. leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1)
  125. rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1)
  126. break
  127. default:
  128. leftBounds = left
  129. rightBounds = left + width
  130. break
  131. }
  132. const octaveStart = Math.floor(key / 12) * 12
  133. const octaveEnd = octaveStart + 11
  134. const octaveLeftBounds = getKeyLeft(octaveStart)
  135. const octaveRightBounds = getKeyLeft(octaveEnd) + getKeyWidth(octaveEnd)
  136. return (
  137. <div
  138. key={key}
  139. data-key={key}
  140. data-octave-left-bounds={octaveLeftBounds}
  141. data-octave-right-bounds={octaveRightBounds}
  142. data-left-bounds={leftBounds}
  143. data-right-bounds={rightBounds}
  144. data-left-full-bounds={isNatural ? left : undefined}
  145. data-right-full-bounds={isNatural ? left + width : undefined}
  146. style={{
  147. zIndex: isNatural ? 0 : 2,
  148. width: width + '%',
  149. height: (isNatural ? 100 : 100 * accidentalKeyLengthRatio!) + '%',
  150. left: left + '%',
  151. position: 'absolute',
  152. top: 0,
  153. }}
  154. >
  155. <Component keyChannels={currentKeyChannels} />
  156. </div>
  157. )
  158. })}
  159. {children! &&
  160. React.Children.map(children, (unknownChild) => {
  161. const child = unknownChild as React.ReactElement
  162. const { props = {} } = child
  163. return React.cloneElement(child, {
  164. ...props,
  165. accidentalKeyLengthRatio,
  166. })
  167. })}
  168. </div>
  169. )
  170. }
  171. Keyboard.propTypes = propTypes
  172. export default Keyboard