Use custom styled keys and add pressed keys for styled key set.master
@@ -41,6 +41,7 @@ | |||
"@types/prop-types": "^15.7.3", | |||
"@types/react": "^16.9.44", | |||
"@types/react-dom": "^16.9.8", | |||
"@types/react-is": "^16.7.1", | |||
"@types/styled-components": "^5.1.2", | |||
"babel-loader": "^8.1.0", | |||
"fast-check": "^2.0.0", | |||
@@ -0,0 +1,15 @@ | |||
import * as React from 'react' | |||
import * as ReactIs from 'react-is' | |||
import AccidentalKey from './AccidentalKey' | |||
it('should exist', () => { | |||
expect(AccidentalKey).toBeDefined() | |||
}) | |||
it('should be a React component', () => { | |||
expect(ReactIs.isValidElementType(AccidentalKey)).toBe(true) | |||
}) | |||
it('should render without crashing', () => { | |||
expect(() => <AccidentalKey />).not.toThrow() | |||
}) |
@@ -0,0 +1,42 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import keyPropTypes from '../../services/keyPropTypes' | |||
import styled from 'styled-components' | |||
const Base = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
backgroundColor: 'var(--color-accidental-key, currentColor)', | |||
border: '1px solid', | |||
boxSizing: 'border-box', | |||
position: 'relative', | |||
}) | |||
const Highlight = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
position: 'absolute', | |||
top: 0, | |||
left: 0, | |||
opacity: 0.75, | |||
}) | |||
type Props = PropTypes.InferProps<typeof keyPropTypes> | |||
const AccidentalKey: React.FC<Props> = ({ keyChannels }) => ( | |||
<Base> | |||
{Array.isArray(keyChannels!) && | |||
keyChannels.map((c) => ( | |||
<Highlight | |||
key={c!.channel} | |||
style={{ | |||
backgroundColor: `var(--color-channel-${c!.channel}, Highlight)`, | |||
}} | |||
/> | |||
))} | |||
</Base> | |||
) | |||
AccidentalKey.propTypes = keyPropTypes | |||
export default AccidentalKey |
@@ -1,443 +0,0 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import styled from 'styled-components' | |||
import Keyboard, { propTypes, } from './Keyboard' | |||
export default { | |||
title: 'Keyboard', | |||
} | |||
type Props = PropTypes.InferProps<typeof propTypes> | |||
// By passing optional props to this story, you can control the props of the component when | |||
// you consume the story in a test. | |||
export const Default = (props?: Partial<Props>) => ( | |||
<Keyboard | |||
{...props} | |||
startKey={21} | |||
endKey={108} | |||
/> | |||
) | |||
export const WithActiveKeys = (props?: Partial<Props>) => ( | |||
<Keyboard | |||
{...props} | |||
startKey={21} | |||
endKey={108} | |||
keyChannels={[ | |||
{ | |||
channel: 0, | |||
key: 60, | |||
velocity: 1, | |||
}, | |||
{ | |||
channel: 0, | |||
key: 64, | |||
velocity: 1, | |||
}, | |||
{ | |||
channel: 0, | |||
key: 67, | |||
velocity: 1, | |||
}, | |||
]} | |||
/> | |||
) | |||
const Base = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
position: 'relative', | |||
'--natural-key-color': '#e3e3e5', | |||
'--accidental-key-color': '#35313b', | |||
}) | |||
const N1 = styled('div')`width: 100%; height: 100%; position: relative;` | |||
const N2 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N3 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
padding: 1px 0 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N4 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: var(--natural-key-color, white); | |||
border-radius: 0 0 1px 1px; | |||
` | |||
const N5 = styled('div')` | |||
width: 100%; | |||
height: calc(33 / 80 * 100%); | |||
background-color: white; | |||
padding: 0 1px 2px 2px; | |||
box-sizing: border-box; | |||
background-clip: content-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
opacity: 0.25; | |||
mask-image: linear-gradient(to bottom, transparent, white); | |||
-webkit-mask-image: linear-gradient(to bottom, transparent, white); | |||
` | |||
const N6 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
padding: 1px 2px 3px 3px; | |||
box-sizing: border-box; | |||
background-clip: content-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
opacity: 0.08; | |||
mask-image: linear-gradient(to bottom, transparent, white); | |||
-webkit-mask-image: linear-gradient(to bottom, transparent, white); | |||
` | |||
const N7 = styled('div')` | |||
width: 100%; | |||
height: 2px; | |||
padding: 0 0 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const N8 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
border-radius: 0 0 1px 1px; | |||
opacity: 0.25; | |||
` | |||
const N9 = styled('div')` | |||
width: 2px; | |||
height: 100%; | |||
padding: 1px 0 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const N10 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
border-radius: 0 0 0 1px; | |||
opacity: 0.07; | |||
` | |||
const N11 = styled('div')` | |||
width: 100%; | |||
height: 6px; | |||
padding: 1px 0 0 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N12 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
mask-image: linear-gradient(to bottom, white, transparent); | |||
-webkit-mask-image: linear-gradient(to bottom, white, transparent); | |||
opacity: 0.12; | |||
` | |||
const N13 = styled('div')` | |||
width: 100%; | |||
height: 3px; | |||
padding: 1px 0 0 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N14 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
opacity: 0.12; | |||
` | |||
const N15 = styled('div')` | |||
width: 1px; | |||
height: 100%; | |||
padding: 1px 0 1px 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
right: 0; | |||
` | |||
const N16 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
border-radius: 0 0 1px 0; | |||
opacity: 0.12; | |||
` | |||
const N17 = styled('div')` | |||
padding: 1px 0 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
width: 100%; | |||
height: 100%; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N18 = styled('div')` | |||
position: relative; | |||
width: 100%; | |||
height: 100%; | |||
border-radius: 0 0 1px 1px; | |||
overflow: hidden; | |||
` | |||
const B1 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
position: relative; | |||
border-radius: 1px; | |||
box-shadow: 0 0 0 1px rgba(0,0,0,0.25); | |||
` | |||
const B2 = styled('div')` | |||
width: 100%; | |||
height: calc(6 / 50 * 100%); | |||
position: absolute; | |||
border-radius: 0 0 1px 1px; | |||
bottom: 0; | |||
left: 0; | |||
background-color: var(--accidental-key-color, #222); | |||
mask-image: linear-gradient(to bottom, white, rgba(0,0,0,0.9)); | |||
-webkit-mask-image: linear-gradient(to bottom, white, rgba(0,0,0,0.9)); | |||
` | |||
const B3 = styled('div')` | |||
width: 100%; | |||
height: calc(44 / 50 * 100%); | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
background-color: var(--accidental-key-color, #222); | |||
` | |||
const B4 = styled('div')` | |||
width: 100%; | |||
height: 4px; | |||
padding: 1px 0 0 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const B5 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
opacity: 0.12; | |||
` | |||
const B6 = styled('div')` | |||
width: 2px; | |||
height: 11px; | |||
padding: 1px 1px 1px 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
right: 0; | |||
` | |||
const B7 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
mask-image: linear-gradient(to bottom, transparent, white); | |||
-webkit-mask-image: linear-gradient(to bottom, transparent, white); | |||
opacity: 0.4; | |||
` | |||
const B8 = styled('div')` | |||
width: 2px; | |||
height: 100%; | |||
padding: 10px 1px 6px 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
right: 0; | |||
` | |||
const B9 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
opacity: 0.4; | |||
` | |||
const B10 = styled('div')` | |||
width: 100%; | |||
height: 4px; | |||
padding: 0 1px 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const B11 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
border-radius: 4px 4px 1px 1px; | |||
opacity: 0.12; | |||
` | |||
const B12 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
padding: 3px 3px 7px 2px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const B13 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
mask-image: linear-gradient(to bottom, transparent, white); | |||
-webkit-mask-image: linear-gradient(to bottom, transparent, white); | |||
border-radius: 99999px; | |||
opacity: 0.12; | |||
` | |||
const B14 = styled('div')` | |||
width: 100%; | |||
height: 6px; | |||
padding: 0 1px 5px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const B15 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
border-radius: 0 0 1px 1px; | |||
opacity: 0.4; | |||
` | |||
const B16 = styled('div')` | |||
padding: 1px 1px 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
width: 100%; | |||
height: 100%; | |||
top: 0; | |||
left: 0; | |||
` | |||
const B17 = styled('div')` | |||
position: relative; | |||
width: 100%; | |||
height: 100%; | |||
border-radius: 0 0 1px 1px; | |||
overflow: hidden; | |||
` | |||
export const Styled = (props?: Partial<Props>) => ( | |||
<Keyboard | |||
{...props} | |||
startKey={21} | |||
endKey={108} | |||
keyChannels={[ | |||
{ | |||
channel: 0, | |||
key: 60, | |||
velocity: 1, | |||
}, | |||
{ | |||
channel: 0, | |||
key: 63, | |||
velocity: 1, | |||
}, | |||
{ | |||
channel: 0, | |||
key: 67, | |||
velocity: 1, | |||
}, | |||
]} | |||
keyComponents={{ | |||
natural: ({ children, }) => ( | |||
<Base> | |||
<N1> | |||
<N2></N2> | |||
<N3> | |||
<N4></N4> | |||
</N3> | |||
<N5></N5> | |||
<N6></N6> | |||
<N7> | |||
<N8></N8> | |||
</N7> | |||
<N9> | |||
<N10></N10> | |||
</N9> | |||
<N11> | |||
<N12></N12> | |||
</N11> | |||
<N13> | |||
<N14></N14> | |||
</N13> | |||
<N15> | |||
<N16></N16> | |||
</N15> | |||
</N1> | |||
<N17> | |||
<N18> | |||
{children} | |||
</N18> | |||
</N17> | |||
</Base> | |||
), | |||
accidental: ({ children, }) => ( | |||
<Base> | |||
<B1> | |||
<B2></B2> | |||
<B3></B3> | |||
<B4> | |||
<B5></B5> | |||
</B4> | |||
<B6> | |||
<B7></B7> | |||
</B6> | |||
<B8> | |||
<B9></B9> | |||
</B8> | |||
<B10> | |||
<B11></B11> | |||
</B10> | |||
<B12> | |||
<B13></B13> | |||
</B12> | |||
<B14> | |||
<B15></B15> | |||
</B14> | |||
</B1> | |||
<B16> | |||
<B17> | |||
{children} | |||
</B17> | |||
</B16> | |||
</Base> | |||
), | |||
}} | |||
/> | |||
) |
@@ -1,9 +0,0 @@ | |||
import * as React from 'react' | |||
import * as ReactDOM from 'react-dom' | |||
import { Default as Keyboard } from './Keyboard.stories'; | |||
it('renders without crashing', () => { | |||
const div = document.createElement('div'); | |||
ReactDOM.render(<Keyboard startKey={21} endKey={108}/>, div); | |||
ReactDOM.unmountComponentAtNode(div); | |||
}); |
@@ -1,160 +0,0 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import styled from 'styled-components' | |||
import isNaturalKey from '../services/isNaturalKey' | |||
import getKeyWidth from '../services/getKeyWidth' | |||
import getKeyLeft from '../services/getKeyLeft' | |||
import generateKeys from '../services/generateKeys' | |||
const Base = styled('div')({ | |||
position: 'relative', | |||
}) | |||
const Key = styled('div')({ | |||
position: 'absolute', | |||
top: 0, | |||
}) | |||
const DefaultNaturalKey = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
backgroundColor: 'white', | |||
border: '1px solid', | |||
boxSizing: 'border-box', | |||
position: 'relative', | |||
}) | |||
const DefaultAccidentalKey = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
backgroundColor: 'black', | |||
border: '1px solid', | |||
boxSizing: 'border-box', | |||
position: 'relative', | |||
}) | |||
const Highlight = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
position: 'absolute', | |||
top: 0, | |||
left: 0, | |||
opacity: 0.75, | |||
}) | |||
export const propTypes = { | |||
startKey: PropTypes.number.isRequired, | |||
endKey: PropTypes.number.isRequired, | |||
//octaveDivision: PropTypes.number, | |||
accidentalKeyLengthRatio: PropTypes.number, | |||
keyChannels: PropTypes.arrayOf(PropTypes.shape({ | |||
channel: PropTypes.number.isRequired, | |||
key: PropTypes.number.isRequired, | |||
velocity: PropTypes.number.isRequired, | |||
})), | |||
channelColors: PropTypes.arrayOf(PropTypes.string), | |||
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
} | |||
const DEFAULT_CHANNEL_COLORS = [ | |||
'#ff5555', | |||
'#ffff00', | |||
'#00aa00', | |||
'#0055aa', | |||
'#aa00ff', | |||
'#aa0000', | |||
'#aa5500', | |||
'#ffaa00', | |||
'#00ff00', | |||
'#00aaaa', | |||
'#00ffff', | |||
'#ff00aa', | |||
'#aaaa00', | |||
'#555500', | |||
'#5500aa', | |||
'#ff55ff', | |||
] | |||
type Props = PropTypes.InferProps<typeof propTypes> | |||
const Keyboard: React.FC<Props> = ({ | |||
startKey, | |||
endKey, | |||
//octaveDivision = 12, | |||
accidentalKeyLengthRatio = 0.65, | |||
keyChannels = [], | |||
channelColors = DEFAULT_CHANNEL_COLORS, | |||
width = '100%', | |||
keyComponents: { | |||
natural: NaturalKey = DefaultNaturalKey, | |||
accidental: AccidentalKey = DefaultAccidentalKey, | |||
} = {}, | |||
height = 80, | |||
}) => { | |||
const [keys, setKeys, ] = React.useState<number[]>([]) | |||
React.useEffect(() => { | |||
setKeys(generateKeys(startKey!, endKey!)) | |||
}, [startKey, endKey, ]) | |||
return ( | |||
<Base | |||
style={{ | |||
width: width!, | |||
height: height!, | |||
backgroundColor: 'black', | |||
overflow: 'hidden', | |||
}} | |||
> | |||
{keys.map(k => { | |||
const isNatural = isNaturalKey(k) | |||
const Component = isNatural ? NaturalKey : AccidentalKey | |||
const width = getKeyWidth(startKey!, endKey!)(k) | |||
const height = isNatural ? 100 : 100 * accidentalKeyLengthRatio! | |||
const left = getKeyLeft(startKey!, endKey!)(k) | |||
const currentKeyChannels = ( | |||
Array.isArray(keyChannels!) | |||
? keyChannels.filter(kc => kc!.key === k) | |||
: null | |||
) | |||
return ( | |||
<Key | |||
style={{ | |||
zIndex: isNatural ? 0 : 2, | |||
width: width + '%', | |||
height: height + '%', | |||
left: left + '%', | |||
}} | |||
> | |||
<Component> | |||
{ | |||
Array.isArray(currentKeyChannels) | |||
&& currentKeyChannels.map(c => ( | |||
<Highlight | |||
key={c!.channel} | |||
style={{ | |||
backgroundColor: channelColors![c!.channel] as string, | |||
}} | |||
/> | |||
)) | |||
} | |||
</Component> | |||
</Key> | |||
) | |||
})} | |||
</Base> | |||
) | |||
} | |||
Keyboard.propTypes = propTypes | |||
export default Keyboard |
@@ -0,0 +1,119 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import StyledAccidentalKey from '../StyledAccidentalKey/StyledAccidentalKey' | |||
import StyledNaturalKey from '../StyledNaturalKey/StyledNaturalKey' | |||
import Keyboard, { propTypes } from './Keyboard' | |||
interface WrapperProps { | |||
style?: object | null | |||
} | |||
const Wrapper: React.FC<WrapperProps> = ({ style, ...etcProps }) => ( | |||
<div | |||
{...etcProps} | |||
style={{ | |||
...style, | |||
// @ts-ignore | |||
'--color-channel-0': '#f55', | |||
'--color-channel-1': '#ff0', | |||
'--color-channel-2': '#0a0', | |||
'--color-channel-3': '#05a', | |||
'--color-channel-4': '#a0f', | |||
'--color-channel-5': '#a00', | |||
'--color-channel-6': '#a50', | |||
'--color-channel-7': '#fa0', | |||
'--color-channel-8': '#0f0', | |||
'--color-channel-9': '#0aa', | |||
'--color-channel-10': '#0ff', | |||
'--color-channel-11': '#f0a', | |||
'--color-channel-12': '#aa0', | |||
'--color-channel-13': '#550', | |||
'--color-channel-14': '#50a', | |||
'--color-channel-15': '#f5f', | |||
}} | |||
/> | |||
) | |||
export default { | |||
title: 'Keyboard', | |||
} | |||
type Props = PropTypes.InferProps<typeof propTypes> | |||
// By passing optional props to this story, you can control the props of the component when | |||
// you consume the story in a test. | |||
export const Default = (props?: Partial<Props>) => ( | |||
<Wrapper> | |||
<Keyboard {...props} startKey={21} endKey={108} /> | |||
</Wrapper> | |||
) | |||
export const WithActiveKeys = (props?: Partial<Props>) => ( | |||
<Wrapper> | |||
<Keyboard | |||
{...props} | |||
startKey={21} | |||
endKey={108} | |||
keyChannels={[ | |||
{ | |||
channel: 0, | |||
key: 60, | |||
velocity: 1, | |||
}, | |||
{ | |||
channel: 0, | |||
key: 64, | |||
velocity: 1, | |||
}, | |||
{ | |||
channel: 0, | |||
key: 67, | |||
velocity: 1, | |||
}, | |||
]} | |||
/> | |||
</Wrapper> | |||
) | |||
export const WithDifferentKeyRange = (props?: Partial<Props>) => ( | |||
<Wrapper> | |||
<Keyboard {...props} height={300} startKey={48} endKey={71} /> | |||
</Wrapper> | |||
) | |||
export const Styled = (props?: Partial<Props>) => ( | |||
<Wrapper | |||
style={{ | |||
// @ts-ignore | |||
'--color-natural-key': '#e3e3e5', | |||
'--color-accidental-key': '#35313b', | |||
}} | |||
> | |||
<Keyboard | |||
{...props} | |||
startKey={21} | |||
endKey={108} | |||
keyChannels={[ | |||
{ | |||
channel: 0, | |||
key: 60, | |||
velocity: 1, | |||
}, | |||
{ | |||
channel: 0, | |||
key: 63, | |||
velocity: 1, | |||
}, | |||
{ | |||
channel: 0, | |||
key: 67, | |||
velocity: 1, | |||
}, | |||
]} | |||
keyComponents={{ | |||
natural: StyledNaturalKey, | |||
accidental: StyledAccidentalKey, | |||
}} | |||
/> | |||
</Wrapper> | |||
) |
@@ -0,0 +1,15 @@ | |||
import * as React from 'react' | |||
import * as ReactIs from 'react-is' | |||
import Keyboard from './Keyboard' | |||
it('should exist', () => { | |||
expect(Keyboard).toBeDefined() | |||
}) | |||
it('should be a React component', () => { | |||
expect(ReactIs.isValidElementType(Keyboard)).toBe(true) | |||
}) | |||
it('should render without crashing', () => { | |||
expect(() => <Keyboard startKey={21} endKey={108} />).not.toThrow() | |||
}) |
@@ -0,0 +1,140 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import styled from 'styled-components' | |||
import isNaturalKey from '../../services/isNaturalKey' | |||
import getKeyWidth from '../../services/getKeyWidth' | |||
import getKeyLeft from '../../services/getKeyLeft' | |||
import generateKeys from '../../services/generateKeys' | |||
import * as DefaultAccidentalKey from '../AccidentalKey/AccidentalKey' | |||
import * as DefaultNaturalKey from '../NaturalKey/NaturalKey' | |||
const Base = styled('div')({ | |||
position: 'relative', | |||
backgroundColor: 'currentColor', | |||
overflow: 'hidden', | |||
}) | |||
const Key = styled('div')({ | |||
position: 'absolute', | |||
top: 0, | |||
}) | |||
export const propTypes = { | |||
/** | |||
* MIDI note of the first key. | |||
*/ | |||
startKey: PropTypes.number.isRequired, | |||
/** | |||
* MIDI note of the last key. | |||
*/ | |||
endKey: PropTypes.number.isRequired, | |||
//octaveDivision: PropTypes.number, | |||
/** | |||
* Ratio of the length of the accidental keys to the natural keys. | |||
*/ | |||
accidentalKeyLengthRatio: PropTypes.number, | |||
/** | |||
* Current active keys and their channel assignments. | |||
*/ | |||
keyChannels: PropTypes.arrayOf( | |||
PropTypes.shape({ | |||
channel: PropTypes.number.isRequired, | |||
key: PropTypes.number.isRequired, | |||
velocity: PropTypes.number.isRequired, | |||
}), | |||
), | |||
/** | |||
* Components to use for each kind of key. | |||
*/ | |||
keyComponents: PropTypes.shape({ | |||
natural: PropTypes.elementType, | |||
accidental: PropTypes.elementType, | |||
}), | |||
/** | |||
* Width of the component. | |||
*/ | |||
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
/** | |||
* Height of the component. | |||
*/ | |||
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
} | |||
type Props = PropTypes.InferProps<typeof propTypes> | |||
/** | |||
* Component for displaying musical notes in the form of a piano keyboard. | |||
* @param startKey - MIDI note of the first key. | |||
* @param endKey - MIDI note of the last key. | |||
* @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. | |||
* @param keyChannels - Current active keys and their channel assignments. | |||
* @param width - Width of the component. | |||
* @param keyComponents - Components to use for each kind of key. | |||
* @param height - Height of the component. | |||
* @constructor | |||
*/ | |||
const Keyboard: React.FC<Props> = ({ | |||
startKey, | |||
endKey, | |||
//octaveDivision = 12, | |||
accidentalKeyLengthRatio = 0.65, | |||
keyChannels = [], | |||
width = '100%', | |||
keyComponents = {}, | |||
height = 80, | |||
}) => { | |||
const [keys, setKeys] = React.useState<number[]>([]) | |||
React.useEffect(() => { | |||
setKeys(generateKeys(startKey!, endKey!)) | |||
}, [startKey, endKey]) | |||
const { | |||
natural: NaturalKey = DefaultNaturalKey.default, | |||
accidental: AccidentalKey = DefaultAccidentalKey.default, | |||
} = keyComponents! | |||
return ( | |||
<Base | |||
style={{ | |||
width: width!, | |||
height: height!, | |||
}} | |||
> | |||
{keys.map((k) => { | |||
const isNatural = isNaturalKey(k) | |||
const Component: any = isNatural ? NaturalKey! : AccidentalKey! | |||
const width = getKeyWidth(startKey!, endKey!)(k) | |||
const height = isNatural ? 100 : 100 * accidentalKeyLengthRatio! | |||
const left = getKeyLeft(startKey!, endKey!)(k) | |||
const currentKeyChannels = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === k) : null | |||
return ( | |||
<Key | |||
key={k} | |||
style={{ | |||
zIndex: isNatural ? 0 : 2, | |||
width: width + '%', | |||
height: height + '%', | |||
left: left + '%', | |||
}} | |||
> | |||
<Component keyChannels={currentKeyChannels} /> | |||
</Key> | |||
) | |||
})} | |||
</Base> | |||
) | |||
} | |||
Keyboard.propTypes = propTypes | |||
export default Keyboard |
@@ -0,0 +1,15 @@ | |||
import * as React from 'react' | |||
import * as ReactIs from 'react-is' | |||
import NaturalKey from './NaturalKey' | |||
it('should exist', () => { | |||
expect(NaturalKey).toBeDefined() | |||
}) | |||
it('should be a React component', () => { | |||
expect(ReactIs.isValidElementType(NaturalKey)).toBe(true) | |||
}) | |||
it('should render without crashing', () => { | |||
expect(() => <NaturalKey />).not.toThrow() | |||
}) |
@@ -0,0 +1,42 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import styled from 'styled-components' | |||
import keyPropTypes from '../../services/keyPropTypes' | |||
const Base = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
backgroundColor: 'var(--color-natural-key, white)', | |||
border: '1px solid', | |||
boxSizing: 'border-box', | |||
position: 'relative', | |||
}) | |||
const Highlight = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
position: 'absolute', | |||
top: 0, | |||
left: 0, | |||
opacity: 0.75, | |||
}) | |||
type Props = PropTypes.InferProps<typeof keyPropTypes> | |||
const NaturalKey: React.FC<Props> = ({ keyChannels }) => ( | |||
<Base> | |||
{Array.isArray(keyChannels!) && | |||
keyChannels.map((c) => ( | |||
<Highlight | |||
key={c!.channel} | |||
style={{ | |||
backgroundColor: `var(--color-channel-${c!.channel}, Highlight)`, | |||
}} | |||
/> | |||
))} | |||
</Base> | |||
) | |||
NaturalKey.propTypes = keyPropTypes | |||
export default NaturalKey |
@@ -0,0 +1,15 @@ | |||
import * as React from 'react' | |||
import * as ReactIs from 'react-is' | |||
import AccidentalKey from './StyledAccidentalKey' | |||
it('should exist', () => { | |||
expect(AccidentalKey).toBeDefined() | |||
}) | |||
it('should be a React component', () => { | |||
expect(ReactIs.isValidElementType(AccidentalKey)).toBe(true) | |||
}) | |||
it('should render without crashing', () => { | |||
expect(() => <AccidentalKey />).not.toThrow() | |||
}) |
@@ -0,0 +1,230 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import keyPropTypes from '../../services/keyPropTypes' | |||
import styled from 'styled-components' | |||
const Base = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
position: 'relative', | |||
}) | |||
const B1 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
position: relative; | |||
border-radius: 1px; | |||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25); | |||
` | |||
const B2 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
border-radius: 0 0 1px 1px; | |||
background-color: var(--color-accidental-key, currentColor); | |||
mask-image: linear-gradient(to bottom, white, rgba(0, 0, 0, 0.9)); | |||
` | |||
const B3 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: var(--color-accidental-key, currentColor); | |||
` | |||
const B4 = styled('div')` | |||
width: 100%; | |||
height: 4px; | |||
padding: 1px 0 0 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const B5 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
opacity: 0.12; | |||
` | |||
const B6 = styled('div')` | |||
width: 2px; | |||
height: 11px; | |||
padding: 1px 1px 1px 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
right: 0; | |||
` | |||
const B7 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
mask-image: linear-gradient(to bottom, transparent, white); | |||
opacity: 0.4; | |||
` | |||
const B8 = styled('div')` | |||
width: 2px; | |||
height: 100%; | |||
padding: 10px 1px 6px 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
right: 0; | |||
` | |||
const B9 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
opacity: 0.4; | |||
border-bottom-right-radius: 1px; | |||
` | |||
const B10 = styled('div')` | |||
width: 100%; | |||
padding: 0 1px 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const B11 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
border-radius: 4px 4px 1px 1px; | |||
opacity: 0.12; | |||
` | |||
const B12 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
padding: 3px 3px 7px 2px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const B13 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
mask-image: linear-gradient(to bottom, transparent, white); | |||
border-radius: 99999px; | |||
` | |||
const B14 = styled('div')` | |||
width: 100%; | |||
height: 6px; | |||
padding: 0 1px 5px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const B15 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
border-radius: 0 0 1px 1px; | |||
opacity: 0.4; | |||
` | |||
const B16 = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
backgroundColor: 'black', | |||
position: 'absolute', | |||
top: 0, | |||
left: 0, | |||
}) | |||
const B17 = styled('div')` | |||
width: 100%; | |||
height: calc(6 / 50 * 100%); | |||
padding: 0 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const B18 = styled('div')` | |||
width: 100%; | |||
height: calc(44 / 50 * 100%); | |||
padding: 1px 1px 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
type Props = PropTypes.InferProps<typeof keyPropTypes> | |||
const StyledAccidentalKey: React.FC<Props> = ({ keyChannels }) => { | |||
const hasKeyChannels = Array.isArray(keyChannels!) && keyChannels.length > 0 | |||
return ( | |||
<Base> | |||
<B1 | |||
style={{ | |||
// @ts-ignore | |||
'--color-accidental-key': hasKeyChannels ? `var(--color-channel-${keyChannels![0]!.channel})` : undefined, | |||
}} | |||
> | |||
<B16 /> | |||
<B17> | |||
<B2 | |||
style={{ | |||
opacity: hasKeyChannels ? 0.75 : 1, | |||
}} | |||
/> | |||
</B17> | |||
<B18> | |||
<B3 | |||
style={{ | |||
opacity: hasKeyChannels ? 0.75 : 1, | |||
}} | |||
/> | |||
</B18> | |||
<B4> | |||
<B5 /> | |||
</B4> | |||
<B6 | |||
style={{ | |||
opacity: hasKeyChannels ? 0.5 : 1, | |||
}} | |||
> | |||
<B7 /> | |||
</B6> | |||
<B8 | |||
style={{ | |||
paddingBottom: hasKeyChannels ? 3 : 5, | |||
opacity: hasKeyChannels ? 0.5 : 1, | |||
}} | |||
> | |||
<B9 /> | |||
</B8> | |||
<B10 | |||
style={{ | |||
opacity: hasKeyChannels ? 3 : 4, | |||
}} | |||
> | |||
<B11 /> | |||
</B10> | |||
<B12> | |||
<B13 | |||
style={{ | |||
opacity: hasKeyChannels ? 0.06 : 0.12, | |||
}} | |||
/> | |||
</B12> | |||
<B14 | |||
style={{ | |||
height: hasKeyChannels ? 4 : 6, | |||
paddingBottom: hasKeyChannels ? 3 : 5, | |||
opacity: hasKeyChannels ? 0.5 : 1, | |||
}} | |||
> | |||
<B15 /> | |||
</B14> | |||
</B1> | |||
</Base> | |||
) | |||
} | |||
StyledAccidentalKey.propTypes = keyPropTypes | |||
export default StyledAccidentalKey |
@@ -0,0 +1,15 @@ | |||
import * as React from 'react' | |||
import * as ReactIs from 'react-is' | |||
import StyledNaturalKey from './StyledNaturalKey' | |||
it('should exist', () => { | |||
expect(StyledNaturalKey).toBeDefined() | |||
}) | |||
it('should be a React component', () => { | |||
expect(ReactIs.isValidElementType(StyledNaturalKey)).toBe(true) | |||
}) | |||
it('should render without crashing', () => { | |||
expect(() => <StyledNaturalKey />).not.toThrow() | |||
}) |
@@ -0,0 +1,204 @@ | |||
import * as React from 'react' | |||
import * as PropTypes from 'prop-types' | |||
import styled from 'styled-components' | |||
import keyPropTypes from '../../services/keyPropTypes' | |||
const Base = styled('div')({ | |||
width: '100%', | |||
height: '100%', | |||
position: 'relative', | |||
}) | |||
const N1 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
position: relative; | |||
` | |||
const N2 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N3 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
padding: 1px 0 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N4 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: var(--color-natural-key, transparent); | |||
border-radius: 0 0 1px 1px; | |||
` | |||
const N5 = styled('div')` | |||
width: 100%; | |||
height: calc(33 / 80 * 100%); | |||
padding: 0 1px 2px 2px; | |||
box-sizing: border-box; | |||
background-clip: content-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
opacity: 0.25; | |||
mask-image: linear-gradient(to bottom, transparent, white); | |||
` | |||
const N6 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
padding: 1px 2px 3px 3px; | |||
box-sizing: border-box; | |||
background-clip: content-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
opacity: 0.08; | |||
mask-image: linear-gradient(to bottom, transparent, white); | |||
` | |||
const N7 = styled('div')` | |||
width: 100%; | |||
height: 2px; | |||
padding: 0 0 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const N8 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
border-radius: 0 0 1px 1px; | |||
opacity: 0.25; | |||
` | |||
const N9 = styled('div')` | |||
width: 2px; | |||
height: 100%; | |||
padding: 1px 0 1px 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
left: 0; | |||
` | |||
const N10 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
border-radius: 0 0 0 1px; | |||
opacity: 0.07; | |||
` | |||
const N11 = styled('div')` | |||
width: 100%; | |||
height: 6px; | |||
padding: 1px 0 0 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N12 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
mask-image: linear-gradient(to bottom, white, transparent); | |||
opacity: 0.12; | |||
` | |||
const N13 = styled('div')` | |||
width: 100%; | |||
padding: 1px 0 0 1px; | |||
box-sizing: border-box; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
` | |||
const N14 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: black; | |||
opacity: 0.12; | |||
` | |||
const N15 = styled('div')` | |||
width: 1px; | |||
height: 100%; | |||
padding: 1px 0 1px 0; | |||
box-sizing: border-box; | |||
position: absolute; | |||
bottom: 0; | |||
right: 0; | |||
` | |||
const N16 = styled('div')` | |||
width: 100%; | |||
height: 100%; | |||
background-color: white; | |||
border-radius: 0 0 1px 0; | |||
opacity: 0.12; | |||
` | |||
type Props = PropTypes.InferProps<typeof keyPropTypes> | |||
const StyledNaturalKey: React.FC<Props> = ({ keyChannels }) => { | |||
const hasKeyChannels = Array.isArray(keyChannels!) && keyChannels.length > 0 | |||
return ( | |||
<Base> | |||
<N1 | |||
style={{ | |||
// @ts-ignore | |||
'--color-natural-key': hasKeyChannels ? `var(--color-channel-${keyChannels![0]!.channel})` : undefined, | |||
}} | |||
> | |||
<N2 /> | |||
<N3> | |||
<N4 | |||
style={{ | |||
opacity: hasKeyChannels ? 0.75 : 1, | |||
}} | |||
/> | |||
</N3> | |||
<N5 | |||
style={{ | |||
backgroundColor: hasKeyChannels ? 'black' : 'white', | |||
opacity: hasKeyChannels ? 0.12 : 0.25, | |||
}} | |||
/> | |||
<N6 /> | |||
<N7> | |||
<N8 /> | |||
</N7> | |||
<N9> | |||
<N10 /> | |||
</N9> | |||
<N11> | |||
<N12 /> | |||
</N11> | |||
<N13 | |||
style={{ | |||
height: hasKeyChannels ? 4 : 3, | |||
}} | |||
> | |||
<N14 /> | |||
</N13> | |||
<N15> | |||
<N16 /> | |||
</N15> | |||
</N1> | |||
</Base> | |||
) | |||
} | |||
StyledNaturalKey.propTypes = keyPropTypes | |||
export default StyledNaturalKey |
@@ -1,3 +1,6 @@ | |||
import Keyboard from './components/Keyboard' | |||
import Keyboard from './components/Keyboard/Keyboard' | |||
import keyPropTypes from './services/keyPropTypes' | |||
export default Keyboard | |||
export { keyPropTypes } |
@@ -1,11 +1,10 @@ | |||
interface GenerateKeys { | |||
(startKey: number, endKey: number): number[], | |||
(startKey: number, endKey: number): number[] | |||
} | |||
const generateKeys: GenerateKeys = (startKey, endKey) => ( | |||
const generateKeys: GenerateKeys = (startKey, endKey) => | |||
Array(endKey! - startKey! + 1) | |||
.fill(0) | |||
.map((_, i) => startKey! + i) | |||
) | |||
export default generateKeys |
@@ -5,44 +5,34 @@ import groupKeysIntoOctaves from './groupKeysIntoOctaves' | |||
import getOctaveCompleteness from './getOctaveCompleteness' | |||
interface GetKeyLeft { | |||
(k: number): number, | |||
(k: number): number | |||
} | |||
interface GetKeyLeftDecorator { | |||
(startKey: number, endKey: number): GetKeyLeft, | |||
(startKey: number, endKey: number): GetKeyLeft | |||
} | |||
const getKeyLeftDecorator: GetKeyLeftDecorator = (startKey, endKey): GetKeyLeft => (k) => { | |||
const dummyKeys = generateKeys(startKey, endKey) | |||
const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) | |||
const octaveCompleteness = Object | |||
.entries(keysGroupedIntoOctaves) | |||
.map<number[]>(([octave, keys]) => [ | |||
octave as unknown as number, | |||
keys[0], | |||
keys.slice(-1)[0], | |||
]) | |||
const octaveCompleteness = Object.entries(keysGroupedIntoOctaves) | |||
.map<number[]>(([octave, keys]) => [(octave as unknown) as number, keys[0], keys.slice(-1)[0]]) | |||
.reduce<Record<number, number>>( | |||
(theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | |||
...theOctaveCompleteness, | |||
[octave]: getOctaveCompleteness(firstKey, lastKey), | |||
}), | |||
{} | |||
{}, | |||
) | |||
const fractionalOctaveCount = Object | |||
.values(octaveCompleteness) | |||
.reduce( | |||
(a, b) => a + b, | |||
0 | |||
) | |||
const fractionalOctaveCount = Object.values(octaveCompleteness).reduce((a, b) => a + b, 0) | |||
const octaveCount = getOctaveCount(startKey, endKey) | |||
const startOctave = Math.floor(startKey! / 12) | |||
const octave = Math.floor(k / 12) | |||
const octaveOffset = (100 * octaveCount / fractionalOctaveCount) / octaveCount * (octave - startOctave) | |||
const theKeyOffset = octaveOffset + (100 * octaveCount / fractionalOctaveCount / octaveCount * getKeyXOffset(k)) | |||
const firstKeyOffset = (100 * octaveCount / fractionalOctaveCount / octaveCount * getKeyXOffset(startKey + 12)) | |||
const octaveOffset = ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * (octave - startOctave) | |||
const theKeyOffset = octaveOffset + ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * getKeyXOffset(k) | |||
const firstKeyOffset = ((100 * octaveCount) / fractionalOctaveCount / octaveCount) * getKeyXOffset(startKey + 12) | |||
return theKeyOffset - firstKeyOffset | |||
} | |||
@@ -5,11 +5,11 @@ import getOctaveCount from './getOctaveCount' | |||
import generateKeys from './generateKeys' | |||
interface GetKeyWidth { | |||
(k: number): number, | |||
(k: number): number | |||
} | |||
interface GetKeyWidthDecorator { | |||
(startKey: number, endKey: number): GetKeyWidth, | |||
(startKey: number, endKey: number): GetKeyWidth | |||
} | |||
const ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO = 13 / 23 | |||
@@ -17,29 +17,19 @@ const ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO = 13 / 23 | |||
const getKeyWidthDecorator: GetKeyWidthDecorator = (startKey, endKey): GetKeyWidth => (k) => { | |||
const dummyKeys = generateKeys(startKey, endKey) | |||
const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) | |||
const octaveCompleteness = Object | |||
.entries(keysGroupedIntoOctaves) | |||
.map<number[]>(([octave, keys]) => [ | |||
octave as unknown as number, | |||
keys[0], | |||
keys.slice(-1)[0], | |||
]) | |||
const octaveCompleteness = Object.entries(keysGroupedIntoOctaves) | |||
.map<number[]>(([octave, keys]) => [(octave as unknown) as number, keys[0], keys.slice(-1)[0]]) | |||
.reduce<Record<number, number>>( | |||
(theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | |||
...theOctaveCompleteness, | |||
[octave]: getOctaveCompleteness(firstKey, lastKey), | |||
}), | |||
{} | |||
{}, | |||
) | |||
const fractionalOctaveCount = Object | |||
.values(octaveCompleteness) | |||
.reduce( | |||
(a, b) => a + b, | |||
0 | |||
) | |||
const fractionalOctaveCount = Object.values(octaveCompleteness).reduce((a, b) => a + b, 0) | |||
const octaveCount = getOctaveCount(startKey, endKey) | |||
const naturalKeyWidth = 100 * (octaveCount / fractionalOctaveCount) / (octaveCount * 7) | |||
const naturalKeyWidth = (100 * (octaveCount / fractionalOctaveCount)) / (octaveCount * 7) | |||
return isNaturalKey(k) ? naturalKeyWidth : naturalKeyWidth * ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO | |||
} | |||
@@ -2,9 +2,9 @@ import getKeyXOffset from './getKeyXOffset' | |||
import isNaturalKey from './isNaturalKey' | |||
// expect firstKey and lastKey within the same octave | |||
export default (firstKey: number, lastKey: number) => ( | |||
export default (firstKey: number, lastKey: number) => | |||
// see if there are missing higher notes | |||
getKeyXOffset(lastKey) + (isNaturalKey(lastKey) ? 1 / 7 : 1 / 7 * 18/36) | |||
getKeyXOffset(lastKey) + | |||
(isNaturalKey(lastKey) ? 1 / 7 : ((1 / 7) * 18) / 36) - | |||
// see if there are missing lower notes | |||
- getKeyXOffset(firstKey) | |||
) | |||
getKeyXOffset(firstKey) |
@@ -1,5 +1,5 @@ | |||
interface GetOctaveCount { | |||
(startKey: number, endKey: number): number, | |||
(startKey: number, endKey: number): number | |||
} | |||
const getOctaveCount: GetOctaveCount = (startKey, endKey) => { | |||
@@ -1,15 +1,10 @@ | |||
export default (dummyKeys: number[]): Record<number, number[]> => ( | |||
export default (dummyKeys: number[]): Record<number, number[]> => | |||
dummyKeys | |||
.map(k => [k, Math.floor(k / 12)]) | |||
.map((k) => [k, Math.floor(k / 12)]) | |||
.reduce<Record<number, number[]>>( | |||
(theOctaves, [key, keyOctave, ]) => ({ | |||
(theOctaves, [key, keyOctave]) => ({ | |||
...theOctaves, | |||
[keyOctave]: ( | |||
Array.isArray(theOctaves[keyOctave]) | |||
? [...theOctaves[keyOctave], key] | |||
: [key] | |||
) | |||
[keyOctave]: Array.isArray(theOctaves[keyOctave]) ? [...theOctaves[keyOctave], key] : [key], | |||
}), | |||
{} | |||
{}, | |||
) | |||
) |
@@ -16,11 +16,11 @@ it('should accept 1 parameter', () => { | |||
it('should throw TypeError upon passing invalid types', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.anything().filter(anything => typeof anything !== 'number'), | |||
anything => { | |||
fc.anything().filter((anything) => typeof anything !== 'number'), | |||
(anything) => { | |||
expect(() => isNaturalKey(anything as number)).toThrowError(TypeError) | |||
} | |||
) | |||
}, | |||
), | |||
) | |||
}) | |||
@@ -31,15 +31,11 @@ it('should throw RangeError upon passing NaN', () => { | |||
it('should throw RangeError upon passing negative numbers', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.anything().filter(anything => ( | |||
typeof anything! === 'number' | |||
&& !isNaN(anything) | |||
&& anything < 0 | |||
)), | |||
negativeValue => { | |||
fc.anything().filter((anything) => typeof anything! === 'number' && !isNaN(anything) && anything < 0), | |||
(negativeValue) => { | |||
expect(() => isNaturalKey(negativeValue as number)).toThrowError(RangeError) | |||
} | |||
) | |||
}, | |||
), | |||
) | |||
}) | |||
@@ -47,30 +43,22 @@ describe('upon passing a positive number or zero', () => { | |||
it('should not throw any error', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.anything().filter(anything => ( | |||
typeof anything! === 'number' | |||
&& !isNaN(anything) | |||
&& anything >= 0 | |||
)), | |||
value => { | |||
fc.anything().filter((anything) => typeof anything! === 'number' && !isNaN(anything) && anything >= 0), | |||
(value) => { | |||
expect(() => isNaturalKey(value as number)).not.toThrow() | |||
} | |||
) | |||
}, | |||
), | |||
) | |||
}) | |||
it('should return a boolean', () => { | |||
fc.assert( | |||
fc.property( | |||
fc.anything().filter(anything => ( | |||
typeof anything! === 'number' | |||
&& !isNaN(anything) | |||
&& anything >= 0 | |||
)), | |||
value => { | |||
fc.anything().filter((anything) => typeof anything! === 'number' && !isNaN(anything) && anything >= 0), | |||
(value) => { | |||
expect(typeof isNaturalKey(value as number)).toBe('boolean') | |||
} | |||
) | |||
}, | |||
), | |||
) | |||
}) | |||
}) |
@@ -2,7 +2,7 @@ const NATURAL_KEYS = [0, 2, 4, 5, 7, 9, 11] | |||
export default (k: number): boolean => { | |||
const type = typeof (k as unknown) | |||
if (type as string !== 'number') { | |||
if ((type as string) !== 'number') { | |||
throw TypeError(`Invalid value type passed to isNaturalKey, expected 'number', got ${type}.`) | |||
} | |||
if (isNaN(k)) { | |||
@@ -0,0 +1,11 @@ | |||
import * as PropTypes from 'prop-types' | |||
export default { | |||
keyChannels: PropTypes.arrayOf( | |||
PropTypes.shape({ | |||
channel: PropTypes.number.isRequired, | |||
key: PropTypes.number.isRequired, | |||
velocity: PropTypes.number.isRequired, | |||
}), | |||
), | |||
} |
@@ -2153,6 +2153,13 @@ | |||
dependencies: | |||
"@types/react" "*" | |||
"@types/react-is@^16.7.1": | |||
version "16.7.1" | |||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-16.7.1.tgz#d3f1c68c358c00ce116b55ef5410cf486dd08539" | |||
integrity sha512-dMLFD2cCsxtDgMkTydQCM0PxDq8vwc6uN5M/jRktDfYvH3nQj6pjC9OrCXS2lKlYoYTNJorI/dI8x9dpLshexQ== | |||
dependencies: | |||
"@types/react" "*" | |||
"@types/react-native@*": | |||
version "0.63.4" | |||
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.63.4.tgz#4610b0df0e5f0db4c68d495754ad9f8bc3b8893f" | |||