├── .eslintignore ├── postcss.config.js ├── assets ├── flow.gif ├── full.gif ├── sc-02.png ├── sc-03.png ├── sc-04.png └── versions.md ├── src ├── assets │ ├── info.png │ ├── play.png │ ├── sig.png │ ├── looop.ico │ ├── pause.png │ ├── play.svg │ └── pause.svg ├── music │ ├── effect │ │ ├── end.wav │ │ ├── beep.wav │ │ ├── wrong.wav │ │ ├── correct.wav │ │ └── transition.wav │ ├── sound.js │ └── clips.js ├── template.html ├── utils │ ├── utils.js │ └── questions.js ├── libs │ └── stats.min.js ├── renderer │ ├── pianoroll-grid.js │ └── renderer.js ├── index.module.scss └── index.js ├── .gitignore ├── .editorconfig ├── .babelrc ├── webpack.prod.config.js ├── readme.md ├── .eslintrc ├── LICENSE.md ├── webpack.dev.config.js ├── package.json └── webpack.common.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *config.js 2 | *min.js 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { plugins: [require('autoprefixer')] }; 2 | -------------------------------------------------------------------------------- /assets/flow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/assets/flow.gif -------------------------------------------------------------------------------- /assets/full.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/assets/full.gif -------------------------------------------------------------------------------- /assets/sc-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/assets/sc-02.png -------------------------------------------------------------------------------- /assets/sc-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/assets/sc-03.png -------------------------------------------------------------------------------- /assets/sc-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/assets/sc-04.png -------------------------------------------------------------------------------- /src/assets/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/assets/info.png -------------------------------------------------------------------------------- /src/assets/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/assets/play.png -------------------------------------------------------------------------------- /src/assets/sig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/assets/sig.png -------------------------------------------------------------------------------- /src/assets/looop.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/assets/looop.ico -------------------------------------------------------------------------------- /src/assets/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/assets/pause.png -------------------------------------------------------------------------------- /src/music/effect/end.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/music/effect/end.wav -------------------------------------------------------------------------------- /src/music/effect/beep.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/music/effect/beep.wav -------------------------------------------------------------------------------- /src/music/effect/wrong.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/music/effect/wrong.wav -------------------------------------------------------------------------------- /src/music/effect/correct.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/music/effect/correct.wav -------------------------------------------------------------------------------- /src/music/effect/transition.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibertthio/sornting/HEAD/src/music/effect/transition.wav -------------------------------------------------------------------------------- /assets/versions.md: -------------------------------------------------------------------------------- 1 | # 🎸Sornting (Song + Sort) 2 | 3 | > ### [play it](https://vibertthio.com/sornting/) 4 | 5 | ## screenshots 6 | 7 | ![](./sc-03.png) -------------------------------------------------------------------------------- /src/assets/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build files 2 | public/ 3 | 4 | # Dependency directory 5 | node_modules 6 | 7 | # Mac file system 8 | .ds_store 9 | .DS_Store 10 | 11 | # Logs 12 | logs 13 | *.log 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | 20 | **/checkpoints 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | 9 | # Matches multiple files with brace expansion notation 10 | # Set default charset 11 | [*.{js,jsx,html,sass}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 2 15 | trim_trailing_whitespace = true 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": ["last 2 versions", "safari >= 7"] 8 | } 9 | } 10 | ], 11 | "react", 12 | [ 13 | "babili", 14 | { 15 | "evaluate": false, 16 | "mangle": false 17 | } 18 | ] 19 | ], 20 | "plugins": [ 21 | "transform-runtime", 22 | "transform-class-properties" 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 4 | const path = require('path'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const common = require('./webpack.common.config.js'); 7 | 8 | module.exports = merge(common, { 9 | plugins: [ 10 | new UglifyJSPlugin(), 11 | new webpack.DefinePlugin({ 12 | 'process.env.NODE_ENV': JSON.stringify('production'), 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | Sornting 16 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 🎸Sornting 2 | 3 | > ### [play it](https://vibertthio.com/sornting/) 4 | 5 | #### = 🎻song + 🕵️‍♂️sort 6 | 7 | The player has to figure out the original order of the interpolation between two different melodies, and the difficulties will increase as the game progresses. 8 | 9 | Recently, I’m exploring the idea of using gamification to make the musical machine learning algorithms more understandable and fun. Therefore, I used MusicVAE.js to create this game “Sornting”, which is basically “Song” + “Sort”. 10 | 11 |
12 | 13 |

14 | 15 |

