UNPKG

superpowers-game-ftween-plugin

Version:

Easy tweening (animation of values) for the supGame system for Superpowers, the extensible HTML5 2D+3D game engine.

1,114 lines (700 loc) 25.4 kB
/** * ftween.js - Licensed under the MIT license * https://github.com/florentpoujol/ftween.js * ---------------------------------------------- * * A fork of Soledad Penadés' tween.js library : https://github.com/tweenjs/tween.js * Thank you all, you're awesome! */ // Date.now shim for (ahem) Internet Explo(d|r)er if ( Date.now === undefined ) { Date.now = function () { return new Date().valueOf(); }; } var FTWEEN = FTWEEN || ( function () { var _tweens = []; return { getAll: function () { return _tweens; }, removeAll: function () { _tweens = []; }, add: function ( tween ) { _tweens.push( tween ); }, remove: function ( tween ) { var i = _tweens.indexOf( tween ); if ( i !== -1 ) { _tweens.splice( i, 1 ); } }, update: function ( time ) { if ( _tweens.length === 0 ) return false; var i = 0; time = time !== undefined ? time : ( typeof window !== 'undefined' && window.performance !== undefined && window.performance.now !== undefined ? window.performance.now() : Date.now() ); while ( i < _tweens.length ) { if ( _tweens[ i ].update( time ) ) { i++; } else { _tweens.splice( i, 1 ); } } return true; } }; } )(); FTWEEN.Tween = function ( object ) { var _object = object || {}; var _valuesStart = {}; var _valuesEnd = {}; var _duration = 1000; var _isRelative = false; var _repeat = 0; var _yoyo = false; var _isPlaying = false; var _delayTime = 0; var _startTime = null; var _isPaused = false; var _pauseDuration = 0; var _pauseStartTime = 0; var _easingFunction = FTWEEN.Easing.Linear.None; var _interpolationFunction = FTWEEN.Interpolation.Linear; var _chainedTweens = []; var _onStartCallback = null; var _onStartCallbackFired = false; var _onPauseCallback = null; var _onResumeCallback = null; var _onUpdateCallback = null; var _onLoopCompleteCallback = null; var _onCompleteCallback = null; var _onStopCallback = null; this.test = false; // Keys are property name, values are object with getter and setter properties. // Filled in _setupDynamicProperty(). // Reset in start(). var _accessorsByProperties = {}; /** * Check if the provided property is "dynamic" on _object, that is if _object[property] === undefined but the property name match the name of a couple of getter/setter : get[Property]()/set[Property](). * Called from start(). * @param {string} property - The property name. */ var _setupDynamicProperty = function( property ) { if ( _object[ property ] === undefined ) { var ucProperty = property.charAt(0).toUpperCase() + property.slice(1); var getter = _object[ "get"+ucProperty ]; var setter = _object[ "set"+ucProperty ]; if ( typeof getter === "function" && typeof setter === "function" ) { _accessorsByProperties[ property ] = { getter: getter, setter: setter }; } } }; /** * Get the provided property's value on the _object. * Called from start() and update(). * @param {string} property - The property name. * @return {any} */ var _getObjectValue = function( property ) { if (this.test) console.log("getobjectvalue", property, _accessorsByProperties[ property ]); if ( _accessorsByProperties[ property ] !== undefined ) { return _accessorsByProperties[ property ].getter.call( _object ); } return _object[ property ]; }; /** * Get the provided property's value on the _object. * Called from start() and update(). * @param {string} property - The property name. * @param value {any} */ var _setObjectValue = function( property, value ) { if (this.test) console.log("setobjectvalue", property, value, _accessorsByProperties[ property ]); if ( _accessorsByProperties[ property ] !== undefined ) { if (this.test) console.log("set", property, value); _accessorsByProperties[ property ].setter.call( _object, value ); } else _object[ property ] = value; }; this.from = function ( object ) { _object = object; return this; }; this.to = function ( object, duration ) { if ( duration !== undefined ) { _duration = duration; } _valuesEnd = object; return this; }; this.duration = function ( duration ) { _duration = duration; return this; }; this.isRelative = function ( isRelative ) { _isRelative = isRelative; return this; }; this.start = function ( time ) { if ( ! ( this in FTWEEN.getAll() ) ) { FTWEEN.add( this ); } _isPlaying = true; _onStartCallbackFired = false; _startTime = time !== undefined ? time : ( typeof window !== 'undefined' && window.performance !== undefined && window.performance.now !== undefined ? window.performance.now() : Date.now() ); _startTime += _delayTime; _pauseDuration = 0; if (this.test) console.log("start tweener",_valuesStart, _object, _valuesEnd); var endValue, objectValue, property, endValueType, nEndValue, nObjectValue, nProperty; // n for "nested" var allowedTypes = [ "number", "string", "object" ]; // type object is for objects, arrays and null _accessorsByProperties = {}; // loop on _valuesEnd to fill _valuesStart // except for object values in _valuesEnd that are undefined in _object, _object doesn't need to be filled or fixed because it will be updated from update(). for ( property in _valuesEnd ) { endValue = _valuesEnd[ property ]; endValueType = typeof endValue; // discard null and not allowed types if ( endValue === null || allowedTypes.indexOf( endValueType ) === -1 ) { continue; } _setupDynamicProperty( property ); // check if an Array was provided as property value if ( Array.isArray( endValueType ) ) { if ( endValue.length === 0 ) { continue; } // create a local copy of the Array with the start value at the front _valuesEnd[ property ] = [ _getObjectValue( property ) || 0 ].concat( endValue ); _valuesStart[ property ] = _getObjectValue( property ) || 0; } else if ( endValueType === "object" ) { // never null or array at this point // create object in _valuesStart and _object if one don't exist yet if ( _valuesStart[ property ] === undefined ) { _valuesStart[ property ] = {}; } objectValue = _getObjectValue( property ); if ( objectValue === undefined ) { objectValue = {}; _setObjectValue( property, objectValue ); // could directly do _object[property] = {} as this case is unlikely to ever happens with dynamic properties ? } for ( nProperty in endValue ) { // endValue is the object, nested in _valuesEnd, that contains the values to tween nEndValue = endValue[ nProperty ]; nObjectValue = objectValue[ nProperty ] || 0; if ( Array.isArray( nEndValue ) ) { _valuesEnd[ property ][ nProperty ] = [ nObjectValue ].concat( nEndValue ); _valuesStart[ property ][ nProperty ] = nObjectValue; } else { // string or number _valuesStart[ property ][ nProperty ] = parseFloat( nObjectValue ) || 0; } } } else { _valuesStart[ property ] = parseFloat( _getObjectValue( property ) ) || 0; } } if (this.test) console.log("start tweener end",_valuesStart, _object, _valuesEnd); return this; }; this.pause = function () { if ( _isPaused ) { return; } _isPaused = true; if ( _onPauseCallback !== null ) { _onPauseCallback.call( _object ); } return this; }; this.isPaused = function () { return _isPaused; }; this.resume = function () { if ( !_isPaused ) { return; } _isPaused = false; if ( _onResumeCallback !== null ) { _onResumeCallback.call( _object ); } return this; }; this.stop = function () { if ( !_isPlaying ) { return this; } FTWEEN.remove( this ); _isPlaying = false; if ( _onStopCallback !== null ) { _onStopCallback.call( _object ); } this.stopChainedTweens(); return this; }; /** * Free as much stuff as possible for garbage collection. * @param {boolean=false} recurse If the tween has one or more chained tweens, tell whether to recursively destro them all (true) or just leave them be (false). */ this.destroy = function ( recurse ) { if ( _isPlaying === true ) { FTWEEN.remove( this ); _isPlaying = false; } this.stopChainedTweens(); if ( recurse === true ) { for ( var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++ ) { _chainedTweens[ i ].destroy( true ); } } _object = null; _valuesStart = null; _valuesEnd = null; _chainedTweens = []; // prevent an error when destroy() is called from onComplete callback, _chainedTweens is used after the callback has been called in update(). _onStartCallback = null; _onPauseCallback = null; _onResumeCallback = null; _onUpdateCallback = null; _onLoopCompleteCallback = null; _onCompleteCallback = null; _onStopCallback = null; }; this.stopChainedTweens = function () { for ( var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++ ) { _chainedTweens[ i ].stop(); } }; /** * Return all chained tweens * @returns {Array} */ this.getChainedTweens = function () { return _chainedTweens; }; /** * Remove one or several chained tweens. * @param {FTWEEN.Tween} [tween] The tween to remove. If null or undefined, all chained tweens will be removed. * @returns {boolean} True if at least one tween has been removed, false otherwise. */ this.removeChainedTweens = function ( tween ) { if ( tween !== null || tween !== undefined ) { var index = _chainedTweens.indexOf( tween ); if ( index !== -1 ) { _chainedTweens.splice( index, 1 ); return true; } return false; } var count = _chainedTweens.length; _chainedTweens = []; return ( count > 0 ); }; this.delay = function ( amount ) { _delayTime = amount; return this; }; this.repeat = function ( times ) { _repeat = times; return this; }; this.yoyo = function( yoyo ) { _yoyo = yoyo; return this; }; this.easing = function ( easing ) { _easingFunction = easing; return this; }; this.interpolation = function ( interpolation ) { _interpolationFunction = interpolation; return this; }; this.chain = function () { _chainedTweens = arguments; return this; }; this.onStart = function ( callback ) { _onStartCallback = callback; return this; }; this.onUpdate = function ( callback ) { _onUpdateCallback = callback; return this; }; this.onPause = function ( callback ) { _onPauseCallback = callback; return this; }; this.onResume = function ( callback ) { _onResumeCallback = callback; return this; }; this.onLoopComplete = function ( callback ) { _onLoopCompleteCallback = callback; return this; }; this.onComplete = function ( callback ) { _onCompleteCallback = callback; return this; }; this.onStop = function ( callback ) { _onStopCallback = callback; return this; }; // returns true when the tween must be kept in the list of tweens to update this.update = function ( time ) { if (this.test) console.log("update tweener", time); if ( _isPaused === true ) { if ( _pauseStartTime === 0 ) { // first update after tween is paused _pauseStartTime = time; } return true; } else if ( _pauseStartTime > 0 ) { // first update after tween is resumed _pauseDuration += ( time - _pauseStartTime ); _pauseStartTime = 0; return true; } if ( time < _startTime ) { return true; } if ( _onStartCallbackFired === false ) { if ( _onStartCallback !== null ) { _onStartCallback.call( _object ); } _onStartCallbackFired = true; } var elapsed = ( time - ( _startTime + _pauseDuration ) ) / _duration; elapsed = elapsed > 1 ? 1 : elapsed; var progression = _easingFunction( elapsed ); // % between 0 and 1 /** * @param {number} start - The value from _valuesStart. * @param {number|string} end - The value from _valuesEnd. * @param {string} endValueType - The type of 'end' param. */ var getCurrentValue = function( start, end, endValueType ) { if ( _isRelative === true || endValueType === "string" ) { // Parses relative end values with start as base (e.g.: +10, -3) end = start + parseFloat( end, 10 ); } return start + ( end - start ) * progression; }; var property, start, end, valueType, nProperty, nStart, nEnd, nObject; if (this.test) console.log("update tweener", _valuesStart, _object, _valuesEnd, progression, elapsed); for ( property in _valuesEnd ) { start = _valuesStart[ property ]; end = _valuesEnd[ property ]; valueType = typeof end; if (this.test) console.log("update property", property, end, _object); if ( Array.isArray( end ) ) { _object[ property ] = _interpolationFunction( end, progression ); } else if ( end !== null && valueType === "object") { nObject = _getObjectValue( property ); // Keeping a reference to the nObject may be useful when getting/setting the property actually calls a getter/setter. // if the reference is meaningfull, updating it will actually update the data but, // typically, the getter returns a new object instance every time (like a Vector3 for get/setPosition()) // so updating the reference (nObject) doesn't actually update the data in the object // unless the setter is called with the new value (as it is done below) for ( nProperty in end ) { nStart = start[ nProperty ]; // console.log("end", end, nProperty, end[ nProperty ]); nEnd = end[ nProperty ]; if ( Array.isArray( nEnd ) ) { nObject[ nProperty ] = _interpolationFunction( nEnd, progression ); } else { nObject[ nProperty ] = getCurrentValue( nStart, nEnd, typeof nEnd ); } if (this.test) console.log( "object value", property, nProperty, nStart, _valuesStart, nEnd, _valuesEnd); } if (this.test) console.log("set object value1", property, nObject, _object); _setObjectValue( property, nObject ); } else if ( valueType === "number" || valueType === "string" ) { _setObjectValue( property, getCurrentValue( start, end, valueType ) ); if (this.test) console.log( "num value", property, start, end, progression, getCurrentValue( start, end )); } } if (this.test) console.log("update tweener 2", _object); if ( _onUpdateCallback !== null ) { _onUpdateCallback.call( _object, progression ); } if ( elapsed == 1 ) { if ( _repeat > 0 ) { if( isFinite( _repeat ) ) { _repeat--; } if ( _onLoopCompleteCallback !== null ) { _onLoopCompleteCallback.call( _object, _repeat + 1 ); } if (this.test) console.log("before repeat", _valuesStart, _object, _valuesEnd, _isRelative); // reassign starting values, restart by making startTime = now // console.log( "repeat", _valuesStart, _object, _valuesEnd ); for( property in _valuesStart ) { // set startValue = currentValue if endValue (or whole tween) is relative endValue = _valuesEnd[ property ]; endValueType = typeof endValue; currentValue = _getObjectValue( property ); if ( _yoyo ) { _valuesEnd[ property ] = _valuesStart[ property ]; _valuesStart[ property ] = currentValue; // if endValue/currentValue is an object, copy it so that _valuesStart doesn't keep a reference to currentValue which would be updated in update() if ( endValue !== null && Array.isArray( endValue ) === false && endValueType === "object" ) { _valuesStart[ property ] = {}; for ( nProperty in endValue ) { _valuesStart[ property ][ nProperty ] = currentValue[ nProperty ]; } } if (this.test) console.log("property after yoyo", property, _valuesStart[ property ], _object[ property ], _valuesEnd[ property ]); } else { // not yoyo // do nothing unless the tween or some of its values are relative // in this case, assign currentValue as the new start value if ( ( _isRelative === true && endValueType === "number" ) || endValueType === "string" ) { _valuesStart[ property ] = currentValue; // if (this.test) console.log("update start value with current value", property, currentValue); } else if ( endValue !== null && Array.isArray( endValue ) === false && endValueType === "object" ) { for ( nProperty in endValue ) { var nEndValueType = typeof endValue[ nProperty ]; if ( ( _isRelative === true && nEndValueType === "number" ) || nEndValueType === "string" ) { _valuesStart[ property ][ nProperty ] = currentValue[ nProperty ]; } } } } // what about arrays ? if (this.test) console.log("property", property, _valuesStart[ property ], _object[ property ], _valuesEnd[ property ]); } if (this.test) console.log("after repeat", _valuesStart, _object, _valuesEnd); if ( _yoyo ) { _isRelative = false; } _startTime = time + _delayTime; _pauseDuration = 0; return true; } else { if ( _onCompleteCallback !== null ) { _onCompleteCallback.call( _object ); } for ( var i = 0, numChainedTweens = _chainedTweens.length; i < numChainedTweens; i++ ) { _chainedTweens[ i ].start( time ); } return false; } } return true; }; }; FTWEEN.Easing = { Linear: { None: function ( k ) { return k; } }, Quadratic: { In: function ( k ) { return k * k; }, Out: function ( k ) { return k * ( 2 - k ); }, InOut: function ( k ) { if ( ( k *= 2 ) < 1 ) return 0.5 * k * k; return - 0.5 * ( --k * ( k - 2 ) - 1 ); } }, Cubic: { In: function ( k ) { return k * k * k; }, Out: function ( k ) { return --k * k * k + 1; }, InOut: function ( k ) { if ( ( k *= 2 ) < 1 ) return 0.5 * k * k * k; return 0.5 * ( ( k -= 2 ) * k * k + 2 ); } }, Quartic: { In: function ( k ) { return k * k * k * k; }, Out: function ( k ) { return 1 - ( --k * k * k * k ); }, InOut: function ( k ) { if ( ( k *= 2 ) < 1) return 0.5 * k * k * k * k; return - 0.5 * ( ( k -= 2 ) * k * k * k - 2 ); } }, Quintic: { In: function ( k ) { return k * k * k * k * k; }, Out: function ( k ) { return --k * k * k * k * k + 1; }, InOut: function ( k ) { if ( ( k *= 2 ) < 1 ) return 0.5 * k * k * k * k * k; return 0.5 * ( ( k -= 2 ) * k * k * k * k + 2 ); } }, Sinusoidal: { In: function ( k ) { return 1 - Math.cos( k * Math.PI / 2 ); }, Out: function ( k ) { return Math.sin( k * Math.PI / 2 ); }, InOut: function ( k ) { return 0.5 * ( 1 - Math.cos( Math.PI * k ) ); } }, Exponential: { In: function ( k ) { return k === 0 ? 0 : Math.pow( 1024, k - 1 ); }, Out: function ( k ) { return k === 1 ? 1 : 1 - Math.pow( 2, - 10 * k ); }, InOut: function ( k ) { if ( k === 0 ) return 0; if ( k === 1 ) return 1; if ( ( k *= 2 ) < 1 ) return 0.5 * Math.pow( 1024, k - 1 ); return 0.5 * ( - Math.pow( 2, - 10 * ( k - 1 ) ) + 2 ); } }, Circular: { In: function ( k ) { return 1 - Math.sqrt( 1 - k * k ); }, Out: function ( k ) { return Math.sqrt( 1 - ( --k * k ) ); }, InOut: function ( k ) { if ( ( k *= 2 ) < 1) return - 0.5 * ( Math.sqrt( 1 - k * k) - 1); return 0.5 * ( Math.sqrt( 1 - ( k -= 2) * k) + 1); } }, Elastic: { In: function ( k ) { var s, a = 0.1, p = 0.4; if ( k === 0 ) return 0; if ( k === 1 ) return 1; if ( !a || a < 1 ) { a = 1; s = p / 4; } else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); return - ( a * Math.pow( 2, 10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) ); }, Out: function ( k ) { var s, a = 0.1, p = 0.4; if ( k === 0 ) return 0; if ( k === 1 ) return 1; if ( !a || a < 1 ) { a = 1; s = p / 4; } else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); return ( a * Math.pow( 2, - 10 * k) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) + 1 ); }, InOut: function ( k ) { var s, a = 0.1, p = 0.4; if ( k === 0 ) return 0; if ( k === 1 ) return 1; if ( !a || a < 1 ) { a = 1; s = p / 4; } else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI ); if ( ( k *= 2 ) < 1 ) return - 0.5 * ( a * Math.pow( 2, 10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) ); return a * Math.pow( 2, -10 * ( k -= 1 ) ) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) * 0.5 + 1; } }, Back: { In: function ( k ) { var s = 1.70158; return k * k * ( ( s + 1 ) * k - s ); }, Out: function ( k ) { var s = 1.70158; return --k * k * ( ( s + 1 ) * k + s ) + 1; }, InOut: function ( k ) { var s = 1.70158 * 1.525; if ( ( k *= 2 ) < 1 ) return 0.5 * ( k * k * ( ( s + 1 ) * k - s ) ); return 0.5 * ( ( k -= 2 ) * k * ( ( s + 1 ) * k + s ) + 2 ); } }, Bounce: { In: function ( k ) { return 1 - FTWEEN.Easing.Bounce.Out( 1 - k ); }, Out: function ( k ) { if ( k < ( 1 / 2.75 ) ) { return 7.5625 * k * k; } else if ( k < ( 2 / 2.75 ) ) { return 7.5625 * ( k -= ( 1.5 / 2.75 ) ) * k + 0.75; } else if ( k < ( 2.5 / 2.75 ) ) { return 7.5625 * ( k -= ( 2.25 / 2.75 ) ) * k + 0.9375; } else { return 7.5625 * ( k -= ( 2.625 / 2.75 ) ) * k + 0.984375; } }, InOut: function ( k ) { if ( k < 0.5 ) return FTWEEN.Easing.Bounce.In( k * 2 ) * 0.5; return FTWEEN.Easing.Bounce.Out( k * 2 - 1 ) * 0.5 + 0.5; } } }; FTWEEN.Interpolation = { Linear: function ( v, k ) { var m = v.length - 1, f = m * k, i = Math.floor( f ), fn = FTWEEN.Interpolation.Utils.Linear; if ( k < 0 ) return fn( v[ 0 ], v[ 1 ], f ); if ( k > 1 ) return fn( v[ m ], v[ m - 1 ], m - f ); return fn( v[ i ], v[ i + 1 > m ? m : i + 1 ], f - i ); }, Bezier: function ( v, k ) { var b = 0, n = v.length - 1, pw = Math.pow, bn = FTWEEN.Interpolation.Utils.Bernstein, i; for ( i = 0; i <= n; i++ ) { b += pw( 1 - k, n - i ) * pw( k, i ) * v[ i ] * bn( n, i ); } return b; }, CatmullRom: function ( v, k ) { var m = v.length - 1, f = m * k, i = Math.floor( f ), fn = FTWEEN.Interpolation.Utils.CatmullRom; if ( v[ 0 ] === v[ m ] ) { if ( k < 0 ) i = Math.floor( f = m * ( 1 + k ) ); return fn( v[ ( i - 1 + m ) % m ], v[ i ], v[ ( i + 1 ) % m ], v[ ( i + 2 ) % m ], f - i ); } else { if ( k < 0 ) return v[ 0 ] - ( fn( v[ 0 ], v[ 0 ], v[ 1 ], v[ 1 ], -f ) - v[ 0 ] ); if ( k > 1 ) return v[ m ] - ( fn( v[ m ], v[ m ], v[ m - 1 ], v[ m - 1 ], f - m ) - v[ m ] ); return fn( v[ i ? i - 1 : 0 ], v[ i ], v[ m < i + 1 ? m : i + 1 ], v[ m < i + 2 ? m : i + 2 ], f - i ); } }, Utils: { Linear: function ( p0, p1, t ) { return ( p1 - p0 ) * t + p0; }, Bernstein: function ( n , i ) { var fc = FTWEEN.Interpolation.Utils.Factorial; return fc( n ) / fc( i ) / fc( n - i ); }, Factorial: ( function () { var a = [ 1 ]; return function ( n ) { var s = 1, i; if ( a[ n ] ) return a[ n ]; for ( i = n; i > 1; i-- ) s *= i; a[ n ] = s; return s; // return a[ n ] = s; }; } )(), CatmullRom: function ( p0, p1, p2, p3, t ) { var v0 = ( p2 - p0 ) * 0.5, v1 = ( p3 - p1 ) * 0.5, t2 = t * t, t3 = t * t2; return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 + ( - 3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 + v0 * t + p1; } } }; if(typeof module !== 'undefined' && module.exports) { module.exports = FTWEEN; }