/* * Tween * Visit http://createjs.com/ for documentation, updates and examples. * * Copyright (c) 2010 gskinner.com, inc. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ /** * @module EaselJS */ // namespace: this.createjs = this.createjs||{}; (function() { "use strict"; // constructor /** * Base class that both {{#crossLink "Tween"}}{{/crossLink}} and {{#crossLink "Timeline"}}{{/crossLink}} extend. Should not be instantiated directly. * @class AbstractTween * @param {Object} [props] The configuration properties to apply to this instance (ex. `{loop:-1, paused:true}`). * Supported props are listed below. These props are set on the corresponding instance properties except where * specified. * @param {boolean} [props.useTicks=false] See the {{#crossLink "AbstractTween/useTicks:property"}}{{/crossLink}} property for more information. * @param {boolean} [props.ignoreGlobalPause=false] See the {{#crossLink "AbstractTween/ignoreGlobalPause:property"}}{{/crossLink}} for more information. * @param {number|boolean} [props.loop=0] See the {{#crossLink "AbstractTween/loop:property"}}{{/crossLink}} for more information. * @param {boolean} [props.reversed=false] See the {{#crossLink "AbstractTween/reversed:property"}}{{/crossLink}} for more information. * @param {boolean} [props.bounce=false] See the {{#crossLink "AbstractTween/bounce:property"}}{{/crossLink}} for more information. * @param {number} [props.timeScale=1] See the {{#crossLink "AbstractTween/timeScale:property"}}{{/crossLink}} for more information. * @param {Function} [props.onChange] Adds the specified function as a listener to the {{#crossLink "AbstractTween/change:event"}}{{/crossLink}} event * @param {Function} [props.onComplete] Adds the specified function as a listener to the {{#crossLink "AbstractTween/complete:event"}}{{/crossLink}} event * @extends EventDispatcher * @constructor */ function AbstractTween(props) { this.EventDispatcher_constructor(); // public properties: /** * Causes this tween to continue playing when a global pause is active. For example, if TweenJS is using {{#crossLink "Ticker"}}{{/crossLink}}, * then setting this to false (the default) will cause this tween to be paused when `Ticker.paused` is set to * `true`. See the {{#crossLink "Tween/tick"}}{{/crossLink}} method for more info. Can be set via the `props` * parameter. * @property ignoreGlobalPause * @type Boolean * @default false */ this.ignoreGlobalPause = false; /** * Indicates the number of times to loop. If set to -1, the tween will loop continuously. * * Note that a tween must loop at _least_ once to see it play in both directions when `{{#crossLink "AbstractTween/bounce:property"}}{{/crossLink}}` * is set to `true`. * @property loop * @type {Number} * @default 0 */ this.loop = 0; /** * Uses ticks for all durations instead of milliseconds. This also changes the behaviour of some actions (such as `call`). * Changing this value on a running tween could have unexpected results. * @property useTicks * @type {Boolean} * @default false * @readonly */ this.useTicks = false; /** * Causes the tween to play in reverse. * @property reversed * @type {Boolean} * @default false */ this.reversed = false; /** * Causes the tween to reverse direction at the end of each loop. Each single-direction play-through of the * tween counts as a single bounce. For example, to play a tween once forward, and once back, set the * `{{#crossLink "AbstractTween/loop:property"}}{{/crossLink}}` to `1`. * @property bounce * @type {Boolean} * @default false */ this.bounce = false; /** * Changes the rate at which the tween advances. For example, a `timeScale` value of `2` will double the * playback speed, a value of `0.5` would halve it. * @property timeScale * @type {Number} * @default 1 */ this.timeScale = 1; /** * Indicates the duration of this tween in milliseconds (or ticks if `useTicks` is true), irrespective of `loops`. * This value is automatically updated as you modify the tween. Changing it directly could result in unexpected * behaviour. * @property duration * @type {Number} * @default 0 * @readonly */ this.duration = 0; /** * The current normalized position of the tween. This will always be a value between 0 and `duration`. * Changing this property directly will have unexpected results, use {{#crossLink "Tween/setPosition"}}{{/crossLink}}. * @property position * @type {Object} * @default 0 * @readonly */ this.position = 0; /** * The raw tween position. This value will be between `0` and `loops * duration` while the tween is active, or -1 before it activates. * @property rawPosition * @type {Number} * @default -1 * @readonly */ this.rawPosition = -1; // private properties: /** * @property _paused * @type {Boolean} * @default false * @protected */ this._paused = true; /** * @property _next * @type {Tween} * @default null * @protected */ this._next = null; /** * @property _prev * @type {Tween} * @default null * @protected */ this._prev = null; /** * @property _parent * @type {Object} * @default null * @protected */ this._parent = null; /** * @property _labels * @type Object * @protected **/ this._labels = null; /** * @property _labelList * @type Array[Object] * @protected **/ this._labelList = null; if (props) { this.useTicks = !!props.useTicks; this.ignoreGlobalPause = !!props.ignoreGlobalPause; this.loop = props.loop === true ? -1 : (props.loop||0); this.reversed = !!props.reversed; this.bounce = !!props.bounce; this.timeScale = props.timeScale||1; props.onChange && this.addEventListener("change", props.onChange); props.onComplete && this.addEventListener("complete", props.onComplete); } // while `position` is shared, it needs to happen after ALL props are set, so it's handled in _init() }; var p = createjs.extend(AbstractTween, createjs.EventDispatcher); // events: /** * Dispatched whenever the tween's position changes. It occurs after all tweened properties are updated and actions * are executed. * @event change **/ /** * Dispatched when the tween reaches its end and has paused itself. This does not fire until all loops are complete; * tweens that loop continuously will never fire a complete event. * @event complete **/ // getter / setters: /** * Use the {{#crossLink "AbstractTween/paused:property"}}{{/crossLink}} property instead. * @method _setPaused * @param {Boolean} [value=true] Indicates whether the tween should be paused (`true`) or played (`false`). * @return {AbstractTween} This tween instance (for chaining calls) * @protected * @chainable */ p._setPaused = function(value) { createjs.Tween._register(this, value); return this; }; p.setPaused = createjs.deprecate(p._setPaused, "AbstractTween.setPaused"); /** * Use the {{#crossLink "AbstractTween/paused:property"}}{{/crossLink}} property instead. * @method _getPaused * @protected */ p._getPaused = function() { return this._paused; }; p.getPaused = createjs.deprecate(p._getPaused, "AbstactTween.getPaused"); /** * Use the {{#crossLink "AbstractTween/currentLabel:property"}}{{/crossLink}} property instead. * @method _getCurrentLabel * @protected * @return {String} The name of the current label or null if there is no label **/ p._getCurrentLabel = function(pos) { var labels = this.getLabels(); if (pos == null) { pos = this.position; } for (var i = 0, l = labels.length; i<l; i++) { if (pos < labels[i].position) { break; } } return (i===0) ? null : labels[i-1].label; }; p.getCurrentLabel = createjs.deprecate(p._getCurrentLabel, "AbstractTween.getCurrentLabel"); /** * Pauses or unpauses the tween. A paused tween is removed from the global registry and is eligible for garbage * collection if no other references to it exist. * @property paused * @type Boolean **/ /** * Returns the name of the label on or immediately before the current position. For example, given a tween with * two labels, "first" on frame index 4, and "second" on frame 8, `currentLabel` would return: * <UL> * <LI>null if the current position is 2.</LI> * <LI>"first" if the current position is 4.</LI> * <LI>"first" if the current position is 7.</LI> * <LI>"second" if the current position is 15.</LI> * </UL> * @property currentLabel * @type String * @readonly **/ try { Object.defineProperties(p, { paused: { set: p._setPaused, get: p._getPaused }, currentLabel: { get: p._getCurrentLabel } }); } catch (e) {} // public methods: /** * Advances the tween by a specified amount. * @method advance * @param {Number} delta The amount to advance in milliseconds (or ticks if useTicks is true). Negative values are supported. * @param {Number} [ignoreActions=false] If true, actions will not be executed due to this change in position. */ p.advance = function(delta, ignoreActions) { this.setPosition(this.rawPosition+delta*this.timeScale, ignoreActions); }; /** * Advances the tween to a specified position. * @method setPosition * @param {Number} rawPosition The raw position to seek to in milliseconds (or ticks if useTicks is true). * @param {Boolean} [ignoreActions=false] If true, do not run any actions that would be triggered by this operation. * @param {Boolean} [jump=false] If true, only actions at the new position will be run. If false, actions between the old and new position are run. * @param {Function} [callback] Primarily for use with MovieClip, this callback is called after properties are updated, but before actions are run. */ p.setPosition = function(rawPosition, ignoreActions, jump, callback) { var d=this.duration, loopCount=this.loop, prevRawPos = this.rawPosition; var loop=0, t=0, end=false; // normalize position: if (rawPosition < 0) { rawPosition = 0; } if (d === 0) { // deal with 0 length tweens. end = true; if (prevRawPos !== -1) { return end; } // we can avoid doing anything else if we're already at 0. } else { loop = rawPosition/d|0; t = rawPosition-loop*d; end = (loopCount !== -1 && rawPosition >= loopCount*d+d); if (end) { rawPosition = (t=d)*(loop=loopCount)+d; } if (rawPosition === prevRawPos) { return end; } // no need to update var rev = !this.reversed !== !(this.bounce && loop%2); // current loop is reversed if (rev) { t = d-t; } } // set this in advance in case an action modifies position: this.position = t; this.rawPosition = rawPosition; this._updatePosition(jump, end); if (end) { this.paused = true; } callback&&callback(this); if (!ignoreActions) { this._runActions(prevRawPos, rawPosition, jump, !jump && prevRawPos === -1); } this.dispatchEvent("change"); if (end) { this.dispatchEvent("complete"); } }; /** * Calculates a normalized position based on a raw position. For example, given a tween with a duration of 3000ms set to loop: * console.log(myTween.calculatePosition(3700); // 700 * @method calculatePosition * @param {Number} rawPosition A raw position. */ p.calculatePosition = function(rawPosition) { // largely duplicated from setPosition, but necessary to avoid having to instantiate generic objects to pass values (end, loop, position) back. var d=this.duration, loopCount=this.loop, loop=0, t=0; if (d===0) { return 0; } if (loopCount !== -1 && rawPosition >= loopCount*d+d) { t = d; loop = loopCount } // end else if (rawPosition < 0) { t = 0; } else { loop = rawPosition/d|0; t = rawPosition-loop*d; } var rev = !this.reversed !== !(this.bounce && loop%2); // current loop is reversed return rev ? d-t : t; }; /** * Returns a list of the labels defined on this tween sorted by position. * @method getLabels * @return {Array[Object]} A sorted array of objects with label and position properties. **/ p.getLabels = function() { var list = this._labelList; if (!list) { list = this._labelList = []; var labels = this._labels; for (var n in labels) { list.push({label:n, position:labels[n]}); } list.sort(function (a,b) { return a.position- b.position; }); } return list; }; /** * Defines labels for use with gotoAndPlay/Stop. Overwrites any previously set labels. * @method setLabels * @param {Object} labels An object defining labels for using {{#crossLink "Timeline/gotoAndPlay"}}{{/crossLink}}/{{#crossLink "Timeline/gotoAndStop"}}{{/crossLink}} * in the form `{myLabelName:time}` where time is in milliseconds (or ticks if `useTicks` is `true`). **/ p.setLabels = function(labels) { this._labels = labels; this._labelList = null; }; /** * Adds a label that can be used with {{#crossLink "Timeline/gotoAndPlay"}}{{/crossLink}}/{{#crossLink "Timeline/gotoAndStop"}}{{/crossLink}}. * @method addLabel * @param {String} label The label name. * @param {Number} position The position this label represents. **/ p.addLabel = function(label, position) { if (!this._labels) { this._labels = {}; } this._labels[label] = position; var list = this._labelList; if (list) { for (var i= 0,l=list.length; i<l; i++) { if (position < list[i].position) { break; } } list.splice(i, 0, {label:label, position:position}); } }; /** * Unpauses this timeline and jumps to the specified position or label. * @method gotoAndPlay * @param {String|Number} positionOrLabel The position in milliseconds (or ticks if `useTicks` is `true`) * or label to jump to. **/ p.gotoAndPlay = function(positionOrLabel) { this.paused = false; this._goto(positionOrLabel); }; /** * Pauses this timeline and jumps to the specified position or label. * @method gotoAndStop * @param {String|Number} positionOrLabel The position in milliseconds (or ticks if `useTicks` is `true`) or label * to jump to. **/ p.gotoAndStop = function(positionOrLabel) { this.paused = true; this._goto(positionOrLabel); }; /** * If a numeric position is passed, it is returned unchanged. If a string is passed, the position of the * corresponding frame label will be returned, or `null` if a matching label is not defined. * @method resolve * @param {String|Number} positionOrLabel A numeric position value or label string. **/ p.resolve = function(positionOrLabel) { var pos = Number(positionOrLabel); if (isNaN(pos)) { pos = this._labels && this._labels[positionOrLabel]; } return pos; }; /** * Returns a string representation of this object. * @method toString * @return {String} a string representation of the instance. */ p.toString = function() { return "[AbstractTween]"; }; /** * @method clone * @protected */ p.clone = function() { throw("AbstractTween can not be cloned.") }; // private methods: /** * Shared logic that executes at the end of the subclass constructor. * @method _init * @protected */ p._init = function(props) { if (!props || !props.paused) { this.paused = false; } if (props&&(props.position!=null)) { this.setPosition(props.position); } }; /** * @method _updatePosition * @protected */ p._updatePosition = function(jump, end) { // abstract. }; /** * @method _goto * @protected **/ p._goto = function(positionOrLabel) { var pos = this.resolve(positionOrLabel); if (pos != null) { this.setPosition(pos, false, true); } }; /** * @method _runActions * @protected */ p._runActions = function(startRawPos, endRawPos, jump, includeStart) { // runs actions between startPos & endPos. Separated to support action deferral. //console.log(this.passive === false ? " > Tween" : "Timeline", "run", startRawPos, endRawPos, jump, includeStart); // if we don't have any actions, and we're not a Timeline, then return: // TODO: a cleaner way to handle this would be to override this method in Tween, but I'm not sure it's worth the overhead. if (!this._actionHead && !this.tweens) { return; } var d=this.duration, reversed=this.reversed, bounce=this.bounce, loopCount=this.loop; var loop0, loop1, t0, t1; if (d === 0) { // deal with 0 length tweens: loop0 = loop1 = t0 = t1 = 0; reversed = bounce = false; } else { loop0=startRawPos/d|0; loop1=endRawPos/d|0; t0=startRawPos-loop0*d; t1=endRawPos-loop1*d; } // catch positions that are past the end: if (loopCount !== -1) { if (loop1 > loopCount) { t1=d; loop1=loopCount; } if (loop0 > loopCount) { t0=d; loop0=loopCount; } } // special cases: if (jump) { return this._runActionsRange(t1, t1, jump, includeStart); } // jump. else if (loop0 === loop1 && t0 === t1 && !jump && !includeStart) { return; } // no actions if the position is identical and we aren't including the start else if (loop0 === -1) { loop0 = t0 = 0; } // correct the -1 value for first advance, important with useTicks. var dir = (startRawPos <= endRawPos), loop = loop0; do { var rev = !reversed !== !(bounce && loop % 2); var start = (loop === loop0) ? t0 : dir ? 0 : d; var end = (loop === loop1) ? t1 : dir ? d : 0; if (rev) { start = d - start; end = d - end; } if (bounce && loop !== loop0 && start === end) { /* bounced onto the same time/frame, don't re-execute end actions */ } else if (this._runActionsRange(start, end, jump, includeStart || (loop !== loop0 && !bounce))) { return true; } includeStart = false; } while ((dir && ++loop <= loop1) || (!dir && --loop >= loop1)); }; p._runActionsRange = function(startPos, endPos, jump, includeStart) { // abstract }; createjs.AbstractTween = createjs.promote(AbstractTween, "EventDispatcher"); }());