diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..959e169 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "bower_components", + "analytics": false +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..47c5438 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.js] +indent_style = space +indent_size = 2 + +[*.hbs] +insert_final_newline = false +indent_style = space +indent_size = 2 + +[*.css] +indent_style = space +indent_size = 2 + +[*.html] +indent_style = space +indent_size = 2 + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/.ember-cli b/.ember-cli new file mode 100644 index 0000000..b4934f3 --- /dev/null +++ b/.ember-cli @@ -0,0 +1,10 @@ +{ + /** + Ember CLI sends analytics information by default. The data is completely + anonymous, but there are times when you might want to disable this behavior. + + Setting `disableAnalytics` to true will prevent any data from being sent. + */ + "disableAnalytics": true, + "usePods": true +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8756776 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp + +# dependencies +/node_modules +/bower_components + +# misc +/.sass-cache +/connect.lock +/coverage/* +/libpeerconnection.log +npm-debug.log +testem.log +/.idea/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..dae9b20 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,37 @@ +{ + "predef": [ + "document", + "window", + "-Promise", + "Dancer", + "ID3", + "FileAPIReader", + "SC", + "introJs" + ], + "browser": true, + "boss": true, + "curly": true, + "debug": false, + "devel": true, + "eqeqeq": true, + "evil": true, + "forin": false, + "immed": false, + "laxbreak": false, + "newcap": true, + "noarg": true, + "noempty": false, + "nonew": false, + "nomen": false, + "onevar": false, + "plusplus": false, + "regexp": false, + "undef": true, + "sub": true, + "strict": false, + "white": false, + "eqnull": true, + "esnext": true, + "unused": true +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..66dd107 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +--- +language: node_js +node_js: + - "0.12" + +sudo: false + +cache: + directories: + - node_modules + +before_install: + - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH + - "npm config set spin false" + - "npm install -g npm@^2" + +install: + - npm install -g bower + - npm install + - bower install + +script: + - npm test diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 0000000..e7834e3 --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp", "dist"] +} diff --git a/README.md b/README.md index 9cf489c..75f66e3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ -# huegasm -www.huegasm.com +# Huegasm +Music awesomeness for hue lights. -This repository here is strictly for hosting huegasm on github pages. +# TODO +## FEATURES + +## BUGS + +## POSSIBLE FUTURE FEATURES +- better visualizations +- beat settings by interval +- auto beat detection mode +- display player time when hovering over seek bar diff --git a/app/app.js b/app/app.js new file mode 100644 index 0000000..831ad61 --- /dev/null +++ b/app/app.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import Resolver from './resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from './config/environment'; + +let App; + +Ember.MODEL_FACTORY_INJECTIONS = true; + +App = Ember.Application.extend({ + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, + Resolver +}); + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..b7e8bd2 --- /dev/null +++ b/app/index.html @@ -0,0 +1,63 @@ + + + + + + Huegasm + + + + + + + + {{content-for 'head'}} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{content-for 'head-footer'}} + + + + + + + {{content-for 'body'}} + + + + + {{content-for 'body-footer'}} + + diff --git a/app/pods/components/add-group-modal/component.js b/app/pods/components/add-group-modal/component.js new file mode 100644 index 0000000..778a912 --- /dev/null +++ b/app/pods/components/add-group-modal/component.js @@ -0,0 +1,61 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + actions: { + close: function(){ + this.sendAction(); + }, + save: function(){ + var newGroupData = {"name": this.get('groupName'), "lights": this.get('selectedLights')}, newGroupsData = this.get('groupsData'); + + Em.$.ajax(this.get('apiURL') + '/groups', { + data: JSON.stringify(newGroupData), + contentType: 'application/json', + type: 'POST' + }); + + // crappy code to redraw the lights + newGroupsData['9999'] = newGroupData; + + this.setProperties({ + updateGroupsData: true, + groupsData: newGroupsData + }); + this.sendAction(); + }, + clickLight: function(id) { + var selectedLights = this.get('selectedLights'); + + if(selectedLights.contains(id)){ + selectedLights.removeObject(id); + } else { + selectedLights.pushObject(id); + } + } + }, + + didInsertElement: function() { + Em.$(document).keypress((event) => { + if(!this.get('saveDisabled') && event.which === 13) { + this.send('save'); + } + }); + }, + + groupName: null, + + selectedLights: [], + + onIsShowingModalChange: function(){ + if(this.get('isShowingModal')){ + this.setProperties({ + selectedLights: [], + groupName: null + }); + } + }.observes('isShowingModal'), + + saveDisabled: function(){ + return Em.isNone(this.get('groupName')) || Em.isEmpty(this.get('selectedLights')) || Em.isEmpty(this.get('groupName').trim()); + }.property('groupName', 'selectedLights.[]') +}); diff --git a/app/pods/components/add-group-modal/template.hbs b/app/pods/components/add-group-modal/template.hbs new file mode 100644 index 0000000..4cc896c --- /dev/null +++ b/app/pods/components/add-group-modal/template.hbs @@ -0,0 +1,12 @@ +{{#if isShowingModal}} + {{#modal-dialog close="close" alignment="center" translucentOverlay=true}} + + {{light-group lightsData=lightsData activeLights=selectedLights action="clickLight" apiURL=apiURL noHover=true}} + + {{paper-input label="Group name" value=groupName max="32" max-errortext="The group name cannot exceed 32 characters"}} + + {{#paper-button action="close"}}Close{{/paper-button}} + {{#paper-button class="pull-right" action="save" disabled=saveDisabled primary=true}}Save{{/paper-button}} + + {{/modal-dialog}} +{{/if}} \ No newline at end of file diff --git a/app/pods/components/add-soundcloud-sound-modal/component.js b/app/pods/components/add-soundcloud-sound-modal/component.js new file mode 100644 index 0000000..fb9b114 --- /dev/null +++ b/app/pods/components/add-soundcloud-sound-modal/component.js @@ -0,0 +1,38 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + actions: { + close () { + this.sendAction(); + }, + add (){ + this.sendAction('action', this.get('url')); + } + }, + + url: null, + + onIsShowingModalChange: function(){ + if(this.get('isShowingModal')){ + this.set('url', null); + setTimeout(()=>{ + Em.$('md-input-container input').focus(); + }, 500); + } + + }.observes('isShowingModal'), + + didInsertElement: function() { + var self = this; + + Em.$(document).keypress(function(event) { + if(!self.get('saveDisabled') && event.which === 13) { + self.send('add'); + } + }); + }, + + saveDisabled: function(){ + return Em.isNone(this.get('url')) || Em.isEmpty(this.get('url').trim()); + }.property('url') +}); diff --git a/app/pods/components/add-soundcloud-sound-modal/template.hbs b/app/pods/components/add-soundcloud-sound-modal/template.hbs new file mode 100644 index 0000000..74f38df --- /dev/null +++ b/app/pods/components/add-soundcloud-sound-modal/template.hbs @@ -0,0 +1,13 @@ +{{#if isShowingModal}} + {{#modal-dialog close="close" alignment="center" translucentOverlay=true attachment="center" targetAttachment="center"}} + +

Enter a SoundCloud track or playlist/set URL

+

( ex. https://soundcloud.com/mrsuicidesheep/tracks )

+ + {{paper-input label="SoundCloud URL" icon="search" value=url}} + + {{#paper-button action="close"}}Close{{/paper-button}} + {{#paper-button class="pull-right" action="add" disabled=saveDisabled primary=true}}Add Music{{/paper-button}} + + {{/modal-dialog}} +{{/if}} \ No newline at end of file diff --git a/app/pods/components/bridge-finder/component.js b/app/pods/components/bridge-finder/component.js new file mode 100644 index 0000000..6b68fd1 --- /dev/null +++ b/app/pods/components/bridge-finder/component.js @@ -0,0 +1,149 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + classNames: ['container', 'bridgeFinder'], + + bridgeIp: null, + trial: false, + bridgeUsername: null, + + bridgeFindStatus: null, + bridgeFindSuccess: Em.computed.equal('bridgeFindStatus', 'success'), + bridgeFindMultiple: Em.computed.equal('bridgeFindStatus', 'multiple'), + bridgeFindFail: Em.computed.equal('bridgeFindStatus', 'fail'), + + // 30 seconds + bridgeUsernamePingMaxTime: 30000, + bridgeUsernamePingIntervalTime: 1000, + bridgeUserNamePingIntervalProgress: 0, + + bridgePingIntervalHandle: null, + bridgeAuthenticateReachedStatus: null, + + manualBridgeIp: null, + manualBridgeIpNotFound: false, + multipleBridgeIps: [], + error: false, + + actions: { + retry(){ + this.onBridgeIpChange(); + }, + + findBridgeByIp() { + var manualBridgeIp = this.get('manualBridgeIp'); + + if (manualBridgeIp.toLowerCase() === 'trial' || manualBridgeIp.toLowerCase() === 'offline') { + this.setProperties({ + trial: true, + bridgeIp: 'trial', + bridgeUsername: 'trial' + }); + } else { + Em.$.ajax('http://' + manualBridgeIp + '/api', { + data: JSON.stringify({"devicetype": "huegasm"}), + contentType: 'application/json', + type: 'POST' + }).fail(() => { + this.set('manualBridgeIpNotFound', true); + setTimeout(() => { this.set('manualBridgeIpNotFound', false); }, 5000); + }).then(() => { + this.set('bridgeIp', manualBridgeIp); + }); + } + } + }, + + didInsertElement() { + var self = this; + + Em.$(document).keypress(function(event) { + if(!Em.isNone(self.get('manualBridgeIp')) && event.which === 13) { + self.send('findBridgeByIp'); + } + }); + }, + + // find the bridge ip here + init() { + this._super(); + + if(this.get('bridgeIp') === null) { + Em.$.ajax('https://www.meethue.com/api/nupnp', { + timeout: 30000 + }) + .done((result, status)=> { + var bridgeFindStatus = 'fail'; + + if (status === 'success' && result.length === 1) { + this.set('bridgeIp', result[0].internalipaddress); + this.get('storage').set('huegasm.bridgeIp', result[0].internalipaddress); + bridgeFindStatus = 'success'; + } else if (result.length > 1) { + var multipleBridgeIps = this.get('multipleBridgeIps'); + + result.forEach(function (item) { + multipleBridgeIps.pushObject(item.internalipaddress); + }); + + bridgeFindStatus = 'multiple'; + } else { + bridgeFindStatus = 'fail'; + } + + this.set('bridgeFindStatus', bridgeFindStatus); + }) + .fail(()=>{ + this.set('bridgeFindStatus', 'fail'); + }); + } + }, + + // try to authenticate against the bridge here + onBridgeIpChange: function () { + if(!this.get('trial') && !this.get('isAuthenticating')) { + this.setProperties({ + bridgePingIntervalHandle: setInterval(this.pingBridgeUser.bind(this), this.get('bridgeUsernamePingIntervalTime')), + bridgeUserNamePingIntervalProgress: 0 + }); + } + }.observes('bridgeIp').on('init'), + + pingBridgeUser() { + var bridgeIp = this.get('bridgeIp'), + bridgeUserNamePingIntervalProgress = this.get('bridgeUserNamePingIntervalProgress'), + bridgeUsernamePingMaxTime = this.get('bridgeUsernamePingMaxTime'); + + if (bridgeIp !== null && bridgeUserNamePingIntervalProgress < 100) { + Em.$.ajax('http://' + bridgeIp + '/api', { + data: JSON.stringify({"devicetype": "huegasm"}), + contentType: 'application/json', + type: 'POST' + }).done((result, status)=>{ + if (status === 'success' && !result[0].error) { + this.clearBridgePingIntervalHandle(); + this.set('bridgeUsername', result[0].success.username); + this.get('storage').set('huegasm.bridgeUsername', result[0].success.username); + } + + this.set('bridgeAuthenticateReachedStatus', status); + }).fail(()=>{ + this.clearBridgePingIntervalHandle(); + this.set('error', true); + }); + + this.incrementProperty('bridgeUserNamePingIntervalProgress', this.get('bridgeUsernamePingIntervalTime')/bridgeUsernamePingMaxTime*100); + } else { + this.clearBridgePingIntervalHandle(); + } + }, + + clearBridgePingIntervalHandle(){ + clearInterval(this.get('bridgePingIntervalHandle')); + this.set('bridgePingIntervalHandle', null); + }, + + isAuthenticating: function(){ + return this.get('bridgePingIntervalHandle') !== null; + }.property('bridgePingIntervalHandle') +}); diff --git a/app/pods/components/bridge-finder/template.hbs b/app/pods/components/bridge-finder/template.hbs new file mode 100644 index 0000000..1b8cb08 --- /dev/null +++ b/app/pods/components/bridge-finder/template.hbs @@ -0,0 +1,54 @@ +
Huegasm
+{{#unless bridgeUsername}} + {{#if bridgeIp}} + {{#if error}} +

Huegasm encountered a critical error while trying to connect to your bridge.

+ This likely happened because you're using an outdated browser and/or because your browser does not support CORS. Feel free to contact me through the link at the bottom of the page if you feel like this is not the case.
+ For the best browsing experience on this site ( and every other one known to man ) please switch to Google Chrome or Firefox

. + {{else}} + + {{paper-progress-linear warn=true value=bridgeUserNamePingIntervalProgress}} + + {{#if isAuthenticating}} +

Your bridge IP is {{bridgeIp}}
+ Press the button on your bridge to authenticate this application.

+ {{else}} +

You failed to press the button. RETRY

+ {{/if}} + {{/if}} + {{else}} + {{#unless bridgeFindStatus}} + {{paper-progress-circular}} +

Trying to find your bridge's IP.

+ {{/unless}} + + {{#if bridgeFindMultiple}} +

Found multiple hue bridges.
+ Please select the one you want to use for this application.

+ +
+ {{#each multipleBridgeIps as |bridge|}} + {{#paper-radio value=bridge selected=bridgeIp}}{{bridge}}{{/paper-radio}} + {{/each}} +
+ {{else}} + {{#if bridgeFindFail}} +

A hue bridge could not be automatically found on your network.
+ Enter one manually?

+ ( or type offline to look around ) +

+ + + {{paper-input label="Hue bridge IP address" value=manualBridgeIp}} + {{#paper-button action="findBridgeByIp" raised=true primary=true}}Find{{/paper-button}} + + + {{#if manualBridgeIpNotFound}} +

+ Could not find a bridge with that IP address. +

+ {{/if}} + {{/if}} + {{/if}} + {{/if}} +{{/unless}} \ No newline at end of file diff --git a/app/pods/components/color-picker/component.js b/app/pods/components/color-picker/component.js new file mode 100644 index 0000000..bfcd79b --- /dev/null +++ b/app/pods/components/color-picker/component.js @@ -0,0 +1,55 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + classNames: ['colorpicker'], + + rgb: null, + + canvas: null, + canvasContext: null, + + mouseUp(){ + this.set('pressingDown', false); + }, + + mouseMove(event){ + if (this.get('pressingDown')) { + this.mouseDown(event); + } + }, + + mouseDown(event){ + var canvasOffset = Em.$(this.get('canvas')).offset(); + var canvasX = Math.floor(event.pageX - canvasOffset.left), canvasY = Math.floor(event.pageY - canvasOffset.top); + + // get current pixel + var imageData = this.get('canvasContext').getImageData(canvasX, canvasY, 1, 1); + var pixel = imageData.data; + + this.set('pressingDown', true); + + if (!(pixel[0] === 0 && pixel[1] === 0 && pixel[2] === 0)) { + this.set('rgb', [pixel[0], pixel[1], pixel[2]]); + } + }, + + pressingDown: false, + + // https://dzone.com/articles/creating-your-own-html5 + didInsertElement(){ + // handle color changes + var canvas = Em.$('#picker')[0], + canvasContext = canvas.getContext('2d'), + image = new Image(); + + image.src = 'assets/images/colormap.png'; + image.onload = function () { + canvasContext.drawImage(image, 0, 0, image.width, image.height); // draw the image on the canvas + }; + + this.setProperties({ + canvas: canvas, + canvasContext: canvasContext + }); + } +}); diff --git a/app/pods/components/color-picker/template.hbs b/app/pods/components/color-picker/template.hbs new file mode 100644 index 0000000..16508e9 --- /dev/null +++ b/app/pods/components/color-picker/template.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/pods/components/delete-group-modal/component.js b/app/pods/components/delete-group-modal/component.js new file mode 100644 index 0000000..418c354 --- /dev/null +++ b/app/pods/components/delete-group-modal/component.js @@ -0,0 +1,35 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + actions: { + close: function(){ + this.sendAction(); + }, + delete: function(){ + var groupId = this.get('groupId'); + + Em.$.ajax(this.get('apiURL') + '/groups/' + groupId, { + contentType: 'application/json', + type: 'DELETE' + }); + + var groupsData = this.get('groupsData'), newGroupsData = []; + for (let key in groupsData) { + if(groupsData.hasOwnProperty(key) && groupsData[key].name !== this.get('groupName') ){ + newGroupsData[key] = groupsData[key]; + } + } + + if(groupId === this.get('groupIdSelection')){ + this.set('groupIdSelection', '0'); + } + + this.setProperties({ + updateGroupsData: true, + groupsData: newGroupsData + }); + + this.sendAction(); + } + } +}); diff --git a/app/pods/components/delete-group-modal/template.hbs b/app/pods/components/delete-group-modal/template.hbs new file mode 100644 index 0000000..2ea092e --- /dev/null +++ b/app/pods/components/delete-group-modal/template.hbs @@ -0,0 +1,10 @@ +{{#if isShowingModal}} + {{#modal-dialog close="close" alignment="center" translucentOverlay=true}} + +

Are you sure you want to delete group "{{groupName}}"?

+ + {{#paper-button action="close"}}Close{{/paper-button}} + {{#paper-button class="pull-right" action="delete" primary=true}}Delete{{/paper-button}} + + {{/modal-dialog}} +{{/if}} \ No newline at end of file diff --git a/app/pods/components/groups-list/component.js b/app/pods/components/groups-list/component.js new file mode 100644 index 0000000..ba8c0b5 --- /dev/null +++ b/app/pods/components/groups-list/component.js @@ -0,0 +1,78 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + classNames: ['dropdown-menu'], + elementId: 'groupList', + + tagName: null, + + groupIdSelection: null, + + actions: { + selectGroup(selection){ + this.set('groupIdSelection', selection); + }, + toggleConfirmDeleteGroupsModal(groupName, groupId){ + this.setProperties({ + deleteGroupName: groupName, + deleteGroupId: groupId + }); + this.toggleProperty('isShowingConfirmDeleteModal'); + }, + toggleAddGroupsModal(){ + this.toggleProperty('isShowingAddGroupsModal'); + } + }, + + groupsArrData: function(){ + var groupsData = this.get('groupsData'), lightsData = this.get('lightsData'), groupsArrData = [], ids = [], groupIdSelection = this.get('groupIdSelection'); + + for (let key in lightsData) { + if(lightsData.hasOwnProperty(key) && lightsData[key].state.reachable){ + ids.push(key); + } + } + groupsArrData.push({name: 'All', data: {lights: ids, key: '0' }, rowClass: groupIdSelection === '0' ? 'groupRow selectedRow' : 'groupRow', deletable: false}); + + for (let key in groupsData) { + if (groupsData.hasOwnProperty(key)) { + var rowClass = 'groupRow'; + + if(key === groupIdSelection){ + rowClass += ' selectedRow'; + } + + groupsArrData.push({name: groupsData[key].name, data: {lights: groupsData[key].lights, key: key}, rowClass: rowClass, deletable: true}); + } + } + + return groupsArrData; + }.property('groupsData', 'groupIdSelection'), + + onGroupIdSelectionChanged: function(){ + var groupIdSelection = this.get('groupIdSelection'), lights = []; + + this.get('groupsArrData').some(function(group){ + if(group.data.key === groupIdSelection){ + lights = group.data.lights; + return true; + } + }); + + this.get('storage').set('huegasm.selectedGroup', groupIdSelection); + + if(!Em.isNone(groupIdSelection) && !Em.isEmpty(lights)){ + this.set('activeLights', lights); + } + }.observes('groupIdSelection', 'groupsArrData'), + + didInsertElement(){ + var selectGroup = '0', storageItem = this.get('storage').get('huegasm.selectedGroup'); + + if(storageItem){ + selectGroup = storageItem; + } + + this.set('groupIdSelection', selectGroup); + } +}); diff --git a/app/pods/components/groups-list/template.hbs b/app/pods/components/groups-list/template.hbs new file mode 100644 index 0000000..a772591 --- /dev/null +++ b/app/pods/components/groups-list/template.hbs @@ -0,0 +1,15 @@ +{{#paper-list}} + {{#paper-item class="newGroupRow"}} +
{{paper-icon icon="group-add"}} Add a new group
+ {{/paper-item}} + + {{#each groupsArrData as |group|}} + {{#paper-item class=group.rowClass}} +
{{group.name}}
{{#if group.deletable}}{{paper-icon icon="close"}}{{/if}} + {{/paper-item}} + {{/each}} +{{/paper-list}} + +{{add-group-modal lightsData=lightsData groupsData=groupsData isShowingModal=isShowingAddGroupsModal apiURL=apiURL updateGroupsData=updateGroupsData action="toggleAddGroupsModal"}} + +{{delete-group-modal groupName=deleteGroupName groupId=deleteGroupId groupsData=groupsData isShowingModal=isShowingConfirmDeleteModal apiURL=apiURL updateGroupsData=updateGroupsData groupIdSelection=groupIdSelection action="toggleConfirmDeleteGroupsModal"}} \ No newline at end of file diff --git a/app/pods/components/hue-controls/component.js b/app/pods/components/hue-controls/component.js new file mode 100644 index 0000000..0138ca7 --- /dev/null +++ b/app/pods/components/hue-controls/component.js @@ -0,0 +1,280 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + classNames: ['container-fluid'], + elementId: 'hueControls', + + bridgeIp: null, + manualBridgeIp: null, + bridgeUsername: null, + + updateGroupsData: true, + groupsData: null, + lightsData: null, + + activeLights: [], + + actions: { + changeTab(tabName){ + var index = this.get('tabList').indexOf(tabName); + this.set('selectedTab', index); + this.get('storage').set('huegasm.selectedTab', index); + }, + clearBridge() { + var storage = this.get('storage'); + storage.remove('huegasm.bridgeUsername'); + storage.remove('huegasm.bridgeIp'); + location.reload(); + }, + clearAllSettings() { + this.get('storage').clear(); + location.reload(); + }, + startIntro(){ + var INTRO = introJs, + intro = INTRO(), + playerBottom = Em.$('#playerBottom'), + beatDetectionAreaArrowIcon = Em.$('#beatDetectionAreaArrowIcon'); + + this.set('dimmerOn', false); + + intro.setOptions({ + steps: [ + { + intro: 'Welcome! This short tutorial will introduce you to Huegasm.' + }, + { + element: '#musicTab', + intro: 'This is the music player. You\'ll use this to play music and synchronize it with your active lights.

' + + 'TIP: Control which lights are active through the Lights tab or through the Groups menu dropdown.' + }, + { + element: '#playlist', + intro: 'You can add and select music to play from your playlist here. You may listen to local audio files, stream music from soundcloud or stream directly from a connected microphone.

' + + 'TIP: Songs added through Soundcloud will be saved for when you visit this page again.' + }, + { + element: '#playerArea', + intro: 'The audio playback may be controlled with the controls here. Basic music visualization effects may be shown here by selecting them from the menu ( eyeball icon in the bottom right ).' + }, + { + element: '#beatOptionRow', + intro: 'These are the settings for the music tab:
' + + 'Sensitivity - The sensitivity of the beat detector ( more sensitivity results in more registered beats )
' + + 'Hue Range - The hue range that the lights may change to on beat.
' + + 'Flashing Transitions - Quickly flash the lights on beat
' + + 'Colorloop - Slowly cycle the lights through all the colors while the music is playing
' + + 'TIP: Your sensitivity settings are saved per song as indicated by the red star icon in the top left corner. These settings they will be restored if you ever listen to the same song again.', + position: 'top' + }, + { + element: '#beatContainer', + intro: 'An interactive speaker that will bump when a beat is registered.

' + + 'TIP: Click on the center of the speaker to simulate a beat.', + position: 'top' + }, + { + element: '#lightsTab', + intro: 'This is the lights tab. Here you\'ll be able to change various light properties:
' + + 'Power - Turn the selected lights on/off
' + + 'Brightness - The brightness level of the selected lights
' + + 'Color - The color of the selected lights
' + + 'Strobe - Selected lights will flash in sequential order
' + + 'Colorloop - Selected lights will slowly cycle through all the colors
' + }, + { + element: '#activeLights', + intro: 'These icons represent the hue lights in your system. Active lights will be controlled by the application while the inactive lights will have a red X over them and will not be controlled.
' + + 'You may toggle a light\'s state by clicking on it.' + }, + { + element: Em.$('.settingsItem')[0], + intro: 'The Groups menu allows for saving and quickly selecting groups of lights.', + position: 'left' + }, + { + element: Em.$('.settingsItem')[1], + intro: 'A few miscellaneous settings can be found here.

' + + 'WARNING: clearing application settings will restore the application to its original state. This will even delete your playlist and any saved song beat preferences.', + position: 'left' + }, + { + element: '#dimmer', + intro: 'And that\'s it...Hope you enjoy the application. ;)

' + + 'TIP: click on the icon to switch to a darker theme.', + position: 'top' + } + ] + }); + + // it's VERY ugly but it works + intro.onchange((element) => { + if(element.id === 'musicTab' || element.id === 'playlist' || element.id === 'playerArea' || element.id === 'beatOptionRow' || element.id === 'beatOptionButtonGroup' || element.id === 'beatContainer' || element.id === 'usingMicAudioTooltip'){ + Em.$('#musicTab').removeClass('hidden'); + Em.$('#lightsTab').addClass('hidden'); + Em.$('.navigationItem').eq(0).removeClass('active'); + Em.$('.navigationItem').eq(1).addClass('active'); + } else { + Em.$('#lightsTab').removeClass('hidden'); + Em.$('#musicTab').addClass('hidden'); + Em.$('.navigationItem').eq(1).removeClass('active'); + Em.$('.navigationItem').eq(0).addClass('active'); + } + + if(element.id === 'musicTab' || element.id === 'playlist' || element.id === 'playerArea'){ + playerBottom.hide(); + + if(beatDetectionAreaArrowIcon.hasClass('keyboard-arrow-up')){ + beatDetectionAreaArrowIcon.removeClass('keyboard-arrow-up').addClass('keyboard-arrow-down'); + } + } else if(element.id === 'beatOptionRow' || element.id === 'beatOptionButtonGroup' || element.id === 'beatContainer'){ + playerBottom.show(); + + if(beatDetectionAreaArrowIcon.hasClass('keyboard-arrow-down')){ + beatDetectionAreaArrowIcon.removeClass('keyboard-arrow-down').addClass('keyboard-arrow-up'); + } + } else if(element.id === 'dimmer'){ + Em.$(document).click(); + } + }); + + var onFinish = ()=>{ + this.set('activeTab', 1); + Em.$('#musicTab').removeClass('hidden'); + Em.$('#lightsTab').addClass('hidden'); + Em.$('.navigationItem').eq(0).removeClass('active'); + Em.$('.navigationItem').eq(1).addClass('active'); + + if(beatDetectionAreaArrowIcon.hasClass('keyboard-arrow-up')){ + playerBottom.show(); + } else { + playerBottom.hide(); + } + }, onExit = ()=>{ + var dimmer = Em.$('#dimmer'); + + onFinish(); + dimmer.popover({ + trigger: 'manual', + placement: 'top', + content: 'Click on this icon to toggle the dark theme.' + }).popover('show'); + + setTimeout(()=>{ + dimmer.popover('hide'); + }, 5000); + }; + + // skip hidden/missing elements + intro.onafterchange((element)=>{ + var elem = Em.$(element); + if(elem.html() === ''){ + Em.$('.introjs-nextbutton').click(); + } + }).onexit(onExit).oncomplete(onFinish).start(); + } + }, + + apiURL: function(){ + return 'http://' + this.get('bridgeIp') + '/api/' + this.get('bridgeUsername'); + }.property('bridgeIp', 'bridgeUsername'), + + didInsertElement(){ + // here's a weird way to automatically initialize bootstrap tooltips + var observer = new MutationObserver(function(mutations) { + var haveTooltip = !mutations.every(function(mutation) { + return Em.isEmpty(mutation.addedNodes) || Em.isNone(mutation.addedNodes[0].classList) || mutation.addedNodes[0].classList.contains('tooltip'); + }); + + if(haveTooltip) { + Em.run.once(this, function(){ + Em.$('.bootstrapTooltip').tooltip(); + }); + } + }); + + observer.observe(Em.$('#hueControls')[0], {childList: true, subtree: true}); + }, + + init() { + this._super(); + + if(!this.get('trial')) { + this.doUpdateGroupsData(); + this.updateLightData(); + this.set('lightsDataIntervalHandle', setInterval(this.updateLightData.bind(this), 2000)); + } + + if (!Em.isNone(this.get('storage').get('huegasm.selectedTab'))) { + this.set('selectedTab', this.get('storage').get('huegasm.selectedTab')); + } + }, + + onUpdateGroupsDataChange: function(){ + if(this.get('updateGroupsData')){ + setTimeout(()=>{ this.doUpdateGroupsData(); }, 1000); + } + }.observes('updateGroupsData'), + + doUpdateGroupsData(){ + Em.$.get(this.get('apiURL') + '/groups', (result, status)=>{ + if (status === 'success' ) { + this.set('groupsData', result); + } + }); + + this.toggleProperty('updateGroupsData'); + }, + + tabList: ["Lights", "Music"], + selectedTab: 1, + tabData: function(){ + var tabData = [], selectedTab = this.get('selectedTab'); + + this.get('tabList').forEach(function(tab, i){ + var selected = false; + + if(i === selectedTab){ + selected = true; + } + + tabData.push({"name": tab, "selected": selected }); + }); + + return tabData; + }.property('tabList', 'selectedTab'), + + lightsTabSelected: Em.computed.equal('selectedTab', 0), + musicTabSelected: Em.computed.equal('selectedTab', 1), + + pauseLightUpdates: false, + + updateLightData(){ + var fail = ()=>{ + clearInterval(this.get('lightsDataIntervalHandle')); + + this.get('storage').remove('huegasm.bridgeIp'); + this.get('storage').remove('huegasm.bridgeUsername'); + + location.reload(); + }; + + if(!this.get('pauseLightUpdates')){ + Em.$.get(this.get('apiURL') + '/lights', (result, status)=>{ + if(!Em.isNone(result[0]) && !Em.isNone(result[0].error)){ + fail(); + } else if (status === 'success' && JSON.stringify(this.get('lightsData')) !== JSON.stringify(result)) { + this.set('lightsData', result); + } + }).fail(fail); + } + }, + + dimmerOnClass: function(){ + return this.get('dimmerOn') ? 'dimmerOn' : null; + }.property('dimmerOn'), + + ready: function() { + return this.get('trial') || !Em.isNone(this.get('lightsData')); + }.property('lightsData', 'trial') +}); diff --git a/app/pods/components/hue-controls/template.hbs b/app/pods/components/hue-controls/template.hbs new file mode 100644 index 0000000..3eac752 --- /dev/null +++ b/app/pods/components/hue-controls/template.hbs @@ -0,0 +1,34 @@ +{{#if ready}} + + + {{lights-tab apiURL=apiURL lightsData=lightsData activeLights=activeLights trial=trial active=lightsTabSelected colorLoopOn=colorLoopOn dimmerOn=dimmerOn}} + + {{music-tab apiURL=apiURL lightsData=lightsData activeLights=activeLights active=musicTabSelected pauseLightUpdates=pauseLightUpdates dimmerOn=dimmerOn storage=storage colorLoopOn=colorLoopOn action="startIntro"}} +{{/if}} \ No newline at end of file diff --git a/app/pods/components/huegasm-app/component.js b/app/pods/components/huegasm-app/component.js new file mode 100644 index 0000000..758b67f --- /dev/null +++ b/app/pods/components/huegasm-app/component.js @@ -0,0 +1,64 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + actions: { + toggleDimmer(){ + this.toggleProperty('dimmerOn'); + }, + isReady(){ + this.set('ready', true); + } + }, + bridgeIp: null, + + bridgeUsername: null, + + trial: false, + + storage: null, + + dimmerOn: false, + + ready: false, + + dimmerOnClass: function () { + var dimmerOn = this.get('dimmerOn'), + storage = this.get('storage'), + dimmerOnClass = null; + + if (dimmerOn) { + Em.$('body').addClass('dimmerOn'); + Em.$('html').addClass('dimmerOn'); + dimmerOnClass = 'active'; + } else { + Em.$('body').removeClass('dimmerOn'); + Em.$('html').removeClass('dimmerOn'); + } + + storage.set('huegasm.dimmerOn', dimmerOn); + + return dimmerOnClass; + }.property('dimmerOn'), + + init(){ + this._super(); + + var storage = new window.Locally.Store({compress: true}); + this.set('storage', storage); + + if (!Em.isNone(storage.get('huegasm.dimmerOn'))) { + this.set('dimmerOn', storage.get('huegasm.dimmerOn')); + } + + if (!Em.isEmpty(storage.get('huegasm.bridgeIp')) && !Em.isEmpty(storage.get('huegasm.bridgeUsername'))) { + this.setProperties({ + bridgeIp: storage.get('huegasm.bridgeIp'), + bridgeUsername: storage.get('huegasm.bridgeUsername') + }); + } + }, + + year: function () { + return new Date().getFullYear(); + }.property() +}); diff --git a/app/pods/components/huegasm-app/template.hbs b/app/pods/components/huegasm-app/template.hbs new file mode 100644 index 0000000..893df7f --- /dev/null +++ b/app/pods/components/huegasm-app/template.hbs @@ -0,0 +1,24 @@ +{{#if bridgeUsername}} + {{hue-controls bridgeIp=bridgeIp bridgeUsername=bridgeUsername trial=trial dimmerOn=dimmerOn storage=storage}} +{{else}} + {{#if ready}} + {{bridge-finder bridgeIp=bridgeIp bridgeUsername=bridgeUsername trial=trial storage=storage}} + {{else}} +
+
Huegasm
+

Your lights, meet your music. Huegasm.

+

Huegasm is a free web application for managing and synchronizing your Philips Hue lights with the beat of your music.

+
+
+ +
+
+ {{#paper-button raised=true primary=true action="isReady" class="goButton center-block"}}Go!{{/paper-button}} +
+ {{/if}} +{{/if}} + + \ No newline at end of file diff --git a/app/pods/components/light-group/component.js b/app/pods/components/light-group/component.js new file mode 100644 index 0000000..7fce746 --- /dev/null +++ b/app/pods/components/light-group/component.js @@ -0,0 +1,126 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + + classNames: ['lightGroup'], + + isHovering: false, + + lightsList: Em.A(), + + actions: { + clickLight(id, data){ + var light = Em.$('.light'+id); + + if(!light.hasClass('bootstrapTooltip')){ + light = light.parent(); + } + + if(light.hasClass('lightInactive')){ + light.addClass('lightActive').removeClass('lightInactive'); + } else if(light.hasClass('lightActive')){ + light.addClass('lightInactive').removeClass('lightActive'); + } + + this.sendAction('action', id, data); + }, + lightStartHover(id){ + var hoveredLight = this.get('lightsList').filter(function(light){ + return light.activeClass !== 'unreachable' && light.id === id[0]; + }); + + if(!Em.isEmpty(hoveredLight) && this.get('noHover') !== true){ + Em.$.ajax(this.get('apiURL') + '/lights/' + id + '/state', { + data: JSON.stringify({"alert": "lselect"}), + contentType: 'application/json', + type: 'PUT' + }); + } + + this.set('isHovering', true); + }, + lightStopHover(id){ + var hoveredLight = this.get('lightsList').filter(function(light){ + return light.activeClass !== 'unreachable' && light.id === id[0]; + }); + + if(!Em.isEmpty(hoveredLight) && this.get('noHover') !== true){ + Em.$.ajax(this.get('apiURL') + '/lights/' + id + '/state', { + data: JSON.stringify({"alert": "none"}), + contentType: 'application/json', + type: 'PUT' + }); + } + + this.set('isHovering', false); + this.onLightsDataChange(); + } + }, + + didInsertElement() { + if(this.get('lightsData')){ + this.onLightsDataChange(); + } + }, + + // list of all the lights in the hue system + onLightsDataChange: function(){ + if(!this.get('isHovering')){ + var lightsData = this.get('lightsData'), lightsList = Em.A(), type; + for (var key in lightsData) { + if (lightsData.hasOwnProperty(key) && lightsData[key].state.reachable) { + switch(lightsData[key].modelid){ + case 'LCT001': + type = 'a19'; + break; + case 'LCT002': + type = 'br30'; + break; + case 'LCT003': + type = 'gu10'; + break; + case 'LST001': + type = 'lightstrip'; + break; + case 'LLC010': + type = 'lc_iris'; + break; + case 'LLC011': + type = 'lc_bloom'; + break; + case 'LLC012': + type = 'lc_bloom'; + break; + case 'LLC006': + type = 'lc_iris'; + break; + case 'LLC007': + type = 'lc_aura'; + break; + case 'LLC013': + type = 'storylight'; + break; + case 'LWB004': + type ='a19'; + break; + case 'LLC020': + type = 'huego'; + break; + default: + type = 'a19'; + } + + var activeClass = 'lightActive'; + + if(!this.get('activeLights').contains(key)){ + activeClass = 'lightInactive'; + } + + lightsList.push({type: type, name: lightsData[key].name, id: key, data: lightsData[key], activeClass: activeClass}); + } + } + + this.set('lightsList', lightsList); + } + }.observes('lightsData', 'activeLights.[]', 'dimmerOn') +}); diff --git a/app/pods/components/light-group/template.hbs b/app/pods/components/light-group/template.hbs new file mode 100644 index 0000000..1a37454 --- /dev/null +++ b/app/pods/components/light-group/template.hbs @@ -0,0 +1,5 @@ +{{#each lightsList as |light|}} +
+ +
+{{/each}} \ No newline at end of file diff --git a/app/pods/components/lights-tab/component.js b/app/pods/components/lights-tab/component.js new file mode 100644 index 0000000..d8cacfa --- /dev/null +++ b/app/pods/components/lights-tab/component.js @@ -0,0 +1,358 @@ +import Em from 'ember'; + +export default Em.Component.extend({ + classNames: ['col-sm-8', 'col-sm-offset-2', 'col-xs-12'], + classNameBindings: ['active::hidden'], + elementId: 'lightsTab', + + activeLights: [], + lightsData: null, + + lightsDataIntervalHandle: null, + + colorPickerDisplayed: false, + + actions: { + clickLight(light){ + var activeLights = this.get('activeLights'), + lightId = activeLights.indexOf(light); + + if(lightId !== -1){ + activeLights.removeObject(light); + } else { + activeLights.pushObject(light); + + // sync the current light settings to the newly added light + var options = {on: this.get('lightsOn'), bri: this.get('lightsBrightness'), effect: this.get('colorLoopOn') ? 'colorloop' : 'none'}, + rgb = this.get('rgb'); + + if(rgb[0] !== 255 && rgb[1] !== 255 && rgb[2] !== 255) { + options['xy'] = this.rgbToXy(rgb[0], rgb[1], rgb[2]); + } + + options['transitiontime'] = 0; + + Em.$.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify(options), + contentType: 'application/json', + type: 'PUT' + }); + } + }, + toggleColorpicker() { + this.toggleProperty('colorPickerDisplayed'); + } + }, + + didInsertElement() { + Em.$(document).click((event)=>{ + if(this.get('colorPickerDisplayed') && !event.target.classList.contains('color') && !Em.$(event.target).closest('.colorpicker, #colorRow').length) { + this.toggleProperty('colorPickerDisplayed'); + } + }); + + Em.$(document).on('click', '#colorRow', () => { + this.send('toggleColorpicker'); + }); + }, + + rgb: [255, 255, 255], + rgbPreview: function() { + var rgb = this.get('rgb'), + xy = this.rgbToXy(rgb[0], rgb[1], rgb[2]); + + this.set('colorLoopOn', false); + + this.get('activeLights').forEach((light) => { + Em.$.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({"xy": xy}), + contentType: 'application/json', + type: 'PUT' + }); + }); + + this.set('colorLoopOn', false); + Em.$('.color').css('background', 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'); + }.observes('rgb'), + + colorRowAction: function() { + if (this.get('trial')) { + return null; + } + + return 'toggleColorpicker'; + }.property('trial'), + + // COLOR LOOP related stuff + colorLoopOn: false, + + onColorLoopOnChange: function(){ + var lightsData = this.get('lightsData'), + activeLights = this.get('activeLights'), + colorLoopsOn = this.get('colorLoopOn'), + effect = colorLoopsOn ? 'colorloop' : 'none'; + + var colorLoopsOnSystem = activeLights.some(function(light) { + return lightsData[light].state.effect === 'colorloop'; + }); + + // if the internal lights state is different than the one from lightsData ( user manually toggled the switch ), send the request to change the bulbs state + if(colorLoopsOn !== colorLoopsOnSystem){ + activeLights.forEach((light)=>{ + if(this.get('lightsData')[light].state.effect !== effect) { + Em.$.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({'effect': effect }), + contentType: 'application/json', + type: 'PUT' + }); + } + }); + } + }.observes('colorLoopOn'), + + lightsOn: false, + + // determines whether the lights are on/off for the lights switch + lightsOnCHange: function(){ + if(!this.get('strobeOn')){ + var lightsData = this.get('lightsData'), lightsOn = this.get('activeLights').some(function(light) { + return lightsData[light].state.on === true; + }); + + this.set('lightsOn', lightsOn); + } + }.observes('lightsData.@each.state.on', 'activeLights.[]'), + + // determines the average brightness of the hue system for the brightness slider + lightsBrightness: function(){ + var lightsData = this.get('lightsData'), activeLights = this.get('activeLights'), lightsBrightness = 0; + + activeLights.forEach(function(light){ + lightsBrightness += lightsData[light].state.bri; + }); + + return lightsBrightness/activeLights.length; + }.property('lightsData'), + + brightnessControlDisabled: Em.computed.not('lightsOn'), + + onLightsOnChange: function(){ + var lightsData = this.get('lightsData'), activeLights = this.get('activeLights'), lightsOn = this.get('lightsOn'), self = this; + + var lightsOnSystem = activeLights.some(function(light) { + return lightsData[light].state.on === true; + }); + + // if the internal lights state is different than the one from lightsData ( user manually toggled the switch ), send the request to change the bulbs state + if(lightsOn !== lightsOnSystem){ + activeLights.forEach(function (light) { + Em.$.ajax(self.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({"on": lightsOn}), + contentType: 'application/json', + type: 'PUT' + }); + }); + } + }.observes('lightsOn'), + + onBrightnessChanged: function(){ + var lightsData = this.get('lightsData'), lightsBrightnessSystem = false, lightsBrightness = this.get('lightsBrightness'), activeLights = this.get('activeLights'), self = this; + + activeLights.forEach(function(light){ + lightsBrightnessSystem += lightsData[light].state.bri; + }); + + lightsBrightnessSystem /= activeLights.length; + + // if the internal lights state is different than the one from lightsData ( user manually toggled the switch ), send the request to change the bulbs state + if(lightsBrightness !== lightsBrightnessSystem){ + activeLights.forEach(function(light){ + Em.$.ajax(self.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({"bri": lightsBrightness}), + contentType: 'application/json', + type: 'PUT' + }); + }); + } + }.observes('lightsBrightness'), + + lightsOnTxt: function(){ + return this.get('lightsOn') ? 'On' : 'Off'; + }.property('lightsOn'), + + colorloopOnTxt: function(){ + return this.get('colorLoopOn') ? 'On' : 'Off'; + }.property('colorLoopOn'), + + // **************** STROBE LIGHT START **************** + + strobeOn: false, + + strobeOnInervalHandle: null, + strobeSat: 0, + preStrobeOnLightsDataCache: null, + lastStrobeLight: 0, + + onStrobeOnChange: function () { + var lightsData = this.get('lightsData'), self = this; + + if (this.get('strobeOn')) { + this.set('preStrobeOnLightsDataCache', lightsData); + var stobeInitRequestData = {'sat': this.get('strobeSat'), 'transitiontime': 0}; + + for (let key in lightsData) { + if (lightsData.hasOwnProperty(key)) { + if (lightsData[key].state.on) { + stobeInitRequestData.on = false; + } + + Em.$.ajax(this.get('apiURL') + '/lights/' + key + '/state', { + data: JSON.stringify(stobeInitRequestData), + contentType: 'application/json', + type: 'PUT' + }); + } + } + + this.set('strobeOnInervalHandle', setInterval(this.strobeStep.bind(this), 200)); + } else { // revert the light system to pre-strobe + var preStrobeOnLightsDataCache = this.get('preStrobeOnLightsDataCache'), updateLight = function (lightIndx) { + Em.$.ajax(self.get('apiURL') + '/lights/' + lightIndx + '/state', { + data: JSON.stringify({ + 'on': preStrobeOnLightsDataCache[lightIndx].state.on, + 'sat': preStrobeOnLightsDataCache[lightIndx].state.sat + }), + contentType: 'application/json', + type: 'PUT' + }); + }; + + for (let key in lightsData) { + if (lightsData.hasOwnProperty(key)) { + setTimeout(updateLight, 2000, key); + } + } + + setTimeout(()=>{this.onColorLoopOnChange();}, 2000); + clearInterval(this.get('strobeOnInervalHandle')); + } + }.observes('strobeOn'), + + strobeStep() { + var lastStrobeLight = (this.get('lastStrobeLight') + 1) % (this.get('activeLights').length + 1), + turnOnOptions = {'on': true, 'transitiontime': 0, 'alert': 'select'}; + + // random light if in cololoop mode + if(this.get('colorLoopOn')) { + turnOnOptions.hue = Math.floor(Math.random() * 65535); + } + + Em.$.ajax(this.get('apiURL') + '/lights/' + lastStrobeLight + '/state', { + data: JSON.stringify(turnOnOptions), + contentType: 'application/json', + type: 'PUT' + }); + Em.$.ajax(this.get('apiURL') + '/lights/' + lastStrobeLight + '/state', { + data: JSON.stringify({'on': false, 'transitiontime': 0}), + contentType: 'application/json', + type: 'PUT' + }); + + this.set('lastStrobeLight', lastStrobeLight); + }, + + strobeOnTxt: function () { + return this.get('strobeOn') ? 'On' : 'Off'; + }.property('strobeOn'), + + dimmerOnClass: function(){ + return this.get('dimmerOn') ? 'dimmerOn' : null; + }.property('dimmerOn'), + + // **************** STROBE LIGHT FINISH **************** + // http://www.developers.meethue.com/documentation/color-conversions-rgb-xy + rgbToXy(red, green, blue){ + var X, Y, Z, x, y; + + // normalize + red = Number((red/255)); + green = Number((green/255)); + blue = Number((blue/255)); + + // gamma correction + red = (red > 0.04045) ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92); + green = (green > 0.04045) ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92); + blue = (blue > 0.04045) ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : (blue / 12.92); + + // RGB to XYZ + X = red * 0.664511 + green * 0.154324 + blue * 0.162028; + Y = red * 0.283881 + green * 0.668433 + blue * 0.047685; + Z = red * 0.000088 + green * 0.072310 + blue * 0.986039; + + x = X / (X + Y + Z); + y = Y / (X + Y + Z); + + return [x,y]; + }, + + xyToRgb(x, y){ + var r, g, b, X, Y = 1.0, Z; + + X = (Y / y) * x; + Z = (Y / y) * (1 - x - y); + + r = X * 1.656492 - Y * 0.354851 - Z * 0.255038; + g = X * -0.707196 + Y * 1.655397 + Z * 0.036152; + b = X * 0.051713 - Y * 0.121364 + Z * 1.011530; + + if (r > b && r > g && r > 1.0) { + // red is too big + g = g / r; + b = b / r; + r = 1.0; + } else if (g > b && g > r && g > 1.0) { + // green is too big + r = r / g; + b = b / g; + g = 1.0; + } else if (b > r && b > g && b > 1.0) { + // blue is too big + r = r / b; + g = g / b; + b = 1.0; + } + + r = (r <= 0.0031308) ? 12.92 * r : 1.055 * Math.pow(r, (1.0 / 2.4)) - 0.055; + g = (g <= 0.0031308) ? 12.92 * g : 1.055 * Math.pow(g, (1.0 / 2.4)) - 0.055; + b = (b <= 0.0031308) ? 12.92 * b : 1.055 * Math.pow(b, (1.0 / 2.4)) - 0.055; + + if (r > b && r > g) { + // red is biggest + if (r > 1.0) { + g = g / r; + b = b / r; + r = 1.0; + } + } else if (g > b && g > r) { + // green is biggest + if (g > 1.0) { + r = r / g; + b = b / g; + g = 1.0; + } + } else if (b > r && b > g) { + // blue is biggest + if (b > 1.0) { + r = r / b; + g = g / b; + b = 1.0; + } + } + + r = r * 255; + g = g * 255; + b = b * 255; + + return [r, g, b]; + } +}); diff --git a/app/pods/components/lights-tab/template.hbs b/app/pods/components/lights-tab/template.hbs new file mode 100644 index 0000000..0aa9a6b --- /dev/null +++ b/app/pods/components/lights-tab/template.hbs @@ -0,0 +1,42 @@ +{{#paper-list}} + {{#paper-item class="item"}} + {{light-group lightsData=lightsData activeLights=activeLights action='clickLight' apiURL=apiURL classNames="horizontalLightGroup" dimmerOn=dimmerOn id="activeLights"}} + {{/paper-item}} + + {{#paper-item}} + {{paper-icon icon="power-settings-new" class=dimmerOnClass}} +

Power

+ {{#paper-switch checked=lightsOn disabled=trial skipProxy=trial}} {{lightsOnTxt}} {{/paper-switch}} + {{/paper-item}} + + {{#paper-item}} + {{paper-icon icon="brightness-4" class=dimmerOnClass}} +

Brightness

+ {{paper-slider flex=true min='1' max='254' value=lightsBrightness disabled=brightnessControlDisabled}} + {{/paper-item}} + + {{#paper-item elementId="colorRow"}} + {{paper-icon icon="color-lens" class=dimmerOnClass}} +

Color

+ {{/paper-item}} + +
+ {{#paper-button raised=true class="color" action="toggleColorpicker" disabled=trial}}{{/paper-button}} + + {{#if colorPickerDisplayed}} + {{color-picker lightsData=lightsData activeLights=activeLights rgb=rgb}} + {{/if}} +
+ + {{#paper-item}} + {{paper-icon icon="flare" class=dimmerOnClass}} +

Strobe

+ {{#paper-switch checked=strobeOn disabled=trial skipProxy=trial}} {{strobeOnTxt}} {{/paper-switch}} + {{/paper-item}} + + {{#paper-item}} + {{paper-icon icon="color-lens" class=dimmerOnClass}} {{paper-icon icon="loop" id="loopAddition" class=dimmerOnClass}} +

Colorloop

+ {{#paper-switch checked=colorLoopOn disabled=trial skipProxy=trial}} {{colorloopOnTxt}} {{/paper-switch}} + {{/paper-item}} +{{/paper-list}} \ No newline at end of file diff --git a/app/pods/components/music-tab/component.js b/app/pods/components/music-tab/component.js new file mode 100644 index 0000000..c61cd61 --- /dev/null +++ b/app/pods/components/music-tab/component.js @@ -0,0 +1,883 @@ +import Em from 'ember'; +import helperMixin from './mixins/helpers'; +import visualizerMixin from './mixins/visualizer'; + +export default Em.Component.extend(helperMixin, visualizerMixin, { + onActiveChange: function(){ + if(this.get('active')){ + Em.$('#playNotification').removeClass('fadeOut'); + Em.$('#beatSpeakerCenterOuter').removeClass('vibrateOuter'); + Em.$('#beatSpeakerCenterInner').removeClass('vibrateInner'); + } + }.observes('active'), + + actions: { + clearPlaylist(){ + this.get('playQueue').clear(); + }, + setVisName(name){ + this.set('currentVisName', name); + }, + hideTooltip(){ + Em.$('.bootstrapTooltip').tooltip('hide'); + }, + gotoSCURL(URL){ + // need to pause the music since soundcloud is going to start playing this song anyways + if(this.get('playing')){ + this.send('play'); + } + + this.send('gotoURL', URL); + }, + gotoURL(URL){ + Em.$('.tooltip').remove(); + window.open(URL, '_blank'); + }, + handleNewSoundCloudURL(URL){ + if(URL) { + SC.resolve(URL).then((resultObj)=>{ + var processResult = (result)=>{ + if(result.kind === 'user'){ + this.get('notify').alert({html: this.get('scUserNotSupportedHtml')}); + } else if(result.kind === 'track') { + if(result.streamable === true){ + var picture = null; + + if(result.artwork_url){ + picture = result.artwork_url.replace('large', 't67x67'); + } else if(result.user.avatar_url){ + picture = result.user.avatar_url; + } + + this.get('playQueue').pushObject({url: result.stream_url + '?client_id=' + this.get('SC_CLIENT_ID'), fileName: result.title + ' - ' + result.user.username, artist: result.user.username, scUrl: result.permalink_url, title: result.title, artworkUrl: result.artwork_url, picture: picture }); + } else { + failedSongs.push(result.title); + } + } else if(result.kind === 'playlist'){ + if(result.streamable === true){ + result.tracks.forEach(processResult); + } else { + failedSongs.push(result.title); + } + } + }, + failedSongs = []; + + if(resultObj instanceof Array){ + resultObj.forEach(processResult); + } else { + processResult(resultObj); + } + + if(failedSongs.length > 0) { + this.get('notify').alert({html: this.get('notStreamableHtml')(failedSongs)}); + } + + if(this.get('playQueuePointer') === -1){ + if(this.get('firstVisit')){ + this.send('goToSong', 0); + } else { + this.send('next'); + } + } + }, () => { + this.get('notify').alert({html: this.get('urlNotFoundHtml')(URL)}); + }); + } + + this.set('isShowingAddSoundCloudModal', false); + }, + toggleIsShowingAddSoundCloudModal() { + this.toggleProperty('isShowingAddSoundCloudModal'); + }, + useLocalAudio(){ + var audioStream = this.get('audioStream'); + this.changePlayerControl('audioMode', 0); + + if(!Em.isNone(audioStream)){ + var tracks = audioStream.getVideoTracks(); + if (tracks && tracks[0] && tracks[0].stop) { + tracks[0].stop(); + } + + if (audioStream.stop) { + // deprecated, may be removed in future + audioStream.stop(); + } + + this.setProperties({ + audioStream: null, + playing: false + }); + } + + if(this.get('playQueuePointer') !== -1) { + this.send('goToSong', this.get('playQueuePointer')); + this.send('volumeChanged', this.get('volume')); + } + + // restore the old beat preferences ( before the user went into mic mode ) + if(!Em.isNone(this.get('oldThreshold'))){ + this.set('threshold', this.get('oldThreshold')); + } + + document.title = 'Huegasm'; + }, + useMicAudio() { + if(this.get('usingMicAudio')) { + this.send('useLocalAudio'); + } else { + this.startUsingMic(); + } + }, + slideTogglePlayerBottom(){ + this.$('#playerBottom').slideToggle(); + this.changePlayerControl('playerBottomDisplayed', !this.get('playerBottomDisplayed')); + }, + goToSong(index, playSong, scrollToSong){ + var dancer = this.get('dancer'), playQueue = this.get('playQueue'); + + if(dancer.audio) { + this.clearCurrentAudio(true); + } + + if(!Em.isNone(playQueue[index])) { + var audio = new Audio(); + audio.src = this.get('playQueue')[index].url; + + audio.crossOrigin = "anonymous"; + audio.oncanplay = ()=>{ + this.set('timeTotal', Math.floor(audio.duration)); + this.set('soundCloudFuckUps', 0); + }; + audio.onerror = (event)=>{ + var playQueuePointer =this.get('playQueuePointer'), + song = this.get('playQueue')[playQueuePointer]; + + if(this.get('soundCloudFuckUps') >= this.get('maxSoundCloudFuckUps')) { + this.get('notify').alert({html: this.get('tooManySoundCloudFuckUps')}); + this.send('play'); + this.set('soundCloudFuckUps', 0); + } else { + if(song.local){ + this.send('removeAudio', playQueuePointer); + } else { + this.send('next', true); + } + + if(event.target.error.code === 2){ + this.get('notify').alert({html: this.get('failedToDecodeFileHtml')(song.fileName)}); + } else { + this.get('notify').alert({html: this.get('failedToPlayFileHtml')(song.fileName)}); + } + + this.set('usingBeatPreferences', false); + this.incrementProperty('soundCloudFuckUps'); + } + }; + audio.ontimeupdate = ()=>{ + this.set('timeElapsed', Math.floor(audio.currentTime)); + }; + audio.onended = ()=> { + this.send('next'); + }; + + dancer.load(audio, 1); + + this.set('playQueuePointer', index); + + this.loadSongBeatPreferences(); + + if(playSong){ + this.send('play'); + } + + if(scrollToSong){ + // this is just a bad workaround to make sure that the track has been rendered to the playlist + Em.run.later(()=>{ + var track = Em.$('.track'+index), playListArea = Em.$('#playListArea'); + + if(!Em.isNone(track) && !Em.isNone(track.offset())) { + playListArea.animate({ + scrollTop: track.offset().top - playListArea.offset().top + playListArea.scrollTop() + }); + } + }, 1000); + } + } + }, + removeAudio(index){ + this.get('playQueue').removeAt(index); + + // need to manually remove the tooltip + Em.$('body .tooltip').remove(); + + if(index === this.get('playQueuePointer')) { + this.send('goToSong', index, true, true); + } + }, + playerAreaPlay(){ + if(Em.isEmpty(Em.$('#playerControls:hover')) && this.get('playQueuePointer') !== -1 ){ + this.send('play'); + this.set('fadeOutNotification', true); + Em.$('#playNotification').removeClass('fadeOut').prop('offsetWidth', Em.$('#playNotification').prop('offsetWidth')).addClass('fadeOut'); + } + }, + play(replayPause) { + var dancer = this.get('dancer'), + playQueuePointer = this.get('playQueuePointer'); + + if(playQueuePointer !== -1 ) { + if (this.get('playing')) { + dancer.pause(); + + if(!replayPause){ + this.set('timeElapsed', Math.floor(dancer.getTime())); + } + } else { + var timeTotal = this.get('timeTotal'); + + if(this.get('volumeMuted')) { + dancer.setVolume(0); + } else { + dancer.setVolume(this.get('volume')/100); + } + + // replay song + if(this.get('timeElapsed') === timeTotal && timeTotal !== 0){ + this.send('next', true); + return; + } + + Em.$(window).trigger('resize'); // workaround to redraw the canvas for the vitualizer + + dancer.play(); + } + + this.onColorloopModeChange(); + this.toggleProperty('playing'); + } + }, + volumeChanged(value) { + this.changePlayerControl('volume', value); + if(this.get('playing')) { + this.get('dancer').setVolume(value/100); + } + + if(this.get('volume') > 0 && this.get('volumeMuted')){ + this.changePlayerControl('volumeMuted', false); + } + }, + next(repeatAll) { + var playQueuePointer = this.get('playQueuePointer'), + playQueue = this.get('playQueue'), + nextSong = (playQueuePointer + 1), + repeat = this.get('repeat'), + shuffle = this.get('shuffle'); + + if(repeat === 2){ // repeating one song takes precedence over shuffling + if(playQueuePointer === -1 && playQueue.length > 0) { + nextSong = 0; + } else { + nextSong = playQueuePointer; + } + } else if(shuffle){ // next shuffle song + var shufflePlayed = this.get('shufflePlayed'); + + // played all the song in shuffle mode + if(shufflePlayed.length === playQueue.length){ + shufflePlayed.clear(); + this.send('play', true); + return; + } + + // we're going to assume that the song URL is the id + do { + nextSong = Math.floor(Math.random() * playQueue.length); + } while(shufflePlayed.contains(playQueue[nextSong].url)); + + shufflePlayed.pushObject(playQueue[nextSong].url); + } else if(nextSong > playQueue.length-1){ + if(repeat === 1 || repeatAll){ + nextSong = nextSong % playQueue.length; + } else { + this.send('play', true); + return; + } + } + + this.send('goToSong', nextSong, true, true); + }, + previous() { + if(this.get('timeElapsed') > 5) { + this.send('seekChanged', 0); + } else { + var nextSong = this.get('playQueuePointer'), + playQueue = this.get('playQueue'); + + if(this.get('shuffle') && !Em.isNone(playQueue[nextSong])) { // go to the previously shuffled song + var shufflePlayed = this.get('shufflePlayed'), + shuffledSongIndx = this.get('shufflePlayed').indexOf(playQueue[nextSong].url), + i = 0; + + if(shufflePlayed.length > 0 && shuffledSongIndx !== -1){ // only if there was one + nextSong = shuffledSongIndx - 1; + + if(nextSong < 0){ + nextSong = shufflePlayed.length - 1; + } + + playQueue.some(function(item){ // try to find the previous song id + if(item.url === shufflePlayed[nextSong]){ + nextSong = i; + return true; + } + i++; + + return false; + }); + } + } else { + nextSong--; + + if(nextSong < 0) { + nextSong = playQueue.length - 1; + } + } + + this.send('goToSong', nextSong, true, true); + } + }, + seekChanged(position) { + var dancer = this.get('dancer'); + + if(dancer.audio){ + dancer.audio.currentTime = Math.floor(this.get('timeTotal') * position / 100); + } + }, + volumeMutedChanged(value) { + var dancer = this.get('dancer'), + volumeMuted = Em.isNone(value) ? !this.get('volumeMuted') : value; + + this.changePlayerControl('volumeMuted', volumeMuted); + + if(this.get('playing')){ + if(volumeMuted){ + dancer.setVolume(0); + } else { + dancer.setVolume(this.get('volume')/100); + } + } + }, + addLocalAudio: function () { + Em.$('#fileInput').click(); + }, + shuffleChanged(value) { + this.changePlayerControl('shuffle', Em.isNone(value) ? !this.get('shuffle') : value); + }, + repeatChanged(value) { + this.changePlayerControl('repeat', Em.isNone(value) ? (this.get('repeat') + 1) % 3 : value); + }, + playerBottomDisplayedChanged(value) { + this.changePlayerControl('playerBottomDisplayed', value); + }, + thresholdChanged(value) { + this.changePlayerControl('threshold', value, true); + }, + hueRangeChanged(value) { + this.changePlayerControl('hueRange', value); + }, + micBoostChanged(value) { + this.set('micBoost', value); + this.get('storage').set('huegasm.micBoost', value); + + if(this.get('usingMicAudio')) { + this.get('dancer').setBoost(value); + } + }, + audioModeChanged(value){ + if(value === 1) { + this.startUsingMic(); + } else if(value === 0) { + this.send('useLocalAudio'); + } else { + this.set('audioMode', value); + } + }, + playQueuePointerChanged(value){ + this.send('goToSong', value, false, true); + }, + clickSpeaker(){ + this.simulateKick(1); + }, + dropFiles(files){ + this.setProperties({ + dragging: false, + draggingOverPlayListArea: false + }); + this.send('handleNewFiles', files); + }, + playListAreaDragOver(){ + this.set('draggingOverPlayListArea', true); + }, + playListAreaDragLeave(){ + this.set('draggingOverPlayListArea', false); + }, + handleNewFiles(files){ + var self = this, + playQueue = this.get('playQueue'), + updatePlayQueue = function(){ + var tags = ID3.getAllTags("local"), + picture = null; + + if(tags.picture){ + var base64String = ""; + for (var i = 0; i < tags.picture.data.length; i++) { + base64String += String.fromCharCode(tags.picture.data[i]); + } + + picture = "data:" + tags.picture.format + ";base64," + window.btoa(base64String); + } + + playQueue.pushObject({fileName: this.name.replace(/\.[^/.]+$/, ""), url: URL.createObjectURL(this), artist: tags.artist, title: tags.title, picture: picture, local: true }); + + ID3.clearAll(); + + if(self.get('playQueuePointer') === -1){ + self.send('next'); + } + }; + + for (var key in files) { + if (files.hasOwnProperty(key)) { + var file = files[key]; + + if(file.type.startsWith('audio')) { + ID3.loadTags("local", updatePlayQueue.bind(file),{ + dataReader: new FileAPIReader(file), + tags: ['title', 'artist', 'album', 'track', 'picture'] + }); + } + } + } + } + }, + + changePlayerControl(name, value, saveBeatPrefs){ + this.set(name, value); + + if(name === 'threshold'){ + this.get('kick').set({threshold: value}); + } + + if(saveBeatPrefs && this.get('usingLocalAudio') && this.get('playQueuePointer') !== -1){ + this.saveSongBeatPreferences(); + } + + this.get('storage').set('huegasm.' + name, value); + }, + + saveSongBeatPreferences() { + var song = this.get('playQueue')[this.get('playQueuePointer')]; + if(song) { + var title = Em.isEmpty(song.artist) ? song.fileName : song.artist + '-' + song.title, + songBeatPreferences = this.get('songBeatPreferences'); + + songBeatPreferences[title] = {threshold: this.get('threshold')}; + + this.set('usingBeatPreferences', true); + this.get('storage').set('huegasm.songBeatPreferences', songBeatPreferences); + } + }, + + loadSongBeatPreferences() { + var song = this.get('playQueue')[this.get('playQueuePointer')], + title = Em.isEmpty(song.artist) ? song.fileName : song.artist + '-' + song.title, + songBeatPreferences = this.get('songBeatPreferences'), + preference = songBeatPreferences[title], + oldBeatPrefCache = this.get('oldBeatPrefCache'), + newOldBeatPrefCache = null; + + if(!Em.isNone(preference)) { // load existing beat prefs + newOldBeatPrefCache = {threshold: this.get('threshold')}; + + this.changePlayerControl('threshold', preference.threshold); + this.set('usingBeatPreferences', true); + } else if(!Em.isNone(oldBeatPrefCache)) { // revert to using beat prefs before the remembered song + this.changePlayerControl('threshold', oldBeatPrefCache.threshold); + this.set('usingBeatPreferences', false); + } + + this.set('oldBeatPrefCache', newOldBeatPrefCache); + }, + + doAmbienceLightChange: function(justOneLight){ + var activeLights = this.get('activeLights'), + lightsData = this.get('lightsData'), + workedLights = this.get('ambienceWorkedLights'), + hueRange = this.get('hueRange'), + ambienceWorkedLightsHandles = this.get('ambienceWorkedLightsHandles'), + lightOff = (light)=>{ + if(this.get('ambienceMode') && this.get('playing')){ + Em.$.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({'on': false, 'transitiontime': 20}), + contentType: 'application/json', + type: 'PUT' + }); + } + }, + lights = [], + transitionTime = Math.floor(Math.random()*20), + iterations = justOneLight ? 1 : activeLights.length/2; + + // pick some random lights + for(let i=0; i < iterations; i++){ + let l = activeLights[Math.floor(Math.random()*activeLights.length)]; + + if(!lights.contains(l) && !workedLights.contains(l)){ + lights.push(l); + workedLights.push(l); + } else if(justOneLight && workedLights.length !== activeLights.length){ // work a light if we only need one + while(workedLights.contains(l)){ + l = activeLights[Math.floor(Math.random()*activeLights.length)]; + } + + lights.push(l); + workedLights.push(l); + } + } + + lights.forEach((light)=>{ + var options = {'hue': Math.floor(Math.random()*(hueRange[1] - hueRange[0] + 1)+hueRange[0]), 'bri': Math.floor(Math.random()*200) + 1, 'transitiontime': transitionTime}; + + if(lightsData[light].state.on === false){ + options.on = true; + } + + Em.$.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify(options), + contentType: 'application/json', + type: 'PUT' + }); + + // stop the light from turning off + if(ambienceWorkedLightsHandles[light]){ + clearTimeout(ambienceWorkedLightsHandles[light]); + delete ambienceWorkedLightsHandles[light]; + } + + // turn the light off after it's been idle for a while + ambienceWorkedLightsHandles[light] = setTimeout(()=>{ + lightOff(light); + workedLights.removeObject(light); + delete ambienceWorkedLightsHandles[light]; + }, transitionTime * 100 + 1000); + }); + }, + + onAmbienceModeChange: function() { + if(this.get('ambienceMode') && this.get('playing')) { + this.set('ambienceModeHandle', setInterval(()=> {this.doAmbienceLightChange();}, 5000)); + this.setProperties({ + 'colorloopMode': false, + 'flashingTransitions': false + }); + } else if(this.get('ambienceModeHandle')) { + this.get('activeLights').forEach((light)=>{ + Em.$.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({'on': true}), + contentType: 'application/json', + type: 'PUT' + }); + }); + + clearInterval(this.get('ambienceModeHandle')); + this.set('ambienceModeHandle', null); + } + }.observes('ambienceMode', 'playing'), + + startUsingMic() { + navigator.getUserMedia( + {audio: true}, + (stream) => { + this.changePlayerControl('audioMode', 1); + var dancer = this.get('dancer'); + + if(dancer.audio && dancer.audio.pause) { + dancer.pause(); + } + + this.setProperties({ + volumeCache: this.get('volume'), + playing: true, + audioStream: stream + }); + + document.title = 'Listening to Mic - Huegasm'; + + dancer.load(stream, this.get('micBoost'), true); + this.set('usingBeatPreferences', false); + + // much more sensitive beat preference settings are needed for mic mode + this.setProperties({ + oldThreshold: this.get('threshold'), + threshold: 0.1 + }); + + dancer.setVolume(0); + }, + (err) => { + if(err.name === 'DevicesNotFoundError'){ + this.get('notify').alert({html: this.get('notFoundHtml')}); + } + + console.log('Error during navigator.getUserMedia: ' + err.name + ', ' + err.message + ', ' + err.constraintName); + } + ); + }, + + updatePageTitle: function(){ + var title = 'Huegasm', playQueuePointer = this.get('playQueuePointer'), playQueue = this.get('playQueue'); + + if(playQueuePointer !== -1){ + var song = playQueue[playQueuePointer]; + if(song.title){ + title = song.title; + + if(song.artist){ + title += (' - ' + song.artist); + } + } else { + title = song.fileName; + } + + title += '- Huegasm'; + } + + document.title = title; + }.observes('playQueuePointer'), + + clearCurrentAudio(resetPointer) { + var dancer = this.get('dancer'); + + if(dancer.audio.pause) { + dancer.pause(); + } + + if(resetPointer){ + this.set('playQueuePointer', -1); + } + + this.setProperties({ + timeElapsed: 0, + timeTotal: 0, + playing: false + }); + }, + + dragOver() { + var dragLeaveTimeoutHandle = this.get('dragLeaveTimeoutHandle'); + this.set('dragging', true); + + if (dragLeaveTimeoutHandle) { + clearTimeout(dragLeaveTimeoutHandle); + } + }, + + dragLeave(){ + // need to delay the dragLeave notification to avoid flickering ( hovering over some page elements causes this event to be sent ) + var self = this; + this.set('dragLeaveTimeoutHandle', setTimeout(function(){ self.set('dragging', false); }, 500)); + }, + + simulateKick(/*mag, ratioKickMag*/) { + var activeLights = this.get('activeLights'), + lightsData = this.get('lightsData'), + color = null, + transitiontime = this.get('flashingTransitions'), + stimulateLight = (light, brightness, hue) => { + var options = {'bri': brightness}; + + if(transitiontime) { + options['transitiontime'] = 0; + } else { + options['transitiontime'] = 1; + } + + if(!Em.isNone(hue)) { + options.hue = hue; + } + + if(lightsData[light].state.on === false){ + options.on = true; + } + + Em.$.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify(options), + contentType: 'application/json', + type: 'PUT' + }); + }, + timeToBriOff = 100; + + if(activeLights.length > 0 && !this.get('ambienceMode')){ + var lastLightBopIndex = this.get('lastLightBopIndex'), + lightBopIndex, + brightnessOnBeat = 254, + light; + + lightBopIndex = Math.floor(Math.random() * activeLights.length); + + // let's try not to select the same light twice in a row + if(activeLights.length > 1) { + while(lightBopIndex === lastLightBopIndex) { + lightBopIndex = Math.floor(Math.random() * activeLights.length); + } + } + + light = activeLights[lightBopIndex]; + this.set('lastLightBopIndex', lightBopIndex); + + if(!this.get('colorloopMode')) { + var hueRange = this.get('hueRange'); + + color = Math.floor(Math.random()*(hueRange[1] - hueRange[0] + 1)+hueRange[0]); + } + + if(transitiontime){ + timeToBriOff = 80; + } + + stimulateLight(light, brightnessOnBeat, color); + setTimeout(stimulateLight, timeToBriOff, light, 1); + } + + this.set('paused', true); + setTimeout(() => { + this.set('paused', false); + }, 150); + + if(this.get('ambienceMode') && activeLights.length > 0){ + this.doAmbienceLightChange(true); + } + + //work the music beat area - simulate the speaker vibration by running a CSS animation on it + Em.$('#beatSpeakerCenterOuter').removeClass('vibrateOuter').prop('offsetWidth', Em.$('#beatSpeakerCenterOuter').prop('offsetWidth')).addClass('vibrateOuter'); + Em.$('#beatSpeakerCenterInner').removeClass('vibrateInner').prop('offsetWidth', Em.$('#beatSpeakerCenterInner').prop('offsetWidth')).addClass('vibrateInner'); + }, + + init() { + this._super(); + + window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; + window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame; + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; + + var dancer = new Dancer(), + storage = this.get('storage'), + kick = dancer.createKick({ + threshold: this.get('threshold'), + onKick: (mag, ratioKickMag) => { + if (this.get('paused') === false) { + this.simulateKick(mag, ratioKickMag); + } + } + }); + + kick.on(); + + this.setProperties({ + dancer: dancer, + kick: kick + }); + + if(navigator.getUserMedia === undefined){ + this.set('usingMicSupported', false); + } + + ['volume', 'shuffle', 'repeat', 'volumeMuted', 'threshold', 'playerBottomDisplayed', 'audioMode', 'songBeatPreferences', 'firstVisit', 'currentVisName', 'playQueue', 'playQueuePointer', 'micBoost', 'flashingTransitions', 'colorloopMode', 'ambienceMode', 'hueRange'].forEach((item)=>{ + if (!Em.isNone(storage.get('huegasm.' + item))) { + var itemVal = storage.get('huegasm.' + item); + + if(Em.isNone(this.actions[item+'Changed'])){ + this.set(item, itemVal); + } else { + this.send(item + 'Changed', itemVal); + } + } + }); + + SC.initialize({ + client_id: this.get('SC_CLIENT_ID') + }); + }, + + didInsertElement() { + this._super(); + + var self = this; + + // file input code + Em.$('#fileInput').on('change', function () { + var files = this.files; + self.send('handleNewFiles', files); + this.value = null; // reset in case upload the second file again + }); + + Em.$(document).on('click', '.alert', (event)=>{ + Em.$(event.target).addClass('removed'); + }); + + // prevent space/text selection when the user repeatedly clicks on the center + Em.$('#beatContainer').on('mousedown', '#beatSpeakerCenterInner', function(event) { + event.preventDefault(); + }); + + Em.$(document).keypress((event) => { + if(event.which === 32 && event.target.type !== 'text'){ + this.send('play'); + } + }); + + this.$().on('drop', '#playListArea', (event)=>{ + this.send('dropFiles', event.dataTransfer.files); + }); + + // control the volume by scrolling up/down + Em.$('#playerArea').on('mousewheel', (event)=>{ + if(this.get('playQueueNotEmpty') && !this.get('usingMicAudio')) { + var scrollSize = 5; + + if(event.deltaY < 0) { + scrollSize *= -1; + } + var newVolume = this.get('volume') + scrollSize; + + this.send('volumeChanged', newVolume < 0 ? 0 : newVolume); + event.preventDefault(); + } + }); + + // demo tracks + if(this.get('firstVisit')){ + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/candyland-speechless-feat-rkcb'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/vallis-alps-young-feki-remix'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/andrew-luce-when-to-love-you-feat-chelsea-cutler'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/ahh-ooh-carefree-with-me'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/odesza/light-feat-little-dragon'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/crywolf-slow-burn'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/clozee-red-forest'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/elo-method-subranger-solace'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/90-pounds-of-pete-waited-too-long-feat-devon-baldwin'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/draper-eyes-open'); + + this.get('storage').set('huegasm.firstVisit', false); + + this.sendAction(); + } + + if(!this.get('playerBottomDisplayed')) { + Em.$('#playerBottom').hide(); + } + } +}); diff --git a/app/pods/components/music-tab/mixins/helpers.js b/app/pods/components/music-tab/mixins/helpers.js new file mode 100644 index 0000000..410d006 --- /dev/null +++ b/app/pods/components/music-tab/mixins/helpers.js @@ -0,0 +1,406 @@ +import Em from 'ember'; + +export default Em.Mixin.create({ + classNames: ['col-sm-10', 'col-sm-offset-1', 'col-xs-12'], + classNameBindings: ['active::hidden'], + elementId: 'musicTab', + + dancer: null, + + notify: Em.inject.service('notify'), + + beatOptions: { + threshold: { + range: {min: 0, max: 0.5}, + step: 0.01, + defaultValue: 0.3, + pips: { + mode: 'values', + values: [0, 0.25, 0.5], + density: 10, + format: { + to: function ( value ) { + if(value === 0) { + value = 'More'; + } else if(value === 0.25) { + value = ''; + } else { + value = 'Less'; + } + + return value; + }, + from: function ( value ) { return value; } + } + } + }, + hueRange: { + range: {min: 0, max: 65535}, + step: 1, + defaultValue: 0.3, + pips: { + mode: 'values', + values: [0, 25500, 46920, 65535], + density: 10, + format: { + to: function ( value ) { + if(value === 0 || value === 65535) { + value = 'Red'; + } else if(value === 25500 ) { + value = 'Green'; + } else { + value = 'Blue'; + } + + return value; + }, + from: function ( value ) { return value; } + } + } + }, + micBoost: { + range: {min: 1, max: 11}, + step: 0.5, + defaultValue: 5, + pips: { + mode: 'positions', + values: [0,20,40,60,80,100], + density: 10, + format: { + to: function ( value ) {return 'x'+value;}, + from: function ( value ) { return value; } + } + } + } + }, + + threshold: 0.3, + hueRange: [0, 65535], + micBoost: 5, + oldThreshold: null, + + playQueuePointer: -1, + playQueue: Em.A(), + timeElapsed: 0, + timeTotal: 0, + lastLightBopIndex: 0, + + usingMicSupported: false, + // 0 - local, 1 - mic, possibly more to come + audioMode: 0, + usingLocalAudio: Em.computed.equal('audioMode', 0), + usingMicAudio: Em.computed.equal('audioMode', 1), + + playerBottomDisplayed: false, + dragging: false, + draggingOverPlayListArea: false, + dragLeaveTimeoutHandle: null, + ambienceModeHandle: null, + audioStream: null, + dimmerOn: false, + isShowingAddSoundCloudModal: false, + + colorloopMode: false, + ambienceMode: false, + flashingTransitions: false, + + SC_CLIENT_ID: 'aeec0034f58ecd85c2bd1deaecc41594', + notFoundHtml: '', + scUserNotSupportedHtml: '', + tooManySoundCloudFuckUps: '', + notStreamableHtml(fileNames){ + var html = ''; + + return html; + }, + urlNotFoundHtml(url){ + return ''; + }, + failedToPlayFileHtml(fileName){ + return ''; + }, + failedToDecodeFileHtml(fileName){ + return ''; + }, + + scUrl: function(){ + var rtn = null, + currentSong = this.get('playQueue')[this.get('playQueuePointer')]; + + if(currentSong && currentSong.scUrl && !this.get('usingMicAudio')){ + rtn = currentSong.scUrl; + } + + return rtn; + }.property('playQueuePointer', 'playQueue.[]', 'usingMicAudio'), + + playQueueEmpty: Em.computed.empty('playQueue'), + playQueueNotEmpty: Em.computed.notEmpty('playQueue'), + playQueueMultiple: function(){ + return this.get('playQueue').length > 1; + }.property('playQueue.[]'), + + seekPosition: function() { + var timeTotal = this.get('timeTotal'), timeElapsed = this.get('timeElapsed'); + + if (timeTotal === 0) { + return 0; + } + + return timeElapsed/timeTotal*100; + }.property('timeElapsed', 'timeTotal'), + + // 0 - no repeat, 1 - repeat all, 2 - repeat one + repeat: 0, + shuffle: false, + volumeMuted: false, + volume: 100, + // beat detection related pausing + paused: false, + // audio: playing or paused + playing: false, + fadeOutNotification: false, + songBeatPreferences: {}, + usingBeatPreferences: false, + oldBeatPrefCache: null, + storage: null, + firstVisit: true, + ambienceWorkedLights: [], + ambienceWorkedLightsHandles: {}, + + soundCloudFuckUps: 0, + maxSoundCloudFuckUps: 3, + + largeArtworkPic: function(){ + var pic = null, + currentVisName = this.get('currentVisName'), + usingMicAudio = this.get('usingMicAudio'), + playQueuePointer = this.get('playQueuePointer'), + playQueue = this.get('playQueue'); + + if(playQueuePointer !== -1 && !usingMicAudio && currentVisName === 'None'){ + var song = playQueue[playQueuePointer]; + if(song.scUrl){ + pic = song.picture.replace('67x67', '500x500'); + } else { + pic = song.picture; + } + } + + return pic; + }.property('playQueuePointer', 'usingMicAudio', 'currentVisName'), + + // used to insure that we don't replay the same thing multiple times in shuffle mode + shufflePlayed: [], + pauseLightUpdates: function(){ + return this.get('playing'); + }.property('playing'), + + micIcon: function () { + if (this.get('usingMicAudio')) { + return 'mic'; + } + + return 'mic-off'; + }.property('usingMicAudio'), + + repeatIcon: function () { + if (this.get('repeat') === 2) { + return 'repeat-one'; + } + + return 'repeat'; + }.property('repeat'), + + playingIcon: function () { + if(this.get('playing')){ + return 'pause'; + } else if(this.get('timeElapsed') === this.get('timeTotal') && this.get('timeTotal') !== 0){ + return 'replay'; + } else { + return 'play-arrow'; + } + }.property('playing'), + + playListAreaClass: function(){ + var classes = 'cursorPointer'; + + if(this.get('dragging')){ + classes += ' dragHereHighlight'; + } + + if(this.get('draggingOverPlayListArea')){ + classes += ' draggingOver'; + } + + if(this.get('dimmerOn')){ + classes += ' dimmerOn'; + } + + return classes; + }.property('dragging', 'draggingOverPlayListArea', 'dimmerOn'), + + dimmerOnClass: function(){ + return this.get('dimmerOn') ? 'dimmerOn' : null; + }.property('dimmerOn'), + + volumeMutedClass: function(){ + var classes = 'playerControllIcon volumeButton'; + + if(this.get('volumeMuted')){ + classes += ' active'; + } + + return classes; + }.property('volumeMuted'), + + usingLocalAudioClass: function() { + return this.get('usingLocalAudio') ? 'playerControllIcon active' : 'playerControllIcon'; + }.property('usingLocalAudio'), + + usingMicAudioClass: function() { + return this.get('usingMicAudio') ? 'playerControllIcon active' : 'playerControllIcon'; + }.property('usingMicAudio'), + + repeatClass: function () { + return this.get('repeat') !== 0 ? 'playerControllIcon active' : 'playerControllIcon'; + }.property('repeat'), + + shuffleClass: function () { + return this.get('shuffle') ? 'playerControllIcon active' : 'playerControllIcon'; + }.property('shuffle'), + + volumeIcon: function () { + var volume = this.get('volume'); + + if (this.get('volumeMuted')) { + return "volume-off"; + } else if (volume >= 70) { + return "volume-up"; + } else if (volume > 10) { + return "volume-down"; + } else { + return 'volume-mute'; + } + }.property('volumeMuted', 'volume'), + + onColorloopModeChange: function(){ + var colorLoop = ((this.get('playing') || this.get('usingMicAudio')) && this.get('colorloopMode')) ? true : false; + + this.set('colorLoopOn', colorLoop); + }.observes('colorloopMode', 'usingMicAudio', 'playing'), + + onOptionChange: function(self, option){ + option = option.replace('.[]', ''); + this.get('storage').set('huegasm.' + option, this.get(option)); + }.observes('flashingTransitions', 'playQueue.[]', 'playQueuePointer', 'colorloopMode', 'ambienceMode'), + + onRepeatChange: function () { + var tooltipTxt = 'Repeat all', type = 'repeat'; + + if (this.get(type) === 1) { + tooltipTxt = 'Repeat one'; + } else if (this.get(type) === 2) { + tooltipTxt = 'Repeat off'; + } + + this.changeTooltipText(type, tooltipTxt); + }.observes('repeat').on('init'), + + onUsingMicAudioChange: function(){ + var tooltipTxt = 'Listen to audio through mic', type = 'usingMicAudio'; + + if (this.get(type)) { + tooltipTxt = 'Listen to audio files'; + } + + this.changeTooltipText(type, tooltipTxt); + }.observes('usingMicAudio').on('init'), + + onShuffleChange: function () { + var tooltipTxt = 'Shuffle', type = 'shuffle'; + + if (this.get(type)) { + this.get('shufflePlayed').clear(); + tooltipTxt = 'Unshuffle'; + } + + this.changeTooltipText(type, tooltipTxt); + }.observes('shuffle').on('init'), + + onVolumeMutedChange: function () { + var tooltipTxt = 'Mute', type = 'volumeMuted', + volumeMuted = this.get(type), dancer = this.get('dancer'), + volume=0; + + if (volumeMuted) { + tooltipTxt = 'Unmute'; + volume = 0; + } else { + volume = this.get('volume')/100; + } + + if(this.get('playing')){ + dancer.setVolume(volume); + } + + this.changeTooltipText(type, tooltipTxt); + }.observes('volumeMuted').on('init'), + + onPrevChange: function() { + if(this.get('playQueueNotEmpty')){ + var tooltipTxt = 'Previous', type = 'prev'; + + if(this.get('timeElapsed') > 5 || this.get('playQueue').length === 1) { + tooltipTxt = 'Replay'; + } + + this.changeTooltipText(type, tooltipTxt); + } + }.observes('timeElapsed', 'playQueueNotEmpty', 'playQueue.[]'), + + onPlayingChange: function () { + var tooltipTxt = 'Play', type = 'playing'; + + if (this.get(type)) { + tooltipTxt = 'Pause'; + } else if(this.get('timeElapsed') === this.get('timeTotal') && this.get('timeTotal') !== 0){ + tooltipTxt = 'Replay'; + } + + this.changeTooltipText(type, tooltipTxt); + }.observes('playing').on('init'), + + changeTooltipText(type, text) { + // change the tooltip text if it's already visible + Em.$('#' + type + 'Tooltip + .tooltip .tooltip-inner').html(text); + //change the tooltip text for hover + Em.$('#' + type + 'Tooltip').attr('data-original-title', text); + + if(Em.isNone(this.get(type + 'TooltipTxt'))) { + this.set(type + 'TooltipTxt', text); + } + }, + + beatDetectionAreaArrowIcon: function(){ + if(!this.get('playerBottomDisplayed')){ + return 'keyboard-arrow-down'; + } else { + return 'keyboard-arrow-up'; + } + }.property('playerBottomDisplayed'), + + timeElapsedTxt: function(){ + return this.formatTime(this.get('timeElapsed')); + }.property('timeElapsed'), + + timeTotalTxt: function() { + return this.formatTime(this.get('timeTotal')); + }.property('timeTotal'), + + formatTime(time){ + return this.pad(Math.floor(time/60), 2) + ':' + this.pad(time%60, 2); + }, + + pad(num, size){ return ('000000000' + num).substr(-size); } +}); diff --git a/app/pods/components/music-tab/mixins/visualizer.js b/app/pods/components/music-tab/mixins/visualizer.js new file mode 100644 index 0000000..98ac505 --- /dev/null +++ b/app/pods/components/music-tab/mixins/visualizer.js @@ -0,0 +1,88 @@ +import Em from 'ember'; + +export default Em.Mixin.create({ + currentVisName: 'None', + + visNames: ['None', 'Bars', 'Wave'], + + onCurrentVisNameChange: function () { + var currentVisName = this.get('currentVisName'); + + if(currentVisName === 'None'){ + var canvasEl = Em.$('#visualization')[0], + ctx = canvasEl.getContext('2d'); + + ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); + } + + this.get('storage').set('huegasm.currentVisName', currentVisName); + }.observes('currentVisName'), + + didInsertElement(){ + var dancer = this.get('dancer'), + canvas = Em.$('#visualization')[0], + playerArea = Em.$('#playerArea'), + ctx = canvas.getContext('2d'), + spacing = 2, + h = playerArea.height(), w; + + canvas.height = h; + + // must be done to preserver resolution so that things don't appear blurry + // note that the height is set to 400px via css so it doesn't need to be recalculated + var syncCanvasHeight = ()=>{ + w = playerArea.width(); + canvas.width = w; + }; + + syncCanvasHeight(); + + Em.$(window).on('resize', syncCanvasHeight); + + dancer.bind('update', () => { + var currentVisName = this.get('currentVisName'), + gradient = ctx.createLinearGradient(0, 0, 0, h), + pageHidden = document.hidden || document.msHidden || document.webkitHidden || document.mozHidden; + + // dont do anything if the page is hidden or no visualization + if(currentVisName === 'None' || pageHidden || !this.get('active')){ + return; + } + + ctx.clearRect(0, 0, w, h); + + if (currentVisName === 'Wave') { + let width = 3, + count = 1024; + + gradient.addColorStop(0.6, 'white'); + gradient.addColorStop(0, '#0036FA'); + + ctx.lineWidth = 1; + ctx.strokeStyle = gradient; + var waveform = dancer.getWaveform(); + + ctx.beginPath(); + ctx.moveTo(0, h / 2); + for (let i = 0, l = waveform.length; i < l && i < count; i++) { + ctx.lineTo(i * ( spacing + width ), ( h / 2 ) + waveform[i] * ( h / 2 )); + } + ctx.stroke(); + ctx.closePath(); + } else if (currentVisName === 'Bars') { + let width = 4, + count = 128; + + gradient.addColorStop(1, '#0f0'); + gradient.addColorStop(0.6, '#ff0'); + gradient.addColorStop(0.2, '#F12B24'); + + ctx.fillStyle = gradient; + var spectrum = dancer.getSpectrum(); + for (let i = 0, l = spectrum.length; i < l && i < count; i++) { + ctx.fillRect(i * ( spacing + width ), h, width, -spectrum[i] * h - 60); + } + } + }); + } +}); diff --git a/app/pods/components/music-tab/template.hbs b/app/pods/components/music-tab/template.hbs new file mode 100644 index 0000000..aec2b29 --- /dev/null +++ b/app/pods/components/music-tab/template.hbs @@ -0,0 +1,207 @@ +
+
+ +
+
+ +
+ {{#if usingLocalAudio}} + {{range-slider start=seekPosition min=0 max=100 id="seekSlider" slide="seekChanged"}} + + {{#if playQueueNotEmpty}} + {{paper-icon icon="skip-previous" class="playerControllIcon"}}{{/if}}{{paper-icon icon=playingIcon class="playerControllIcon"}}{{#if playQueueMultiple}}{{paper-icon icon="skip-next" action="" class="playerControllIcon"}}{{/if}}{{paper-icon icon=volumeIcon class=volumeMutedClass}}{{range-slider start=volume min=0 max=100 slide="volumeChanged" id="volumeBar" class="hidden-xs"}} + +
{{timeElapsedTxt}} / {{timeTotalTxt}}
+ {{/if}} + + + {{#if scUrl}} + + + + + {{/if}} + + + + {{paper-icon icon="remove-red-eye" class="playerControllIcon"}} + + + + + + +
+
+ +
+ + +
+ {{#if usingLocalAudio}} + + + + + {{!--{{#if (and usingLocalAudio playQueueNotEmpty)}} + + {{/if}} --}} + {{/if}} + + {{#if usingMicSupported}} + {{paper-icon icon=micIcon class=usingMicAudioClass}} + {{/if}} + + {{#if usingLocalAudio}} + {{paper-icon icon="shuffle" class=shuffleClass}} + {{paper-icon icon=repeatIcon class=repeatClass}} + {{paper-icon icon="clear-all" class="playerControllIcon"}} + {{/if}} +
+ +{{#if usingMicAudio}} +
+ {{paper-icon icon="mic" class=dimmerOnClass}} +
+{{else}} + {{#if usingLocalAudio}} +
+ {{#if (or playQueueEmpty dragging)}} +
+ {{#if dragging}} + Drag your music files here + {{else}} + Add your music files here + {{/if}} +
+ {{paper-icon icon="library-music" class=dimmerOnClass}} + {{/if}} + + {{#each playQueue as |item index|}} +
+ {{#if item.picture}} + + {{else}} + + {{/if}} + +
+ {{#if item.title}} +
{{item.title}}
+
+ {{#if item.artistUrl}} + {{item.artist}} + {{else}} + {{item.artist}} + {{/if}} +
+ {{else}} + {{item.fileName}} + {{/if}} +
+ + {{paper-icon icon="close"}} +
+ {{/each}} +
+ {{/if}} +{{/if}} +
+
+ +
+
+ {{paper-icon icon=beatDetectionAreaArrowIcon id="beatDetectionAreaArrowIcon"}} +
+
+ +
+
+{{#if usingBeatPreferences}} + + {{paper-icon class=dimmerOnClass icon="star"}} + +{{/if}} + +
+
+ Sensitivity + {{range-slider start=threshold orientation="vertical" step=beatOptions.threshold.step range=beatOptions.threshold.range slide="thresholdChanged" pips=beatOptions.threshold.pips}} +
+ +
+ Hue Range + {{range-slider start=hueRange orientation="vertical" step=beatOptions.hueRange.step range=beatOptions.hueRange.range slide="hueRangeChanged" pips=beatOptions.hueRange.pips}} +
+ + {{#if usingMicAudio}} +
+ Mic Boost + {{range-slider start=micBoost orientation="vertical" step=beatOptions.micBoost.step range=beatOptions.micBoost.range slide="micBoostChanged" pips=beatOptions.micBoost.pips}} +
+ {{/if}} + +
+ + {{#paper-checkbox checked=flashingTransitions}}Flashing Transitions{{/paper-checkbox}} + + + + {{#paper-checkbox checked=colorloopMode}}Colorloop{{/paper-checkbox}} + + +{{!-- + {{#paper-checkbox checked=ambienceMode}}Ambience{{/paper-checkbox}} +--}} +
+
+ +
+ + +
+ +{{ember-notify closeAfter=100000 classPrefix="customNotify"}} + +{{add-soundcloud-sound-modal action="handleNewSoundCloudURL" isShowingModal=isShowingAddSoundCloudModal}} \ No newline at end of file diff --git a/app/resolver.js b/app/resolver.js new file mode 100644 index 0000000..2fb563d --- /dev/null +++ b/app/resolver.js @@ -0,0 +1,3 @@ +import Resolver from 'ember-resolver'; + +export default Resolver; diff --git a/app/router.js b/app/router.js new file mode 100644 index 0000000..3bba78e --- /dev/null +++ b/app/router.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; +import config from './config/environment'; + +const Router = Ember.Router.extend({ + location: config.locationType +}); + +Router.map(function() { +}); + +export default Router; diff --git a/app/styles/app.scss b/app/styles/app.scss new file mode 100644 index 0000000..a0a6a26 --- /dev/null +++ b/app/styles/app.scss @@ -0,0 +1,1169 @@ +@import 'ember-paper'; +@import 'bower_components/bootstrap-sass/assets/stylesheets/_bootstrap'; +@import 'ember-modal-dialog/ember-modal-structure'; +@import 'ember-modal-dialog/ember-modal-appearance'; +@import 'fancy-speaker'; + +$playerHeight: 400px; +$playerDefaultIconColor: #BBBBBB; +$footerHeight: 40px; +$playerBottomHeight: 250px; +$secondaryThemeColor: #F12B24; +$glowingText: 0 0 2px #fff, 0 0 8px #fff, 0 0 20px #228DFF; +$dimmerOnButtonColor: #404040; + + // BRIDGE FINDER +html { + min-height: 100%; + height: auto; +} + +body { + font-family: 'Slabo 27px', serif; + margin-bottom: $footerHeight + 30px; + position: static; +} + +#intro { + font-size: 18px; +} + +#introParagraph { + margin-bottom: 50px; +} + +.embedContainer { + position:relative; + padding-bottom:56.25%; + padding-top:30px; + height:0; + overflow:hidden; +} + +.embedContainerWrapper { + max-width: 600px; + margin: auto; +} + +.embedContainer iframe, .embedContainer object, .embedContainer embed { + position:absolute; + top:0; + left:0; + width:100%; + height:100%; +} + +.goButton { + margin-top: 20px; + border-radius: 100% !important; + width: 100px; + height: 100px; + span { + font-size: 28px; + width: 100%; + } +} + +.introjs-overlay { + background: black; +} + +md-checkbox.md-default-theme .md-icon { + border-color: inherit !important; +} + +md-checkbox.md-default-theme.md-checked .md-icon { + background: $secondaryThemeColor; +} + +md-checkbox .md-label { + width: 125px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.text-left { + text-align: left !important; +} + +.goButton:hover { + background: darken(#3f51b5, 10%) !important; +} + +.md-button { + flex-direction: unset; + span { + width: 100%; + } +} + +.alert { + margin-bottom: 0; +} + +.removed { + animation: disapear 1s; + animation-fill-mode: forwards; +} + +@keyframes disapear{ + 50% { + -webkit-transform: translateX(-5%); + transform: translateX(-5%); + } + 100% { + -webkit-transform: translateX(200%); + transform: translateX(200%); + } +} + +.relative { + position: relative; +} + +#settings { + padding-right: 5px; + text-align: right; + z-index: 3; + font-size: 16px; +} + +.settingsItem { + position: relative; + display: inline-block; + transition: 0.1s all ease-in-out; + cursor: pointer; + span md-icon { + position: relative; + bottom: 5px; + right: 5px; + } +} + +.settingsItem:nth-of-type(2){ + margin-left: 10px; +} + +md-switch[disabled=disabled], md-switch[disabled=disabled] .md-container, md-slider[disabled=disabled] { + cursor: not-allowed; +} + +.settingsItem .settings::before, .settingsItem .group::before { + font-size: 28px; + transition: 0.1s all ease-in-out; +} + +.settingsItem span:hover { + text-decoration: underline; + md-icon.group { + color: black !important; + } + md-icon.settings { + color: black !important; + } +} + +.ember-app { + padding-bottom: 50px; +} + +#footer { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: $footerHeight; + p { + padding-left: 0; + text-align: center; + } + a { + margin-left: 5px; + } +} + +.cursorPointer { + cursor: pointer; +} + +.bridgeFinder, .readyBlock { + text-align: center; + padding-top: 10px; +} + +// preloading image +.readyBlock:after { + display: none; + content: url(images/pressButtonBridge.png); +} + +.title { + margin-bottom: 30px; + img { + width: 200px; + } +} + +#bridgeInput { + md-input-container { + max-width: 200px; + margin: 0 auto; + } +} + +md-progress-circular { + margin: 0 auto 20px auto !important; +} + +md-progress-linear { + margin-bottom: 50px; +} + +#pressButtonBridgeImg { + width: 200px; + margin: 0 auto 30px auto; + display: inherit; +} + +.noTextDecoration { + text-decoration: none; +} + +#bridgeButtonGroup { + width: 150px; + margin: 30px auto; + text-align: left; +} + +// HUE CONTROLS +#appSettings { + position: absolute; + background: white; + box-shadow: 5px 10px 15px 5px rgba(0, 0, 0, 0.3); + width: 175px; + left: -85px; + top: 20px; + border: none; + color: black; +} + +.bootstrapTooltip md-icon { + font-size: 22px; + cursor: pointer; + -webkit-transform: translate3d(0, 0, 0); // hack for chrome to force hardware acceleration and stop flickering +} + +md-list { + padding: 0; +} + +md-list-item .md-no-style { + padding: 0; +} + +#lightsTab { + min-height: 400px; +} + +.lightCtrlTooltip + .tooltip { + left: -20px !important; +} + +#colorRow { + cursor: pointer; +} + +#colorRow * .tooltip { + left: -7px !important; +} + +#hueControls { + max-width: 1200px; + position: relative; +} + +// preload images +#hueControls:after, md-progress-circular:after { + display: none; + content: url(images/colormap.png) url(images/missingArtwork.png) url(images/sc-white.png) url(/favicon-96x96.png) url(images/lights/a19.svg) url(images/lights/a19w.svg) url(images/lights/br30.svg) url(images/lights/br30w.svg) url(images/lights/gu10.svg) url(images/lights/gu10w.svg) url(images/lights/huego.svg) url(images/lights/huegow.svg) url(images/lights/lc_aura.svg) url(images/lights/lc_auraw.svg) url(images/lights/lc_bloom.svg) url(images/lights/lc_bloomw.svg) url(images/lights/lc_iris.svg) url(images/lights/lc_irisw.svg) url(images/lights/lightstrip.svg) url(images/lights/lightstripw.svg) url(images/lights/storylight.svg) url(images/lights/storylightw.svg); +} + +.navigation { + text-align: center; + padding: 30px 0; +} + +.navigationItem { + font-size: 18px; + padding: 0 10px 0 10px; +} + +.navigationItem.active { + font-weight: bold; + cursor: default; + text-decoration: none !important; +} + +.navigationItem:hover { + text-decoration: underline; +} + +.color { + border: 1px solid rgba(0, 0, 0, 0.5); + position: absolute; + top: -53px; + right: 0; +} + +.colorpicker { + padding: 5px; + background: rgba(0, 0, 0, 0.7); + box-shadow: 5px 10px 15px 5px rgba(0, 0, 0, 0.3); + color: #FFFFFF; + position: absolute; + width: 266px; + height: 266px; + right: 6px; + top: -9px; + z-index: 3; +} + +#picker { + cursor: crosshair; +} + +#loopAddition { + position: absolute; + left: 18px; + top: 13px; + font-size: 16px; +} + +// LIGHT GROUP +md-slider { + cursor: pointer; +} + +md-slider.md-default-theme .md-thumb:after { + border-color: $secondaryThemeColor; + background-color: $secondaryThemeColor; +} + +.paper-sidenav { + overflow: visible; +} + +#groupList { + box-shadow: 5px 10px 15px 5px rgba(0, 0, 0, 0.3); + border-radius: 0 0 5px 5px; + width: 300px; + top: 20px; + left: -210px; + position: absolute; + background-color: white; + max-height: 400px; + overflow: auto; + text-align: left; + padding: 0; + color: black; +} + +.lightGroup { + margin: 0 auto 0 auto; + .tooltip.top { + margin-top: 4px; + margin-left: 0; + } + div { + display: inline-block; + } +} + +.lightInactive { + cursor: pointer; + position: relative; +} + +.lightInactive::before { + font-weight: bold; + position: absolute; + content: "X"; + top: -10px; + left: 5px; + font-size: 40px; + color: rgba(255, 0, 0, 0.37); + font-family: cursive; +} + +.horizontalLightGroup { + .lightInactive::before { + top: -9px; + left: 6px; + } + .tooltip.top { + margin-top: 1px; + margin-left: 2px; + } +} + +.lightActive { + cursor: pointer; + + img { + transition-duration: 0.3s; + transition-property: transform; + box-shadow: 0 0 1px rgba(0, 0, 0, 0); + } + img:hover { + -webkit-transform: scale(1.2); + transform: scale(1.2); + } +} + +.ember-modal-overlay.translucent { + background-color: rgba(0, 0, 0, 0.50); +} + +md-icon { + color: rgba(0, 0, 0, 0.54) !important; +} + +.addButton { + width: 100%; + cursor: pointer; + margin-left: 10px; +} + +.removeButton { + margin: 10px 0 10px 60px; +} + +.sideNavTitle { + margin-left: 16px; + margin-top: 10px; + text-align: left; +} + +md-toolbar { + background-color: inherit !important; +} + +// GROUP CONTROL +.groupRow { + transition: 0.1s all ease-in-out; +} + + +.groupRow.selectedRow { + background-color: #7F7F7F !important; + color: white; +} + +.groupRow:hover { + background-color: #DEDEDE; +} + +.groupRow.selectedRow .groupSelect { + cursor: default; +} + +.groupSelect { + cursor: pointer; + padding: 10px 0 10px 10px; + width: 70%; + font-family: 'Open Sans', sans-serif; +} + +.newGroup { + font-size: 18px; + min-height: 100%; + width: 100%; + padding: 10px 0 10px 10px; + margin: 0; + .group-add { + margin-right: 5px; + } +} + +.newGroupRow{ + background: white; +} + +.newGroupRow:hover { + background-color: darken(white, 5%); +} + +.groupRow:hover * .close { + display: block; +} + +.groupRow:hover * .close { + display: block; +} + +.selectedRow * .close { + color: white !important; +} + +.selectedRow.groupRow * .close:hover { + color: darken(white, 20%) !important; +} + +.close:hover { + color: darken(#333333, 5%) !important; +} + +.close:hover { + opacity: 1; +} + +.close { + font-size: 18px !important; + color: rgb(51, 51, 51); + display: none; + opacity: 1; + text-shadow: none; +} + +// MUSIC TAB +.row { + margin: 0; +} + +#beatOptionRow { + height: 250px; +} + +#lightOption { + text-align: left !important; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + .tooltip { + margin-top: 5px !important; + left: 0 !important; + } +} + +#musicTab{ + padding: 0; + margin-bottom: $footerHeight + 30px; +} + +#slideToggle { + color: $playerDefaultIconColor; + background: #730B07; + div md-icon { + color: inherit !important; + } +} + +#slideToggle:hover{ + color: lighten($playerDefaultIconColor, 30%) !important;; +} + +md-switch.md-default-theme.md-checked .md-bar { + background-color: rgba(241, 43, 36, 0.5); +} + +md-switch.md-default-theme.md-checked .md-thumb { + background-color: $secondaryThemeColor; +} + +#playerControls { + transition: all 0.2s ease-in-out; + position: absolute; + bottom: 0; + left: 0; + padding: 5px 10px; + width: 100%; + color: white !important; + z-index: 20; + background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 1)); + .tooltip.top { + margin-top: -17px; + } + .tooltip-arrow { + display: none; + } + .play-arrow, .pause, .replay { + font-size: 30px; + } +} + +#playerTimeControls { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + display: inline-block; + margin-left: 1em; +} + +.playerControllIcon { + color: $playerDefaultIconColor !important; + transition-duration: 0.1s; + margin-right: 5px; +} + +.playerControllIcon.active { + color: $secondaryThemeColor !important; +} + +.playerControllIcon:hover { + color: white !important; +} + +#playNotification { + position: relative; + color: white; + top: 50%; + left: 50%; + opacity: 0; + background: black; + border-radius: 100%; +} + +.fadeOut { + animation-duration: 1s; + animation-name: fadeOut; +} + +@keyframes fadeOut { + from { + opacity: 0.8; + font-size: 24px; + } + to { + opacity: 0; + transform: scale(3); + -webkit-transform: scale(3); + } +} + +#playerArea { + height: $playerHeight; + background-color: black; + display: inline-block; + padding: 0; +} + +#playlist { + height: $playerHeight; + background-color: #1E1E1E; + padding: 0 5px 0 5px; +} + +#playerArea * .noUi-origin { + background-color: black; + border-radius: 5px; +} + +#playerArea * .noUi-base { + background-color: $secondaryThemeColor; + border-radius: 5px; +} + +.noUi-base { + cursor: pointer; +} + +.noUi-connect { + background-color: $secondaryThemeColor; +} + +#volumeBar { + width: 5em; + height: 0.4em; + display: inline-block; +} + +.noUi-handle { + cursor: pointer; +} + +.noUi-horizontal .noUi-handle { + width: 0.4em; + height: 1.3em; + left: -0.071em; + top: -0.500em; + transition-duration: 0.1s; + background: $playerDefaultIconColor !important; +} + +.noUi-horizontal .noUi-handle:hover { + background: white !important; +} + +#playerArea * .noUi-handle::after, #playerArea * .noUi-handle::before { + content: none; +} + +#seekSlider { + height: 6px; + margin-bottom: 10px; + transition-duration: 0.2s; +} + +// mobile overrides +@media(max-width:767px) { + #seekSlider { + height: 8px; + .noUi-handle { + opacity: 1 !important; + } + } + .settingsItem:nth-of-type(2){ + margin-left: 0; + } + #seekSlider { + margin-bottom: 15px; + } + .beatOption { + text-align: center !important; + } +} + +#seekSlider:hover { + height: 8px; +} + +#seekSlider:hover * .noUi-handle { + opacity: 1; +} + +#seekSlider * .noUi-handle { + border: none; + height: 13px; + width: 13px; + border-radius: 50%; + top: -4px; + left: -6px; + opacity: 0; + transition-duration: 0.1s; + background-color: $secondaryThemeColor !important; + box-shadow: none; +} + +#playListControls { + min-height: 30px; + margin-top: 5px; + border-bottom: 1px solid #3a3a3a; + position: relative; +} + +#playListArea, #playAreaMic { + background-color: white; + width: 100%; + height: 350px; + margin: 10px auto 0 auto; + border-radius: 5px; + transition: 0.1s all ease-in-out; + position: relative; + overflow: auto; + #dragHere { + position: absolute; + top: 27%; + font-size: 20px; + text-align: center; + width: 100%; + } + .library-music, .mic { + position: absolute; + top: 40%; + font-size: 100px; + opacity: 0.5; + width: 100%; + text-align: center; + } +} + +.customNotify { + top: 0; + position: absolute; + right: 20px; + max-width: 400px; + .alert-box { + position: relative; + overflow: hidden; + } +} + +.songArtist { + font-weight: bold; +} + +#playListArea.dragHereHighlight { + background-color: white; + border: 5px dotted #5383ff; +} + +#playListArea.draggingOver { + background-color: darken(white, 5%); + box-shadow: inset 0 0 20px 0 rgba(0, 0, 0, 1); +} + +#fileInput { + width: 1px; + height: 1px; + visibility: hidden; +} + +.playlistItem { + border-bottom: 1px solid rgba(128, 128, 128, 0.3); + border-top: 1px solid rgba(128,128,128,0.3); + height: 62px; + font-family: 'Open Sans', sans-serif; + padding: 0 20px 0 5px; + position: relative; + color: black; + background: darken(white, 5%); + .close { + font-size: 18px; + } + .albumArt { + height: 60px; + float: left; + margin-right: 5px; + border: 1px solid rgba(0, 0, 0, 0.5); + } + .songInfo { + .songTitle { + max-height: 40px; + overflow: hidden; + } + .songArtist { + max-height: 20px; + overflow: hidden; + } + } + .audioRemoveButton { + position: absolute; + top: 20px; + right: 5px; + } +} + +.playlistItem.active { + background: darken(white, 15%) !important; + border-top: 1px solid $secondaryThemeColor; + border-bottom: 1px solid $secondaryThemeColor; +} + +.playlistItem:hover { + background: darken(white, 10%); + .close { + display: block; + } +} + +#beatArea { + height: $playerBottomHeight; + position: relative; + padding: 0; +} + +.noUi-target { + margin: 0 auto; +} + +.noUi-base, .noUi-background { + background-color: #ADADAD; + border: 1px solid #797979; +} + +.noUi-vertical { + height: 200px; + margin-top: 10px; + margin-bottom: 15px; +} + +.star { + cursor: auto !important; +} + +#beatOptionButtonGroup { + margin: 20px 0 10px 0; +} + +.beatOption { + padding: 5px 0; + text-align: center; + md-switch { + margin: 0; + } + .optionDescription { + font-size: 16px; + } + button { + margin-top: 0; + } + .tooltip { + margin: 0; + } +} + +#playerBottom { + color: black; + border: 1px solid black; + width: 100%; + background: white; +} + +#beatContainer { + padding: 0; + height: $playerBottomHeight; +} + +#beatArea .lightGroup { + margin: 10px 20px 0 40px; + float: right; + div { + display: block; + padding: 10px; + } +} + +#playerButtonGroup { + margin-top: 10px; +} + +div.dimmerOn { + color: white !important; + background: #171717 !important; +} + +html.dimmerOn { + color: white; + background: #242424; +} + +body.dimmerOn { + color: white; + background: #242424; + .md-track { + background: white; + } + .color { + border: 1px solid white; + } + .playlistItem { + color: #cdcdcd; + background: $dimmerOnButtonColor; + } + .playlistItem.active { + background: darken($dimmerOnButtonColor, 15%) !important; + } + .playlistItem:hover { + background: darken($dimmerOnButtonColor, 10%); + } + .playlistItem .close { + color: #cdcdcd !important; + } + .playlistItem .close:hover { + color: white !important; + } + svg { + -webkit-filter: drop-shadow(0 0 5px #228DFF); + } + md-input-container { + label { + color: white; + } + input { + border-bottom-color: white !important; + } + input { + color: white !important; + } + } + .addNewMusic { + background: $dimmerOnButtonColor + } + .addNewMusic:hover { + background: darken($dimmerOnButtonColor, 5%); + } + #extraOptionsMenu { + opacity: 0.8; + } + #extraOptionsMenu:hover { + background: rgba(0, 0, 0, 0.9); + } + .popover-content { + color: black !important; + } +} + +.power-settings-new.dimmerOn, +.brightness-4.dimmerOn, +.color-lens.dimmerOn, +.flare.dimmerOn, +.loop.dimmerOn, +.group.dimmerOn, +.settings.dimmerOn, +.mic.dimmerOn, +.star.dimmerOn, +.library-music.dimmerOn { + color: inherit !important; + text-shadow: $glowingText; + opacity: 0.9 !important; +} + +#dimmer { + position: absolute; + left: -50px; + bottom: -10px; + cursor: pointer; + width: 40px; + height: 40px; + background: url(/favicon-96x96.png) center center no-repeat; + background-size: 40px 40px; +} + +.noUi-value-vertical { + margin-top: -10px; +} + +.noUi-value-vertical, .noUi-pips { + color: inherit !important; + font: 400 14px 'Slabo 27px'; +} + +.noUi-vertical .noUi-handle { + border: 1px solid #A3A0A0; + width: 26px; +} + +.noUi-vertical .noUi-handle:after, .noUi-vertical .noUi-handle:before{ + background: grey; +} + +#saveBeatPreferencesStar { + position: absolute; + top: 3px; + z-index: 1000; + md-icon { + color: $secondaryThemeColor !important; + font-size: 25px; + } +} + +button.md-warn { + background: $secondaryThemeColor; +} + +div.ember-modal-dialog { + padding: 20px; + color: black; + md-input-container input { + color: black !important; + } + md-input-container label { + color: rgba(0, 0, 0, 0.26); + } +} + +.addMusicButton { + float: right; + color: white; +} + +#addMusicChoices { + min-width: initial; + right: 0; + left: initial; + width: 100px; + top: 25px; +} + +.addNewMusic { + padding: 0 5px 0 10px; + font-size: 16px; + border-radius: 5px; + background: #f8f8f8; + border: none; +} + +.addNewMusic:hover { + background: darken(#f8f8f8, 5%); +} + +.soundCloudLink { + margin-right: 5px; +} + +#visualization { + position: absolute; + top: 0; + left: 0; +} + +.check { + margin-left: 10px; + position: relative; + top: -4px; +} + +.visualizersMenu { + left: -135px; +} + +.displayIcon { + background: url(/favicon-96x96.png) center center no-repeat; + background-size: 80px 80px; +} + +#artwork { + position: absolute; + width: 100%; + overflow: hidden; + img { + display: block; + margin: 0 auto; + max-height: 400px; + } +} + +.keyboard-arrow-down { + font-size: 20px; +} + +#extraOptionsMenu { + z-index: 1; + background: rgba(0, 0, 0, 0.5); + padding: 3px 15px; + position: absolute; + bottom: -39px; + right: 17px; + border-radius: 0 0 5px 5px; +} + +#extraOptionsMenu:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.8); +} + +.introjs-helperNumberLayer { + line-height: 17px; + padding: 0; + width: 23px; + height: 23px; +} + +#settings.introjs-fixParent{ + position: inherit !important; +} + +.introjs-tooltip { + color: black; +} + +.introjs-skipbutton{ + color: $secondaryThemeColor; +} + +.introjs-bullets ul li a.active { + position: relative; + height: 10px; + width: 10px; + top: -2px; +} diff --git a/app/styles/fancy-speaker.scss b/app/styles/fancy-speaker.scss new file mode 100644 index 0000000..c161d69 --- /dev/null +++ b/app/styles/fancy-speaker.scss @@ -0,0 +1,153 @@ +/* Variables */ +$centersize: 80px; +$center1size: 205px; +$bezelsize: 240px; +$vibratesize: $centersize*1.06; +$vibratemargin:-$vibratesize/2; +$vibrateblurinner: 3px; +$vibrateblurouter: 2px; + +/* Extenders */ +%base { + border-radius: 100%; +} +%rivet { + position: absolute; + height: 8px; + width: 8px; + background-color: #555; + border-radius: 100%; + box-shadow: inset 0 0 3px #000, 0 0 2px #000; +} + +/* Keyframes */ +@keyframes vibrateInner { + 50% { + -webkit-filter: blur($vibrateblurinner); + transform: scale(1.05); + } +} + +@keyframes vibrateOuter { + 0% { + -webkit-filter: blur($vibrateblurouter); + filter: blur($vibrateblurouter); + } + 30% { + -webkit-filter: blur(0); + filter: blur(0); + } + 50% { + -webkit-filter: blur($vibrateblurouter); + filter: blur($vibrateblurouter); + } + 65% { + -webkit-filter: blur(0); + filter: blur(0); + } + 70% { + -webkit-filter: blur($vibrateblurouter); + filter: blur($vibrateblurouter); + } + 80% { + -webkit-filter: blur($vibrateblurouter); + filter: blur($vibrateblurouter); + } + 100% { + -webkit-filter: blur($vibrateblurouter); + filter: blur($vibrateblurouter); + } +} + +#beatSpeakerCenterInner { + @extend %base; + height: $centersize; + width: $centersize; + position: absolute; + bottom: 47px; + right: 47px; + -webkit-filter: blur(1px); + filter: blur(1px); + background: rgb(0,0,0); + background: -moz-radial-gradient(center, ellipse cover, rgba(0,0,0,1) 0%, rgba(79,79,79,1) 0%, rgba(0,0,0,1) 100%); + background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(0,0,0,1)), color-stop(0%,rgba(79,79,79,1)), color-stop(100%,rgba(0,0,0,1))); + background: -webkit-radial-gradient(center, ellipse cover, rgba(0,0,0,1) 0%,rgba(79,79,79,1) 0%,rgba(0,0,0,1) 100%); + background: -o-radial-gradient(center, ellipse cover, rgba(0,0,0,1) 0%,rgba(79,79,79,1) 0%,rgba(0,0,0,1) 100%); + background: -ms-radial-gradient(center, ellipse cover, rgba(0,0,0,1) 0%,rgba(79,79,79,1) 0%,rgba(0,0,0,1) 100%); + background: radial-gradient(ellipse at center, rgba(0,0,0,1) 0%,rgba(79,79,79,1) 0%,rgba(0,0,0,1) 100%); + box-shadow: 0 0 10px rgba(0, 0, 0, 1); +} +.vibrateInner{ + -webkit-animation: vibrateInner 0.15s linear 1; + animation: vibrateInner 0.15s linear 1; +} +#beatSpeakerCenterOuter { + @extend %base; + position: absolute; + top: 16px; + left: 16px; + height: $center1size; + width: $center1size; + border: 15px solid #333; + box-shadow: -3px -3px 15px rgba(0, 0, 0, 0.4), inset -3px -3px 15px rgba(0, 0, 0, 0.5); + background: -moz-linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); + background: -webkit-linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); + background: -o-linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); + background: -ms-linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); + background: linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); +} +.vibrateOuter { + -webkit-animation: vibrateOuter 0.15s linear 1; + animation: vibrateOuter 0.15s linear 1; +} +.bezel { + @extend %base; + margin: 0 auto; + height: $bezelsize; + width: $bezelsize; + position: relative; + background-color: #A8A8A8; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.8), inset 3px 3px 10px rgba(0, 0, 0, 0.8), 0 0 2px rgba(0, 0, 0, 0.8), inset 0 0 30px -5px rgba(0, 0, 0, 0.8); + top: 50%; + transform: translateY(-50%); +} +.rivet1 { + @extend %rivet; + top: 6px; + left: 50%; +} +.rivet2 { + @extend %rivet; + bottom: 6px; + left: 50%; +} +.rivet3 { + @extend %rivet; + top: 50%; + left: 6px; +} +.rivet4 { + @extend %rivet; + top: 50%; + right: 6px; +} +.rivet5 { + @extend %rivet; + top: 18%; + left: 13.7%; +} +.rivet6 { + @extend %rivet; + top: 18%; + right: 13.5%; +} +.rivet7 { + @extend %rivet; + bottom: 17%; + left: 13.5%; +} +.rivet8 { + @extend %rivet; + bottom: 17%; + right: 13.5%; +} diff --git a/app/templates/application.hbs b/app/templates/application.hbs new file mode 100644 index 0000000..d53e9c5 --- /dev/null +++ b/app/templates/application.hbs @@ -0,0 +1 @@ +{{huegasm-app}} \ No newline at end of file diff --git a/assets/icon.psd b/assets/icon.psd new file mode 100644 index 0000000..fef175b Binary files /dev/null and b/assets/icon.psd differ diff --git a/assets/intro.psd b/assets/intro.psd new file mode 100644 index 0000000..4f1d8f8 Binary files /dev/null and b/assets/intro.psd differ diff --git a/assets/missingArtwork.psd b/assets/missingArtwork.psd new file mode 100644 index 0000000..1f06b62 Binary files /dev/null and b/assets/missingArtwork.psd differ diff --git a/assets/pressButtonBridge.psd b/assets/pressButtonBridge.psd new file mode 100644 index 0000000..5dcf20d Binary files /dev/null and b/assets/pressButtonBridge.psd differ diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..aa1f43b --- /dev/null +++ b/bower.json @@ -0,0 +1,19 @@ +{ + "name": "huegasm", + "dependencies": { + "bootstrap-sass": "~3.3.5", + "ember": "~2.4.1", + "ember-cli-shims": "0.1.0", + "ember-cli-test-loader": "0.2.2", + "ember-load-initializers": "0.5.1", + "ember-qunit-notifications": "0.1.0", + "hammerjs": "~2.0.4", + "intro.js": "~1.1.1", + "JavaScript-ID3-Reader": "https://github.com/aadsm/JavaScript-ID3-Reader.git", + "jquery-mousewheel": "~3.1.13", + "locallyjs": "~0.3.2", + "matchMedia": "~0.2.0", + "nouislider": "^8.3.0", + "HackTimer": "https://github.com/turuslan/HackTimer.git#~1.0.0" + } +} diff --git a/config/environment.js b/config/environment.js new file mode 100644 index 0000000..d502f2d --- /dev/null +++ b/config/environment.js @@ -0,0 +1,48 @@ +/* jshint node: true */ + +module.exports = function(environment) { + var ENV = { + modulePrefix: 'huegasm', + podModulePrefix: 'huegasm/pods', + environment: environment, + baseURL: '/', + locationType: 'auto', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. 'with-controller': true + } + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + } + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.baseURL = '/'; + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + } + + if (environment === 'production') { + //ENV.baseURL = '/huegasm'; + } + + return ENV; +}; diff --git a/ember-cli-build.js b/ember-cli-build.js new file mode 100644 index 0000000..8cb6cde --- /dev/null +++ b/ember-cli-build.js @@ -0,0 +1,32 @@ +/* global require, module */ +var EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function(defaults) { + var app = new EmberApp(defaults); + + app.import('vendor/dancer.js'); + app.import('bower_components/bootstrap-sass/assets/javascripts/bootstrap/tooltip.js'); + app.import('bower_components/bootstrap-sass/assets/javascripts/bootstrap/dropdown.js'); + app.import('bower_components/bootstrap-sass/assets/javascripts/bootstrap/popover.js'); + app.import('bower_components/JavaScript-ID3-Reader/dist/id3-minimized.js'); + app.import('bower_components/jquery-mousewheel/jquery.mousewheel.js'); + app.import('bower_components/locallyjs/dist/locally.min.js'); + app.import('bower_components/HackTimer/HackTimer.js'); + app.import('bower_components/intro.js/intro.js'); + app.import('bower_components/intro.js/introjs.css'); + + // Use `app.import` to add additional libraries to the generated + // output files. + // + // If you need to use different assets in different + // environments, specify an object as the first parameter. That + // object's keys should be the environment name and the values + // should be the asset to use in that environment. + // + // If the library that you are including contains AMD or ES6 + // modules that you would like to import into your application + // please specify an object with the list of modules as keys + // along with the exports of each module as its value. + + return app.toTree(); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ae4d006 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "huegasm", + "version": "1.0.0", + "description": "Small description for huegasm goes here", + "private": true, + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "start": "ember server", + "build": "ember build", + "test": "ember test" + }, + "repository": "", + "engines": { + "node": ">= 0.10.0" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "broccoli-asset-rev": "^2.2.0", + "ember-ajax": "0.7.1", + "ember-cli": "^2.4.2", + "ember-cli-app-version": "^1.0.0", + "ember-cli-babel": "^5.1.5", + "ember-cli-dependency-checker": "^1.2.0", + "ember-cli-htmlbars": "^1.0.1", + "ember-cli-htmlbars-inline-precompile": "^0.3.1", + "ember-cli-inject-live-reload": "^1.3.1", + "ember-cli-nouislider": "0.7.0", + "ember-cli-qunit": "^1.2.1", + "ember-cli-release": "0.2.8", + "ember-cli-sass": "5.2.1", + "ember-cli-sri": "^2.1.0", + "ember-cli-uglify": "^1.2.0", + "ember-cli-windows-addon": "^1.2.2", + "ember-data": "^2.4.0", + "ember-disable-proxy-controllers": "^1.0.1", + "ember-export-application-global": "^1.0.4", + "ember-load-initializers": "^0.5.0", + "ember-modal-dialog": "0.8.3", + "ember-notify": "^5.0.2", + "ember-paper": "^0.2.12", + "ember-resolver": "^2.0.3", + "ember-truth-helpers": "1.2.0", + "loader.js": "^4.0.0" + } +} diff --git a/public/android-chrome-144x144.png b/public/android-chrome-144x144.png new file mode 100644 index 0000000..ab446d1 Binary files /dev/null and b/public/android-chrome-144x144.png differ diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000..2e52768 Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-36x36.png b/public/android-chrome-36x36.png new file mode 100644 index 0000000..630f243 Binary files /dev/null and b/public/android-chrome-36x36.png differ diff --git a/public/android-chrome-48x48.png b/public/android-chrome-48x48.png new file mode 100644 index 0000000..b6ab65a Binary files /dev/null and b/public/android-chrome-48x48.png differ diff --git a/public/android-chrome-72x72.png b/public/android-chrome-72x72.png new file mode 100644 index 0000000..bdd5349 Binary files /dev/null and b/public/android-chrome-72x72.png differ diff --git a/public/android-chrome-96x96.png b/public/android-chrome-96x96.png new file mode 100644 index 0000000..83fdad4 Binary files /dev/null and b/public/android-chrome-96x96.png differ diff --git a/public/apple-touch-icon-114x114.png b/public/apple-touch-icon-114x114.png new file mode 100644 index 0000000..aecee12 Binary files /dev/null and b/public/apple-touch-icon-114x114.png differ diff --git a/public/apple-touch-icon-120x120.png b/public/apple-touch-icon-120x120.png new file mode 100644 index 0000000..af6dff0 Binary files /dev/null and b/public/apple-touch-icon-120x120.png differ diff --git a/public/apple-touch-icon-144x144.png b/public/apple-touch-icon-144x144.png new file mode 100644 index 0000000..ffe60f1 Binary files /dev/null and b/public/apple-touch-icon-144x144.png differ diff --git a/public/apple-touch-icon-152x152.png b/public/apple-touch-icon-152x152.png new file mode 100644 index 0000000..19462d7 Binary files /dev/null and b/public/apple-touch-icon-152x152.png differ diff --git a/public/apple-touch-icon-180x180.png b/public/apple-touch-icon-180x180.png new file mode 100644 index 0000000..4b2e681 Binary files /dev/null and b/public/apple-touch-icon-180x180.png differ diff --git a/public/apple-touch-icon-57x57.png b/public/apple-touch-icon-57x57.png new file mode 100644 index 0000000..6deb761 Binary files /dev/null and b/public/apple-touch-icon-57x57.png differ diff --git a/public/apple-touch-icon-60x60.png b/public/apple-touch-icon-60x60.png new file mode 100644 index 0000000..60034fe Binary files /dev/null and b/public/apple-touch-icon-60x60.png differ diff --git a/public/apple-touch-icon-72x72.png b/public/apple-touch-icon-72x72.png new file mode 100644 index 0000000..320db5f Binary files /dev/null and b/public/apple-touch-icon-72x72.png differ diff --git a/public/apple-touch-icon-76x76.png b/public/apple-touch-icon-76x76.png new file mode 100644 index 0000000..ac7ae14 Binary files /dev/null and b/public/apple-touch-icon-76x76.png differ diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..6704c8e Binary files /dev/null and b/public/apple-touch-icon-precomposed.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..4b2e681 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/assets/images/colormap.png b/public/assets/images/colormap.png new file mode 100644 index 0000000..53c80ef Binary files /dev/null and b/public/assets/images/colormap.png differ diff --git a/public/assets/images/lights/a19.svg b/public/assets/images/lights/a19.svg new file mode 100644 index 0000000..cf3eee3 --- /dev/null +++ b/public/assets/images/lights/a19.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/a19w.svg b/public/assets/images/lights/a19w.svg new file mode 100644 index 0000000..b05e873 --- /dev/null +++ b/public/assets/images/lights/a19w.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/br30.svg b/public/assets/images/lights/br30.svg new file mode 100644 index 0000000..7d71208 --- /dev/null +++ b/public/assets/images/lights/br30.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/br30w.svg b/public/assets/images/lights/br30w.svg new file mode 100644 index 0000000..ba1585a --- /dev/null +++ b/public/assets/images/lights/br30w.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/gu10.svg b/public/assets/images/lights/gu10.svg new file mode 100644 index 0000000..014d2f5 --- /dev/null +++ b/public/assets/images/lights/gu10.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/public/assets/images/lights/gu10w.svg b/public/assets/images/lights/gu10w.svg new file mode 100644 index 0000000..1ea01cb --- /dev/null +++ b/public/assets/images/lights/gu10w.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/public/assets/images/lights/huego.svg b/public/assets/images/lights/huego.svg new file mode 100644 index 0000000..e912e5a --- /dev/null +++ b/public/assets/images/lights/huego.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/huegow.svg b/public/assets/images/lights/huegow.svg new file mode 100644 index 0000000..b3b0b88 --- /dev/null +++ b/public/assets/images/lights/huegow.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/lc_aura.svg b/public/assets/images/lights/lc_aura.svg new file mode 100644 index 0000000..9faf3b9 --- /dev/null +++ b/public/assets/images/lights/lc_aura.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/lc_auraw.svg b/public/assets/images/lights/lc_auraw.svg new file mode 100644 index 0000000..c125947 --- /dev/null +++ b/public/assets/images/lights/lc_auraw.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/lc_bloom.svg b/public/assets/images/lights/lc_bloom.svg new file mode 100644 index 0000000..a498b9a --- /dev/null +++ b/public/assets/images/lights/lc_bloom.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/lc_bloomw.svg b/public/assets/images/lights/lc_bloomw.svg new file mode 100644 index 0000000..3dce9f7 --- /dev/null +++ b/public/assets/images/lights/lc_bloomw.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/lc_iris.svg b/public/assets/images/lights/lc_iris.svg new file mode 100644 index 0000000..876b3d2 --- /dev/null +++ b/public/assets/images/lights/lc_iris.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/lc_irisw.svg b/public/assets/images/lights/lc_irisw.svg new file mode 100644 index 0000000..cdf472b --- /dev/null +++ b/public/assets/images/lights/lc_irisw.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/lightstrip.svg b/public/assets/images/lights/lightstrip.svg new file mode 100644 index 0000000..1d55b8e --- /dev/null +++ b/public/assets/images/lights/lightstrip.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/lightstripw.svg b/public/assets/images/lights/lightstripw.svg new file mode 100644 index 0000000..fb32dab --- /dev/null +++ b/public/assets/images/lights/lightstripw.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/storylight.svg b/public/assets/images/lights/storylight.svg new file mode 100644 index 0000000..d04a5db --- /dev/null +++ b/public/assets/images/lights/storylight.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/lights/storylightw.svg b/public/assets/images/lights/storylightw.svg new file mode 100644 index 0000000..8bc66a5 --- /dev/null +++ b/public/assets/images/lights/storylightw.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/images/logo.png b/public/assets/images/logo.png new file mode 100644 index 0000000..e384e77 Binary files /dev/null and b/public/assets/images/logo.png differ diff --git a/public/assets/images/missingArtwork.png b/public/assets/images/missingArtwork.png new file mode 100644 index 0000000..3cf2548 Binary files /dev/null and b/public/assets/images/missingArtwork.png differ diff --git a/public/assets/images/pressButtonBridge.png b/public/assets/images/pressButtonBridge.png new file mode 100644 index 0000000..e98ac23 Binary files /dev/null and b/public/assets/images/pressButtonBridge.png differ diff --git a/public/assets/images/sc-white-sm.png b/public/assets/images/sc-white-sm.png new file mode 100644 index 0000000..4c009dc Binary files /dev/null and b/public/assets/images/sc-white-sm.png differ diff --git a/public/assets/images/sc-white.png b/public/assets/images/sc-white.png new file mode 100644 index 0000000..e48ae8f Binary files /dev/null and b/public/assets/images/sc-white.png differ diff --git a/public/browserconfig.xml b/public/browserconfig.xml new file mode 100644 index 0000000..fe44cae --- /dev/null +++ b/public/browserconfig.xml @@ -0,0 +1,12 @@ + + + + + + + + + #da532c + + + diff --git a/public/crossdomain.xml b/public/crossdomain.xml new file mode 100644 index 0000000..0c16a7a --- /dev/null +++ b/public/crossdomain.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000..179d72a Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-194x194.png b/public/favicon-194x194.png new file mode 100644 index 0000000..e0332f4 Binary files /dev/null and b/public/favicon-194x194.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..71debba Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png new file mode 100644 index 0000000..6490342 Binary files /dev/null and b/public/favicon-96x96.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..85a103c Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/humans.txt b/public/humans.txt new file mode 100644 index 0000000..1408b60 --- /dev/null +++ b/public/humans.txt @@ -0,0 +1,13 @@ +/* TEAM */ +Your title: Egor Philippov +Site: https://www.linkedin.com/pub/egor-philippov/7b/220/148 +Location: Vancouver, Canada. + +/* THANKS */ +Edmond Cheung - favicons + huegasm logo +Liviu Antonescu - filming + video editing of the intro + +/* SITE */ +Last update: 2015 +Standards: HTML5, CSS3 +Components: ember, jQuery, bootstrap, font-awesome, intro.js, locallyjs, nouislider, dancer.js, jquery-mousewheel, ember paper, ember notify, JavaScript-ID3-Reader diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..49b4409 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "Huegasm", + "icons": [ + { + "src": "\/android-chrome-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-chrome-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-chrome-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-chrome-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-chrome-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} diff --git a/public/mstile-144x144.png b/public/mstile-144x144.png new file mode 100644 index 0000000..0f2b0d1 Binary files /dev/null and b/public/mstile-144x144.png differ diff --git a/public/mstile-150x150.png b/public/mstile-150x150.png new file mode 100644 index 0000000..cbf7529 Binary files /dev/null and b/public/mstile-150x150.png differ diff --git a/public/mstile-310x150.png b/public/mstile-310x150.png new file mode 100644 index 0000000..0b3d505 Binary files /dev/null and b/public/mstile-310x150.png differ diff --git a/public/mstile-310x310.png b/public/mstile-310x310.png new file mode 100644 index 0000000..08cef23 Binary files /dev/null and b/public/mstile-310x310.png differ diff --git a/public/mstile-70x70.png b/public/mstile-70x70.png new file mode 100644 index 0000000..049a8c6 Binary files /dev/null and b/public/mstile-70x70.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..f591645 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# http://www.robotstxt.org +User-agent: * +Disallow: diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg new file mode 100644 index 0000000..bb82950 --- /dev/null +++ b/public/safari-pinned-tab.svg @@ -0,0 +1,44 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/testem.js b/testem.js new file mode 100644 index 0000000..26044b2 --- /dev/null +++ b/testem.js @@ -0,0 +1,13 @@ +/*jshint node:true*/ +module.exports = { + "framework": "qunit", + "test_page": "tests/index.html?hidepassed", + "disable_watching": true, + "launch_in_ci": [ + "PhantomJS" + ], + "launch_in_dev": [ + "PhantomJS", + "Chrome" + ] +}; diff --git a/tests/.jshintrc b/tests/.jshintrc new file mode 100644 index 0000000..6ec0b7c --- /dev/null +++ b/tests/.jshintrc @@ -0,0 +1,52 @@ +{ + "predef": [ + "document", + "window", + "location", + "setTimeout", + "$", + "-Promise", + "define", + "console", + "visit", + "exists", + "fillIn", + "click", + "keyEvent", + "triggerEvent", + "find", + "findWithAssert", + "wait", + "DS", + "andThen", + "currentURL", + "currentPath", + "currentRouteName" + ], + "node": false, + "browser": false, + "boss": true, + "curly": true, + "debug": false, + "devel": false, + "eqeqeq": true, + "evil": true, + "forin": false, + "immed": false, + "laxbreak": false, + "newcap": true, + "noarg": true, + "noempty": false, + "nonew": false, + "nomen": false, + "onevar": false, + "plusplus": false, + "regexp": false, + "undef": true, + "sub": true, + "strict": false, + "white": false, + "eqnull": true, + "esnext": true, + "unused": true +} diff --git a/tests/helpers/destroy-app.js b/tests/helpers/destroy-app.js new file mode 100644 index 0000000..c3d4d1a --- /dev/null +++ b/tests/helpers/destroy-app.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +export default function destroyApp(application) { + Ember.run(application, 'destroy'); +} diff --git a/tests/helpers/module-for-acceptance.js b/tests/helpers/module-for-acceptance.js new file mode 100644 index 0000000..8c8b74e --- /dev/null +++ b/tests/helpers/module-for-acceptance.js @@ -0,0 +1,23 @@ +import { module } from 'qunit'; +import startApp from '../helpers/start-app'; +import destroyApp from '../helpers/destroy-app'; + +export default function(name, options = {}) { + module(name, { + beforeEach() { + this.application = startApp(); + + if (options.beforeEach) { + options.beforeEach.apply(this, arguments); + } + }, + + afterEach() { + if (options.afterEach) { + options.afterEach.apply(this, arguments); + } + + destroyApp(this.application); + } + }); +} diff --git a/tests/helpers/resolver.js b/tests/helpers/resolver.js new file mode 100644 index 0000000..b208d38 --- /dev/null +++ b/tests/helpers/resolver.js @@ -0,0 +1,11 @@ +import Resolver from '../../resolver'; +import config from '../../config/environment'; + +const resolver = Resolver.create(); + +resolver.namespace = { + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix +}; + +export default resolver; diff --git a/tests/helpers/start-app.js b/tests/helpers/start-app.js new file mode 100644 index 0000000..e098f1d --- /dev/null +++ b/tests/helpers/start-app.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import Application from '../../app'; +import config from '../../config/environment'; + +export default function startApp(attrs) { + let application; + + let attributes = Ember.merge({}, config.APP); + attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; + + Ember.run(() => { + application = Application.create(attributes); + application.setupForTesting(); + application.injectTestHelpers(); + }); + + return application; +} diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 0000000..0cdd486 --- /dev/null +++ b/tests/index.html @@ -0,0 +1,34 @@ + + + + + + Huegasm Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + + + + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + diff --git a/tests/integration/pods/components/add-group-modal/component-test.js b/tests/integration/pods/components/add-group-modal/component-test.js new file mode 100644 index 0000000..43767d1 --- /dev/null +++ b/tests/integration/pods/components/add-group-modal/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('add-group-modal', 'Integration | Component | add group modal', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{add-group-modal}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#add-group-modal}} + template block text + {{/add-group-modal}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/add-soundcloud-sound-modal/component-test.js b/tests/integration/pods/components/add-soundcloud-sound-modal/component-test.js new file mode 100644 index 0000000..2c2d873 --- /dev/null +++ b/tests/integration/pods/components/add-soundcloud-sound-modal/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('add-soundcloud-sound-modal', 'Integration | Component | add soundcloud sound modal', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{add-soundcloud-sound-modal}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#add-soundcloud-sound-modal}} + template block text + {{/add-soundcloud-sound-modal}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/bridge-finder/component-test.js b/tests/integration/pods/components/bridge-finder/component-test.js new file mode 100644 index 0000000..c42803d --- /dev/null +++ b/tests/integration/pods/components/bridge-finder/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('bridge-finder', 'Integration | Component | bridge finder', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{bridge-finder}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#bridge-finder}} + template block text + {{/bridge-finder}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/color-picker/component-test.js b/tests/integration/pods/components/color-picker/component-test.js new file mode 100644 index 0000000..fb1d925 --- /dev/null +++ b/tests/integration/pods/components/color-picker/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('color-picker', 'Integration | Component | color picker', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{color-picker}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#color-picker}} + template block text + {{/color-picker}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/delete-group-modal/component-test.js b/tests/integration/pods/components/delete-group-modal/component-test.js new file mode 100644 index 0000000..0555ede --- /dev/null +++ b/tests/integration/pods/components/delete-group-modal/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('delete-group-modal', 'Integration | Component | delete group modal', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{delete-group-modal}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#delete-group-modal}} + template block text + {{/delete-group-modal}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/groups-list/component-test.js b/tests/integration/pods/components/groups-list/component-test.js new file mode 100644 index 0000000..aa4dcff --- /dev/null +++ b/tests/integration/pods/components/groups-list/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('groups-list', 'Integration | Component | groups list', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{groups-list}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#groups-list}} + template block text + {{/groups-list}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/hue-controls/component-test.js b/tests/integration/pods/components/hue-controls/component-test.js new file mode 100644 index 0000000..1d1ebc2 --- /dev/null +++ b/tests/integration/pods/components/hue-controls/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('hue-controls', 'Integration | Component | hue controls', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{hue-controls}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#hue-controls}} + template block text + {{/hue-controls}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/huegasm-app/component-test.js b/tests/integration/pods/components/huegasm-app/component-test.js new file mode 100644 index 0000000..53484b6 --- /dev/null +++ b/tests/integration/pods/components/huegasm-app/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('huegasm-app', 'Integration | Component | huegasm app', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{huegasm-app}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#huegasm-app}} + template block text + {{/huegasm-app}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/light-group/component-test.js b/tests/integration/pods/components/light-group/component-test.js new file mode 100644 index 0000000..22a66cd --- /dev/null +++ b/tests/integration/pods/components/light-group/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('light-group', 'Integration | Component | light group', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{light-group}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#light-group}} + template block text + {{/light-group}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/lights-tab/component-test.js b/tests/integration/pods/components/lights-tab/component-test.js new file mode 100644 index 0000000..d7af880 --- /dev/null +++ b/tests/integration/pods/components/lights-tab/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('lights-tab', 'Integration | Component | lights tab', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{lights-tab}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#lights-tab}} + template block text + {{/lights-tab}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/integration/pods/components/music-tab/component-test.js b/tests/integration/pods/components/music-tab/component-test.js new file mode 100644 index 0000000..d9636e1 --- /dev/null +++ b/tests/integration/pods/components/music-tab/component-test.js @@ -0,0 +1,26 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('music-tab', 'Integration | Component | music tab', { + integration: true +}); + +test('it renders', function(assert) { + assert.expect(2); + + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + + this.render(hbs`{{music-tab}}`); + + assert.equal(this.$().text().trim(), ''); + + // Template block usage: + this.render(hbs` + {{#music-tab}} + template block text + {{/music-tab}} + `); + + assert.equal(this.$().text().trim(), 'template block text'); +}); diff --git a/tests/test-helper.js b/tests/test-helper.js new file mode 100644 index 0000000..e6cfb70 --- /dev/null +++ b/tests/test-helper.js @@ -0,0 +1,6 @@ +import resolver from './helpers/resolver'; +import { + setResolver +} from 'ember-qunit'; + +setResolver(resolver); diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/dancer.js b/vendor/dancer.js new file mode 100644 index 0000000..b928621 --- /dev/null +++ b/vendor/dancer.js @@ -0,0 +1,703 @@ +/* + * dancer - v0.4.0 - 2014-02-01 + * https://github.com/jsantell/dancer.js + * Copyright (c) 2014 Jordan Santell + * Licensed MIT + */ +(function() { + + var Dancer = function () { + this.audioAdapter = Dancer._getAdapter( this ); + this.events = {}; + this.sections = []; + this.bind( 'update', update ); + }; + + Dancer.version = 'X.X.X'; + Dancer.adapters = {}; + + Dancer.prototype = { + + load : function ( source, micBoost, useMic ) { + // Loading an Audio element + if ( source instanceof HTMLElement ) { + this.source = source; + // Loading an object with src, [codecs] + } else if(source instanceof EventTarget){ + this.source = source; + } else { + this.source = window.Audio ? new Audio() : {}; + this.source.src = Dancer._makeSupportedPath( source.src, source.codecs ); + } + + this.useMic = useMic === true; + this.boost = micBoost ? micBoost : 1; + this.audio = this.audioAdapter.load(this.source, this.useMic, this.boost); + + return this; + }, + + /* Controls */ + play : function () { + this.audioAdapter.play(); + return this; + }, + + pause : function () { + this.audioAdapter.pause(); + return this; + }, + + setVolume : function ( volume ) { + this.audioAdapter.setVolume( volume ); + return this; + }, + + setBoost : function ( boost ) { + this.audioAdapter.setBoost( boost ); + return this; + }, + + /* Actions */ + createKick : function ( options ) { + return new Dancer.Kick( this, options ); + }, + + bind : function ( name, callback ) { + if ( !this.events[ name ] ) { + this.events[ name ] = []; + } + this.events[ name ].push( callback ); + return this; + }, + + unbind : function ( name ) { + if ( this.events[ name ] ) { + delete this.events[ name ]; + } + return this; + }, + + trigger : function ( name ) { + var _this = this; + if ( this.events[ name ] ) { + this.events[ name ].forEach(function( callback ) { + callback.call( _this ); + }); + } + return this; + }, + + + /* Getters */ + + getVolume : function () { + return this.audioAdapter.getVolume(); + }, + + getProgress : function () { + return this.audioAdapter.getProgress(); + }, + + getTime : function () { + return this.audioAdapter.getTime(); + }, + + // Returns the magnitude of a frequency or average over a range of frequencies + getFrequency : function ( freq, endFreq ) { + var sum = 0; + if ( endFreq !== undefined ) { + for ( var i = freq; i <= endFreq; i++ ) { + sum += this.getSpectrum()[ i ]; + } + return sum / ( endFreq - freq + 1 ); + } else { + return this.getSpectrum()[ freq ]; + } + }, + + getWaveform : function () { + return this.audioAdapter.getWaveform(); + }, + + getSpectrum : function () { + return this.audioAdapter.getSpectrum(); + }, + + isLoaded : function () { + return this.audioAdapter.isLoaded; + }, + + isPlaying : function () { + return this.audioAdapter.isPlaying; + }, + + + /* Sections */ + + after : function ( time, callback ) { + var _this = this; + this.sections.push({ + condition : function () { + return _this.getTime() > time; + }, + callback : callback + }); + return this; + }, + + before : function ( time, callback ) { + var _this = this; + this.sections.push({ + condition : function () { + return _this.getTime() < time; + }, + callback : callback + }); + return this; + }, + + between : function ( startTime, endTime, callback ) { + var _this = this; + this.sections.push({ + condition : function () { + return _this.getTime() > startTime && _this.getTime() < endTime; + }, + callback : callback + }); + return this; + }, + + onceAt : function ( time, callback ) { + var + _this = this, + thisSection = null; + this.sections.push({ + condition : function () { + return _this.getTime() > time && !this.called; + }, + callback : function () { + callback.call( this ); + thisSection.called = true; + }, + called : false + }); + // Baking the section in the closure due to callback's this being the dancer instance + thisSection = this.sections[ this.sections.length - 1 ]; + return this; + } + }; + + function update () { + for (var i in this.sections) { + if (this.sections[i].condition && this.sections[i].condition() ) + this.sections[i].callback.call(this); + } + } + + window.Dancer = Dancer; +})(); + +(function ( Dancer ) { + + var CODECS = { + 'mp3' : 'audio/mpeg;', + 'ogg' : 'audio/ogg; codecs="vorbis"', + 'wav' : 'audio/wav; codecs="1"', + 'aac' : 'audio/mp4; codecs="mp4a.40.2"' + }, + audioEl = document.createElement( 'audio' ); + + Dancer.options = {}; + + Dancer.setOptions = function ( o ) { + for ( var option in o ) { + if ( o.hasOwnProperty( option ) ) { + Dancer.options[ option ] = o[ option ]; + } + } + }; + + Dancer.isSupported = function () { + if ( !window.Float32Array || !window.Uint32Array ) { + return null; + } else if ( !isUnsupportedSafari() && ( window.AudioContext || window.webkitAudioContext )) { + return 'webaudio'; + } else { + return ''; + } + }; + + Dancer.canPlay = function ( type ) { + var canPlay = audioEl.canPlayType; + return !!( + type.toLowerCase() === 'mp3' || + audioEl.canPlayType && + audioEl.canPlayType( CODECS[ type.toLowerCase() ] ).replace( /no/, '')); + }; + + Dancer.addPlugin = function ( name, fn ) { + if ( Dancer.prototype[ name ] === undefined ) { + Dancer.prototype[ name ] = fn; + } + }; + + Dancer._makeSupportedPath = function ( source, codecs ) { + if ( !codecs ) { return source; } + + for ( var i = 0; i < codecs.length; i++ ) { + if ( Dancer.canPlay( codecs[ i ] ) ) { + return source + '.' + codecs[ i ]; + } + } + return source; + }; + + Dancer._getAdapter = function ( instance ) { + switch ( Dancer.isSupported() ) { + case 'webaudio': + return new Dancer.adapters.webaudio( instance ); + default: + return null; + } + }; + + Dancer._getMP3SrcFromAudio = function ( audioEl ) { + var sources = audioEl.children; + if ( audioEl.src ) { return audioEl.src; } + for ( var i = sources.length; i--; ) { + if (( sources[ i ].type || '' ).match( /audio\/mpeg/ )) return sources[ i ].src; + } + return null; + }; + + // Browser detection is lame, but Safari 6 has Web Audio API, + // but does not support processing audio from a Media Element Source + // https://gist.github.com/3265344 + function isUnsupportedSafari () { + var + isApple = !!( navigator.vendor || '' ).match( /Apple/ ), + version = navigator.userAgent.match( /Version\/([^ ]*)/ ); + version = version ? parseFloat( version[ 1 ] ) : 0; + return isApple && version <= 6; + } + +})( window.Dancer ); + +(function ( undefined ) { + var Kick = function ( dancer, o ) { + o = o || {}; + this.dancer = dancer; + this.frequency = o.frequency !== undefined ? o.frequency : [ 0, 5 ]; + this.threshold = o.threshold !== undefined ? o.threshold : 0.3; + this.decay = o.decay !== undefined ? o.decay : 0.02; + this.onKick = o.onKick; + this.offKick = o.offKick; + this.isOn = false; + this.currentThreshold = this.threshold; + this.previousMag = 0; + this.canUseRatio = true; + this.canUseRatioHandle = null; + + var _this = this; + this.dancer.bind( 'update', function () { + _this.onUpdate(); + }); + }; + + Kick.prototype = { + on : function () { + this.isOn = true; + return this; + }, + off : function () { + this.isOn = false; + return this; + }, + + set : function ( o ) { + o = o || {}; + this.frequency = o.frequency !== undefined ? o.frequency : this.frequency; + this.threshold = o.threshold !== undefined ? o.threshold : this.threshold; + this.decay = o.decay !== undefined ? o.decay : this.decay; + this.onKick = o.onKick || this.onKick; + this.offKick = o.offKick || this.offKick; + }, + + onUpdate : function () { + if ( !this.isOn ) { return; } + + var magnitude = this.maxAmplitude(this.frequency); + + if (magnitude >= this.currentThreshold && magnitude >= this.threshold) { + this.currentThreshold = magnitude; + this.onKick && this.onKick.call(this.dancer, magnitude); + this.canUseRatio = false; + + if(this.canUseRatioHandle) { + clearTimeout(this.canUseRatioHandle); + this.canUseRatioHandle = null; + } + + var self = this; + this.canUseRatioHandle = setTimeout(function(){ + self.canUseRatio = true; + }, 5000); + } else { + if(magnitude/this.previousMag > this.threshold*5 && magnitude>0.1 && this.canUseRatio) { + this.onKick && this.onKick.call(this.dancer, magnitude, magnitude/this.previousMag); + } else { + this.offKick && this.offKick.call(this.dancer, magnitude); + } + + this.currentThreshold -= this.decay; + this.previousMag = (magnitude > 0) ? magnitude : 0.0001; + } + }, + maxAmplitude : function ( frequency ) { + var max = 0, fft = this.dancer.getSpectrum(); + + // Sloppy array check + if ( !frequency.length ) { + return frequency < fft.length ? + fft[ ~~frequency ] : + null; + } + + for ( var i = frequency[ 0 ], l = frequency[ 1 ]; i <= l; i++ ) { + if ( fft[ i ] > max ) { max = fft[ i ]; } + } + return max; + } + }; + + window.Dancer.Kick = Kick; +})(); + +(function() { + var + SAMPLE_SIZE = 2048, + SAMPLE_RATE = 44100; + + var adapter = function ( dancer ) { + var context = new AudioContext(); + + this.dancer = dancer; + this.audio = new Audio(); + this.context = context; + }; + + adapter.prototype = { + + load : function (_source, useMic, boost) { + var _this = this; + this.audio = _source; + this.useMic = useMic; + this.boost = boost; + + this.isLoaded = false; + this.progress = 0; + + if(this.proc){ + this.proc.onaudioprocess = null; + delete this.proc; + } + + this.proc = this.context.createScriptProcessor( SAMPLE_SIZE / 2, 1, 1 ); + + this.proc.onaudioprocess = function ( e ) { + _this.update.call( _this, e ); + }; + + this.gain = this.context.createGain(); + + this.fft = new FFT( SAMPLE_SIZE / 2, SAMPLE_RATE, this.boost ); + this.signal = new Float32Array( SAMPLE_SIZE / 2 ); + + if ( this.audio.readyState < 3 ) { + this.audio.addEventListener( 'canplay', function () { + connectContext.call( _this ); + }); + } else { + connectContext.call( _this ); + } + + this.audio.addEventListener( 'progress', function ( e ) { + if ( e.currentTarget.duration && e.currentTarget.duration !== Infinity ) { + _this.progress = e.currentTarget.seekable.end( 0 ) / e.currentTarget.duration; + } + }); + + return this.audio; + }, + + play : function () { + this.audio.play(); + this.isPlaying = true; + }, + + pause : function () { + this.audio.pause(); + this.isPlaying = false; + }, + + setVolume : function ( volume ) { + this.gain.gain.value = volume; + }, + + setBoost : function( boost ){ + if(this.fft){ + this.fft.setBoost(boost); + } + + this.boost = boost; + }, + + getVolume : function () { + return this.gain.gain.value; + }, + + getProgress : function() { + return this.progress; + }, + + getWaveform : function () { + return this.signal; + }, + + getSpectrum : function () { + return this.fft.spectrum; + }, + + getTime : function () { + return this.audio.currentTime; + }, + + update : function ( e ) { + if ((!this.isPlaying || !this.isLoaded) && this.useMic !== true ) return; + + var + buffers = [], + channels = e.inputBuffer.numberOfChannels, + resolution = SAMPLE_SIZE / channels, + sum = function ( prev, curr ) { + return prev[ i ] + curr[ i ]; + }, i; + + for ( i = channels; i--; ) { + buffers.push( e.inputBuffer.getChannelData( i ) ); + } + + for ( i = 0; i < resolution; i++ ) { + this.signal[ i ] = channels > 1 ? + buffers.reduce( sum ) / channels : + buffers[ 0 ][ i ]; + } + + this.fft.forward( this.signal ); + this.dancer.trigger( 'update' ); + } + }; + + function connectContext () { + try { + if(this.useMic){ + this.source = this.context.createMediaStreamSource(this.audio); + } else { + this.source = this.context.createMediaElementSource(this.audio); + } + } catch (err) { + console.info('Dancer: '+ err); + return; + } + + this.source.connect(this.proc); + this.source.connect(this.gain); + this.gain.connect(this.context.destination); + this.proc.connect(this.context.destination); + + this.isLoaded = true; + this.progress = 1; + this.dancer.trigger( 'loaded' ); + } + + Dancer.adapters.webaudio = adapter; + +})(); + + +/* + * DSP.js - a comprehensive digital signal processing library for javascript + * + * Created by Corban Brook on 2010-01-01. + * Copyright 2010 Corban Brook. All rights reserved. + * + */ + +// Fourier Transform Module used by DFT, FFT, RFFT +function FourierTransform(bufferSize, sampleRate, boost) { + this.bufferSize = bufferSize; + this.sampleRate = sampleRate; + this.bandwidth = 2 / bufferSize * sampleRate / 2; + this.boost = boost ? boost : 1; + + this.spectrum = new Float32Array(bufferSize/2); + this.real = new Float32Array(bufferSize); + this.imag = new Float32Array(bufferSize); + + this.peakBand = 0; + this.peak = 0; + + /** + * Calculates the *middle* frequency of an FFT band. + * + * @param {Number} index The index of the FFT band. + * + * @returns The middle frequency in Hz. + */ + this.getBandFrequency = function(index) { + return this.bandwidth * index + this.bandwidth / 2; + }; + + this.setBoost = function(boost){ + this.boost = boost; + }; + + this.calculateSpectrum = function() { + var spectrum = this.spectrum, + real = this.real, + imag = this.imag, + boost = this.boost, + bSi = 2 / this.bufferSize, + sqrt = Math.sqrt, + rval, + ival, + mag; + + for (var i = 0, N = bufferSize/2; i < N; i++) { + rval = real[i]; + ival = imag[i]; + mag = bSi * sqrt(rval * rval + ival * ival); + + if (mag > this.peak) { + this.peakBand = i; + this.peak = mag; + } + + spectrum[i] = mag * boost; + } + }; +} + +/** + * FFT is a class for calculating the Discrete Fourier Transform of a signal + * with the Fast Fourier Transform algorithm. + * + * @param {Number} bufferSize The size of the sample buffer to be computed. Must be power of 2 + * @param {Number} sampleRate The sampleRate of the buffer (eg. 44100) + * @param {Number} boost The coefficient + * + * @constructor + */ +function FFT(bufferSize, sampleRate, boost) { + FourierTransform.call(this, bufferSize, sampleRate, boost); + + this.reverseTable = new Uint32Array(bufferSize); + + var limit = 1; + var bit = bufferSize >> 1; + + var i; + + while (limit < bufferSize) { + for (i = 0; i < limit; i++) { + this.reverseTable[i + limit] = this.reverseTable[i] + bit; + } + + limit = limit << 1; + bit = bit >> 1; + } + + this.sinTable = new Float32Array(bufferSize); + this.cosTable = new Float32Array(bufferSize); + + for (i = 0; i < bufferSize; i++) { + this.sinTable[i] = Math.sin(-Math.PI/i); + this.cosTable[i] = Math.cos(-Math.PI/i); + } +} + +/** + * Performs a forward transform on the sample buffer. + * Converts a time domain signal to frequency domain spectra. + * + * @param {Array} buffer The sample buffer. Buffer Length must be power of 2 + * + * @returns The frequency spectrum array + */ +FFT.prototype.forward = function(buffer) { + // Locally scope variables for speed up + var bufferSize = this.bufferSize, + cosTable = this.cosTable, + sinTable = this.sinTable, + reverseTable = this.reverseTable, + real = this.real, + imag = this.imag, + spectrum = this.spectrum; + + var k = Math.floor(Math.log(bufferSize) / Math.LN2); + + if (Math.pow(2, k) !== bufferSize) { throw "Invalid buffer size, must be a power of 2."; } + if (bufferSize !== buffer.length) { throw "Supplied buffer is not the same size as defined FFT. FFT Size: " + bufferSize + " Buffer Size: " + buffer.length; } + + var halfSize = 1, + phaseShiftStepReal, + phaseShiftStepImag, + currentPhaseShiftReal, + currentPhaseShiftImag, + off, + tr, + ti, + tmpReal, + i; + + for (i = 0; i < bufferSize; i++) { + real[i] = buffer[reverseTable[i]]; + imag[i] = 0; + } + + while (halfSize < bufferSize) { + //phaseShiftStepReal = Math.cos(-Math.PI/halfSize); + //phaseShiftStepImag = Math.sin(-Math.PI/halfSize); + phaseShiftStepReal = cosTable[halfSize]; + phaseShiftStepImag = sinTable[halfSize]; + + currentPhaseShiftReal = 1; + currentPhaseShiftImag = 0; + + for (var fftStep = 0; fftStep < halfSize; fftStep++) { + i = fftStep; + + while (i < bufferSize) { + off = i + halfSize; + tr = (currentPhaseShiftReal * real[off]) - (currentPhaseShiftImag * imag[off]); + ti = (currentPhaseShiftReal * imag[off]) + (currentPhaseShiftImag * real[off]); + + real[off] = real[i] - tr; + imag[off] = imag[i] - ti; + real[i] += tr; + imag[i] += ti; + + i += halfSize << 1; + } + + tmpReal = currentPhaseShiftReal; + currentPhaseShiftReal = (tmpReal * phaseShiftStepReal) - (currentPhaseShiftImag * phaseShiftStepImag); + currentPhaseShiftImag = (tmpReal * phaseShiftStepImag) + (currentPhaseShiftImag * phaseShiftStepReal); + } + + halfSize = halfSize << 1; + } + + return this.calculateSpectrum(); +};