UNPKG

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
/** * @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;