diff --git a/.gitattributes b/.gitattributes index 7632623..8eb495a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -19,3 +19,5 @@ *.as binary *.mp3 binary +*.ogg binary +*.wav binary diff --git a/bower.json b/bower.json index 3cb92d9..9ffb42b 100644 --- a/bower.json +++ b/bower.json @@ -5,6 +5,7 @@ "dependencies": { "red-sass": "~0.2.0", "particulate": "0.3.1", - "handlebars": "1.3.0" + "handlebars": "1.3.0", + "howler.js": "~1.1.25" } } diff --git a/grunt/config/copy.js b/grunt/config/copy.js index 9b7e5b8..620cecb 100644 --- a/grunt/config/copy.js +++ b/grunt/config/copy.js @@ -13,6 +13,7 @@ module.exports = function (config) { config.source + '*.{ico,txt}', config.source + '.htaccess', config.source + 'img/{,*/}*.{jpg,jpeg,png,webp,gif}', + config.source + 'audio/{,*/}*.{mp3,ogg,wav}', config.source + 'fonts/*', config.source + 'lib/modernizr/modernizr.js' ], diff --git a/pages/_base.html b/pages/_base.html index e5acada..924ea42 100644 --- a/pages/_base.html +++ b/pages/_base.html @@ -30,6 +30,7 @@

Medusae

