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: "";
}