Use custom styled keys and add pressed keys for styled key set.master
@@ -41,6 +41,7 @@ | |||||
"@types/prop-types": "^15.7.3", | "@types/prop-types": "^15.7.3", | ||||
"@types/react": "^16.9.44", | "@types/react": "^16.9.44", | ||||
"@types/react-dom": "^16.9.8", | "@types/react-dom": "^16.9.8", | ||||
"@types/react-is": "^16.7.1", | |||||
"@types/styled-components": "^5.1.2", | "@types/styled-components": "^5.1.2", | ||||
"babel-loader": "^8.1.0", | "babel-loader": "^8.1.0", | ||||
"fast-check": "^2.0.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 default Keyboard | ||||
export { keyPropTypes } |
@@ -1,11 +1,10 @@ | |||||
interface GenerateKeys { | 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) | Array(endKey! - startKey! + 1) | ||||
.fill(0) | .fill(0) | ||||
.map((_, i) => startKey! + i) | .map((_, i) => startKey! + i) | ||||
) | |||||
export default generateKeys | export default generateKeys |
@@ -5,44 +5,34 @@ import groupKeysIntoOctaves from './groupKeysIntoOctaves' | |||||
import getOctaveCompleteness from './getOctaveCompleteness' | import getOctaveCompleteness from './getOctaveCompleteness' | ||||
interface GetKeyLeft { | interface GetKeyLeft { | ||||
(k: number): number, | |||||
(k: number): number | |||||
} | } | ||||
interface GetKeyLeftDecorator { | interface GetKeyLeftDecorator { | ||||
(startKey: number, endKey: number): GetKeyLeft, | |||||
(startKey: number, endKey: number): GetKeyLeft | |||||
} | } | ||||
const getKeyLeftDecorator: GetKeyLeftDecorator = (startKey, endKey): GetKeyLeft => (k) => { | const getKeyLeftDecorator: GetKeyLeftDecorator = (startKey, endKey): GetKeyLeft => (k) => { | ||||
const dummyKeys = generateKeys(startKey, endKey) | const dummyKeys = generateKeys(startKey, endKey) | ||||
const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) | 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>>( | .reduce<Record<number, number>>( | ||||
(theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | (theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | ||||
...theOctaveCompleteness, | ...theOctaveCompleteness, | ||||
[octave]: getOctaveCompleteness(firstKey, lastKey), | [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 octaveCount = getOctaveCount(startKey, endKey) | ||||
const startOctave = Math.floor(startKey! / 12) | const startOctave = Math.floor(startKey! / 12) | ||||
const octave = Math.floor(k / 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 | return theKeyOffset - firstKeyOffset | ||||
} | } | ||||
@@ -5,11 +5,11 @@ import getOctaveCount from './getOctaveCount' | |||||
import generateKeys from './generateKeys' | import generateKeys from './generateKeys' | ||||
interface GetKeyWidth { | interface GetKeyWidth { | ||||
(k: number): number, | |||||
(k: number): number | |||||
} | } | ||||
interface GetKeyWidthDecorator { | interface GetKeyWidthDecorator { | ||||
(startKey: number, endKey: number): GetKeyWidth, | |||||
(startKey: number, endKey: number): GetKeyWidth | |||||
} | } | ||||
const ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO = 13 / 23 | 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 getKeyWidthDecorator: GetKeyWidthDecorator = (startKey, endKey): GetKeyWidth => (k) => { | ||||
const dummyKeys = generateKeys(startKey, endKey) | const dummyKeys = generateKeys(startKey, endKey) | ||||
const keysGroupedIntoOctaves = groupKeysIntoOctaves(dummyKeys) | 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>>( | .reduce<Record<number, number>>( | ||||
(theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | (theOctaveCompleteness, [octave, firstKey, lastKey]) => ({ | ||||
...theOctaveCompleteness, | ...theOctaveCompleteness, | ||||
[octave]: getOctaveCompleteness(firstKey, lastKey), | [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 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 | return isNaturalKey(k) ? naturalKeyWidth : naturalKeyWidth * ACCIDENTAL_KEY_TO_NATURAL_KEY_WIDTH_RATIO | ||||
} | } | ||||
@@ -2,9 +2,9 @@ import getKeyXOffset from './getKeyXOffset' | |||||
import isNaturalKey from './isNaturalKey' | import isNaturalKey from './isNaturalKey' | ||||
// expect firstKey and lastKey within the same octave | // 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 | // 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 | // see if there are missing lower notes | ||||
- getKeyXOffset(firstKey) | |||||
) | |||||
getKeyXOffset(firstKey) |
@@ -1,5 +1,5 @@ | |||||
interface GetOctaveCount { | interface GetOctaveCount { | ||||
(startKey: number, endKey: number): number, | |||||
(startKey: number, endKey: number): number | |||||
} | } | ||||
const getOctaveCount: GetOctaveCount = (startKey, endKey) => { | const getOctaveCount: GetOctaveCount = (startKey, endKey) => { | ||||
@@ -1,15 +1,10 @@ | |||||
export default (dummyKeys: number[]): Record<number, number[]> => ( | |||||
export default (dummyKeys: number[]): Record<number, number[]> => | |||||
dummyKeys | dummyKeys | ||||
.map(k => [k, Math.floor(k / 12)]) | |||||
.map((k) => [k, Math.floor(k / 12)]) | |||||
.reduce<Record<number, number[]>>( | .reduce<Record<number, number[]>>( | ||||
(theOctaves, [key, keyOctave, ]) => ({ | |||||
(theOctaves, [key, keyOctave]) => ({ | |||||
...theOctaves, | ...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', () => { | it('should throw TypeError upon passing invalid types', () => { | ||||
fc.assert( | fc.assert( | ||||
fc.property( | 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) | 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', () => { | it('should throw RangeError upon passing negative numbers', () => { | ||||
fc.assert( | fc.assert( | ||||
fc.property( | 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) | 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', () => { | it('should not throw any error', () => { | ||||
fc.assert( | fc.assert( | ||||
fc.property( | 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() | expect(() => isNaturalKey(value as number)).not.toThrow() | ||||
} | |||||
) | |||||
}, | |||||
), | |||||
) | ) | ||||
}) | }) | ||||
it('should return a boolean', () => { | it('should return a boolean', () => { | ||||
fc.assert( | fc.assert( | ||||
fc.property( | 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') | 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 => { | export default (k: number): boolean => { | ||||
const type = typeof (k as unknown) | 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}.`) | throw TypeError(`Invalid value type passed to isNaturalKey, expected 'number', got ${type}.`) | ||||
} | } | ||||
if (isNaN(k)) { | 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: | dependencies: | ||||
"@types/react" "*" | "@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@*": | "@types/react-native@*": | ||||
version "0.63.4" | version "0.63.4" | ||||
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.63.4.tgz#4610b0df0e5f0db4c68d495754ad9f8bc3b8893f" | resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.63.4.tgz#4610b0df0e5f0db4c68d495754ad9f8bc3b8893f" | ||||