├── public ├── demo.gif ├── demo.png ├── keys.png ├── logo.png ├── piano.gif ├── favicon.ico ├── keycode.png ├── transfer.png ├── samples │ └── piano │ │ ├── a48.mp3 │ │ ├── a49.mp3 │ │ ├── a50.mp3 │ │ ├── a51.mp3 │ │ ├── a52.mp3 │ │ ├── a53.mp3 │ │ ├── a54.mp3 │ │ ├── a55.mp3 │ │ ├── a56.mp3 │ │ ├── a57.mp3 │ │ ├── a65.mp3 │ │ ├── a66.mp3 │ │ ├── a67.mp3 │ │ ├── a68.mp3 │ │ ├── a69.mp3 │ │ ├── a70.mp3 │ │ ├── a71.mp3 │ │ ├── a72.mp3 │ │ ├── a73.mp3 │ │ ├── a74.mp3 │ │ ├── a75.mp3 │ │ ├── a76.mp3 │ │ ├── a77.mp3 │ │ ├── a78.mp3 │ │ ├── a79.mp3 │ │ ├── a80.mp3 │ │ ├── a81.mp3 │ │ ├── a82.mp3 │ │ ├── a83.mp3 │ │ ├── a84.mp3 │ │ ├── a85.mp3 │ │ ├── a86.mp3 │ │ ├── a87.mp3 │ │ ├── a88.mp3 │ │ ├── a89.mp3 │ │ ├── a90.mp3 │ │ ├── b49.mp3 │ │ ├── b50.mp3 │ │ ├── b52.mp3 │ │ ├── b53.mp3 │ │ ├── b54.mp3 │ │ ├── b56.mp3 │ │ ├── b57.mp3 │ │ ├── b66.mp3 │ │ ├── b67.mp3 │ │ ├── b68.mp3 │ │ ├── b69.mp3 │ │ ├── b71.mp3 │ │ ├── b72.mp3 │ │ ├── b73.mp3 │ │ ├── b74.mp3 │ │ ├── b76.mp3 │ │ ├── b79.mp3 │ │ ├── b80.mp3 │ │ ├── b81.mp3 │ │ ├── b83.mp3 │ │ ├── b84.mp3 │ │ ├── b86.mp3 │ │ ├── b87.mp3 │ │ ├── b89.mp3 │ │ └── b90.mp3 ├── manifest.json └── index.html ├── src ├── assets │ └── index.css ├── elements │ ├── app-piano │ │ ├── songs │ │ │ ├── ndda.js │ │ │ ├── later.js │ │ │ ├── fuji.js │ │ │ ├── xxy.js │ │ │ ├── test.js │ │ │ ├── pgydyd.js │ │ │ └── moon.js │ │ ├── notes.js │ │ ├── pianoKeys.js │ │ ├── index.eno │ │ └── index.js │ ├── app │ │ ├── index.js │ │ ├── _index.less │ │ └── logo.svg │ ├── app-footer │ │ ├── keys.js │ │ ├── index.eno │ │ └── index.js │ └── app-header │ │ ├── index.eno │ │ └── index.js └── index.js ├── .github └── FUNDING.yml ├── config ├── jest │ ├── fileTransform.js │ └── cssTransform.js ├── entry.js ├── polyfills.js ├── paths.js ├── env.js ├── webpackDevServer.config.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .gitignore ├── scripts ├── test.js ├── start.js └── build.js ├── .eslintrc ├── package.json ├── introduce.md └── README.md /public/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/demo.gif -------------------------------------------------------------------------------- /public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/demo.png -------------------------------------------------------------------------------- /public/keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/keys.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/logo.png -------------------------------------------------------------------------------- /public/piano.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/piano.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/favicon.ico -------------------------------------------------------------------------------- /public/keycode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/keycode.png -------------------------------------------------------------------------------- /public/transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/transfer.png -------------------------------------------------------------------------------- /src/assets/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /public/samples/piano/a48.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a48.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a49.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a49.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a50.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a50.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a51.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a51.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a52.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a52.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a53.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a53.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a54.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a54.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a55.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a55.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a56.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a56.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a57.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a57.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a65.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a65.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a66.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a66.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a67.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a67.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a68.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a68.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a69.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a69.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a70.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a70.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a71.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a71.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a72.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a72.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a73.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a73.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a74.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a74.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a75.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a75.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a76.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a76.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a77.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a77.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a78.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a78.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a79.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a79.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a80.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a80.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a81.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a81.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a82.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a82.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a83.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a83.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a84.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a84.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a85.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a85.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a86.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a86.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a87.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a87.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a88.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a88.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a89.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a89.mp3 -------------------------------------------------------------------------------- /public/samples/piano/a90.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/a90.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b49.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b49.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b50.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b50.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b52.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b52.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b53.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b53.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b54.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b54.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b56.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b56.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b57.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b57.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b66.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b66.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b67.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b67.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b68.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b68.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b69.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b69.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b71.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b71.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b72.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b72.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b73.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b73.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b74.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b74.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b76.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b76.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b79.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b79.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b80.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b80.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b81.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b81.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b83.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b83.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b84.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b84.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b86.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b86.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b87.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b87.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b89.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b89.mp3 -------------------------------------------------------------------------------- /public/samples/piano/b90.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wscats/piano/master/public/samples/piano/b90.mp3 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: piano # Replace with a single Open Collective username 4 | custom: ['https://gitee.com/wscats/piano#license'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 5 | -------------------------------------------------------------------------------- /src/elements/app-piano/songs/ndda.js: -------------------------------------------------------------------------------- 1 | // 你的答案 2 | const song = [ 3 | // 也许世界就这样我也还在路上 4 | '1', '1', '1', '2', '1', '1', '1.', '1', '1', '1', '1', '2', '3', '3.', 5 | '5', '3', '3', '4', '3', '2.', '1', '1', '1', '1', '2', '1', '1', '1.', 6 | '-5', '1', '1', '1', '2', '3', '3.' 7 | ] 8 | 9 | export default [...song] -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Omi App", 3 | "name": "Omi Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | # /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /src/elements/app/index.js: -------------------------------------------------------------------------------- 1 | import { define, WeElement } from 'omi' 2 | import '../app-header' 3 | import '../app-footer' 4 | import '../app-piano' 5 | 6 | define('my-app', class extends WeElement { 7 | render() { 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ) 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/elements/app-piano/songs/later.js: -------------------------------------------------------------------------------- 1 | // -1 -2 -3 -4 -5 -6 -7 2 | // C3 D3 E3 F3 G3 A3 B3 3 | 4 | // 1 2 3 4 5 6 7 5 | // C4 D4 E4 F4 G4 A4 B4 6 | 7 | // +1 +2 +3 +4 +5 +6 +7 8 | // C5 D5 E5 F5 G5 A5 B5 9 | 10 | const song = [ 11 | '3', '2', '1.', '1', '1.', '2', '3', '4', '3.', '4', '2', '1', '2', '2', '-7', '-7', '1', '1', '-6', '-6', '-7', '1', '1', '2' 12 | ] 13 | 14 | export default [...song] 15 | -------------------------------------------------------------------------------- /src/elements/app/_index.less: -------------------------------------------------------------------------------- 1 | @bg-color: #222; 2 | 3 | .app { 4 | text-align: center; 5 | } 6 | 7 | .app-logo { 8 | animation: app-logo-spin infinite 20s linear; 9 | height: 80px; 10 | } 11 | 12 | .app-header { 13 | background-color: @bg-color; 14 | height: 150px; 15 | padding: 20px; 16 | color: white; 17 | } 18 | 19 | .app-title { 20 | font-size: 1.5em; 21 | } 22 | 23 | .app-logo { 24 | cursor: pointer; 25 | } 26 | 27 | @keyframes app-logo-spin { 28 | from { 29 | transform: rotate(0deg); 30 | } 31 | to { 32 | transform: rotate(360deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/entry.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'), 2 | fileList = []; 3 | 4 | 5 | if (typeof String.prototype.endsWith != 'function') { 6 | String.prototype.endsWith = function (suffix) { 7 | return this.indexOf(suffix, this.length - suffix.length) !== -1; 8 | }; 9 | } 10 | 11 | function walk(path) { 12 | let dirList = fs.readdirSync(path); 13 | dirList.forEach(function (item) { 14 | if (!fs.statSync(path + '/' + item).isDirectory()) { 15 | if (item.endsWith('\.js')) { 16 | fileList.push(item.substr(0, item.length - 3)); 17 | } 18 | } else { 19 | //walk(path + '/' + item); 20 | } 21 | }); 22 | } 23 | 24 | walk('./src'); 25 | 26 | 27 | 28 | module.exports = fileList; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { render } from 'omi' 2 | import './assets/index.css' 3 | import './elements/app' 4 | 5 | 6 | render(, '#root', { 7 | data: { 8 | count: 0, 9 | song: [] 10 | }, 11 | sub() { 12 | this.data.count-- 13 | }, 14 | add() { 15 | this.data.count++ 16 | }, 17 | setSong(song) { 18 | // 构建新的数组,给它下标值来做索引 19 | let melody = []; 20 | song.map((item, index) => { 21 | melody.push({ 22 | ...item, 23 | index 24 | }) 25 | }) 26 | // 处理成每30个音符一个数组,自动播放时候自动显示按键 27 | for (let j = 0; j < melody.length; j += 30) { 28 | this.data.song.push(melody.slice(j, j + 30)) 29 | } 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | let argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /src/elements/app/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["prettier"], 4 | "plugins": ["prettier"], 5 | "env": { 6 | "browser": true, 7 | "mocha": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "modules": true, 14 | "jsx": true 15 | } 16 | }, 17 | "globals": { 18 | "sinon": true, 19 | "expect": true 20 | }, 21 | "rules": { 22 | "prettier/prettier": "error", 23 | "no-cond-assign": 1, 24 | "no-empty": 0, 25 | "no-console": 1, 26 | "semi": [1, "never"], 27 | "camelcase": 0, 28 | "comma-style": 2, 29 | "comma-dangle": [2, "never"], 30 | "indent": ["error", 2], 31 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 32 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 33 | "max-nested-callbacks": [2, 3], 34 | "no-eval": 2, 35 | "no-implied-eval": 2, 36 | "no-new-func": 2, 37 | "guard-for-in": 0, 38 | "eqeqeq": 0, 39 | "no-else-return": 2, 40 | "no-redeclare": 2, 41 | "no-dupe-keys": 2, 42 | "radix": 2, 43 | "strict": [2, "never"], 44 | "no-shadow": 0, 45 | "callback-return": [1, ["callback", "cb", "next", "done"]], 46 | "no-delete-var": 2, 47 | "no-undef-init": 2, 48 | "no-shadow-restricted-names": 2, 49 | "handle-callback-err": 0, 50 | "no-lonely-if": 2, 51 | "keyword-spacing": 2, 52 | "constructor-super": 2, 53 | "no-this-before-super": 2, 54 | "no-dupe-class-members": 2, 55 | "no-const-assign": 2, 56 | "prefer-spread": 2, 57 | "no-useless-concat": 2, 58 | "no-var": 2, 59 | "object-shorthand": 2, 60 | "prefer-arrow-callback": 2, 61 | "quotes": [1, "single"] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Piano 23 | 24 | 25 | 28 |
29 | 39 | 40 | -------------------------------------------------------------------------------- /src/elements/app-footer/keys.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // A 3 | A2: "Shift+6", 4 | A3: "Ctrl+6", 5 | A4: "6", 6 | A5: "Option+6", 7 | A6: "Command+6", 8 | 'A#3': "Ctrl+t", 9 | 'A#4': "t", 10 | 'A#5': "Option+t", 11 | 'A#6': "Command+t", 12 | // B 13 | B2: "Shift+7", 14 | B3: "Ctrl+7", 15 | B4: "7", 16 | B5: "Option+7", 17 | B6: "Command+7", 18 | // C 19 | C2: "Shift+1", 20 | C3: "Ctrl+1", 21 | C4: "1", 22 | C5: "Option+1", 23 | C6: "Command+1", 24 | C7: "", 25 | 'C#2': "Shift+q", 26 | 'C#3': "Ctrl+q", 27 | 'C#4': "q", 28 | 'C#5': "Option+q", 29 | 'C#6': "Command+q", 30 | // D 31 | D2: "Shift+2", 32 | D3: "Ctrl+2", 33 | D4: "2", 34 | D5: "Option+2", 35 | D6: "Command+2", 36 | 'D#2': "Shift+w", 37 | 'D#3': "Ctrl+w", 38 | 'D#4': "w", 39 | 'D#5': "Option+w", 40 | 'D#6': "Command+w", 41 | // E 42 | E2: "Shift+3", 43 | E3: "Ctrl+3", 44 | E4: "3", 45 | E5: "Option+3", 46 | E6: "Command+3", 47 | 'E#2': "Shift+e", 48 | 'E#3': "Ctrl+e", 49 | 'E#4': "e", 50 | 'E#5': "Option+e", 51 | 'E#6': "Command+e", 52 | // F 53 | F2: "Shift+4", 54 | F3: "Ctrl+4", 55 | F4: "4", 56 | F5: "Option+4", 57 | F6: "Command+4", 58 | 'E#2': "Shift+e", 59 | 'E#3': "Ctrl+e", 60 | 'E#4': "e", 61 | 'E#5': "Option+e", 62 | 'E#6': "Command+e", 63 | // G 64 | G2: "Shift+5", 65 | G3: "Ctrl+5", 66 | G4: "5", 67 | G5: "Option+5", 68 | G6: "Command+5", 69 | 'G#2': "./samples/piano/b53.mp3", 70 | 'G#3': "./samples/piano/b87.mp3", 71 | 'G#4': "./samples/piano/b79.mp3", 72 | 'G#5': "./samples/piano/b72.mp3", 73 | 'G#6': "./samples/piano/b86.mp3" 74 | } -------------------------------------------------------------------------------- /src/elements/app-piano/songs/fuji.js: -------------------------------------------------------------------------------- 1 | // -1 -2 -3 -4 -5 -6 -7 2 | // C3 D3 E3 F3 G3 A3 B3 3 | 4 | // 1 2 3 4 5 6 7 5 | // C4 D4 E4 F4 G4 A4 B4 6 | 7 | // +1 +2 +3 +4 +5 +6 +7 8 | // C5 D5 E5 F5 G5 A5 B5 9 | 10 | const song = [ 11 | '-5', '-6', '1', '2', '1', '2', '3', 12 | '3', '3', '2', '1', '2', '3', 13 | '3', '3', '2', '1', '2.', '1', '1', '-6', '1.', '2', '2', '3.', 14 | '-5', '-6', '1', '2', '1', '2', '1', '1', '5', 15 | '5', '3', '2', '3', '2', '1', '1', '3', 16 | // 今天想着你回家 17 | '3', '2', '1', '2.', '2', '2.', '1', '1', '-6', '2.', 18 | // 原谅我不再送花 19 | '-5', '-6', '1', '2', '1', '2', '3', '3', '5', 20 | // 伤口应要结疤 21 | '5', '3', '2', '1', '2', '3', '3', '6', 22 | // 花 23 | '6', '3', '2', '1', '2.', '1', '1', '6', '1.', '2', '2', '3', '5.', 24 | // 如若你非我不嫁 25 | '-5', '-6', '1', '2', '1', '2', '1', '1', 26 | // 彼此 27 | '6', '6', '5', '3', '3', '2', '1', 28 | // 一生一世 29 | '1', '3', '3', '3', '2', '1', '2.', '2', '2.', '1', '1', '-6', '1', 30 | // 谁都只得 31 | '1', '+1', '7', '+1', '6', '+1.', '6', '6', '5', '6', '5', '3', '2', '3', '5', '6', '5', 32 | // 要拥有 33 | '5', '5', '6', '5', '6.', '6', '6.', '5', '5', '6', '5', '3', '3', 34 | // 曾沿着 35 | '1', '2', '3', '5', '3', '2', '1', '1', '1', '2', '3', '6', '3', '2', '1', 36 | // 谁能凭爱意要富士山私有 37 | '1', '1', '+1.', '+1', '+1.', '6', '6', '+1', '6.', '5', 38 | // 何不把 39 | '5', '1', '+1', '7', '+1', '7', '6', '5', '6', '5', '3', '2', '3', '5', '6', '5', 40 | // 试管里找不到 41 | '5', '5', '6', '5', '6.', '6', '6.', '5', '5', '6', '5', '3', '3', 42 | // 前尘 43 | '1', '2', '3', '5', '3', '2', '1', '1', '1', '2', '3', '6', '3', '2', '1', '+1..', 44 | // 我绝不 45 | '+1', '6', '+1', '6', '5', '5', '5', '6', '5', '3', '2', '3', '2', '2', '1', '-6', '1', '1', '2', 46 | // 你好嫌不够 47 | '1', '1', '+1', '6', '+1', '+2', '+1', '+1', '+1', '+2', '+1', '6', '+1', '+3', '+2', '+2', '+1', '6', '+1', '+2', '1' 48 | ] 49 | 50 | export default [...song, ...song] 51 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 62 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebookincubator/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { 24 | choosePort, 25 | createCompiler, 26 | prepareProxy, 27 | prepareUrls, 28 | } = require('react-dev-utils/WebpackDevServerUtils'); 29 | const openBrowser = require('react-dev-utils/openBrowser'); 30 | const paths = require('../config/paths'); 31 | const config = require('../config/webpack.config.dev'); 32 | const createDevServerConfig = require('../config/webpackDevServer.config'); 33 | 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | const isInteractive = process.stdout.isTTY; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // Tools like Cloud9 rely on this. 43 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 44 | const HOST = process.env.HOST || '0.0.0.0'; 45 | 46 | if (process.env.HOST) { 47 | console.log( 48 | chalk.cyan( 49 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 50 | chalk.bold(process.env.HOST) 51 | )}` 52 | ) 53 | ); 54 | console.log( 55 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 56 | ); 57 | console.log(`Learn more here: ${chalk.yellow('http://bit.ly/2mwWSwH')}`); 58 | console.log(); 59 | } 60 | 61 | // We attempt to use the default port but if it is busy, we offer the user to 62 | // run on a different port. `choosePort()` Promise resolves to the next free port. 63 | choosePort(HOST, DEFAULT_PORT) 64 | .then(port => { 65 | if (port == null) { 66 | // We have not found a port. 67 | return; 68 | } 69 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 70 | const appName = require(paths.appPackageJson).name; 71 | const urls = prepareUrls(protocol, HOST, port); 72 | // Create a webpack compiler that is configured with custom messages. 73 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 74 | // Load proxy config 75 | const proxySetting = require(paths.appPackageJson).proxy; 76 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 77 | // Serve webpack assets generated by the compiler over a web sever. 78 | const serverConfig = createDevServerConfig( 79 | proxyConfig, 80 | urls.lanUrlForConfig 81 | ); 82 | const devServer = new WebpackDevServer(compiler, serverConfig); 83 | // Launch WebpackDevServer. 84 | devServer.listen(port, HOST, err => { 85 | if (err) { 86 | return console.log(err); 87 | } 88 | if (isInteractive) { 89 | clearConsole(); 90 | } 91 | console.log(chalk.cyan('Starting the development server...\n')); 92 | openBrowser(urls.localUrlForBrowser); 93 | }); 94 | 95 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 96 | process.on(sig, function() { 97 | devServer.close(); 98 | process.exit(); 99 | }); 100 | }); 101 | }) 102 | .catch(err => { 103 | if (err && err.message) { 104 | console.log(err.message); 105 | } 106 | process.exit(1); 107 | }); 108 | -------------------------------------------------------------------------------- /src/elements/app-header/index.eno: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /src/elements/app-footer/index.js: -------------------------------------------------------------------------------- 1 | import { WeElement, define, h } from "omi"; 2 | import moon from "../app-piano/songs/moon.js"; 3 | import keys from "./keys.js"; 4 | 5 | class AppFooter extends WeElement { 6 | constructor(...args) { 7 | super(...args); 8 | 9 | this.setSong = song => this.store.setSong(song); 10 | } 11 | 12 | render(props) { 13 | return h( 14 | "div", 15 | { 16 | class: "app-footer" 17 | }, 18 | h("hr", { 19 | class: "mt-5" 20 | }), 21 | h( 22 | "div", 23 | { 24 | class: "row mt-5" 25 | }, 26 | h( 27 | "div", 28 | { 29 | class: "col" 30 | }, 31 | h( 32 | "div", 33 | { 34 | class: "text-center" 35 | }, 36 | h( 37 | "p", 38 | { 39 | class: "mt-4" 40 | }, 41 | "You can click on the keyboard and play the melody that belongs to you. Here is an example of a piano piece:" 42 | ), 43 | h( 44 | "p", 45 | null, 46 | "\u4F60\u53EF\u4EE5\u70B9\u51FB\u952E\u76D8\u4F9D\u987A\u5E8F\u6309\u4EE5\u4E0B\u952E\uFF0C\u63A7\u5236\u597D\u8282\u594F\u6F14\u594F\u5C5E\u4E8E\u4F60\u7684\u65CB\u5F8B\uFF0C\u4E0B\u9762\u662F\u4E00\u9996\u94A2\u7434\u66F2\u7684\u4F8B\u5B50:" 47 | ), 48 | h( 49 | "p", 50 | { 51 | class: "mt-4" 52 | }, 53 | "Enjoy it!" 54 | ), 55 | this.store.data.song.map(item => { 56 | if (item[0].note) { 57 | return h( 58 | "p", 59 | { 60 | class: "mt-3 code" 61 | }, 62 | h( 63 | "code", 64 | { 65 | class: "p-2 text-dark" 66 | }, 67 | item.map(item2 => { 68 | if (item2.note) { 69 | return h( 70 | "span", 71 | { 72 | style: { 73 | color: 74 | this.store.data.count === item2.index 75 | ? "red" 76 | : "black" 77 | } 78 | }, 79 | this.data.keys[item2.note], 80 | "," 81 | ); 82 | } 83 | }) 84 | ) 85 | ); 86 | } 87 | }) 88 | ) 89 | ) 90 | ), 91 | h( 92 | "div", 93 | { 94 | class: "bg-yellow mt-5 py-5" 95 | }, 96 | h( 97 | "div", 98 | { 99 | class: "container" 100 | }, 101 | h( 102 | "div", 103 | { 104 | class: "text-center text-secondary" 105 | }, 106 | "Made with ", 107 | h( 108 | "span", 109 | { 110 | role: "img", 111 | "aria-label": "keyboard emoji" 112 | }, 113 | "\uD83C\uDFB5" 114 | ), 115 | "by ", 116 | h( 117 | "a", 118 | { 119 | class: "text-secondary", 120 | href: "https://github.com/Wscats" 121 | }, 122 | h("strong", null, "@Eno Yao") 123 | ) 124 | ) 125 | ) 126 | ) 127 | ); 128 | } 129 | 130 | install() { 131 | this.data = { 132 | title: "omi", 133 | song: [], 134 | keys 135 | }; 136 | this.setSong(moon); 137 | } 138 | } 139 | 140 | AppFooter.css = `hr{margin:0 50px;border:0;border-top-color:currentcolor;border-top-style:none;border-top-width:0px;border-top:1px solid rgba(0,0,0,0.1)}hr{box-sizing:content-box;height:0;overflow:visible}.bg-yellow{background-color:#f8e8d5}.container{line-height:50px;height:50px;width:100%;margin-right:auto;margin-left:auto}.text-secondary{color:#6c757d !important}.text-center{text-align:center !important}.code{padding:0 250px}@media screen and (max-width: 1000px){.code{padding:0 20px}}code{overflow:hidden;background-color:#ececec;width:100%;display:block;padding:10px 0}`; 141 | AppFooter.use = [ 142 | { 143 | count: "count", 144 | song: "song" 145 | } 146 | ]; 147 | define("app-footer", AppFooter); 148 | -------------------------------------------------------------------------------- /src/elements/app-header/index.js: -------------------------------------------------------------------------------- 1 | import { WeElement, define, h } from "omi"; 2 | 3 | class AppHeader extends WeElement { 4 | render(props) { 5 | return h( 6 | "div", 7 | null, 8 | h( 9 | "div", 10 | { 11 | style: "background: rgb(51, 51, 51) none repeat scroll 0% 0%;" 12 | }, 13 | h( 14 | "div", 15 | { 16 | class: "container" 17 | }, 18 | h( 19 | "div", 20 | { 21 | class: "text-sm-center text-white py-5" 22 | }, 23 | h("h1", null, "Omi Piano"), 24 | h( 25 | "p", 26 | null, 27 | "An interactive piano keyboard for omi. Supports custom sounds,", 28 | h("br", { 29 | class: "d-none d-sm-block" 30 | }), 31 | "touch/click/keyboard events, and fully auto play song." 32 | ), 33 | h( 34 | "div", 35 | { 36 | class: "mt-4" 37 | }, 38 | h( 39 | "a", 40 | { 41 | class: "btn btn-outline-light btn-lg", 42 | href: "https://github.com/Wscats/piano" 43 | }, 44 | "View docs on Github" 45 | ), 46 | h( 47 | "a", 48 | { 49 | class: "btn btn-outline-light btn-lg", 50 | href: "https://github.com/Wscats/piano" 51 | }, 52 | "\u67E5\u770B\u8BE5\u9879\u76EEGithub\u5730\u5740" 53 | ) 54 | ) 55 | ) 56 | ) 57 | ), 58 | h( 59 | "div", 60 | { 61 | class: "text-center" 62 | }, 63 | h( 64 | "p", 65 | { 66 | class: "" 67 | }, 68 | "Try it by clicking, tapping, or using your keyboard 1 to 9:" 69 | ), 70 | h( 71 | "p", 72 | { 73 | class: "" 74 | }, 75 | "\u9F20\u6807\u70B9\u51FB\u94A2\u7434\u952E\u6216\u8005\u952E\u76D8\u6309\u6570\u5B57\u952E 1 ~ 9:" 76 | ), 77 | h( 78 | "div", 79 | { 80 | style: "color: rgb(119, 119, 119);" 81 | }, 82 | h( 83 | "svg", 84 | { 85 | fill: "currentColor", 86 | preserveAspectRatio: "xMidYMid meet", 87 | height: "32", 88 | width: "32", 89 | viewBox: "0 0 40 40", 90 | style: "vertical-align: middle;" 91 | }, 92 | h( 93 | "g", 94 | null, 95 | h("path", { 96 | d: 97 | "m33.4 20l-13.4 13.4-13.4-13.4 2.5-2.3 9.3 9.3v-20.4h3.2v20.4l9.4-9.3z" 98 | }) 99 | ) 100 | ) 101 | ) 102 | ) 103 | ); 104 | } 105 | } 106 | 107 | AppHeader.css = ` 108 | * { 109 | margin: 0; 110 | padding: 0; 111 | } 112 | 113 | .container { 114 | width: 100%; 115 | margin-right: auto; 116 | margin-left: auto; 117 | 118 | } 119 | 120 | .text-white { 121 | color: #fff !important; 122 | 123 | } 124 | 125 | .text-sm-center { 126 | text-align: center !important; 127 | } 128 | 129 | .mt-4, 130 | .my-4 { 131 | margin-top: 1.5rem !important; 132 | } 133 | 134 | .pb-5, 135 | .py-5 { 136 | padding-bottom: 3rem !important; 137 | } 138 | 139 | .pt-5, 140 | .py-5 { 141 | padding-top: 3rem !important; 142 | } 143 | 144 | .btn:not(:disabled):not(.disabled) { 145 | 146 | cursor: pointer; 147 | 148 | } 149 | 150 | .btn-group-lg>.btn, 151 | .btn-lg { 152 | 153 | padding: 5px 10px; 154 | font-size: 12px; 155 | line-height: 15px; 156 | border-radius: 3px; 157 | 158 | } 159 | 160 | .btn-outline-light { 161 | 162 | color: #f8f9fa; 163 | background-color: transparent; 164 | background-image: none; 165 | border-color: #f8f9fa; 166 | 167 | } 168 | 169 | .btn { 170 | display: inline-block; 171 | font-weight: 400; 172 | text-align: center; 173 | white-space: nowrap; 174 | vertical-align: middle; 175 | -webkit-user-select: none; 176 | -moz-user-select: none; 177 | -ms-user-select: none; 178 | user-select: none; 179 | border: 1px solid transparent; 180 | border-top-color: transparent; 181 | border-right-color: transparent; 182 | border-bottom-color: transparent; 183 | border-left-color: transparent; 184 | padding: 15px 7.5px; 185 | font-size: 15px; 186 | line-height: 15px; 187 | border: 1px solid #fff; 188 | border-radius: 5px; 189 | transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out; 190 | } 191 | 192 | a { 193 | margin: 0 10px; 194 | text-decoration: none; 195 | } 196 | 197 | .text-center { 198 | margin-top: 12px; 199 | text-align: center !important; 200 | } 201 | 202 | p { 203 | margin: 0 10px; 204 | } 205 | `; 206 | define("app-header", AppHeader); 207 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omi-piano", 3 | "version": "0.2.0", 4 | "private": false, 5 | "keywords": [ 6 | "piano", 7 | "music", 8 | "mp3", 9 | "omi", 10 | "omijs", 11 | "omi.js", 12 | "eno", 13 | "eno yao", 14 | "yao", 15 | "wscats", 16 | "yjl", 17 | "css", 18 | "scss", 19 | "sass", 20 | "lemon", 21 | "laoyao", 22 | "one", 23 | "jing", 24 | "jialong", 25 | "yaojialong", 26 | "xie", 27 | "DK", 28 | "lan", 29 | "wang", 30 | "sha", 31 | "react", 32 | "jsx", 33 | "sfc", 34 | "component", 35 | "vue", 36 | "single file component", 37 | "老姚", 38 | "姚", 39 | "一一", 40 | "htm", 41 | "html", 42 | "reky" 43 | ], 44 | "dependencies": { 45 | "@babel/cli": "^7.0.0", 46 | "@babel/core": "^7.2.2", 47 | "@babel/plugin-proposal-class-properties": "^7.2.3", 48 | "@babel/plugin-proposal-decorators": "^7.2.3", 49 | "@babel/plugin-proposal-function-bind": "^7.2.0", 50 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0", 51 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 52 | "@babel/plugin-transform-runtime": "^7.2.0", 53 | "@babel/preset-env": "^7.2.3", 54 | "@babel/preset-react": "^7.0.0", 55 | "@babel/runtime": "^7.3.1", 56 | "autoprefixer": "7.1.6", 57 | "babel-core": "^7.0.0-bridge.0", 58 | "babel-eslint": "7.2.3", 59 | "babel-jest": "20.0.3", 60 | "babel-loader": "^8.0.5", 61 | "case-sensitive-paths-webpack-plugin": "2.1.1", 62 | "chalk": "1.1.3", 63 | "css": "^2.2.4", 64 | "css-loader": "^1.0.1", 65 | "dotenv": "4.0.0", 66 | "dotenv-expand": "4.2.0", 67 | "eslint": "^4.18.2", 68 | "eslint-config-prettier": "^3.1.0", 69 | "eslint-loader": "1.9.0", 70 | "eslint-plugin-flowtype": "2.39.1", 71 | "eslint-plugin-import": "2.8.0", 72 | "eslint-plugin-jsx-a11y": "5.1.1", 73 | "eslint-plugin-prettier": "^3.0.0", 74 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 75 | "file": "^0.2.2", 76 | "file-loader": "^2.0.0", 77 | "fs-extra": "3.0.1", 78 | "html-webpack-plugin": "^4.0.0-beta.5", 79 | "jest": "20.0.4", 80 | "less": "^3.9.0", 81 | "less-loader": "^4.1.0", 82 | "mini-css-extract-plugin": "^0.5.0", 83 | "object-assign": "4.1.1", 84 | "omi": "latest", 85 | "omi-router": "latest", 86 | "omil": "latest", 87 | "omio": "latest", 88 | "omiu": "latest", 89 | "opencollective-postinstall": "^2.0.2", 90 | "optimize-css-assets-webpack-plugin": "^5.0.1", 91 | "postcss-flexbugs-fixes": "3.2.0", 92 | "postcss-loader": "2.0.8", 93 | "prettier": "^1.14.3", 94 | "promise": "8.0.1", 95 | "raf": "3.4.0", 96 | "react-dev-utils": "^7.0.1", 97 | "reomi": "latest", 98 | "resolve": "1.6.0", 99 | "style-loader": "0.19.0", 100 | "sw-precache-webpack-plugin": "^0.11.5", 101 | "to-string-loader": "^1.1.5", 102 | "url": "^0.11.0", 103 | "url-loader": "^1.1.2", 104 | "webpack": "^4.28.3", 105 | "webpack-cli": "^3.2.0", 106 | "webpack-dev-server": "^3.1.10", 107 | "webpack-manifest-plugin": "^2.0.4", 108 | "webpack-merge": "^4.2.1", 109 | "whatwg-fetch": "2.0.3" 110 | }, 111 | "scripts": { 112 | "start": "node scripts/start.js", 113 | "build": "PUBLIC_URL=. node scripts/build.js", 114 | "build-windows": "set PUBLIC_URL=.&& node scripts/build.js", 115 | "fix": "eslint src --fix", 116 | "postinstall": "opencollective-postinstall || true" 117 | }, 118 | "jest": { 119 | "collectCoverageFrom": [ 120 | "src/**/*.{js,jsx,mjs}" 121 | ], 122 | "setupFiles": [ 123 | "/config/polyfills.js" 124 | ], 125 | "testMatch": [ 126 | "/src/**/__tests__/**/*.{js,jsx,mjs}", 127 | "/src/**/?(*.)(spec|test).{js,jsx,mjs}" 128 | ], 129 | "testEnvironment": "node", 130 | "testURL": "http://localhost", 131 | "transform": { 132 | "^.+\\.(js|jsx|mjs)$": "/node_modules/babel-jest", 133 | "^.+\\.css$": "/config/jest/cssTransform.js", 134 | "^(?!.*\\.(js|jsx|mjs|css|json)$)": "/config/jest/fileTransform.js" 135 | }, 136 | "transformIgnorePatterns": [ 137 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$" 138 | ], 139 | "moduleNameMapper": { 140 | "^react-native$": "react-native-web" 141 | }, 142 | "moduleFileExtensions": [ 143 | "web.js", 144 | "js", 145 | "json", 146 | "web.jsx", 147 | "jsx", 148 | "node", 149 | "mjs" 150 | ] 151 | }, 152 | "babel": { 153 | "presets": [ 154 | "@babel/preset-env", 155 | [ 156 | "@babel/preset-react", 157 | { 158 | "pragma": "Omi.h" 159 | } 160 | ] 161 | ], 162 | "plugins": [ 163 | "@babel/plugin-proposal-class-properties", 164 | "@babel/transform-runtime", 165 | [ 166 | "@babel/plugin-proposal-decorators", 167 | { 168 | "legacy": true 169 | } 170 | ], 171 | "@babel/plugin-proposal-function-bind", 172 | "@babel/plugin-proposal-object-rest-spread", 173 | "@babel/plugin-syntax-dynamic-import" 174 | ] 175 | }, 176 | "prettier": { 177 | "singleQuote": true, 178 | "semi": false, 179 | "tabWidth": 2, 180 | "useTabs": false 181 | }, 182 | "alias": { 183 | "omi": "omio" 184 | }, 185 | "collective": { 186 | "type": "opencollective", 187 | "url": "https://opencollective.com/piano" 188 | } 189 | } -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = 30 | FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // First, read the current file sizes in build directory. 44 | // This lets us display how much they changed later. 45 | measureFileSizesBeforeBuild(paths.appBuild) 46 | .then(previousFileSizes => { 47 | // Remove all content but keep the directory so that 48 | // if you're in it, you don't end up in Trash 49 | fs.emptyDirSync(paths.appBuild); 50 | // Merge with the public folder 51 | copyPublicFolder(); 52 | // Start the webpack build 53 | return build(previousFileSizes); 54 | }) 55 | .then( 56 | ({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | console.log(chalk.yellow('Compiled with warnings.\n')); 59 | console.log(warnings.join('\n\n')); 60 | console.log( 61 | '\nSearch for the ' + 62 | chalk.underline(chalk.yellow('keywords')) + 63 | ' to learn more about each warning.' 64 | ); 65 | console.log( 66 | 'To ignore, add ' + 67 | chalk.cyan('// eslint-disable-next-line') + 68 | ' to the line before.\n' 69 | ); 70 | } else { 71 | console.log(chalk.green('Compiled successfully.\n')); 72 | } 73 | 74 | console.log('File sizes after gzip:\n'); 75 | printFileSizesAfterBuild( 76 | stats, 77 | previousFileSizes, 78 | paths.appBuild, 79 | WARN_AFTER_BUNDLE_GZIP_SIZE, 80 | WARN_AFTER_CHUNK_GZIP_SIZE 81 | ); 82 | console.log(); 83 | 84 | const appPackage = require(paths.appPackageJson); 85 | const publicUrl = paths.publicUrl; 86 | const publicPath = config.output.publicPath; 87 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 88 | printHostingInstructions( 89 | appPackage, 90 | publicUrl, 91 | publicPath, 92 | buildFolder, 93 | useYarn 94 | ); 95 | }, 96 | err => { 97 | console.log(chalk.red('Failed to compile.\n')); 98 | printBuildError(err); 99 | process.exit(1); 100 | } 101 | ); 102 | 103 | // Create the production build and print the deployment instructions. 104 | function build(previousFileSizes) { 105 | console.log('Creating an optimized production build...'); 106 | 107 | let compiler = webpack(config); 108 | return new Promise((resolve, reject) => { 109 | compiler.run((err, stats) => { 110 | if (err) { 111 | return reject(err); 112 | } 113 | const messages = formatWebpackMessages(stats.toJson({}, true)); 114 | if (messages.errors.length) { 115 | // Only keep the first error. Others are often indicative 116 | // of the same problem, but confuse the reader with noise. 117 | if (messages.errors.length > 1) { 118 | messages.errors.length = 1; 119 | } 120 | return reject(new Error(messages.errors.join('\n\n'))); 121 | } 122 | if ( 123 | process.env.CI && 124 | (typeof process.env.CI !== 'string' || 125 | process.env.CI.toLowerCase() !== 'false') && 126 | messages.warnings.length 127 | ) { 128 | console.log( 129 | chalk.yellow( 130 | '\nTreating warnings as errors because process.env.CI = true.\n' + 131 | 'Most CI servers set it automatically.\n' 132 | ) 133 | ); 134 | return reject(new Error(messages.warnings.join('\n\n'))); 135 | } 136 | return resolve({ 137 | stats, 138 | previousFileSizes, 139 | warnings: messages.warnings, 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | function copyPublicFolder() { 146 | fs.copySync(paths.appPublic, paths.appBuild, { 147 | dereference: true, 148 | filter: file => file !== paths.appHtml, 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /src/elements/app-piano/pianoKeys.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default[{ 4 | black: { name: "C#2", keyCode: 81 }, 5 | white: { name: "C2", keyCode: 49 }, 6 | }, 7 | { 8 | black: { name: "D#2", keyCode: 87 }, 9 | white: { name: "D2", keyCode: 50 }, 10 | }, 11 | { 12 | black: { name: null, keyCode: null }, 13 | white: { name: "E2", keyCode: 51 }, 14 | }, 15 | { 16 | black: { name: "F#2", keyCode: 69 }, 17 | white: { name: "F2", keyCode: 52 }, 18 | }, 19 | { 20 | black: { name: "G#2", keyCode: 82 }, 21 | white: { name: "G2", keyCode: 53 }, 22 | }, 23 | { 24 | black: { name: "A#2", keyCode: 84 }, 25 | white: { name: "A2", keyCode: 54 }, 26 | }, 27 | { 28 | black: { name: null, keyCode: null }, 29 | white: { name: "B2", keyCode: 55 }, 30 | }, 31 | { 32 | black: { name: "C#3", keyCode: 81 }, 33 | white: { name: "C3", keyCode: 49 }, 34 | }, 35 | { 36 | black: { name: "D#3", keyCode: 87 }, 37 | white: { name: "D3", keyCode: 50 }, 38 | }, 39 | { 40 | black: { name: null, keyCode: null }, 41 | white: { name: "E3", keyCode: 51 }, 42 | }, 43 | { 44 | black: { name: "F#3", keyCode: 69 }, 45 | white: { name: "F3", keyCode: 52 }, 46 | }, 47 | { 48 | black: { name: "G#3", keyCode: 82 }, 49 | white: { name: "G3", keyCode: 53 }, 50 | }, 51 | { 52 | black: { name: "A#3", keyCode: 84 }, 53 | white: { name: "A3", keyCode: 54 }, 54 | }, 55 | { 56 | black: { name: null, keyCode: null }, 57 | white: { name: "B3", keyCode: 55 }, 58 | }, 59 | { 60 | black: { name: "C#4", keyCode: 81 }, 61 | white: { name: "C4", keyCode: 49 }, 62 | }, 63 | { 64 | black: { name: "D#4", keyCode: 87 }, 65 | white: { name: "D4", keyCode: 50 }, 66 | }, 67 | { 68 | black: { name: null, keyCode: null }, 69 | white: { name: "E4", keyCode: 51 }, 70 | }, 71 | { 72 | black: { name: "F#4", keyCode: 69 }, 73 | white: { name: "F4", keyCode: 52 }, 74 | }, 75 | { 76 | black: { name: "G#4", keyCode: 82 }, 77 | white: { name: "G4", keyCode: 53 }, 78 | }, 79 | { 80 | black: { name: "A#4", keyCode: 84 }, 81 | white: { name: "A4", keyCode: 54 }, 82 | }, 83 | { 84 | black: { name: null, keyCode: null }, 85 | white: { name: "B4", keyCode: 55 }, 86 | }, 87 | { 88 | black: { name: "C#5", keyCode: 81 }, 89 | white: { name: "C5", keyCode: 49 }, 90 | }, 91 | { 92 | black: { name: "D#5", keyCode: 87 }, 93 | white: { name: "D5", keyCode: 50 }, 94 | }, 95 | { 96 | black: { name: null, keyCode: null }, 97 | white: { name: "E5", keyCode: 51 }, 98 | }, 99 | { 100 | black: { name: "F#5", keyCode: 69 }, 101 | white: { name: "F5", keyCode: 52 }, 102 | }, 103 | { 104 | black: { name: "G#5", keyCode: 82 }, 105 | white: { name: "G5", keyCode: 53 }, 106 | }, 107 | { 108 | black: { name: "A#5", keyCode: 84 }, 109 | white: { name: "A5", keyCode: 54 }, 110 | }, 111 | { 112 | black: { name: null, keyCode: null }, 113 | white: { name: "B5", keyCode: 55 }, 114 | }, 115 | { 116 | black: { name: "C#6", keyCode: 81 }, 117 | white: { name: "C6", keyCode: 49 }, 118 | }, 119 | { 120 | black: { name: "D#6", keyCode: 87 }, 121 | white: { name: "D6", keyCode: 50 }, 122 | }, 123 | { 124 | black: { name: null, keyCode: null }, 125 | white: { name: "E6", keyCode: 51 }, 126 | }, 127 | { 128 | black: { name: "F#6", keyCode: 69 }, 129 | white: { name: "F6", keyCode: 52 }, 130 | }, 131 | { 132 | black: { name: "G#6", keyCode: 82 }, 133 | white: { name: "G6", keyCode: 53 }, 134 | }, 135 | { 136 | black: { name: "A#6", keyCode: 84 }, 137 | white: { name: "A6", keyCode: 54 }, 138 | }, 139 | { 140 | black: { name: null, keyCode: null }, 141 | white: { name: "B6", keyCode: 55 } 142 | }] 143 | 144 | // 创建音符和键盘的映射关系表 用于生成上面的数组 145 | const createPianoKeys = () => { 146 | // 存放钢琴的按键顺序 147 | let pianoKeys = []; 148 | [2, 3, 4, 5, 6].map((item) => { 149 | pianoKeys = pianoKeys.concat([{ 150 | white: { 151 | name: `C${item}`, 152 | keyCode: 49 153 | }, 154 | black: { 155 | name: `C#${item}`, 156 | keyCode: 81 157 | } 158 | }, { 159 | white: { 160 | name: `D${item}`, 161 | keyCode: 50 162 | }, 163 | black: { 164 | name: `D#${item}`, 165 | keyCode: 87 166 | } 167 | }, { 168 | white: { 169 | name: `E${item}`, 170 | keyCode: 51 171 | }, 172 | black: { 173 | name: null, 174 | keyCode: null 175 | } 176 | }, { 177 | white: { 178 | name: `F${item}`, 179 | keyCode: 52 180 | }, 181 | black: { 182 | name: `F#${item}`, 183 | keyCode: 69 184 | } 185 | }, { 186 | white: { 187 | name: `G${item}`, 188 | keyCode: 53 189 | }, 190 | black: { 191 | name: `G#${item}`, 192 | keyCode: 82 193 | } 194 | }, { 195 | white: { 196 | name: `A${item}`, 197 | keyCode: 54 198 | }, 199 | black: { 200 | name: `A#${item}`, 201 | keyCode: 84 202 | } 203 | }, { 204 | white: { 205 | name: `B${item}`, 206 | keyCode: 55 207 | }, 208 | black: { 209 | name: null, 210 | keyCode: null 211 | } 212 | }]) 213 | }) 214 | return pianoKeys 215 | } -------------------------------------------------------------------------------- /config/webpackDevServer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware'); 4 | const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware'); 5 | const ignoredFiles = require('react-dev-utils/ignoredFiles'); 6 | const config = require('./webpack.config.dev'); 7 | const paths = require('./paths'); 8 | 9 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 10 | const host = process.env.HOST || '0.0.0.0'; 11 | 12 | // const fileList = require('./entry'); 13 | 14 | // let rewrites = []; 15 | 16 | // fileList.forEach(function (item) { 17 | 18 | // rewrites.push({ from: new RegExp('^\/' + item + '.html', 'g'), to: '/build/' + item + '.html' }) 19 | 20 | // }); 21 | 22 | 23 | 24 | module.exports = function(proxy, allowedHost) { 25 | return { 26 | // WebpackDevServer 2.4.3 introduced a security fix that prevents remote 27 | // websites from potentially accessing local content through DNS rebinding: 28 | // https://github.com/webpack/webpack-dev-server/issues/887 29 | // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a 30 | // However, it made several existing use cases such as development in cloud 31 | // environment or subdomains in development significantly more complicated: 32 | // https://github.com/facebookincubator/create-react-app/issues/2271 33 | // https://github.com/facebookincubator/create-react-app/issues/2233 34 | // While we're investigating better solutions, for now we will take a 35 | // compromise. Since our WDS configuration only serves files in the `public` 36 | // folder we won't consider accessing them a vulnerability. However, if you 37 | // use the `proxy` feature, it gets more dangerous because it can expose 38 | // remote code execution vulnerabilities in backends like Django and Rails. 39 | // So we will disable the host check normally, but enable it if you have 40 | // specified the `proxy` setting. Finally, we let you override it if you 41 | // really know what you're doing with a special environment variable. 42 | disableHostCheck: 43 | !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true', 44 | // Enable gzip compression of generated files. 45 | compress: true, 46 | // Silence WebpackDevServer's own logs since they're generally not useful. 47 | // It will still show compile warnings and errors with this setting. 48 | clientLogLevel: 'none', 49 | // By default WebpackDevServer serves physical files from current directory 50 | // in addition to all the virtual build products that it serves from memory. 51 | // This is confusing because those files won’t automatically be available in 52 | // production build folder unless we copy them. However, copying the whole 53 | // project directory is dangerous because we may expose sensitive files. 54 | // Instead, we establish a convention that only files in `public` directory 55 | // get served. Our build script will copy `public` into the `build` folder. 56 | // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%: 57 | // 58 | // In JavaScript code, you can access it with `process.env.PUBLIC_URL`. 59 | // Note that we only recommend to use `public` folder as an escape hatch 60 | // for files like `favicon.ico`, `manifest.json`, and libraries that are 61 | // for some reason broken when imported through Webpack. If you just want to 62 | // use an image, put it in `src` and `import` it from JavaScript instead. 63 | contentBase: paths.appPublic, 64 | // By default files from `contentBase` will not trigger a page reload. 65 | watchContentBase: true, 66 | // Enable hot reloading server. It will provide /sockjs-node/ endpoint 67 | // for the WebpackDevServer client so it can learn when the files were 68 | // updated. The WebpackDevServer client is included as an entry point 69 | // in the Webpack development configuration. Note that only changes 70 | // to CSS are currently hot reloaded. JS changes will refresh the browser. 71 | hot: true, 72 | // It is important to tell WebpackDevServer to use the same "root" path 73 | // as we specified in the config. In development, we always serve from /. 74 | publicPath: config.output.publicPath, 75 | // WebpackDevServer is noisy by default so we emit custom message instead 76 | // by listening to the compiler events with `compiler.plugin` calls above. 77 | quiet: true, 78 | // Reportedly, this avoids CPU overload on some systems. 79 | // https://github.com/facebookincubator/create-react-app/issues/293 80 | // src/node_modules is not ignored to support absolute imports 81 | // https://github.com/facebookincubator/create-react-app/issues/1065 82 | watchOptions: { 83 | ignored: ignoredFiles(paths.appSrc), 84 | }, 85 | // Enable HTTPS if the HTTPS environment variable is set to 'true' 86 | https: protocol === 'https', 87 | host: host, 88 | overlay: false, 89 | historyApiFallback: { 90 | // Paths with dots should still use the history fallback. 91 | // See https://github.com/facebookincubator/create-react-app/issues/387. 92 | disableDotRule: true 93 | //rewrites: rewrites 94 | }, 95 | public: allowedHost, 96 | proxy, 97 | before(app) { 98 | // This lets us open files from the runtime error overlay. 99 | app.use(errorOverlayMiddleware()); 100 | // This service worker file is effectively a 'no-op' that will reset any 101 | // previous service worker registered for the same host:port combination. 102 | // We do this in development to avoid hitting the production cache if 103 | // it used the same host and port. 104 | // https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 105 | app.use(noopServiceWorkerMiddleware()); 106 | }, 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /src/elements/app-piano/songs/pgydyd.js: -------------------------------------------------------------------------------- 1 | // const song = [ 2 | // '3', '4', 3 | // '5', '5', '5', '6', '7', '+1..', 4 | // '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..', 5 | // '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6', 6 | // '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2', 7 | // // 将愿望 8 | // '+2..', '3', '4', '5', 9 | // // 折飞机寄成信 10 | // '5', '5', '5', '6', '7', '+1..', 11 | // '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..', 12 | // '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6', 13 | // '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2..', 14 | // // 一起长大的约定 15 | // '3', '5', '+1', '+3', '+3.', '+4', '+2..', '+2', '+5', '7', '+1..', 16 | // '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..', 17 | // // 说好要一起旅行 18 | // '3', '5', '+1.', '+3', '+3.', '+4', '+2..', 19 | // // 是你如今 20 | // '+2', '+5', '7', '+1..', 21 | // // 唯一坚持的任性 22 | // '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1', 23 | // // '+2', '+1', '7', '+1', '+3', '+2', '+1', '+2', '+3', '+2', '+4', '+3', '+2', '+1', '7', '+1', '+2', '+1', '7', '+1', '+3', '+2', '+1', '+2', '+3', '+2', '+4', '+3', '+1..', 24 | // // 在走廊 25 | // '3', '4', 26 | // '5', '5', '5', '6', '7', '+1..', 27 | // '+1', '+1', '7', '+2', '6', '5', '5', '5', '+2', '+1', '+1', '+3', '+3..', 28 | // '+1', '+2', '+3', '+3', '+4', '+3', '+2', '+3', '+1', '+1', '6', '6', 29 | // '6', '7', '+1', '+2', '+2', '+1', '7', '6', '+4', '+2', 30 | 31 | // // 一起长大的约定 32 | // '3', '5', '+1', '+3', '+3.', '+4', '+2..', '+2', '+5', '7', '+1..', 33 | // '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..', 34 | // // 说好要一起旅行 35 | // '3', '5', '+1.', '+3', '+3.', '+4', '+2..', 36 | // // 是你如今 37 | // '+2', '+5', '7', '+1..', 38 | // // 唯一坚持的任性 39 | // '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1', 40 | 41 | 42 | // // 一起长大2 43 | // '+6', '+5', '+3', '+2', '+1', '+3.', '+4', '+2..', 44 | // '+6', '+5', '7', '+1..', 45 | // // 与你聊不完的曾经 46 | // '+3', '+4', '+5', '+1', '+1', '+2', '+3', '+3..', 47 | // // 而我已经分不清 48 | // '3', '5', '+1', '+3', '+3.', '+2', '+2', '+2..', '+2', '+5', '7', '+2', '+1', '+1', 49 | // // 还是错过的爱情 50 | // '+3', '+4', '+5', '+1', '+1', '+2.', '+1', '+1..' 51 | // ] 52 | // export default [...song] 53 | 54 | const song = [ 55 | '3', '5', '+1', '+3', '+3.6.3.-4', '1', '3', '+4', '+274-4', '-7', '2', 56 | '+25-3', '-7', '+52', '7', '+13--6', '-3', '-1', 57 | '+3+14-2', '+4-6', '+51', '+1', '+164--5', '-4', '+22-7', '+353-1', '-5', '1', '2', 58 | '3-#6-3-1', '5', '+1#41--#4', '+33', '+3.6.3.--4', '-1', '-6', '+4', '+274--4', '-2', '-7', 59 | '+27--3', '--7', '+5-5', '7', '+13--6', '-3', '1', 60 | '+3+14-2', '+4-6', '+51', '+1', '+164--5', '-4', '+272-7', '+1#54-1', '-#5', 61 | '2', '4', '31-5-1', '3', '4', '5', '5-1', '5-5', '53', '5', '--7', '6-5', '72', '+1', '+1--6', '-3', '1', 62 | '+12-3--5', '7', '+22-7', '6', '6--4', '5-1', '5-6', '+2', '--5', '+1-2', '+3-7', '+3', '41-5-1', 63 | '31', '-5', '+13-#6-1', '+2', '+3#61', '+3', '+3-3--3', '+3-#5', '+42-7', '+3-3', '+3--#5', '+23-3', '#52', '+3', 64 | '+2.5.1.--6', '-3', '+1.', '-6', '-3', '641--4', '7-3', '+1-6', '+2-3', '+2--4', '+1-1', '+1-6-4', '+6', '+6--2', '+36--6', '+36-4-2', '+2' 65 | ] 66 | 67 | const song2 = [ 68 | '+2--5', '-2', '1-5', '-7-5--5', '3', '4', '5---5', '5-1', '5-5', '52', '53', '5--7', '6-5', '7-7', '52', '+2', '+2--6', '+1-3', '1-5', '-3', '+12-3--5', '7', '+22-7', '6', '6--4', '5-1', 69 | '5-6', '+2--4', '+2--5', '+1-2', '+3-7', '+3-2', '+3-1', '-5', '31', '-5', '+1#63--1', '+2', '+3+11-#6-5', '+35-1', '+3--7---7', '+3-4', '+42-6', '+3---4', '---5', '+3--#5---#5', '+2-3', '2-7', '+3-3', 70 | '+2.5.1.--6---6', '-3', '+1.', '-6', '-3', '642--5', '7-2', '+15-6-4', '+2-2', '+2--2', '+1--6', '+14-4-2', '6--2', '--3', '--#4', '+3-2', '+362-4', '+2-2', 71 | '1-6-4.--5.', '1-6', '-4--5', '3-7-4--5', '5', '+14--5---5', '+3', '+3.6.--4', '-1', '-4', '+44-1', '+242--4', '-2', '-7', '-2', '+25--3', '--7', '+5-5-2', '7--7', '+13--6', '-3', '1', '-3', '+314--4', '+4-1', '+5+14-6', '+1--4', '+14-4--5', '-2', '+274-5', '--5', 72 | '+3+15--1', '-5', '2', '-5', '3-#6--1', '5', '+13---#4', '+3', '+3.6.--4', '-1', '-4', '+44-1', '+242--4', '-2', '-7', '-2', '+25--3', '--7', '+5-5-2', '7--7', '+13--6', '-3', '1', '-3', '+314--4', '+4-1', '+5+14-6', '+1--4', '+14-4--5', '-2', '+274-5', '--5', 73 | '+2-1--1', '2', '5-3', '+2', '+11-5', '3-3', '+1', '7-1--1', '2', '3-3', '7', '+11-5', '2', '3-3', '+1', '+2--6---6', '2', '5-3', '+2', '+11-5', '3', '5-3', '+1', '7--6---6', '2', '3-3', '+1', '+3--#4', '4', '6-2', '+3', '+22-#4', '4', '6-2', '+2', '+3--2', '4', '6--6', '+3', '+2-4-2', '4', '6--6', '+2', '+47--5', '4', '5-2', '+47', '+37-7-4', '4', '5-2', '+37', 74 | '+2-1--1', '2', '5-3', '+2', '+11-5', '3-3', '+1', '7-1--1', '2', '3-3', '7', '+11-5', '2', '3-3', '+1', '+2--6---6', '2', '5-3', '+2', '+11-5', '3', '5-3', '+1', '7--6---6', '2', '3-3', '+1', '+3--#4', '4', '6-2', '+3', '+22-#4', '4', '6-2', '+2', '+3--2', '4', '6--6', '+3', '+2-4-2', '4', '6--6', 75 | '--5---5', 76 | '51--1', '-1', '51', '-1', '51--1', '3', '4', '5', '+153-1--1', '-#6-5-3', '3-6-5-1--1', '53-6-1--1', '+6+3--#6--1', '+5', '+3--1', '+2-1', '+1--1', '+3.6.--4', '-1', '-4', '+44-1', '+242--4', '-2', '-7', '-2', '+6.', '+2.', '--3', '--7', '+5.7.', '-5-2', '7--7', '+13--6', '-3', '1', '-3', 77 | '+314', '+314--4', '+4-1', '+5+14-6', '+1--4', '+14-4--5', '-2', '+274-5', '--5', 78 | '+3+15--1', '-5', '2', '-5', '3-#6--1', '5', '+13---#4', '+3', '+3.6.--4', '-1', '-4', '+44-1', '+242--4', '-2', '-7', '-2', '+25--3', '--7', '+5-5-2', '7--7', '+13--6', '-3', '1', '-3', '+314--4', '+4-1', '+5+14-6', '+1--4', '+14-4--5', '-2', '+274-5', 79 | '+153--1', '31', '-5--1', '31--1', '5', '+1', '+2', '+3+15-4', '+4-6', '+51', '+13', '+1-5', '-7', '+22', '+1-6', '1', '3', '1', '-7-5', '51', '+13', '+21', '+315-4', '+41', '+53', '+1', '+1-5', '-7', '+22', '+1-1', '-5', '42', '-5', '31--1' 80 | 81 | ] 82 | 83 | export default [...song, ...song2] -------------------------------------------------------------------------------- /src/elements/app-piano/songs/moon.js: -------------------------------------------------------------------------------- 1 | const song = [ 2 | {// 月亮代表我的心 3 | // -5 4 | note: 'G3', 5 | time: 500 6 | }, { 7 | // 1. 8 | note: 'C4', 9 | time: 1000 10 | }, { 11 | // 3 12 | note: 'E4', 13 | time: 500 14 | }, { 15 | // 5. 16 | note: 'G4', 17 | time: 1000 18 | }, { 19 | // 1 20 | note: 'C4', 21 | time: 500 22 | }, { 23 | // -7 24 | note: 'B3', 25 | time: 1000 26 | }, { 27 | // 3 28 | note: 'E4', 29 | time: 500 30 | }, { 31 | // 5 32 | // 0 33 | // 我 34 | note: 'G4', 35 | time: 1500 36 | }, { 37 | // 5 38 | note: 'G4', 39 | time: 500 40 | }, { 41 | // 6. 42 | note: 'A4', 43 | time: 1000 44 | }, { 45 | // 7 46 | note: 'B4', 47 | time: 500 48 | }, { 49 | // +1. 50 | note: 'C5', 51 | time: 1000 52 | }, { 53 | // 6 54 | note: 'A4', 55 | time: 500 56 | }, { 57 | // 5 58 | note: 'G4', 59 | time: 1500 60 | }, { 61 | // 3 62 | note: 'E4', 63 | time: 500 64 | }, { 65 | // 2 66 | note: 'D4', 67 | time: 500 68 | }, { 69 | // 1. 70 | note: 'C4', 71 | time: 1000 72 | }, { 73 | // 1 74 | note: 'C4', 75 | time: 500 76 | }, { 77 | // 1 78 | note: 'C4', 79 | time: 500 80 | }, { 81 | // 3 82 | // 我的 83 | note: 'E4', 84 | time: 500 85 | }, { 86 | // 2 87 | note: 'D4', 88 | time: 500 89 | }, { 90 | // 1 91 | note: 'C4', 92 | time: 1000 93 | }, { 94 | // 1 95 | note: 'C4', 96 | time: 500 97 | }, { 98 | // 1 99 | note: 'C4', 100 | time: 500 101 | }, { 102 | // 2 103 | // 月亮 104 | note: 'D4', 105 | time: 500 106 | }, { 107 | // 3 108 | note: 'E4', 109 | time: 500 110 | }, { 111 | // 2 112 | note: 'D4', 113 | time: 1000 114 | }, { 115 | // 1 116 | note: 'C4', 117 | time: 500 118 | }, { 119 | // -6 120 | note: 'A3', 121 | time: 500 122 | }, { 123 | // 2 124 | note: 'D4', 125 | time: 500 126 | }, { 127 | // 3 128 | note: 'E4', 129 | time: 500 130 | }, { 131 | // 2 132 | note: 'D4', 133 | time: 1500 134 | }, {// 月亮代表我的心 135 | // -5 136 | note: 'G3', 137 | time: 500 138 | }, { 139 | // 1. 140 | note: 'C4', 141 | time: 1000 142 | }, { 143 | // 3 144 | note: 'E4', 145 | time: 500 146 | }, { 147 | // 5. 148 | note: 'G4', 149 | time: 1000 150 | }, { 151 | // 1 152 | note: 'C4', 153 | time: 500 154 | }, { 155 | // -7 156 | note: 'B3', 157 | time: 1000 158 | }, { 159 | // 3 160 | note: 'E4', 161 | time: 500 162 | }, { 163 | // 5 164 | // 0 165 | // 我 166 | note: 'G4', 167 | time: 1500 168 | }, { 169 | // 5 170 | note: 'G4', 171 | time: 500 172 | }, { 173 | // 6. 174 | note: 'A4', 175 | time: 1000 176 | }, { 177 | // 7 178 | note: 'B4', 179 | time: 500 180 | }, { 181 | // +1. 182 | note: 'C5', 183 | time: 1000 184 | }, { 185 | // 6 186 | note: 'A4', 187 | time: 500 188 | }, { 189 | // 5 190 | note: 'G4', 191 | time: 1500 192 | }, { 193 | // 3 194 | note: 'E4', 195 | time: 500 196 | }, { 197 | // 2 198 | note: 'D4', 199 | time: 500 200 | }, { 201 | // 1. 202 | note: 'C4', 203 | time: 1000 204 | }, { 205 | // 1 206 | note: 'C4', 207 | time: 500 208 | }, { 209 | // 1 210 | note: 'C4', 211 | time: 500 212 | }, { 213 | // 3 214 | // 我的 215 | note: 'E4', 216 | time: 500 217 | }, { 218 | // 2 219 | note: 'D4', 220 | time: 500 221 | }, { 222 | // 1 223 | note: 'C4', 224 | time: 1000 225 | }, { 226 | // 1 227 | note: 'C4', 228 | time: 500 229 | }, { 230 | // 1 231 | note: 'C4', 232 | time: 500 233 | }, { 234 | // 2 235 | // 月亮 236 | note: 'D4', 237 | time: 500 238 | }, { 239 | // 3 240 | note: 'E4', 241 | time: 500 242 | }, { 243 | // 2 244 | note: 'D4', 245 | time: 1000 246 | }, { 247 | // 1 248 | note: 'C4', 249 | time: 500 250 | }, { 251 | // -6 252 | note: 'A3', 253 | time: 500 254 | }, { 255 | // 2 256 | note: 'D4', 257 | time: 500 258 | }, { 259 | // 3 260 | note: 'E4', 261 | time: 500 262 | }, { 263 | // 2 264 | note: 'D4', 265 | time: 1500 266 | }, 267 | 268 | { 269 | // 轻轻地一个吻 270 | // 3 271 | note: 'E4', 272 | time: 500 273 | }, { 274 | // 5 275 | note: 'G4', 276 | time: 500 277 | }, { 278 | // 3 279 | note: 'E4', 280 | time: 1000 281 | }, { 282 | // 2 283 | note: 'D4', 284 | time: 500 285 | }, { 286 | // 1 287 | note: 'C4', 288 | time: 500 289 | }, { 290 | // 5 291 | note: 'G4', 292 | time: 500 293 | }, { 294 | // -7 295 | note: 'B3', 296 | time: 1500 297 | }, { 298 | // -6 299 | note: 'A3', 300 | time: 500 301 | }, { 302 | // -7 303 | note: 'B3', 304 | time: 500 305 | }, { 306 | // -6 307 | note: 'A3', 308 | time: 1000 309 | }, { 310 | // -7 311 | note: 'B3', 312 | time: 500 313 | }, { 314 | // -6 315 | note: 'A3', 316 | time: 500 317 | }, { 318 | // -5 319 | note: 'G3', 320 | time: 500 321 | }, { 322 | // 3 323 | note: 'E4', 324 | time: 1500 325 | }, { 326 | // 5 327 | note: 'G4', 328 | time: 500 329 | }, { 330 | // 3. 331 | note: 'E4', 332 | time: 1000 333 | }, { 334 | // 2 335 | note: 'D4', 336 | time: 500 337 | }, { 338 | // 1 339 | note: 'C4', 340 | time: 500 341 | }, { 342 | // 5 343 | note: 'G4', 344 | time: 500 345 | }, { 346 | // -7... 347 | note: 'B3', 348 | time: 1500 349 | }, { 350 | // -6 351 | note: 'A3', 352 | time: 500 353 | }, { 354 | // -7 355 | note: 'B3', 356 | time: 500 357 | }, { 358 | // 1. 359 | note: 'C4', 360 | time: 1000 361 | }, { 362 | // 1 363 | note: 'C4', 364 | time: 500 365 | }, { 366 | // 1 367 | note: 'C4', 368 | time: 500 369 | }, { 370 | // 2 371 | note: 'D4', 372 | time: 500 373 | }, { 374 | // 3. 375 | note: 'E4', 376 | time: 500 377 | }, { 378 | // 2 379 | note: 'D4', 380 | time: 1500 381 | }, 382 | // 你问我 383 | { 384 | // -5 385 | note: 'G3', 386 | time: 500 387 | }, { 388 | // 1. 389 | note: 'C4', 390 | time: 1000 391 | }, { 392 | // 3 393 | note: 'E4', 394 | time: 500 395 | }, { 396 | // 5. 397 | note: 'G4', 398 | time: 1000 399 | }, { 400 | // 1 401 | note: 'C4', 402 | time: 500 403 | }, { 404 | // -7 405 | note: 'B3', 406 | time: 1000 407 | }, { 408 | // 3 409 | note: 'E4', 410 | time: 500 411 | }, { 412 | // 5 413 | // 0 414 | // 我 415 | note: 'G4', 416 | time: 1500 417 | }, { 418 | // 5 419 | note: 'G4', 420 | time: 500 421 | }, { 422 | // 6. 423 | note: 'A4', 424 | time: 1000 425 | }, { 426 | // 7 427 | note: 'B4', 428 | time: 500 429 | }, { 430 | // +1. 431 | note: 'C5', 432 | time: 1000 433 | }, { 434 | // 6 435 | note: 'A4', 436 | time: 500 437 | }, { 438 | // 5 439 | note: 'G4', 440 | time: 1500 441 | }, { 442 | // 3 443 | note: 'E4', 444 | time: 500 445 | }, { 446 | // 2 447 | note: 'D4', 448 | time: 500 449 | }, { 450 | // 1. 451 | note: 'C4', 452 | time: 1000 453 | }, { 454 | // 1 455 | note: 'C4', 456 | time: 500 457 | }, { 458 | // 1 459 | note: 'C4', 460 | time: 500 461 | }, { 462 | // 3 463 | // 我的 464 | note: 'E4', 465 | time: 500 466 | }, { 467 | // 2 468 | note: 'D4', 469 | time: 500 470 | }, { 471 | // 1 472 | note: 'C4', 473 | time: 1000 474 | }, { 475 | // 1 476 | note: 'C4', 477 | time: 500 478 | }, { 479 | // 1 480 | note: 'C4', 481 | time: 500 482 | }, { 483 | // 2 484 | // 月亮 485 | note: 'D4', 486 | time: 500 487 | }, { 488 | // 3 489 | note: 'E4', 490 | time: 500 491 | }, { 492 | // 2 493 | note: 'D4', 494 | time: 1000 495 | }, { 496 | // -6 497 | note: 'A3', 498 | time: 1000 499 | }, { 500 | // -7 501 | note: 'B3', 502 | time: 500 503 | }, { 504 | // 1 505 | note: 'C4', 506 | time: 1000 507 | }, { 508 | // 2 509 | note: 'D4', 510 | time: 500 511 | }, { 512 | // 1 513 | note: 'C4', 514 | time: 1000 515 | } 516 | ] 517 | export default [...song, ...song] -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const autoprefixer = require('autoprefixer'); 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 8 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 9 | const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); 10 | const eslintFormatter = require('react-dev-utils/eslintFormatter'); 11 | const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); 12 | const getClientEnvironment = require('./env'); 13 | const paths = require('./paths'); 14 | const fileList = require('./entry'); 15 | 16 | const pjson = require('../package.json'); 17 | 18 | let entry = {}; 19 | let htmlWebpackPlugins = []; 20 | 21 | fileList.forEach(function (item) { 22 | entry[item] = [ 23 | require.resolve('./polyfills'), 24 | require.resolve('react-dev-utils/webpackHotDevClient'), 25 | paths.appSrc + '/' + item + '.js', 26 | ]; 27 | 28 | htmlWebpackPlugins.push( 29 | new HtmlWebpackPlugin({ 30 | inject: true, 31 | chunks: [item], 32 | template: paths.appHtml, 33 | filename: item + '.html' 34 | })); 35 | }); 36 | 37 | // Webpack uses `publicPath` to determine where the app is being served from. 38 | // In development, we always serve from the root. This makes config easier. 39 | const publicPath = '/'; 40 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 41 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 42 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 43 | const publicUrl = ''; 44 | // Get environment variables to inject into our app. 45 | const env = getClientEnvironment(publicUrl); 46 | 47 | // This is the development configuration. 48 | // It is focused on developer experience and fast rebuilds. 49 | // The production configuration is different and lives in a separate file. 50 | module.exports = { 51 | // You may want 'eval' instead if you prefer to see the compiled output in DevTools. 52 | // See the discussion in https://github.com/facebookincubator/create-react-app/issues/343. 53 | devtool: 'cheap-module-source-map', 54 | // These are the "entry points" to our application. 55 | // This means they will be the "root" imports that are included in JS bundle. 56 | // The first two entry points enable "hot" CSS and auto-refreshes for JS. 57 | entry: entry, 58 | output: { 59 | // Add /* filename */ comments to generated require()s in the output. 60 | pathinfo: true, 61 | path: paths.appBuild, 62 | // This does not produce a real file. It's just the virtual path that is 63 | // served by WebpackDevServer in development. This is the JS bundle 64 | // containing code from all our entry points, and the Webpack runtime. 65 | filename: 'static/js/[name].bundle.js', 66 | // There are also additional JS chunk files if you use code splitting. 67 | chunkFilename: 'static/js/[name].chunk.js', 68 | // This is the URL that app is served from. We use "/" in development. 69 | publicPath: publicPath, 70 | // Point sourcemap entries to original disk location (format as URL on Windows) 71 | devtoolModuleFilenameTemplate: info => 72 | path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'), 73 | }, 74 | resolve: { 75 | // This allows you to set a fallback for where Webpack should look for modules. 76 | // We placed these paths second because we want `node_modules` to "win" 77 | // if there are any conflicts. This matches Node resolution mechanism. 78 | // https://github.com/facebookincubator/create-react-app/issues/253 79 | modules: ['node_modules', paths.appNodeModules].concat( 80 | // It is guaranteed to exist because we tweak it in `env.js` 81 | process.env.NODE_PATH.split(path.delimiter).filter(Boolean) 82 | ), 83 | // These are the reasonable defaults supported by the Node ecosystem. 84 | // We also include JSX as a common component filename extension to support 85 | // some tools, although we do not recommend using it, see: 86 | // https://github.com/facebookincubator/create-react-app/issues/290 87 | // `web` extension prefixes have been added for better support 88 | // for React Native Web. 89 | extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'], 90 | alias: pjson.alias, 91 | plugins: [ 92 | // Prevents users from importing files from outside of src/ (or node_modules/). 93 | // This often causes confusion because we only process files within src/ with babel. 94 | // To fix this, we prevent you from importing files out of src/ -- if you'd like to, 95 | // please link the files into your node_modules/ and let module-resolution kick in. 96 | // Make sure your source files are compiled, as they will not be processed in any way. 97 | new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), 98 | ], 99 | }, 100 | module: { 101 | strictExportPresence: true, 102 | rules: [ 103 | // TODO: Disable require.ensure as it's not a standard language feature. 104 | // We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176. 105 | // { parser: { requireEnsure: false } }, 106 | 107 | // First, run the linter. 108 | // It's important to do this before Babel processes the JS. 109 | // { 110 | // test: /\.(js|jsx|mjs)$/, 111 | // enforce: 'pre', 112 | // use: [ 113 | // { 114 | // options: { 115 | // formatter: eslintFormatter, 116 | // eslintPath: require.resolve('eslint'), 117 | 118 | // }, 119 | // loader: require.resolve('eslint-loader'), 120 | // }, 121 | // ], 122 | // include: paths.appSrc, 123 | // }, 124 | { 125 | // "oneOf" will traverse all following loaders until one will 126 | // match the requirements. When no loader matches it will fall 127 | // back to the "file" loader at the end of the loader list. 128 | oneOf: [ 129 | // "url" loader works like "file" loader except that it embeds assets 130 | // smaller than specified limit in bytes as data URLs to avoid requests. 131 | // A missing `test` is equivalent to a match. 132 | { 133 | test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 134 | loader: require.resolve('url-loader'), 135 | options: { 136 | limit: 10000, 137 | name: 'static/media/[name].[hash:8].[ext]', 138 | }, 139 | }, 140 | // Process JS with Babel. 141 | { 142 | test: /\.(js|jsx|mjs)$/, 143 | //include: paths.appSrc, 144 | loader: require.resolve('babel-loader'), 145 | options: { 146 | "presets": [ 147 | "@babel/preset-env", 148 | [ 149 | "@babel/preset-react", 150 | { 151 | "pragma": "Omi.h" 152 | } 153 | ] 154 | ], 155 | "plugins": [ 156 | "@babel/plugin-proposal-class-properties", 157 | [ 158 | "@babel/plugin-proposal-decorators", 159 | { 160 | "legacy": true 161 | } 162 | ], 163 | "@babel/plugin-proposal-function-bind", 164 | "@babel/plugin-proposal-object-rest-spread", 165 | "@babel/plugin-syntax-dynamic-import" 166 | ], 167 | // This is a feature of `babel-loader` for webpack (not Babel itself). 168 | // It enables caching results in ./node_modules/.cache/babel-loader/ 169 | // directory for faster rebuilds. 170 | cacheDirectory: true, 171 | }, 172 | }, 173 | { 174 | test: /[\\|\/]_[\S]*\.css$/, 175 | use: [ 176 | 'to-string-loader', 177 | 'css-loader' 178 | ] 179 | }, 180 | { 181 | test: /[\\|\/]_[\S]*\.less$/, 182 | use: [ 183 | 'to-string-loader', 184 | 'css-loader', 185 | 'less-loader' 186 | ] 187 | }, 188 | { 189 | test: /\.less$/, 190 | use: [ 191 | 'style-loader', 192 | 'css-loader', 193 | 'less-loader' 194 | ] 195 | }, 196 | // "postcss" loader applies autoprefixer to our CSS. 197 | // "css" loader resolves paths in CSS and adds assets as dependencies. 198 | // "style" loader turns CSS into JS modules that inject -------------------------------------------------------------------------------- /src/elements/app-piano/index.js: -------------------------------------------------------------------------------- 1 | import { WeElement, define, h } from "omi"; 2 | import notes from "./notes.js"; 3 | import moon from "./songs/moon.js"; 4 | import fuji from "./songs/fuji.js"; 5 | import later from "./songs/later.js"; 6 | import pgydyd from "./songs/pgydyd.js"; 7 | import xxy from "./songs/xxy.js"; 8 | import pianoKeys from "./pianoKeys.js"; 9 | 10 | class AppPiano extends WeElement { 11 | constructor(...args) { 12 | super(...args); 13 | 14 | this.add = () => this.store.add(); 15 | 16 | this.sub = () => this.store.sub(); 17 | 18 | this.setSong = song => this.store.setSong(song); 19 | 20 | this.setCount = count => this.store.setSong(count); 21 | } 22 | 23 | render(props) { 24 | return h( 25 | "div", 26 | { 27 | class: "" 28 | }, 29 | h( 30 | "div", 31 | { 32 | class: "piano" 33 | }, 34 | this.data.pianoKeys.map(item => { 35 | return h( 36 | "div", 37 | { 38 | class: "piano-key" 39 | }, 40 | h( 41 | "div", 42 | { 43 | "data-type": "white", 44 | ref: e => { 45 | this[item.white.name] = e; 46 | }, 47 | class: "piano-key__white", 48 | onClick: this.playNote.bind(this, item.white.name), 49 | "data-key": item.white.keyCode, 50 | "data-note": item.white.name 51 | }, 52 | h( 53 | "span", 54 | { 55 | class: "piano-note" 56 | }, 57 | item.white.name 58 | ), 59 | h("audio", { 60 | preload: "auto", 61 | src: this.data.notes[item.white.name].url, 62 | hidden: "true", 63 | "data-note": item.white.name, 64 | class: "audioEle" 65 | }) 66 | ), 67 | h( 68 | "div", 69 | { 70 | "data-type": "black", 71 | ref: e => { 72 | this[item.black.name] = e; 73 | }, 74 | style: { 75 | display: item.black.name ? "block" : "none" 76 | }, 77 | class: "piano-key__black", 78 | onClick: this.playNote.bind(this, item.black.name), 79 | "data-key": item.black.keyCode, 80 | "data-note": item.black.name 81 | }, 82 | h( 83 | "span", 84 | { 85 | class: "piano-note", 86 | style: "color:#fff" 87 | }, 88 | item.black.name 89 | ), 90 | h("audio", { 91 | preload: "auto", 92 | src: 93 | this.data.notes[item.black.name] && 94 | this.data.notes[item.black.name].url, 95 | hidden: "true", 96 | "data-note": item.black.name, 97 | class: "audioEle" 98 | }) 99 | ) 100 | ); 101 | }) 102 | ), 103 | h( 104 | "div", 105 | { 106 | class: "text-center" 107 | }, 108 | h( 109 | "p", 110 | null, 111 | "Click the button below to let the piano play the song automatically:" 112 | ), 113 | h( 114 | "p", 115 | null, 116 | "\u70B9\u51FB\u4E0B\u9762\u6309\u94AE\u8BA9\u94A2\u7434\u81EA\u52A8\u6F14\u594F\u6B4C\u66F2:", 117 | this.store.data.count > 0 ? "1" : "0" 118 | ), 119 | h( 120 | "div", 121 | null, 122 | this.store.data.count > 0 123 | ? h( 124 | "button", 125 | { 126 | onClick: this.stopSong.bind(this), 127 | class: "btn btn-outline-info btn-stop" 128 | }, 129 | "Stop & \u6682\u505C" 130 | ) 131 | : h( 132 | "div", 133 | null, 134 | h( 135 | "button", 136 | { 137 | onClick: this.playSong.bind(this, moon), 138 | class: "btn btn-outline-info" 139 | }, 140 | "\u6708\u4EAE\u4EE3\u8868\u6211\u7684\u5FC3" 141 | ), 142 | h( 143 | "button", 144 | { 145 | onClick: this.playSong.bind(this, pgydyd), 146 | class: "btn btn-outline-info" 147 | }, 148 | "\u84B2\u516C\u82F1\u7684\u7EA6\u5B9A" 149 | ), 150 | h( 151 | "button", 152 | { 153 | onClick: this.playSong.bind(this, xxy), 154 | class: "btn btn-outline-info" 155 | }, 156 | "\u5C0F\u5E78\u8FD0" 157 | ), 158 | h( 159 | "button", 160 | { 161 | onClick: this.playSong.bind(this, fuji), 162 | class: "btn btn-outline-info" 163 | }, 164 | "\u5BCC\u58EB\u5C71\u4E0B&\u7231\u60C5\u8F6C\u79FB" 165 | ) 166 | ) 167 | ) 168 | ) 169 | ); 170 | } 171 | 172 | install() { 173 | this.data = { 174 | notes, 175 | pianoKeys 176 | }; 177 | 178 | document.onkeydown = event => { 179 | var e = event || window.event || arguments.callee.caller.arguments[0]; 180 | 181 | let playNote = key => { 182 | if (e.shiftKey === true) { 183 | this.playNote(`${key}2`); 184 | } else if (e.altKey === true) { 185 | this.playNote(`${key}5`); 186 | } else if (e.ctrlKey === true) { 187 | this.playNote(`${key}3`); 188 | } else if (e.metaKey === true) { 189 | this.playNote(`${key}6`); 190 | e.returnValue = false; 191 | } else { 192 | this.playNote(`${key}4`); 193 | } 194 | }; 195 | 196 | if (e && 49 <= e.keyCode && e.keyCode <= 55) { 197 | switch (e.keyCode) { 198 | case 49: 199 | playNote("C"); 200 | break; 201 | 202 | case 50: 203 | playNote("D"); 204 | break; 205 | 206 | case 51: 207 | playNote("E"); 208 | break; 209 | 210 | case 52: 211 | playNote("F"); 212 | break; 213 | 214 | case 53: 215 | playNote("G"); 216 | break; 217 | 218 | case 54: 219 | playNote("A"); 220 | break; 221 | 222 | case 55: 223 | playNote("B"); 224 | break; 225 | } 226 | } 227 | 228 | if ( 229 | e && 230 | (81 === e.keyCode || 231 | e.keyCode === 87 || 232 | e.keyCode === 69 || 233 | e.keyCode === 82 || 234 | e.keyCode === 84) 235 | ) { 236 | switch (e.keyCode) { 237 | case 81: 238 | playNote("C#"); 239 | break; 240 | 241 | case 87: 242 | playNote("D#"); 243 | break; 244 | 245 | case 69: 246 | playNote("F#"); 247 | break; 248 | 249 | case 82: 250 | playNote("G#"); 251 | break; 252 | 253 | case 84: 254 | playNote("A#"); 255 | break; 256 | } 257 | } 258 | }; 259 | } 260 | 261 | stopSong() { 262 | clearTimeout(this.timer); 263 | this.store.data.song = []; 264 | this.store.data.count = 0; 265 | console.log("reset"); 266 | } 267 | 268 | playNote(name) { 269 | console.log(this.data.notes[name]); 270 | 271 | if (!this.data.notes[name]["isPlay"]) { 272 | let audio = this[name].childNodes[1]; 273 | this[ 274 | name 275 | ].style.background = `linear-gradient(-20deg, #3330fb, #000, #222)`; 276 | let timer = setTimeout(() => { 277 | this[name].getAttribute("data-type") === "white" 278 | ? (this[ 279 | name 280 | ].style.background = `linear-gradient(-30deg, #f8f8f8, #fff)`) 281 | : (this[ 282 | name 283 | ].style.background = `linear-gradient(-20deg, #222, #000, #222)`); 284 | clearTimeout(timer); 285 | }, 1000); 286 | audio.currentTime = 0; 287 | audio.play(); 288 | this.data.notes[name]["isPlay"] = true; 289 | let isPlay = setTimeout(() => { 290 | this.data.notes[name]["isPlay"] = false; 291 | clearTimeout(isPlay); 292 | }, 500); 293 | } 294 | } 295 | 296 | playSong(song) { 297 | this.setSong([...song]); 298 | let offset = 0; 299 | let time = 0; 300 | 301 | let playSong = async () => { 302 | if (offset < song.length && this.store.data.song.length > 0) { 303 | switch (typeof song[offset]) { 304 | case "string": 305 | let letters = song[offset].match(/[0-9]/g); 306 | 307 | switch (letters.length) { 308 | case 1: 309 | time = this.handleString(song, offset); 310 | break; 311 | 312 | default: 313 | time = this.handleStrings(song, offset); 314 | break; 315 | } 316 | 317 | break; 318 | 319 | case "object": 320 | console.log(song[offset]["note"]); 321 | time = song[offset]["time"]; 322 | this.playNote(song[offset]["note"]); 323 | break; 324 | 325 | case "number": 326 | switch (song[offset]) { 327 | case 0: 328 | time = 1000; 329 | break; 330 | } 331 | 332 | break; 333 | } 334 | 335 | await new Promise(resolve => { 336 | let timer = setTimeout(() => { 337 | clearInterval(timer); 338 | resolve(); 339 | }, time); 340 | }); 341 | offset++; 342 | this.update(); 343 | this.add(); 344 | playSong(); 345 | } else { 346 | clearTimeout(this.timer); 347 | this.store.data.song = []; 348 | this.store.data.count = 0; 349 | return; 350 | } 351 | }; 352 | 353 | playSong(); 354 | } 355 | 356 | playSongByInterval(song) { 357 | clearInterval(this.interval); 358 | let offset = 0; 359 | let time = 0; 360 | this.interval = setInterval(() => { 361 | if (offset < song.length) { 362 | switch (typeof song[offset]) { 363 | case "string": 364 | let letters = song[offset].match(/[0-9]/g); 365 | 366 | switch (letters.length) { 367 | case 1: 368 | time = this.handleString(song, offset); 369 | break; 370 | 371 | default: 372 | time = this.handleStrings(song, offset); 373 | break; 374 | } 375 | 376 | break; 377 | 378 | case "object": 379 | console.log(song[offset]["note"]); 380 | time = song[offset]["time"]; 381 | this.playNote(song[offset]["note"]); 382 | break; 383 | 384 | case "number": 385 | switch (song[offset]) { 386 | case 0: 387 | time = 1000; 388 | break; 389 | } 390 | 391 | break; 392 | } 393 | 394 | ++offset; 395 | } else { 396 | clearInterval(this.interval); 397 | } 398 | }, 500); 399 | } 400 | 401 | handleStrings(song, offset) { 402 | let reg = /[0-9]/g; 403 | let str = song[offset]; 404 | let order = 1; 405 | let result = []; 406 | 407 | while (true) { 408 | let temp = reg.exec(str); 409 | 410 | if (temp) { 411 | result.push({ 412 | text: temp[0], 413 | index: temp.index, 414 | order: order 415 | }); 416 | order++; 417 | } else { 418 | break; 419 | } 420 | } 421 | 422 | result.map(item => { 423 | switch (str[item.index - 1]) { 424 | case "1": 425 | case "2": 426 | case "3": 427 | case "4": 428 | case "5": 429 | case "6": 430 | case "7": 431 | break; 432 | 433 | case "+": 434 | item.text = `+${item.text}`; 435 | 436 | switch (str[item.index - 2]) { 437 | case "+": 438 | item.text = `+${item.text}`; 439 | break; 440 | } 441 | 442 | break; 443 | 444 | case "-": 445 | item.text = `-${item.text}`; 446 | 447 | switch (str[item.index - 2]) { 448 | case "-": 449 | item.text = `-${item.text}`; 450 | break; 451 | } 452 | 453 | break; 454 | 455 | case "#": 456 | item.text = `#${item.text}`; 457 | 458 | switch (str[item.index - 2]) { 459 | case "-": 460 | item.text = `-${item.text}`; 461 | 462 | switch (str[item.index - 3]) { 463 | case "-": 464 | item.text = `-${item.text}`; 465 | break; 466 | } 467 | 468 | break; 469 | 470 | case "+": 471 | item.text = `+${item.text}`; 472 | 473 | switch (str[item.index - 3]) { 474 | case "+": 475 | item.text = `+${item.text}`; 476 | break; 477 | } 478 | 479 | break; 480 | } 481 | 482 | break; 483 | } 484 | 485 | switch (str[item.index + 1]) { 486 | case ".": 487 | item.text = `${item.text}.`; 488 | 489 | switch (str[item.index + 2]) { 490 | case ".": 491 | item.text = `${item.text}.`; 492 | break; 493 | } 494 | 495 | break; 496 | } 497 | }); 498 | let notes = result.map(item => { 499 | return item.text; 500 | }); 501 | let time = []; 502 | notes.forEach((item, index) => { 503 | time.push(this.handleString(notes, index)); 504 | }); 505 | return time.sort()[0]; 506 | } 507 | 508 | handleString(song, offset) { 509 | let letter = song[offset].match(/[0-9]/g)[0]; 510 | let subKey = song[offset].split("-").length - 1; 511 | let addKey = song[offset].split("+").length - 1; 512 | let pointKey = song[offset].split(".").length - 1; 513 | let halfKey = song[offset].split("#").length - 1; 514 | let note; 515 | let key; 516 | let time; 517 | 518 | switch (letter) { 519 | case "0": 520 | return (time = 1000); 521 | break; 522 | 523 | case "1": 524 | note = "C"; 525 | break; 526 | 527 | case "2": 528 | note = "D"; 529 | break; 530 | 531 | case "3": 532 | note = "E"; 533 | break; 534 | 535 | case "4": 536 | note = "F"; 537 | break; 538 | 539 | case "5": 540 | note = "G"; 541 | break; 542 | 543 | case "6": 544 | note = "A"; 545 | break; 546 | 547 | case "7": 548 | note = "B"; 549 | break; 550 | } 551 | 552 | switch (subKey) { 553 | case 0: 554 | key = 4; 555 | break; 556 | 557 | case 1: 558 | key = 3; 559 | break; 560 | 561 | case 2: 562 | key = 2; 563 | break; 564 | } 565 | 566 | switch (addKey) { 567 | case 0: 568 | key = 4; 569 | break; 570 | 571 | case 1: 572 | key = 5; 573 | break; 574 | 575 | case 2: 576 | key = 6; 577 | break; 578 | } 579 | 580 | switch (pointKey) { 581 | case 0: 582 | time = 500; 583 | break; 584 | 585 | case 1: 586 | time = 1000; 587 | break; 588 | 589 | case 2: 590 | time = 1500; 591 | break; 592 | } 593 | 594 | console.log(`${note + (halfKey > 0 ? "#" : "") + key}`); 595 | this.playNote(`${note + (halfKey > 0 ? "#" : "") + key}`); 596 | return time; 597 | } 598 | 599 | recordSong() {} 600 | } 601 | 602 | AppPiano.css = ` 603 | * { 604 | margin: 0; 605 | padding: 0; 606 | } 607 | 608 | .icon { 609 | width: 24px; 610 | } 611 | 612 | .piano { 613 | margin: 0 200px; 614 | background: linear-gradient(-65deg, #000, #222, #000, #666, #222 75%); 615 | border-top: .8rem solid #282828; 616 | -webkit-box-shadow: inset 0 -1px 1px hsla(0, 0%, 100%, .5), inset -0.4rem 0.4rem #282828; 617 | box-shadow: inset 0 -1px 1px hsla(0, 0%, 100%, .5), inset -0.4rem 0.4rem #282828; 618 | display: -webkit-box; 619 | display: -ms-flexbox; 620 | display: flex; 621 | height: 80vh; 622 | height: 20vh; 623 | -webkit-box-pack: center; 624 | -ms-flex-pack: center; 625 | justify-content: center; 626 | overflow: hidden; 627 | padding-bottom: 2%; 628 | padding-left: 2.5%; 629 | padding-right: 2.5%; 630 | } 631 | 632 | @media screen and (max-width: 1000px) { 633 | 634 | /*当屏幕尺寸小于600px时,应用下面的CSS样式*/ 635 | .piano { 636 | margin: 0 10px; 637 | } 638 | } 639 | 640 | .piano-key { 641 | color: blue; 642 | -webkit-box-flex: 1; 643 | -ms-flex: 1; 644 | flex: 1; 645 | margin: 0 .1rem; 646 | max-width: 8.8rem; 647 | position: relative; 648 | } 649 | 650 | .piano-key__white { 651 | display: flex; 652 | flex-direction: column-reverse; 653 | background: linear-gradient(-30deg, #f8f8f8, #fff); 654 | -webkit-box-shadow: inset 0 1px 0 #fff, inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px 0 0 #fff, 0 4px 3px rgba(0, 0, 0, .7), inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px -1px 15px rgba(0, 0, 0, .5), -3px 4px 6px rgba(0, 0, 0, .5); 655 | box-shadow: inset 0 1px 0 #fff, inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px 0 0 #fff, 0 4px 3px rgba(0, 0, 0, .7), inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px -1px 15px rgba(0, 0, 0, .5), -3px 4px 6px rgba(0, 0, 0, .5); 656 | height: 100%; 657 | position: relative; 658 | } 659 | 660 | .piano-key__black { 661 | display: flex; 662 | flex-direction: column-reverse; 663 | background: linear-gradient(-20deg, #222, #000, #222); 664 | -webkit-box-shadow: inset 0 -1px 2px hsla(0, 0%, 100%, .4), 0 2px 3px rgba(0, 0, 0, .4); 665 | box-shadow: inset 0 -1px 2px hsla(0, 0%, 100%, .4), 0 2px 3px rgba(0, 0, 0, .4); 666 | border-width: .2rem .4rem 1.2rem; 667 | border-style: solid; 668 | border-color: #666 #222 #111 #555; 669 | height: 60%; 670 | left: 100%; 671 | position: absolute; 672 | -webkit-transform: translateX(-50%); 673 | transform: translateX(-50%); 674 | top: 0; 675 | width: 70%; 676 | z-index: 1; 677 | } 678 | 679 | .piano-note { 680 | color: #000; 681 | /* 隐藏音符显示 */ 682 | /* font-size: 8px; */ 683 | font-size: 0px; 684 | text-align: center; 685 | height: 20px; 686 | } 687 | 688 | a { 689 | text-decoration: none; 690 | } 691 | 692 | .text-center { 693 | margin: 15px; 694 | text-align: center !important; 695 | } 696 | 697 | /* .btn:not(:disabled):not(.disabled) { 698 | cursor: pointer; 699 | } */ 700 | 701 | .btn-outline-info { 702 | color: #17a2b8; 703 | background-color: transparent; 704 | background-image: none; 705 | border-color: #17a2b8; 706 | 707 | } 708 | 709 | .btn { 710 | text-transform: none; 711 | margin: 15px; 712 | display: inline-block; 713 | font-weight: 400; 714 | text-align: center; 715 | /* white-space: nowrap; */ 716 | vertical-align: middle; 717 | /* -webkit-user-select: none; 718 | -moz-user-select: none; 719 | -ms-user-select: none; 720 | user-select: none; */ 721 | border: 1px solid #17a2b8; 722 | padding: 8px 8px; 723 | font-size: 16px; 724 | line-height: 16px; 725 | border-radius: 2.5px; 726 | /* transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out; */ 727 | } 728 | 729 | .btn-stop { 730 | color: #ff7171; 731 | border-color: #ff7171; 732 | } 733 | `; 734 | AppPiano.use = [ 735 | { 736 | count: "count", 737 | song: "song" 738 | } 739 | ]; 740 | define("app-piano", AppPiano); 741 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Omi Piano

