phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
868 lines (749 loc) • 28.2 kB
JavaScript
/**
* @author Richard Davey <rich@phaser.io>
* @copyright 2013-2025 Phaser Studio Inc.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var BaseTween = require('./BaseTween');
var Class = require('../../utils/Class');
var Events = require('../events');
var GameObjectCreator = require('../../gameobjects/GameObjectCreator');
var GameObjectFactory = require('../../gameobjects/GameObjectFactory');
var MATH_CONST = require('../../math/const');
var TWEEN_CONST = require('./const');
var TweenData = require('./TweenData');
var TweenFrameData = require('./TweenFrameData');
/**
* @classdesc
* A Tween is able to manipulate the properties of one or more objects to any given value, based
* on a duration and type of ease. They are rarely instantiated directly and instead should be
* created via the TweenManager.
*
* Please note that a Tween will not manipulate any property that begins with an underscore.
*
* @class Tween
* @memberof Phaser.Tweens
* @extends Phaser.Tweens.BaseTween
* @constructor
* @since 3.0.0
*
* @param {Phaser.Tweens.TweenManager} parent - A reference to the Tween Manager that owns this Tween.
* @param {object[]} targets - An array of targets to be tweened. This array should not be manipulated outside of this Tween.
*/
var Tween = new Class({
Extends: BaseTween,
initialize:
function Tween (parent, targets)
{
BaseTween.call(this, parent);
/**
* An array of references to the target/s this Tween is operating on.
*
* This array should not be manipulated outside of this Tween.
*
* @name Phaser.Tweens.Tween#targets
* @type {object[]}
* @since 3.0.0
*/
this.targets = targets;
/**
* Cached target total.
*
* Used internally and should be treated as read-only.
*
* This is not necessarily the same as the data total.
*
* @name Phaser.Tweens.Tween#totalTargets
* @type {number}
* @since 3.0.0
*/
this.totalTargets = targets.length;
/**
* Is this Tween currently seeking?
*
* This boolean is toggled in the `Tween.seek` method.
*
* When a tween is seeking, by default it will not dispatch any events or callbacks.
*
* @name Phaser.Tweens.Tween#isSeeking
* @type {boolean}
* @readonly
* @since 3.19.0
*/
this.isSeeking = false;
/**
* Does this Tween loop or repeat infinitely?
*
* @name Phaser.Tweens.Tween#isInfinite
* @type {boolean}
* @readonly
* @since 3.60.0
*/
this.isInfinite = false;
/**
* Elapsed time in milliseconds of this run through of the Tween.
*
* @name Phaser.Tweens.Tween#elapsed
* @type {number}
* @default 0
* @since 3.60.0
*/
this.elapsed = 0;
/**
* Total elapsed time in milliseconds of the entire Tween, including looping.
*
* @name Phaser.Tweens.Tween#totalElapsed
* @type {number}
* @default 0
* @since 3.60.0
*/
this.totalElapsed = 0;
/**
* Time in milliseconds for the whole Tween to play through once, excluding loop amounts and loop delays.
*
* This value is set in the `Tween.initTweenData` method and is zero before that point.
*
* @name Phaser.Tweens.Tween#duration
* @type {number}
* @default 0
* @since 3.60.0
*/
this.duration = 0;
/**
* Value between 0 and 1. The amount of progress through the Tween, excluding loops.
*
* @name Phaser.Tweens.Tween#progress
* @type {number}
* @default 0
* @since 3.60.0
*/
this.progress = 0;
/**
* Time in milliseconds it takes for the Tween to complete a full playthrough (including looping)
*
* For an infinite Tween, this value is a very large integer.
*
* @name Phaser.Tweens.Tween#totalDuration
* @type {number}
* @default 0
* @since 3.60.0
*/
this.totalDuration = 0;
/**
* The amount of progress that has been made through the entire Tween, including looping.
*
* A value between 0 and 1.
*
* @name Phaser.Tweens.Tween#totalProgress
* @type {number}
* @default 0
* @since 3.60.0
*/
this.totalProgress = 0;
/**
* Is this Tween a Number Tween? Number Tweens are a special kind of tween that don't have a target.
*
* @name Phaser.Tweens.Tween#isNumberTween
* @type {boolean}
* @default false
* @since 3.88.0
*/
this.isNumberTween = false;
},
/**
* Adds a new TweenData to this Tween. Typically, this method is called
* automatically by the TweenBuilder, however you can also invoke it
* yourself.
*
* @method Phaser.Tweens.Tween#add
* @since 3.60.0
*
* @param {number} targetIndex - The target index within the Tween targets array.
* @param {string} key - The property of the target to tween.
* @param {Phaser.Types.Tweens.GetEndCallback} getEnd - What the property will be at the END of the Tween.
* @param {Phaser.Types.Tweens.GetStartCallback} getStart - What the property will be at the START of the Tween.
* @param {?Phaser.Types.Tweens.GetActiveCallback} getActive - If not null, is invoked _immediately_ as soon as the TweenData is running, and is set on the target property.
* @param {function} ease - The ease function this tween uses.
* @param {function} delay - Function that returns the time in milliseconds before tween will start.
* @param {number} duration - The duration of the tween in milliseconds.
* @param {boolean} yoyo - Determines whether the tween should return back to its start value after hold has expired.
* @param {number} hold - Function that returns the time in milliseconds the tween will pause before repeating or returning to its starting value if yoyo is set to true.
* @param {number} repeat - Function that returns the number of times to repeat the tween. The tween will always run once regardless, so a repeat value of '1' will play the tween twice.
* @param {number} repeatDelay - Function that returns the time in milliseconds before the repeat will start.
* @param {boolean} flipX - Should toggleFlipX be called when yoyo or repeat happens?
* @param {boolean} flipY - Should toggleFlipY be called when yoyo or repeat happens?
* @param {?function} interpolation - The interpolation function to be used for arrays of data. Defaults to 'null'.
* @param {?number[]} interpolationData - The array of interpolation data to be set. Defaults to 'null'.
*
* @return {Phaser.Tweens.TweenData} The TweenData instance that was added.
*/
add: function (targetIndex, key, getEnd, getStart, getActive, ease, delay, duration, yoyo, hold, repeat, repeatDelay, flipX, flipY, interpolation, interpolationData)
{
var tweenData = new TweenData(this, targetIndex, key, getEnd, getStart, getActive, ease, delay, duration, yoyo, hold, repeat, repeatDelay, flipX, flipY, interpolation, interpolationData);
this.totalData = this.data.push(tweenData);
return tweenData;
},
/**
* Adds a new TweenFrameData to this Tween. Typically, this method is called
* automatically by the TweenBuilder, however you can also invoke it
* yourself.
*
* @method Phaser.Tweens.Tween#addFrame
* @since 3.60.0
*
* @param {number} targetIndex - The target index within the Tween targets array.
* @param {string} texture - The texture to set on the target at the end of the tween.
* @param {string|number} frame - The texture frame to set on the target at the end of the tween.
* @param {function} delay - Function that returns the time in milliseconds before tween will start.
* @param {number} duration - The duration of the tween in milliseconds.
* @param {number} hold - Function that returns the time in milliseconds the tween will pause before repeating or returning to its starting value if yoyo is set to true.
* @param {number} repeat - Function that returns the number of times to repeat the tween. The tween will always run once regardless, so a repeat value of '1' will play the tween twice.
* @param {number} repeatDelay - Function that returns the time in milliseconds before the repeat will start.
* @param {boolean} flipX - Should toggleFlipX be called when yoyo or repeat happens?
* @param {boolean} flipY - Should toggleFlipY be called when yoyo or repeat happens?
*
* @return {Phaser.Tweens.TweenFrameData} The TweenFrameData instance that was added.
*/
addFrame: function (targetIndex, texture, frame, delay, duration, hold, repeat, repeatDelay, flipX, flipY)
{
var tweenData = new TweenFrameData(this, targetIndex, texture, frame, delay, duration, hold, repeat, repeatDelay, flipX, flipY);
this.totalData = this.data.push(tweenData);
return tweenData;
},
/**
* Returns the current value of the specified Tween Data.
*
* If this Tween has been destroyed, it will return `null`.
*
* @method Phaser.Tweens.Tween#getValue
* @since 3.0.0
*
* @param {number} [index=0] - The Tween Data to return the value from.
*
* @return {number} The value of the requested Tween Data, or `null` if this Tween has been destroyed.
*/
getValue: function (index)
{
if (index === undefined) { index = 0; }
var value = null;
if (this.data)
{
value = this.data[index].current;
}
return value;
},
/**
* See if this Tween is currently acting upon the given target.
*
* @method Phaser.Tweens.Tween#hasTarget
* @since 3.0.0
*
* @param {object} target - The target to check against this Tween.
*
* @return {boolean} `true` if the given target is a target of this Tween, otherwise `false`.
*/
hasTarget: function (target)
{
return (this.targets && this.targets.indexOf(target) !== -1);
},
/**
* Updates the 'end' value of the given property across all matching targets, as long
* as this Tween is currently playing (either forwards or backwards).
*
* Calling this does not adjust the duration of the Tween, or the current progress.
*
* You can optionally tell it to set the 'start' value to be the current value.
*
* If this Tween is in any other state other than playing then calling this method has no effect.
*
* Additionally, if the Tween repeats, is reset, or is seeked, it will revert to the original
* starting and ending values.
*
* @method Phaser.Tweens.Tween#updateTo
* @since 3.0.0
*
* @param {string} key - The property to set the new value for. You cannot update the 'texture' property via this method.
* @param {number} value - The new value of the property.
* @param {boolean} [startToCurrent=false] - Should this change set the start value to be the current value?
*
* @return {this} This Tween instance.
*/
updateTo: function (key, value, startToCurrent)
{
if (startToCurrent === undefined) { startToCurrent = false; }
if (key !== 'texture')
{
for (var i = 0; i < this.totalData; i++)
{
var tweenData = this.data[i];
if (tweenData.key === key && (tweenData.isPlayingForward() || tweenData.isPlayingBackward()))
{
tweenData.end = value;
if (startToCurrent)
{
tweenData.start = tweenData.current;
}
}
}
}
return this;
},
/**
* Restarts the Tween from the beginning.
*
* If the Tween has already finished and been destroyed, restarting it will throw an error.
*
* If you wish to restart the Tween from a specific point, use the `Tween.seek` method instead.
*
* @method Phaser.Tweens.Tween#restart
* @since 3.0.0
*
* @return {this} This Tween instance.
*/
restart: function ()
{
switch (this.state)
{
case TWEEN_CONST.REMOVED:
case TWEEN_CONST.FINISHED:
this.seek();
this.parent.makeActive(this);
break;
case TWEEN_CONST.PENDING:
case TWEEN_CONST.PENDING_REMOVE:
this.parent.reset(this);
break;
case TWEEN_CONST.DESTROYED:
console.warn('Cannot restart destroyed Tween', this);
break;
default:
this.seek();
break;
}
this.paused = false;
this.hasStarted = false;
return this;
},
/**
* Internal method that advances to the next state of the Tween during playback.
*
* @method Phaser.Tweens.Tween#nextState
* @fires Phaser.Tweens.Events#TWEEN_COMPLETE
* @fires Phaser.Tweens.Events#TWEEN_LOOP
* @since 3.0.0
*
* @return {boolean} `true` if this Tween has completed, otherwise `false`.
*/
nextState: function ()
{
if (this.loopCounter > 0)
{
this.elapsed = 0;
this.progress = 0;
this.loopCounter--;
this.initTweenData(true);
if (this.loopDelay > 0)
{
this.countdown = this.loopDelay;
this.setLoopDelayState();
}
else
{
this.setActiveState();
this.dispatchEvent(Events.TWEEN_LOOP, 'onLoop');
}
}
else if (this.completeDelay > 0)
{
this.countdown = this.completeDelay;
this.setCompleteDelayState();
}
else
{
this.onCompleteHandler();
return true;
}
return false;
},
/**
* Internal method that handles this tween completing and starting
* the next tween in the chain, if any.
*
* @method Phaser.Tweens.Tween#onCompleteHandler
* @since 3.60.0
*/
onCompleteHandler: function ()
{
this.progress = 1;
this.totalProgress = 1;
BaseTween.prototype.onCompleteHandler.call(this);
},
/**
* Starts a Tween playing.
*
* You only need to call this method if you have configured the tween to be paused on creation.
*
* If the Tween is already playing, calling this method again will have no effect. If you wish to
* restart the Tween, use `Tween.restart` instead.
*
* Calling this method after the Tween has completed will start the Tween playing again from the beginning.
* This is the same as calling `Tween.seek(0)` and then `Tween.play()`.
*
* @method Phaser.Tweens.Tween#play
* @since 3.0.0
*
* @return {this} This Tween instance.
*/
play: function ()
{
if (this.isDestroyed())
{
console.warn('Cannot play destroyed Tween', this);
return this;
}
if (this.isPendingRemove() || this.isFinished())
{
this.seek();
}
this.paused = false;
this.setActiveState();
return this;
},
/**
* Seeks to a specific point in the Tween.
*
* The given amount is a value in milliseconds that represents how far into the Tween
* you wish to seek, based on the start of the Tween.
*
* Note that the seek amount takes the entire duration of the Tween into account, including delays, loops and repeats.
* For example, a Tween that lasts for 2 seconds, but that loops 3 times, would have a total duration of 6 seconds,
* so seeking to 3000 ms would seek to the Tweens half-way point based on its _entire_ duration.
*
* Prior to Phaser 3.60 this value was given as a number between 0 and 1 and didn't
* work for Tweens had an infinite repeat. This new method works for all Tweens.
*
* Seeking works by resetting the Tween to its initial values and then iterating through the Tween at `delta`
* jumps per step. The longer the Tween, the longer this can take. If you need more precision you can
* reduce the delta value. If you need a faster seek, you can increase it. When the Tween is
* reset it will refresh the starting and ending values. If these are coming from a dynamic function,
* or a random array, it will be called for each seek.
*
* While seeking the Tween will _not_ emit any of its events or callbacks unless
* the 3rd parameter is set to `true`.
*
* If this Tween is paused, seeking will not change this fact. It will advance the Tween
* to the desired point and then pause it again.
*
* @method Phaser.Tweens.Tween#seek
* @since 3.0.0
*
* @param {number} [amount=0] - The number of milliseconds to seek into the Tween from the beginning.
* @param {number} [delta=16.6] - The size of each step when seeking through the Tween. A higher value completes faster but at the cost of less precision.
* @param {boolean} [emit=false] - While seeking, should the Tween emit any of its events or callbacks? The default is 'false', i.e. to seek silently.
*
* @return {this} This Tween instance.
*/
seek: function (amount, delta, emit)
{
if (amount === undefined) { amount = 0; }
if (delta === undefined) { delta = 16.6; }
if (emit === undefined) { emit = false; }
if (this.isDestroyed())
{
console.warn('Cannot seek destroyed Tween', this);
return this;
}
if (!emit)
{
this.isSeeking = true;
}
this.reset(true);
this.initTweenData(true);
this.setActiveState();
this.dispatchEvent(Events.TWEEN_ACTIVE, 'onActive');
var isPaused = this.paused;
this.paused = false;
if (amount > 0)
{
var iterations = Math.floor(amount / delta);
var remainder = amount - (iterations * delta);
for (var i = 0; i < iterations; i++)
{
this.update(delta);
}
if (remainder > 0)
{
this.update(remainder);
}
}
this.paused = isPaused;
this.isSeeking = false;
return this;
},
/**
* Initialises all of the Tween Data and Tween values.
*
* This is called automatically and should not typically be invoked directly.
*
* @method Phaser.Tweens.Tween#initTweenData
* @since 3.60.0
*
* @param {boolean} [isSeeking=false] - Is the Tween Data being reset as part of a seek?
*/
initTweenData: function (isSeeking)
{
if (isSeeking === undefined) { isSeeking = false; }
// These two values are updated directly during TweenData.reset:
this.duration = 0;
this.startDelay = MATH_CONST.MAX_SAFE_INTEGER;
var data = this.data;
for (var i = 0; i < this.totalData; i++)
{
data[i].reset(isSeeking);
}
// Clamp duration to ensure we never divide by zero
this.duration = Math.max(this.duration, 0.01);
var duration = this.duration;
var completeDelay = this.completeDelay;
var loopCounter = this.loopCounter;
var loopDelay = this.loopDelay;
if (loopCounter > 0)
{
this.totalDuration = duration + completeDelay + ((duration + loopDelay) * loopCounter);
}
else
{
this.totalDuration = duration + completeDelay;
}
},
/**
* Resets this Tween ready for another play-through.
*
* This is called automatically from the Tween Manager, or from the parent TweenChain,
* and should not typically be invoked directly.
*
* If you wish to restart this Tween, use the `Tween.restart` or `Tween.seek` methods instead.
*
* @method Phaser.Tweens.Tween#reset
* @fires Phaser.Tweens.Events#TWEEN_ACTIVE
* @since 3.60.0
*
* @param {boolean} [skipInit=false] - Skip resetting the TweenData and Active State?
*
* @return {this} This Tween instance.
*/
reset: function (skipInit)
{
if (skipInit === undefined) { skipInit = false; }
this.elapsed = 0;
this.totalElapsed = 0;
this.progress = 0;
this.totalProgress = 0;
this.loopCounter = this.loop;
if (this.loop === -1)
{
this.isInfinite = true;
this.loopCounter = TWEEN_CONST.MAX;
}
if (!skipInit)
{
this.initTweenData();
this.setActiveState();
this.dispatchEvent(Events.TWEEN_ACTIVE, 'onActive');
}
return this;
},
/**
* Internal method that advances the Tween based on the time values.
*
* @method Phaser.Tweens.Tween#update
* @fires Phaser.Tweens.Events#TWEEN_COMPLETE
* @fires Phaser.Tweens.Events#TWEEN_LOOP
* @fires Phaser.Tweens.Events#TWEEN_START
* @since 3.0.0
*
* @param {number} delta - The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
*
* @return {boolean} Returns `true` if this Tween has finished and should be removed from the Tween Manager, otherwise returns `false`.
*/
update: function (delta)
{
if (this.isPendingRemove() || this.isDestroyed())
{
if (this.persist)
{
this.setFinishedState();
return false;
}
return true;
}
else if (this.paused || this.isFinished())
{
return false;
}
delta *= this.timeScale * this.parent.timeScale;
if (this.isLoopDelayed())
{
this.updateLoopCountdown(delta);
return false;
}
else if (this.isCompleteDelayed())
{
this.updateCompleteDelay(delta);
return false;
}
else if (!this.hasStarted)
{
this.startDelay -= delta;
if (this.startDelay <= 0)
{
this.hasStarted = true;
this.dispatchEvent(Events.TWEEN_START, 'onStart');
// Reset the delta so we always start progress from zero
delta = 0;
}
}
var stillRunning = false;
if (this.isActive())
{
var data = this.data;
for (var i = 0; i < this.totalData; i++)
{
if (data[i].update(delta))
{
stillRunning = true;
}
}
}
this.elapsed += delta;
this.progress = Math.min(this.elapsed / this.duration, 1);
this.totalElapsed += delta;
this.totalProgress = Math.min(this.totalElapsed / this.totalDuration, 1);
// Anything still running? If not, we're done
if (!stillRunning)
{
// This calls onCompleteHandler if this tween is over
this.nextState();
}
// if nextState called onCompleteHandler then we're ready to be removed, unless we persist
var remove = this.isPendingRemove();
if (remove && this.persist)
{
this.setFinishedState();
remove = false;
}
return remove;
},
/**
* Moves this Tween forward by the given amount of milliseconds.
*
* It will only advance through the current loop of the Tween. For example, if the
* Tween is set to repeat or yoyo, it can only fast forward through a single
* section of the sequence. Use `Tween.seek` for more complex playhead control.
*
* If the Tween is paused or has already finished, calling this will have no effect.
*
* @method Phaser.Tweens.Tween#forward
* @since 3.60.0
*
* @param {number} ms - The number of milliseconds to advance this Tween by.
*
* @return {this} This Tween instance.
*/
forward: function (ms)
{
this.update(ms);
return this;
},
/**
* Moves this Tween backward by the given amount of milliseconds.
*
* It will only rewind through the current loop of the Tween. For example, if the
* Tween is set to repeat or yoyo, it can only fast forward through a single
* section of the sequence. Use `Tween.seek` for more complex playhead control.
*
* If the Tween is paused or has already finished, calling this will have no effect.
*
* @method Phaser.Tweens.Tween#rewind
* @since 3.60.0
*
* @param {number} ms - The number of milliseconds to rewind this Tween by.
*
* @return {this} This Tween instance.
*/
rewind: function (ms)
{
this.update(-ms);
return this;
},
/**
* Internal method that will emit a Tween based Event and invoke the given callback.
*
* @method Phaser.Tweens.Tween#dispatchEvent
* @since 3.60.0
*
* @param {Phaser.Types.Tweens.Event} event - The Event to be dispatched.
* @param {Phaser.Types.Tweens.TweenCallbackTypes} [callback] - The name of the callback to be invoked. Can be `null` or `undefined` to skip invocation.
*/
dispatchEvent: function (event, callback)
{
if (!this.isSeeking)
{
this.emit(event, this, this.targets);
if (!this.callbacks)
{
return;
}
var handler = this.callbacks[callback];
if (handler)
{
handler.func.apply(this.callbackScope, [ this, this.targets ].concat(handler.params));
}
}
},
/**
* Handles the destroy process of this Tween, clearing out the
* Tween Data and resetting the targets. A Tween that has been
* destroyed cannot ever be played or used again.
*
* @method Phaser.Tweens.Tween#destroy
* @since 3.60.0
*/
destroy: function ()
{
BaseTween.prototype.destroy.call(this);
this.targets = null;
}
});
/**
* Creates a new Tween object.
*
* Note: This method will only be available if Tweens have been built into Phaser.
*
* @method Phaser.GameObjects.GameObjectFactory#tween
* @since 3.0.0
*
* @param {Phaser.Types.Tweens.TweenBuilderConfig|Phaser.Types.Tweens.TweenChainBuilderConfig|Phaser.Tweens.Tween|Phaser.Tweens.TweenChain} config - A Tween Configuration object, or a Tween or TweenChain instance.
*
* @return {Phaser.Tweens.Tween} The Tween that was created.
*/
GameObjectFactory.register('tween', function (config)
{
return this.scene.sys.tweens.add(config);
});
/**
* Creates a new Tween object and returns it.
*
* Note: This method will only be available if Tweens have been built into Phaser.
*
* @method Phaser.GameObjects.GameObjectCreator#tween
* @since 3.0.0
*
* @param {Phaser.Types.Tweens.TweenBuilderConfig|Phaser.Types.Tweens.TweenChainBuilderConfig|Phaser.Tweens.Tween|Phaser.Tweens.TweenChain} config - A Tween Configuration object, or a Tween or TweenChain instance.
*
* @return {Phaser.Tweens.Tween} The Tween that was created.
*/
GameObjectCreator.register('tween', function (config)
{
return this.scene.sys.tweens.create(config);
});
module.exports = Tween;