Browse Source

Improve component styling

Use custom styled keys and add pressed keys for styled key set.
master
TheoryOfNekomata 4 years ago
parent
commit
25ef42763f
26 changed files with 920 additions and 696 deletions
  1. +1
    -0
      package.json
  2. +15
    -0
      src/components/AccidentalKey/AccidentalKey.test.tsx
  3. +42
    -0
      src/components/AccidentalKey/AccidentalKey.tsx
  4. +0
    -443
      src/components/Keyboard.stories.tsx
  5. +0
    -9
      src/components/Keyboard.test.tsx
  6. +0
    -160
      src/components/Keyboard.tsx
  7. +119
    -0
      src/components/Keyboard/Keyboard.stories.tsx
  8. +15
    -0
      src/components/Keyboard/Keyboard.test.tsx
  9. +140
    -0
      src/components/Keyboard/Keyboard.tsx
  10. +15
    -0
      src/components/NaturalKey/NaturalKey.test.tsx
  11. +42
    -0
      src/components/NaturalKey/NaturalKey.tsx
  12. +15
    -0
      src/components/StyledAccidentalKey/StyledAccidentalKey.test.tsx
  13. +230
    -0
      src/components/StyledAccidentalKey/StyledAccidentalKey.tsx
  14. +15
    -0
      src/components/StyledNaturalKey/StyledNaturalKey.test.tsx
  15. +204
    -0
      src/components/StyledNaturalKey/StyledNaturalKey.tsx
  16. +4
    -1
      src/index.ts
  17. +2
    -3
      src/services/generateKeys.ts
  18. +9
    -19
      src/services/getKeyLeft.ts
  19. +7
    -17
      src/services/getKeyWidth.ts
  20. +4
    -4
      src/services/getOctaveCompleteness.ts
  21. +1
    -1
      src/services/getOctaveCount.ts
  22. +5
    -10
      src/services/groupKeysIntoOctaves.ts
  23. +16
    -28
      src/services/isNaturalKey.test.ts
  24. +1
    -1
      src/services/isNaturalKey.ts
  25. +11
    -0
      src/services/keyPropTypes.ts
  26. +7
    -0
      yarn.lock

+ 1
- 0
package.json View File

@@ -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",


+ 15
- 0
src/components/AccidentalKey/AccidentalKey.test.tsx View File

@@ -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()
})

+ 42
- 0
src/components/AccidentalKey/AccidentalKey.tsx View File

@@ -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

+ 0
- 443
src/components/Keyboard.stories.tsx View File

@@ -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>
),
}}
/>
)

+ 0
- 9
src/components/Keyboard.test.tsx View File

@@ -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);
});

+ 0
- 160
src/components/Keyboard.tsx View File

@@ -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

+ 119
- 0
src/components/Keyboard/Keyboard.stories.tsx View File

@@ -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>
)

+ 15
- 0
src/components/Keyboard/Keyboard.test.tsx View File

@@ -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()
})

+ 140
- 0
src/components/Keyboard/Keyboard.tsx View File

@@ -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

+ 15
- 0
src/components/NaturalKey/NaturalKey.test.tsx View File

@@ -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()
})

+ 42
- 0
src/components/NaturalKey/NaturalKey.tsx View File

@@ -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

+ 15
- 0
src/components/StyledAccidentalKey/StyledAccidentalKey.test.tsx View File

@@ -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()
})

+ 230
- 0
src/components/StyledAccidentalKey/StyledAccidentalKey.tsx View File

@@ -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

+ 15
- 0
src/components/StyledNaturalKey/StyledNaturalKey.test.tsx View File

@@ -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()
})

+ 204
- 0
src/components/StyledNaturalKey/StyledNaturalKey.tsx View File

@@ -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

+ 4
- 1
src/index.ts View File

@@ -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 }

+ 2
- 3
src/services/generateKeys.ts View File

@@ -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

+ 9
- 19
src/services/getKeyLeft.ts View File

@@ -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
}



+ 7
- 17
src/services/getKeyWidth.ts View File

@@ -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
}



+ 4
- 4
src/services/getOctaveCompleteness.ts View File

@@ -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
- 1
src/services/getOctaveCount.ts View File

@@ -1,5 +1,5 @@
interface GetOctaveCount {
(startKey: number, endKey: number): number,
(startKey: number, endKey: number): number
}

const getOctaveCount: GetOctaveCount = (startKey, endKey) => {


+ 5
- 10
src/services/groupKeysIntoOctaves.ts View File

@@ -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
- 28
src/services/isNaturalKey.test.ts View File

@@ -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')
}
)
},
),
)
})
})

+ 1
- 1
src/services/isNaturalKey.ts View File

@@ -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)) {


+ 11
- 0
src/services/keyPropTypes.ts View File

@@ -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,
}),
),
}

+ 7
- 0
yarn.lock View File

@@ -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"


Loading…
Cancel
Save