/** * @module Animation * @namespace springroll * @requires Core */ (function(undefined) { //imports var AnimatorTimeline = include('springroll.AnimatorTimeline'), Debug; /** * Animator is a static class designed to provided * base animation functionality, using frame labels of MovieClips * @class Animator * @constructor * @param {springroll.Application} app Reference to the application */ var Animator = function(app) { /** * If we fire debug statements * @property {Boolean} debug */ this.debug = false; /** * The global captions object to use with animator * @property {springroll.Captions} captions */ this.captions = null; /** * Reference to the application * @property {springroll.Application} app * @private */ _app = app; /** * The collection of AnimatorPlugin definitions * @property {Array} _definitions * @private */ _definitions = []; /** * The collection of timelines * @property {Array} _timelines * @private */ _timelines = []; /** * The collection of active timelines, indexed by MovieClip/instance. This will be * null in browsers where Map is not supported. * @property {Map} _timelineMap * @private */ try { //having a parameter causes an error in non-fully compliant implementations, //like iOS 8.X - there is a serious issue that sometimes happens in iOS 8.0-8.2 //This prevents 8.3 from using the faster map, but beyond attempting to detect exactly //which version of iOS is being used, there isn't much of a choice. _timelineMap = new Map([]); //ensure that all the Map features we need are supported if (typeof _timelineMap.delete != "function" || typeof _timelineMap.has != "function" || typeof _timelineMap.set != "function" || typeof _timelineMap.get != "function") { _timelineMap = null; } } catch (e) { // no catch } /** * The collection of used timeline objects * @property {Array} _timelinePool * @private */ _timelinePool = []; /** * If there are timelines available * @property {Boolean} _hasTimelines * @private */ _hasTimelines = false; /** * If the Animator is paused * @property {Boolean} _paused * @private */ _paused = false; //update bind this._update = this._update.bind(this); Debug = include('springroll.Debug', false); }; //reference to the prototype var p = extend(Animator); //private local vars var _timelines, _timelineMap, _definitions, _hasTimelines, _paused, _timelinePool, _app; /** * Register an animator instance definition type * @method register * @param {String} qualifiedClassName The class name * @param {int} priority The priority order for definition */ p.register = function(qualifiedClassName, priority) { var plugin = include(qualifiedClassName, false); if (!plugin) { return; } plugin.priority = priority; _definitions.push(plugin); _definitions.sort(function(a, b) { return b.priority - a.priority; }); }; /** * Play an animation for a frame label event, with more verbose play options. * @method play * @param {*} clip The display object with the same API to animate. * @param {Object} options One of or an array of the following * @param {String} options.anim the frame label of the animation to play, * e.g. "onClose" to "onClose_stop". * @param {int} [options.start=0] Milliseconds into the animation to start. * A value of -1 starts from a random time in the animation. * @param {int} [options.speed=1] a multiplier for the animation speed. * @param {Object|String} [options.audio] Audio to sync the animation to using * springroll.Sound. audio can be a String if you want the audio to start 0 milliseconds * into the animation. * @param {String} [options.audio.alias] The sound alias * @param {int} [options.audio.start] The sound delay * @param {Function} [onComplete] The callback function for when the animation is done. * @param {Function|Boolean} [onCancelled] A callback function for when an animation * is stopped with Animator.stop() or to play another animation. A value of 'true' * uses onComplete for onCancelled. * @return {springroll.AnimatorTimeline} The Timeline object that represents this play() call. */ /** * Play an animation for a frame label event or events * @method play * @param {*} clip The display object with the same API to animate. * @param {String|Array} eventList The name of an event or collection of events * @param {Function} [onComplete] The callback function for when the animation is done. * @param {Function|Boolean} [onCancelled] A callback function for when an animation is * stopped with Animator.stop() or to play another * animation. A value of 'true' uses onComplete for * onCancelled. * @return {springroll.AnimatorTimeline} The Timeline object that represents this play() call. */ p.play = function(clip, eventList, onComplete, onCancelled) { var audio, options; if (onCancelled === true) { onCancelled = onComplete; } if (!Array.isArray(eventList)) { eventList = [eventList]; } this.stop(clip); var timeline = this._makeTimeline( clip, eventList, onComplete, onCancelled ); //if the animation is present and complete if (timeline.eventList && timeline.eventList.length >= 1) { timeline._nextItem(); //advance the timeline to the first item //Before we add the timeline, we should check to see //if there are no timelines, then start the enter frame //updating if (!_hasTimelines) { this._startUpdate(); } if (_timelineMap) { _timelineMap.set(clip, timeline); } _timelines.push(timeline); _hasTimelines = true; return timeline; } if (DEBUG && Debug) { var label = eventList[0].anim || eventList[0].audio || eventList[0] || '<label unknown>'; var readableInstance = clip.name || clip.key || clip.label || clip.id || clip.toString() || clip; Debug.groupCollapsed("No valid animation label \"" + label + "\" in MovieClip " + readableInstance); Debug.red("eventList:", eventList); Debug.red("instance:", clip); Debug.trace("Animator.play"); Debug.groupEnd(); } //reset the timeline and add to the pool of timeline objects _timelinePool.push(timeline.reset()); if (onComplete) { onComplete(); } return null; }; /** * Creates the AnimatorTimeline for a given animation * @method _makeTimeline * @param {*} clip The instance to animate * @param {Array} eventList List of animation events * @param {Function} onComplete The function to callback when we're done * @param {Function} onCancelled The function to callback when cancelled * @return {springroll.AnimatorTimeline} The Timeline object * @private */ p._makeTimeline = function(clip, eventList, onComplete, onCancelled) { var timeline = _timelinePool.length ? _timelinePool.pop() : new AnimatorTimeline(); var Definition = getDefinitionByClip(clip); if (!Definition) return timeline; var instance = Definition.create(clip); if (!instance) { if (DEBUG && Debug) { Debug.warn("Attempting to use Animator to play something that is not compatible: ", clip); } return timeline; } var fps; timeline.instance = instance; timeline.eventList = []; //create a duplicate event list with specific info timeline.onComplete = onComplete; timeline.onCancelled = onCancelled; timeline.speed = speed; var anim, audio, start, speed, alias; for (var j = 0, jLen = eventList.length; j < jLen; ++j) { var listItem = eventList[j]; if (isString(listItem)) { if (!Definition.hasAnimation(clip, listItem)) continue; timeline.eventList.push( { anim: listItem, audio: null, start: 0, speed: 1 }); } else if (typeof listItem == "object") { if (!Definition.hasAnimation(clip, listItem.anim)) { continue; } var animData = { anim: listItem.anim, //convert into seconds, as that is what the time uses internally start: isNumber(listItem.start) ? listItem.start * 0.001 : 0, speed: listItem.speed > 0 ? listItem.speed : 1, loop: listItem.loop }; audio = listItem.audio; //figure out audio stuff if it is okay to use if (audio && _app.sound) { if (isString(audio)) { start = 0; alias = audio; } else { start = audio.start > 0 ? audio.start * 0.001 : 0; //seconds alias = audio.alias; } if (_app.sound.isSupported && !_app.sound.systemMuted && _app.sound.exists(alias)) { _app.sound.preload(alias); animData.alias = alias; animData.audioStart = start; animData.useCaptions = this.captions && this.captions.hasCaption(alias); } } timeline.eventList.push(animData); } else if (isNumber(listItem)) { //convert to seconds timeline.eventList.push(listItem * 0.001); } else if (isFunction(listItem)) { //add functions directly timeline.eventList.push(listItem); } } return timeline; }; /** * Determines if a given instance can be animated by Animator. Note - `id` is a property * with a unique value for each `createjs.DisplayObject`. If a custom object is made that does * not inherit from DisplayObject, it needs to not have an id that is identical to anything * from EaselJS. * @method canAnimate * @param {*} clip The object to check for animation properties. * @return {Boolean} If the instance can be animated or not. */ p.canAnimate = function(clip) { if (!clip) { return false; } return !!getDefinitionByClip(clip); }; /** * Create an instance by clip * @method createInstance * @private * @param {*} clip The animation object to animate * @return {springroll.AnimatorInstance} The animator instance */ var createInstance = function(clip) { if (!clip) { return null; } var Definition = getDefinitionByClip(clip); return Definition ? Definition.create(clip) : null; }; /** * Destroy an instance * @method poolInstance * @private * @param {springroll.AnimatorInstance} instance The instance to destroy */ var poolInstance = function(instance) { var Definition = getDefinitionByClip(instance.clip); Definition.pool(instance); }; /** * Get a definition by clip * @private * @method getDefinitionByClip * @param {*} clip The animation clip * @return {function|null} The new definition */ var getDefinitionByClip = function(clip) { for (var Definition, i = 0, len = _definitions.length; i < len; i++) { Definition = _definitions[i]; if (Definition.test(clip)) { return Definition; } } return null; }; /** * Checks if animation exists * @method hasAnimation * @param {*} clip The instance to check * @param {String} event The frame label event (e.g. "onClose" to "onClose_stop") * @public * @return {Boolean} does this animation exist? */ p.hasAnimation = function(clip, event) { var Definition = getDefinitionByClip(clip); if (!Definition) { return false; } return Definition.hasAnimation(clip, event); }; /** * Get duration of animation event (or sequence of events) in seconds * @method getDuration * @param {*} instance The timeline to check * @param {String|Array} event The frame label event or array, in the format that play() uses. * @public * @return {Number} Duration of animation event in milliseconds */ p.getDuration = function(clip, event) { var Definition = getDefinitionByClip(clip); if (!Definition) { return 0; } if (!Array.isArray(event)) { return Definition.getDuration(clip, event.anim || event); } var duration = 0; for (var i = 0; i < event.length; ++i) { var item = event[i]; if (typeof item == "number") { duration += item; } else if (typeof item == "string") { duration += Definition.getDuration(clip, item); } else if (typeof item == "object" && item.anim) { duration += Definition.getDuration(clip, item.anim); } } return duration; }; /** * Stop the animation. * @method stop * @param {*} clip The instance to stop the action on * @param {Boolean} [removeCallbacks=false] Completely disregard the on complete * or on cancelled callback of this animation. */ p.stop = function(clip, removeCallbacks) { var timeline = getTimelineByClip(clip); if (!timeline) { return; } if (removeCallbacks) { timeline.onComplete = timeline.onCancelled = null; } this._remove(timeline, true); }; /** * Stop all current Animator animations. This is good for cleaning up all * animation, as it doesn't do a callback on any of them. * @method stopAll * @param {createjs.Container} [container] Specify a container to stop timelines * contained within. This only checks one layer deep. * @param {Boolean} [removeCallbacks=false] Completely disregard the on complete * or on cancelled callback of the current animations. */ p.stopAll = function(container, removeCallbacks) { if (!_hasTimelines) { return; } var timeline; for (var i = _timelines.length - 1; i >= 0; --i) { timeline = _timelines[i]; if (!container || container.contains(timeline.instance.clip)) { if (removeCallbacks) { timeline.onComplete = timeline.onCancelled = null; } this._remove(timeline, true); } } }; /** * Remove a timeline from the stack * @method _remove * @param {springroll.AnimatorTimeline} timeline * @param {Boolean} doCancelled If we do the on complete callback * @private */ p._remove = function(timeline, doCancelled) { var index = _timelines.indexOf(timeline); //We can't remove an animation twice if (index < 0) { return; } var onComplete = timeline.onComplete, onCancelled = timeline.onCancelled; //in most cases, if doOnComplete is true, it's a natural stop and //the audio can be allowed to continue if (doCancelled && timeline.soundInst) { timeline.soundInst.stop(); //stop the sound from playing } if (_timelineMap) { _timelineMap.delete(timeline.instance.clip); } //Remove from the stack if (index == _timelines.length - 1) { _timelines.pop(); } else { _timelines.splice(index, 1); } _hasTimelines = _timelines.length > 0; //stop the captions, if relevant if (timeline.useCaptions) { this.captions.stop(); } //Reset the timeline and add to the pool //of timeline objects _timelinePool.push(timeline.reset()); //Check if we should stop the update if (!_hasTimelines) { this._stopUpdate(); } //call the appropriate callback if (doCancelled) { if (onCancelled) { onCancelled(); } } else if (onComplete) { onComplete(); } }; /** * Pause all tweens which have been excuted by `play()` * @method pause */ p.pause = function() { if (_paused) { return; } _paused = true; for (var i = _timelines.length - 1; i >= 0; --i) { _timelines[i].paused = true; } this._stopUpdate(); }; /** * Resumes all tweens executed by the `play()` * @method resume */ p.resume = function() { if (!_paused) { return; } _paused = false; //Resume playing of all the instances for (var i = _timelines.length - 1; i >= 0; --i) { _timelines[i].paused = false; } if (_hasTimelines) { this._startUpdate(); } }; /** * Pauses or unpauses all timelines that are children of the specified DisplayObjectContainer. * @method pauseInGroup * @param {Boolean} paused If this should be paused or unpaused * @param {createjs.Container} container The container to stop timelines contained within */ p.pauseInGroup = function(paused, container) { if (!_hasTimelines || !container) { return; } for (var i = _timelines.length - 1; i >= 0; --i) { if (container.contains(_timelines[i].instance.clip)) { _timelines[i].paused = paused; } } }; /** * Get the timeline object for an instance * @method getTimeline * @param {*} clip The animation clip * @return {springroll.AnimatorTimeline} The timeline */ p.getTimeline = function(clip) { if (!_hasTimelines) { return null; } return getTimelineByClip(clip); }; /** * Loop a clip by timeline * @method getTimelineByClip * @private * @param {*} clip The clip to check * @return {springroll.AnimatorTimeline} The timeline for clip */ var getTimelineByClip = function(clip) { if (_timelineMap) { return _timelineMap.has(clip) ? _timelineMap.get(clip) : null; } else { for (var i = _timelines.length - 1; i >= 0; --i) { if (_timelines[i].instance.clip === clip) { return _timelines[i]; } } } return null; }; /** * Whether the Animator class is currently paused. * @property {Boolean} paused * @readOnly */ Object.defineProperty(p, 'paused', { get: function() { return _paused; } }); /** * Start the updating * @method _startUpdate * @private */ p._startUpdate = function() { _app.on("update", this._update); }; /** * Stop the updating * @method _stopUpdate * @private */ p._stopUpdate = function() { _app.off("update", this._update); }; /** * The update every frame * @method * @param {int} elapsed The time in milliseconds since the last frame * @private */ p._update = function(elapsed) { var delta = elapsed * 0.001; //ms -> sec var t; var instance; var audioPos; var position; for (var i = _timelines.length - 1; i >= 0; --i) { t = _timelines[i]; if (!t) { return; //error checking or stopping of all timelines during update } instance = t.instance; if (t.paused) { continue; } //we'll use this to figure out if the timeline is on the next item //to avoid code repetition position = 0; if (t.soundInst) { if (t.soundInst.isValid) { //convert sound position ms -> sec audioPos = t.soundInst.position * 0.001; if (audioPos < 0) { audioPos = 0; } position = t.soundStart + audioPos; if (t.useCaptions) { this.captions.seek(t.soundInst.position); } } //if sound is no longer valid, stop animation playback immediately else { position = t.duration; } } else { position = t.position + delta * t.speed; } if (position >= t.duration) { while (position >= t.duration) { position -= t.duration; if (t.isLooping) { //error checking if (!t.duration) { t.complete = true; break; } //call the on complete function each time if (t.onComplete) t.onComplete(); } t._nextItem(); if (t.complete) { break; } } if (t.complete) { this._remove(t); continue; } } if (t.playSound && position >= t.soundStart) { t.position = t.soundStart; t.playSound = false; t.soundInst = _app.sound.play( t.soundAlias, this._onSoundDone.bind(this, t, t.listIndex, t.soundAlias), onSoundStarted.bind(null, t, t.listIndex) ); if (t.useCaptions) { this.captions.play(t.soundAlias); } } else { t.position = position; } } }; /** * The sound has been started * @method onSoundStarted * @private * @param {springroll.AnimatorTimeline} timeline * @param {int} playIndex */ var onSoundStarted = function(timeline, playIndex) { if (timeline.listIndex != playIndex) { return; } //convert sound length to seconds timeline.soundEnd = timeline.soundStart + timeline.soundInst.length * 0.001; }; /** * The sound is done * @method _onSoundDone * @private * @param {springroll.AnimatorTimeline} timeline * @param {int} playIndex * @param {String} soundAlias */ p._onSoundDone = function(timeline, playIndex, soundAlias) { if (this.captions && this.captions.currentAlias == soundAlias) { this.captions.stop(); } if (timeline.listIndex != playIndex) { return; } if (timeline.soundEnd > timeline.position) { timeline.position = timeline.soundEnd; } timeline.soundInst = null; }; /** * Stops all animations and cleans up the variables used. * @method destroy */ p.destroy = function() { this.stopAll(null, true); this.captions = null; _app = null; _timelines = null; _timelinePool = null; _timelineMap = null; _definitions = null; }; //Type checking, produces better uglify /** * Check to see if object is a String * @method isString * @param {*} str The string * @return {Boolean} if object is String * @private */ function isString(str) { return typeof str == "string"; } /** * Check to see if object is a Number * @method isNumber * @param {*} num The object to check * @return {Boolean} if object is Number * @private */ function isNumber(num) { return typeof num == "number"; } /** * Check to see if object is a Function * @method isFunction * @param {*} func The object to check * @return {Boolean} if object is Function * @private */ function isFunction(func) { return typeof func == "function"; } //Assign to the global namespace namespace('springroll').Animator = Animator; }());