4 | 5 | 6 |

7 | Build piano with Omi and Omi Snippets 8 |

9 | 基于Omi和Omi Snippets构建钢琴应用 10 |

11 | 12 | 13 |

14 | Made with ❤︎ by 15 | Eno Yao 16 | 17 |

18 | 19 |

20 | 21 | 22 | 23 | 24 |

25 |
26 | 27 | 32 | 33 | # Usage 34 | 35 | 36 | 37 | > 体验地址: http://wscats.gitee.io/piano/build/ 或者 https://wscats.github.io/piano/build/ 38 | 39 | 40 | > 项目地址: https://github.com/Wscats/piano 41 | 42 | 43 | 44 | 45 | 46 | 用键盘8个键演奏一首蒲公英的约定送给996的自己或者一首月亮代表我的心给七夕的她,非常简单~ 47 | 48 | 这个项目仅仅用了几个简单的前端技术实现,献给每一位挚爱音乐的代码家🎹 49 | 50 | 如果你喜欢或者对你有帮助,给我点个赞支持下吧😊 51 | 52 | # Develop & Installation 53 | 54 | 55 | 开发,构建和运行。 56 | 57 | ```bash 58 | # 获取远程仓库代码 59 | git clone https://github.com/Wscats/piano 60 | # 进入目录 61 | cd piano 62 | # 安装依赖 63 | npm install 64 | # 启动项目 65 | npm start 66 | # 在浏览器访问 http://localhost:3000 67 | ``` 68 | 69 | 使用 npm 包管理器安装。 70 | 71 | ```bash 72 | npm install omi-piano 73 | ``` 74 | 75 | 运行或者发布属于自己的演奏版本。 76 | 77 | ```bash 78 | # 进入目录 79 | cd omi-piano 80 | # 安装依赖 81 | npm install 82 | # 启动项目 83 | npm start 84 | # 发布自已的演奏版本 85 | npm run build 86 | ``` 87 | 88 | # 技术点和目录结构 89 | 90 | 项目中没有使用市面主流的框架(React,Vue 和 Angular )和热门的技术,而用的是 Omi 框架(`JSX+WebComponents`),还有 `Omil` 的单文件组件 `SFCs` 加载器,组件通讯基于`Proxy`特性,并结合了 VScode 的插件 `Eno-Snippets`基于`AST`和`正则`实时编译`.eno或.omi` 后缀组件减轻部分的 `Webpack` 的局部编译压力,当然其他同学们熟知的技术这里就不提及了。 91 | 92 | 93 | 94 | 95 | 96 | - src 97 | - assets 98 | - element 99 | - app-piano 100 | - songs 钢琴简谱目录 101 | - app-piano.eno 单文件组件 102 | - app-piano.js 组件编译后的JS文件 103 | - notes.js 键盘按键和音符的映射 104 | - index.js 组件根容器,配置`Proxy`的通信方法 105 | - public 106 | - samples/piano 钢琴单音符素材 107 | 108 | |app-piano.eno|开发中你需要编写的单文件组件| 109 | |-|-| 110 | |app-piano.js|经过`Eno-Snippets`修改或者保存文件`Hello.eno`后经过插件转化的`js`文件| 111 | 112 | 如右图,左边的代码是我们编写的 `.eno` 后缀的单文件组件,右边是经过 `Eno Snippets` 生成的 `.js` 后缀文件。 113 | 114 | 115 | # 简单乐理知识 116 | 117 | 首先我们先补习点音乐基础,提前收集好最基本的[钢琴单音素材](https://github.com/Wscats/piano/tree/master/public/samples/piano),每个音符对应一份`.mp3`文件,用一个对象记录起来,类似下面这样,举个例子这里的`A`指的是`CDEFGAB`音名中`A`也就是`La`,这是最基本的乐理,有没有让你想起小时候上音乐课,画板上的五线谱。 118 | 119 | ```js 120 | export default { 121 | A2: "./samples/piano/a54.mp3", 122 | A3: "./samples/piano/a69.mp3", 123 | A4: "./samples/piano/a80.mp3", 124 | A5: "./samples/piano/a74.mp3", 125 | A6: "./samples/piano/a66.mp3", 126 | 'A#3': "./samples/piano/b69.mp3", 127 | 'A#4': "./samples/piano/b80.mp3", 128 | 'A#5': "./samples/piano/b74.mp3", 129 | 'A#6': "./samples/piano/b66.mp3", 130 | // other... 131 | } 132 | ``` 133 | 134 | 当然这里我们使用数字来等价替代,降低初学者的难度,看下表`1`等价于`C`中音也就是`Do`,由于很多歌都会用到钢琴更密集的中间部分按键,所以我们默认中音对应数字键: 135 | 136 | > `1 === C4 === Do` 137 | 138 | 139 | |数字键|1|2|3|4|5|6|7| 140 | |-|-|-|-|-|-|-|-| 141 | |音名|C4|D4|E4|F4|G4|A4|B4| 142 | |音符|Do|Re|Mi|Fa|Sol|La|Si| 143 | 144 | 145 | 146 | 这里专门制作一张图方便我们理解,请看右图: 147 | 148 | 当然实际情况还有全音和半音的区分,比如`A`的半音就是`A#`,还有中音,高音和倍高音,我们这里用`A4`表示中音,`A5`表示高音,`A6`表示倍高音,所以表格可以继续整理得更清晰,当我们要弹奏中音`A4`,只需要按键盘上的数字键`6`,如果要弹奏高音`A5`,只需要用组合键`Option+6`,我们只需要举一反三,就可以知道每个音符对应的键盘按键。 149 | 150 | |倍低音|C2|D2|E2|F2|G2|A2|B2| 151 | |-|-|-|-|-|-|-|-| 152 | |Shift键+(1-7)|Shift+1|Shift+2|Shift+3|Shift+4|Shift+5|Shift+6|Shift+7| 153 | |低音|C3|D3|E3|F3|G3|A3|B3| 154 | |Ctrl键+(1-7)|Ctrl+1|Ctrl+2|Ctrl+3|Ctrl+4|Ctrl+5|Ctrl+6|Ctrl+7| 155 | |中音|C4|D4|E4|F4|G4|A4|B4| 156 | |数字键1-7|1|2|3|4|5|6|7| 157 | |高音|C5|D5|E5|F5|G5|A5|B5| 158 | |Option键+(1-7)|Option+1|Option+2|Option+3|Option+4|Option+5|Option+6|Option+7| 159 | |倍高音|C6|D6|E6|F6|G6|A6|B6| 160 | |Command键+(1-7)|Command+1|Command+2|Command+3|Command+4|Command+5|Command+6|Command+7| 161 | |音符|Do|Re|Mi|Fa|Sol|La|Si| 162 | 163 | 上面是全音表,这里附上半音表: 164 | 165 | |倍低半音|C#2|D#2|F#2|G#2|A#2| 166 | |-|-|-|-|-|-| 167 | |Shift+|Shift+q|Shift+w|Shift+e|Shift+r|Shift+t| 168 | |低半音|C#3|D#3|F#3|G#3|A#3| 169 | |Ctrl+|Ctrl+q|Ctrl+w|Ctrl+e|Ctrl+r|Ctrl+t| 170 | |中半音|C#4|D#4|F#4|G#4|A#4| 171 | |字母键|q|w|e|r|t| 172 | |高半音|C#5|D#5|F#5|G#5|A#5| 173 | |Option+|Option+q|Option+w|Option+e|Option+r|Option+t| 174 | |倍高半音|C#6|D#6|F#6|G#6|A#6| 175 | |Command+|Command+q|Command+w|Command+e|Command+r|Command+t| 176 | 177 | 那么我们现在只需要用键盘上的5个`字母键(q,w,e,r,t)` + 4个`功能键(Shift,Control,Option和Command)` + 7个`数字键(1,2,3,4,5,6,7)`总共16个键,演奏钢琴60个单音(35个全音+25个半音),实际情况一首简单的钢琴曲可以不需要用到那么多,用几个简单的和弦即可。 178 | 179 | # 构建钢琴界面 180 | 181 | 有上面的前期准备,下面就是转化为我们的编程知识了,我们需要使用 HTML 来绘制我们的钢琴界面,我们可以参考 [codepen](https://codepen.io/search/pens?q=piano&page=1&order=popularity&depth=everything) 和 [codesandbox](https://codesandbox.io/search?query=piano&page=1&configure%5BhitsPerPage%5D=12) 的素材,这里我用了 `flex` 布局配合阴影和过度实现钢琴的黑白键,里面用了 React 的 JSX 语法去遍历渲染黑白键。 182 | 183 | 184 | 185 | ```html 186 |
187 | {this.data.pianoKeys.map((item)=>{return( 188 |
189 |
{ this[item.white.name] = e }} class="piano-key__white" 190 | onClick={this.playNote.bind(this,item.white.name)} data-key={item.white.keyCode} 191 | data-note={item.white.name}> 192 | {item.white.name} 193 | 195 |
196 |
{ this[item.black.name] = e }} style={{ 197 | display: item.black.name ? 'block' : 'none' 198 | }} class="piano-key__black" onClick={this.playNote.bind(this,item.black.name)} data-key={item.black.keyCode} 199 | data-note={item.black.name}> 200 | {item.black.name} 201 | 203 |
204 |
205 | )})} 206 |
207 | ``` 208 | 209 | 可以观察 CSS 的源代码,分别对应写黑键和白键的样式,还可以另外写多一个样式,用于键盘或者鼠标点击琴键时候的效果,可以简单给它加一个背景色即可,整体实现不会太复杂,具体可以调整样式的参数来打造属于自己的钢琴风格。 210 | 211 | ```css 212 | .piano { 213 | margin: 0 200px; 214 | background: linear-gradient(-65deg, #000, #222, #000, #666, #222 75%); 215 | border-top: .8rem solid #282828; 216 | box-shadow: inset 0 -1px 1px hsla(0, 0%, 100%, .5), inset -0.4rem 0.4rem #282828; 217 | display: flex; 218 | height: 80vh; 219 | height: 20vh; 220 | justify-content: center; 221 | overflow: hidden; 222 | padding-bottom: 2%; 223 | padding-left: 2.5%; 224 | padding-right: 2.5%; 225 | } 226 | .piano-key { 227 | color: blue; 228 | flex: 1; 229 | margin: 0 .1rem; 230 | max-width: 8.8rem; 231 | position: relative; 232 | } 233 | 234 | .piano-key__white { 235 | display: flex; 236 | flex-direction: column-reverse; 237 | background: linear-gradient(-30deg, #f8f8f8, #fff); 238 | box-shadow: inset 0 1px 0 #fff, inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px 0 0 #fff, 0 4px 3px rgba(0, 0, 0, .7), inset 0 -1px 0 #fff, inset 1px 0 0 #fff, inset -1px -1px 15px rgba(0, 0, 0, .5), -3px 4px 6px rgba(0, 0, 0, .5); 239 | height: 100%; 240 | position: relative; 241 | } 242 | 243 | .piano-key__black { 244 | display: flex; 245 | flex-direction: column-reverse; 246 | background: linear-gradient(-20deg, #222, #000, #222); 247 | box-shadow: inset 0 -1px 2px hsla(0, 0%, 100%, .4), 0 2px 3px rgba(0, 0, 0, .4); 248 | border-width: .2rem .4rem 1.2rem; 249 | border-style: solid; 250 | border-color: #666 #222 #111 #555; 251 | height: 60%; 252 | left: 100%; 253 | position: absolute; 254 | transform: translateX(-50%); 255 | top: 0; 256 | width: 70%; 257 | z-index: 1; 258 | } 259 | ``` 260 | 261 | # 播放钢琴音 262 | 263 | 当我们实现完钢琴界面,我们就需要为每个按键匹配声音,这里使用 HTML5 的 `