├── public ├── music │ └── .keep ├── fonts │ ├── MaterialIcons-Regular.eot │ ├── MaterialIcons-Regular.ttf │ ├── MaterialIcons-Regular.woff │ └── MaterialIcons-Regular.woff2 ├── images │ └── background.jpg └── lovecall.html ├── src ├── css │ ├── fonts │ ├── images │ ├── _reset-minimal.css │ ├── _dimens.css │ ├── _call.css │ ├── _mixins.css │ ├── index.css │ ├── _about.css │ ├── _song-loading.css │ ├── _content.css │ ├── _config.css │ ├── _song-selector.css │ ├── _transport.css │ ├── _navigation.css │ ├── _font.css │ └── _metadata.css ├── misc │ └── empty.opus ├── js │ ├── ui │ │ ├── container.js │ │ ├── about.js │ │ ├── config.js │ │ ├── index.js │ │ ├── song-selector.js │ │ ├── song-loading.js │ │ ├── dpi.js │ │ ├── images.js │ │ ├── frame.js │ │ ├── metadata.js │ │ └── navigation.js │ ├── provider │ │ ├── resize-detector.js │ │ ├── font-selector.js │ │ ├── mouseevent.js │ │ ├── song.js │ │ └── choreography.js │ ├── util │ │ └── data-helper.js │ ├── data │ │ ├── susutomo.js │ │ ├── snowhare.js │ │ ├── start-dash.js │ │ ├── bokuima.js │ │ ├── kiseki.js │ │ └── nbg.js │ ├── main.js │ ├── init.js │ ├── choreography │ │ ├── manager.js │ │ ├── tempo.js │ │ └── metronome.js │ ├── update.js │ ├── conf.js │ └── engine │ │ └── audio-compat.js ├── templates │ ├── song-loading.tmpl.html │ ├── config.tmpl.html │ ├── about.tmpl.html │ ├── song-selector.tmpl.html │ └── index.tmpl.html └── images │ ├── d.svg │ ├── k.svg │ ├── hi.svg │ ├── oh.svg │ ├── fu.svg │ ├── sj.svg │ ├── _original │ ├── special.svg │ ├── ld.svg │ ├── qh.svg │ ├── sj.svg │ ├── hh.svg │ ├── jump.svg │ ├── kh.svg │ ├── lt.svg │ ├── clap.svg │ ├── fu.svg │ ├── hi.svg │ ├── fuwa.svg │ ├── d.svg │ ├── k.svg │ └── oh.svg │ ├── ld.svg │ ├── jump.svg │ ├── clap.svg │ └── hh.svg ├── Makefile ├── dev-server.sh ├── lovecall.config.example.js ├── plugins ├── android.json └── fetch.json ├── config.xml ├── bower.json ├── README.md ├── .gitignore ├── package.json └── webpack.config.js /public/music/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/fonts: -------------------------------------------------------------------------------- 1 | ../../public/fonts -------------------------------------------------------------------------------- /src/css/images: -------------------------------------------------------------------------------- 1 | ../../public/images -------------------------------------------------------------------------------- /src/css/_reset-minimal.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | -------------------------------------------------------------------------------- /src/misc/empty.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveCallProject/lovecall-frontend/HEAD/src/misc/empty.opus -------------------------------------------------------------------------------- /public/fonts/MaterialIcons-Regular.eot: -------------------------------------------------------------------------------- 1 | ../../bower_components/material-design-icons/iconfont/MaterialIcons-Regular.eot -------------------------------------------------------------------------------- /public/fonts/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- 1 | ../../bower_components/material-design-icons/iconfont/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- 1 | ../../bower_components/material-design-icons/iconfont/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /public/fonts/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- 1 | ../../bower_components/material-design-icons/iconfont/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: webpack 2 | 3 | webpack: 4 | @./node_modules/.bin/webpack -p --devtool='#sourcemap' 5 | 6 | .PHONY: webpack 7 | -------------------------------------------------------------------------------- /public/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveCallProject/lovecall-frontend/HEAD/public/images/background.jpg -------------------------------------------------------------------------------- /src/css/_dimens.css: -------------------------------------------------------------------------------- 1 | $md-sidenav-locked-open-width: 320px; 2 | 3 | $toolbar-height: 8rem; 4 | 5 | $metadata-height: 7rem; 6 | -------------------------------------------------------------------------------- /dev-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # without this webpack-cordova-plugin will override the content base 4 | export BUILD_CORDOVA=0 5 | 6 | exec webpack-dev-server --inline --compress --content-base=public/ --devtool='#cheap-module-eval-source-map' $@ 7 | -------------------------------------------------------------------------------- /src/css/_call.css: -------------------------------------------------------------------------------- 1 | .call__canvas-container { 2 | width: 100%; 3 | height: 201px; /* keep this in sync with dimensions in js/ui/call.js */ 4 | position: relative; 5 | 6 | > canvas { 7 | width: 100%; 8 | height: 100%; 9 | 10 | position: absolute; 11 | left: 0; 12 | top: 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/css/_mixins.css: -------------------------------------------------------------------------------- 1 | @define-mixin sidenav-locked-open-aware { 2 | &.sidenav-locked-open { 3 | @mixin-content; 4 | } 5 | } 6 | 7 | 8 | @define-mixin reserve-space-for-locked-sidenav { 9 | @mixin sidenav-locked-open-aware { 10 | padding-left: $md-sidenav-locked-open-width; 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /lovecall.config.example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 远程音乐文件 URL 前缀, 一定要以斜线结束 3 | // URL prefix for remote music files, MUST end with a slash 4 | remoteMusicPrefix: 'music/', 5 | // 远程专辑封面 URL 前缀, 也要以斜线结束 6 | // URL prefix for remote cover art files, also MUST end with a slash 7 | remoteCoverArtPrefix: 'music/cover/', 8 | }; 9 | -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | @import 'dimens'; 2 | @import 'mixins'; 3 | 4 | @import 'reset-minimal'; 5 | @import 'font'; 6 | @import 'navigation'; 7 | @import 'content'; 8 | @import 'metadata'; 9 | @import 'call'; 10 | @import 'transport'; 11 | @import 'song-loading'; 12 | @import 'song-selector'; 13 | @import 'about'; 14 | @import 'config'; 15 | -------------------------------------------------------------------------------- /src/css/_about.css: -------------------------------------------------------------------------------- 1 | .about__toolbar__icon { 2 | margin: 1rem; 3 | } 4 | 5 | .about__content__listitem { 6 | display: flex; 7 | align-items: center; 8 | } 9 | 10 | .about { 11 | width: 50rem; 12 | height: 60rem; 13 | display: flex; 14 | } 15 | 16 | .about__content { 17 | flex: 1; 18 | md-tabs { 19 | height: 100%; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/css/_song-loading.css: -------------------------------------------------------------------------------- 1 | .song-loading { 2 | align-items: center; 3 | } 4 | 5 | .song-loading__progress { 6 | flex: 0 0 auto; 7 | } 8 | 9 | .song-loading__message { 10 | flex: 1; 11 | 12 | font-weight: normal; 13 | font-size: 1rem; 14 | } 15 | 16 | .song-loading__actions { 17 | /* XXX: why the f**k is this necessary */ 18 | order: 2; 19 | } 20 | -------------------------------------------------------------------------------- /src/js/ui/container.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | require('angular-material'); 6 | 7 | 8 | var mod = angular.module('lovecall/ui/container', ['ngMaterial']); 9 | 10 | mod.controller('ContainerController', function($scope, $mdMedia) { 11 | $scope.$mdMedia = $mdMedia; 12 | }); 13 | -------------------------------------------------------------------------------- /src/css/_content.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | } 6 | 7 | .content-container { 8 | @mixin reserve-space-for-locked-sidenav; 9 | 10 | flex: 1; 11 | padding-top: $metadata-height / 2; 12 | background-image: url('images/background.jpg'); 13 | background-size: cover; 14 | } 15 | 16 | .main-container { 17 | width: 100%; 18 | position: relative; 19 | flex: 1; 20 | padding: 8px; 21 | } 22 | -------------------------------------------------------------------------------- /plugins/android.json: -------------------------------------------------------------------------------- 1 | { 2 | "prepare_queue": { 3 | "installed": [], 4 | "uninstalled": [] 5 | }, 6 | "config_munge": { 7 | "files": {} 8 | }, 9 | "installed_plugins": { 10 | "cordova-plugin-whitelist": { 11 | "PACKAGE_NAME": "moe.lovecall.android" 12 | }, 13 | "cordova-plugin-fileopener": { 14 | "PACKAGE_NAME": "moe.lovecall.android" 15 | } 16 | }, 17 | "dependent_plugins": {} 18 | } -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | LoveCall 4 | 5 | LoveCall Android app 6 | 7 | 8 | LoveCallProject 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /plugins/fetch.json: -------------------------------------------------------------------------------- 1 | { 2 | "cordova-plugin-whitelist": { 3 | "source": { 4 | "type": "registry", 5 | "id": "cordova-plugin-whitelist" 6 | }, 7 | "is_top_level": true, 8 | "variables": {} 9 | }, 10 | "cordova-plugin-fileopener": { 11 | "source": { 12 | "type": "registry", 13 | "id": "cordova-plugin-fileopener" 14 | }, 15 | "is_top_level": true, 16 | "variables": {} 17 | } 18 | } -------------------------------------------------------------------------------- /src/css/_config.css: -------------------------------------------------------------------------------- 1 | .config { 2 | min-width: 500px; 3 | } 4 | 5 | .config__toolbar__icon { 6 | margin: 0px 10px 0px 10px; 7 | } 8 | 9 | .config__content { 10 | padding: 10px; 11 | overflow: hidden; 12 | } 13 | 14 | .config__content__value { 15 | font-weight: bold; 16 | } 17 | 18 | .config__preference--switch { 19 | display: flex; 20 | align-items: center; 21 | } 22 | 23 | .config__preference__desc { 24 | flex: 1 0 auto; 25 | } 26 | 27 | .config__submit { 28 | float: right; 29 | } 30 | -------------------------------------------------------------------------------- /src/js/provider/resize-detector.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | var elementResizeDetector = require('element-resize-detector'); 6 | 7 | 8 | var mod = angular.module('lovecall/provider/resize-detector', [ 9 | ]); 10 | 11 | mod.factory('ResizeDetector', function() { 12 | return elementResizeDetector(); 13 | }); 14 | /* @license-end */ 15 | 16 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 17 | -------------------------------------------------------------------------------- /src/css/_song-selector.css: -------------------------------------------------------------------------------- 1 | .song-selector { 2 | width: 50rem; 3 | max-height: 80rem; 4 | } 5 | 6 | .md-dialog-fullscreen { 7 | max-height: 100vh; 8 | } 9 | 10 | .toolbar__icon { 11 | margin: 1rem; 12 | } 13 | 14 | .toolbar__space { 15 | flex: 1; 16 | } 17 | 18 | .song-selector__content { 19 | max-height: 600px; 20 | } 21 | 22 | .song-selector__content__button { 23 | text-align: start; 24 | text-transform: none; 25 | width: 100%; 26 | margin: 0; 27 | border-radius: 0; 28 | .md-ripple-container { 29 | border-radius: 0; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lovecall-frontend", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/LoveCallProject/lovecall-frontend", 5 | "authors": [ 6 | "xen0n" 7 | ], 8 | "description": "Frontend to LoveCall", 9 | "keywords": [ 10 | "lovecall", 11 | "frontend" 12 | ], 13 | "license": "GPL-3", 14 | "ignore": [ 15 | "**/.*", 16 | "build", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "angular-logex": "~0.0.10", 24 | "material-design-icons": "~2.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/templates/song-loading.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 |