+
diff --git a/static/audio/bg-loop.mp3 b/static/audio/bg-loop.mp3 new file mode 100644 index 0000000..63153de Binary files /dev/null and b/static/audio/bg-loop.mp3 differ diff --git a/static/audio/bg-loop.ogg b/static/audio/bg-loop.ogg new file mode 100644 index 0000000..0373e58 Binary files /dev/null and b/static/audio/bg-loop.ogg differ diff --git a/static/js/app.js b/static/js/app.js index 5fe6eee..91e5f63 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -13,11 +13,24 @@ require('js/items/*'); require('js/scenes/*'); require('js/controllers/*'); +var scene = App.MainScene.create(); +var audioToggle = App.ToggleController.create({ + name : 'audio' +}); + App.ModalController.create({ name : 'info' }); setTimeout(function () { - var scene = App.MainScene.create(); + scene.initItems(); + scene.initAudio(); + scene.appendRenderer(); scene.loop.start(); + + audioToggle.addListener(scene, 'toggleAudio'); }, 0); + +setTimeout(function () { + audioToggle.toggleState(); +}, 2000); diff --git a/static/js/controllers/AudioController.js b/static/js/controllers/AudioController.js new file mode 100644 index 0000000..c0ceeb7 --- /dev/null +++ b/static/js/controllers/AudioController.js @@ -0,0 +1,123 @@ +/*global Howl*/ +var Tweens = App.Tweens; + +App.AudioController = AudioController; +function AudioController(config) { + this._isEnabled = true; + this._urls = {}; + this._soundNames = []; + this._volume = {}; + this._volumeTarget = {}; + this._playing = {}; + + this.baseUrl = config.baseUrl; + this.sounds = {}; + + this.tweenVolume = Tweens.factorTween(this._volume, 0.05); +} + +AudioController.create = App.ctor(AudioController); + +AudioController.prototype.addSound = function (path, name) { + name = name || path; + var url = this.baseUrl + path; + + this._soundNames.push(name); + + this._urls[name] = [ + url + '.mp3', + url + '.ogg' + ]; +}; + +AudioController.prototype.createSound = function (name, params) { + var sound = this.sounds[name]; + if (sound) { return sound; } + + params = params || {}; + params.urls = this._urls[name]; + + sound = new Howl(params); + sound.sid = name; + + this._volume[name] = 0; + this._volumeTarget[name] = params.volume || 0; + this.sounds[name] = sound; + return sound; +}; + +AudioController.prototype.getSound = function (name) { + return this.sounds[name] || this.createSound(name); +}; + +AudioController.prototype.playSound = function (name) { + if (!this._isEnabled) { return; } + var sound = this.getSound(name); + if (!sound) { return; } + if (!sound._loaded) { + setTimeout(this.playSound.bind(this, name), 250); + return; + } + + this._playing[name] = true; + sound.play(); +}; + +AudioController.prototype.pauseSound = function (name) { + if (!this._playing[name]) { return; } + this.sounds[name].pause(); + this._playing[name] = false; +}; + +AudioController.prototype.setVolume = function (name, volume) { + this._volumeTarget[name] = volume; +}; + +AudioController.prototype.stopSound = function (name) { + this.getSound(name).stop(); +}; + +AudioController.prototype.stopAllSounds = function () { + var names = this._soundNames; + var sounds = this.sounds; + var sound; + + for (var i = 0, il = names.length; i < il; i ++) { + sound = sounds[names[i]]; + if (!sound) { continue; } + sound.stop(); + } +}; + +AudioController.prototype.enableSound = function () { + this._isEnabled = true; +}; + +AudioController.prototype.disableSound = function () { + this._isEnabled = false; + this.stopAllSounds(); +}; + +AudioController.prototype.update = function () { + var sounds = this.sounds; + var names = this._soundNames; + var playing = this._playing; + var volumeTarget = this._volumeTarget; + var name, sound, volume; + + for (var i = 0, il = names.length; i < il; i ++) { + name = names[i]; + sound = sounds[name]; + if (!(sound && sound._loaded)) { continue; } + + // Update volume + volume = this.tweenVolume(name, volumeTarget[name]); + + if (volume < 0.0001 && playing[name]) { + sound.pause(); + } else { + if (!playing[name]) { this.playSound(name); } + sound.volume(volume); + } + } +}; diff --git a/static/js/controllers/ToggleController.js b/static/js/controllers/ToggleController.js new file mode 100644 index 0000000..c16e967 --- /dev/null +++ b/static/js/controllers/ToggleController.js @@ -0,0 +1,42 @@ +App.ToggleController = ToggleController; +function ToggleController(config) { + var name = config.name; + var toggle = this.toggle = document.getElementById('toggle-' + name); + + this.isActive = config.isActive != null ? config.isActive : false; + this._toggleClassName = toggle.className; + this._listeners = []; + + toggle.addEventListener('click', this.toggleState.bind(this), false); +} + +ToggleController.create = App.ctor(ToggleController); + +ToggleController.prototype.addListener = function (context, fn) { + this._listeners.push({ + context : context, + fn : fn + }); +}; + +ToggleController.prototype.triggerListeners = function () { + var listeners = this._listeners; + var listener; + + for (var i = 0, il = listeners.length; i < il; i ++) { + listener = listeners[i]; + listener.context[listener.fn].call(listener.context, this.isActive); + } +}; + +ToggleController.prototype.toggleState = function (event) { + if (this.isActive) { + this.toggle.className = this._toggleClassName; + this.isActive = false; + } else { + this.toggle.className += ' active'; + this.isActive = true; + } + + this.triggerListeners(); +}; diff --git a/static/js/libs.js b/static/js/libs.js index 6d7711d..9046f85 100644 --- a/static/js/libs.js +++ b/static/js/libs.js @@ -15,3 +15,5 @@ require('lib-extras/three/postprocessing/ShaderPass'); require('lib-extras/three/postprocessing/TexturePass'); require('lib-extras/three/postprocessing/MaskPass'); require('lib-extras/three/postprocessing/BloomPass'); + +require('lib/howler.js/howler.js'); diff --git a/static/js/scenes/MainScene.js b/static/js/scenes/MainScene.js index 3b199e4..b5c9d3d 100644 --- a/static/js/scenes/MainScene.js +++ b/static/js/scenes/MainScene.js @@ -13,10 +13,8 @@ function MainScene() { this.initRenderer(); this.initFxComposer(); this.addPostFx(); - this.onWindowResize(); - this.initControls(); - this.initItems(); + this.onWindowResize(); camera.position.set(200, 100, 0); camera.lookAt(scene.position); @@ -29,6 +27,10 @@ function MainScene() { MainScene.create = App.ctor(MainScene); +// .................................................. +// Graphics +// + MainScene.prototype.initRenderer = function () { var renderer = this.renderer = new THREE.WebGLRenderer({ devicePixelRatio : this.pxRatio, @@ -37,8 +39,15 @@ MainScene.prototype.initRenderer = function () { renderer.setClearColor(0x111111, 1); renderer.autoClear = false; +}; + +MainScene.prototype.appendRenderer = function () { + var canvas = this.renderer.domElement; - this.el.appendChild(renderer.domElement); + this.el.appendChild(canvas); + setTimeout(function () { + canvas.className = 'active'; + }, 0); }; MainScene.prototype.initFxComposer = function () { @@ -104,6 +113,27 @@ MainScene.prototype.initItems = function () { dust.addTo(this.scene); }; +MainScene.prototype.onWindowResize = function () { + var width = window.innerWidth; + var height = window.innerHeight; + var pxRatio = this.pxRatio; + var postWidth = width * pxRatio; + var postHeight = height * pxRatio; + + this.width = width; + this.height = height; + + this.camera.aspect = width / height; + this.camera.updateProjectionMatrix(); + + this.renderer.setSize(width, height); + this.composer.setSize(postWidth, postHeight); +}; + +// .................................................. +// Controls +// + MainScene.prototype.initControls = function () { var controls = new THREE.TrackballControls(this.camera, this.el); @@ -122,23 +152,6 @@ MainScene.prototype.initControls = function () { this.controlsUp = controls.object.up; }; -MainScene.prototype.onWindowResize = function () { - var width = window.innerWidth; - var height = window.innerHeight; - var pxRatio = this.pxRatio; - var postWidth = width * pxRatio; - var postHeight = height * pxRatio; - - this.width = width; - this.height = height; - - this.camera.aspect = width / height; - this.camera.updateProjectionMatrix(); - - this.renderer.setSize(width, height); - this.composer.setSize(postWidth, postHeight); -}; - MainScene.prototype.onDocumentKey = function (event) { switch (event.which) { case 32: @@ -148,12 +161,55 @@ MainScene.prototype.onDocumentKey = function (event) { } }; +// .................................................. +// Audio +// + +MainScene.prototype.initAudio = function () { + var audio = this.audio = App.AudioController.create({ + baseUrl : App.STATIC_URL + 'audio/' + }); + + audio.addSound('bg-loop', 'bgLoop'); + audio.createSound('bgLoop', { + loop : true + }); +}; + +MainScene.prototype.beginAudio = function () { + var audio = this.audio; + + audio.playSound('bgLoop'); + audio.setVolume('bgLoop', 1); + this._audioIsPlaying = true; +}; + +MainScene.prototype.pauseAudio = function () { + var audio = this.audio; + + audio.setVolume('bgLoop', 0); + this._audioIsPlaying = false; +}; + +MainScene.prototype.toggleAudio = function () { + if (this._audioIsPlaying) { + this.pauseAudio(); + } else { + this.beginAudio(); + } +}; + +// .................................................. +// Loop +// + MainScene.prototype.update = function (delta) { var up = this.controlsUp; var gravity = this.gravity; this.gravityForce.set(up.x * gravity, up.y * gravity, up.z * gravity); this.medusae.update(delta); + this.audio.update(delta); }; MainScene.prototype.render = function (delta, stepProgress) { diff --git a/static/js/utils/Tweens.js b/static/js/utils/Tweens.js new file mode 100644 index 0000000..2f097d6 --- /dev/null +++ b/static/js/utils/Tweens.js @@ -0,0 +1,30 @@ +var Tweens = App.Tweens = {}; + +// Tween to target by difference factor +Tweens.factorTween = function (context, defaultFactor) { + return function (name, target, instanceFactor) { + var state = context[name]; + if (state == null) { state = context[name] = target; } + var factor = instanceFactor || defaultFactor; + + return context[name] += (target - state) * factor; + }; +}; + +// Tween to target by fixed step +Tweens.stepTween = function (context, defaultStep) { + return function (name, target, instanceStep) { + var state = context[name]; + if (state == null) { state = context[name] = target; } + if (state === target) { return state; } + var step = instanceStep || defaultStep; + var dir = state < target ? 1 : -1; + + if ((target - state) * dir < step) { + context[name] = target; + return state; + } + + return context[name] += step * dir; + }; +}; diff --git a/static/scss/apps/_application.scss b/static/scss/apps/_application.scss index 51dcd9d..4a8ed56 100644 --- a/static/scss/apps/_application.scss +++ b/static/scss/apps/_application.scss @@ -1,6 +1,19 @@ +body { + background: #1b1b1b; +} + #container { @include absolute(0); overflow: hidden; + + canvas { + opacity: 0; + transition: opacity 2500ms 500ms; + + &.active { + opacity: 1; + } + } } .controls { @@ -11,6 +24,8 @@ } .controls-button { + position: relative; + display: block; width: 50px; height: 50px; cursor: pointer; @@ -24,7 +39,7 @@ border-radius: 2px; background: #fff; - transition: background 100ms; + transition: background 200ms; content: ""; }