├── .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 | 
--------------------------------------------------------------------------------
/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 |
this.onPlay()}
485 | >
486 | {loadingText}
487 |
488 |
489 |
490 | Built with tone.js + musicvae.js.
491 |
492 | Learn more about how it works.
493 |
494 |
495 |
Made by
496 |
497 |
Vibert Thio
498 |
499 |
500 |
505 |
509 |
510 |
511 |
512 |
{finalText}
513 |
Score
514 |
{scoreText}
515 |
516 |
517 |
this.onPlay()}
521 | >
522 | play again
523 |
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 |
535 |
Vibert Thio
536 |
537 |
538 |
543 |
547 |
548 |
549 |
550 |
555 |
this.handleClickMenu()}
558 | onKeyDown={e => e.preventDefault()}
559 | >
560 |
561 |
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 | this.onClickTheButton()}
583 | onKeyDown={e => e.preventDefault()}
584 | >
585 | {buttonText}
586 |
587 |
588 | {/*
589 |
{bottomScoreText}
590 |
*/}
591 |
592 |
593 |
594 | {history.map((value, i) => {
595 | if (value === 1) {
596 | return (
597 |
600 | );
601 | } else if (value === 2) {
602 | return (
603 |
606 | );
607 | } else if (i === level) {
608 | return (
609 |
612 | );
613 | } else if (value === -1) {
614 | return (
615 |
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 |
--------------------------------------------------------------------------------