{{message}}

10 |
11 | 12 | 13 | 好吧 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/css/_transport.css: -------------------------------------------------------------------------------- 1 | #transport { 2 | margin-top:2rem; 3 | width: 100%; 4 | position: absolute; 5 | bottom: 0; 6 | left: 0; 7 | 8 | /* padding is necessary because position is absolute */ 9 | padding: 0 8px 8px; 10 | } 11 | 12 | .transport__canvas-container { 13 | width: 100%; 14 | height: 3rem; 15 | 16 | > canvas { 17 | width: 100%; 18 | height: 100%; 19 | cursor: pointer; 20 | } 21 | } 22 | 23 | #transport__play { 24 | display: flex; 25 | margin: auto; 26 | } 27 | 28 | .transport__volume { 29 | display: flex; 30 | flex-direction: row; 31 | position: absolute; 32 | bottom: 0; 33 | right: 2rem; 34 | md-slider { 35 | width: 5rem; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/js/ui/about.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | require('angular-material'); 6 | 7 | require('../conf'); 8 | 9 | 10 | var mod = angular.module('lovecall/ui/about', [ 11 | 'ngMaterial', 12 | 'lovecall/conf', 13 | ]); 14 | 15 | mod.controller('AboutDialogController', function($scope, $mdDialog, LCConfig) { 16 | $scope.version = LCConfig.VERSION + ' (' + LCConfig.HASH + ')'; 17 | 18 | $scope.contributors = [ 19 | { 20 | name: 'xen0n' 21 | }, 22 | { 23 | name: 'disoul' 24 | }, 25 | { 26 | name: 'fakedestinyck' 27 | } 28 | ] 29 | 30 | $scope.close = function() { 31 | $mdDialog.cancel(); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/js/util/data-helper.js: -------------------------------------------------------------------------------- 1 | // modified from SO answer http://stackoverflow.com/a/5100158/596531 2 | module.exports.dataUriToArray = function(dataURI) { 3 | // convert base64/URLEncoded data component to raw binary data held in a string 4 | var byteString; 5 | if (dataURI.split(',')[0].indexOf('base64') >= 0) 6 | byteString = atob(dataURI.split(',')[1]); 7 | else 8 | byteString = unescape(dataURI.split(',')[1]); 9 | 10 | // separate out the mime component 11 | var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; 12 | 13 | // write the bytes of the string to a typed array 14 | var ia = new Uint8Array(byteString.length); 15 | for (var i = 0; i < byteString.length; i++) { 16 | ia[i] = byteString.charCodeAt(i); 17 | } 18 | 19 | return ia; 20 | }; 21 | -------------------------------------------------------------------------------- /src/css/_navigation.css: -------------------------------------------------------------------------------- 1 | #sidenav { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | .md-button,.md-ripple-container{ 6 | border-radius: 0; 7 | } 8 | } 9 | 10 | md-icon.inline { 11 | display: inline-block; 12 | } 13 | 14 | .hidden-button { 15 | visibility: hidden; 16 | } 17 | 18 | .hidden-android { 19 | display: none; 20 | } 21 | 22 | .line-button { 23 | width: 100%; 24 | margin: 0; 25 | padding: 0.3rem 0 0.3rem 1rem; 26 | text-align: left; 27 | font-size: 18px; 28 | md-icon { 29 | font-size: 30px; 30 | } 31 | } 32 | 33 | .toolbar-container { 34 | height: $toolbar-height; 35 | position: relative; 36 | /* TODO */ 37 | background: #333; 38 | overflow-y: visible; 39 | } 40 | 41 | #toolbar { 42 | background: none; 43 | 44 | @mixin sidenav-locked-open-aware { 45 | display: none; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoveCall 2 | 3 | [![devDependency Status](https://david-dm.org/LoveCallProject/lovecall-frontend/dev-status.svg)](https://david-dm.org/LoveCallProject/lovecall-frontend#info=devDependencies) 4 | 5 | TODO 6 | 7 | 8 | ## License 9 | 10 | * GPLv3+ (see the `COPYING` file for the full text) 11 | 12 | 13 | ## Hacking 14 | 15 | ```sh 16 | # install build and dev tools 17 | npm install -g bower webpack webpack-dev-server 18 | 19 | # install local deps 20 | # material-design-icons is *large*, please be patient and make sure to have 21 | # enough free space 22 | npm install 23 | bower install 24 | 25 | # to build 26 | webpack # -p for production build 27 | 28 | # to fire up the dev server 29 | webpack-dev-server --inline --compress --content-base=public/ 30 | 31 | # invoke webpack with webpack-cordova-plugin enabled (instructions TODO) 32 | BUILD_CORDOVA=1 webpack 33 | ``` 34 | -------------------------------------------------------------------------------- /src/js/ui/config.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | require('angular-material'); 6 | 7 | require('../conf'); 8 | 9 | 10 | var mod = angular.module('lovecall/ui/config', [ 11 | 'ngMaterial', 12 | 'lovecall/conf', 13 | ]); 14 | 15 | mod.controller('ConfigDialogController', function($scope, $mdDialog, LCConfig) { 16 | $scope.bufferSizeOrder = LCConfig.getAudioBufferSizeOrder(); 17 | $scope.useRomaji = LCConfig.isRomajiEnabled(); 18 | 19 | 20 | $scope.$watch('bufferSizeOrder', function(to, from) { 21 | if (to === from) { 22 | return; 23 | } 24 | 25 | LCConfig.setAudioBufferSizeOrder(to); 26 | }); 27 | 28 | 29 | $scope.$watch('useRomaji', function(to, from) { 30 | if (to === from) { 31 | return; 32 | } 33 | 34 | LCConfig.setRomajiEnabled(to); 35 | }); 36 | 37 | 38 | $scope.close = function() { 39 | $mdDialog.cancel(); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # for obvious copyright reasons 2 | *.mp3 3 | *.ogg 4 | *.wav 5 | *.opus 6 | 7 | /public/music/cover/ 8 | 9 | # site config 10 | /lovecall.config.js 11 | 12 | # build intermediates 13 | /build 14 | 15 | # dependencies 16 | /bower_components 17 | 18 | 19 | # Node.gitignore 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (http://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directory 46 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 47 | node_modules 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | -------------------------------------------------------------------------------- /src/js/ui/index.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | 6 | require('./about'); 7 | require('./container'); 8 | require('./config.js'); 9 | require('./call'); 10 | require('./frame'); 11 | require('./metadata'); 12 | require('./navigation'); 13 | require('./song-loading'); 14 | require('./song-selector'); 15 | require('./transport'); 16 | 17 | require('../../templates/index.tmpl.html'); 18 | 19 | 20 | var mod = angular.module('lovecall/ui/index', [ 21 | 'lovecall/ui/about', 22 | 'lovecall/ui/container', 23 | 'lovecall/ui/config', 24 | 'lovecall/ui/call', 25 | 'lovecall/ui/frame', 26 | 'lovecall/ui/metadata', 27 | 'lovecall/ui/navigation', 28 | 'lovecall/ui/song-loading', 29 | 'lovecall/ui/song-selector', 30 | 'lovecall/ui/transport' 31 | ]); 32 | 33 | mod.directive('lovecallApp', function() { 34 | return { 35 | restrict: 'EA', 36 | templateUrl: 'index.tmpl.html', 37 | }; 38 | }); 39 | /* @license-end */ 40 | 41 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 42 | -------------------------------------------------------------------------------- /src/js/ui/song-selector.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | 6 | require('angular'); 7 | require('angular-material'); 8 | 9 | require('../provider/font-selector'); 10 | 11 | 12 | var mod = angular.module('lovecall/ui/song-selector', [ 13 | 'ngMaterial', 14 | 'lovecall/provider/choreography', 15 | 'lovecall/provider/font-selector', 16 | ]); 17 | 18 | mod.controller('SongSelectorController', function($scope, $mdDialog, Choreography, FontSelector) { 19 | 20 | var init = function() { 21 | var songs = Choreography.getAvailableSongs(); 22 | var fontFamilies = _(songs) 23 | .map('lang') 24 | .uniq() 25 | .transform(function(result, v) { 26 | result[v] = FontSelector.fontFamilyForLanguage(v); 27 | }, {}) 28 | .value(); 29 | 30 | $scope.songs = songs; 31 | $scope.fontFamilies = fontFamilies; 32 | }; 33 | 34 | $scope.close = function() { 35 | $mdDialog.cancel(); 36 | } 37 | 38 | $scope.answer = function(answer) { 39 | $mdDialog.hide(answer); 40 | } 41 | 42 | init(); 43 | }); 44 | -------------------------------------------------------------------------------- /src/css/_font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(fonts/MaterialIcons-Regular.eot); /* For IE6-8 */ 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url(fonts/MaterialIcons-Regular.woff2) format('woff2'), 9 | url(fonts/MaterialIcons-Regular.woff) format('woff'), 10 | url(fonts/MaterialIcons-Regular.ttf) format('truetype'); 11 | } 12 | 13 | .material-icons { 14 | font-family: 'Material Icons'; 15 | font-weight: normal; 16 | font-style: normal; 17 | font-size: 24px; /* Preferred icon size */ 18 | display: inline-block; 19 | width: 1em; 20 | height: 1em; 21 | line-height: 1; 22 | text-transform: none; 23 | letter-spacing: normal; 24 | word-wrap: normal; 25 | white-space: nowrap; 26 | direction: ltr; 27 | 28 | /* Support for all WebKit browsers. */ 29 | -webkit-font-smoothing: antialiased; 30 | /* Support for Safari and Chrome. */ 31 | text-rendering: optimizeLegibility; 32 | 33 | /* Support for Firefox. */ 34 | -moz-osx-font-smoothing: grayscale; 35 | 36 | /* Support for IE. */ 37 | font-feature-settings: 'liga'; 38 | } 39 | -------------------------------------------------------------------------------- /src/js/provider/font-selector.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 3 | 'use strict'; 4 | 5 | require('angular'); 6 | 7 | 8 | var BUILTIN_FONT_FAMILIES = { 9 | en: '"Source Sans Pro", "DejaVu Sans", "Bitstream Vera", sans-serif', 10 | cmn: '"Source Han Sans CN", "Source Han Sans SC", "思源黑体 CN", "思源黑体", "Noto Sans CJK SC", "微软雅黑", "Microsoft YaHei", sans-serif', 11 | ja: '"Source Han Sans JP", "源ノ角ゴシック", "Noto Sans CJK JP", "ヒラギノ角ゴ Pro W3", "Hiragino Kaku Gothic Pro",Osaka, "メイリオ", Meiryo, "MS Pゴシック", "MS PGothic", sans-serif', 12 | }; 13 | 14 | 15 | var mod = angular.module('lovecall/provider/font-selector', [ 16 | ]); 17 | 18 | mod.factory('FontSelector', function() { 19 | this.fontFamilyForLanguage = function(language) { 20 | var result = BUILTIN_FONT_FAMILIES[language]; 21 | return typeof(result) !== 'undefined' ? result : 'sans-serif'; 22 | }; 23 | 24 | 25 | this.canvasFontForLanguage = function(language, size) { 26 | return size + 'px ' + this.fontFamilyForLanguage(language); 27 | }; 28 | 29 | 30 | return this; 31 | }); 32 | /* @license-end */ 33 | 34 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 35 | -------------------------------------------------------------------------------- /src/templates/config.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | settings 8 | 9 |