16 |
17 | 18 | I’m not trying to justify the result of the interpolation is perfect here. The user will find that some weird effect of interpolation while playing the game and listening to the melodies carefully. It will help the user to not only understand the model but also find out the weakness of model. 19 | 20 | 21 | 22 | ## Links 23 | 24 | [screenshots](./assets/versions.md) -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | "mocha": true 9 | }, 10 | 11 | "plugins": [ 12 | 'react', 13 | 'jsx-a11y', 14 | 'import', 15 | ], 16 | 17 | "rules": { 18 | "global-require": 0, 19 | "no-restricted-globals": 0, 20 | "no-shadow": 0, 21 | "no-console": 0, 22 | "no-tabs": 0, 23 | "indent": 0, 24 | "no-use-before-define": [2, "nofunc"], 25 | "valid-jsdoc": 0, 26 | "require-jsdoc": 0, 27 | 28 | "import/no-extraneous-dependencies": 0, 29 | "import/no-unresolved": 0, 30 | "import/extensions": 0, 31 | 32 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 33 | "react/prop-types": 0, 34 | "react/no-danger": 0, 35 | 36 | "jsx-a11y/anchor-is-valid": 0, 37 | 38 | "no-mixed-operators": 0, 39 | }, 40 | 41 | "globals": { 42 | "graphql": true, 43 | "module": true, 44 | "require": true, 45 | "__dirname": true, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Vibert Thio 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const path = require('path'); 4 | const BrowserSyncPlugin = require('browser-sync-webpack-plugin'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const common = require('./webpack.common.config.js'); 7 | 8 | // Dashboard 9 | const Dashboard = require('webpack-dashboard'); 10 | const DashboardPlugin = require('webpack-dashboard/plugin'); 11 | const dashboard = new Dashboard(); 12 | 13 | module.exports = merge(common, { 14 | plugins: [ 15 | new DashboardPlugin(dashboard.setData), 16 | new webpack.NamedModulesPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new BrowserSyncPlugin( 19 | { 20 | host: 'localhost', 21 | port: 3001, 22 | proxy: 'http://localhost:8080/', 23 | logLevel: "silent", 24 | files: [ 25 | { 26 | match: ['**/*.html'], 27 | fn: (event) => { 28 | if (event === 'change') { 29 | const bs = require('browser-sync').get('bs-webpack-plugin'); 30 | bs.reload(); 31 | } 32 | }, 33 | }, 34 | ], 35 | }, 36 | { reload: false } 37 | ), 38 | ], 39 | devServer: { 40 | hot: false, // Tell the dev-server we're using HMR 41 | quiet: true, 42 | contentBase: path.resolve(__dirname, 'public'), 43 | publicPath: '/', 44 | }, 45 | watch: true, 46 | // devtool: 'cheap-eval-source-map', 47 | }); 48 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | function lerp (low, high, from, to, v) { 2 | const ratio = (v - low) / (high - low); 3 | return from + (to - from) * ratio; 4 | } 5 | 6 | function clamp(value, min, max) { 7 | return Math.max(min, Math.min(max, value)); 8 | } 9 | 10 | /** 11 | * A linear interpolator for hexadecimal colors 12 | * @param {String} a 13 | * @param {String} b 14 | * @param {Number} amount 15 | * @example 16 | * // returns #7F7F7F 17 | * lerpColor('#000000', '#ffffff', 0.5) 18 | * @returns {String} 19 | */ 20 | function lerpColor(a, b, amount) { 21 | var ah = +a.replace('#', '0x'), 22 | bh = +b.replace('#', '0x'), 23 | ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff, 24 | br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff, 25 | rr = ar + amount * (br - ar), 26 | rg = ag + amount * (bg - ag), 27 | rb = ab + amount * (bb - ab); 28 | 29 | return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1); 30 | } 31 | 32 | function roundedRect(ctx, x, y, width, height, radius) { 33 | ctx.beginPath(); 34 | ctx.moveTo(x, y + radius); 35 | ctx.lineTo(x, y + height - radius); 36 | ctx.arcTo(x, y + height, x + radius, y + height, radius); 37 | ctx.lineTo(x + width - radius, y + height); 38 | ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius); 39 | ctx.lineTo(x + width, y + radius); 40 | ctx.arcTo(x + width, y, x + width - radius, y, radius); 41 | ctx.lineTo(x + radius, y); 42 | ctx.arcTo(x, y, x, y + radius, radius); 43 | ctx.fill(); 44 | } 45 | 46 | export { 47 | lerp, 48 | clamp, 49 | lerpColor, 50 | roundedRect, 51 | }; 52 | -------------------------------------------------------------------------------- /src/libs/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){function h(a){c.appendChild(a.dom);return a}function k(a){for(var d=0;de+1E3&&(r.update(1E3*a/(c-e),100),e=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){g=this.end()},domElement:c,setMode:k}}; 4 | Stats.Panel=function(h,k,l){var c=Infinity,g=0,e=Math.round,a=e(window.devicePixelRatio||1),r=80*a,f=48*a,t=3*a,u=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=f;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,f);b.fillStyle=k;b.fillText(h,t,u);b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(f, 5 | v){c=Math.min(c,f);g=Math.max(g,f);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=k;b.fillText(e(f)+" "+h+" ("+e(c)+"-"+e(g)+")",t,u);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,e((1-f/v)*p))}}};"object"===typeof module&&(module.exports=Stats); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drum-vae-client-template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "webpack-dev-server --config ./webpack.dev.config.js", 9 | "prebuild": "yarn run clean", 10 | "clean": "rimraf public", 11 | "build": "webpack -d --config ./webpack.prod.config.js --colors", 12 | "start": "serve public/", 13 | "lint": "eslint ./src", 14 | "predeploy": "npm run build", 15 | "deploy": "gh-pages -d public" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@magenta/music": "^1.4.0", 22 | "glsl-noise": "^0.0.0", 23 | "glslify-import-loader": "^0.1.1", 24 | "gsap": "^1.19.1", 25 | "noisejs": "^2.1.0", 26 | "normalize-css": "^2.3.1", 27 | "rc-slider": "^8.6.3", 28 | "react": "^16.2.0", 29 | "react-dom": "^16.2.0", 30 | "react-tap-event-plugin": "^3.0.2", 31 | "startaudiocontext": "^1.2.1", 32 | "three": "^0.88.0", 33 | "tonal-chord": "^2.1.0", 34 | "tone": "^0.11.11", 35 | "uuid": "^3.2.1" 36 | }, 37 | "devDependencies": { 38 | "autoprefixer": "^7.2.4", 39 | "babel-core": "^6.24.1", 40 | "babel-eslint": "^8.0.3", 41 | "babel-loader": "^7.0.0", 42 | "babel-plugin-transform-class-properties": "^6.24.1", 43 | "babel-plugin-transform-runtime": "^6.23.0", 44 | "babel-preset-babili": "^0.1.4", 45 | "babel-preset-env": "^1.4.0", 46 | "babel-preset-react": "^6.24.1", 47 | "browser-sync": "^2.18.8", 48 | "browser-sync-webpack-plugin": "^1.1.4", 49 | "copy-webpack-plugin": "^4.3.1", 50 | "css-loader": "^0.28.7", 51 | "eslint": "^4.13.0", 52 | "eslint-config-airbnb": "^16.1.0", 53 | "eslint-plugin-import": "^2.8.0", 54 | "eslint-plugin-jsx-a11y": "^6.0.2", 55 | "eslint-plugin-react": "^7.5.1", 56 | "extract-text-webpack-plugin": "2.1.2", 57 | "file-loader": "^1.1.6", 58 | "gh-pages": "^1.1.0", 59 | "glslify-loader": "^1.0.2", 60 | "html-webpack-plugin": "^2.30.1", 61 | "node-sass": "^4.7.2", 62 | "postcss-loader": "^2.0.10", 63 | "prettier": "^1.9.1", 64 | "raw-loader": "^0.5.1", 65 | "rimraf": "^2.6.2", 66 | "sass-loader": "^6.0.6", 67 | "style-loader": "^0.19.0", 68 | "three-obj-loader": "^1.1.3", 69 | "uglifyjs-webpack-plugin": "^1.1.4", 70 | "url-loader": "^0.6.2", 71 | "webpack": "^2.5.1", 72 | "webpack-dashboard": "^1.0.2", 73 | "webpack-dev-server": "^2.4.5", 74 | "webpack-merge": "^4.1.1" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/music/sound.js: -------------------------------------------------------------------------------- 1 | import Tone, { Transport, Sequence, Part, Event } from 'tone'; 2 | import StartAudioContext from 'startaudiocontext'; 3 | 4 | import beepSound from './effect/beep.wav'; 5 | import wrongSound from './effect/wrong.wav'; 6 | import correctSound from './effect/correct.wav'; 7 | import endSound from './effect/end.wav'; 8 | import transitionSound from './effect/transition.wav'; 9 | 10 | export default class Sound { 11 | constructor(app) { 12 | this.app = app; 13 | StartAudioContext(Tone.context); 14 | this.currentIndex = 0; 15 | this.beat = 0; 16 | this.matrix = []; 17 | this.melodies = []; 18 | this.melodiesIndex = 0; 19 | this.chords = []; 20 | this.section = []; 21 | this.barIndex = 0; 22 | this.sectionIndex = 0; 23 | this.noteOn = -1; 24 | this.loop = false; 25 | 26 | this.comp = new Tone.PolySynth(6, Tone.Synth, { 27 | "oscillator": { 28 | "partials": [0, 2, 3, 4], 29 | } 30 | }).toMaster(); 31 | 32 | this.synth = new Tone.PolySynth(3, Tone.Synth, { 33 | "oscillator": { 34 | // "type": "fatsawtooth", 35 | "type": "triangle8", 36 | // "type": "square", 37 | "count": 1, 38 | "spread": 30, 39 | }, 40 | "envelope": { 41 | "attack": 0.01, 42 | "decay": 0.1, 43 | "sustain": 0.5, 44 | "release": 0.4, 45 | "attackCurve": "exponential" 46 | }, 47 | }).toMaster(); 48 | 49 | this.effects = []; 50 | this.effects[0] = new Tone.Player(beepSound).toMaster(); 51 | this.effects[1] = new Tone.Player(wrongSound).toMaster(); 52 | this.effects[2] = new Tone.Player(correctSound).toMaster(); 53 | this.effects[3] = new Tone.Player(endSound).toMaster(); 54 | this.effects[4] = new Tone.Player(transitionSound).toMaster(); 55 | 56 | this.initTable(); 57 | 58 | Transport.bpm.value = 150; 59 | 60 | Transport.start(); 61 | } 62 | 63 | initTable() { 64 | this.section = new Array(4).fill(new Array(48).fill(new Array(128).fill(0))); 65 | } 66 | 67 | updateMelodies(m) { 68 | this.melodies = m; 69 | const notes = m[this.melodiesIndex].notes.map(note => { 70 | const s = note.quantizedStartStep; 71 | return { 72 | 'time': `${Math.floor(s / 16)}:${Math.floor(s / 4) % 4}:${(s % 4)}`, 73 | 'note': Tone.Frequency(note.pitch, 'midi') 74 | }; 75 | }); 76 | if (this.part) { 77 | this.part.stop(); 78 | } 79 | this.part = new Part((time, value) => { 80 | this.synth.triggerAttackRelease(value.note, "8n", time); 81 | }, notes); 82 | 83 | this.part.loop = 1; 84 | this.part.loopEnd = '2:0:0'; 85 | } 86 | 87 | changeMelody(i) { 88 | this.melodiesIndex = i; 89 | const notes = this.melodies[this.melodiesIndex].notes.map(note => { 90 | const s = note.quantizedStartStep; 91 | return { 92 | 'time': `${Math.floor(s / 16)}:${Math.floor(s / 4) % 4}:${(s % 4)}`, 93 | 'note': Tone.Frequency(note.pitch, 'midi') 94 | }; 95 | }); 96 | if (this.part) { 97 | this.part.stop(); 98 | } 99 | this.part = new Part((time, value) => { 100 | this.synth.triggerAttackRelease(value.note, "8n", time); 101 | }, notes); 102 | 103 | this.part.loop = 1; 104 | this.part.loopEnd = '2:0:0'; 105 | } 106 | 107 | changeBpm(b) { 108 | Transport.bpm.value = b; 109 | } 110 | 111 | stop() { 112 | 113 | this.part.stop(); 114 | this.synth.releaseAll(); 115 | } 116 | 117 | start() { 118 | this.noteOn = -1; 119 | this.stop(); 120 | this.part.start(); 121 | this.part.stop('+2m'); 122 | 123 | if (this.stopEvent) { 124 | this.stopEvent.dispose(); 125 | } 126 | this.stopEvent = new Event(time => { 127 | this.app.stop(); 128 | }); 129 | this.stopEvent.start('+2m'); 130 | } 131 | 132 | trigger() { 133 | if (this.part.state === 'started') { 134 | this.stop(); 135 | return false; 136 | } 137 | this.start(); 138 | return true; 139 | } 140 | 141 | triggerSoundEffect(index = 0) { 142 | if (index > -1 && index < this.effects.length) { 143 | this.effects[index].start(); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /webpack.common.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | module.exports = { 8 | entry: { 9 | main: './src/index.js', 10 | }, 11 | output: { 12 | path: path.resolve(__dirname, 'public/'), 13 | publicPath: './', 14 | filename: './js/[name].[hash].bundle.js', 15 | }, 16 | resolve: { 17 | extensions: ['.js', '.jsx'], 18 | alias: { 19 | libs: path.resolve(__dirname, 'src/libs'), 20 | utils: path.resolve(__dirname, 'src/utils'), 21 | 'three/loaders': path.join(__dirname, 'node_modules/three/examples/js/loaders'), 22 | 'three/controls': path.join(__dirname, 'node_modules/three/examples/js/controls'), 23 | }, 24 | }, 25 | module: { 26 | loaders: [ 27 | { 28 | test: /\.jsx?$/, 29 | loader: 'babel-loader', 30 | exclude: /(node_modules|public\/)/, 31 | }, 32 | { 33 | test: /\.css$/, 34 | use: ExtractTextPlugin.extract({ 35 | fallback: 'style-loader', 36 | use: [ 37 | { 38 | loader: 'css-loader', 39 | query: { 40 | modules: true, 41 | sourceMap: true, 42 | localIdentName: '[name]__[local]___[hash:base64:5]', 43 | }, 44 | }, 45 | 'postcss-loader', 46 | ], 47 | }), 48 | exclude: ['node_modules'], 49 | }, 50 | { 51 | test: /\.scss$/, 52 | exclude: /node_modules/, 53 | use: ExtractTextPlugin.extract({ 54 | fallback: 'style-loader', 55 | 56 | // Could also be write as follow: 57 | // use: 'css-loader?modules&importLoader=2&sourceMap&localIdentName=[name]__[local]___[hash:base64:5]!sass-loader' 58 | use: [ 59 | { 60 | loader: 'css-loader', 61 | query: { 62 | modules: true, 63 | sourceMap: true, 64 | importLoaders: 2, 65 | localIdentName: '[name]__[local]___[hash:base64:5]', 66 | }, 67 | }, 68 | 'postcss-loader', 69 | 'sass-loader', 70 | ], 71 | }), 72 | }, 73 | { 74 | test: /\.(glsl|frag|vert)$/, 75 | loader: 'glslify-import-loader', 76 | exclude: /node_modules/, 77 | }, 78 | { 79 | test: /\.(glsl|frag|vert)$/, 80 | loader: 'raw-loader', 81 | exclude: /node_modules/, 82 | }, 83 | { 84 | test: /\.(glsl|frag|vert)$/, 85 | loader: 'glslify-loader', 86 | exclude: /node_modules/, 87 | }, 88 | { 89 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 90 | exclude: /(node_modules|bower_components)/, 91 | loader: "file-loader" 92 | }, 93 | { 94 | test: /\.(woff|woff2)$/, 95 | exclude: /(node_modules|bower_components)/, 96 | loader: "url-loader?prefix=font/&limit=5000&name=assets/fonts/[hash].[ext]" 97 | }, 98 | { 99 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 100 | exclude: /(node_modules|bower_components)/, 101 | loader: "url-loader?limit=10000&mimetype=application/octet-stream&name=assets/fonts/[hash].[ext]" 102 | }, 103 | { 104 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 105 | exclude: /(node_modules|bower_components)/, 106 | loader: "url-loader?limit=10000&mimetype=image/svg+xml" 107 | }, 108 | { 109 | test: /\.gif/, 110 | exclude: /(node_modules|bower_components)/, 111 | loader: "url-loader?limit=10000&mimetype=image/gif&name=assets/images/[hash].[ext]" 112 | }, 113 | { 114 | test: /\.jpg/, 115 | exclude: /(node_modules|bower_components)/, 116 | loader: "url-loader?limit=10000&mimetype=image/jpg&name=assets/images/[hash].[ext]" 117 | }, 118 | { 119 | test: /\.png/, 120 | exclude: /(node_modules|bower_components)/, 121 | loader: "url-loader?limit=10000&mimetype=image/png&name=assets/images/[hash].[ext]" 122 | }, 123 | { 124 | test: /\.ico$/, 125 | loader: 'file-loader?name=[name].[ext]' // <-- retain original file name 126 | }, 127 | { 128 | test: /\.mp3$/, 129 | exclude: /(node_modules|bower_components)/, 130 | loader: 'file-loader?name=[name].[ext]' 131 | }, 132 | { 133 | test: /\.wav$/, 134 | exclude: /(node_modules|bower_components)/, 135 | loader: 'file-loader?name=[name].[ext]' 136 | }, 137 | { 138 | test: /\.obj$/, 139 | exclude: /(node_modules|bower_components)/, 140 | loader: "file-loader" 141 | }, 142 | { 143 | test: /\.mtl$/, 144 | exclude: /(node_modules|bower_components)/, 145 | loader: "file-loader" 146 | }, 147 | ], 148 | }, 149 | plugins: [ 150 | new webpack.ProvidePlugin({ 151 | 'THREE': 'three', 152 | }), 153 | new ExtractTextPlugin("style.css"), 154 | new HtmlWebpackPlugin({ 155 | template: './src/template.html', 156 | files: { 157 | js: ['bundle.js'], 158 | }, 159 | filename: 'index.html', 160 | }), 161 | new CopyWebpackPlugin([ 162 | { from: 'src/assets/looop.ico' }, 163 | { from: 'src/checkpoints', to: 'checkpoints', toType: 'dir' }, 164 | ]), 165 | ], 166 | }; 167 | -------------------------------------------------------------------------------- /src/utils/questions.js: -------------------------------------------------------------------------------- 1 | const questions = [ 2 | 3 | // 1. 4 | { 5 | melodies: [ 6 | 'Arpeggiated', 7 | 'Twinkle', 8 | ], 9 | numInterpolations: 5, 10 | answers: [ 11 | { 12 | index: 0, 13 | ans: false, 14 | show: true, 15 | }, 16 | { 17 | index: -1, 18 | ans: true, 19 | show: true, 20 | }, 21 | { 22 | index: -1, 23 | ans: true, 24 | show: true, 25 | }, 26 | { 27 | index: 3, 28 | ans: false, 29 | show: true, 30 | }, 31 | { 32 | index: 4, 33 | ans: false, 34 | show: true, 35 | }, 36 | ], 37 | options: [ 38 | { 39 | index: 1, 40 | show: true, 41 | }, 42 | { 43 | index: 2, 44 | show: true, 45 | }, 46 | ], 47 | }, 48 | 49 | // 2. 50 | { 51 | melodies: [ 52 | 'Sparse', 53 | 'Bounce', 54 | ], 55 | numInterpolations: 5, 56 | answers: [ 57 | { 58 | index: 0, 59 | ans: false, 60 | show: true, 61 | }, 62 | { 63 | index: -1, 64 | ans: true, 65 | show: true, 66 | }, 67 | { 68 | index: -1, 69 | ans: true, 70 | show: true, 71 | }, 72 | { 73 | index: -1, 74 | ans: true, 75 | show: true, 76 | }, 77 | { 78 | index: 4, 79 | ans: false, 80 | show: true, 81 | }, 82 | ], 83 | options: [ 84 | { 85 | index: 2, 86 | show: false, 87 | }, 88 | { 89 | index: 1, 90 | show: true, 91 | }, 92 | { 93 | index: 3, 94 | show: true, 95 | }, 96 | ], 97 | }, 98 | 99 | // 3. 100 | { 101 | melodies: [ 102 | 'Arpeggiated', 103 | 'Bounce', 104 | ], 105 | numInterpolations: 7, 106 | answers: [ 107 | { 108 | index: 0, 109 | ans: false, 110 | show: true, 111 | }, 112 | { 113 | index: -1, 114 | ans: true, 115 | show: true, 116 | }, 117 | { 118 | index: -1, 119 | ans: true, 120 | show: true, 121 | }, 122 | { 123 | index: -1, 124 | ans: true, 125 | show: true, 126 | }, 127 | { 128 | index: 4, 129 | ans: false, 130 | show: true, 131 | }, 132 | { 133 | index: -1, 134 | ans: true, 135 | show: true, 136 | }, 137 | { 138 | index: 6, 139 | ans: false, 140 | show: true, 141 | }, 142 | ], 143 | options: [ 144 | { 145 | index: 5, 146 | show: true, 147 | }, 148 | { 149 | index: 1, 150 | show: false, 151 | }, 152 | { 153 | index: 3, 154 | show: false, 155 | }, 156 | { 157 | index: 2, 158 | show: true, 159 | }, 160 | ], 161 | }, 162 | 163 | // 4. 164 | { 165 | melodies: [ 166 | 'Sparse', 167 | 'Bounce', 168 | ], 169 | numInterpolations: 7, 170 | answers: [ 171 | { 172 | index: 0, 173 | ans: false, 174 | show: true, 175 | }, 176 | { 177 | index: -1, 178 | ans: true, 179 | show: true, 180 | }, 181 | { 182 | index: -1, 183 | ans: true, 184 | show: true, 185 | }, 186 | { 187 | index: -1, 188 | ans: true, 189 | show: true, 190 | }, 191 | { 192 | index: -1, 193 | ans: true, 194 | show: true, 195 | }, 196 | { 197 | index: -1, 198 | ans: true, 199 | show: true, 200 | }, 201 | { 202 | index: 6, 203 | ans: false, 204 | show: true, 205 | }, 206 | ], 207 | options: [ 208 | { 209 | index: 4, 210 | show: true, 211 | }, 212 | { 213 | index: 3, 214 | show: false, 215 | }, 216 | { 217 | index: 5, 218 | show: true, 219 | }, 220 | { 221 | index: 1, 222 | show: false, 223 | }, 224 | { 225 | index: 2, 226 | show: true, 227 | }, 228 | ], 229 | }, 230 | 231 | // 5. 232 | { 233 | melodies: [ 234 | 'Melody 1', 235 | 'Twinkle', 236 | ], 237 | numInterpolations: 10, 238 | answers: [ 239 | { 240 | index: 0, 241 | ans: false, 242 | show: true, 243 | }, 244 | { 245 | index: 1, 246 | ans: false, 247 | show: true, 248 | }, 249 | { 250 | index: -1, 251 | ans: true, 252 | show: true, 253 | }, 254 | { 255 | index: -1, 256 | ans: true, 257 | show: true, 258 | }, 259 | { 260 | index: 4, 261 | ans: false, 262 | show: true, 263 | }, 264 | { 265 | index: -1, 266 | ans: true, 267 | show: true, 268 | }, 269 | { 270 | index: -1, 271 | ans: true, 272 | show: true, 273 | }, 274 | { 275 | index: -1, 276 | ans: true, 277 | show: true, 278 | }, 279 | { 280 | index: -1, 281 | ans: true, 282 | show: true, 283 | }, 284 | { 285 | index: 9, 286 | ans: false, 287 | show: true, 288 | }, 289 | ], 290 | options: [ 291 | { 292 | index: 7, 293 | show: false, 294 | }, 295 | { 296 | index: 3, 297 | show: true, 298 | }, 299 | { 300 | index: 8, 301 | show: true, 302 | }, 303 | { 304 | index: 6, 305 | show: true, 306 | }, 307 | { 308 | index: 5, 309 | show: false, 310 | }, 311 | { 312 | index: 2, 313 | show: false, 314 | }, 315 | ], 316 | }, 317 | 318 | // 6. 319 | { 320 | melodies: [ 321 | 'Arpeggiated', 322 | 'Melody 1', 323 | ], 324 | numInterpolations: 10, 325 | answers: [ 326 | { 327 | index: 0, 328 | ans: false, 329 | show: true, 330 | }, 331 | { 332 | index: 1, 333 | ans: false, 334 | show: true, 335 | }, 336 | { 337 | index: -1, 338 | ans: true, 339 | show: true, 340 | }, 341 | { 342 | index: -1, 343 | ans: true, 344 | show: true, 345 | }, 346 | { 347 | index: -1, 348 | ans: true, 349 | show: true, 350 | }, 351 | { 352 | index: -1, 353 | ans: true, 354 | show: true, 355 | }, 356 | { 357 | index: -1, 358 | ans: true, 359 | show: true, 360 | }, 361 | { 362 | index: -1, 363 | ans: true, 364 | show: true, 365 | }, 366 | { 367 | index: 8, 368 | ans: false, 369 | show: true, 370 | }, 371 | { 372 | index: 9, 373 | ans: false, 374 | show: true, 375 | }, 376 | ], 377 | options: [ 378 | { 379 | index: 4, 380 | // show: true, 381 | show: false, 382 | }, 383 | { 384 | index: 7, 385 | // show: true, 386 | show: false, 387 | }, 388 | { 389 | index: 3, 390 | // show: true, 391 | show: false, 392 | }, 393 | { 394 | index: 6, 395 | // show: true, 396 | show: false, 397 | }, 398 | { 399 | index: 5, 400 | // show: true, 401 | show: false, 402 | }, 403 | { 404 | index: 2, 405 | // show: true, 406 | show: false, 407 | }, 408 | ], 409 | }, 410 | 411 | ]; 412 | 413 | 414 | function getQuestions(index) { 415 | return questions[index % questions.length]; 416 | } 417 | 418 | function checkEnd(index) { 419 | return (index === questions.length - 1); 420 | } 421 | 422 | export { 423 | questions, 424 | getQuestions, 425 | checkEnd, 426 | }; 427 | -------------------------------------------------------------------------------- /src/renderer/pianoroll-grid.js: -------------------------------------------------------------------------------- 1 | import { lerpColor, roundedRect } from '../utils/utils'; 2 | import { presetMelodies } from '../music/clips'; 3 | 4 | export default class PianorollGrid { 5 | 6 | constructor(renderer, ysr = 0, xsr = 0, fixed = -1, ans = true, dynamic = true) { 7 | this.matrix = []; 8 | this.noteList = []; 9 | this.renderer = renderer; 10 | this.ans = ans; 11 | this.fixed = fixed; 12 | this.sectionIndex = fixed; 13 | this.frameRatio = 1.1; 14 | this.dynamic = dynamic; 15 | 16 | this.gridWidth = 0; 17 | this.gridHeight = 0; 18 | this.gridXShift = 0; 19 | this.gridYShift = 0; 20 | this.dragX = 0; 21 | this.dragY = 0; 22 | this.noteOnColor = 'rgba(255, 255, 255, 1.0)'; 23 | 24 | this.yShiftRatio = ysr; 25 | this.xShiftRatio = xsr; 26 | 27 | // animation 28 | this.currentNoteIndex = -1; 29 | this.currentNoteYShift = 0; 30 | this.currentChordIndex = -1; 31 | this.currentChordYShift = 0; 32 | this.newSectionYShift = 1; 33 | 34 | // instruction 35 | this.showingInstruction = false; 36 | } 37 | 38 | update(w, h) { 39 | this.gridWidth = w; 40 | this.gridHeight = h; 41 | this.gridYShift = h * this.yShiftRatio * 0.7; 42 | this.gridXShift = w * this.xShiftRatio * 0.7; 43 | } 44 | 45 | decodeMatrix(mat) { 46 | let noteList = new Array(mat.length).fill([]).map((l, i) => { 47 | let list = []; 48 | let noteOn = false; 49 | let currentNote = -1; 50 | let currentStart = 0; 51 | let currentEnd = 0; 52 | // flatten 53 | let section = [].concat.apply([], mat[i].slice()).forEach((note, j) => { 54 | if (note !== currentNote) { 55 | 56 | // current note end 57 | if (noteOn && currentNote !== -1) { 58 | currentEnd = j - 1; 59 | list = list.concat([[currentNote, currentStart, currentEnd]]); 60 | } 61 | 62 | currentNote = note; 63 | 64 | // new note start 65 | if (note !== -1) { 66 | noteOn = true; 67 | currentStart = j; 68 | } 69 | } else if ((j === (mat[0][0].length * mat[0].length - 1)) && note !== -1) { 70 | // last one 71 | currentEnd = j; 72 | list = list.concat([[currentNote, currentStart, currentEnd]]) 73 | } 74 | }); 75 | return list; 76 | }); 77 | this.noteList = noteList; 78 | 79 | } 80 | 81 | draw(ctx, w, h, frameOnly = false) { 82 | this.update(w, h) 83 | this.updateYShift(); 84 | 85 | ctx.save(); 86 | ctx.translate(this.gridXShift, this.gridYShift); 87 | 88 | this.drawFrame( 89 | ctx, 90 | this.gridWidth * this.frameRatio, 91 | this.gridHeight * this.frameRatio, 92 | ); 93 | 94 | ctx.translate(this.dragX, this.dragY); 95 | 96 | 97 | this.drawBling( 98 | ctx, 99 | this.gridWidth * this.frameRatio - 15, 100 | this.gridHeight * this.frameRatio - 12, 101 | ); 102 | 103 | let id = this.getId(); 104 | 105 | if (!frameOnly && id !== -1) { 106 | // if (((this.ans) && (!this.renderer.app.answers[this.fixed].show)) || 107 | // ((!this.ans) && (!this.renderer.app.options[this.fixed].show))) { 108 | // this.drawCross(ctx, w, h); 109 | // } 110 | 111 | ctx.save(); 112 | ctx.translate(-w * 0.5, -h * 0.5); 113 | 114 | 115 | // roll 116 | const nOfBars = 2; 117 | const nOfBeats = 16; 118 | 119 | const wStep = w / (nOfBars * nOfBeats); 120 | const p = this.renderer.progress; 121 | 122 | const hStep = h / 64; 123 | 124 | if (((this.ans) && (this.renderer.app.answers[this.fixed].show)) || 125 | ((!this.ans) && (this.renderer.app.options[this.fixed].show))) { 126 | 127 | const melody = this.renderer.melodies[id]; 128 | melody.notes.forEach((item, index) => { 129 | const { pitch, quantizedStartStep, quantizedEndStep } = item; 130 | const y = 56 - (pitch - 40); 131 | let wStepDisplay = wStep * (1 - this.newSectionYShift); 132 | ctx.save(); 133 | ctx.strokeStyle = 'none'; 134 | ctx.translate(quantizedStartStep * wStep, y * hStep); 135 | 136 | if ((p * (nOfBars * nOfBeats)) >= quantizedStartStep 137 | && (p * (nOfBars * nOfBeats)) <= quantizedEndStep 138 | && this.checkCurrent() 139 | && this.isPlaying()) { 140 | if (this.currentNoteIndex !== index) { 141 | // change pitch 142 | this.currentNoteYShift = 1; 143 | this.currentNoteIndex = index; 144 | } 145 | ctx.fillStyle = '#FFF'; 146 | ctx.fillText(pitch, 5, -8); 147 | ctx.fillStyle = '#F00'; 148 | ctx.translate(0, this.currentNoteYShift * -2); 149 | // stretch 150 | // wStepDisplay *= (1 + this.currentNoteYShift * 0.1) 151 | } else { 152 | ctx.fillStyle = this.noteOnColor; 153 | } 154 | 155 | ctx.fillRect(0, 0, wStepDisplay * (quantizedEndStep - quantizedStartStep - 0.2), hStep); 156 | 157 | ctx.restore(); 158 | }); 159 | 160 | } else { 161 | ctx.save(); 162 | ctx.translate(w * 0.5, h * 0.5); 163 | this.drawCross(ctx, w, h); 164 | ctx.restore(); 165 | 166 | } 167 | 168 | // progress 169 | if (this.checkCurrent() 170 | && (this.isPlaying())) { 171 | // ctx.translate((b % (nOfBars * nOfBeats)) * wStep, 0); 172 | ctx.translate(w * p, 0); 173 | ctx.strokeStyle = '#F00'; 174 | ctx.beginPath(); 175 | ctx.moveTo(0, 0); 176 | ctx.lineTo(0, h); 177 | ctx.stroke(); 178 | } 179 | 180 | ctx.restore(); 181 | } 182 | 183 | ctx.restore(); 184 | } 185 | 186 | isPlaying() { 187 | return this.renderer.playing; 188 | } 189 | 190 | checkCurrent() { 191 | return (this.renderer.melodiesIndex === this.getId()); 192 | } 193 | 194 | updateYShift() { 195 | this.currentNoteYShift *= 0.9; 196 | this.currentChordYShift *= 0.9; 197 | this.newSectionYShift *= 0.9; 198 | } 199 | 200 | triggerStartAnimation() { 201 | this.newSectionYShift = 1; 202 | } 203 | 204 | drawFrame(ctx, w, h) { 205 | const { hoverIndex, hoverAns } = this.renderer.draggingState; 206 | const on = (hoverIndex === this.fixed) && (hoverAns === this.ans); 207 | const ratio = this.dynamic ? 0.08 : 0.06; 208 | const unit = this.renderer.h * ratio; 209 | let size = 0.5; 210 | if (this.dynamic) { 211 | size = 0.5 + Math.sin(this.renderer.frameCount * 0.04) * 0.02 212 | if (on) { 213 | size = 0.45; 214 | } 215 | 216 | if (this.ans) { 217 | if (this.renderer.app.answers[this.fixed].index !== -1) { 218 | size = 0.5; 219 | } 220 | } else { 221 | if (this.renderer.app.options[this.fixed].index === -1) { 222 | size = 0.35; 223 | } 224 | } 225 | } 226 | 227 | ctx.save(); 228 | 229 | if (this.dynamic) { 230 | if (this.ans) { 231 | ctx.strokeStyle = '#f39c12'; 232 | } else { 233 | ctx.strokeStyle = '#2ecc71'; 234 | } 235 | } else { 236 | ctx.strokeStyle = '#FFF'; 237 | } 238 | 239 | const { waitingNext, answerCorrect } = this.renderer.app.state; 240 | if (waitingNext) { 241 | if (answerCorrect) { 242 | ctx.strokeStyle = '#2ecc71'; 243 | } else { 244 | if (this.ans) { 245 | if (this.fixed !== this.renderer.app.answers[this.fixed].index) { 246 | ctx.strokeStyle = '#e74c3c'; 247 | } else if (this.renderer.app.answers[this.fixed].ans) { 248 | ctx.strokeStyle = '#2ecc71'; 249 | } 250 | } else { 251 | ctx.strokeStyle = '#FFF'; 252 | } 253 | } 254 | size = 0.5; 255 | } 256 | 257 | ctx.beginPath() 258 | ctx.moveTo(size * w, size * h - unit); 259 | ctx.lineTo(size * w, size * h); 260 | ctx.lineTo(size * w - unit, size * h); 261 | ctx.stroke(); 262 | 263 | ctx.beginPath() 264 | ctx.moveTo(-size * w, size * h - unit); 265 | ctx.lineTo(-size * w, size * h); 266 | ctx.lineTo(-size * w + unit, size * h); 267 | ctx.stroke(); 268 | 269 | ctx.beginPath() 270 | ctx.moveTo(size * w, -size * h + unit); 271 | ctx.lineTo(size * w, -size * h); 272 | ctx.lineTo(size * w - unit, -size * h); 273 | ctx.stroke(); 274 | 275 | ctx.beginPath() 276 | ctx.moveTo(-size * w, -size * h + unit); 277 | ctx.lineTo(-size * w, -size * h); 278 | ctx.lineTo(-size * w + unit, -size * h); 279 | ctx.stroke(); 280 | 281 | ctx.restore(); 282 | } 283 | 284 | drawCross(ctx, w, h) { 285 | let size = 0.5; 286 | 287 | ctx.save(); 288 | 289 | ctx.strokeStyle = '#FFF'; 290 | ctx.lineWidth = 1.5; 291 | ctx.beginPath() 292 | ctx.moveTo(0.1 * w, 0.1 * h); 293 | ctx.lineTo(-0.1 * w, -0.1 * h); 294 | ctx.stroke(); 295 | 296 | ctx.beginPath() 297 | ctx.moveTo(-0.1 * w, 0.1 * h); 298 | ctx.lineTo(0.1 * w, -0.1 * h); 299 | ctx.stroke(); 300 | 301 | ctx.restore(); 302 | } 303 | 304 | drawBling(ctx, w, h) { 305 | if (this.showingInstruction) { 306 | ctx.save(); 307 | ctx.translate(-0.5 * w, -0.5 * h); 308 | ctx.fillStyle = '#555'; 309 | // ctx.fillStyle = lerpColor( 310 | // '#555555', 311 | // '#AA0000', 312 | // Math.pow( 313 | // Math.sin(this.renderer.frameCount * 0.03), 314 | // 2, 315 | // ), 316 | // ); 317 | roundedRect(ctx, 0, 0, w, h, 5); 318 | ctx.restore(); 319 | } 320 | } 321 | 322 | drawInstructionText(ctx, w, h) { 323 | 324 | } 325 | 326 | changeFixed(i) { 327 | this.fixed = i; 328 | this.sectionIndex = i; 329 | } 330 | 331 | getId() { 332 | let id = this.renderer.app.answers[this.fixed].index; 333 | if (!this.ans) { 334 | id = this.renderer.app.options[this.fixed].index; 335 | } 336 | return id; 337 | } 338 | 339 | 340 | } 341 | -------------------------------------------------------------------------------- /src/music/clips.js: -------------------------------------------------------------------------------- 1 | // generates an array where indices correspond to midi notes 2 | const everyNote = 'C,C#,D,D#,E,F,F#,G,G#,A,A#,B,'.repeat(20).split(',').map(function (x, i) { 3 | return x + '' + Math.floor(i / 12); 4 | }); 5 | 6 | //returns the midi pitch value for the given note. 7 | //returns -1 if not found 8 | function toMidi(note) { 9 | return everyNote.indexOf(note); 10 | } 11 | 12 | const presetMelodies = { 13 | 'Twinkle': { 14 | notes: [ 15 | { pitch: 60, quantizedStartStep: 0, quantizedEndStep: 2 }, 16 | { pitch: 60, quantizedStartStep: 2, quantizedEndStep: 4 }, 17 | { pitch: 67, quantizedStartStep: 4, quantizedEndStep: 6 }, 18 | { pitch: 67, quantizedStartStep: 6, quantizedEndStep: 8 }, 19 | { pitch: 69, quantizedStartStep: 8, quantizedEndStep: 10 }, 20 | { pitch: 69, quantizedStartStep: 10, quantizedEndStep: 12 }, 21 | { pitch: 67, quantizedStartStep: 12, quantizedEndStep: 16 }, 22 | { pitch: 65, quantizedStartStep: 16, quantizedEndStep: 18 }, 23 | { pitch: 65, quantizedStartStep: 18, quantizedEndStep: 20 }, 24 | { pitch: 64, quantizedStartStep: 20, quantizedEndStep: 22 }, 25 | { pitch: 64, quantizedStartStep: 22, quantizedEndStep: 24 }, 26 | { pitch: 62, quantizedStartStep: 24, quantizedEndStep: 26 }, 27 | { pitch: 62, quantizedStartStep: 26, quantizedEndStep: 28 }, 28 | { pitch: 60, quantizedStartStep: 28, quantizedEndStep: 32 }, 29 | ], 30 | quantizationInfo: { stepsPerQuarter: 4 }, 31 | tempos: [{ time: 0, qpm: 120 }], 32 | totalQuantizedSteps: 32, 33 | }, 34 | 'Sparse': { 35 | notes: [ 36 | { pitch: 64, quantizedStartStep: 0, quantizedEndStep: 1 }, 37 | { pitch: 62, quantizedStartStep: 1, quantizedEndStep: 2 }, 38 | { pitch: 64, quantizedStartStep: 2, quantizedEndStep: 3 }, 39 | { pitch: 65, quantizedStartStep: 3, quantizedEndStep: 4 }, 40 | { pitch: 67, quantizedStartStep: 4, quantizedEndStep: 8 }, 41 | { pitch: 60, quantizedStartStep: 16, quantizedEndStep: 17 }, 42 | { pitch: 59, quantizedStartStep: 17, quantizedEndStep: 18 }, 43 | { pitch: 60, quantizedStartStep: 18, quantizedEndStep: 19 }, 44 | { pitch: 62, quantizedStartStep: 19, quantizedEndStep: 20 }, 45 | { pitch: 64, quantizedStartStep: 20, quantizedEndStep: 24 } 46 | ], 47 | quantizationInfo: { stepsPerQuarter: 4 }, 48 | tempos: [{ time: 0, qpm: 120 }], 49 | totalQuantizedSteps: 32, 50 | }, 51 | 'Arpeggiated': { 52 | notes: [ 53 | { pitch: 48, quantizedStartStep: 0, quantizedEndStep: 2 }, 54 | { pitch: 52, quantizedStartStep: 2, quantizedEndStep: 4 }, 55 | { pitch: 55, quantizedStartStep: 4, quantizedEndStep: 6 }, 56 | { pitch: 60, quantizedStartStep: 6, quantizedEndStep: 8 }, 57 | { pitch: 64, quantizedStartStep: 8, quantizedEndStep: 10 }, 58 | { pitch: 67, quantizedStartStep: 10, quantizedEndStep: 12 }, 59 | { pitch: 64, quantizedStartStep: 12, quantizedEndStep: 14 }, 60 | { pitch: 60, quantizedStartStep: 14, quantizedEndStep: 16 }, 61 | { pitch: 57, quantizedStartStep: 16, quantizedEndStep: 18 }, 62 | { pitch: 60, quantizedStartStep: 18, quantizedEndStep: 20 }, 63 | { pitch: 64, quantizedStartStep: 20, quantizedEndStep: 22 }, 64 | { pitch: 69, quantizedStartStep: 22, quantizedEndStep: 24 }, 65 | { pitch: 72, quantizedStartStep: 24, quantizedEndStep: 26 }, 66 | { pitch: 76, quantizedStartStep: 26, quantizedEndStep: 28 }, 67 | { pitch: 72, quantizedStartStep: 28, quantizedEndStep: 30 }, 68 | { pitch: 69, quantizedStartStep: 30, quantizedEndStep: 32 } 69 | ], 70 | quantizationInfo: { stepsPerQuarter: 4 }, 71 | tempos: [{ time: 0, qpm: 120 }], 72 | totalQuantizedSteps: 32, 73 | }, 74 | 'Dense' : { 75 | notes:[ 76 | {pitch: 72, quantizedStartStep: 0, quantizedEndStep: 1}, 77 | {pitch: 75, quantizedStartStep: 1, quantizedEndStep: 2}, 78 | {pitch: 80, quantizedStartStep: 2, quantizedEndStep: 3}, 79 | {pitch: 75, quantizedStartStep: 3, quantizedEndStep: 4}, 80 | {pitch: 84, quantizedStartStep: 4, quantizedEndStep: 5}, 81 | {pitch: 80, quantizedStartStep: 5, quantizedEndStep: 6}, 82 | {pitch: 75, quantizedStartStep: 6, quantizedEndStep: 7}, 83 | {pitch: 72, quantizedStartStep: 7, quantizedEndStep: 8}, 84 | {pitch: 74, quantizedStartStep: 8, quantizedEndStep: 9}, 85 | {pitch: 77, quantizedStartStep: 9, quantizedEndStep: 10}, 86 | {pitch: 82, quantizedStartStep: 10, quantizedEndStep: 11}, 87 | {pitch: 77, quantizedStartStep: 11, quantizedEndStep: 12}, 88 | {pitch: 86, quantizedStartStep: 12, quantizedEndStep: 13}, 89 | {pitch: 82, quantizedStartStep: 13, quantizedEndStep: 14}, 90 | {pitch: 77, quantizedStartStep: 14, quantizedEndStep: 15}, 91 | {pitch: 74, quantizedStartStep: 15, quantizedEndStep: 16}, 92 | {pitch: 75, quantizedStartStep: 16, quantizedEndStep: 17}, 93 | {pitch: 79, quantizedStartStep: 17, quantizedEndStep: 18}, 94 | {pitch: 84, quantizedStartStep: 18, quantizedEndStep: 19}, 95 | {pitch: 79, quantizedStartStep: 19, quantizedEndStep: 20}, 96 | {pitch: 87, quantizedStartStep: 20, quantizedEndStep: 21}, 97 | {pitch: 84, quantizedStartStep: 21, quantizedEndStep: 22}, 98 | {pitch: 79, quantizedStartStep: 22, quantizedEndStep: 23}, 99 | {pitch: 75, quantizedStartStep: 23, quantizedEndStep: 24}, 100 | {pitch: 75, quantizedStartStep: 24, quantizedEndStep: 25}, 101 | {pitch: 79, quantizedStartStep: 25, quantizedEndStep: 26}, 102 | {pitch: 84, quantizedStartStep: 26, quantizedEndStep: 27}, 103 | {pitch: 84, quantizedStartStep: 27, quantizedEndStep: 28}, 104 | {pitch: 87, quantizedStartStep: 28, quantizedEndStep: 29}, 105 | {pitch: 91, quantizedStartStep: 29, quantizedEndStep: 30}, 106 | {pitch: 84, quantizedStartStep: 30, quantizedEndStep: 31}, 107 | {pitch: 91, quantizedStartStep: 31, quantizedEndStep: 32} 108 | ], 109 | quantizationInfo: { stepsPerQuarter: 4 }, 110 | tempos: [{ time: 0, qpm: 120 }], 111 | totalQuantizedSteps: 32, 112 | }, 113 | 'Bounce': { 114 | notes: [ 115 | { pitch: 64, quantizedStartStep: 0, quantizedEndStep: 2 }, 116 | { pitch: 60, quantizedStartStep: 2, quantizedEndStep: 4 }, 117 | { pitch: 64, quantizedStartStep: 4, quantizedEndStep: 6 }, 118 | { pitch: 60, quantizedStartStep: 6, quantizedEndStep: 8 }, 119 | { pitch: 65, quantizedStartStep: 8, quantizedEndStep: 10 }, 120 | { pitch: 60, quantizedStartStep: 10, quantizedEndStep: 12 }, 121 | { pitch: 65, quantizedStartStep: 12, quantizedEndStep: 14 }, 122 | { pitch: 60, quantizedStartStep: 14, quantizedEndStep: 16 }, 123 | { pitch: 67, quantizedStartStep: 16, quantizedEndStep: 18 }, 124 | { pitch: 60, quantizedStartStep: 18, quantizedEndStep: 20 }, 125 | { pitch: 67, quantizedStartStep: 20, quantizedEndStep: 22 }, 126 | { pitch: 60, quantizedStartStep: 22, quantizedEndStep: 24 }, 127 | { pitch: 62, quantizedStartStep: 24, quantizedEndStep: 26 }, 128 | { pitch: 59, quantizedStartStep: 26, quantizedEndStep: 28 }, 129 | { pitch: 62, quantizedStartStep: 28, quantizedEndStep: 30 }, 130 | { pitch: 59, quantizedStartStep: 30, quantizedEndStep: 32 } 131 | ], 132 | quantizationInfo: { stepsPerQuarter: 4 }, 133 | tempos: [{ time: 0, qpm: 120 }], 134 | totalQuantizedSteps: 32, 135 | }, 136 | 'Melody 1' : { 137 | notes: [ 138 | {pitch: toMidi('A3'), quantizedStartStep: 0, quantizedEndStep: 4}, 139 | {pitch: toMidi('D4'), quantizedStartStep: 4, quantizedEndStep: 6}, 140 | {pitch: toMidi('E4'), quantizedStartStep: 6, quantizedEndStep: 8}, 141 | {pitch: toMidi('F4'), quantizedStartStep: 8, quantizedEndStep: 10}, 142 | {pitch: toMidi('D4'), quantizedStartStep: 10, quantizedEndStep: 12}, 143 | {pitch: toMidi('E4'), quantizedStartStep: 12, quantizedEndStep: 16}, 144 | {pitch: toMidi('C4'), quantizedStartStep: 16, quantizedEndStep: 20}, 145 | {pitch: toMidi('D4'), quantizedStartStep: 20, quantizedEndStep: 26}, 146 | {pitch: toMidi('A3'), quantizedStartStep: 26, quantizedEndStep: 28}, 147 | {pitch: toMidi('A3'), quantizedStartStep: 28, quantizedEndStep: 32} 148 | ], 149 | quantizationInfo: { stepsPerQuarter: 4 }, 150 | tempos: [{ time: 0, qpm: 120 }], 151 | totalQuantizedSteps: 32, 152 | }, 153 | 'Melody 2' : { 154 | notes: [ 155 | {pitch: 50, quantizedStartStep: 0, quantizedEndStep: 1}, 156 | {pitch: 53, quantizedStartStep: 1, quantizedEndStep: 2}, 157 | {pitch: 58, quantizedStartStep: 2, quantizedEndStep: 3}, 158 | {pitch: 58, quantizedStartStep: 3, quantizedEndStep: 4}, 159 | {pitch: 58, quantizedStartStep: 4, quantizedEndStep: 5}, 160 | {pitch: 53, quantizedStartStep: 5, quantizedEndStep: 6}, 161 | {pitch: 53, quantizedStartStep: 6, quantizedEndStep: 7}, 162 | {pitch: 53, quantizedStartStep: 7, quantizedEndStep: 8}, 163 | {pitch: 52, quantizedStartStep: 8, quantizedEndStep: 9}, 164 | {pitch: 55, quantizedStartStep: 9, quantizedEndStep: 10}, 165 | {pitch: 60, quantizedStartStep: 10, quantizedEndStep: 11}, 166 | {pitch: 60, quantizedStartStep: 11, quantizedEndStep: 12}, 167 | {pitch: 60, quantizedStartStep: 12, quantizedEndStep: 13}, 168 | {pitch: 60, quantizedStartStep: 13, quantizedEndStep: 14}, 169 | {pitch: 60, quantizedStartStep: 14, quantizedEndStep: 15}, 170 | {pitch: 52, quantizedStartStep: 15, quantizedEndStep: 16}, 171 | {pitch: 57, quantizedStartStep: 16, quantizedEndStep: 17}, 172 | {pitch: 57, quantizedStartStep: 17, quantizedEndStep: 18}, 173 | {pitch: 57, quantizedStartStep: 18, quantizedEndStep: 19}, 174 | {pitch: 65, quantizedStartStep: 19, quantizedEndStep: 20}, 175 | {pitch: 65, quantizedStartStep: 20, quantizedEndStep: 21}, 176 | {pitch: 65, quantizedStartStep: 21, quantizedEndStep: 22}, 177 | {pitch: 57, quantizedStartStep: 22, quantizedEndStep: 23}, 178 | {pitch: 57, quantizedStartStep: 23, quantizedEndStep: 24}, 179 | {pitch: 57, quantizedStartStep: 24, quantizedEndStep: 25}, 180 | {pitch: 57, quantizedStartStep: 25, quantizedEndStep: 26}, 181 | {pitch: 62, quantizedStartStep: 26, quantizedEndStep: 27}, 182 | {pitch: 62, quantizedStartStep: 27, quantizedEndStep: 28}, 183 | {pitch: 65, quantizedStartStep: 28, quantizedEndStep: 29}, 184 | {pitch: 65, quantizedStartStep: 29, quantizedEndStep: 30}, 185 | {pitch: 69, quantizedStartStep: 30, quantizedEndStep: 31}, 186 | {pitch: 69, quantizedStartStep: 31, quantizedEndStep: 32} 187 | ], 188 | quantizationInfo: { stepsPerQuarter: 4 }, 189 | tempos: [{ time: 0, qpm: 120 }], 190 | totalQuantizedSteps: 32, 191 | }, 192 | }; 193 | 194 | export { 195 | presetMelodies, 196 | toMidi, 197 | }; 198 | -------------------------------------------------------------------------------- /src/index.module.scss: -------------------------------------------------------------------------------- 1 | $font-default: monospace, Arial, sans-serif; 2 | // $font-default: Poppins, Arial, sans-serif; 3 | 4 | $link-color: #f39c12; 5 | // $link-color: #29f1c3; 6 | // $link-color: #94EDEE; 7 | 8 | $background-color: rgba(15, 15, 15, 1.0); 9 | 10 | body { 11 | background-color: $background-color; 12 | margin: 0; 13 | overflow: hidden; 14 | } 15 | 16 | .dimming { 17 | animation-name: dim; 18 | animation-duration: 2s; 19 | animation-iteration-count: infinite; 20 | 21 | @keyframes dim { 22 | 0% { opacity: 0.3; } 23 | 50% { opacity: 1.0; } 24 | 100% { opacity: 0.3; } 25 | } 26 | } 27 | 28 | .no-btn-style { 29 | background-color: Transparent; 30 | background-repeat: no-repeat; 31 | border: none; 32 | outline: none; 33 | } 34 | 35 | .links { 36 | position: absolute; 37 | width: 100%; 38 | font-family: $font-default; 39 | font-size: 0.7rem; 40 | text-align: center; 41 | color: #ffffff; 42 | 43 | // transform: scale(1, 1.2); 44 | } 45 | 46 | .links a { 47 | color: #fff; 48 | text-align:center; 49 | letter-spacing: 0.5rem; 50 | text-decoration: none; 51 | 52 | user-select: none; 53 | } 54 | .links a:hover { 55 | color: #fff; 56 | } 57 | 58 | .links p { 59 | color: #fff; 60 | letter-spacing: 0.2rem; 61 | } 62 | 63 | .title { 64 | @extend .links; 65 | top: 1.5rem; 66 | font-size: 1.2rem; 67 | // border: 2px solid $link-color; 68 | // margin-left: auto; 69 | // margin-right: auto; 70 | 71 | 72 | 73 | .link { 74 | position: relative; 75 | 76 | letter-spacing: 0.3rem; 77 | margin-right: 1rem; 78 | margin-left: 1rem; 79 | padding-bottom: 5px; 80 | } 81 | 82 | 83 | button { 84 | position: relative; 85 | top: -25px; 86 | left: 6.5rem; 87 | 88 | img { 89 | width: 1.1rem; 90 | opacity: 0.7; 91 | &:hover{ 92 | opacity: 1.0; 93 | } 94 | } 95 | } 96 | 97 | .tips { 98 | @extend .dimming; 99 | width: 40%; 100 | min-width: 300px; 101 | margin: 0.1rem auto 0.2rem auto; 102 | position: relative; 103 | top: -20px; 104 | 105 | h3 { 106 | margin: 0.1rem; 107 | font-size: 1.2rem; 108 | } 109 | 110 | p { 111 | margin: 0.2rem; 112 | font-size: 1.0rem; 113 | // font-size: 0.7rem; 114 | } 115 | 116 | @media screen and (max-width: 1000px) { 117 | margin: 0.8rem auto; 118 | } 119 | } 120 | 121 | .result { 122 | @extend .dimming; 123 | display: none; 124 | margin-top: 3rem; 125 | font-size: 2.2rem; 126 | font-family: $font-default; 127 | color: white; 128 | } 129 | } 130 | 131 | 132 | // Base Colors 133 | $shade-10: #b33b3b !default; 134 | $shade-1: #d7dcdf !default; 135 | $shade-0: #fff !default; 136 | $teal: #1abc9c !default; 137 | 138 | $range-width: 100% !default; 139 | 140 | $range-handle-color: $shade-10 !default; 141 | $range-handle-color-hover: $teal !default; 142 | $range-handle-size: 10px !default; 143 | 144 | $range-track-color: $shade-1 !default; 145 | $range-track-height: 1.5px !default; 146 | 147 | $range-label-color: $shade-10 !default; 148 | $range-label-width: 60px !default; 149 | 150 | .control { 151 | position: absolute; 152 | display: block; 153 | 154 | width: 100%; 155 | bottom: 4rem; 156 | text-align: center; 157 | 158 | .score { 159 | font-family: $font-default; 160 | font-size: 1.2rem; 161 | margin: 10px auto; 162 | color: white; 163 | 164 | width: 11rem; 165 | display: flex; 166 | flex-direction: row; 167 | justify-content: space-evenly; 168 | 169 | .item { 170 | align-items: center; 171 | width: 0.6rem; 172 | height: 0.6rem; 173 | 174 | .breathing { 175 | animation-name: breath; 176 | animation-duration: 2s; 177 | animation-iteration-count: infinite; 178 | 179 | @keyframes breath { 180 | 0% { width: 0.4rem; height: 0.4rem; } 181 | 50% { width: 0.6rem; height: 0.6rem; } 182 | 100% { width: 0.4rem; height: 0.4rem; } 183 | } 184 | } 185 | .circle { 186 | width: 0.5rem; 187 | height: 0.5rem; 188 | border-radius: 50%; 189 | border: 1.5px solid $link-color; 190 | } 191 | .current { 192 | @extend .dimming; 193 | @extend .circle; 194 | } 195 | .unfinished { 196 | @extend .circle; 197 | } 198 | .correct { 199 | @extend .circle; 200 | border: 1.5px solid $link-color; 201 | background-color: $link-color; 202 | } 203 | .wrong { 204 | @extend .circle; 205 | border: 1.5px solid #c0392b; 206 | background-color: #c0392b; 207 | } 208 | } 209 | } 210 | 211 | 212 | .slider { 213 | // display: block; 214 | display: flex; 215 | justify-content: space-evenly; 216 | width: 30rem; 217 | margin: auto; 218 | 219 | .sendButton { 220 | cursor: pointer; 221 | // z-index: 3; 222 | margin-left: auto; 223 | margin-right: auto; 224 | margin-bottom: 10px; 225 | font-size: 1.2rem; 226 | display: block; 227 | box-sizing: border-box; 228 | width: 205px; 229 | height: 50px; 230 | border: 0; 231 | // border: 2px solid $link-color; 232 | border-bottom: 2px solid $link-color; 233 | border-top: 2px solid $link-color; 234 | background-color: transparent; 235 | // text-transform: uppercase; 236 | // color: $link-color; 237 | color: white; 238 | text-align: center; 239 | // transition: background-color 300ms ease-in-out; 240 | font-family: $font-default; 241 | } 242 | .sendButton:hover { 243 | color: white; 244 | background-color: $link-color; 245 | -webkit-user-select: none; /* Chrome all / Safari all */ 246 | -moz-user-select: none; /* Firefox all */ 247 | -ms-user-select: none; 248 | } 249 | .sendButton:focus { 250 | outline: none; 251 | } 252 | 253 | // button { 254 | // background: none; 255 | // color: inherit; 256 | // border: none; 257 | // padding: 0; 258 | // font: inherit; 259 | // cursor: pointer; 260 | // outline: inherit; 261 | // } 262 | 263 | input[type='range'] { 264 | -webkit-appearance: none; 265 | width: 7rem; 266 | height: $range-track-height; 267 | border-radius: 5px; 268 | outline: none; 269 | 270 | display: inline; 271 | margin: auto 0.5rem; 272 | 273 | // Range Handle 274 | &::-webkit-slider-thumb { 275 | appearance: none; 276 | width: $range-handle-size; 277 | height: $range-handle-size; 278 | border-radius: 40%; 279 | background: $range-handle-color; 280 | cursor: pointer; 281 | transition: background .05s ease-in-out; 282 | 283 | &:hover { 284 | background: $range-handle-color-hover; 285 | } 286 | } 287 | } 288 | } 289 | } 290 | 291 | .foot { 292 | @extend .links; 293 | bottom: 12px; 294 | } 295 | 296 | .overlay { 297 | /* Height & width depends on how you want to reveal the overlay (see JS below) */ 298 | height: 0%; 299 | width: 100%; 300 | position: fixed; /* Stay in place */ 301 | z-index: 1; 302 | left: 0; 303 | top: 0; 304 | background-color: rgb(0, 0, 0); /* Black fallback color */ 305 | background-color: rgba(0, 0, 0, 0.85); /* Black w/opacity */ 306 | overflow-x: hidden; /* Disable horizontal scroll */ 307 | transition: 0.5s; /* 0.5 second transition effect to slide in or slide down the overlay (height or width, depending on reveal) */ 308 | -webkit-transition: 0.5s; 309 | 310 | .intro { 311 | max-width: 450px; 312 | margin: 0rem auto; 313 | 314 | p { 315 | // strong { 316 | // font-size: 3.0rem; 317 | // } 318 | z-index: 3; 319 | font-size: 1.0rem; 320 | letter-spacing: 0.1rem; 321 | line-height: 1.5rem; 322 | 323 | @media screen and (max-width: 650px) { 324 | line-height: 1rem; 325 | font-size: 0.8rem; 326 | // margin: 0rem 2rem; 327 | } 328 | // text-align: center; 329 | font-family: $font-default; 330 | // margin: 10rem 7rem; 331 | // margin: 0rem 7rem; 332 | color: #ffffff; 333 | 334 | a { 335 | color: white; 336 | font-style: italic; 337 | } 338 | } 339 | } 340 | 341 | .overlayBtn { 342 | z-index: 2; 343 | 344 | @extend .no-btn-style; 345 | // position: fixed; 346 | top: 0; 347 | width: 100%; 348 | height: 30%; 349 | } 350 | } 351 | 352 | .btn { 353 | @extend .no-btn-style; 354 | 355 | position: relative; 356 | padding-left: 3px; 357 | top: 2px; 358 | 359 | 360 | img { 361 | width: 12px; 362 | } 363 | } 364 | .interactive { 365 | display: flex; 366 | flex-flow: row wrap; 367 | justify-content: space-around; 368 | 369 | position: absolute; 370 | // width: 100%; 371 | // height: 100%; 372 | z-index: -1; 373 | max-width: 600px; 374 | max-height: 600px; 375 | 376 | 377 | top: 0; 378 | 379 | @media screen and (max-height: 750px) { 380 | top:10%; 381 | } 382 | 383 | bottom: 0; 384 | left: 0; 385 | right: 0; 386 | 387 | margin: auto; 388 | 389 | &.stop { 390 | .musicBtn { 391 | background: #444; 392 | 393 | &.current { 394 | background: #666; 395 | } 396 | 397 | &:hover { 398 | background: #c0392b; 399 | } 400 | } 401 | } 402 | 403 | .musicBtn { 404 | @extend .no-btn-style; 405 | width: 120px; 406 | height: 120px; 407 | position: relative; 408 | 409 | margin: 1rem; 410 | border-radius: 10%; 411 | background: #666; 412 | 413 | &.current { 414 | background: #aaa; 415 | } 416 | 417 | &:hover { 418 | background: #e67e22; 419 | } 420 | 421 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 422 | 423 | @media screen and (max-width: 370px) { 424 | width: 70px; 425 | height: 70px; 426 | margin: 0.5rem; 427 | } 428 | 429 | .matrix { 430 | 431 | display: flex; 432 | flex-flow: row wrap; 433 | justify-content: space-around; 434 | 435 | } 436 | 437 | } 438 | } 439 | 440 | .loadingText { 441 | position: absolute; 442 | width: 80px; 443 | height: 80px; 444 | top:0; 445 | bottom: 0; 446 | left: 0; 447 | right: 0; 448 | margin: auto; 449 | 450 | p { 451 | font-size: 1rem; 452 | font-family: $font-default; 453 | color: #ffffff; 454 | } 455 | } 456 | 457 | .canvas { 458 | margin: auto; 459 | } 460 | 461 | 462 | // splash 463 | 464 | .splash { 465 | position: fixed; 466 | width: 100%; 467 | height: 100%; 468 | z-index: 3; 469 | background-color: rgba(15, 15, 15, 1.0); 470 | color: rgba(255,255,255, 0.8); 471 | transition: opacity 500ms ease; 472 | font-family: $font-default; 473 | font-weight: medium; 474 | text-align: center; 475 | 476 | .wrapper { 477 | position: absolute; 478 | top: 50%; 479 | left: 50%; 480 | transform: translate(-50%, -50%); 481 | margin-left: auto; 482 | margin-right: auto; 483 | width: 500px; 484 | vertical-align: middle; 485 | 486 | 487 | a { 488 | color: $link-color; 489 | text-decoration: none; 490 | } 491 | 492 | .name { 493 | color: #fff; 494 | &:hover { 495 | border-bottom: 1px solid #fff; 496 | } 497 | } 498 | 499 | .about:hover { 500 | border-bottom: 1px solid $link-color; 501 | // transition: border 300ms ease-in-out; 502 | } 503 | 504 | h1 { 505 | font-size: 2rem; 506 | letter-spacing: 0.5rem; 507 | } 508 | 509 | p { 510 | font-size: 1rem; 511 | line-height: 1.5rem; 512 | } 513 | } 514 | 515 | 516 | .playButton { 517 | cursor: pointer; 518 | margin-left: auto; 519 | margin-right: auto; 520 | margin-bottom: 40px; 521 | font-size: 22px; 522 | display: block; 523 | // box-sizing: border-box; 524 | width: 205px; 525 | height: 65px; 526 | border: 0; 527 | // border: 2px solid $link-color; 528 | border-bottom: 2px solid $link-color; 529 | border-top: 2px solid $link-color; 530 | background-color: transparent; 531 | // text-transform: uppercase; 532 | color: $link-color; 533 | text-align: center; 534 | transition: background-color 300ms ease-in-out; 535 | font-family: $font-default; 536 | } 537 | .playButton:hover { 538 | color: white; 539 | background-color: $link-color; 540 | -webkit-user-select: none; /* Chrome all / Safari all */ 541 | -moz-user-select: none; /* Firefox all */ 542 | -ms-user-select: none; 543 | } 544 | .playButton:focus { 545 | outline: none; 546 | } 547 | 548 | .description, .builtWith { 549 | font-size: 14px; 550 | margin-bottom: 40px; 551 | } 552 | 553 | 554 | 555 | .badgeWrapper { 556 | left: 0; 557 | bottom: 0; 558 | margin: 20px; 559 | position: absolute; 560 | width: 80%; 561 | max-width:200px; 562 | } 563 | 564 | .magentaLink { 565 | margin-left: 0; 566 | margin-right: 0; 567 | color: #aaa; 568 | } 569 | 570 | .privacy { 571 | position: absolute; 572 | bottom: 20px; 573 | right: 20px; 574 | a { 575 | color: #aaa; 576 | } 577 | } 578 | } 579 | 580 | -------------------------------------------------------------------------------- /src/renderer/renderer.js: -------------------------------------------------------------------------------- 1 | import PianorollGrid from './pianoroll-grid'; 2 | import { Noise } from 'noisejs'; 3 | import { lerpColor } from './../utils/utils'; 4 | 5 | export default class Renderer { 6 | constructor(app, canvas) { 7 | this.app = app; 8 | this.canvas = canvas; 9 | this.melodies = []; 10 | this.melodiesIndex = 0; 11 | this.pianorollGridsAnswer = []; 12 | this.pianorollGridsOptions = []; 13 | this.nOfAns = app.answers.length; 14 | this.nOfOptions = app.options.length; 15 | this.fontSize = 1.0; 16 | this.playing = false; 17 | 18 | this.draggingState = { 19 | ans: true, // true: ans, false: options 20 | index: -1, 21 | hoverAns: true, 22 | hoverIndex: -1, 23 | }; 24 | 25 | this.frameCount = 0; 26 | this.halt = false; 27 | 28 | // this.backgroundColor = 'rgba(37, 38, 35, 1.0)'; 29 | this.backgroundColor = 'rgba(15, 15, 15, 1.0)'; 30 | this.noteOnColor = 'rgba(255, 255, 255, 1.0)'; 31 | this.mouseOnColor = 'rgba(150, 150, 150, 1.0)'; 32 | this.noteOnCurrentColor = 'rgba(255, 100, 100, 1.0)'; 33 | this.boxColor = 'rgba(200, 200, 200, 1.0)'; 34 | this.displayWidth = 0; 35 | this.h = 0; 36 | 37 | for (let i = 0; i < this.nOfAns; i += 1) { 38 | let pos = -1 * (this.nOfAns - 1) + 2 * i; 39 | const dynamic = this.app.answers[i].ans; 40 | this.pianorollGridsAnswer[i] = new PianorollGrid(this, -1.2, pos, i, true, dynamic) 41 | } 42 | 43 | for (let i = 0; i < this.nOfOptions; i += 1) { 44 | let pos = -1 * (this.nOfOptions - 1) + 2 * i; 45 | this.pianorollGridsOptions[i] = new PianorollGrid(this, 1.2, pos, i, false) 46 | } 47 | 48 | this.noise = new Noise(Math.random()); 49 | 50 | // interpolation display 51 | this.h_step = 0; 52 | 53 | // instruction 54 | this.endOfSection = false; 55 | this.instructionState = 0; 56 | } 57 | 58 | updateMelodies(ms) { 59 | this.melodies = ms; 60 | this.nOfAns = this.app.answers.length; 61 | this.nOfOptions = this.app.options.length; 62 | this.pianorollGridsAnswer = []; 63 | this.pianorollGridsOptions = []; 64 | 65 | for (let i = 0; i < this.nOfAns; i += 1) { 66 | let pos = -1 * (this.nOfAns - 1) + 2 * i; 67 | const dynamic = this.app.answers[i].ans; 68 | this.pianorollGridsAnswer[i] = new PianorollGrid(this, -1.2, pos, i, true, dynamic) 69 | } 70 | 71 | for (let i = 0; i < this.nOfOptions; i += 1) { 72 | let pos = -1 * (this.nOfOptions - 1) + 2 * i; 73 | this.pianorollGridsOptions[i] = new PianorollGrid(this, 1.2, pos, i, false) 74 | } 75 | } 76 | 77 | draw(src, progress = 0) { 78 | // console.log(this.app.state.loadingNextInterpolation); 79 | // console.log(this.melodies); 80 | 81 | this.frameCount += 1; 82 | this.progress = progress; 83 | 84 | const ctx = this.canvas.getContext('2d'); 85 | // ctx.font = this.fontSize.toString() + 'rem monospace'; 86 | this.width = src.width; 87 | this.height = src.height; 88 | const width = src.width; 89 | const height = src.height; 90 | 91 | ctx.save(); 92 | ctx.fillStyle = this.backgroundColor; 93 | ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); 94 | 95 | // const h = Math.min(width, height) * 0.18; 96 | // const h = width * 0.1; 97 | // const w = Math.max(Math.min(((width - 100) / (this.nOfAns * 1.5)), 70), 30); 98 | const w = Math.max(Math.min(((width - 100) / (this.nOfAns * 1.5)), 150), 20); 99 | // const w = h; 100 | const h = w; 101 | this.h = h; 102 | this.displayWidth = w; 103 | this.setFontSize(ctx, Math.pow(w / 800, 0.3)); 104 | 105 | ctx.translate(width * 0.5, height * 0.5); 106 | 107 | this.pianorollGridsAnswer.forEach((p, i) => { 108 | const frameOnly = this.app.answers[i].index === -1; 109 | p.draw(ctx, w, h, frameOnly); 110 | }); 111 | this.pianorollGridsOptions.forEach((p, i) => { 112 | const frameOnly = this.app.options[i].index === -1; 113 | p.draw(ctx, w, h, frameOnly); 114 | }); 115 | 116 | ctx.restore(); 117 | } 118 | 119 | drawInterpolation(ctx, w, h) {} 120 | 121 | handleInterpolationClick(x, y) { 122 | const xpos = x + (this.displayWidth * 0.5); 123 | const ypos = y; 124 | return false; 125 | } 126 | 127 | handleMouseDownOnAnswers(x, y) { 128 | if (Math.abs(this.pianorollGridsAnswer[0].gridYShift - y) < this.h * 0.5) { 129 | // console.log(`x:${x}, y:${y}`); 130 | 131 | const id = Math.floor((x / (2 * this.displayWidth * 0.7)) + this.nOfAns * 0.5); 132 | // console.log(`id: ${id}`); 133 | return id; 134 | } 135 | return -1; 136 | } 137 | 138 | handleMouseDownOnOptions(x, y) { 139 | if (Math.abs(this.pianorollGridsOptions[0].gridYShift - y) < this.h * 0.5) { 140 | // console.log(`x:${x}, y:${y}`); 141 | 142 | const id = Math.floor((x / (2 * this.displayWidth * 0.7)) + this.nOfOptions * 0.5); 143 | // console.log(`id: ${id}`); 144 | return id; 145 | } 146 | return -1; 147 | } 148 | 149 | handleMouseClick(e) { 150 | let cx = e.clientX - this.width * 0.5;; 151 | let cy = e.clientY - this.height * 0.5; 152 | 153 | const { dragging } = this.app.state; 154 | 155 | const onAnsId = this.handleMouseDownOnAnswers(cx, cy); 156 | let onAns = -1; 157 | if (onAnsId > -1 && onAnsId < this.app.answers.length) { 158 | onAns = this.app.answers[onAnsId].index; 159 | if (!dragging) { 160 | this.melodiesIndex = onAns; 161 | } 162 | } 163 | 164 | const onOptionsId = this.handleMouseDownOnOptions(cx, cy); 165 | let onOptions = -1; 166 | if (onOptionsId > -1 && onOptionsId < this.app.options.length) { 167 | onOptions = this.app.options[onOptionsId].index; 168 | if (!dragging) { 169 | this.melodiesIndex = onOptions; 170 | } 171 | } 172 | 173 | return [ 174 | onAns, 175 | onOptions, 176 | ]; 177 | } 178 | 179 | handleMouseDown(e) { 180 | let cx = e.clientX - this.width * 0.5;; 181 | let cy = e.clientY - this.height * 0.5; 182 | 183 | let mouseIn = false; 184 | 185 | const onAnsId = this.handleMouseDownOnAnswers(cx, cy); 186 | let onAns = -1; 187 | if (onAnsId > -1 && onAnsId < this.app.answers.length) { 188 | onAns = this.app.answers[onAnsId].index; 189 | // this.melodiesIndex = onAns; 190 | 191 | if (this.app.answers[onAnsId].ans) { 192 | this.draggingState.ans = true; 193 | this.draggingState.index = onAnsId; 194 | this.draggingState.hoverAns = true; 195 | } 196 | mouseIn = true; 197 | } 198 | 199 | const onOptionsId = this.handleMouseDownOnOptions(cx, cy); 200 | let onOptions = -1; 201 | if (onOptionsId > -1 && onOptionsId < this.app.options.length) { 202 | onOptions = this.app.options[onOptionsId].index; 203 | // this.melodiesIndex = onOptions; 204 | 205 | this.draggingState.ans = false; 206 | this.draggingState.index = onOptionsId; 207 | this.draggingState.hoverAns = false; 208 | 209 | mouseIn = true; 210 | } 211 | 212 | if (!mouseIn) { 213 | this.draggingState.index = -1; 214 | } else { 215 | // click on something 216 | } 217 | 218 | this.mouseDownX = cx; 219 | this.mouseDownY = cy; 220 | 221 | return [ 222 | onAns, 223 | onOptions, 224 | ]; 225 | } 226 | 227 | handleMouseMove(e) { 228 | const x = e.clientX - (this.width * 0.5); 229 | const y = e.clientY - (this.height * 0.5); 230 | 231 | const { ans, index } = this.draggingState; 232 | const onAnsId = this.handleMouseDownOnAnswers(x, y); 233 | const onOptionsId = this.handleMouseDownOnOptions(x, y); 234 | 235 | if (index !== -1) { 236 | if (!ans) { 237 | this.pianorollGridsOptions[index].dragX = (x - this.mouseDownX); 238 | this.pianorollGridsOptions[index].dragY = (y - this.mouseDownY); 239 | } else { 240 | this.pianorollGridsAnswer[index].dragX = (x - this.mouseDownX); 241 | this.pianorollGridsAnswer[index].dragY = (y - this.mouseDownY); 242 | } 243 | } 244 | 245 | let hoverOnSomething = false; 246 | if (onAnsId > -1 && onAnsId < this.app.answers.length) { 247 | // console.log('into hovering ans'); 248 | this.draggingState.hoverAns = true; 249 | this.draggingState.hoverIndex = onAnsId; 250 | hoverOnSomething = true; 251 | } 252 | 253 | if (onOptionsId > -1 && onOptionsId < this.app.options.length) { 254 | // console.log('into hovering options'); 255 | this.draggingState.hoverAns = false; 256 | this.draggingState.hoverIndex = onOptionsId; 257 | hoverOnSomething = true; 258 | } 259 | 260 | if (!hoverOnSomething) { 261 | // console.log('hover out'); 262 | this.draggingState.hoverIndex = -1; 263 | } 264 | // if (index !== -1) { 265 | // } 266 | } 267 | 268 | handleMouseUp(e) { 269 | const { ans, index, hoverAns, hoverIndex } = this.draggingState; 270 | // console.log(this.draggingState); 271 | if (index !== -1) { 272 | if (!ans) { 273 | this.pianorollGridsOptions[index].dragX = 0; 274 | this.pianorollGridsOptions[index].dragY = 0; 275 | } else { 276 | this.pianorollGridsAnswer[index].dragX = 0; 277 | this.pianorollGridsAnswer[index].dragY = 0; 278 | } 279 | 280 | if (!ans) { 281 | if (hoverIndex !== -1) { 282 | if (hoverAns) { 283 | 284 | const originalId = this.app.answers[hoverIndex].index; 285 | if (originalId === -1) { 286 | this.app.triggerSoundEffect(); 287 | 288 | this.app.answers[hoverIndex].index = this.app.options[index].index; 289 | this.app.options[index].index = -1; 290 | 291 | // show 292 | this.app.answers[hoverIndex].show = this.app.options[index].show; 293 | } 294 | } else { 295 | const downId = this.app.options[index].index; 296 | if (downId !== -1 && index != hoverIndex) { 297 | this.app.triggerSoundEffect(); 298 | const originalId = this.app.options[hoverIndex].index; 299 | this.app.options[hoverIndex].index = this.app.options[index].index; 300 | this.app.options[index].index = originalId; 301 | 302 | // show 303 | const originalShow = this.app.options[hoverIndex].show; 304 | this.app.options[hoverIndex].show = this.app.options[index].show; 305 | this.app.options[index].show = originalShow; 306 | } 307 | } 308 | } 309 | } else { 310 | if (hoverIndex !== -1) { 311 | if (!hoverAns) { 312 | 313 | const originalId = this.app.options[hoverIndex].index; 314 | if (originalId === -1) { 315 | this.app.triggerSoundEffect(); 316 | 317 | this.app.options[hoverIndex].index = this.app.answers[index].index; 318 | this.app.answers[index].index = -1; 319 | } 320 | } else { 321 | if (this.app.answers[hoverIndex].ans) { 322 | const downId = this.app.answers[index].index; 323 | if (downId !== -1 && index != hoverIndex) { 324 | this.app.triggerSoundEffect(); 325 | const originalId = this.app.answers[hoverIndex].index; 326 | this.app.answers[hoverIndex].index = this.app.answers[index].index; 327 | this.app.answers[index].index = originalId; 328 | 329 | // show 330 | const originalShow = this.app.answers[hoverIndex].show; 331 | this.app.answers[hoverIndex].show = this.app.answers[index].show; 332 | this.app.answers[index].show = originalShow; 333 | 334 | } 335 | } 336 | } 337 | } 338 | } 339 | } 340 | 341 | this.draggingState.index = -1; 342 | this.draggingState.hoverIndex = -1; 343 | 344 | // console.log(...this.app.answers); 345 | } 346 | 347 | // instruction 348 | changeInstructionState(s) { 349 | this.instructionState = s; 350 | } 351 | 352 | // draw frame 353 | drawFrame(ctx, w, h) { 354 | const unit = this.h * 0.04; 355 | 356 | ctx.save(); 357 | 358 | ctx.strokeStyle = '#FFF'; 359 | 360 | ctx.beginPath() 361 | ctx.moveTo(0.5 * w, 0.5 * h - unit); 362 | ctx.lineTo(0.5 * w, 0.5 * h); 363 | ctx.lineTo(0.5 * w - unit, 0.5 * h); 364 | ctx.stroke(); 365 | 366 | ctx.beginPath() 367 | ctx.moveTo(-0.5 * w, 0.5 * h - unit); 368 | ctx.lineTo(-0.5 * w, 0.5 * h); 369 | ctx.lineTo(-0.5 * w + unit, 0.5 * h); 370 | ctx.stroke(); 371 | 372 | ctx.beginPath() 373 | ctx.moveTo(0.5 * w, -0.5 * h + unit); 374 | ctx.lineTo(0.5 * w, -0.5 * h); 375 | ctx.lineTo(0.5 * w - unit, -0.5 * h); 376 | ctx.stroke(); 377 | 378 | ctx.beginPath() 379 | ctx.moveTo(-0.5 * w, -0.5 * h + unit); 380 | ctx.lineTo(-0.5 * w, -0.5 * h); 381 | ctx.lineTo(-0.5 * w + unit, -0.5 * h); 382 | ctx.stroke(); 383 | 384 | ctx.restore(); 385 | } 386 | 387 | setFontSize(ctx, amt) { 388 | this.fontSize = amt; 389 | ctx.font = this.fontSize.toString() + 'rem monospace'; 390 | } 391 | 392 | // animation 393 | triggerStartAnimation() { 394 | this.pianorollGridsAnswer.forEach(p => p.triggerStartAnimation()); 395 | } 396 | 397 | } 398 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { MusicVAE } from '@magenta/music'; 4 | import uuidv4 from 'uuid/v4'; 5 | 6 | 7 | import styles from './index.module.scss'; 8 | import sig from './assets/sig.png'; 9 | import info from './assets/info.png'; 10 | import Sound from './music/sound'; 11 | import Renderer from './renderer/renderer'; 12 | import { presetMelodies } from './music/clips'; 13 | import { questions, getQuestions, checkEnd } from './utils/questions'; 14 | 15 | class App extends Component { 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = { 20 | open: false, // menu 21 | slash: true, 22 | playing: false, 23 | mouseDown: false, 24 | dragging: false, 25 | loadingModel: true, 26 | loadingNextInterpolation: false, 27 | rhythmThreshold: 0.6, 28 | finishedAnswer: false, 29 | answerCorrect: false, 30 | waitingNext: false, 31 | restart: false, 32 | bpm: 120, 33 | screen: { 34 | width: window.innerWidth, 35 | height: window.innerHeight, 36 | ratio: window.devicePixelRatio || 1, 37 | }, 38 | 39 | level: 0, 40 | score: 0, 41 | history: Array(questions.length).fill(-1), 42 | }; 43 | 44 | this.sound = new Sound(this), 45 | this.canvas = []; 46 | this.melodies = []; 47 | this.bpms = []; 48 | this.questionIndex = 0; 49 | this.initAns(0); 50 | } 51 | 52 | componentDidMount() { 53 | this.renderer = new Renderer(this, this.canvas); 54 | this.initVAE(); 55 | this.addEventListeners(); 56 | requestAnimationFrame(() => { this.update() }); 57 | } 58 | 59 | addEventListeners() { 60 | window.addEventListener('keydown', this.handleKeyDown.bind(this), false); 61 | window.addEventListener('resize', this.handleResize.bind(this, false)); 62 | window.addEventListener('click', this.handleClick.bind(this)); 63 | window.addEventListener('mousedown', this.handleMouseDown.bind(this)); 64 | window.addEventListener('mousemove', this.handleMouseMove.bind(this)); 65 | window.addEventListener('mouseup', this.handleMouseUp.bind(this)); 66 | } 67 | 68 | removeEventListener() { 69 | window.removeEventListener('keydown', this.handleKeyDown.bind(this)); 70 | window.removeEventListener('mousedown', this.handleMouseDown.bind(this)); 71 | window.removeEventListener('click', this.handleClick.bind(this)); 72 | window.removeEventListener('mousemove', this.handleMouseMove.bind(this)); 73 | window.removeEventListener('mouseup', this.handleMouseUp.bind(this)); 74 | window.removeEventListener('resize', this.handleResize.bind(this, false)); 75 | } 76 | 77 | componentWillUnmount() { 78 | removeEventListener(); 79 | } 80 | 81 | initVAE() { 82 | const modelCheckPoint = 'https://storage.googleapis.com/magentadata/js/checkpoints/music_vae/mel_2bar_small'; 83 | // const modelCheckPoint = './checkpoints/mel_2bar_small'; 84 | const n = this.numInterpolations; 85 | const vae = new MusicVAE(modelCheckPoint); 86 | 87 | this.setMelodies(new Array(n).fill(presetMelodies['Twinkle'])); 88 | vae.initialize() 89 | .then(() => { 90 | console.log('initialized!'); 91 | return vae.interpolate([ 92 | presetMelodies[this.melodiesName[0]], 93 | presetMelodies[this.melodiesName[1]], 94 | ], n); 95 | }) 96 | .then((i) => { 97 | this.setMelodies(i); 98 | this.vae = vae; 99 | this.setState({ 100 | loadingModel: false, 101 | }); 102 | this.sound.triggerSoundEffect(2); 103 | }); 104 | } 105 | 106 | initAns(lv) { 107 | const q = JSON.parse(JSON.stringify(getQuestions(lv))); 108 | this.answers = q.answers.slice(0); 109 | this.options = q.options.slice(0); 110 | this.numInterpolations = q.numInterpolations; 111 | this.melodiesName = q.melodies.slice(0); 112 | } 113 | 114 | resetAns() { 115 | const { level } = this.state; 116 | const nextLevel = level + 1; 117 | const q = JSON.parse(JSON.stringify(getQuestions(nextLevel))); 118 | 119 | this.vae.interpolate([ 120 | presetMelodies[q.melodies[0]], 121 | presetMelodies[q.melodies[1]], 122 | ], q.numInterpolations) 123 | .then((i) => { 124 | this.initAns(nextLevel); 125 | this.setMelodies(i); 126 | 127 | this.setState({ 128 | loadingNextInterpolation: false, 129 | }); 130 | 131 | if (this.state.level !== 0) { 132 | this.sound.triggerSoundEffect(4); 133 | } 134 | }); 135 | 136 | this.setState({ 137 | loadingNextInterpolation: true, 138 | }); 139 | } 140 | 141 | showAns() { 142 | this.answers.forEach(a => { 143 | a.show = true; 144 | }); 145 | } 146 | 147 | setMelodies(ms) { 148 | this.renderer.updateMelodies(ms); 149 | this.sound.updateMelodies(ms); 150 | this.interpolatedMelodies = ms; 151 | } 152 | 153 | update() { 154 | const { progress } = this.sound.part; 155 | this.renderer.draw(this.state.screen, progress); 156 | requestAnimationFrame(() => { this.update() }); 157 | } 158 | 159 | handleResize(value, e) { 160 | this.setState({ 161 | screen: { 162 | width: window.innerWidth, 163 | height: window.innerHeight, 164 | ratio: window.devicePixelRatio || 1, 165 | } 166 | }); 167 | } 168 | 169 | handleClick(e) { 170 | e.stopPropagation(); 171 | const { slash, dragging } = this.state; 172 | 173 | if (!slash) { 174 | const [onAns, onOptions] = this.renderer.handleMouseClick(e); 175 | if (!dragging) { 176 | if (onAns > -1) { 177 | this.sound.changeMelody(onAns); 178 | this.start(); 179 | } else if (onOptions > -1) { 180 | this.sound.changeMelody(onOptions); 181 | this.start(); 182 | } 183 | } 184 | 185 | this.setState({ 186 | dragging: false, 187 | }); 188 | } 189 | } 190 | 191 | handleMouseDown(e) { 192 | e.stopPropagation(); 193 | const { slash, waitingNext } = this.state; 194 | 195 | if (!slash && !waitingNext) { 196 | const [ onAns, onOptions] = this.renderer.handleMouseDown(e); 197 | if (onAns > -1) { 198 | // this.sound.changeMelody(onAns); 199 | // this.start(); 200 | this.setState({ 201 | mouseDown: true, 202 | }); 203 | } else if (onOptions > -1) { 204 | // this.sound.changeMelody(onOptions); 205 | // this.start(); 206 | this.setState({ 207 | mouseDown: true, 208 | }); 209 | } 210 | } 211 | 212 | } 213 | 214 | handleMouseUp(e) { 215 | e.stopPropagation(); 216 | const { slash, waitingNext } = this.state; 217 | if (!slash && !waitingNext) { 218 | // console.log('m up'); 219 | this.renderer.handleMouseUp(e) 220 | const finished = this.checkFinished(); 221 | 222 | this.setState({ 223 | mouseDown: false, 224 | finishedAnswer: finished, 225 | }); 226 | } 227 | } 228 | 229 | handleMouseMove(e) { 230 | e.stopPropagation(); 231 | const { slash } = this.state; 232 | if (!slash) { 233 | this.renderer.handleMouseMove(e); 234 | if (this.state.mouseDown) { 235 | this.setState({ 236 | dragging: true, 237 | }); 238 | } 239 | } 240 | } 241 | 242 | handleClickMenu() { 243 | const { open } = this.state; 244 | if (open) { 245 | this.closeMenu(); 246 | } else { 247 | this.openMenu(); 248 | } 249 | } 250 | 251 | handleKeyDown(event) { 252 | event.stopPropagation(); 253 | const { loadingModel } = this.state; 254 | if (!loadingModel) { 255 | if (event.keyCode === 32) { 256 | // space 257 | this.trigger(); 258 | } 259 | if (event.keyCode === 65) { 260 | // a 261 | } 262 | if (event.keyCode === 82) { 263 | // r 264 | } 265 | } 266 | } 267 | 268 | openMenu() { 269 | document.getElementById('menu').style.height = '100%'; 270 | this.setState({ 271 | open: true, 272 | }); 273 | } 274 | 275 | closeMenu() { 276 | document.getElementById('menu').style.height = '0%'; 277 | this.setState({ 278 | open: false, 279 | }); 280 | } 281 | 282 | trigger() { 283 | const playing = this.sound.trigger(); 284 | this.renderer.playing = playing; 285 | this.setState({ 286 | playing, 287 | }); 288 | } 289 | 290 | start() { 291 | this.sound.start(); 292 | this.renderer.playing = true; 293 | this.setState({ 294 | playing: true, 295 | }); 296 | } 297 | 298 | stop() { 299 | this.sound.stop(); 300 | this.renderer.playing = false; 301 | this.setState({ 302 | playing: false, 303 | }); 304 | } 305 | 306 | onPlay() { 307 | const { restart } = this.state; 308 | console.log('press play!'); 309 | 310 | this.sound.triggerSoundEffect(4); 311 | 312 | const id = restart ? 'splash-score' : 'splash'; 313 | const splash = document.getElementById(id); 314 | splash.style.opacity = 0.0; 315 | setTimeout(() => { 316 | splash.style.display = 'none'; 317 | this.setState({ 318 | score: 0, 319 | slash: false, 320 | }); 321 | }, 500); 322 | } 323 | 324 | checkFinished() { 325 | let ret = true; 326 | this.answers.forEach(a => { 327 | if (a.ans && (a.index === -1)) { 328 | ret = false; 329 | } 330 | }); 331 | return ret; 332 | } 333 | 334 | checkCorrect() { 335 | let ret = true; 336 | this.answers.forEach((a, i) => { 337 | if (a.index !== i) { 338 | ret = false; 339 | } 340 | }); 341 | return ret; 342 | } 343 | 344 | onClickTheButton() { 345 | const { loadingNextInterpolation } = this.state; 346 | 347 | if (loadingNextInterpolation) { 348 | return; 349 | } 350 | 351 | if (this.state.waitingNext) { 352 | const tips = document.getElementById('tips'); 353 | tips.style.display = 'block'; 354 | 355 | const result = document.getElementById('resultText'); 356 | result.style.display = 'none'; 357 | 358 | const { level } = this.state; 359 | 360 | if (checkEnd(level)) { 361 | // reset game 362 | this.sound.triggerSoundEffect(3); 363 | 364 | const splash = document.getElementById('splash-score'); 365 | splash.style.display = 'block'; 366 | splash.style.opacity = 1.0; 367 | 368 | this.resetAns(); 369 | this.setState({ 370 | level: 0, 371 | restart: true, 372 | waitingNext: false, 373 | finishedAnswer: false, 374 | slash: true, 375 | 376 | history: Array(questions.length).fill(-1), 377 | }); 378 | return; 379 | } 380 | this.resetAns(); 381 | this.setState({ 382 | level: level + 1, 383 | loadingNextInterpolation: true, 384 | waitingNext: false, 385 | finishedAnswer: false, 386 | }); 387 | 388 | return; 389 | } 390 | 391 | if (this.state.finishedAnswer) { 392 | const { level, history } = this.state; 393 | 394 | const tips = document.getElementById('tips'); 395 | tips.style.display = 'none'; 396 | 397 | const result = document.getElementById('resultText'); 398 | result.style.display = 'block'; 399 | 400 | // update score 401 | const correct = this.checkCorrect(); 402 | const score = this.state.score + (correct ? 1 : 0); 403 | 404 | if (correct) { 405 | this.sound.triggerSoundEffect(2); 406 | history[level] = 2; 407 | } else { 408 | this.sound.triggerSoundEffect(1); 409 | history[level] = 1; 410 | } 411 | 412 | // reveal ans 413 | this.showAns(); 414 | 415 | 416 | 417 | this.setState({ 418 | waitingNext: true, 419 | answerCorrect: correct, 420 | score, 421 | history, 422 | }); 423 | 424 | return; 425 | } 426 | } 427 | 428 | setLevel(lv) { 429 | 430 | } 431 | 432 | triggerSoundEffect() { 433 | this.sound.triggerSoundEffect(); 434 | } 435 | 436 | render() { 437 | const { waitingNext, loadingModel, finishedAnswer, answerCorrect, score, loadingNextInterpolation, history, level } = this.state; 438 | const loadingText = loadingModel ? 'loading...' : 'play'; 439 | let buttonText = finishedAnswer ? 'send' : 'sorting...'; 440 | 441 | if (loadingNextInterpolation) { 442 | buttonText = 'loading ...'; 443 | } else if (waitingNext) { 444 | if (!checkEnd(level)) { 445 | buttonText = 'next'; 446 | } else { 447 | buttonText = 'end'; 448 | } 449 | } 450 | 451 | 452 | const resultText = answerCorrect ? 'correct!' : 'wrong!'; 453 | const scoreText = `${score.toString()}/${(questions.length).toString()}`; 454 | const bottomScoreText = `[ score: ${score.toString()}/${(questions.length).toString()} ]`; 455 | 456 | let finalText = 'Ears of a musician!'; 457 | if (score < 3) { 458 | finalText = 'You can do this!'; 459 | } else if (score < 5) { 460 | finalText = 'You have a good ear!'; 461 | } 462 | 463 | 464 | const arr = Array.from(Array(9).keys()); 465 | const mat = Array.from(Array(9 * 16).keys()); 466 | const { rhythmThreshold, bpm } = this.state; 467 | return ( 468 |
469 |
470 |
471 |

