@@ -0,0 +1,11 @@ | |||||
root = true | |||||
[*] | |||||
charset = utf-8 | |||||
end_of_line = lf | |||||
indent_size = 2 | |||||
indent_style = space | |||||
insert_final_newline = true | |||||
max_line_length = 120 | |||||
tab_width = 8 | |||||
trim_trailing_whitespace = true |
@@ -0,0 +1,7 @@ | |||||
*.log | |||||
.DS_Store | |||||
node_modules | |||||
.cache | |||||
dist | |||||
.idea/ | |||||
coverage/ |
@@ -0,0 +1,6 @@ | |||||
{ | |||||
"jsxSingleQuote": false, | |||||
"singleQuote": true, | |||||
"semi": false, | |||||
"trailingComma": "es5" | |||||
} |
@@ -0,0 +1,24 @@ | |||||
module.exports = { | |||||
stories: ['../src/**/*.stories.(ts|tsx)'], | |||||
addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-docs'], | |||||
webpackFinal: async (config) => { | |||||
config.module.rules.push({ | |||||
test: /\.(ts|tsx)$/, | |||||
use: [ | |||||
{ | |||||
loader: require.resolve('ts-loader'), | |||||
options: { | |||||
transpileOnly: true, | |||||
}, | |||||
}, | |||||
{ | |||||
loader: require.resolve('react-docgen-typescript-loader'), | |||||
}, | |||||
], | |||||
}); | |||||
config.resolve.extensions.push('.ts', '.tsx'); | |||||
return config; | |||||
}, | |||||
}; |
@@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2020 TheoryOfNekomata | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@@ -0,0 +1,7 @@ | |||||
# React Fretboard | |||||
Fretboard for any stringed instrument, made with React. | |||||
## License | |||||
[MIT](./LICENSE) |
@@ -0,0 +1,3 @@ | |||||
node_modules | |||||
.cache | |||||
dist |
@@ -0,0 +1,14 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<meta charset="UTF-8" /> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> | |||||
<title>Playground</title> | |||||
</head> | |||||
<body> | |||||
<div id="root"></div> | |||||
<script src="./index.tsx"></script> | |||||
</body> | |||||
</html> |
@@ -0,0 +1,14 @@ | |||||
import 'react-app-polyfill/ie11'; | |||||
import * as React from 'react'; | |||||
import * as ReactDOM from 'react-dom'; | |||||
import Fretboard from '../.'; | |||||
const App = () => { | |||||
return ( | |||||
<div> | |||||
<Fretboard /> | |||||
</div> | |||||
); | |||||
}; | |||||
ReactDOM.render(<App />, document.getElementById('root')); |
@@ -0,0 +1,24 @@ | |||||
{ | |||||
"name": "example", | |||||
"version": "1.0.0", | |||||
"main": "index.js", | |||||
"license": "MIT", | |||||
"scripts": { | |||||
"start": "parcel index.html", | |||||
"build": "parcel build index.html" | |||||
}, | |||||
"dependencies": { | |||||
"react-app-polyfill": "^1.0.0" | |||||
}, | |||||
"alias": { | |||||
"react": "../node_modules/react", | |||||
"react-dom": "../node_modules/react-dom/profiling", | |||||
"scheduler/tracing": "../node_modules/scheduler/tracing-profiling" | |||||
}, | |||||
"devDependencies": { | |||||
"@types/react": "^16.9.11", | |||||
"@types/react-dom": "^16.8.4", | |||||
"parcel": "^1.12.3", | |||||
"typescript": "^3.4.5" | |||||
} | |||||
} |
@@ -0,0 +1,18 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
"allowSyntheticDefaultImports": false, | |||||
"target": "es5", | |||||
"module": "commonjs", | |||||
"jsx": "react", | |||||
"moduleResolution": "node", | |||||
"noImplicitAny": false, | |||||
"noUnusedLocals": false, | |||||
"noUnusedParameters": false, | |||||
"removeComments": true, | |||||
"strictNullChecks": true, | |||||
"preserveConstEnums": true, | |||||
"sourceMap": true, | |||||
"lib": ["es2015", "es2016", "dom"], | |||||
"types": ["node"] | |||||
} | |||||
} |
@@ -0,0 +1,56 @@ | |||||
{ | |||||
"version": "0.1.0", | |||||
"license": "MIT", | |||||
"main": "dist/index.js", | |||||
"typings": "dist/index.d.ts", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=10" | |||||
}, | |||||
"scripts": { | |||||
"start": "tsdx watch", | |||||
"build": "tsdx build", | |||||
"test": "tsdx test --passWithNoTests", | |||||
"lint": "tsdx lint", | |||||
"prepare": "tsdx build", | |||||
"storybook": "start-storybook -p 6006", | |||||
"build-storybook": "build-storybook" | |||||
}, | |||||
"peerDependencies": { | |||||
"react": ">=16" | |||||
}, | |||||
"husky": { | |||||
"hooks": { | |||||
"pre-commit": "tsdx lint" | |||||
} | |||||
}, | |||||
"name": "@theoryofnekomata/react-fretboard", | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"module": "dist/react-fretboard.esm.js", | |||||
"devDependencies": { | |||||
"@babel/core": "^7.11.6", | |||||
"@storybook/addon-actions": "^6.0.22", | |||||
"@storybook/addon-docs": "^6.0.22", | |||||
"@storybook/addon-info": "^5.3.21", | |||||
"@storybook/addon-links": "^6.0.22", | |||||
"@storybook/addons": "^6.0.22", | |||||
"@storybook/react": "^6.0.22", | |||||
"@types/prop-types": "^15.7.3", | |||||
"@types/react": "^16.9.49", | |||||
"@types/react-dom": "^16.9.8", | |||||
"babel-loader": "^8.1.0", | |||||
"husky": "^4.3.0", | |||||
"prop-types": "^15.7.2", | |||||
"react": "^16.13.1", | |||||
"react-docgen-typescript-loader": "^3.7.2", | |||||
"react-dom": "^16.13.1", | |||||
"react-is": "^16.13.1", | |||||
"ts-loader": "^8.0.4", | |||||
"tsdx": "^0.14.0", | |||||
"tslib": "^2.0.1", | |||||
"typescript": "^4.0.3" | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
import React from 'react' | |||||
import Fretboard, { Props } from './Fretboard' | |||||
export default { | |||||
title: 'Welcome', | |||||
} | |||||
// 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>) => <Fretboard {...props} /> |
@@ -0,0 +1,185 @@ | |||||
import React, { FC } from 'react' | |||||
import * as PropTypes from 'prop-types' | |||||
const propTypes = { | |||||
tunings: PropTypes.arrayOf(PropTypes.number), | |||||
frets: PropTypes.number, | |||||
leftHanded: PropTypes.bool, | |||||
} | |||||
export type Props = PropTypes.InferProps<typeof propTypes> | |||||
const PITCHES = 'C C# D D# E F F# G G# A A# B'.split(' ') | |||||
const Fretboard: FC<Props> = ({ tunings = [64, 59, 55, 50, 45, 40], frets = 24, leftHanded = false }) => { | |||||
if (!Array.isArray(tunings!)) { | |||||
return null | |||||
} | |||||
const strings = tunings!.map(t => new Array(frets).fill(0).map((_, i) => t! + i + 1)) | |||||
return ( | |||||
<div | |||||
style={{ | |||||
border: '0.0625rem solid', | |||||
boxSizing: 'border-box', | |||||
transform: leftHanded | |||||
? 'perspective(6rem) rotateY(2deg) scaleX(1.375)' | |||||
: 'perspective(6rem) rotateY(-2deg) scaleX(1.375)', | |||||
transformOrigin: leftHanded ? 'left' : 'right', | |||||
}} | |||||
> | |||||
{strings!.map((pitches, stringNumber) => ( | |||||
<div | |||||
style={{ | |||||
display: 'flex', | |||||
flexDirection: leftHanded ? 'row-reverse' : 'row', | |||||
}} | |||||
> | |||||
<div | |||||
style={{ | |||||
display: 'flex', | |||||
flexDirection: leftHanded ? 'row-reverse' : 'row', | |||||
width: '5rem', | |||||
height: '1rem', | |||||
padding: 0, | |||||
boxSizing: 'border-box', | |||||
}} | |||||
> | |||||
<input | |||||
type="number" | |||||
style={{ | |||||
display: 'block', | |||||
width: '3rem', | |||||
height: '1rem', | |||||
padding: 0, | |||||
boxSizing: 'border-box', | |||||
}} | |||||
defaultValue={tunings[stringNumber]!} | |||||
min={0} | |||||
max={127} | |||||
/> | |||||
<span | |||||
style={{ | |||||
display: 'block', | |||||
width: '2rem', | |||||
height: '1rem', | |||||
textAlign: leftHanded ? 'left' : 'right', | |||||
}} | |||||
> | |||||
{PITCHES[tunings[stringNumber]! % 12]} | |||||
{Math.floor(tunings[stringNumber]! / 12)} | |||||
</span> | |||||
</div> | |||||
<div | |||||
style={{ | |||||
display: 'block', | |||||
position: 'relative', | |||||
width: '1rem', | |||||
height: '1rem', | |||||
flexShrink: 0, | |||||
padding: 0, | |||||
borderWidth: '0 0.0625rem', | |||||
borderStyle: 'none solid', | |||||
}} | |||||
> | |||||
<button | |||||
type="button" | |||||
style={{ | |||||
position: 'relative', | |||||
display: 'block', | |||||
width: '1rem', | |||||
height: '1rem', | |||||
flexShrink: 0, | |||||
padding: 0, | |||||
border: 0, | |||||
backgroundColor: 'transparent', | |||||
}} | |||||
> | |||||
<span | |||||
style={{ | |||||
display: 'block', | |||||
width: '100%', | |||||
height: `${0.03125 + stringNumber * 0.03125}rem`, | |||||
backgroundColor: 'currentColor', | |||||
}} | |||||
/> | |||||
</button> | |||||
</div> | |||||
{pitches.map((_, i) => ( | |||||
<div | |||||
style={{ | |||||
width: `${(frets! - i + frets! / 2) * 100}%`, | |||||
height: '1rem', | |||||
position: 'relative', | |||||
}} | |||||
> | |||||
{(i % 12 === 2 || i % 12 === 4 || i % 12 === 6 || i % 12 === 8 || i % 12 === 11) && stringNumber === 0 && ( | |||||
<div | |||||
style={{ | |||||
position: 'absolute', | |||||
top: 0, | |||||
height: `${tunings.length * 100}%`, | |||||
width: '100%', | |||||
display: 'flex', | |||||
alignItems: 'center', | |||||
justifyContent: 'space-around', | |||||
flexDirection: 'column', | |||||
}} | |||||
> | |||||
<div | |||||
style={{ | |||||
width: `${0.75 - i * (1 / 80)}rem`, | |||||
height: `${0.75 - i * (1 / 256)}rem`, | |||||
backgroundColor: 'currentColor', | |||||
opacity: 0.5, | |||||
borderRadius: '50%', | |||||
}} | |||||
/> | |||||
{i % 12 === 11 && ( | |||||
<div | |||||
style={{ | |||||
width: `${0.75 - i * (1 / 80)}rem`, | |||||
height: `${0.75 - i * (1 / 256)}rem`, | |||||
backgroundColor: 'currentColor', | |||||
opacity: 0.5, | |||||
borderRadius: '50%', | |||||
}} | |||||
/> | |||||
)} | |||||
</div> | |||||
)} | |||||
<button | |||||
key={i} | |||||
type="button" | |||||
style={{ | |||||
position: 'relative', | |||||
display: 'block', | |||||
width: '100%', | |||||
height: '1rem', | |||||
padding: 0, | |||||
borderWidth: '0 0.0625rem', | |||||
borderStyle: 'none solid', | |||||
backgroundColor: 'transparent', | |||||
}} | |||||
> | |||||
<span | |||||
style={{ | |||||
display: 'block', | |||||
width: '100%', | |||||
height: `${0.03125 + stringNumber * 0.03125}rem`, | |||||
backgroundColor: 'currentColor', | |||||
}} | |||||
/> | |||||
</button> | |||||
</div> | |||||
))} | |||||
</div> | |||||
))} | |||||
</div> | |||||
) | |||||
} | |||||
Fretboard.propTypes = propTypes | |||||
export default Fretboard |
@@ -0,0 +1,11 @@ | |||||
import React, { FC, HTMLAttributes, ReactChild } from 'react' | |||||
export interface Props extends HTMLAttributes<HTMLDivElement> { | |||||
children?: ReactChild | |||||
} | |||||
// Please do not use types off of a default export module or else Storybook Docs will suffer. | |||||
// see: https://github.com/storybookjs/storybook/issues/9556 | |||||
export const Thing: FC<Props> = ({ children }) => { | |||||
return <div>{children || `the snozzberries taste like snozzberries`}</div> | |||||
} |
@@ -0,0 +1,19 @@ | |||||
{ | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "esnext", | |||||
"lib": ["dom", "esnext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./src", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"jsx": "react", | |||||
"esModuleInterop": true | |||||
} | |||||
} |