UNPKG

@createjs/tweenjs

Version:

A simple but powerful tweening / animation library for Javascript. Part of the CreateJS suite of libraries.

816 lines (741 loc) 23.8 kB
/** * @license Tween * Visit http://createjs.com/ for documentation, updates and examples. * * Copyright (c) 2017 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. */ import AbstractTween from "./AbstractTween"; import { linear } from "./Ease"; import Ticker from "@createjs/core/src/utils/Ticker"; /** * Tweens properties for a single target. Methods can be chained to create complex animation sequences: * * @example * Tween.get(target) * .wait(500) * .to({ alpha: 0, visible: false }, 1000) * .call(handleComplete); * * Multiple tweens can share a target, however if they affect the same properties there could be unexpected * behaviour. To stop all tweens on an object, use {@link tweenjs.Tween#removeTweens} or pass `override:true` * in the props argument. * * createjs.Tween.get(target, {override:true}).to({x:100}); * * Subscribe to the {@link tweenjs.Tween#event:change} event to be notified when the tween position changes. * * createjs.Tween.get(target, {override:true}).to({x:100}).addEventListener("change", handleChange); * function handleChange(event) { * // The tween changed. * } * * @see {@link tweenjs.Tween.get} * * @memberof tweenjs * @extends tweenjs.AbstractTween * * @param {Object} target The target object that will have its properties tweened. * @param {Object} [props] The configuration properties to apply to this instance (ex. `{loop:-1, paused:true}`). * @param {Boolean} [props.useTicks] * @param {Boolean} [props.ignoreGlobalPause] * @param {Number|Boolean} [props.loop] * @param {Boolean} [props.reversed] * @param {Boolean} [props.bounce] * @param {Number} [props.timeScale] * @param {Object} [props.pluginData] * @param {Boolean} [props.paused] * @param {*} [props.position] indicates the initial position for this tween * @param {*} [props.onChange] adds the specified function as a listener to the `change` event * @param {*} [props.onComplete] adds the specified function as a listener to the `complete` event * @param {*} [props.override] if true, removes all existing tweens for the target */ export default class Tween extends AbstractTween { constructor (target, props) { super(props); /** * Allows you to specify data that will be used by installed plugins. Each plugin uses this differently, but in general * you specify data by assigning it to a property of `pluginData` with the same name as the plugin. * Note that in many cases, this data is used as soon as the plugin initializes itself for the tween. * As such, this data should be set before the first `to` call in most cases. * * Some plugins also store working data in this object, usually in a property named `_PluginClassName`. * See the documentation for individual plugins for more details. * * @example * myTween.pluginData.SmartRotation = data; * myTween.pluginData.SmartRotation_disabled = true; * * * @default null * @type {Object} */ this.pluginData = null; /** * The target of this tween. This is the object on which the tweened properties will be changed. * @type {Object} * @readonly */ this.target = target; /** * Indicates the tween's current position is within a passive wait. * @type {Boolean} * @default false * @readonly */ this.passive = false; /** * @private * @type {TweenStep} */ this._stepHead = new TweenStep(null, 0, 0, {}, null, true); /** * @private * @type {TweenStep} */ this._stepTail = this._stepHead; /** * The position within the current step. Used by MovieClip. * @private * @type {Number} * @default 0 */ this._stepPosition = 0; /** * @private * @type {TweenAction} * @default null */ this._actionHead = null; /** * @private * @type {TweenAction} * @default null */ this._actionTail = null; /** * Plugins added to this tween instance. * @private * @type {Object[]} * @default null */ this._plugins = null; /** * Hash for quickly looking up added plugins. Null until a plugin is added. * @private * @type {Object} * @default null */ this._pluginIds = null; /** * Used by plugins to inject new properties. * @private * @type {Object} * @default null */ this._injected = null; if (props) { this.pluginData = props.pluginData; if (props.override) { Tween.removeTweens(target); } } if (!this.pluginData) { this.pluginData = {}; } this._init(props); } /** * Returns a new tween instance. This is functionally identical to using `new Tween(...)`, but may look cleaner * with the chained syntax of TweenJS. * * @static * @example * let tween = Tween.get(target).to({ x: 100 }, 500); * // equivalent to: * let tween = new Tween(target).to({ x: 100 }, 500); * * @param {Object} target The target object that will have its properties tweened. * @param {Object} [props] The configuration properties to apply to this instance (ex. `{loop:-1, paused:true}`). * @param {Boolean} [props.useTicks] * @param {Boolean} [props.ignoreGlobalPause] * @param {Number|Boolean} [props.loop] * @param {Boolean} [props.reversed] * @param {Boolean} [props.bounce] * @param {Number} [props.timeScale] * @param {Object} [props.pluginData] * @param {Boolean} [props.paused] * @param {*} [props.position] indicates the initial position for this tween * @param {*} [props.onChange] adds the specified function as a listener to the `change` event * @param {*} [props.onComplete] adds the specified function as a listener to the `complete` event * @param {*} [props.override] if true, removes all existing tweens for the target * @return {Tween} A reference to the created tween. */ static get (target, props) { return new Tween(target, props); } /** * Advances all tweens. This typically uses the {{#crossLink "Ticker"}}{{/crossLink}} class, but you can call it * manually if you prefer to use your own "heartbeat" implementation. * * @static * * @param {Number} delta The change in time in milliseconds since the last tick. Required unless all tweens have * `useTicks` set to true. * @param {Boolean} paused Indicates whether a global pause is in effect. Tweens with {@link tweenjs.Tween#ignoreGlobalPause} * will ignore this, but all others will pause if this is `true`. */ static tick (delta, paused) { let tween = Tween._tweenHead; while (tween) { let next = tween._next; // in case it completes and wipes its _next property if ((paused && !tween.ignoreGlobalPause) || tween._paused) { /* paused */ } else { tween.advance(tween.useTicks ? 1: delta); } tween = next; } } /** * Handle events that result from Tween being used as an event handler. This is included to allow Tween to handle * {@link tweenjs.Ticker#event:tick} events from the {@link tweenjs.Ticker}. * No other events are handled in Tween. * * @static * @since 0.4.2 * * @param {Object} event An event object passed in by the {@link core.EventDispatcher}. Will * usually be of type "tick". */ static handleEvent (event) { if (event.type === "tick") { this.tick(event.delta, event.paused); } } /** * Removes all existing tweens for a target. This is called automatically by new tweens if the `override` * property is `true`. * * @static * * @param {Object} target The target object to remove existing tweens from.= */ static removeTweens (target) { if (!target.tweenjs_count) { return; } let tween = Tween._tweenHead; while (tween) { let next = tween._next; if (tween.target === target) { tween.paused = true; } tween = next; } target.tweenjs_count = 0; } /** * Stop and remove all existing tweens. * * @static * @since 0.4.1 */ static removeAllTweens () { let tween = Tween._tweenHead; while (tween) { let next = tween._next; tween._paused = true; tween.target && (tween.target.tweenjs_count = 0); tween._next = tween._prev = null; tween = next; } Tween._tweenHead = Tween._tweenTail = null; } /** * Indicates whether there are any active tweens on the target object (if specified) or in general. * * @static * * @param {Object} [target] The target to check for active tweens. If not specified, the return value will indicate * if there are any active tweens on any target. * @return {Boolean} Indicates if there are active tweens. */ static hasActiveTweens (target) { if (target) { return !!target.tweenjs_count; } return !!Tween._tweenHead; } /** * Installs a plugin, which can modify how certain properties are handled when tweened. See the {{#crossLink "SamplePlugin"}}{{/crossLink}} * for an example of how to write TweenJS plugins. Plugins should generally be installed via their own `install` method, in order to provide * the plugin with an opportunity to configure itself. * * @static * * @param {Object} plugin The plugin to install * @param {Object} props The props to pass to the plugin */ static installPlugin (plugin, props) { plugin.install(props); const priority = (plugin.priority = plugin.priority || 0), arr = (Tween._plugins = Tween._plugins || []); for (let i = 0, l = arr.length; i < l; i++) { if (priority < arr[i].priority) { break; } } arr.splice(i, 0, plugin); } /** * Registers or unregisters a tween with the ticking system. * * @private * @static * * @param {Tween} tween The tween instance to register or unregister. * @param {Boolean} paused If `false`, the tween is registered. If `true` the tween is unregistered. */ static _register (tween, paused) { const target = tween.target; if (!paused && tween._paused) { // TODO: this approach might fail if a dev is using sealed objects if (target) { target.tweenjs_count = target.tweenjs_count ? target.tweenjs_count + 1 : 1; } let tail = Tween._tweenTail; if (!tail) { Tween._tweenHead = Tween._tweenTail = tween; } else { Tween._tweenTail = tail._next = tween; tween._prev = tail; } if (!Tween._inited) { Ticker.addEventListener("tick", Tween); Tween._inited = true; } } else if (paused && !tween._paused) { if (target) { target.tweenjs_count--; } let next = tween._next, prev = tween._prev; if (next) { next._prev = prev; } else { Tween._tweenTail = prev; } // was tail if (prev) { prev._next = next; } else { Tween._tweenHead = next; } // was head. tween._next = tween._prev = null; } } /** * Adds a wait (essentially an empty tween). * * @example * // This tween will wait 1s before alpha is faded to 0. * Tween.get(target) * .wait(1000) * .to({ alpha: 0 }, 1000); * * @param {Number} duration The duration of the wait in milliseconds (or in ticks if `useTicks` is true). * @param {Boolean} [passive=false] Tween properties will not be updated during a passive wait. This * is mostly useful for use with {@link tweenjs.Timeline} instances that contain multiple tweens * affecting the same target at different times. * @chainable */ wait (duration, passive = false) { if (duration > 0) { this._addStep(+duration, this._stepTail.props, null, passive); } return this; } /** * Adds a tween from the current values to the specified properties. Set duration to 0 to jump to these value. * Numeric properties will be tweened from their current value in the tween to the target value. Non-numeric * properties will be set at the end of the specified duration. * * @example * Tween.get(target) * .to({ alpha: 0, visible: false }, 1000); * * @param {Object} props An object specifying property target values for this tween (Ex. `{x:300}` would tween the x * property of the target to 300). * @param {Number} [duration=0] The duration of the tween in milliseconds (or in ticks if `useTicks` is true). * @param {Function} [ease=Ease.linear] The easing function to use for this tween. See the {@link tweenjs.Ease} * class for a list of built-in ease functions. * @chainable */ to (props, duration = 0, ease = linear) { if (duration < 0) { duration = 0; } const step = this._addStep(+duration, null, ease); this._appendProps(props, step); return this; } /** * Adds a label that can be used with {@link tweenjs.Tween#gotoAndPlay}/{@link tweenjs.Tween#gotoAndStop} * at the current point in the tween. * * @example * let tween = Tween.get(foo) * .to({ x: 100 }, 1000) * .label("myLabel") * .to({ x: 200 }, 1000); * // ... * tween.gotoAndPlay("myLabel"); // would play from 1000ms in. * * @param {String} label The label name. * @chainable */ label (name) { this.addLabel(name, this.duration); return this; } /** * Adds an action to call the specified function. * * @example * // would call myFunction() after 1 second. * Tween.get() * .wait(1000) * .call(myFunction); * * @param {Function} callback The function to call. * @param {Array} [params]. The parameters to call the function with. If this is omitted, then the function * will be called with a single param pointing to this tween. * @param {Object} [scope]. The scope to call the function in. If omitted, it will be called in the target's scope. * @chainable */ call (callback, params, scope) { return this._addAction(scope || this.target, callback, params || [this]); } /** * Adds an action to set the specified props on the specified target. If `target` is null, it will use this tween's * target. Note that for properties on the target object, you should consider using a zero duration {@link tweenjs.Tween#to} * operation instead so the values are registered as tweened props. * * @example * tween.wait(1000) * .set({ visible: false }, foo); * * @param {Object} props The properties to set (ex. `{ visible: false }`). * @param {Object} [target] The target to set the properties on. If omitted, they will be set on the tween's target. * @chainable */ set (props, target) { return this._addAction(target || this.target, this._set, [ props ]); } /** * Adds an action to play (unpause) the specified tween. This enables you to sequence multiple tweens. * * @example * tween.to({ x: 100 }, 500) * .play(otherTween); * * @param {Tween} [tween] The tween to play. Defaults to this tween. * @chainable */ play (tween) { return this._addAction(tween || this, this._set, [{ paused: false }]); } /** * Adds an action to pause the specified tween. * At 60fps the tween will advance by ~16ms per tick, if the tween above was at 999ms prior to the current tick, it * will advance to 1015ms (15ms into the second "step") and then pause. * * @example * tween.pause(otherTween) * .to({ alpha: 1 }, 1000) * .play(otherTween); * * // Note that this executes at the end of a tween update, * // so the tween may advance beyond the time the pause action was inserted at. * * tween.to({ foo: 0 }, 1000) * .pause() * .to({ foo: 1 }, 1000); * * @param {Tween} [tween] The tween to pause. Defaults to this tween. * @chainable */ pause (tween) { return this._addAction(tween || this, this._set, [{ paused: false }]); } /** * @throws Tween cannot be cloned. */ clone () { throw "Tween can not be cloned."; } /** * @private * @param {Object} plugin */ _addPlugin (plugin) { let ids = this._pluginIds || (this._pluginIds = {}), id = plugin.id; if (!id || ids[id]) { return; } // already added ids[id] = true; let plugins = this._plugins || (this._plugins = []), priority = plugin.priority || 0; for (let i = 0, l = plugins.length; i < l; i++) { if (priority < plugins[i].priority) { plugins.splice(i, 0, plugin); return; } } plugins.push(plugin); } /** * @private * @param {} jump * @param {Boolean} end */ _updatePosition (jump, end) { let step = this._stepHead.next, t = this.position, d = this.duration; if (this.target && step) { // find our new step index: let stepNext = step.next; while (stepNext && stepNext.t <= t) { step = step.next; stepNext = step.next; } let ratio = end ? d === 0 ? 1 : t/d : (t-step.t)/step.d; // TODO: revisit this. this._updateTargetProps(step, ratio, end); } this._stepPosition = step ? t - step.t : 0; } /** * @private * @param {Object} step * @param {Number} ratio * @param {Boolean} end Indicates to plugins that the full tween has ended. */ _updateTargetProps (step, ratio, end) { if (this.passive = !!step.passive) { return; } // don't update props. let v, v0, v1, ease; let p0 = step.prev.props; let p1 = step.props; if (ease = step.ease) { ratio = ease(ratio, 0, 1, 1); } let plugins = this._plugins; proploop : for (let n in p0) { v0 = p0[n]; v1 = p1[n]; // values are different & it is numeric then interpolate: if (v0 !== v1 && (typeof(v0) === "number")) { v = v0 + (v1 - v0) * ratio; } else { v = ratio >= 1 ? v1 : v0; } if (plugins) { for (let i = 0, l = plugins.length; i < l; i++) { let value = plugins[i].change(this, step, n, v, ratio, end); if (value === Tween.IGNORE) { continue proploop; } if (value !== undefined) { v = value; } } } this.target[n] = v; } } /** * @private * @param {Number} startPos * @param {Number} endPos * @param {Boolean} includeStart */ _runActionsRange (startPos, endPos, jump, includeStart) { let rev = startPos > endPos; let action = rev ? this._actionTail : this._actionHead; let ePos = endPos, sPos = startPos; if (rev) { ePos = startPos; sPos = endPos; } let t = this.position; while (action) { let pos = action.t; if (pos === endPos || (pos > sPos && pos < ePos) || (includeStart && pos === startPos)) { action.funct.apply(action.scope, action.params); if (t !== this.position) { return true; } } action = rev ? action.prev : action.next; } } /** * @private * @param {Object} props */ _appendProps (props, step, stepPlugins) { let initProps = this._stepHead.props, target = this.target, plugins = Tween._plugins; let n, i, l, value, initValue, inject; let oldStep = step.prev, oldProps = oldStep.props; let stepProps = step.props || (step.props = this._cloneProps(oldProps)); let cleanProps = {}; for (n in props) { if (!props.hasOwnProperty(n)) { continue; } cleanProps[n] = stepProps[n] = props[n]; if (initProps[n] !== undefined) { continue; } initValue = undefined; // accessing missing properties on DOMElements when using CSSPlugin is INSANELY expensive, so we let the plugin take a first swing at it. if (plugins) { for (i = plugins.length - 1; i >= 0; i--) { value = plugins[i].init(this, n, initValue); if (value !== undefined) { initValue = value; } if (initValue === Tween.IGNORE) { (ignored = ignored || {})[n] = true; delete(stepProps[n]); delete(cleanProps[n]); break; } } } if (initValue !== Tween.IGNORE) { if (initValue === undefined) { initValue = target[n]; } oldProps[n] = (initValue === undefined) ? null : initValue; } } for (n in cleanProps) { value = props[n]; // propagate old value to previous steps: let o, prev = oldStep; while ((o = prev) && (prev = o.prev)) { if (prev.props === o.props) { continue; } // wait step if (prev.props[n] !== undefined) { break; } // already has a value, we're done. prev.props[n] = oldProps[n]; } } if (stepPlugins && (plugins = this._plugins)) { for (i = plugins.length - 1; i >= 0; i--) { plugins[i].step(this, step, cleanProps); } } if (inject = this._injected) { this._injected = null; this._appendProps(inject, step, false); } } /** * Used by plugins to inject properties onto the current step. Called from within `Plugin.step` calls. * For example, a plugin dealing with color, could read a hex color, and inject red, green, and blue props into the tween. * See the SamplePlugin for more info. * @see tweenjs.SamplePlugin * @private * @param {String} name * @param {Object} value */ _injectProp (name, value) { let o = this._injected || (this._injected = {}); o[name] = value; } /** * @private * @param {Number} duration * @param {Object} props * @param {Function} ease * @param {Boolean} [passive=false] */ _addStep (duration, props, ease, passive = false) { let step = new TweenStep(this._stepTail, this.duration, duration, props, ease, passive); this.duration += duration; return this._stepTail = (this._stepTail.next = step); } /** * @private * @param {Object} scope * @param {Function} funct * @param {Array} params */ _addAction (scope, funct, params) { let action = new TweenAction(this._actionTail, this.duration, scope, funct, params); if (this._actionTail) { this._actionTail.next = action; } else { this._actionHead = action; } this._actionTail = action; return this; } /** * @private * @param {Object} props */ _set (props) { for (let n in props) { this[n] = props[n]; } } /** * @private * @param {Object} props */ _cloneProps (props) { let o = {}; for (let n in props) { o[n] = props[n]; } return o; } } // tiny api (primarily for tool output): { let p = Tween.prototype; p.w = p.wait; p.t = p.to; p.c = p.call; p.s = p.set; } // static properties /** * Constant returned by plugins to tell the tween not to use default assignment. * @property IGNORE * @type {Object} * @static */ Tween.IGNORE = {}; /** * @property _listeners * @type {Tween[]} * @static * @private */ Tween._tweens = []; /** * @property _plugins * @type {Object} * @static * @private */ Tween._plugins = null; /** * @property _tweenHead * @type {Tween} * @static * @private */ Tween._tweenHead = null; /** * @property _tweenTail * @type {Tween} * @static * @private */ Tween._tweenTail = null; // helpers: /** * @private * @param {*} prev * @param {*} t * @param {*} d * @param {*} props * @param {*} ease * @param {*} passive */ class TweenStep { constructor (prev, t, d, props, ease, passive) { this.next = null; this.prev = prev; this.t = t; this.d = d; this.props = props; this.ease = ease; this.passive = passive; this.index = prev ? prev.index + 1 : 0; } } /** * @private * @param {*} prev * @param {*} t * @param {*} scope * @param {*} funct * @param {*} params */ class TweenAction { constructor (prev, t, scope, funct, params) { this.next = null; this.d = 0; this.prev = prev; this.t = t; this.scope = scope; this.funct = funct; this.params = params; } }