File:Cutscene.js
/**
* @module EaselJS Cutscene
* @namespace springroll.easeljs
* @requires Core, EaselJS Display
*/
(function()
{
var Debug,
Container = include('createjs.Container'),
Application,
Sound;
/**
* Cutscene is a class for playing a single EaselJS animation synced to a
* single audio file with springroll.Sound, with optional captions.
*
* @class Cutscene
* @extends createjs.Container
* @constructor
* @param {Object} options The runtime specific setup data for the cutscene.
* @param {createjs.Container} options.clip The movieclip animation
* @param {int} options.width The width of the animation
* @param {int} options.height The height of the animation
* @param {String|springroll.AbstractDisplay} options.display The display or display
* id of the EaselJSDisplay to draw on.
* @param {Array} options.audio The audio being played
* @param {Number} [options.imageScale=1] Scaling to apply to all images loaded for the
* Cutscene.
* @param {springroll.Captions} [options.captions] A Captions instance to display captions text on.
*/
var Cutscene = function(options)
{
if (!Application)
{
Debug = include('springroll.Debug', false);
Application = include('springroll.Application');
Sound = include('springroll.Sound');
}
Container.call(this);
if (!options)
{
throw "Need options to create Cutscene";
}
/**
* Reference to the display we are drawing on
* @property {Display} display
* @private
*/
var display = this._display = options.display;
/**
* The designed width of the animation
* @property {Number} width
* @private
*/
this.width = options.width;
/**
* The designed height of the animation
* @property {Number} height
* @private
*/
this.height = options.height;
/**
* The time elapsed in seconds.
* @property {Number} _elapsedTime
* @private
*/
this._elapsedTime = 0;
/**
* Time sorted list of audio that needs to be played, as well as information on if they
* should be synced or not.
* @property {Array} _audio
* @private
*/
this._audio = options.audio ? options.audio.slice() : [];
this._audio.sort(audioSorter);
/**
* Index of the sound that is next up in _audio.
* @property {int} _audioIndex
* @private
*/
this._audioIndex = 0;
/**
* The clip that is being animated.
* @property {createjs.MovieClip} _clip
* @private
*/
var clip = this._clip = options.clip;
/**
* The queue of sound instances of playing audio that the animation should be synced to.
* Only the most recent active one will be synced to.
* @property {Array} _activeSyncAudio
* @private
*/
this._activeSyncAudio = [];
/**
* The time in seconds into the animation that the current synced audio started.
* @property {Number} _soundStartTime
* @private
*/
this._soundStartTime = -1;
/**
* Array of active SoundInstances that are not the currently synced one.
* @property {Array} _activeAudio
* @private
*/
this._activeAudio = [];
/**
* If the audio has finished playing.
* @property {Boolean} _audioFinished
* @private
*/
this._audioFinished = false;
/**
* The function to call when playback is complete.
* @property {Function} _endCallback
* @private
*/
this._endCallback = null;
/**
* The Captions object to use to manage captions.
* @property {Captions} _captions
* @private
*/
var captions = this._captions = options.captions;
// Make sure the captions don't update themselves
if (captions)
{
captions.selfUpdate = false;
}
// bind some callbacks
this.update = this.update.bind(this);
this.resize = this.resize.bind(this);
// Set some clip defaults
clip.mouseEnabled = false;
clip.tickEnabled = false;
clip.gotoAndPlay(0);
clip.loop = false;
// Add the clip to the stage
this.addChild(clip);
this.resize(display.width, display.height);
// Handle the resize of the application
Application.instance.on("resize", this.resize);
};
// Reference to the container
var p = extend(Cutscene, Container);
/**
* Audio sort based on the start time
* @method audioSorter
* @private
*/
function audioSorter(a, b)
{
return a.start - b.start;
}
/**
* Listener for when the Application is resized.
* @method resize
* @param {int} width The new width of the display.
* @param {int} height The new height of the display.
* @private
*/
p.resize = function(width, height)
{
if (!this._clip) return;
var designedRatio = this.width / this.height,
currentRatio = width / height,
scale;
if (designedRatio > currentRatio)
{
// current ratio is narrower than the designed ratio, scale to width
scale = width / this.width;
this.x = 0;
this.y = (height - this.height * scale) * 0.5;
}
else
{
scale = height / this.height;
this.x = (width - this.width * scale) * 0.5;
this.y = 0;
}
this._clip.scaleX = this._clip.scaleY = scale;
};
/**
* Starts playing the cutscene.
* @method start
* @param {Function} callback The function to call when playback is complete.
*/
p.start = function(callback)
{
this._endCallback = callback;
this._elapsedTime = 0;
this._animFinished = false;
this._audioFinished = !this._audio.length;
for (var i = 0; i < this._audio.length; ++i)
{
var data = this._audio[i];
if (data.start === 0)
{
var alias = data.alias;
var instanceRef = {};
if (data.sync)
{
var audio = Sound.instance.play(
alias,
this._audioCallback.bind(this, instanceRef)
);
this._activeSyncAudio.unshift(audio);
instanceRef.instance = audio;
audio._audioData = data;
this._soundStartTime = data.start;
if (this._captions)
{
this._captions.play(alias);
}
}
else
{
var instance = Sound.instance.play(
alias,
this._audioCallback.bind(this, instanceRef)
);
instanceRef.instance = instance;
this._activeAudio.push(instance);
}
++this._audioIndex;
}
else
{
break;
}
}
Application.instance.on("update", this.update);
};
/**
* Callback for when the audio has finished playing.
* @method _audioCallback
* @private
*/
p._audioCallback = function(instanceRef)
{
var index = this._activeSyncAudio.indexOf(instanceRef.instance);
if (index != -1)
{
if (index === 0)
{
this._activeSyncAudio.shift();
}
else
{
this._activeSyncAudio.splice(index, 1);
}
if (this._activeSyncAudio.length < 1)
{
this._audioFinished = true;
this._soundStartTime = -1;
if (this._captions)
{
this._captions.stop();
}
if (this._animFinished)
{
this.stop(true);
}
}
else
{
var data = this._activeSyncAudio[0]._audioData;
this._soundStartTime = data.start;
if (this._captions)
{
this._captions.play(data.alias);
}
}
}
else
{
index = this._activeAudio.indexOf(instanceRef.instance);
if (index != -1)
{
this._activeAudio.splice(index, 1);
}
}
};
/**
* Listener for frame updates.
* @method update
* @param {int} elapsed Time in milliseconds
* @private
*/
p.update = function(elapsed)
{
if (this._animFinished) return;
// update the elapsed time first, in case it starts audio
if (!this._activeSyncAudio.length)
{
this._elapsedTime += elapsed * 0.001;
}
for (var i = this._audioIndex; i < this._audio.length; ++i)
{
var data = this._audio[i];
if (data.start <= this._elapsedTime)
{
var alias = data.alias;
var instanceRef = {};
if (data.sync)
{
this._audioFinished = false;
var audio = Sound.instance.play(
alias,
this._audioCallback.bind(this, instanceRef)
);
this._activeSyncAudio.unshift(audio);
instanceRef.instance = audio;
audio._audioData = data;
//immediately walk back the elapsed time so that we sync to the audio
//and don't run into a situation where we are playing multiple synced
//audio that should not be simultaneous
this._elapsedTime = this._soundStartTime = data.start;
if (this._captions)
{
this._captions.play(alias);
}
}
else
{
var instance = Sound.instance.play(alias,
{
complete: this._audioCallback.bind(this, instanceRef),
offset: (this._elapsedTime - data.start) * 1000
});
instanceRef.instance = instance;
this._activeAudio.push(instance);
}
++this._audioIndex;
}
else
{
break;
}
}
if (this._activeSyncAudio.length)
{
var pos = this._activeSyncAudio[0].position * 0.001;
// sometimes (at least with the flash plugin), the first check of the
// position would be very incorrect
if (this._elapsedTime === 0 && pos > elapsed * 2)
{
// do nothing here
}
else
{
// save the time elapsed
this._elapsedTime = this._soundStartTime + pos;
}
}
if (this._captions && this._soundStartTime >= 0)
{
this._captions.seek(this._activeSyncAudio[0].position);
}
if (!this._animFinished)
{
// set the elapsed time of the clip
var clip = (!this._clip.timeline || this._clip.timeline.duration == 1) ?
this._clip.getChildAt(0) :
this._clip;
clip.elapsedTime = this._elapsedTime;
clip.advance();
if (clip.currentFrame == clip.timeline.duration)
{
this._animFinished = true;
if (this._audioFinished)
{
this.stop(true);
}
}
}
};
/**
* Stops playback of the cutscene.
* @method stop
* @param {Boolean} [doCallback=false] If the end callback should be performed.
*/
p.stop = function(doCallback)
{
Application.instance.off("update", this.update);
var i;
for (i = 0; i < this._activeSyncAudio.length; ++i)
{
this._activeSyncAudio[i].stop();
}
for (i = 0; i < this._activeAudio.length; ++i)
{
this._activeAudio[i].stop();
}
this._activeAudio.length = this._activeSyncAudio.length = 0;
if (this._captions)
{
this._captions.stop();
}
if (doCallback)
{
this.dispatchEvent('complete');
if (this._endCallback)
{
this._endCallback();
this._endCallback = null;
}
}
};
/**
* Destroys the cutscene.
* @method destroy
*/
p.destroy = function()
{
this.stop();
this.dispatchEvent('destroy');
Application.instance.off("resize", this.resize);
this.removeAllChildren(true);
this._activeSyncAudio =
this._activeAudio =
this._audio =
this._display =
this._endCallback =
this._clip =
this._captions = null;
if (this.parent)
{
this.parent.removeChild(this);
}
this.removeAllEventListeners();
};
namespace("springroll").Cutscene = Cutscene;
namespace("springroll.easeljs").Cutscene = Cutscene;
}());