diff --git a/TODO.md b/TODO.md
index ffa1855..001e76a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -12,4 +12,5 @@ MINOR SHTUFF
- Word box class less annoying
- Refactoring, ugh
-- Draw: Pavlov, TF2T, Random
\ No newline at end of file
+- Draw: Pavlov, TF2T, Random
+- a better handwritten font, with REAL bold & italics???
\ No newline at end of file
diff --git a/assets/all_c.png b/assets/all_c.png
deleted file mode 100644
index b23161f..0000000
Binary files a/assets/all_c.png and /dev/null differ
diff --git a/assets/all_d.png b/assets/all_d.png
deleted file mode 100644
index 9443377..0000000
Binary files a/assets/all_d.png and /dev/null differ
diff --git a/assets/grim.png b/assets/grim.png
deleted file mode 100644
index 063dba9..0000000
Binary files a/assets/grim.png and /dev/null differ
diff --git a/assets/pavlov.png b/assets/pavlov.png
deleted file mode 100644
index 0a00e6e..0000000
Binary files a/assets/pavlov.png and /dev/null differ
diff --git a/assets/prober.png b/assets/prober.png
deleted file mode 100644
index 4e1f41b..0000000
Binary files a/assets/prober.png and /dev/null differ
diff --git a/assets/random.png b/assets/random.png
deleted file mode 100644
index 40911c6..0000000
Binary files a/assets/random.png and /dev/null differ
diff --git a/assets/tf2t.png b/assets/tf2t.png
deleted file mode 100644
index 2d0ccf5..0000000
Binary files a/assets/tf2t.png and /dev/null differ
diff --git a/assets/tft.png b/assets/tft.png
deleted file mode 100644
index d43126c..0000000
Binary files a/assets/tft.png and /dev/null differ
diff --git a/assets/tournament_peep.json b/assets/tournament_peep.json
new file mode 100644
index 0000000..f54c06c
--- /dev/null
+++ b/assets/tournament_peep.json
@@ -0,0 +1,75 @@
+{"frames": {
+
+"tournament_peep0000":
+{
+ "frame": {"x":10,"y":10,"w":79,"h":140},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":79,"h":140},
+ "sourceSize": {"w":79,"h":140}
+},
+"tournament_peep0001":
+{
+ "frame": {"x":99,"y":10,"w":79,"h":140},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":79,"h":140},
+ "sourceSize": {"w":79,"h":140}
+},
+"tournament_peep0002":
+{
+ "frame": {"x":188,"y":10,"w":79,"h":140},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":79,"h":140},
+ "sourceSize": {"w":79,"h":140}
+},
+"tournament_peep0003":
+{
+ "frame": {"x":277,"y":10,"w":79,"h":140},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":79,"h":140},
+ "sourceSize": {"w":79,"h":140}
+},
+"tournament_peep0004":
+{
+ "frame": {"x":366,"y":10,"w":79,"h":140},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":79,"h":140},
+ "sourceSize": {"w":79,"h":140}
+},
+"tournament_peep0005":
+{
+ "frame": {"x":10,"y":160,"w":79,"h":140},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":79,"h":140},
+ "sourceSize": {"w":79,"h":140}
+},
+"tournament_peep0006":
+{
+ "frame": {"x":99,"y":160,"w":79,"h":140},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":79,"h":140},
+ "sourceSize": {"w":79,"h":140}
+},
+"tournament_peep0007":
+{
+ "frame": {"x":188,"y":160,"w":79,"h":140},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":79,"h":140},
+ "sourceSize": {"w":79,"h":140}
+}},
+"meta": {
+ "app": "Adobe Animate",
+ "version": "15.2.0.66",
+ "image": "tournament_peep.png",
+ "format": "RGBA8888",
+ "size": {"w":512,"h":512},
+ "scale": "1"
+}
+}
diff --git a/assets/tournament_peep.png b/assets/tournament_peep.png
new file mode 100644
index 0000000..d2a105a
Binary files /dev/null and b/assets/tournament_peep.png differ
diff --git a/css/paper@2x.png b/css/paper@2x.png
deleted file mode 100644
index 68f4d65..0000000
Binary files a/css/paper@2x.png and /dev/null differ
diff --git a/index.html b/index.html
index 476ed6e..d226924 100644
--- a/index.html
+++ b/index.html
@@ -25,6 +25,7 @@
+
@@ -44,7 +45,10 @@
var slideshow, slideSelect;
window.onload = function(){
- Words.convert("lang/en.html").then(function(){
+ Q.all([
+ Loader.loadAssets(Loader.manifest),
+ Words.convert("lang/en.html")
+ ]).then(function(){
// Slideshow
slideshow = new Slideshow({
diff --git a/js/core/Loader.js b/js/core/Loader.js
new file mode 100644
index 0000000..5e6bf66
--- /dev/null
+++ b/js/core/Loader.js
@@ -0,0 +1,113 @@
+window.Loader = {};
+
+Loader.manifest = {};
+Loader.manifestPreload = {}; // For Preloader
+Loader.sounds = {};
+
+/***************
+
+Actually LOAD all the assets in a manifest. Like so:
+
+Loader.loadAssets(Loader.manifest, function(){
+ Loader.sceneManager.gotoScene(Loader.START_SCENE);
+ Loader.startUpdateAndDraw();
+});
+
+***************/
+Loader.loadAssets = function(manifest, completeCallback, progressCallback){
+
+ var deferred = Q.defer();
+ completeCallback = completeCallback || function(){};
+ progressCallback = progressCallback || function(){};
+
+ // ABSOLUTE NUMBER OF ASSETS!
+ var _isLoadingImages = 0;
+ var _isLoadingSounds = 0;
+ var _totalAssetsLoaded = 0;
+ var _totalAssetsToLoad = 0;
+ for(var key in manifest){
+ var src = manifest[key];
+
+ // Loading sounds or images?
+ if(src.slice(-4)==".mp3") _isLoadingSounds=1;
+ else _isLoadingImages=1;
+
+ // Loading sprite or image?
+ if(src.slice(-5)==".json") _totalAssetsToLoad+=2; // Is Sprite. Actually TWO assets.
+ else _totalAssetsToLoad+=1;
+
+ }
+
+ // When you load an asset
+ var _onAssetLoad = function(){
+ _totalAssetsLoaded++;
+ if(progressCallback){
+ progressCallback(_totalAssetsLoaded/_totalAssetsToLoad); // Callback PROGRESS
+ }
+ };
+
+ // When you load a group
+ var _groupsToLoad = _isLoadingImages + _isLoadingSounds;
+ var _onGroupLoaded = function(){
+ _groupsToLoad--;
+ if(_groupsToLoad==0){
+ completeCallback(); // DONE.
+ deferred.resolve();
+ }
+ };
+
+ // HOWLER - Loading Sounds
+ var _soundsToLoad = 0;
+ var _onSoundLoad = function(){
+ _soundsToLoad--;
+ _onAssetLoad();
+ if(_soundsToLoad==0) _onGroupLoaded();
+ };
+
+ // PIXI - Loading Images & Sprites (or pass it to Howler)
+ var loader = PIXI.loader;
+ var resources = PIXI.loader.resources;
+ for(var key in manifest){
+
+ var src = manifest[key];
+
+ // Is MP3. Leave it to Howler.
+ if(src.slice(-4)==".mp3"){
+ var sound = new Howl({ src:[src] });
+ _soundsToLoad++;
+ sound.once('load', _onSoundLoad);
+ Loader.sounds[key] = sound;
+ continue;
+ }
+
+ // Otherwise, is an image (or json). Leave it to PIXI.
+ loader.add(key, src);
+
+ }
+ loader.on('progress', _onAssetLoad);
+ loader.once('complete', _onGroupLoaded);
+ loader.load();
+
+ // Promise!
+ return deferred.promise;
+
+};
+
+/***************
+
+Add assets to manifest! Like so:
+
+Loader.addToManifest(Loader.manifest,{
+ bg: "sprites/bg.png",
+ button: "sprites/button/button.json",
+ [key]: [filepath],
+ [key]: [filepath],
+ etc...
+});
+
+***************/
+Loader.addToManifest = function(manifest, keyValues){
+ for(var key in keyValues){
+ manifest[key] = keyValues[key];
+ }
+};
diff --git a/js/lib/helpers.js b/js/lib/helpers.js
index f93c95b..784d231 100644
--- a/js/lib/helpers.js
+++ b/js/lib/helpers.js
@@ -1,3 +1,8 @@
+/**********************************
+
+RANDOM CRAP TO MAKE MY LIFE EASIER
+
+**********************************/
// Pi is for unwashed plebians
Math.TAU = 2*Math.PI;
@@ -39,4 +44,63 @@ var _removeFade = function(self, INSTANT){
},300);
return deferred.promise;
}
-};
\ No newline at end of file
+};
+
+/*******
+
+Make a Sprite. e.g:
+
+_makeSprite("bg", {width:960});
+
+*******/
+function _makeSprite(textureName, options){
+ options = options || {};
+
+ // Make Sprite
+ var sprite = new PIXI.Sprite(PIXI.loader.resources[textureName].texture);
+
+ // Options
+ if(options.width!==undefined) _scaleToWidth(sprite, options.width);
+ if(options.anchorX!==undefined) sprite.anchor.x=options.anchorX;
+ if(options.anchorY!==undefined) sprite.anchor.y=options.anchorY;
+
+ // Gimme
+ return sprite;
+}
+
+/*******
+
+Make a MovieClip. e.g:
+
+_makeSprite("button", {width:960});
+
+*******/
+function _makeMovieClip(resourceName, options){
+ options = options || {};
+
+ // Make that MovieClip!
+ var resources = PIXI.loader.resources;
+ var resource = resources[resourceName];
+ if(!resource) throw Error("There's no MovieClip named '"+resourceName+"'!");
+ var numFrames = Object.keys(resource.data.frames).length;
+ var frames = [];
+ for(var i=0; i= 0 && vol <= 1) {
+ self._volume = vol;
+
+ // Don't update any of the nodes if we are muted.
+ if (self._muted) {
+ return self;
+ }
+
+ // When using Web Audio, we just need to adjust the master gain.
+ if (self.usingWebAudio) {
+ self.masterGain.gain.value = vol;
+ }
+
+ // Loop through and change volume for all HTML5 audio nodes.
+ for (var i=0; i=0; i--) {
+ self._howls[i].unload();
+ }
+
+ // Create a new AudioContext to make sure it is fully reset.
+ if (self.usingWebAudio && typeof self.ctx.close !== 'undefined') {
+ self.ctx.close();
+ self.ctx = null;
+ setupAudioContext();
+ }
+
+ return self;
+ },
+
+ /**
+ * Check for codec support of specific extension.
+ * @param {String} ext Audio file extention.
+ * @return {Boolean}
+ */
+ codecs: function(ext) {
+ return (this || Howler)._codecs[ext];
+ },
+
+ /**
+ * Setup various state values for global tracking.
+ * @return {Howler}
+ */
+ _setup: function() {
+ var self = this || Howler;
+
+ // Keeps track of the suspend/resume state of the AudioContext.
+ self.state = self.ctx ? self.ctx.state || 'running' : 'running';
+
+ // Automatically begin the 30-second suspend process
+ self._autoSuspend();
+
+ // Check for supported codecs.
+ if (!self.noAudio) {
+ self._setupCodecs();
+ }
+
+ return self;
+ },
+
+ /**
+ * Check for browser support for various codecs and cache the results.
+ * @return {Howler}
+ */
+ _setupCodecs: function() {
+ var self = this || Howler;
+ var audioTest = (typeof Audio !== 'undefined') ? new Audio() : null;
+
+ if (!audioTest || typeof audioTest.canPlayType !== 'function') {
+ return self;
+ }
+
+ var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, '');
+
+ // Opera version <33 has mixed MP3 support, so we need to check for and block it.
+ var checkOpera = self._navigator && self._navigator.userAgent.match(/OPR\/([0-6].)/g);
+ var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33);
+
+ self._codecs = {
+ mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))),
+ mpeg: !!mpegTest,
+ opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''),
+ ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''),
+ oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''),
+ wav: !!audioTest.canPlayType('audio/wav; codecs="1"').replace(/^no$/, ''),
+ aac: !!audioTest.canPlayType('audio/aac;').replace(/^no$/, ''),
+ caf: !!audioTest.canPlayType('audio/x-caf;').replace(/^no$/, ''),
+ m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''),
+ mp4: !!(audioTest.canPlayType('audio/x-mp4;') || audioTest.canPlayType('audio/mp4;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''),
+ weba: !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, ''),
+ webm: !!audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, ''),
+ dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, '')
+ };
+
+ return self;
+ },
+
+ /**
+ * Mobile browsers will only allow audio to be played after a user interaction.
+ * Attempt to automatically unlock audio on the first user interaction.
+ * Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/
+ * @return {Howler}
+ */
+ _enableMobileAudio: function() {
+ var self = this || Howler;
+
+ // Only run this on mobile devices if audio isn't already eanbled.
+ var isMobile = /iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi/i.test(self._navigator && self._navigator.userAgent);
+ var isTouch = !!(('ontouchend' in window) || (self._navigator && self._navigator.maxTouchPoints > 0) || (self._navigator && self._navigator.msMaxTouchPoints > 0));
+ if (self._mobileEnabled || !self.ctx || (!isMobile && !isTouch)) {
+ return;
+ }
+
+ self._mobileEnabled = false;
+
+ // Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views.
+ // Bugs in the browser (especially Mobile Safari) can cause the sampleRate to change from 44100 to 48000.
+ // By calling Howler.unload(), we create a new AudioContext with the correct sampleRate.
+ if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) {
+ self._mobileUnloaded = true;
+ self.unload();
+ }
+
+ // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per:
+ // http://stackoverflow.com/questions/24119684
+ self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050);
+
+ // Call this method on touch start to create and play a buffer,
+ // then check if the audio actually played to determine if
+ // audio has now been unlocked on iOS, Android, etc.
+ var unlock = function() {
+ // Create an empty buffer.
+ var source = self.ctx.createBufferSource();
+ source.buffer = self._scratchBuffer;
+ source.connect(self.ctx.destination);
+
+ // Play the empty buffer.
+ if (typeof source.start === 'undefined') {
+ source.noteOn(0);
+ } else {
+ source.start(0);
+ }
+
+ // Setup a timeout to check that we are unlocked on the next event loop.
+ source.onended = function() {
+ source.disconnect(0);
+
+ // Update the unlocked state and prevent this check from happening again.
+ self._mobileEnabled = true;
+ self.mobileAutoEnable = false;
+
+ // Remove the touch start listener.
+ document.removeEventListener('touchend', unlock, true);
+ };
+ };
+
+ // Setup a touch start listener to attempt an unlock in.
+ document.addEventListener('touchend', unlock, true);
+
+ return self;
+ },
+
+ /**
+ * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds.
+ * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck.
+ * @return {Howler}
+ */
+ _autoSuspend: function() {
+ var self = this;
+
+ if (!self.autoSuspend || !self.ctx || typeof self.ctx.suspend === 'undefined' || !Howler.usingWebAudio) {
+ return;
+ }
+
+ // Check if any sounds are playing.
+ for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000;
+ var duration = ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek;
+ var timeout = (duration * 1000) / Math.abs(sound._rate);
+
+ // Update the parameters of the sound
+ sound._paused = false;
+ sound._ended = false;
+ sound._sprite = sprite;
+ sound._seek = seek;
+ sound._start = self._sprite[sprite][0] / 1000;
+ sound._stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000;
+ sound._loop = !!(sound._loop || self._sprite[sprite][2]);
+
+ // Begin the actual playback.
+ var node = sound._node;
+ if (self._webAudio) {
+ // Fire this when the sound is ready to play to begin Web Audio playback.
+ var playWebAudio = function() {
+ self._refreshBuffer(sound);
+
+ // Setup the playback params.
+ var vol = (sound._muted || self._muted) ? 0 : sound._volume;
+ node.gain.setValueAtTime(vol, Howler.ctx.currentTime);
+ sound._playStart = Howler.ctx.currentTime;
+
+ // Play the sound using the supported method.
+ if (typeof node.bufferSource.start === 'undefined') {
+ sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration);
+ } else {
+ sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration);
+ }
+
+ // Start a new timer if none is present.
+ if (timeout !== Infinity) {
+ self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
+ }
+
+ if (!internal) {
+ setTimeout(function() {
+ self._emit('play', sound._id);
+ }, 0);
+ }
+ };
+
+ if (self._state === 'loaded') {
+ playWebAudio();
+ } else {
+ // Wait for the audio to load and then begin playback.
+ self.once('load', playWebAudio, sound._id);
+
+ // Cancel the end timer.
+ self._clearTimer(sound._id);
+ }
+ } else {
+ // Fire this when the sound is ready to play to begin HTML5 Audio playback.
+ var playHtml5 = function() {
+ node.currentTime = seek;
+ node.muted = sound._muted || self._muted || Howler._muted || node.muted;
+ node.volume = sound._volume * Howler.volume();
+ node.playbackRate = sound._rate;
+
+ setTimeout(function() {
+ node.play();
+
+ // Setup the new end timer.
+ if (timeout !== Infinity) {
+ self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
+ }
+
+ if (!internal) {
+ self._emit('play', sound._id);
+ }
+ }, 0);
+ };
+
+ // Play immediately if ready, or wait for the 'canplaythrough'e vent.
+ var loadedNoReadyState = (self._state === 'loaded' && (window && window.ejecta || !node.readyState && Howler._navigator.isCocoonJS));
+ if (node.readyState === 4 || loadedNoReadyState) {
+ playHtml5();
+ } else {
+ var listener = function() {
+ // Begin playback.
+ playHtml5();
+
+ // Clear this listener.
+ node.removeEventListener(Howler._canPlayEvent, listener, false);
+ };
+ node.addEventListener(Howler._canPlayEvent, listener, false);
+
+ // Cancel the end timer.
+ self._clearTimer(sound._id);
+ }
+ }
+
+ return sound._id;
+ },
+
+ /**
+ * Pause playback and save current position.
+ * @param {Number} id The sound ID (empty to pause all in group).
+ * @return {Howl}
+ */
+ pause: function(id) {
+ var self = this;
+
+ // If the sound hasn't loaded, add it to the load queue to pause when capable.
+ if (self._state !== 'loaded') {
+ self._queue.push({
+ event: 'pause',
+ action: function() {
+ self.pause(id);
+ }
+ });
+
+ return self;
+ }
+
+ // If no id is passed, get all ID's to be paused.
+ var ids = self._getSoundIds(id);
+
+ for (var i=0; i Returns the group's volume value.
+ * volume(id) -> Returns the sound id's current volume.
+ * volume(vol) -> Sets the volume of all sounds in this Howl group.
+ * volume(vol, id) -> Sets the volume of passed sound id.
+ * @return {Howl/Number} Returns self or current volume.
+ */
+ volume: function() {
+ var self = this;
+ var args = arguments;
+ var vol, id;
+
+ // Determine the values based on arguments.
+ if (args.length === 0) {
+ // Return the value of the groups' volume.
+ return self._volume;
+ } else if (args.length === 1) {
+ // First check if this is an ID, and if not, assume it is a new volume.
+ var ids = self._getSoundIds();
+ var index = ids.indexOf(args[0]);
+ if (index >= 0) {
+ id = parseInt(args[0], 10);
+ } else {
+ vol = parseFloat(args[0]);
+ }
+ } else if (args.length >= 2) {
+ vol = parseFloat(args[0]);
+ id = parseInt(args[1], 10);
+ }
+
+ // Update the volume or return the current volume.
+ var sound;
+ if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) {
+ // If the sound hasn't loaded, add it to the load queue to change volume when capable.
+ if (self._state !== 'loaded') {
+ self._queue.push({
+ event: 'volume',
+ action: function() {
+ self.volume.apply(self, args);
+ }
+ });
+
+ return self;
+ }
+
+ // Set the group volume.
+ if (typeof id === 'undefined') {
+ self._volume = vol;
+ }
+
+ // Update one or all volumes.
+ id = self._getSoundIds(id);
+ for (var i=0; i to ? 'out' : 'in';
+ var steps = diff / 0.01;
+ var stepLen = len / steps;
+
+ // If the sound hasn't loaded, add it to the load queue to fade when capable.
+ if (self._state !== 'loaded') {
+ self._queue.push({
+ event: 'fade',
+ action: function() {
+ self.fade(from, to, len, id);
+ }
+ });
+
+ return self;
+ }
+
+ // Set the volume to the start position.
+ self.volume(from, id);
+
+ // Fade the volume of one or all sounds.
+ var ids = self._getSoundIds(id);
+ for (var i=0; i Returns the group's loop value.
+ * loop(id) -> Returns the sound id's loop value.
+ * loop(loop) -> Sets the loop value for all sounds in this Howl group.
+ * loop(loop, id) -> Sets the loop value of passed sound id.
+ * @return {Howl/Boolean} Returns self or current loop value.
+ */
+ loop: function() {
+ var self = this;
+ var args = arguments;
+ var loop, id, sound;
+
+ // Determine the values for loop and id.
+ if (args.length === 0) {
+ // Return the grou's loop value.
+ return self._loop;
+ } else if (args.length === 1) {
+ if (typeof args[0] === 'boolean') {
+ loop = args[0];
+ self._loop = loop;
+ } else {
+ // Return this sound's loop value.
+ sound = self._soundById(parseInt(args[0], 10));
+ return sound ? sound._loop : false;
+ }
+ } else if (args.length === 2) {
+ loop = args[0];
+ id = parseInt(args[1], 10);
+ }
+
+ // If no id is passed, get all ID's to be looped.
+ var ids = self._getSoundIds(id);
+ for (var i=0; i Returns the first sound node's current playback rate.
+ * rate(id) -> Returns the sound id's current playback rate.
+ * rate(rate) -> Sets the playback rate of all sounds in this Howl group.
+ * rate(rate, id) -> Sets the playback rate of passed sound id.
+ * @return {Howl/Number} Returns self or the current playback rate.
+ */
+ rate: function() {
+ var self = this;
+ var args = arguments;
+ var rate, id;
+
+ // Determine the values based on arguments.
+ if (args.length === 0) {
+ // We will simply return the current rate of the first node.
+ id = self._sounds[0]._id;
+ } else if (args.length === 1) {
+ // First check if this is an ID, and if not, assume it is a new rate value.
+ var ids = self._getSoundIds();
+ var index = ids.indexOf(args[0]);
+ if (index >= 0) {
+ id = parseInt(args[0], 10);
+ } else {
+ rate = parseFloat(args[0]);
+ }
+ } else if (args.length === 2) {
+ rate = parseFloat(args[0]);
+ id = parseInt(args[1], 10);
+ }
+
+ // Update the playback rate or return the current value.
+ var sound;
+ if (typeof rate === 'number') {
+ // If the sound hasn't loaded, add it to the load queue to change playback rate when capable.
+ if (self._state !== 'loaded') {
+ self._queue.push({
+ event: 'rate',
+ action: function() {
+ self.rate.apply(self, args);
+ }
+ });
+
+ return self;
+ }
+
+ // Set the group rate.
+ if (typeof id === 'undefined') {
+ self._rate = rate;
+ }
+
+ // Update one or all volumes.
+ id = self._getSoundIds(id);
+ for (var i=0; i Returns the first sound node's current seek position.
+ * seek(id) -> Returns the sound id's current seek position.
+ * seek(seek) -> Sets the seek position of the first sound node.
+ * seek(seek, id) -> Sets the seek position of passed sound id.
+ * @return {Howl/Number} Returns self or the current seek position.
+ */
+ seek: function() {
+ var self = this;
+ var args = arguments;
+ var seek, id;
+
+ // Determine the values based on arguments.
+ if (args.length === 0) {
+ // We will simply return the current position of the first node.
+ id = self._sounds[0]._id;
+ } else if (args.length === 1) {
+ // First check if this is an ID, and if not, assume it is a new seek position.
+ var ids = self._getSoundIds();
+ var index = ids.indexOf(args[0]);
+ if (index >= 0) {
+ id = parseInt(args[0], 10);
+ } else {
+ id = self._sounds[0]._id;
+ seek = parseFloat(args[0]);
+ }
+ } else if (args.length === 2) {
+ seek = parseFloat(args[0]);
+ id = parseInt(args[1], 10);
+ }
+
+ // If there is no ID, bail out.
+ if (typeof id === 'undefined') {
+ return self;
+ }
+
+ // If the sound hasn't loaded, add it to the load queue to seek when capable.
+ if (self._state !== 'loaded') {
+ self._queue.push({
+ event: 'seek',
+ action: function() {
+ self.seek.apply(self, args);
+ }
+ });
+
+ return self;
+ }
+
+ // Get the sound.
+ var sound = self._soundById(id);
+
+ if (sound) {
+ if (typeof seek === 'number' && seek >= 0) {
+ // Pause the sound and update position for restarting playback.
+ var playing = self.playing(id);
+ if (playing) {
+ self.pause(id, true);
+ }
+
+ // Move the position of the track and cancel timer.
+ sound._seek = seek;
+ sound._ended = false;
+ self._clearTimer(id);
+
+ // Restart the playback if the sound was playing.
+ if (playing) {
+ self.play(id, true);
+ }
+
+ // Update the seek position for HTML5 Audio.
+ if (!self._webAudio && sound._node) {
+ sound._node.currentTime = seek;
+ }
+
+ self._emit('seek', id);
+ } else {
+ if (self._webAudio) {
+ var realTime = self.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0;
+ var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0;
+ return sound._seek + (rateSeek + realTime * Math.abs(sound._rate));
+ } else {
+ return sound._node.currentTime;
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not.
+ * @param {Number} id The sound id to check. If none is passed, the whole sound group is checked.
+ * @return {Boolean} True if playing and false if not.
+ */
+ playing: function(id) {
+ var self = this;
+
+ // Check the passed sound ID (if any).
+ if (typeof id === 'number') {
+ var sound = self._soundById(id);
+ return sound ? !sound._paused : false;
+ }
+
+ // Otherwise, loop through all sounds and check if any are playing.
+ for (var i=0; i= 0) {
+ Howler._howls.splice(index, 1);
+ }
+ }
+
+ // Delete this sound from the cache (if no other Howl is using it).
+ var remCache = true;
+ for (i=0; i=0; i--) {
+ if (!events[i].id || events[i].id === id || event === 'load') {
+ setTimeout(function(fn) {
+ fn.call(this, id, msg);
+ }.bind(self, events[i].fn), 0);
+
+ // If this event was setup with `once`, remove it.
+ if (events[i].once) {
+ self.off(event, events[i].fn, events[i].id);
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * Queue of actions initiated before the sound has loaded.
+ * These will be called in sequence, with the next only firing
+ * after the previous has finished executing (even if async like play).
+ * @return {Howl}
+ */
+ _loadQueue: function() {
+ var self = this;
+
+ if (self._queue.length > 0) {
+ var task = self._queue[0];
+
+ // don't move onto the next task until this one is done
+ self.once(task.event, function() {
+ self._queue.shift();
+ self._loadQueue();
+ });
+
+ task.action();
+ }
+
+ return self;
+ },
+
+ /**
+ * Fired when playback ends at the end of the duration.
+ * @param {Sound} sound The sound object to work with.
+ * @return {Howl}
+ */
+ _ended: function(sound) {
+ var self = this;
+ var sprite = sound._sprite;
+
+ // Should this sound loop?
+ var loop = !!(sound._loop || self._sprite[sprite][2]);
+
+ // Fire the ended event.
+ self._emit('end', sound._id);
+
+ // Restart the playback for HTML5 Audio loop.
+ if (!self._webAudio && loop) {
+ self.stop(sound._id, true).play(sound._id);
+ }
+
+ // Restart this timer if on a Web Audio loop.
+ if (self._webAudio && loop) {
+ self._emit('play', sound._id);
+ sound._seek = sound._start || 0;
+ sound._rateSeek = 0;
+ sound._playStart = Howler.ctx.currentTime;
+
+ var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate);
+ self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout);
+ }
+
+ // Mark the node as paused.
+ if (self._webAudio && !loop) {
+ sound._paused = true;
+ sound._ended = true;
+ sound._seek = sound._start || 0;
+ sound._rateSeek = 0;
+ self._clearTimer(sound._id);
+
+ // Clean up the buffer source.
+ self._cleanBuffer(sound._node);
+
+ // Attempt to auto-suspend AudioContext if no sounds are still playing.
+ Howler._autoSuspend();
+ }
+
+ // When using a sprite, end the track.
+ if (!self._webAudio && !loop) {
+ self.stop(sound._id);
+ }
+
+ return self;
+ },
+
+ /**
+ * Clear the end timer for a sound playback.
+ * @param {Number} id The sound ID.
+ * @return {Howl}
+ */
+ _clearTimer: function(id) {
+ var self = this;
+
+ if (self._endTimers[id]) {
+ clearTimeout(self._endTimers[id]);
+ delete self._endTimers[id];
+ }
+
+ return self;
+ },
+
+ /**
+ * Return the sound identified by this ID, or return null.
+ * @param {Number} id Sound ID
+ * @return {Object} Sound object or null.
+ */
+ _soundById: function(id) {
+ var self = this;
+
+ // Loop through all sounds and find the one with this ID.
+ for (var i=0; i=0; i--) {
+ if (cnt <= limit) {
+ return;
+ }
+
+ if (self._sounds[i]._ended) {
+ // Disconnect the audio source when using Web Audio.
+ if (self._webAudio && self._sounds[i]._node) {
+ self._sounds[i]._node.disconnect(0);
+ }
+
+ // Remove sounds until we have the pool size.
+ self._sounds.splice(i, 1);
+ cnt--;
+ }
+ }
+ },
+
+ /**
+ * Get all ID's from the sounds pool.
+ * @param {Number} id Only return one ID if one is passed.
+ * @return {Array} Array of IDs.
+ */
+ _getSoundIds: function(id) {
+ var self = this;
+
+ if (typeof id === 'undefined') {
+ var ids = [];
+ for (var i=0; i 0) {
+ cache[self._src] = buffer;
+ loadSound(self, buffer);
+ }
+ }, function() {
+ self._emit('loaderror', null, 'Decoding audio data failed.');
+ });
+ };
+
+ /**
+ * Sound is now loaded, so finish setting everything up and fire the loaded event.
+ * @param {Howl} self
+ * @param {Object} buffer The decoded buffer sound source.
+ */
+ var loadSound = function(self, buffer) {
+ // Set the duration.
+ if (buffer && !self._duration) {
+ self._duration = buffer.duration;
+ }
+
+ // Setup a sprite if none is defined.
+ if (Object.keys(self._sprite).length === 0) {
+ self._sprite = {__default: [0, self._duration * 1000]};
+ }
+
+ // Fire the loaded event.
+ if (self._state !== 'loaded') {
+ self._state = 'loaded';
+ self._emit('load');
+ self._loadQueue();
+ }
+
+ // Begin playback if specified.
+ if (self._autoplay) {
+ self.play();
+ }
+ };
+
+ /**
+ * Setup the audio context when available, or switch to HTML5 Audio mode.
+ */
+ var setupAudioContext = function() {
+ Howler.noAudio = false;
+
+ // Check if we are using Web Audio and setup the AudioContext if we are.
+ try {
+ if (typeof AudioContext !== 'undefined') {
+ Howler.ctx = new AudioContext();
+ } else if (typeof webkitAudioContext !== 'undefined') {
+ Howler.ctx = new webkitAudioContext();
+ } else {
+ Howler.usingWebAudio = false;
+ }
+ } catch(e) {
+ Howler.usingWebAudio = false;
+ }
+
+ if (!Howler.usingWebAudio) {
+ // No audio is available on this system if noAudio is set to true.
+ if (typeof Audio !== 'undefined') {
+ try {
+ var test = new Audio();
+
+ // Check if the canplaythrough event is available.
+ if (typeof test.oncanplaythrough === 'undefined') {
+ Howler._canPlayEvent = 'canplay';
+ }
+ } catch(e) {
+ Howler.noAudio = true;
+ }
+ } else {
+ Howler.noAudio = true;
+ }
+ }
+
+ // Test to make sure audio isn't disabled in Internet Explorer
+ try {
+ var test = new Audio();
+ if (test.muted) {
+ Howler.noAudio = true;
+ }
+ } catch (e) {}
+
+ // Check if a webview is being used on iOS8 or earlier (rather than the browser).
+ // If it is, disable Web Audio as it causes crashing.
+ var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform));
+ var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
+ var version = appVersion ? parseInt(appVersion[1], 10) : null;
+ if (iOS && version && version < 9) {
+ var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase());
+ if (Howler._navigator && Howler._navigator.standalone && !safari || Howler._navigator && !Howler._navigator.standalone && !safari) {
+ Howler.usingWebAudio = false;
+ }
+ }
+
+ // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage).
+ if (Howler.usingWebAudio) {
+ Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain();
+ Howler.masterGain.gain.value = 1;
+ Howler.masterGain.connect(Howler.ctx.destination);
+ }
+
+ // Re-run the setup on Howler.
+ Howler._setup();
+ };
+
+ // Add support for AMD (Asynchronous Module Definition) libraries such as require.js.
+ if (typeof define === 'function' && define.amd) {
+ define([], function() {
+ return {
+ Howler: Howler,
+ Howl: Howl
+ };
+ });
+ }
+
+ // Add support for CommonJS libraries such as browserify.
+ if (typeof exports !== 'undefined') {
+ exports.Howler = Howler;
+ exports.Howl = Howl;
+ }
+
+ // Define globally in case AMD is not available or unused.
+ if (typeof window !== 'undefined') {
+ window.HowlerGlobal = HowlerGlobal;
+ window.Howler = Howler;
+ window.Howl = Howl;
+ window.Sound = Sound;
+ } else if (typeof global !== 'undefined') { // Add to global in Node.js (for testing, etc).
+ global.HowlerGlobal = HowlerGlobal;
+ global.Howler = Howler;
+ global.Howl = Howl;
+ global.Sound = Sound;
+ }
+})();
+
+
+/*!
+ * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported.
+ *
+ * howler.js v2.0.0
+ * howlerjs.com
+ *
+ * (c) 2013-2016, James Simpson of GoldFire Studios
+ * goldfirestudios.com
+ *
+ * MIT License
+ */
+
+(function() {
+
+ 'use strict';
+
+ // Setup default properties.
+ HowlerGlobal.prototype._pos = [0, 0, 0];
+ HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0];
+
+ /** Global Methods **/
+ /***************************************************************************/
+
+ /**
+ * Helper method to update the stereo panning position of all current Howls.
+ * Future Howls will not use this value unless explicitly set.
+ * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right.
+ * @return {Howler/Number} Self or current stereo panning value.
+ */
+ HowlerGlobal.prototype.stereo = function(pan) {
+ var self = this;
+
+ // Stop right here if not using Web Audio.
+ if (!self.ctx || !self.ctx.listener) {
+ return self;
+ }
+
+ // Loop through all Howls and update their stereo panning.
+ for (var i=self._howls.length-1; i>=0; i--) {
+ self._howls[i].stereo(pan);
+ }
+
+ return self;
+ };
+
+ /**
+ * Get/set the position of the listener in 3D cartesian space. Sounds using
+ * 3D position will be relative to the listener's position.
+ * @param {Number} x The x-position of the listener.
+ * @param {Number} y The y-position of the listener.
+ * @param {Number} z The z-position of the listener.
+ * @return {Howler/Array} Self or current listener position.
+ */
+ HowlerGlobal.prototype.pos = function(x, y, z) {
+ var self = this;
+
+ // Stop right here if not using Web Audio.
+ if (!self.ctx || !self.ctx.listener) {
+ return self;
+ }
+
+ // Set the defaults for optional 'y' & 'z'.
+ y = (typeof y !== 'number') ? self._pos[1] : y;
+ z = (typeof z !== 'number') ? self._pos[2] : z;
+
+ if (typeof x === 'number') {
+ self._pos = [x, y, z];
+ self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]);
+ } else {
+ return self._pos;
+ }
+
+ return self;
+ };
+
+ /**
+ * Get/set the direction the listener is pointing in the 3D cartesian space.
+ * A front and up vector must be provided. The front is the direction the
+ * face of the listener is pointing, and up is the direction the top of the
+ * listener is pointing. Thus, these values are expected to be at right angles
+ * from each other.
+ * @param {Number} x The x-orientation of the listener.
+ * @param {Number} y The y-orientation of the listener.
+ * @param {Number} z The z-orientation of the listener.
+ * @param {Number} xUp The x-orientation of the top of the listener.
+ * @param {Number} yUp The y-orientation of the top of the listener.
+ * @param {Number} zUp The z-orientation of the top of the listener.
+ * @return {Howler/Array} Returns self or the current orientation vectors.
+ */
+ HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) {
+ var self = this;
+
+ // Stop right here if not using Web Audio.
+ if (!self.ctx || !self.ctx.listener) {
+ return self;
+ }
+
+ // Set the defaults for optional 'y' & 'z'.
+ var or = self._orientation;
+ y = (typeof y !== 'number') ? or[1] : y;
+ z = (typeof z !== 'number') ? or[2] : z;
+ xUp = (typeof xUp !== 'number') ? or[3] : xUp;
+ yUp = (typeof yUp !== 'number') ? or[4] : yUp;
+ zUp = (typeof zUp !== 'number') ? or[5] : zUp;
+
+ if (typeof x === 'number') {
+ self._orientation = [x, y, z, xUp, yUp, zUp];
+ self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp);
+ } else {
+ return or;
+ }
+
+ return self;
+ };
+
+ /** Group Methods **/
+ /***************************************************************************/
+
+ /**
+ * Add new properties to the core init.
+ * @param {Function} _super Core init method.
+ * @return {Howl}
+ */
+ Howl.prototype.init = (function(_super) {
+ return function(o) {
+ var self = this;
+
+ // Setup user-defined default properties.
+ self._orientation = o.orientation || [1, 0, 0];
+ self._stereo = o.stereo || null;
+ self._pos = o.pos || null;
+ self._pannerAttr = {
+ coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : 360,
+ coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : 360,
+ coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : 0,
+ distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : 'inverse',
+ maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : 10000,
+ panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : 'HRTF',
+ refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : 1,
+ rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : 1
+ };
+
+ // Setup event listeners.
+ self._onstereo = o.onstereo ? [{fn: o.onstereo}] : [];
+ self._onpos = o.onpos ? [{fn: o.onpos}] : [];
+ self._onorientation = o.onorientation ? [{fn: o.onorientation}] : [];
+
+ // Complete initilization with howler.js core's init function.
+ return _super.call(this, o);
+ };
+ })(Howl.prototype.init);
+
+ /**
+ * Get/set the stereo panning of the audio source for this sound or all in the group.
+ * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right.
+ * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated.
+ * @return {Howl/Number} Returns self or the current stereo panning value.
+ */
+ Howl.prototype.stereo = function(pan, id) {
+ var self = this;
+
+ // Stop right here if not using Web Audio.
+ if (!self._webAudio) {
+ return self;
+ }
+
+ // If the sound hasn't loaded, add it to the load queue to change stereo pan when capable.
+ if (self._state !== 'loaded') {
+ self._queue.push({
+ event: 'stereo',
+ action: function() {
+ self.stereo(pan, id);
+ }
+ });
+
+ return self;
+ }
+
+ // Check for PannerStereoNode support and fallback to PannerNode if it doesn't exist.
+ var pannerType = (typeof Howler.ctx.createStereoPanner === 'undefined') ? 'spatial' : 'stereo';
+
+ // Setup the group's stereo panning if no ID is passed.
+ if (typeof id === 'undefined') {
+ // Return the group's stereo panning if no parameters are passed.
+ if (typeof pan === 'number') {
+ self._stereo = pan;
+ self._pos = [pan, 0, 0];
+ } else {
+ return self._stereo;
+ }
+ }
+
+ // Change the streo panning of one or all sounds in group.
+ var ids = self._getSoundIds(id);
+ for (var i=0; i Returns the group's values.
+ * pannerAttr(id) -> Returns the sound id's values.
+ * pannerAttr(o) -> Set's the values of all sounds in this Howl group.
+ * pannerAttr(o, id) -> Set's the values of passed sound id.
+ *
+ * Attributes:
+ * coneInnerAngle - (360 by default) There will be no volume reduction inside this angle.
+ * coneOuterAngle - (360 by default) The volume will be reduced to a constant value of
+ * `coneOuterGain` outside this angle.
+ * coneOuterGain - (0 by default) The amount of volume reduction outside of `coneOuterAngle`.
+ * distanceModel - ('inverse' by default) Determines algorithm to use to reduce volume as audio moves
+ * away from listener. Can be `linear`, `inverse` or `exponential`.
+ * maxDistance - (10000 by default) Volume won't reduce between source/listener beyond this distance.
+ * panningModel - ('HRTF' by default) Determines which spatialization algorithm is used to position audio.
+ * Can be `HRTF` or `equalpower`.
+ * refDistance - (1 by default) A reference distance for reducing volume as the source
+ * moves away from the listener.
+ * rolloffFactor - (1 by default) How quickly the volume reduces as source moves from listener.
+ *
+ * @return {Howl/Object} Returns self or current panner attributes.
+ */
+ Howl.prototype.pannerAttr = function() {
+ var self = this;
+ var args = arguments;
+ var o, id, sound;
+
+ // Stop right here if not using Web Audio.
+ if (!self._webAudio) {
+ return self;
+ }
+
+ // Determine the values based on arguments.
+ if (args.length === 0) {
+ // Return the group's panner attribute values.
+ return self._pannerAttr;
+ } else if (args.length === 1) {
+ if (typeof args[0] === 'object') {
+ o = args[0];
+
+ // Set the grou's panner attribute values.
+ if (typeof id === 'undefined') {
+ self._pannerAttr = {
+ coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : self._coneInnerAngle,
+ coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : self._coneOuterAngle,
+ coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : self._coneOuterGain,
+ distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : self._distanceModel,
+ maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : self._maxDistance,
+ panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : self._panningModel,
+ refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : self._refDistance,
+ rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : self._rolloffFactor
+ };
+ }
+ } else {
+ // Return this sound's panner attribute values.
+ sound = self._soundById(parseInt(args[0], 10));
+ return sound ? sound._pannerAttr : self._pannerAttr;
+ }
+ } else if (args.length === 2) {
+ o = args[0];
+ id = parseInt(args[1], 10);
+ }
+
+ // Update the values of the specified sounds.
+ var ids = self._getSoundIds(id);
+ for (var i=0; i