🎸Sornting

472 |

473 | = Sort + Song 474 |

475 |
476 |

477 | A game based on a musical machine learning algorithm which can interpolate different melodies.
478 | The player has to listen to the music to find out the right order, or "sort" the song. 479 |

480 | 481 | 488 | 489 |

490 | Built with tone.js + musicvae.js. 491 |
492 | Learn more about how it works. 493 |

494 | 495 |

Made by

496 | Vibert Thio Icon 497 |

Vibert Thio

498 |
499 |
500 |
501 | 502 |
Music and AI Lab
503 |
504 |
505 |
506 | Privacy & 507 | Terms 508 |
509 |
510 |
511 |
512 |

{finalText}

513 |

Score

514 |

{scoreText}

515 |
516 | 517 | 524 | 525 |

526 | Challenge your friend with this game. 527 |
528 | Built with tone.js + musicvae.js. 529 |
530 | Learn more about how it works. 531 |

532 | 533 |

Made by

534 | Vibert Thio Icon 535 |

Vibert Thio

536 |
537 |
538 |
539 | 540 |
Music and AI Lab
541 |
542 |
543 |
544 | Privacy & 545 | Terms 546 |
547 |
548 | 549 |
550 |
551 | 552 | Sornting 553 | 554 |
555 | 562 | 563 |
564 | {this.tipsText(level)} 565 |
566 |