设置

10 | 11 | 12 | close 13 | 14 |
15 | 16 | 17 |
18 |

音频缓冲区大小量级

19 | 27 | 28 | 29 |
30 |

跟唱部分使用罗马字

31 | 36 | 37 |
38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /public/lovecall.html: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | 22 | ラブコール 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/js/data/susutomo.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "lovecall": 0, 3 | "metadata": { 4 | "song": { 5 | "title": "ススメ→トゥモロウ", 6 | "artist": "μ's", 7 | "album": "ススメ→トゥモロウ/START:DASH!!", 8 | "remoteBasename": "susutomo", 9 | "sources": { 10 | "fallback:": { 11 | "offset": 5134 12 | }, 13 | "md5:c58b2892e24d0239a6f31b769c230dac": { 14 | "offset": 5134 15 | } 16 | }, 17 | "timing": [ 18 | [0, 100.0, 4, 4, 0], 19 | [21178, 190.0, 4, 4, 9] 20 | ] 21 | }, 22 | "palette": [ 23 | "#ffa500", 24 | "#eeeeee", 25 | "#0000ff" 26 | ] 27 | }, 28 | "form": [ 29 | [0, 0, 9, 0, "I"], 30 | [9, 0, 25, 0, "G0"], 31 | [25, 0, 43, 0, "A1"], 32 | [43, 0, 61, 0, "B1"], 33 | [53, 0, 71, 0, "C1"], 34 | [69, 0, 80, 0, "G1"], 35 | [80, 0, 97, 0, "A2"], 36 | [97, 0, 107, 0, "B2"], 37 | [107, 0, 125, 0, "C2"], 38 | [125, 0, 132, 0, "G2"], 39 | [132, 0, 148, 0, "S1"], 40 | [148, 0, 167, 0, "G3"], 41 | [167, 0, 176, 0, "S2"], 42 | [176, 0, 190, 0, "C3"], 43 | [190, 0, -1, -1, "O"] 44 | ], 45 | "colors": [ 46 | [0, 0, -1, -1, -1] 47 | ], 48 | "timeline": [ 49 | [1, 0, 0, 16, 0, "上举", 16] 50 | ] 51 | }; 52 | -------------------------------------------------------------------------------- /src/js/ui/song-loading.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | require('angular-material'); 6 | 7 | var mod = angular.module('lovecall/ui/song-loading', [ 8 | 'ngMaterial', 9 | ]); 10 | 11 | mod.controller('SongLoadingController', function($scope, $timeout, $mdDialog) { 12 | $scope.message = '歌曲加载中…'; 13 | $scope.progressVisibility = 'visible'; 14 | $scope.loadFailed = false; 15 | 16 | 17 | var closeDialog = function() { 18 | $mdDialog.hide(null); 19 | }; 20 | 21 | 22 | $scope.closeDialog = closeDialog; 23 | 24 | 25 | $scope.$on('audio:decoding', function(e) { 26 | $scope.message = '音频解码中…(移动设备下可能非常缓慢,请耐心等候)'; 27 | }); 28 | 29 | 30 | $scope.$on('audio:loadFailed', function(e) { 31 | $scope.message = '音频解码失败,请刷新重试或更换浏览器'; 32 | $scope.loadFailed = true; 33 | }); 34 | 35 | 36 | $scope.$on('song:hideLoadingDialog', function(e, errored) { 37 | // hide progress indicator 38 | $scope.progressVisibility = 'hidden'; 39 | 40 | if (!errored) { 41 | $scope.message = '歌曲加载完成'; 42 | $timeout(closeDialog, 1000); 43 | } else { 44 | $scope.message = '歌曲加载失败'; 45 | $scope.loadFailed = true; 46 | } 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | require('angular-animate'); 6 | require('angular-aria'); 7 | require('angular-material'); 8 | require('angular-logex'); 9 | require('angular-local-storage'); 10 | 11 | require('./ui/index'); 12 | require('./init'); 13 | require('./update.js'); 14 | 15 | require('../../node_modules/angular-material/angular-material.css'); 16 | require('../css/index.css'); 17 | 18 | var mod = angular.module('lovecall/main', [ 19 | 'lovecall/init', 20 | 'lovecall/update', 21 | 'lovecall/ui/index', 22 | 'log.ex.uo', 23 | 'LocalStorageModule', 24 | ]); 25 | 26 | mod.config(function(logExProvider) { 27 | logExProvider.enableLogging(true); 28 | 29 | logExProvider.overrideLogPrefix(function(className) { 30 | var timeFrag = '[' + new Date().toISOString() + '] '; 31 | var classFrag = angular.isString(className) ? '[' + className + '] ' : ''; 32 | 33 | return timeFrag + classFrag; 34 | }); 35 | }); 36 | 37 | mod.config(function(localStorageServiceProvider) { 38 | localStorageServiceProvider.setPrefix('lovecall.'); 39 | }); 40 | 41 | angular.bootstrap(angular.element(document.getElementById('appmount')), ['lovecall/main', 'ngMaterial']); 42 | /* @license-end */ 43 | 44 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lovecall-frontend", 3 | "version": "0.0.1", 4 | "description": "Frontend to LoveCall", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/LoveCallProject/lovecall-frontend.git" 13 | }, 14 | "keywords": [ 15 | "lovecall", 16 | "frontend" 17 | ], 18 | "author": "xen0n", 19 | "license": "GPL-3", 20 | "bugs": { 21 | "url": "https://github.com/LoveCallProject/lovecall-frontend/issues" 22 | }, 23 | "homepage": "https://github.com/LoveCallProject/lovecall-frontend", 24 | "devDependencies": { 25 | "angular": "^1.4.8", 26 | "angular-animate": "^1.4.8", 27 | "angular-aria": "^1.4.8", 28 | "angular-local-storage": "^0.2.2", 29 | "angular-material": "^1.0.1", 30 | "autoprefixer": "^6.3.1", 31 | "css-loader": "^0.23.1", 32 | "element-resize-detector": "^1.0.3", 33 | "file-loader": "^0.8.5", 34 | "html-webpack-plugin": "^1.7.0", 35 | "lodash": "^4.0.0", 36 | "ng-annotate-webpack-plugin": "^0.1.2", 37 | "ng-cache-loader": "0.0.15", 38 | "postcss-loader": "^0.8.0", 39 | "precss": "^1.4.0", 40 | "script-loader": "^0.6.1", 41 | "spark-md5": "^2.0.0", 42 | "style-loader": "^0.13.0", 43 | "url-loader": "^0.5.7", 44 | "webpack": "^1.12.11", 45 | "webpack-cordova-plugin": "^0.1.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/js/init.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | 6 | require('./provider/choreography'); 7 | require('./ui/frame'); 8 | 9 | var mimimi = require('./data/mimimi'); 10 | var snowhare = require('./data/snowhare'); 11 | var wr = require('./data/wr'); 12 | //var susutomo = require('./data/susutomo'); 13 | var startDash = require('./data/start-dash'); 14 | var nbg = require('./data/nbg'); 15 | var bokuima = require('./data/bokuima'); 16 | var kiseki = require('./data/kiseki'); 17 | 18 | // easter egg 19 | var fdInnerOni = require('./data/fd-inner-oni'); 20 | 21 | 22 | var mod = angular.module('lovecall/init', [ 23 | 'lovecall/provider/choreography', 24 | 'lovecall/ui/frame' 25 | ]); 26 | 27 | mod.run(function($window, Choreography, FrameManager) { 28 | // load bundled call tables 29 | Choreography.loadTable(mimimi); 30 | Choreography.loadTable(snowhare); 31 | Choreography.loadTable(wr); 32 | Choreography.loadTable(bokuima); 33 | Choreography.loadTable(kiseki); 34 | //Choreography.loadTable(susutomo); 35 | Choreography.loadTable(startDash); 36 | Choreography.loadTable(nbg); 37 | 38 | // easter egg 39 | if (Math.random() < 0.1) { 40 | Choreography.loadTable(fdInnerOni); 41 | } 42 | 43 | // frame loop 44 | FrameManager.startFrameLoop($window); 45 | }); 46 | /* @license-end */ 47 | 48 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 49 | -------------------------------------------------------------------------------- /src/templates/about.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | info 8 | 9 |

关于

10 | 11 | 12 | close 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 |

LoveCall

21 |

Version:{{version}}

22 |
23 |
24 | 25 | 26 | 27 | 31 | 32 | person 33 | 34 |
35 |

{{ contributor.name }}

36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/js/choreography/manager.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | 6 | 7 | var LoveCallTableManager = function() { 8 | this.tables = {}; 9 | this.nextTableIdx = 0; 10 | 11 | this.hashCache = {}; 12 | this.songCache = {}; 13 | }; 14 | 15 | 16 | LoveCallTableManager.prototype.registerTable = function(table) { 17 | var idx = this.nextTableIdx; 18 | this.tables[idx] = table; 19 | this.nextTableIdx += 1; 20 | 21 | var hashMap = _.transform(table.metadata.song.sources, function(result, v, k) { 22 | result[k.toLowerCase()] = idx; 23 | }); 24 | 25 | _.extend(this.hashCache, hashMap); 26 | 27 | this.songCache[idx] = { 28 | ti: table.metadata.song.title, 29 | ar: table.metadata.song.artist, 30 | al: table.metadata.song.album, 31 | lang: table.metadata.song.lang, 32 | idx: idx 33 | }; 34 | 35 | return idx; 36 | }; 37 | 38 | 39 | LoveCallTableManager.prototype.lookupTable = function(idx, hash) { 40 | var lookupKey = hash.toLowerCase(); 41 | 42 | if (hash === 'fallback:') { 43 | return this.tables[idx]; 44 | } 45 | 46 | var tableByHash = this.tables[this.hashCache[lookupKey]]; 47 | return typeof(tableByHash) !== 'undefined' ? tableByHash : this.tables[idx]; 48 | }; 49 | 50 | 51 | LoveCallTableManager.prototype.getAvailableSongs = function() { 52 | // return a copy 53 | return _.extend({}, this.songCache); 54 | } 55 | 56 | 57 | module.exports = { 58 | LoveCallTableManager: LoveCallTableManager 59 | }; 60 | /* @license-end */ 61 | 62 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 63 | -------------------------------------------------------------------------------- /src/js/ui/dpi.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | 6 | 7 | var mod = angular.module('lovecall/ui/dpi', []); 8 | 9 | mod.factory('DPIManager', function($window, $log) { 10 | $log = $log.getInstance('DPIManager'); 11 | 12 | var devicePixelRatio = $window.devicePixelRatio || 1; 13 | var canvasBackingStoreRatio = (function() { 14 | var testCanvas = document.createElement('canvas'); 15 | var ctx = testCanvas.getContext('2d'); 16 | var result = ( 17 | ctx.webkitBackingStorePixelRatio || 18 | ctx.mozBackingStorePixelRatio || 19 | ctx.msBackingStorePixelRatio || 20 | ctx.oBackingStorePixelRatio || 21 | ctx.backingStorePixelRatio || 22 | 1 23 | ); 24 | testCanvas = null; 25 | return result; 26 | })(); 27 | 28 | var ratio = devicePixelRatio / canvasBackingStoreRatio; 29 | 30 | 31 | var scaleCanvas = function(canvas, ctx, w, h) { 32 | canvas.width = w * ratio; 33 | canvas.height = h * ratio; 34 | 35 | if (devicePixelRatio !== canvasBackingStoreRatio) { 36 | ctx.setTransform(ratio, 0, 0, ratio, 0, 0); 37 | } 38 | }; 39 | 40 | 41 | $log.info( 42 | 'devicePixelRatio', 43 | devicePixelRatio, 44 | 'canvasBackingStoreRatio', 45 | canvasBackingStoreRatio 46 | ); 47 | 48 | return { 49 | devicePixelRatio: devicePixelRatio, 50 | canvasBackingStoreRatio: canvasBackingStoreRatio, 51 | ratio: ratio, 52 | scaleCanvas: scaleCanvas, 53 | }; 54 | }); 55 | /* @license-end */ 56 | 57 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 58 | -------------------------------------------------------------------------------- /src/js/ui/images.js: -------------------------------------------------------------------------------- 1 | var fu = require('../../images/fu.svg'); 2 | var fuwa = require('../../images/fuwa.svg'); 3 | var hh = require('../../images/hh.svg'); 4 | var hi = require('../../images/hi.svg'); 5 | var jump = require('../../images/jump.svg'); 6 | var kh = require('../../images/kh.svg'); 7 | var ld = require('../../images/ld.svg'); 8 | var lt = require('../../images/lt.svg'); 9 | var oh = require('../../images/oh.svg'); 10 | var qh = require('../../images/qh.svg'); 11 | var sj = require('../../images/sj.svg'); 12 | var special = require('../../images/special.svg'); 13 | var clap = require('../../images/clap.svg'); 14 | var d = require('../../images/d.svg'); 15 | var k = require('../../images/k.svg'); 16 | 17 | var taicallImages = {}; 18 | 19 | 20 | var makeImageObj = function(key, uri) { 21 | var result = new Image(100, 100); 22 | result.src = uri; 23 | result.onload = function() { 24 | console.log('builtin image onload'); 25 | taicallImages[key] = result; 26 | }; 27 | return result; 28 | }; 29 | 30 | 31 | var objects = [ 32 | makeImageObj('fu', fu), 33 | makeImageObj('fuwa', fuwa), 34 | makeImageObj('hh', hh), 35 | makeImageObj('hi', hi), 36 | makeImageObj('jump', jump), 37 | makeImageObj('kh', kh), 38 | makeImageObj('ld', ld), 39 | makeImageObj('lt', lt), 40 | makeImageObj('oh', oh), 41 | makeImageObj('qh', qh), 42 | makeImageObj('sj', sj), 43 | makeImageObj('special', special), 44 | makeImageObj('clap', clap), 45 | makeImageObj('d', d), 46 | makeImageObj('k', k), 47 | ]; 48 | 49 | var taicallImagesCount = objects.length; 50 | 51 | module.exports = { 52 | taicall: taicallImages, 53 | taicallImagesCount: taicallImagesCount, 54 | }; 55 | -------------------------------------------------------------------------------- /src/css/_metadata.css: -------------------------------------------------------------------------------- 1 | #metadata { 2 | display: flex; 3 | flex-direction: row; 4 | z-index: 1; 5 | overflow: visible; 6 | position: absolute; 7 | bottom: -$metadata-height / 2; 8 | width: 100%; 9 | padding: 0 8px; 10 | } 11 | 12 | #metadata.sidenav-locked-open { 13 | padding-left: 8px + $md-sidenav-locked-open-width; 14 | } 15 | 16 | .metadata__image-container, 17 | .metadata__image { 18 | width: $metadata-height; 19 | height: $metadata-height; 20 | } 21 | 22 | .metadata__image-container { 23 | border-radius: 50%; 24 | overflow: hidden; 25 | 26 | background-color: #666; 27 | box-shadow: 0 2px 5px #000; 28 | } 29 | 30 | .metadata__image { 31 | background-size: 100%; 32 | 33 | /* very weird bug of Chromium */ 34 | border-radius: 50%; 35 | 36 | animation-name: rotate; 37 | animation-iteration-count: infinite; 38 | animation-timing-function: linear; 39 | animation-play-state: paused; 40 | } 41 | 42 | @keyframes rotate { 43 | from { 44 | transform: rotate(0); 45 | } 46 | to { 47 | transform: rotate(360deg); 48 | } 49 | } 50 | 51 | .metadata__text { 52 | width: 100%; 53 | flex: 1; 54 | position: relative; 55 | } 56 | 57 | .metadata__text__title-container { 58 | position: absolute; 59 | bottom: 50%; 60 | left: 0; 61 | } 62 | 63 | .metadata__text__other { 64 | position: absolute; 65 | top: 50%; 66 | left: 0; 67 | } 68 | 69 | .metadata__text__other, 70 | .metadata__text__title-container { 71 | padding-left: 0.5rem; 72 | } 73 | 74 | .metadata__text__title { 75 | margin: 0; 76 | color: rgba(255, 255, 255, 0.87); 77 | } 78 | 79 | .metadata__text__other__artist { 80 | &:after { 81 | content: ' /'; 82 | } 83 | 84 | &:empty:after { 85 | display: none; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/templates/song-selector.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | queue_music 12 | 13 |

选择想练习的歌曲

14 |
15 | 16 | 17 | close 18 | 19 | 20 |
21 | 22 | 23 | 24 | 32 | 35 | 39 | music_note 40 | 41 |
42 |

{{ song.ti }}

43 |

{{ song.ar }}

44 |

{{ song.al }}

45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /src/js/update.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | 6 | require('./conf'); 7 | 8 | var mod = angular.module('lovecall/update', [ 9 | 'ngMaterial', 10 | 'lovecall/conf', 11 | ]); 12 | 13 | mod.run(function($http, $mdDialog, LCConfig) { 14 | // only check in cordova 15 | if (window.cordova !== undefined) { 16 | $http({ 17 | method: 'GET', 18 | url: 'http://lovecall.moe/static/version.json' 19 | }).then(function successCallback(res) { 20 | var versionInfo = res.data; 21 | console.log(versionInfo); 22 | if (versionInfo.version > LCConfig.VERSION) { 23 | var confirm = $mdDialog.confirm() 24 | .title('LoveCall有新版本v' + versionInfo.version + ',是否更新') 25 | .textContent('更新内容:' + versionInfo.content) 26 | .ariaLabel('Lucky day') 27 | .ok('立即更新') 28 | .openFrom({ 29 | top: document.body.height / 2, 30 | width: 30, 31 | height: 80 32 | }) 33 | .closeTo({ 34 | top: document.body.height / 2 35 | }) 36 | .cancel('下次再说') 37 | 38 | $mdDialog.show(confirm).then(function() { 39 | window.cordova.plugins.FileOpener.openFile( 40 | LCConfig.REMOTE_APK_PREFIX + 'lovecall' + versionInfo.version +'.apk', 41 | function() { 42 | // success 43 | } 44 | ); 45 | 46 | console.log("let's update"); 47 | }, function() {}); 48 | } 49 | 50 | }, function errorCallback(res) { 51 | 52 | }); 53 | } 54 | }); 55 | /* @license-end */ 56 | 57 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 58 | -------------------------------------------------------------------------------- /src/images/d.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/images/k.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/js/provider/mouseevent.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | 6 | 7 | var mod = angular.module('lovecall/provider/mouseevent', [ 8 | ]); 9 | 10 | mod.factory('MouseEvent', function($window) { 11 | var mouseEventCallbackHandlerFactory = function(eventName) { 12 | var listeners = {}; 13 | var id = 0; 14 | 15 | var callback = function(e) { 16 | for (var k in listeners) { 17 | listeners[k](e); 18 | } 19 | }; 20 | 21 | 22 | var add = function(callback) { 23 | var usedId = id; 24 | listeners[usedId] = callback; 25 | id += 1; 26 | return usedId; 27 | }; 28 | 29 | 30 | var remove = function(index) { 31 | delete listeners[index]; 32 | } 33 | 34 | 35 | $window.addEventListener(eventName, callback); 36 | 37 | return { 38 | add: add, 39 | remove: remove 40 | }; 41 | }; 42 | 43 | 44 | var mouseMoveHandler = mouseEventCallbackHandlerFactory('mousemove'); 45 | var mouseDownHandler = mouseEventCallbackHandlerFactory('mousedown'); 46 | var mouseUpHandler = mouseEventCallbackHandlerFactory('mouseup'); 47 | 48 | var touchMoveHandler = mouseEventCallbackHandlerFactory('touchmove'); 49 | var touchEndHandler = mouseEventCallbackHandlerFactory('touchend'); 50 | var touchCancelHandler = mouseEventCallbackHandlerFactory('touchcancel'); 51 | 52 | 53 | return { 54 | addMouseMoveListener: mouseMoveHandler.add, 55 | removeMouseMoveListener: mouseMoveHandler.remove, 56 | addMouseDownListener: mouseDownHandler.add, 57 | removeMouseDownListener: mouseDownHandler.remove, 58 | addMouseUpListener: mouseUpHandler.add, 59 | removeMouseUpListener: mouseUpHandler.remove, 60 | addTouchMoveListener: touchMoveHandler.add, 61 | removeTouchMoveListener: touchMoveHandler.remove, 62 | addTouchEndListener: touchEndHandler.add, 63 | removeTouchEndListener: touchEndHandler.remove, 64 | addTouchCancelListener: touchCancelHandler.add, 65 | removeTouchCancelListener: touchCancelHandler.remove, 66 | }; 67 | }); 68 | /* @license-end */ 69 | 70 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 71 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var webpack = require('webpack'); 6 | var ngAnnotatePlugin = require('ng-annotate-webpack-plugin'); 7 | var htmlWebpackPlugin = require('html-webpack-plugin'); 8 | var CordovaPlugin = require('webpack-cordova-plugin'); 9 | var autoprefixer = require('autoprefixer'); 10 | var precss = require('precss'); 11 | 12 | var useCordova = (function() { 13 | var tmp = parseInt(process.env.BUILD_CORDOVA); 14 | return isNaN(tmp) ? false : tmp !== 0; // fsck JS 15 | })(); 16 | 17 | 18 | module.exports = { 19 | resolve: { 20 | root: [ 21 | path.join(__dirname, 'node_modules'), 22 | path.join(__dirname, "bower_components"), 23 | ] 24 | }, 25 | plugins: [ 26 | new webpack.ExtendedAPIPlugin(), 27 | new webpack.ResolverPlugin( 28 | new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin("bower.json", ["main"]) 29 | ), 30 | new ngAnnotatePlugin({ 31 | add: true 32 | }), 33 | new htmlWebpackPlugin({ 34 | template: 'public/lovecall.html', 35 | hash: true, 36 | inject: 'body', 37 | minify: { 38 | removeComments: true, 39 | collapseWhitespace: true, 40 | caseSensitive: true, 41 | }, 42 | }), 43 | ], 44 | 45 | devServer: { 46 | contentBase: './build', 47 | }, 48 | 49 | entry: "./src/js/main.js", 50 | output: { 51 | path: path.join(__dirname, 'build'), 52 | filename: "assets/bundle.js", 53 | }, 54 | module: { 55 | loaders: [ 56 | { test: /\.css$/, loader: "style!css!postcss" }, 57 | { test: /\.tmpl\.html$/, loader: "ng-cache?-conservativeCollapse&-preserveLineBreaks" }, 58 | { test: /\.(jpg|png|woff|woff2|eot|ttf|svg|opus)$/, loader: 'url-loader?limit=100000' }, 59 | ] 60 | }, 61 | postcss: function() { 62 | return [autoprefixer, precss]; 63 | } 64 | }; 65 | 66 | 67 | if (useCordova) { 68 | module.exports.plugins.push( 69 | new CordovaPlugin({ 70 | config: 'config.xml', 71 | src: 'index.html', 72 | platform: 'android', 73 | version: true, 74 | }) 75 | ); 76 | } 77 | 78 | /* @license-end */ 79 | 80 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 81 | -------------------------------------------------------------------------------- /src/js/ui/frame.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | 6 | 7 | var mod = angular.module('lovecall/ui/frame', []); 8 | 9 | mod.factory('FrameManager', function($rootScope, $window, $log) { 10 | // states 11 | var frameCallbacks = []; 12 | var playbackPosMeasure = 0; 13 | var playbackPosStep = 0; 14 | 15 | $log = $log.getInstance('FrameManager'); 16 | 17 | 18 | var requestAnimFrame = (function(window) { 19 | return window.requestAnimationFrame || 20 | window.webkitRequestAnimationFrame || 21 | window.mozRequestAnimationFrame || 22 | window.oRequestAnimationFrame || 23 | window.msRequestAnimationFrame || 24 | function(/* function */ callback, /* DOMElement */ element) { 25 | window.setTimeout(callback, 1000 / 60); 26 | }; 27 | })($window); 28 | 29 | 30 | var frameCallback = function(ts) { 31 | requestAnimFrame(frameCallback); 32 | 33 | var i = 0; 34 | for (; i < frameCallbacks.length; i++) { 35 | frameCallbacks[i](ts); 36 | } 37 | }; 38 | 39 | 40 | var addFrameCallback = function(callback) { 41 | frameCallbacks.push(callback); 42 | }; 43 | 44 | 45 | var removeFrameCallback = function(callback) { 46 | var i = 0; 47 | var found = false; 48 | for (i = 0; i < frameCallbacks.length; i++) { 49 | if (frameCallbacks[i] == callback) { 50 | found = true; 51 | break; 52 | } 53 | } 54 | 55 | if (found) { 56 | frameCallbacks.splice(i, 1); 57 | } 58 | }; 59 | 60 | 61 | var startFrameLoop = function() { 62 | $log.info('Starting frame loop with rAF impl', requestAnimFrame); 63 | requestAnimFrame(frameCallback); 64 | }; 65 | 66 | 67 | var tickCallback = function(beat) { 68 | playbackPosMeasure = beat.m; 69 | playbackPosStep = beat.s; 70 | } 71 | 72 | 73 | var getMeasure = function() { 74 | return playbackPosMeasure; 75 | }; 76 | 77 | 78 | var getStep = function() { 79 | return playbackPosStep; 80 | }; 81 | 82 | 83 | return { 84 | 'addFrameCallback': addFrameCallback, 85 | 'removeFrameCallback': removeFrameCallback, 86 | 'startFrameLoop': startFrameLoop, 87 | 'tickCallback': tickCallback, 88 | 'getMeasure': getMeasure, 89 | 'getStep': getStep, 90 | }; 91 | }); 92 | /* @license-end */ 93 | 94 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 95 | -------------------------------------------------------------------------------- /src/js/ui/metadata.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | require('../provider/choreography'); 6 | require('../provider/font-selector'); 7 | 8 | 9 | var mod = angular.module('lovecall/ui/metadata', [ 10 | 'lovecall/provider/choreography', 11 | 'lovecall/provider/font-selector', 12 | ]); 13 | 14 | mod.controller('MetadataController', function($scope, $window, $log, Choreography, FontSelector) { 15 | $log = $log.getInstance('MetadataController'); 16 | 17 | $scope.title = ''; 18 | $scope.artist = ''; 19 | $scope.album = ''; 20 | $scope.lang = ''; 21 | $scope.fontFamily = 'sans-serif'; 22 | $scope.songImage = 'none'; 23 | 24 | var metadataImg = document.querySelector('.metadata__image'); 25 | 26 | 27 | var setCoverArtUrl = function(url) { 28 | $scope.songImage = 'url(' + url + ')'; 29 | }; 30 | 31 | 32 | var setSongImage = function(imageBlob) { 33 | if (!imageBlob) { 34 | $scope.songImage = 'none'; 35 | return; 36 | } 37 | 38 | var url = $window.URL || $window.webkitURL; 39 | setCoverArtUrl(url.createObjectURL(imageBlob)); 40 | }; 41 | 42 | 43 | $scope.$on('song:remoteCoverArtRequest', function(e, url) { 44 | $log.debug('using remote cover art', url); 45 | setCoverArtUrl(url); 46 | }); 47 | 48 | 49 | $scope.$on('audio:unloaded', function(e) { 50 | $scope.title = ''; 51 | $scope.artist = ''; 52 | $scope.album = ''; 53 | setSongImage(null); 54 | }); 55 | 56 | 57 | $scope.$on('audio:loaded', function(e) { 58 | var songMetadata = Choreography.getSongMetadata(); 59 | var tempo = Choreography.getTempo(); 60 | 61 | $scope.title = songMetadata.ti; 62 | $scope.artist = songMetadata.ar; 63 | $scope.album = songMetadata.al; 64 | 65 | var lang = Choreography.getLanguage(); 66 | $scope.lang = lang; 67 | $scope.fontFamily = FontSelector.fontFamilyForLanguage(lang); 68 | 69 | //TODO: change BPM 70 | var rotateDuration = (tempo.stepToTime(20, 0) - tempo.stepToTime(0, 0)) / 1000; 71 | metadataImg.style.animationDuration = rotateDuration + 's'; 72 | }); 73 | 74 | $scope.$on('audio:resume', function(e) { 75 | metadataImg.style.animationPlayState = 'running'; 76 | }); 77 | 78 | $scope.$on('audio:pause', function(e) { 79 | metadataImg.style.animationPlayState = 'paused'; 80 | }); 81 | 82 | 83 | $scope.$on('song:imageLoaded', function(e, imageBlob) { 84 | setSongImage(imageBlob); 85 | }); 86 | 87 | 88 | $log.debug('$scope', $scope); 89 | }); 90 | /* @license-end */ 91 | -------------------------------------------------------------------------------- /src/js/ui/navigation.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | require('angular-material'); 6 | 7 | require('../provider/song'); 8 | 9 | require('../../templates/song-selector.tmpl.html'); 10 | require('../../templates/about.tmpl.html'); 11 | require('../../templates/config.tmpl.html'); 12 | 13 | 14 | var mod = angular.module('lovecall/ui/navigation', [ 15 | 'ngMaterial', 16 | 'lovecall/provider/song', 17 | 'lovecall/provider/choreography' 18 | ]); 19 | 20 | mod.controller('NavigationController', function($scope, $mdSidenav, $mdMedia, $mdDialog, $log, Choreography, Song) { 21 | $log = $log.getInstance('NavigationController'); 22 | 23 | $scope.showSide = function() { 24 | $mdSidenav('sidenav').open(); 25 | }; 26 | 27 | $scope.closeSide = function() { 28 | $mdSidenav('sidenav').close(); 29 | }; 30 | 31 | $scope.$mdMedia = $mdMedia; 32 | 33 | $scope.showSongSelector = function(ev) { 34 | var useFullScreen = ($mdMedia('sm') || $mdMedia('xs')); 35 | $mdDialog.show({ 36 | controller: 'SongSelectorController', 37 | templateUrl: 'song-selector.tmpl.html', 38 | parent: angular.element(document.body), 39 | targetEvent: ev, 40 | clickOutsideToClose: true, 41 | fullscreen: useFullScreen 42 | }).then(function(answer) { 43 | $log.debug('selected song index', answer); 44 | 45 | var basename = Choreography.getSongRemoteBasenameByIndex(answer); 46 | 47 | // load song via Ajax 48 | Song.load(answer, basename); 49 | }, function() { 50 | $log.debug('cancelled song select'); 51 | }); 52 | }; 53 | 54 | $scope.showAboutDialog = function(ev) { 55 | $mdDialog.show({ 56 | controller: 'AboutDialogController', 57 | templateUrl: 'about.tmpl.html', 58 | parent: angular.element(document.body), 59 | targetEvent: ev, 60 | clickOutsideToClose: true 61 | }).then(function(){}, function(){}); 62 | } 63 | 64 | $scope.showConfigDialog = function(ev) { 65 | var useFullScreen = ($mdMedia('sm') || $mdMedia('xs')); 66 | $mdDialog.show({ 67 | controller: 'ConfigDialogController', 68 | templateUrl: 'config.tmpl.html', 69 | parent: angular.element(document.body), 70 | targetEvent: ev, 71 | fullscreen: useFullScreen, 72 | clickOutsideToClose: true 73 | }).then(function() {}, function(){}); 74 | } 75 | 76 | $scope.canGetApp = function() { 77 | var userAgent = window.navigator.userAgent; 78 | var re = /Android (\d+(?:\.\d+)+);/; 79 | 80 | var version = re.exec(userAgent); 81 | if (window.cordova === undefined && version && version[1] > '4.4') { 82 | return true; 83 | } 84 | 85 | return false; 86 | } 87 | 88 | // close navigation drawer when song loading ends 89 | $scope.$on('song:hideLoadingDialog', function(e, errored) { 90 | $scope.closeSide(); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/images/hi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/js/conf.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | require('angular'); 5 | require('angular-local-storage'); 6 | 7 | var siteConf = require('../../lovecall.config'); 8 | 9 | 10 | var mod = angular.module('lovecall/conf', [ 11 | 'LocalStorageModule', 12 | ]); 13 | 14 | mod.factory('LCConfig', function($rootScope, localStorageService) { 15 | var VERSION = '20160112.2'; 16 | var HASH = __webpack_hash__; 17 | 18 | 19 | var getAudioBufferSizeOrder = function() { 20 | var storedSizeOrder = parseInt(localStorageService.get('audioBufferSizeOrder')); 21 | if (isNaN(storedSizeOrder) || storedSizeOrder < 8 || storedSizeOrder > 14) { 22 | storedSizeOrder = 12; 23 | doSetAudioBufferSizeOrder(storedSizeOrder, false); 24 | } 25 | 26 | return storedSizeOrder; 27 | }; 28 | 29 | 30 | var getAudioBufferSize = function() { 31 | return 1 << getAudioBufferSizeOrder(); 32 | }; 33 | 34 | 35 | var setAudioBufferSizeOrder = function(order) { 36 | return doSetAudioBufferSizeOrder(order, true); 37 | }; 38 | 39 | 40 | var doSetAudioBufferSizeOrder = function(order, fireEvent) { 41 | var newSizeOrder = order < 8 ? 8 : order > 14 ? 14 : order; 42 | localStorageService.set('audioBufferSizeOrder', '' + newSizeOrder); 43 | 44 | if (fireEvent) { 45 | $rootScope.$broadcast('config:audioBufferSizeChanged', 1 << order); 46 | } 47 | }; 48 | 49 | 50 | var isRomajiEnabled = function() { 51 | var storedRomajiEnabled = parseInt(localStorageService.get('romajiEnabled')); 52 | if (isNaN(storedRomajiEnabled)) { 53 | storedRomajiEnabled = 0; 54 | doSetRomajiEnabled(false, false); 55 | } 56 | 57 | return !!storedRomajiEnabled; 58 | }; 59 | 60 | 61 | var setRomajiEnabled = function(enabled) { 62 | return doSetRomajiEnabled(enabled, true); 63 | }; 64 | 65 | 66 | var doSetRomajiEnabled = function(enabled, fireEvent) { 67 | localStorageService.set('romajiEnabled', enabled ? '1' : '0'); 68 | fireEvent && $rootScope.$broadcast('config:romajiEnabledChanged', enabled); 69 | }; 70 | 71 | 72 | var getGlobalOffsetMs = function() { 73 | // TODO 74 | return 0; 75 | }; 76 | 77 | 78 | var getKnownLocalSongs = function() { 79 | // TODO 80 | return []; 81 | }; 82 | 83 | 84 | return { 85 | VERSION: VERSION, 86 | HASH: HASH, 87 | REMOTE_MUSIC_PREFIX: siteConf.remoteMusicPrefix, 88 | REMOTE_COVER_ART_PREFIX: siteConf.remoteCoverArtPrefix, 89 | REMOTE_APK_PREFIX: siteConf.remoteApkPrefix, 90 | getAudioBufferSize: getAudioBufferSize, 91 | getAudioBufferSizeOrder: getAudioBufferSizeOrder, 92 | setAudioBufferSizeOrder: setAudioBufferSizeOrder, 93 | isRomajiEnabled: isRomajiEnabled, 94 | setRomajiEnabled: setRomajiEnabled, 95 | getGlobalOffsetMs: getGlobalOffsetMs, 96 | getKnownLocalSongs: getKnownLocalSongs, 97 | }; 98 | }); 99 | /* @license-end */ 100 | 101 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 102 | -------------------------------------------------------------------------------- /src/js/data/snowhare.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "lovecall": 0, 3 | "metadata": { 4 | "song": { 5 | "title": "Snow halation", 6 | "artist": "μ's", 7 | "album": "Snow halation", 8 | "lang": "ja", 9 | "remoteBasename": "snowhare", 10 | "sources": { 11 | "fallback:": { 12 | "offset": 401 13 | }, 14 | "md5:89efa6e06c8a2e310f20cac27ddb1bcc": { 15 | "offset": 2199 16 | }, 17 | "md5:c80b88e686eb5044532d74722bb09069": { 18 | "offset": 401 19 | } 20 | }, 21 | "timing": [ 22 | [0, 173.0, 4, 4, 0] 23 | ] 24 | }, 25 | "palette": [ 26 | "#eeeeee", 27 | "#ffa500" 28 | ] 29 | }, 30 | "form": [ 31 | [0, 0, 9, 0, "I"], 32 | [9, 0, 25, 0, "A1"], 33 | [25, 0, 37, 0, "B1"], 34 | [37, 0, 61, 0, "C1"], 35 | [61, 0, 69, 0, "G1"], 36 | [69, 0, 85, 0, "A2"], 37 | [85, 0, 97, 0, "B2"], 38 | [97, 0, 121, 0, "C2"], 39 | [121, 0, 137, 0, "G2"], 40 | [137, 0, 145, 0, "S"], 41 | [137, 0, 161, 0, "C3"], 42 | [161, 0, 167, 0, "G3"], 43 | [167, 0, -1, -1, "O"] 44 | ], 45 | "colors": [ 46 | [-2, -2, 137, 0, 0], 47 | [137, 0, -1, -1, 1] 48 | ], 49 | "timeline": [ 50 | [1, 0, 0, 16, 0, "上举", 16], 51 | [0, 16, 0, "fufu"], 52 | [0, 16, 8, "fufu"], 53 | [1, 17, 0, 23, 0, "里打"], 54 | [0, 23, 0, "警报"], 55 | [1, 25, 0, 29, 0, "PPPH", "OOOH"], 56 | [1, 29, 0, 35, 0, "里跳", true], 57 | [1, 35, 0, 36, 0, "快挥"], 58 | [1, 36, 0, 37, 0, "前挥", 8], 59 | [1, 36, 0, 36, 8, "跟唱", "な", "Na"], 60 | [1, 36, 8, 37, 0, "跟唱", "ぜ", "ze"], 61 | [1, 37, 0, 43, 14, "里打"], 62 | [1, 44, 0, 45, 0, "前挥", 8], 63 | [1, 43, 14, 44, 4, "跟唱", "Snow"], 64 | [1, 44, 4, 45, 0, "跟唱", "halation"], 65 | [1, 45, 0, 53, 0, "里打"], 66 | [1, 53, 0, 59, 0, "前挥"], 67 | [1, 59, 0, 63, 0, "上举", 32], 68 | [1, 63, 0, 65, 0, "欢呼"], 69 | [1, 65, 0, 68, 0, "里跳", true], 70 | [0, 68, 0, "fufu"], 71 | [0, 68, 8, "fufu"], 72 | [1, 69, 0, 77, 0, "上举", 16], 73 | [1, 77, 0, 83, 0, "里打"], 74 | [0, 83, 0, "警报"], 75 | [1, 85, 0, 89, 0, "PPPH", "OOOH"], 76 | [1, 89, 0, 95, 0, "里跳", true], 77 | [1, 95, 0, 96, 0, "快挥"], 78 | [1, 96, 0, 97, 0, "前挥", 8], 79 | [1, 96, 0, 96, 8, "跟唱", "Fly"], 80 | [1, 96, 8, 97, 0, "跟唱", "high"], 81 | [1, 97, 0, 103, 14, "里打"], 82 | [1, 104, 0, 105, 0, "前挥", 8], 83 | [1, 103, 14, 104, 4, "跟唱", "True"], 84 | [1, 104, 4, 105, 0, "跟唱", "emotion"], 85 | [1, 105, 0, 113, 0, "里打"], 86 | [1, 113, 0, 119, 0, "前挥"], 87 | [1, 119, 0, 137, 0, "上举", 32], 88 | [1, 137, 0, 143, 14, "上举", 16], 89 | [1, 144, 0, 145, 0, "前挥", 8], 90 | [1, 143, 14, 144, 4, "跟唱", "Snow"], 91 | [1, 144, 4, 145, 0, "跟唱", "halation"], 92 | [1, 145, 0, 153, 0, "里跳"], 93 | [1, 153, 0, 159, 0, "前挥"], 94 | [1, 159, 0, 163, 0, "上举", 32], 95 | [1, 163, 0, 174, 0, "里跳", true], 96 | [0, 174, 0, "fufu"], 97 | [0, 174, 8, "fufu"], 98 | [1, 175, 4, 183, 4, "上举", 128] 99 | ] 100 | }; 101 | -------------------------------------------------------------------------------- /src/images/oh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/images/fu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/js/choreography/tempo.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | 5 | var tempoFactory = function(timingSpec, globalOffsetMs) { 6 | var timingSections = timingSpec.map(function(v) { 7 | // [offset, bpm, timeSignNumer, timeSignDenom, startingMeasure] 8 | // to 9 | // [offsetMs, stepMs, startingMeasure, stepsPerMeasure, stepsPerStep] 10 | // where a "step" is an 1/16 note 11 | var timeSignNumer = v[2]; 12 | var timeSignDenom = v[3]; 13 | 14 | var beatMs = 60000 / v[1]; 15 | var stepsPerStep = 16 / timeSignDenom; 16 | var stepsPerMeasure = timeSignNumer * stepsPerStep; 17 | var stepMs = beatMs / stepsPerStep; 18 | 19 | return [v[0] + globalOffsetMs, stepMs, v[4], stepsPerMeasure, stepsPerStep]; 20 | }); 21 | 22 | console.log('tempo: timingSections=', timingSections); 23 | 24 | return { 25 | timingSections: timingSections, 26 | timeToStep: function(posMs) { 27 | // find out the timing section to use 28 | var sectionIdx; 29 | var section; 30 | if (posMs < timingSections[0][0]) { 31 | // just use the first timing section for leading 32 | sectionIdx = 0; 33 | section = timingSections[0]; 34 | } else { 35 | for (sectionIdx = timingSections.length - 1; sectionIdx >= 0; sectionIdx--) { 36 | section = timingSections[sectionIdx]; 37 | if (section[0] <= posMs) { 38 | break; 39 | } 40 | } 41 | } 42 | 43 | var posAfterOffset = posMs - section[0]; 44 | var stepMs = section[1]; 45 | var startingMeasure = section[2]; 46 | var stepsPerMeasure = section[3]; 47 | 48 | var totalSteps = (posAfterOffset / stepMs)|0; 49 | var measure = (startingMeasure + totalSteps / stepsPerMeasure)|0; 50 | var step = (totalSteps % stepsPerMeasure)|0; 51 | return { 52 | m: (totalSteps < 0 ? measure - 1 : measure)|0, 53 | s: (step < 0 ? step + stepsPerMeasure : step)|0, 54 | i: sectionIdx 55 | }; 56 | }, 57 | stepToTime: function(measure, step) { 58 | var sectionIdx = 0; 59 | var section = timingSections[timingSections.length - 1]; 60 | 61 | for (; sectionIdx < timingSections.length; sectionIdx++) { 62 | if (measure < timingSections[sectionIdx][2]) { 63 | var prevIdx = sectionIdx - 1; 64 | section = timingSections[prevIdx < 0 ? 0 : prevIdx]; 65 | break; 66 | } 67 | } 68 | 69 | var measureInSection = measure - section[2]; 70 | var stepsPerMeasure = section[3]; 71 | var stepMs = section[1]; 72 | return section[0] + stepMs * (stepsPerMeasure * measureInSection + step); 73 | } 74 | }; 75 | }; 76 | 77 | 78 | var stepAdd = function(one, other, stepsPerMeasure) { 79 | stepsPerMeasure || (stepsPerMeasure = 16); 80 | 81 | var newMeasure = one.m + other.m; 82 | var newStep = one.s + other.s; 83 | newMeasure += Math.floor(newStep / stepsPerMeasure); 84 | return { 85 | m: newMeasure, 86 | s: newStep % stepsPerMeasure 87 | }; 88 | }; 89 | 90 | 91 | var stepCompare = function(one, other, stepsPerMeasure) { 92 | stepsPerMeasure || (stepsPerMeasure = 16); 93 | 94 | return (one.m * stepsPerMeasure + one.s) - (other.m * stepsPerMeasure + other.s); 95 | }; 96 | 97 | 98 | module.exports = { 99 | 'tempoFactory': tempoFactory, 100 | 'stepAdd': stepAdd, 101 | 'stepCompare': stepCompare 102 | }; 103 | /* @license-end */ 104 | 105 | // vim:set ai et ts=2 sw=2 sts=2 fenc=utf-8: 106 | -------------------------------------------------------------------------------- /src/js/engine/audio-compat.js: -------------------------------------------------------------------------------- 1 | /* @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later */ 2 | 'use strict'; 3 | 4 | 5 | // only support MP3's for the moment 6 | module.exports = function CompatAudioEngineImpl(commonEngineInterface, FrameManager, logProvider) { 7 | logProvider || (logProvider = console); 8 | 9 | // don't bother checking if even