{resultText}

567 | 568 |
569 |
570 | this.canvas = c } 572 | className={styles.canvas} 573 | width={this.state.screen.width * this.state.screen.ratio} 574 | height={this.state.screen.height * this.state.screen.ratio} 575 | /> 576 |
577 |
578 | 579 |
580 | 587 |
588 | {/*
589 |

{bottomScoreText}

590 |
*/} 591 | 592 | 593 |
594 | {history.map((value, i) => { 595 | if (value === 1) { 596 | return ( 597 |
598 |
599 |
600 | ); 601 | } else if (value === 2) { 602 | return ( 603 |
604 |
605 |
606 | ); 607 | } else if (i === level) { 608 | return ( 609 |
610 |
611 |
612 | ); 613 | } else if (value === -1) { 614 | return ( 615 |
616 |
617 |
618 | ); 619 | } 620 | })} 621 |
622 |
623 | 650 |
651 | ); 652 | } 653 | 654 | tipsText(level) { 655 | if (level === 0) { 656 | return ( 657 |
658 |

🙋‍♀️Tips

659 |

⚡Drag the melodies below
660 | into the golden box above
661 | to complete the interpolation.

662 | 663 |

👇Click on the boxes to listen to the melodies.

664 |
665 | ); 666 | } else if (level === 1) { 667 | return ( 668 |
669 |

👀Some answers are hidden away. Listen carefully.

670 |
671 | ); 672 | } else if (level === 2) { 673 | return ( 674 |
675 |

🚵‍♂️It will get harder every new level.

676 |
677 | ); 678 | } else if (level === 3) { 679 | return ( 680 |
681 |

🧗‍♂Whether you are a musician,
682 | you may challenge yourself.

683 |
684 | ); 685 | } else if (level === 4) { 686 | return ( 687 |
688 |

🔊Listen carefully.

689 |

👀Also, observe the patterns carefully.

690 |
691 | ); 692 | } else if (level === 5) { 693 | return ( 694 |
695 |

😈 Invisible

696 |

Now you can only listen to figure out the answer.

697 |
698 | ); 699 | } 700 | } 701 | } 702 | 703 | render(, document.getElementById('root')); 704 | --------------------------------------------------------------------------------